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}
377
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
379#[serde(tag = "result", rename_all = "snake_case")]
380pub enum PluginResult {
381    LaunchBrowser(BrowserLaunchInfo),
382    OpenPage(PageInfo),
383    RegisterHook(HookRegistration),
384    PollHook(HookResult),
385    SetFileChooserFiles(FileChooserFilesSetInfo),
386    SaveDownload(DownloadSavedInfo),
387    ClosePage,
388    NavigatePage(TabNavigationInfo),
389    ClickElement(ClickInfo),
390    CountElements(ElementCountInfo),
391    HighlightElements(HighlightElementsInfo),
392    FocusElement(FocusInfo),
393    FillElement(FillInfo),
394    HoverElement(HoverInfo),
395    PressKey(PressKeyInfo),
396    GetTextContent(TextInfo),
397    GetInnerText(TextInfo),
398    WaitForSelector(WaitForSelectorInfo),
399    AccessibilitySnapshot(AccessibilitySnapshotInfo),
400    Screenshot(ScreenshotInfo),
401    OpenChromeWindow(ChromeLaunchInfo),
402    DiscoverInitialTab(ChromeTabInfo),
403    OpenChromeTab(ChromeTabInfo),
404    CloseBrowserProcess,
405    CloseChromeTab,
406    NavigateChromeTab(TabNavigationInfo),
407    InjectChromiumBidiMapper(ChromiumBidiMapperInfo),
408    ResolveBidiContextForTab {
409        browsing_context_id: String,
410        mapper: ChromiumBidiMapperInfo,
411    },
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
415pub struct PluginEnvelope {
416    pub ok: bool,
417    pub result: Option<PluginResult>,
418    pub error: Option<String>,
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn poll_hook_envelope_round_trips_without_duplicate_result_fields() {
427        let envelope = PluginEnvelope {
428            ok: true,
429            result: Some(PluginResult::PollHook(HookResult::NewPage(PageInfo {
430                note: "opened".to_string(),
431                page_session: PageSessionHandle::Chromium {
432                    target_id: "target-2".to_string(),
433                    browsing_context_id: None,
434                    mapper_target_id: None,
435                },
436            }))),
437            error: None,
438        };
439
440        let json = serde_json::to_string(&envelope).unwrap();
441        assert_eq!(
442            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
443            envelope
444        );
445    }
446
447    #[test]
448    fn file_chooser_hook_envelope_round_trips() {
449        let envelope = PluginEnvelope {
450            ok: true,
451            result: Some(PluginResult::PollHook(HookResult::FileChooser(
452                FileChooserInfo {
453                    file_chooser_id: "chooser-1".to_string(),
454                    is_multiple: true,
455                    note: "opened".to_string(),
456                },
457            ))),
458            error: None,
459        };
460
461        let json = serde_json::to_string(&envelope).unwrap();
462        assert_eq!(
463            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
464            envelope
465        );
466    }
467
468    #[test]
469    fn download_hook_envelope_round_trips() {
470        let envelope = PluginEnvelope {
471            ok: true,
472            result: Some(PluginResult::PollHook(HookResult::Download(DownloadInfo {
473                download_id: "download-1".to_string(),
474                url: "https://example.test/report.csv".to_string(),
475                suggested_filename: "report.csv".to_string(),
476                note: "started".to_string(),
477            }))),
478            error: None,
479        };
480
481        let json = serde_json::to_string(&envelope).unwrap();
482        assert_eq!(
483            serde_json::from_str::<PluginEnvelope>(&json).unwrap(),
484            envelope
485        );
486    }
487}