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