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