1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet};
6
7use crate::{
8 AttemptId, CommandId, FormControlTarget, PageId, SessionId, WorkflowId, MAX_FORM_REFERENCES,
9 MAX_FORM_VALUE_BYTES,
10};
11
12pub const MAX_INTENT_PURPOSE_BYTES: usize = 256;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(rename_all = "camelCase")]
19pub struct CommandEnvelope {
20 pub schema_version: u16,
21 pub command_id: CommandId,
22 pub workflow_id: WorkflowId,
23 pub attempt_id: AttemptId,
24 pub session_id: SessionId,
25 pub page_id: Option<PageId>,
26 pub deadline: DateTime<Utc>,
27 pub command: RuntimeCommand,
28}
29
30impl CommandEnvelope {
31 pub const SCHEMA_VERSION: u16 = 2;
32
33 pub fn journal_safe(&self) -> Self {
36 let mut safe = self.clone();
37 safe.command.sanitize_urls();
38 safe
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
46pub enum RuntimeCommand {
47 Primitive(PrimitiveCommand),
48 Intent(IntentCommand),
49}
50
51impl RuntimeCommand {
52 pub fn class(&self) -> CommandClass {
53 match self {
54 Self::Primitive(command) => command.class(),
55 Self::Intent(command) => command.class(),
56 }
57 }
58
59 pub fn sanitize_urls(&mut self) {
60 match self {
61 Self::Primitive(command) => command.sanitize_urls(),
62 Self::Intent(_) => {}
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
70pub enum IntentCommand {
71 Locate(LocateIntent),
72 Fill(FillIntent),
73 CompleteForm(CompleteFormIntent),
74 SubmitAndVerify(SubmitAndVerifyIntent),
75 WaitForState(WaitForStateIntent),
76 Follow(FollowIntent),
77 DismissObstruction(DismissObstructionIntent),
78 Extract(ExtractIntent),
79}
80
81impl IntentCommand {
82 pub fn class(&self) -> CommandClass {
83 match self {
84 Self::Locate(_) | Self::WaitForState(_) | Self::Extract(_) => CommandClass::Replayable,
85 Self::Fill(_) | Self::CompleteForm(_) | Self::DismissObstruction(_) => {
86 CommandClass::Reconciliable
87 }
88 Self::SubmitAndVerify(_) => CommandClass::Boundary,
89 Self::Follow(intent) => {
90 if intent.boundary {
91 CommandClass::Boundary
92 } else {
93 CommandClass::Reconciliable
94 }
95 }
96 }
97 }
98}
99
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104#[serde(rename_all = "camelCase", default)]
105pub struct IntentHints {
106 pub role: Option<String>,
107 pub near_text: Option<TextMatch>,
108 pub accessible_name: Option<String>,
115 pub ordinal: Option<usize>,
116 pub frame_path: Vec<TargetSpec>,
117 pub shadow_path: Vec<TargetSpec>,
118 pub allow_best_match: bool,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123#[serde(rename_all = "camelCase")]
124pub struct LocateIntent {
125 pub purpose: String,
126 #[serde(default)]
127 pub hints: IntentHints,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(rename_all = "camelCase")]
133pub struct FillIntent {
134 pub purpose: String,
135 #[serde(default)]
136 pub hints: IntentHints,
137 pub value: FillValue,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
142#[serde(rename_all = "camelCase")]
143pub struct CompleteFormField {
144 pub name: String,
145 pub purpose: String,
146 #[serde(default)]
147 pub hints: IntentHints,
148 pub value: FillValue,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153#[serde(rename_all = "camelCase")]
154pub struct CompleteFormIntent {
155 pub purpose: String,
156 pub fields: Vec<CompleteFormField>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161#[serde(tag = "kind", rename_all = "camelCase")]
162pub enum FillValue {
163 Text {
164 text: String,
165 #[serde(default)]
166 #[serde(rename = "clearFirst")]
167 clear_first: bool,
168 },
169 Select {
170 option: String,
171 },
172 Checked {
173 checked: bool,
174 },
175 Files {
176 paths: Vec<String>,
177 },
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
182#[serde(rename_all = "camelCase")]
183pub struct SubmitAndVerifyIntent {
184 pub purpose: String,
185 #[serde(default)]
186 pub hints: IntentHints,
187 pub expected_state: WaitForCommand,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192#[serde(rename_all = "camelCase")]
193pub struct WaitForStateIntent {
194 pub condition: WaitCondition,
195 pub timeout_ms: u64,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204#[serde(rename_all = "camelCase")]
205pub struct FollowIntent {
206 pub purpose: String,
207 #[serde(default)]
208 pub hints: IntentHints,
209 pub expected_destination: WaitForCommand,
210 #[serde(default)]
211 pub boundary: bool,
212}
213
214pub const DEFAULT_DISMISS_OBSTRUCTION_TIMEOUT_MS: u64 = 5_000;
215
216fn default_dismiss_timeout_ms() -> u64 {
217 DEFAULT_DISMISS_OBSTRUCTION_TIMEOUT_MS
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
226#[serde(rename_all = "camelCase")]
227pub struct DismissObstructionIntent {
228 pub purpose: String,
229 #[serde(default)]
230 pub hints: IntentHints,
231 #[serde(default = "default_dismiss_timeout_ms")]
232 pub timeout_ms: u64,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
239#[serde(tag = "kind", rename_all = "camelCase")]
240pub enum ExtractValueKind {
241 Text,
242 Attribute { attribute: String },
243 Href,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(rename_all = "camelCase")]
250pub struct ExtractField {
251 pub name: String,
252 pub purpose: String,
253 #[serde(default)]
254 pub hints: IntentHints,
255 pub value: ExtractValueKind,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264#[serde(rename_all = "camelCase")]
265pub struct ExtractIntent {
266 pub purpose: String,
267 pub fields: Vec<ExtractField>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272#[serde(tag = "kind", content = "input", rename_all = "camelCase")]
273pub enum PrimitiveCommand {
274 Navigate(NavigateCommand),
275 DownloadUrl(DownloadUrlCommand),
276 Inspect(InspectCommand),
277 Click(ClickCommand),
278 TypeText(TypeTextCommand),
279 UploadFiles(UploadFilesCommand),
280 OpenPage(OpenPageCommand),
281 ListPages(ListPagesCommand),
282 ClosePage(ClosePageCommand),
283 ActivatePage(ActivatePageCommand),
284 AccessibilitySnapshot(AccessibilitySnapshotCommand),
285 ExtractStructured(ExtractStructuredCommand),
286 GetCookies(GetCookiesCommand),
287 PrintToPdf(PrintToPdfCommand),
288 HandleDialog(HandleDialogCommand),
289 Emulate(EmulateCommand),
290 NetworkLog(NetworkLogCommand),
291 SetCookies(SetCookiesCommand),
292 DeleteCookies(DeleteCookiesCommand),
293 ClickAndWaitForPopup(ClickAndWaitForPopupCommand),
294 ClickAndWaitForDownload(ClickAndWaitForDownloadCommand),
295 WaitFor(WaitForCommand),
296 CaptureScreenshot(CaptureScreenshotCommand),
297 ControlAction(ControlActionCommand),
298 SetFocusEmulation(SetFocusEmulationCommand),
299 SetEmulatedMedia(SetEmulatedMediaCommand),
300 EvaluateJavaScript(EvaluateJavaScriptCommand),
301}
302
303impl PrimitiveCommand {
304 fn sanitize_urls(&mut self) {
305 fn sanitize(value: &mut String) {
306 let Ok(mut url) = url::Url::parse(value) else {
307 *value = "[redacted-invalid-url]".into();
308 return;
309 };
310 let _ = url.set_username("");
311 let _ = url.set_password(None);
312 url.set_query(None);
313 url.set_fragment(None);
314 *value = url.to_string();
315 }
316 match self {
317 Self::Navigate(command) => sanitize(&mut command.url),
318 Self::DownloadUrl(command) => sanitize(&mut command.url),
319 Self::UploadFiles(command) => {
320 for (index, path) in command.paths.iter_mut().enumerate() {
321 *path = format!("upload://input/{index}");
322 }
323 }
324 Self::OpenPage(command) => {
325 if let Some(url) = &mut command.url {
326 sanitize(url);
327 }
328 }
329 Self::Click(command) => {
330 if let Some(url) = &mut command.expected_url {
331 sanitize(url);
332 }
333 }
334 Self::ControlAction(command) => {
335 if let ControlAction::SetFiles { paths } = &mut command.action {
336 for (index, path) in paths.iter_mut().enumerate() {
337 *path = format!("upload://input/{index}");
338 }
339 }
340 }
341 _ => {}
342 }
343 }
344
345 pub fn class(&self) -> CommandClass {
346 match self {
347 Self::Navigate(_)
348 | Self::Inspect(_)
349 | Self::OpenPage(_)
350 | Self::ListPages(_)
351 | Self::WaitFor(_)
352 | Self::ActivatePage(_)
353 | Self::Emulate(_)
354 | Self::NetworkLog(_)
355 | Self::AccessibilitySnapshot(_)
356 | Self::ExtractStructured(_)
357 | Self::GetCookies(_)
358 | Self::PrintToPdf(_)
359 | Self::CaptureScreenshot(_) => CommandClass::Replayable,
360 Self::SetCookies(_) | Self::DeleteCookies(_) | Self::HandleDialog(_) => {
361 CommandClass::Reconciliable
362 }
363 Self::DownloadUrl(_)
364 | Self::TypeText(_)
365 | Self::UploadFiles(_)
366 | Self::ClosePage(_)
367 | Self::EvaluateJavaScript(_) => CommandClass::Reconciliable,
368 Self::ControlAction(_) => CommandClass::Reconciliable,
369 Self::ClickAndWaitForPopup(_) | Self::ClickAndWaitForDownload(_) => {
370 CommandClass::Boundary
371 }
372 Self::Click(command) if command.boundary => CommandClass::Boundary,
373 Self::Click(_) => CommandClass::Reconciliable,
374 Self::SetFocusEmulation(_) => CommandClass::Reconciliable,
375 Self::SetEmulatedMedia(_) => CommandClass::Reconciliable,
376 }
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
382#[serde(rename_all = "camelCase", deny_unknown_fields)]
383pub struct ControlActionCommand {
384 pub target: FormControlTarget,
385 pub action: ControlAction,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
390#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
391pub enum ControlAction {
392 SetText { value: String },
393 SetChecked { checked: bool },
394 SelectOne { value: String },
395 SelectMany { values: Vec<String> },
396 SetFiles { paths: Vec<String> },
397 Clear,
398 Activate,
399}
400
401impl ControlAction {
402 pub fn validate(&self) -> Result<(), String> {
403 fn bounded(value: &str, field: &str) -> Result<(), String> {
404 if value.len() > MAX_FORM_VALUE_BYTES {
405 return Err(format!("{field} exceeds {MAX_FORM_VALUE_BYTES} bytes"));
406 }
407 Ok(())
408 }
409
410 match self {
411 Self::SetText { value } | Self::SelectOne { value } => bounded(value, "value"),
412 Self::SelectMany { values } => {
413 if values.is_empty() || values.len() > MAX_FORM_REFERENCES {
414 return Err(format!(
415 "values must contain between 1 and {MAX_FORM_REFERENCES} items"
416 ));
417 }
418 let mut unique = BTreeSet::new();
419 for value in values {
420 bounded(value, "selection value")?;
421 if !unique.insert(value) {
422 return Err("selection values must be unique".into());
423 }
424 }
425 Ok(())
426 }
427 Self::SetFiles { paths } => {
428 if paths.is_empty() || paths.len() > MAX_FORM_REFERENCES {
429 return Err(format!(
430 "paths must contain between 1 and {MAX_FORM_REFERENCES} items"
431 ));
432 }
433 for path in paths {
434 bounded(path, "file path")?;
435 }
436 Ok(())
437 }
438 Self::SetChecked { .. } | Self::Clear | Self::Activate => Ok(()),
439 }
440 }
441
442 pub fn operation(&self) -> crate::FormControlOperation {
443 match self {
444 Self::SetText { .. } => crate::FormControlOperation::SetText,
445 Self::SetChecked { .. } => crate::FormControlOperation::SetChecked,
446 Self::SelectOne { .. } => crate::FormControlOperation::SelectOne,
447 Self::SelectMany { .. } => crate::FormControlOperation::SelectMany,
448 Self::SetFiles { .. } => crate::FormControlOperation::SetFiles,
449 Self::Clear => crate::FormControlOperation::Clear,
450 Self::Activate => crate::FormControlOperation::Activate,
451 }
452 }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[serde(rename_all = "camelCase")]
458pub struct DownloadUrlCommand {
459 pub url: String,
460 pub expected_content_type: Option<String>,
461 pub max_bytes: u64,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
465#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466#[serde(rename_all = "camelCase")]
467pub enum CommandClass {
468 Replayable,
469 Reconciliable,
470 Boundary,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
474#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
475#[serde(rename_all = "camelCase")]
476pub struct NavigateCommand {
477 pub url: String,
478 pub wait_until: WaitUntil,
479 pub timeout_ms: u64,
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
483#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
484#[serde(rename_all = "camelCase")]
485pub enum WaitUntil {
486 Commit,
487 DomContentLoaded,
488 Interactive,
489 NetworkIdle,
490}
491
492#[derive(Debug, Clone, Default, Serialize, Deserialize)]
493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
494#[serde(rename_all = "camelCase")]
495pub struct InspectCommand {
496 pub selector: Option<String>,
497 pub target: Option<TargetSpec>,
498 pub include_html: bool,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
502#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
503#[serde(rename_all = "camelCase")]
504pub struct ClickCommand {
505 pub selector: String,
506 pub target: Option<TargetSpec>,
507 pub boundary: bool,
508 pub expected_url: Option<String>,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
513#[serde(rename_all = "camelCase")]
514pub struct TypeTextCommand {
515 pub selector: String,
516 pub target: Option<TargetSpec>,
517 pub value: String,
518 pub clear_first: bool,
519 #[serde(default)]
522 pub expected_url: Option<String>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
527#[serde(rename_all = "camelCase")]
528pub struct UploadFilesCommand {
529 pub selector: String,
530 pub target: Option<TargetSpec>,
531 pub paths: Vec<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
535#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
536#[serde(rename_all = "camelCase")]
537pub struct OpenPageCommand {
538 pub url: Option<String>,
539}
540
541#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
543pub struct ListPagesCommand;
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
546#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
547#[serde(rename_all = "camelCase")]
548pub struct ClosePageCommand {
549 pub page_id: PageId,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
553#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
554#[serde(rename_all = "camelCase")]
555pub struct ActivatePageCommand {
556 pub page_id: PageId,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
561#[serde(rename_all = "camelCase")]
562pub struct AccessibilitySnapshotCommand {
563 pub max_nodes: Option<u32>,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568#[serde(rename_all = "camelCase")]
569pub struct ExtractStructuredCommand {
570 pub schema: serde_json::Value,
571 pub purpose: Option<String>,
572}
573
574#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
576#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
577#[serde(rename_all = "camelCase", deny_unknown_fields)]
578pub struct CookieRecord {
579 pub name: String,
580 pub value: String,
581 pub domain: String,
582 pub path: String,
583 pub secure: bool,
584 pub http_only: bool,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub same_site: Option<String>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
588 pub expires_unix: Option<f64>,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595#[serde(rename_all = "camelCase", deny_unknown_fields)]
596pub struct SetCookieParam {
597 pub name: String,
598 pub value: String,
599 pub url: String,
600 #[serde(default)]
601 pub path: Option<String>,
602 #[serde(default)]
603 pub secure: bool,
604 #[serde(default)]
605 pub http_only: bool,
606 #[serde(default)]
607 pub same_site: Option<String>,
608 #[serde(default)]
609 pub expires_unix: Option<f64>,
610}
611
612#[derive(Debug, Clone, Default, Serialize, Deserialize)]
613#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
614#[serde(rename_all = "camelCase", deny_unknown_fields)]
615pub struct NetworkLogCommand {
616 #[serde(default = "default_true")]
618 pub clear: bool,
619}
620
621fn default_true() -> bool {
622 true
623}
624
625#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
626#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
627#[serde(rename_all = "camelCase", deny_unknown_fields)]
628pub struct ViewportSize {
629 pub width: u32,
630 pub height: u32,
631}
632
633#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
634#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
635#[serde(rename_all = "camelCase", deny_unknown_fields)]
636pub struct GeolocationCoordinates {
637 pub latitude: f64,
638 pub longitude: f64,
639 #[serde(default)]
640 pub accuracy: Option<f64>,
641}
642
643#[derive(Debug, Clone, Default, Serialize, Deserialize)]
644#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
645#[serde(rename_all = "camelCase", deny_unknown_fields)]
646pub struct EmulateCommand {
647 #[serde(default)]
648 pub viewport: Option<ViewportSize>,
649 #[serde(default)]
650 pub geolocation: Option<GeolocationCoordinates>,
651 #[serde(default)]
653 pub mobile: Option<bool>,
654}
655
656#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
657#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
658#[serde(rename_all = "camelCase")]
659pub enum DialogAction {
660 Accept,
661 Dismiss,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
665#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
666#[serde(rename_all = "camelCase", deny_unknown_fields)]
667pub struct HandleDialogCommand {
668 pub action: DialogAction,
669 #[serde(default)]
670 pub timeout_ms: Option<u64>,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
674#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
675#[serde(rename_all = "camelCase", deny_unknown_fields)]
676pub struct PrintToPdfCommand {
677 #[serde(default)]
678 pub landscape: bool,
679 #[serde(default = "default_print_background")]
680 pub print_background: bool,
681 #[serde(default)]
682 pub scale: Option<f64>,
683 #[serde(default)]
684 pub page_ranges: Option<String>,
685}
686
687fn default_print_background() -> bool {
688 true
689}
690
691#[derive(Debug, Clone, Default, Serialize, Deserialize)]
692#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
693#[serde(rename_all = "camelCase", deny_unknown_fields)]
694pub struct GetCookiesCommand {
695 #[serde(default)]
697 pub urls: Vec<String>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
701#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
702#[serde(rename_all = "camelCase", deny_unknown_fields)]
703pub struct SetCookiesCommand {
704 pub cookies: Vec<SetCookieParam>,
705}
706
707#[derive(Debug, Clone, Default, Serialize, Deserialize)]
708#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
709#[serde(rename_all = "camelCase", deny_unknown_fields)]
710pub struct DeleteCookiesCommand {
711 #[serde(default)]
713 pub urls: Vec<String>,
714 #[serde(default)]
716 pub names: Vec<String>,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize)]
720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
721#[serde(rename_all = "camelCase")]
722pub struct ClickAndWaitForPopupCommand {
723 pub selector: String,
724 pub target: Option<TargetSpec>,
725 pub timeout_ms: u64,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
729#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
730#[serde(rename_all = "camelCase")]
731pub struct ClickAndWaitForDownloadCommand {
732 pub selector: String,
733 pub target: Option<TargetSpec>,
734 pub timeout_ms: u64,
735}
736
737#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
738#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
739#[serde(rename_all = "camelCase")]
740pub struct TargetSpec {
741 pub css: Option<String>,
742 pub test_id: Option<String>,
743 pub role: Option<String>,
744 pub accessible_name: Option<String>,
745 pub label: Option<String>,
746 pub text: Option<TextMatch>,
747 #[serde(default)]
748 pub attributes: BTreeMap<String, String>,
749 #[serde(default)]
750 pub frame_path: Vec<Box<TargetSpec>>,
751 #[serde(default)]
752 pub shadow_path: Vec<Box<TargetSpec>>,
753 pub ordinal: Option<usize>,
754 #[serde(default)]
755 pub allow_best_match: bool,
756}
757
758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
759#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
760#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
761pub enum TextMatch {
762 Exact(String),
763 Contains(String),
764 Regex(String),
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
769#[serde(rename_all = "camelCase")]
770pub enum ElementState {
771 Attached,
772 Detached,
773 Visible,
774 Hidden,
775 Enabled,
776 Disabled,
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781#[serde(rename_all = "PascalCase")]
782pub enum NetworkResourceType {
783 Document,
784 Stylesheet,
785 Image,
786 Media,
787 Font,
788 Script,
789 TextTrack,
790 #[serde(rename = "XHR", alias = "Xhr")]
791 Xhr,
792 Fetch,
793 Prefetch,
794 EventSource,
795 WebSocket,
796 Manifest,
797 SignedExchange,
798 Ping,
799 #[serde(rename = "CSPViolationReport")]
800 CspViolationReport,
801 Preflight,
802 #[serde(rename = "FedCM")]
803 FedCm,
804 Other,
805}
806
807impl NetworkResourceType {
808 pub fn as_str(&self) -> &'static str {
809 match self {
810 Self::Document => "Document",
811 Self::Stylesheet => "Stylesheet",
812 Self::Image => "Image",
813 Self::Media => "Media",
814 Self::Font => "Font",
815 Self::Script => "Script",
816 Self::TextTrack => "TextTrack",
817 Self::Xhr => "XHR",
818 Self::Fetch => "Fetch",
819 Self::Prefetch => "Prefetch",
820 Self::EventSource => "EventSource",
821 Self::WebSocket => "WebSocket",
822 Self::Manifest => "Manifest",
823 Self::SignedExchange => "SignedExchange",
824 Self::Ping => "Ping",
825 Self::CspViolationReport => "CSPViolationReport",
826 Self::Preflight => "Preflight",
827 Self::FedCm => "FedCM",
828 Self::Other => "Other",
829 }
830 }
831}
832
833impl std::str::FromStr for NetworkResourceType {
834 type Err = String;
835
836 fn from_str(s: &str) -> Result<Self, Self::Err> {
837 match s {
838 "Document" | "document" => Ok(Self::Document),
839 "Stylesheet" | "stylesheet" => Ok(Self::Stylesheet),
840 "Image" | "image" => Ok(Self::Image),
841 "Media" | "media" => Ok(Self::Media),
842 "Font" | "font" => Ok(Self::Font),
843 "Script" | "script" => Ok(Self::Script),
844 "TextTrack" | "textTrack" | "texttrack" => Ok(Self::TextTrack),
845 "XHR" | "Xhr" | "xhr" => Ok(Self::Xhr),
846 "Fetch" | "fetch" => Ok(Self::Fetch),
847 "Prefetch" | "prefetch" => Ok(Self::Prefetch),
848 "EventSource" | "eventSource" | "eventsource" => Ok(Self::EventSource),
849 "WebSocket" | "webSocket" | "websocket" => Ok(Self::WebSocket),
850 "Manifest" | "manifest" => Ok(Self::Manifest),
851 "SignedExchange" | "signedExchange" => Ok(Self::SignedExchange),
852 "Ping" | "ping" => Ok(Self::Ping),
853 "CSPViolationReport" | "cspViolationReport" => Ok(Self::CspViolationReport),
854 "Preflight" | "preflight" => Ok(Self::Preflight),
855 "FedCM" | "FedCm" | "fedCM" => Ok(Self::FedCm),
856 "Other" | "other" => Ok(Self::Other),
857 other => Err(format!("unknown network resource type: {other}")),
858 }
859 }
860}
861
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
863#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
864#[serde(tag = "kind", rename_all = "camelCase")]
865pub enum WaitCondition {
866 Element {
867 target: Box<TargetSpec>,
868 state: ElementState,
869 },
870 Text {
871 target: Box<TargetSpec>,
872 matcher: TextMatch,
873 },
874 Value {
875 target: Box<TargetSpec>,
876 matcher: TextMatch,
877 },
878 Url {
879 matcher: TextMatch,
880 },
881 Document {
882 ready: WaitUntil,
883 },
884 NetworkQuiet {
885 #[serde(rename = "idleMs", alias = "idle_ms")]
886 idle_ms: u64,
887 #[serde(rename = "maxInFlight", alias = "max_in_flight")]
888 max_in_flight: usize,
889 #[serde(
890 default,
891 rename = "ignoreUrlSubstrings",
892 alias = "ignore_url_substrings",
893 skip_serializing_if = "Vec::is_empty"
894 )]
895 ignore_url_substrings: Vec<String>,
896 #[serde(
897 default,
898 rename = "ignoreResourceTypes",
899 alias = "ignore_resource_types",
900 skip_serializing_if = "Vec::is_empty"
901 )]
902 ignore_resource_types: Vec<NetworkResourceType>,
903 #[serde(
904 default,
905 rename = "ignoreLongLived",
906 alias = "ignore_long_lived",
907 skip_serializing_if = "std::ops::Not::not"
908 )]
909 ignore_long_lived: bool,
910 },
911}
912
913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
914#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
915#[serde(rename_all = "camelCase")]
916pub struct WaitForCommand {
917 pub condition: WaitCondition,
918 pub timeout_ms: u64,
919}
920
921#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
922#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
923#[serde(tag = "kind", rename_all = "camelCase")]
924pub enum ScreenshotMode {
925 Viewport,
926 FullPage,
927 Element {
928 target: Box<TargetSpec>,
929 },
930 Clip {
931 x: f64,
932 y: f64,
933 width: f64,
934 height: f64,
935 },
936}
937
938#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
940#[serde(rename_all = "camelCase")]
941pub struct CaptureScreenshotCommand {
942 pub mode: ScreenshotMode,
943}
944
945#[derive(Debug, Clone, Serialize, Deserialize)]
946#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
947#[serde(rename_all = "camelCase")]
948pub struct SetFocusEmulationCommand {
949 pub enabled: bool,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954#[serde(rename_all = "camelCase")]
955pub struct SetEmulatedMediaCommand {
956 pub media: String,
957 pub features: BTreeMap<String, String>,
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize)]
961#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
962#[serde(rename_all = "camelCase")]
963pub struct EvaluateJavaScriptCommand {
964 pub expression: String,
965 pub timeout_ms: u64,
966 #[serde(default)]
967 pub await_promise: bool,
968}
969
970#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
974#[serde(rename_all = "camelCase")]
975pub struct ExecutionPolicy {
976 #[serde(default)]
977 pub javascript_evaluation: bool,
978 #[serde(default)]
979 pub vision_assist: bool,
980 #[serde(default)]
985 pub fingerprint: bool,
986 #[serde(default)]
993 pub humanize: bool,
994 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub vision_node: Option<String>,
1000}
1001
1002#[derive(Debug, Clone, Serialize, Deserialize)]
1004#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1005#[serde(rename_all = "camelCase")]
1006pub struct CreateSessionRequest {
1007 pub profile: String,
1008 pub proxy: Option<String>,
1009 #[serde(default)]
1010 pub execution_policy: ExecutionPolicy,
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1016pub struct OpenPageRequest {
1017 pub session_id: SessionId,
1018}
1019
1020#[derive(Debug, Clone, Serialize, Deserialize)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022pub struct NavigationRequest {
1023 pub page_id: PageId,
1024 pub url: String,
1025 pub wait_until: Option<String>,
1026 pub timeout_ms: Option<u64>,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1031pub struct ExtractRequest {
1032 pub page_id: PageId,
1033 pub fields: serde_json::Value,
1034}