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