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    NewPage,
109    FileChooser,
110    Download,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct HookRegistration {
115    pub opaque_state: String,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(tag = "hook_type", content = "value", rename_all = "snake_case")]
120pub enum HookResult {
121    NewPage(PageInfo),
122    FileChooser(FileChooserInfo),
123    Download(DownloadInfo),
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct FileChooserInfo {
128    pub file_chooser_id: String,
129    pub is_multiple: bool,
130    pub note: String,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
134pub struct FileChooserFilesSetInfo {
135    pub file_chooser_id: String,
136    pub files: Vec<String>,
137    pub note: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141pub struct DownloadInfo {
142    pub download_id: String,
143    pub url: String,
144    pub suggested_filename: String,
145    pub note: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct DownloadSavedInfo {
150    pub download_id: String,
151    pub path: String,
152    pub suggested_filename: String,
153    pub size: u64,
154    pub note: String,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct AutomationSessionInfo {
159    pub bidi_session_id: String,
160    pub note: String,
161    pub mapper_target_id: Option<String>,
162    pub mapper_session_id: Option<String>,
163    pub package_version: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167pub struct ClickInfo {
168    pub css_selector: String,
169    pub note: String,
170    pub bidi_session_id: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub struct ElementCountInfo {
175    pub css_selector: String,
176    pub count: u32,
177    pub note: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct HighlightElementsInfo {
182    pub css_selector: String,
183    pub count: u32,
184    pub note: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
188pub struct FocusInfo {
189    pub css_selector: String,
190    pub note: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
194pub struct FillInfo {
195    pub css_selector: String,
196    pub value: String,
197    pub note: String,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
201pub struct HoverInfo {
202    pub css_selector: String,
203    pub note: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
207pub struct PressKeyInfo {
208    pub css_selector: String,
209    pub key: String,
210    pub note: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
214pub struct TextInfo {
215    pub css_selector: String,
216    pub text: String,
217    pub note: String,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
221pub struct WaitForSelectorInfo {
222    pub css_selector: String,
223    pub visible: bool,
224    pub note: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
228pub struct AccessibilitySnapshotInfo {
229    pub snapshot: String,
230    pub format: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234pub struct ScreenshotInfo {
235    pub png_data: Vec<u8>,
236    pub note: String,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
240#[serde(tag = "command", rename_all = "snake_case")]
241pub enum PluginCommand {
242    LaunchBrowser {
243        browser_kind: BrowserKind,
244        browser_binary: Option<String>,
245    },
246    ResolveFrame {
247        browser_session: BrowserSessionHandle,
248        page_session: PageSessionHandle,
249        css_selector: String,
250        timeout_ms: u64,
251    },
252    OpenPage {
253        browser_session: BrowserSessionHandle,
254    },
255    RegisterHook {
256        browser_session: BrowserSessionHandle,
257        page_session: PageSessionHandle,
258        hook_type: HookType,
259    },
260    PollHook {
261        browser_session: BrowserSessionHandle,
262        registration: HookRegistration,
263    },
264    SetFileChooserFiles {
265        browser_session: BrowserSessionHandle,
266        page_session: PageSessionHandle,
267        file_chooser_id: String,
268        files: Vec<String>,
269    },
270    SaveDownload {
271        browser_session: BrowserSessionHandle,
272        page_session: PageSessionHandle,
273        download_id: String,
274        path: String,
275    },
276    ClosePage {
277        browser_session: BrowserSessionHandle,
278        page_session: PageSessionHandle,
279    },
280    NavigatePage {
281        browser_session: BrowserSessionHandle,
282        page_session: PageSessionHandle,
283        url: String,
284    },
285    ClickElement {
286        browser_session: BrowserSessionHandle,
287        page_session: PageSessionHandle,
288        css_selector: String,
289    },
290    CountElements {
291        browser_session: BrowserSessionHandle,
292        page_session: PageSessionHandle,
293        css_selector: String,
294    },
295    HighlightElements {
296        browser_session: BrowserSessionHandle,
297        page_session: PageSessionHandle,
298        css_selector: String,
299        duration_ms: u64,
300    },
301    FocusElement {
302        browser_session: BrowserSessionHandle,
303        page_session: PageSessionHandle,
304        css_selector: String,
305    },
306    FillElement {
307        browser_session: BrowserSessionHandle,
308        page_session: PageSessionHandle,
309        css_selector: String,
310        value: String,
311    },
312    HoverElement {
313        browser_session: BrowserSessionHandle,
314        page_session: PageSessionHandle,
315        css_selector: String,
316    },
317    PressKey {
318        browser_session: BrowserSessionHandle,
319        page_session: PageSessionHandle,
320        css_selector: String,
321        key: String,
322        text: Option<String>,
323    },
324    Capture {
325        browser_session: BrowserSessionHandle,
326        page_session: PageSessionHandle,
327        kind: String,
328        css_selector: String,
329        attribute_name: String,
330    },
331    GetTextContent {
332        browser_session: BrowserSessionHandle,
333        page_session: PageSessionHandle,
334        css_selector: String,
335    },
336    GetInnerText {
337        browser_session: BrowserSessionHandle,
338        page_session: PageSessionHandle,
339        css_selector: String,
340    },
341    WaitForSelector {
342        browser_session: BrowserSessionHandle,
343        page_session: PageSessionHandle,
344        css_selector: String,
345        visible: bool,
346    },
347    AccessibilitySnapshot {
348        browser_session: BrowserSessionHandle,
349        page_session: PageSessionHandle,
350        format: String,
351        #[serde(default)]
352        mode: String,
353    },
354    Screenshot {
355        browser_session: BrowserSessionHandle,
356        page_session: PageSessionHandle,
357        full_page: bool,
358    },
359    OpenChromeWindow {
360        chrome_binary: Option<String>,
361    },
362    DiscoverInitialTab {
363        cdp_websocket_url: String,
364    },
365    OpenChromeTab {
366        cdp_websocket_url: String,
367    },
368    CloseBrowserProcess {
369        process_id: u32,
370    },
371    CloseChromeTab {
372        cdp_websocket_url: String,
373        target_id: String,
374    },
375    NavigateChromeTab {
376        cdp_websocket_url: String,
377        target_id: String,
378        url: String,
379    },
380    InjectChromiumBidiMapper {
381        cdp_websocket_url: String,
382    },
383    ResolveBidiContextForTab {
384        cdp_websocket_url: String,
385        mapper_target_id: Option<String>,
386        browsing_context_id: Option<String>,
387        url: Option<String>,
388    },
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
392#[serde(tag = "result", rename_all = "snake_case")]
393pub enum PluginResult {
394    LaunchBrowser(BrowserLaunchInfo),
395    ResolveFrame(PageInfo),
396    OpenPage(PageInfo),
397    RegisterHook(HookRegistration),
398    PollHook(HookResult),
399    SetFileChooserFiles(FileChooserFilesSetInfo),
400    SaveDownload(DownloadSavedInfo),
401    ClosePage,
402    NavigatePage(TabNavigationInfo),
403    ClickElement(ClickInfo),
404    CountElements(ElementCountInfo),
405    HighlightElements(HighlightElementsInfo),
406    FocusElement(FocusInfo),
407    FillElement(FillInfo),
408    HoverElement(HoverInfo),
409    PressKey(PressKeyInfo),
410    Capture(CaptureInfo),
411    GetTextContent(TextInfo),
412    GetInnerText(TextInfo),
413    WaitForSelector(WaitForSelectorInfo),
414    AccessibilitySnapshot(AccessibilitySnapshotInfo),
415    Screenshot(ScreenshotInfo),
416    OpenChromeWindow(ChromeLaunchInfo),
417    DiscoverInitialTab(ChromeTabInfo),
418    OpenChromeTab(ChromeTabInfo),
419    CloseBrowserProcess,
420    CloseChromeTab,
421    NavigateChromeTab(TabNavigationInfo),
422    InjectChromiumBidiMapper(ChromiumBidiMapperInfo),
423    ResolveBidiContextForTab {
424        browsing_context_id: String,
425        mapper: ChromiumBidiMapperInfo,
426    },
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
430pub struct PluginEnvelope {
431    pub ok: bool,
432    pub result: Option<PluginResult>,
433    pub error: Option<String>,
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn poll_hook_envelope_round_trips_without_duplicate_result_fields() {
442        let envelope = PluginEnvelope {
443            ok: true,
444            result: Some(PluginResult::PollHook(HookResult::NewPage(PageInfo {
445                note: "opened".to_string(),
446                page_session: PageSessionHandle::Chromium {
447                    target_id: "target-2".to_string(),
448                    browsing_context_id: None,
449                    mapper_target_id: None,
450                },
451            }))),
452            error: None,
453        };
454
455        let json = serde_json::to_string(&envelope).unwrap();
456        assert_eq!(
457            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
458            envelope
459        );
460    }
461
462    #[test]
463    fn file_chooser_hook_envelope_round_trips() {
464        let envelope = PluginEnvelope {
465            ok: true,
466            result: Some(PluginResult::PollHook(HookResult::FileChooser(
467                FileChooserInfo {
468                    file_chooser_id: "chooser-1".to_string(),
469                    is_multiple: true,
470                    note: "opened".to_string(),
471                },
472            ))),
473            error: None,
474        };
475
476        let json = serde_json::to_string(&envelope).unwrap();
477        assert_eq!(
478            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
479            envelope
480        );
481    }
482
483    #[test]
484    fn download_hook_envelope_round_trips() {
485        let envelope = PluginEnvelope {
486            ok: true,
487            result: Some(PluginResult::PollHook(HookResult::Download(DownloadInfo {
488                download_id: "download-1".to_string(),
489                url: "https://example.test/report.csv".to_string(),
490                suggested_filename: "report.csv".to_string(),
491                note: "started".to_string(),
492            }))),
493            error: None,
494        };
495
496        let json = serde_json::to_string(&envelope).unwrap();
497        assert_eq!(
498            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
499            envelope
500        );
501    }
502}
503
504#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
505pub struct CaptureInfo {
506    pub value: Option<String>,
507    pub checked: Option<bool>,
508    pub selected_options: Vec<CapturedOption>,
509    pub bounding_box: Option<BoundingBox>,
510}
511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
512pub struct CapturedOption {
513    pub value: String,
514    pub label: String,
515    pub index: u32,
516}
517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
518pub struct BoundingBox {
519    pub x: f64,
520    pub y: f64,
521    pub width: f64,
522    pub height: f64,
523}