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    GetTextContent {
325        browser_session: BrowserSessionHandle,
326        page_session: PageSessionHandle,
327        css_selector: String,
328    },
329    GetInnerText {
330        browser_session: BrowserSessionHandle,
331        page_session: PageSessionHandle,
332        css_selector: String,
333    },
334    WaitForSelector {
335        browser_session: BrowserSessionHandle,
336        page_session: PageSessionHandle,
337        css_selector: String,
338        visible: bool,
339    },
340    AccessibilitySnapshot {
341        browser_session: BrowserSessionHandle,
342        page_session: PageSessionHandle,
343        format: String,
344        #[serde(default)]
345        mode: String,
346    },
347    Screenshot {
348        browser_session: BrowserSessionHandle,
349        page_session: PageSessionHandle,
350        full_page: bool,
351    },
352    OpenChromeWindow {
353        chrome_binary: Option<String>,
354    },
355    DiscoverInitialTab {
356        cdp_websocket_url: String,
357    },
358    OpenChromeTab {
359        cdp_websocket_url: String,
360    },
361    CloseBrowserProcess {
362        process_id: u32,
363    },
364    CloseChromeTab {
365        cdp_websocket_url: String,
366        target_id: String,
367    },
368    NavigateChromeTab {
369        cdp_websocket_url: String,
370        target_id: String,
371        url: String,
372    },
373    InjectChromiumBidiMapper {
374        cdp_websocket_url: String,
375    },
376    ResolveBidiContextForTab {
377        cdp_websocket_url: String,
378        mapper_target_id: Option<String>,
379        browsing_context_id: Option<String>,
380        url: Option<String>,
381    },
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385#[serde(tag = "result", rename_all = "snake_case")]
386pub enum PluginResult {
387    LaunchBrowser(BrowserLaunchInfo),
388    ResolveFrame(PageInfo),
389    OpenPage(PageInfo),
390    RegisterHook(HookRegistration),
391    PollHook(HookResult),
392    SetFileChooserFiles(FileChooserFilesSetInfo),
393    SaveDownload(DownloadSavedInfo),
394    ClosePage,
395    NavigatePage(TabNavigationInfo),
396    ClickElement(ClickInfo),
397    CountElements(ElementCountInfo),
398    HighlightElements(HighlightElementsInfo),
399    FocusElement(FocusInfo),
400    FillElement(FillInfo),
401    HoverElement(HoverInfo),
402    PressKey(PressKeyInfo),
403    GetTextContent(TextInfo),
404    GetInnerText(TextInfo),
405    WaitForSelector(WaitForSelectorInfo),
406    AccessibilitySnapshot(AccessibilitySnapshotInfo),
407    Screenshot(ScreenshotInfo),
408    OpenChromeWindow(ChromeLaunchInfo),
409    DiscoverInitialTab(ChromeTabInfo),
410    OpenChromeTab(ChromeTabInfo),
411    CloseBrowserProcess,
412    CloseChromeTab,
413    NavigateChromeTab(TabNavigationInfo),
414    InjectChromiumBidiMapper(ChromiumBidiMapperInfo),
415    ResolveBidiContextForTab {
416        browsing_context_id: String,
417        mapper: ChromiumBidiMapperInfo,
418    },
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
422pub struct PluginEnvelope {
423    pub ok: bool,
424    pub result: Option<PluginResult>,
425    pub error: Option<String>,
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn poll_hook_envelope_round_trips_without_duplicate_result_fields() {
434        let envelope = PluginEnvelope {
435            ok: true,
436            result: Some(PluginResult::PollHook(HookResult::NewPage(PageInfo {
437                note: "opened".to_string(),
438                page_session: PageSessionHandle::Chromium {
439                    target_id: "target-2".to_string(),
440                    browsing_context_id: None,
441                    mapper_target_id: None,
442                },
443            }))),
444            error: None,
445        };
446
447        let json = serde_json::to_string(&envelope).unwrap();
448        assert_eq!(
449            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
450            envelope
451        );
452    }
453
454    #[test]
455    fn file_chooser_hook_envelope_round_trips() {
456        let envelope = PluginEnvelope {
457            ok: true,
458            result: Some(PluginResult::PollHook(HookResult::FileChooser(
459                FileChooserInfo {
460                    file_chooser_id: "chooser-1".to_string(),
461                    is_multiple: true,
462                    note: "opened".to_string(),
463                },
464            ))),
465            error: None,
466        };
467
468        let json = serde_json::to_string(&envelope).unwrap();
469        assert_eq!(
470            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
471            envelope
472        );
473    }
474
475    #[test]
476    fn download_hook_envelope_round_trips() {
477        let envelope = PluginEnvelope {
478            ok: true,
479            result: Some(PluginResult::PollHook(HookResult::Download(DownloadInfo {
480                download_id: "download-1".to_string(),
481                url: "https://example.test/report.csv".to_string(),
482                suggested_filename: "report.csv".to_string(),
483                note: "started".to_string(),
484            }))),
485            error: None,
486        };
487
488        let json = serde_json::to_string(&envelope).unwrap();
489        assert_eq!(
490            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
491            envelope
492        );
493    }
494}