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}