Skip to main content

allwright_plugin_sdk/
lib.rs

1pub mod accessibility_yaml;
2use serde::{Deserialize, Serialize};
3
4pub const ALLWRIGHT_PLUGIN_API_VERSION: u32 = 1;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SurfaceFamily {
8    Web,
9    Mobile,
10    Desktop,
11}
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum BrowserKind {
16    Chromium,
17    Firefox,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct SurfacePluginDescriptor {
22    pub id: &'static str,
23    pub family: SurfaceFamily,
24    pub version: &'static str,
25    pub description: &'static str,
26}
27
28pub trait SurfacePlugin: Send + Sync {
29    fn descriptor(&self) -> SurfacePluginDescriptor;
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct ChromeLaunchInfo {
34    pub browser: String,
35    pub note: String,
36    pub cdp_websocket_url: String,
37    pub user_data_dir: String,
38    pub process_id: u32,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct BrowserLaunchInfo {
43    pub browser_kind: BrowserKind,
44    pub browser: String,
45    pub note: String,
46    pub user_data_dir: String,
47    pub process_id: u32,
48    pub browser_session: BrowserSessionHandle,
49    pub initial_page: PageInfo,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct ChromeTabInfo {
54    pub note: String,
55    pub target_id: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59pub struct TabNavigationInfo {
60    pub url: String,
61    pub note: String,
62    pub page_session: PageSessionHandle,
63    pub automation: AutomationSessionInfo,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct ChromiumBidiMapperInfo {
68    pub package_version: String,
69    pub mapper_target_id: String,
70    pub mapper_session_id: String,
71    pub note: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(tag = "kind", rename_all = "snake_case")]
76pub enum BrowserSessionHandle {
77    Chromium {
78        cdp_websocket_url: String,
79    },
80    Firefox {
81        connection_id: String,
82        bidi_session_id: String,
83    },
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(tag = "kind", rename_all = "snake_case")]
88pub enum PageSessionHandle {
89    Chromium {
90        target_id: String,
91        browsing_context_id: Option<String>,
92        #[serde(default)]
93        mapper_target_id: Option<String>,
94    },
95    Firefox {
96        browsing_context_id: String,
97    },
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101pub struct PageInfo {
102    pub note: String,
103    pub page_session: PageSessionHandle,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub enum HookType {
108    Dialog,
109    NewPage,
110    FileChooser,
111    Download,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115pub struct HookRegistration {
116    pub opaque_state: String,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120#[serde(tag = "hook_type", content = "value", rename_all = "snake_case")]
121pub enum HookResult {
122    Dialog(DialogInfo),
123    NewPage(PageInfo),
124    FileChooser(FileChooserInfo),
125    Download(DownloadInfo),
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129pub struct DialogInfo {
130    pub dialog_id: String,
131    pub kind: String,
132    pub message: String,
133    pub default_value: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137pub struct FileChooserInfo {
138    pub file_chooser_id: String,
139    pub is_multiple: bool,
140    pub note: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct FileChooserFilesSetInfo {
145    pub file_chooser_id: String,
146    pub files: Vec<String>,
147    pub note: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
151pub struct DownloadInfo {
152    pub download_id: String,
153    pub url: String,
154    pub suggested_filename: String,
155    pub note: String,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub struct DownloadSavedInfo {
160    pub download_id: String,
161    pub path: String,
162    pub suggested_filename: String,
163    pub size: u64,
164    pub note: String,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168pub struct AutomationSessionInfo {
169    pub bidi_session_id: String,
170    pub note: String,
171    pub mapper_target_id: Option<String>,
172    pub mapper_session_id: Option<String>,
173    pub package_version: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
177pub struct ClickInfo {
178    pub css_selector: String,
179    pub note: String,
180    pub bidi_session_id: String,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184pub struct ElementCountInfo {
185    pub css_selector: String,
186    pub count: u32,
187    pub note: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
191pub struct HighlightElementsInfo {
192    pub css_selector: String,
193    pub count: u32,
194    pub note: String,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198pub struct FocusInfo {
199    pub css_selector: String,
200    pub note: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204pub struct FillInfo {
205    pub css_selector: String,
206    pub value: String,
207    pub note: String,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
211pub struct HoverInfo {
212    pub css_selector: String,
213    pub note: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub struct PressKeyInfo {
218    pub css_selector: String,
219    pub key: String,
220    pub note: String,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
224pub struct TextInfo {
225    pub css_selector: String,
226    pub text: String,
227    pub note: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
231pub struct WaitForSelectorInfo {
232    pub css_selector: String,
233    pub visible: bool,
234    pub note: String,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238pub struct AccessibilitySnapshotInfo {
239    pub snapshot: String,
240    pub format: String,
241}
242
243#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
244#[serde(rename_all = "camelCase")]
245pub struct InspectorBounds {
246    pub x: f64,
247    pub y: f64,
248    pub width: f64,
249    pub height: f64,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
253#[serde(rename_all = "camelCase")]
254pub struct InspectorNode {
255    pub role: String,
256    pub name: String,
257    pub automation_id: String,
258    pub class_name: String,
259    pub enabled: bool,
260    pub offscreen: bool,
261    pub bounds: InspectorBounds,
262    #[serde(default)]
263    pub properties: serde_json::Value,
264    #[serde(default)]
265    pub states: serde_json::Value,
266    pub children: Vec<InspectorNode>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
270pub struct InspectorSnapshot {
271    pub version: u32,
272    pub platform: String,
273    pub backend: String,
274    pub root: InspectorNode,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278pub struct ScreenshotInfo {
279    pub png_data: Vec<u8>,
280    pub note: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284#[serde(tag = "command", rename_all = "snake_case")]
285pub enum PluginCommand {
286    LaunchBrowser {
287        browser_kind: BrowserKind,
288        browser_binary: Option<String>,
289    },
290    ResolveFrame {
291        browser_session: BrowserSessionHandle,
292        page_session: PageSessionHandle,
293        css_selector: String,
294        timeout_ms: u64,
295    },
296    OpenPage {
297        browser_session: BrowserSessionHandle,
298    },
299    RegisterHook {
300        browser_session: BrowserSessionHandle,
301        page_session: PageSessionHandle,
302        hook_type: HookType,
303    },
304    PollHook {
305        browser_session: BrowserSessionHandle,
306        registration: HookRegistration,
307    },
308    HandleDialog {
309        browser_session: BrowserSessionHandle,
310        page_session: PageSessionHandle,
311        dialog_id: String,
312        accept: bool,
313        prompt_text: Option<String>,
314    },
315    SetFileChooserFiles {
316        browser_session: BrowserSessionHandle,
317        page_session: PageSessionHandle,
318        file_chooser_id: String,
319        files: Vec<String>,
320    },
321    SaveDownload {
322        browser_session: BrowserSessionHandle,
323        page_session: PageSessionHandle,
324        download_id: String,
325        path: String,
326    },
327    ClosePage {
328        browser_session: BrowserSessionHandle,
329        page_session: PageSessionHandle,
330    },
331    NavigatePage {
332        browser_session: BrowserSessionHandle,
333        page_session: PageSessionHandle,
334        url: String,
335    },
336    ClickElement {
337        browser_session: BrowserSessionHandle,
338        page_session: PageSessionHandle,
339        css_selector: String,
340        button: u8,
341        click_count: u32,
342    },
343    CountElements {
344        browser_session: BrowserSessionHandle,
345        page_session: PageSessionHandle,
346        css_selector: String,
347    },
348    HighlightElements {
349        browser_session: BrowserSessionHandle,
350        page_session: PageSessionHandle,
351        css_selector: String,
352        duration_ms: u64,
353    },
354    FocusElement {
355        browser_session: BrowserSessionHandle,
356        page_session: PageSessionHandle,
357        css_selector: String,
358    },
359    FillElement {
360        browser_session: BrowserSessionHandle,
361        page_session: PageSessionHandle,
362        css_selector: String,
363        value: String,
364    },
365    HoverElement {
366        browser_session: BrowserSessionHandle,
367        page_session: PageSessionHandle,
368        css_selector: String,
369    },
370    PressKey {
371        browser_session: BrowserSessionHandle,
372        page_session: PageSessionHandle,
373        css_selector: String,
374        key: String,
375        text: Option<String>,
376    },
377    Capture {
378        browser_session: BrowserSessionHandle,
379        page_session: PageSessionHandle,
380        kind: String,
381        css_selector: String,
382        attribute_name: String,
383    },
384    GetTextContent {
385        browser_session: BrowserSessionHandle,
386        page_session: PageSessionHandle,
387        css_selector: String,
388    },
389    GetInnerText {
390        browser_session: BrowserSessionHandle,
391        page_session: PageSessionHandle,
392        css_selector: String,
393    },
394    WaitForSelector {
395        browser_session: BrowserSessionHandle,
396        page_session: PageSessionHandle,
397        css_selector: String,
398        visible: bool,
399    },
400    AccessibilitySnapshot {
401        browser_session: BrowserSessionHandle,
402        page_session: PageSessionHandle,
403        format: String,
404        #[serde(default)]
405        mode: String,
406    },
407    Screenshot {
408        browser_session: BrowserSessionHandle,
409        page_session: PageSessionHandle,
410        full_page: bool,
411    },
412    OpenChromeWindow {
413        chrome_binary: Option<String>,
414    },
415    DiscoverInitialTab {
416        cdp_websocket_url: String,
417    },
418    OpenChromeTab {
419        cdp_websocket_url: String,
420    },
421    CloseBrowserProcess {
422        process_id: u32,
423    },
424    CloseChromeTab {
425        cdp_websocket_url: String,
426        target_id: String,
427    },
428    NavigateChromeTab {
429        cdp_websocket_url: String,
430        target_id: String,
431        url: String,
432    },
433    InjectChromiumBidiMapper {
434        cdp_websocket_url: String,
435    },
436    ResolveBidiContextForTab {
437        cdp_websocket_url: String,
438        mapper_target_id: Option<String>,
439        browsing_context_id: Option<String>,
440        url: Option<String>,
441    },
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
445#[serde(tag = "result", rename_all = "snake_case")]
446pub enum PluginResult {
447    LaunchBrowser(BrowserLaunchInfo),
448    ResolveFrame(PageInfo),
449    OpenPage(PageInfo),
450    RegisterHook(HookRegistration),
451    PollHook(HookResult),
452    HandleDialog,
453    SetFileChooserFiles(FileChooserFilesSetInfo),
454    SaveDownload(DownloadSavedInfo),
455    ClosePage,
456    NavigatePage(TabNavigationInfo),
457    ClickElement(ClickInfo),
458    CountElements(ElementCountInfo),
459    HighlightElements(HighlightElementsInfo),
460    FocusElement(FocusInfo),
461    FillElement(FillInfo),
462    HoverElement(HoverInfo),
463    PressKey(PressKeyInfo),
464    Capture(CaptureInfo),
465    GetTextContent(TextInfo),
466    GetInnerText(TextInfo),
467    WaitForSelector(WaitForSelectorInfo),
468    AccessibilitySnapshot(AccessibilitySnapshotInfo),
469    Screenshot(ScreenshotInfo),
470    OpenChromeWindow(ChromeLaunchInfo),
471    DiscoverInitialTab(ChromeTabInfo),
472    OpenChromeTab(ChromeTabInfo),
473    CloseBrowserProcess,
474    CloseChromeTab,
475    NavigateChromeTab(TabNavigationInfo),
476    InjectChromiumBidiMapper(ChromiumBidiMapperInfo),
477    ResolveBidiContextForTab {
478        browsing_context_id: String,
479        mapper: ChromiumBidiMapperInfo,
480    },
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
484pub struct PluginEnvelope {
485    pub ok: bool,
486    pub result: Option<PluginResult>,
487    pub error: Option<String>,
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn poll_hook_envelope_round_trips_without_duplicate_result_fields() {
496        let envelope = PluginEnvelope {
497            ok: true,
498            result: Some(PluginResult::PollHook(HookResult::NewPage(PageInfo {
499                note: "opened".to_string(),
500                page_session: PageSessionHandle::Chromium {
501                    target_id: "target-2".to_string(),
502                    browsing_context_id: None,
503                    mapper_target_id: None,
504                },
505            }))),
506            error: None,
507        };
508
509        let json = serde_json::to_string(&envelope).unwrap();
510        assert_eq!(
511            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
512            envelope
513        );
514    }
515
516    #[test]
517    fn dialog_hook_envelope_round_trips() {
518        let envelope = PluginEnvelope {
519            ok: true,
520            result: Some(PluginResult::PollHook(HookResult::Dialog(DialogInfo {
521                dialog_id: "dialog-1".into(),
522                kind: "prompt".into(),
523                message: "Name?".into(),
524                default_value: "Ada".into(),
525            }))),
526            error: None,
527        };
528        let json = serde_json::to_string(&envelope).unwrap();
529        assert_eq!(
530            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
531            envelope
532        );
533    }
534
535    #[test]
536    fn file_chooser_hook_envelope_round_trips() {
537        let envelope = PluginEnvelope {
538            ok: true,
539            result: Some(PluginResult::PollHook(HookResult::FileChooser(
540                FileChooserInfo {
541                    file_chooser_id: "chooser-1".to_string(),
542                    is_multiple: true,
543                    note: "opened".to_string(),
544                },
545            ))),
546            error: None,
547        };
548
549        let json = serde_json::to_string(&envelope).unwrap();
550        assert_eq!(
551            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
552            envelope
553        );
554    }
555
556    #[test]
557    fn download_hook_envelope_round_trips() {
558        let envelope = PluginEnvelope {
559            ok: true,
560            result: Some(PluginResult::PollHook(HookResult::Download(DownloadInfo {
561                download_id: "download-1".to_string(),
562                url: "https://example.test/report.csv".to_string(),
563                suggested_filename: "report.csv".to_string(),
564                note: "started".to_string(),
565            }))),
566            error: None,
567        };
568
569        let json = serde_json::to_string(&envelope).unwrap();
570        assert_eq!(
571            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
572            envelope
573        );
574    }
575}
576
577#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
578pub struct CaptureInfo {
579    pub value: Option<String>,
580    pub checked: Option<bool>,
581    pub selected_options: Vec<CapturedOption>,
582    pub bounding_box: Option<BoundingBox>,
583}
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585pub struct CapturedOption {
586    pub value: String,
587    pub label: String,
588    pub index: u32,
589}
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
591pub struct BoundingBox {
592    pub x: f64,
593    pub y: f64,
594    pub width: f64,
595    pub height: f64,
596}