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, Serialize, Deserialize, PartialEq, Eq)]
244pub struct ScreenshotInfo {
245    pub png_data: Vec<u8>,
246    pub note: String,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(tag = "command", rename_all = "snake_case")]
251pub enum PluginCommand {
252    LaunchBrowser {
253        browser_kind: BrowserKind,
254        browser_binary: Option<String>,
255    },
256    ResolveFrame {
257        browser_session: BrowserSessionHandle,
258        page_session: PageSessionHandle,
259        css_selector: String,
260        timeout_ms: u64,
261    },
262    OpenPage {
263        browser_session: BrowserSessionHandle,
264    },
265    RegisterHook {
266        browser_session: BrowserSessionHandle,
267        page_session: PageSessionHandle,
268        hook_type: HookType,
269    },
270    PollHook {
271        browser_session: BrowserSessionHandle,
272        registration: HookRegistration,
273    },
274    HandleDialog {
275        browser_session: BrowserSessionHandle,
276        page_session: PageSessionHandle,
277        dialog_id: String,
278        accept: bool,
279        prompt_text: Option<String>,
280    },
281    SetFileChooserFiles {
282        browser_session: BrowserSessionHandle,
283        page_session: PageSessionHandle,
284        file_chooser_id: String,
285        files: Vec<String>,
286    },
287    SaveDownload {
288        browser_session: BrowserSessionHandle,
289        page_session: PageSessionHandle,
290        download_id: String,
291        path: String,
292    },
293    ClosePage {
294        browser_session: BrowserSessionHandle,
295        page_session: PageSessionHandle,
296    },
297    NavigatePage {
298        browser_session: BrowserSessionHandle,
299        page_session: PageSessionHandle,
300        url: String,
301    },
302    ClickElement {
303        browser_session: BrowserSessionHandle,
304        page_session: PageSessionHandle,
305        css_selector: String,
306        button: u8,
307        click_count: u32,
308    },
309    CountElements {
310        browser_session: BrowserSessionHandle,
311        page_session: PageSessionHandle,
312        css_selector: String,
313    },
314    HighlightElements {
315        browser_session: BrowserSessionHandle,
316        page_session: PageSessionHandle,
317        css_selector: String,
318        duration_ms: u64,
319    },
320    FocusElement {
321        browser_session: BrowserSessionHandle,
322        page_session: PageSessionHandle,
323        css_selector: String,
324    },
325    FillElement {
326        browser_session: BrowserSessionHandle,
327        page_session: PageSessionHandle,
328        css_selector: String,
329        value: String,
330    },
331    HoverElement {
332        browser_session: BrowserSessionHandle,
333        page_session: PageSessionHandle,
334        css_selector: String,
335    },
336    PressKey {
337        browser_session: BrowserSessionHandle,
338        page_session: PageSessionHandle,
339        css_selector: String,
340        key: String,
341        text: Option<String>,
342    },
343    Capture {
344        browser_session: BrowserSessionHandle,
345        page_session: PageSessionHandle,
346        kind: String,
347        css_selector: String,
348        attribute_name: String,
349    },
350    GetTextContent {
351        browser_session: BrowserSessionHandle,
352        page_session: PageSessionHandle,
353        css_selector: String,
354    },
355    GetInnerText {
356        browser_session: BrowserSessionHandle,
357        page_session: PageSessionHandle,
358        css_selector: String,
359    },
360    WaitForSelector {
361        browser_session: BrowserSessionHandle,
362        page_session: PageSessionHandle,
363        css_selector: String,
364        visible: bool,
365    },
366    AccessibilitySnapshot {
367        browser_session: BrowserSessionHandle,
368        page_session: PageSessionHandle,
369        format: String,
370        #[serde(default)]
371        mode: String,
372    },
373    Screenshot {
374        browser_session: BrowserSessionHandle,
375        page_session: PageSessionHandle,
376        full_page: bool,
377    },
378    OpenChromeWindow {
379        chrome_binary: Option<String>,
380    },
381    DiscoverInitialTab {
382        cdp_websocket_url: String,
383    },
384    OpenChromeTab {
385        cdp_websocket_url: String,
386    },
387    CloseBrowserProcess {
388        process_id: u32,
389    },
390    CloseChromeTab {
391        cdp_websocket_url: String,
392        target_id: String,
393    },
394    NavigateChromeTab {
395        cdp_websocket_url: String,
396        target_id: String,
397        url: String,
398    },
399    InjectChromiumBidiMapper {
400        cdp_websocket_url: String,
401    },
402    ResolveBidiContextForTab {
403        cdp_websocket_url: String,
404        mapper_target_id: Option<String>,
405        browsing_context_id: Option<String>,
406        url: Option<String>,
407    },
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
411#[serde(tag = "result", rename_all = "snake_case")]
412pub enum PluginResult {
413    LaunchBrowser(BrowserLaunchInfo),
414    ResolveFrame(PageInfo),
415    OpenPage(PageInfo),
416    RegisterHook(HookRegistration),
417    PollHook(HookResult),
418    HandleDialog,
419    SetFileChooserFiles(FileChooserFilesSetInfo),
420    SaveDownload(DownloadSavedInfo),
421    ClosePage,
422    NavigatePage(TabNavigationInfo),
423    ClickElement(ClickInfo),
424    CountElements(ElementCountInfo),
425    HighlightElements(HighlightElementsInfo),
426    FocusElement(FocusInfo),
427    FillElement(FillInfo),
428    HoverElement(HoverInfo),
429    PressKey(PressKeyInfo),
430    Capture(CaptureInfo),
431    GetTextContent(TextInfo),
432    GetInnerText(TextInfo),
433    WaitForSelector(WaitForSelectorInfo),
434    AccessibilitySnapshot(AccessibilitySnapshotInfo),
435    Screenshot(ScreenshotInfo),
436    OpenChromeWindow(ChromeLaunchInfo),
437    DiscoverInitialTab(ChromeTabInfo),
438    OpenChromeTab(ChromeTabInfo),
439    CloseBrowserProcess,
440    CloseChromeTab,
441    NavigateChromeTab(TabNavigationInfo),
442    InjectChromiumBidiMapper(ChromiumBidiMapperInfo),
443    ResolveBidiContextForTab {
444        browsing_context_id: String,
445        mapper: ChromiumBidiMapperInfo,
446    },
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
450pub struct PluginEnvelope {
451    pub ok: bool,
452    pub result: Option<PluginResult>,
453    pub error: Option<String>,
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn poll_hook_envelope_round_trips_without_duplicate_result_fields() {
462        let envelope = PluginEnvelope {
463            ok: true,
464            result: Some(PluginResult::PollHook(HookResult::NewPage(PageInfo {
465                note: "opened".to_string(),
466                page_session: PageSessionHandle::Chromium {
467                    target_id: "target-2".to_string(),
468                    browsing_context_id: None,
469                    mapper_target_id: None,
470                },
471            }))),
472            error: None,
473        };
474
475        let json = serde_json::to_string(&envelope).unwrap();
476        assert_eq!(
477            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
478            envelope
479        );
480    }
481
482    #[test]
483    fn dialog_hook_envelope_round_trips() {
484        let envelope = PluginEnvelope {
485            ok: true,
486            result: Some(PluginResult::PollHook(HookResult::Dialog(DialogInfo {
487                dialog_id: "dialog-1".into(),
488                kind: "prompt".into(),
489                message: "Name?".into(),
490                default_value: "Ada".into(),
491            }))),
492            error: None,
493        };
494        let json = serde_json::to_string(&envelope).unwrap();
495        assert_eq!(
496            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
497            envelope
498        );
499    }
500
501    #[test]
502    fn file_chooser_hook_envelope_round_trips() {
503        let envelope = PluginEnvelope {
504            ok: true,
505            result: Some(PluginResult::PollHook(HookResult::FileChooser(
506                FileChooserInfo {
507                    file_chooser_id: "chooser-1".to_string(),
508                    is_multiple: true,
509                    note: "opened".to_string(),
510                },
511            ))),
512            error: None,
513        };
514
515        let json = serde_json::to_string(&envelope).unwrap();
516        assert_eq!(
517            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
518            envelope
519        );
520    }
521
522    #[test]
523    fn download_hook_envelope_round_trips() {
524        let envelope = PluginEnvelope {
525            ok: true,
526            result: Some(PluginResult::PollHook(HookResult::Download(DownloadInfo {
527                download_id: "download-1".to_string(),
528                url: "https://example.test/report.csv".to_string(),
529                suggested_filename: "report.csv".to_string(),
530                note: "started".to_string(),
531            }))),
532            error: None,
533        };
534
535        let json = serde_json::to_string(&envelope).unwrap();
536        assert_eq!(
537            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
538            envelope
539        );
540    }
541}
542
543#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
544pub struct CaptureInfo {
545    pub value: Option<String>,
546    pub checked: Option<bool>,
547    pub selected_options: Vec<CapturedOption>,
548    pub bounding_box: Option<BoundingBox>,
549}
550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
551pub struct CapturedOption {
552    pub value: String,
553    pub label: String,
554    pub index: u32,
555}
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
557pub struct BoundingBox {
558    pub x: f64,
559    pub y: f64,
560    pub width: f64,
561    pub height: f64,
562}