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) => {
319 sanitize(&mut command.url);
320 if let Some(save_as) = &mut command.save_as {
321 *save_as = "[redacted-download-path]".into();
322 }
323 }
324 Self::UploadFiles(command) => {
325 for (index, path) in command.paths.iter_mut().enumerate() {
326 *path = format!("upload://input/{index}");
327 }
328 }
329 Self::OpenPage(command) => {
330 if let Some(url) = &mut command.url {
331 sanitize(url);
332 }
333 }
334 Self::Click(command) => {
335 if let Some(url) = &mut command.expected_url {
336 sanitize(url);
337 }
338 }
339 Self::ControlAction(command) => {
340 if let ControlAction::SetFiles { paths } = &mut command.action {
341 for (index, path) in paths.iter_mut().enumerate() {
342 *path = format!("upload://input/{index}");
343 }
344 }
345 }
346 _ => {}
347 }
348 }
349
350 pub fn class(&self) -> CommandClass {
351 match self {
352 Self::Navigate(_)
353 | Self::Inspect(_)
354 | Self::OpenPage(_)
355 | Self::ListPages(_)
356 | Self::WaitFor(_)
357 | Self::ActivatePage(_)
358 | Self::Emulate(_)
359 | Self::NetworkLog(_)
360 | Self::AccessibilitySnapshot(_)
361 | Self::ExtractStructured(_)
362 | Self::GetCookies(_)
363 | Self::PrintToPdf(_)
364 | Self::CaptureScreenshot(_) => CommandClass::Replayable,
365 Self::SetCookies(_) | Self::DeleteCookies(_) | Self::HandleDialog(_) => {
366 CommandClass::Reconciliable
367 }
368 Self::DownloadUrl(_)
369 | Self::TypeText(_)
370 | Self::UploadFiles(_)
371 | Self::ClosePage(_)
372 | Self::EvaluateJavaScript(_) => CommandClass::Reconciliable,
373 Self::ControlAction(_) => CommandClass::Reconciliable,
374 Self::ClickAndWaitForPopup(_) | Self::ClickAndWaitForDownload(_) => {
375 CommandClass::Boundary
376 }
377 Self::Click(command) if command.boundary => CommandClass::Boundary,
378 Self::Click(_) => CommandClass::Reconciliable,
379 Self::SetFocusEmulation(_) => CommandClass::Reconciliable,
380 Self::SetEmulatedMedia(_) => CommandClass::Reconciliable,
381 }
382 }
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
387#[serde(rename_all = "camelCase", deny_unknown_fields)]
388pub struct ControlActionCommand {
389 pub target: FormControlTarget,
390 pub action: ControlAction,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
395#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
396pub enum ControlAction {
397 SetText { value: String },
398 SetChecked { checked: bool },
399 SelectOne { value: String },
400 SelectMany { values: Vec<String> },
401 SetFiles { paths: Vec<String> },
402 Clear,
403 Activate,
404}
405
406impl ControlAction {
407 pub fn validate(&self) -> Result<(), String> {
408 fn bounded(value: &str, field: &str) -> Result<(), String> {
409 if value.len() > MAX_FORM_VALUE_BYTES {
410 return Err(format!("{field} exceeds {MAX_FORM_VALUE_BYTES} bytes"));
411 }
412 Ok(())
413 }
414
415 match self {
416 Self::SetText { value } | Self::SelectOne { value } => bounded(value, "value"),
417 Self::SelectMany { values } => {
418 if values.is_empty() || values.len() > MAX_FORM_REFERENCES {
419 return Err(format!(
420 "values must contain between 1 and {MAX_FORM_REFERENCES} items"
421 ));
422 }
423 let mut unique = BTreeSet::new();
424 for value in values {
425 bounded(value, "selection value")?;
426 if !unique.insert(value) {
427 return Err("selection values must be unique".into());
428 }
429 }
430 Ok(())
431 }
432 Self::SetFiles { paths } => {
433 if paths.is_empty() || paths.len() > MAX_FORM_REFERENCES {
434 return Err(format!(
435 "paths must contain between 1 and {MAX_FORM_REFERENCES} items"
436 ));
437 }
438 for path in paths {
439 bounded(path, "file path")?;
440 }
441 Ok(())
442 }
443 Self::SetChecked { .. } | Self::Clear | Self::Activate => Ok(()),
444 }
445 }
446
447 pub fn operation(&self) -> crate::FormControlOperation {
448 match self {
449 Self::SetText { .. } => crate::FormControlOperation::SetText,
450 Self::SetChecked { .. } => crate::FormControlOperation::SetChecked,
451 Self::SelectOne { .. } => crate::FormControlOperation::SelectOne,
452 Self::SelectMany { .. } => crate::FormControlOperation::SelectMany,
453 Self::SetFiles { .. } => crate::FormControlOperation::SetFiles,
454 Self::Clear => crate::FormControlOperation::Clear,
455 Self::Activate => crate::FormControlOperation::Activate,
456 }
457 }
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
461#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
462#[serde(rename_all = "camelCase")]
463pub struct DownloadUrlCommand {
464 pub url: String,
465 pub expected_content_type: Option<String>,
466 pub max_bytes: u64,
467 pub save_as: Option<String>,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
471#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
472#[serde(rename_all = "camelCase")]
473pub enum CommandClass {
474 Replayable,
475 Reconciliable,
476 Boundary,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
481#[serde(rename_all = "camelCase")]
482pub struct NavigateCommand {
483 pub url: String,
484 pub wait_until: WaitUntil,
485 pub timeout_ms: u64,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
489#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
490#[serde(rename_all = "camelCase")]
491pub enum WaitUntil {
492 Commit,
493 DomContentLoaded,
494 Interactive,
495 NetworkIdle,
496}
497
498#[derive(Debug, Clone, Default, Serialize, Deserialize)]
499#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
500#[serde(rename_all = "camelCase")]
501pub struct InspectCommand {
502 pub selector: Option<String>,
503 pub target: Option<TargetSpec>,
504 pub include_html: bool,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
509#[serde(rename_all = "camelCase")]
510pub struct ClickCommand {
511 pub selector: String,
512 pub target: Option<TargetSpec>,
513 pub boundary: bool,
514 pub expected_url: Option<String>,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
518#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
519#[serde(rename_all = "camelCase")]
520pub struct TypeTextCommand {
521 pub selector: String,
522 pub target: Option<TargetSpec>,
523 pub value: String,
524 pub clear_first: bool,
525 #[serde(default)]
528 pub expected_url: Option<String>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
532#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
533#[serde(rename_all = "camelCase")]
534pub struct UploadFilesCommand {
535 pub selector: String,
536 pub target: Option<TargetSpec>,
537 pub paths: Vec<String>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542#[serde(rename_all = "camelCase")]
543pub struct OpenPageCommand {
544 pub url: Option<String>,
545}
546
547#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
548#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
549pub struct ListPagesCommand;
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553#[serde(rename_all = "camelCase")]
554pub struct ClosePageCommand {
555 pub page_id: PageId,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
559#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
560#[serde(rename_all = "camelCase")]
561pub struct ActivatePageCommand {
562 pub page_id: PageId,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
566#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
567#[serde(rename_all = "camelCase")]
568pub struct AccessibilitySnapshotCommand {
569 pub max_nodes: Option<u32>,
570 #[serde(default)]
574 pub target: Option<TargetSpec>,
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
578#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
579#[serde(rename_all = "camelCase")]
580pub struct ExtractStructuredCommand {
581 pub schema: serde_json::Value,
582 pub purpose: Option<String>,
583}
584
585#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
587#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
588#[serde(rename_all = "camelCase", deny_unknown_fields)]
589pub struct CookieRecord {
590 pub name: String,
591 pub value: String,
592 pub domain: String,
593 pub path: String,
594 pub secure: bool,
595 pub http_only: bool,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub same_site: Option<String>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub expires_unix: Option<f64>,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
605#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
606#[serde(rename_all = "camelCase", deny_unknown_fields)]
607pub struct SetCookieParam {
608 pub name: String,
609 pub value: String,
610 pub url: String,
611 #[serde(default)]
612 pub path: Option<String>,
613 #[serde(default)]
614 pub secure: bool,
615 #[serde(default)]
616 pub http_only: bool,
617 #[serde(default)]
618 pub same_site: Option<String>,
619 #[serde(default)]
620 pub expires_unix: Option<f64>,
621}
622
623#[derive(Debug, Clone, Default, Serialize, Deserialize)]
624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
625#[serde(rename_all = "camelCase", deny_unknown_fields)]
626pub struct NetworkLogCommand {
627 #[serde(default = "default_true")]
629 pub clear: bool,
630}
631
632fn default_true() -> bool {
633 true
634}
635
636#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
638#[serde(rename_all = "camelCase", deny_unknown_fields)]
639pub struct ViewportSize {
640 pub width: u32,
641 pub height: u32,
642}
643
644#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
645#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
646#[serde(rename_all = "camelCase", deny_unknown_fields)]
647pub struct GeolocationCoordinates {
648 pub latitude: f64,
649 pub longitude: f64,
650 #[serde(default)]
651 pub accuracy: Option<f64>,
652}
653
654#[derive(Debug, Clone, Default, Serialize, Deserialize)]
655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
656#[serde(rename_all = "camelCase", deny_unknown_fields)]
657pub struct EmulateCommand {
658 #[serde(default)]
659 pub viewport: Option<ViewportSize>,
660 #[serde(default)]
661 pub geolocation: Option<GeolocationCoordinates>,
662 #[serde(default)]
664 pub mobile: Option<bool>,
665}
666
667#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
669#[serde(rename_all = "camelCase")]
670pub enum DialogAction {
671 Accept,
672 Dismiss,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize)]
676#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
677#[serde(rename_all = "camelCase", deny_unknown_fields)]
678pub struct HandleDialogCommand {
679 pub action: DialogAction,
680 #[serde(default)]
681 pub timeout_ms: Option<u64>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
685#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
686#[serde(rename_all = "camelCase", deny_unknown_fields)]
687pub struct PrintToPdfCommand {
688 #[serde(default)]
689 pub landscape: bool,
690 #[serde(default = "default_print_background")]
691 pub print_background: bool,
692 #[serde(default)]
693 pub scale: Option<f64>,
694 #[serde(default)]
695 pub page_ranges: Option<String>,
696}
697
698fn default_print_background() -> bool {
699 true
700}
701
702#[derive(Debug, Clone, Default, Serialize, Deserialize)]
703#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
704#[serde(rename_all = "camelCase", deny_unknown_fields)]
705pub struct GetCookiesCommand {
706 #[serde(default)]
708 pub urls: Vec<String>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
713#[serde(rename_all = "camelCase", deny_unknown_fields)]
714pub struct SetCookiesCommand {
715 pub cookies: Vec<SetCookieParam>,
716}
717
718#[derive(Debug, Clone, Default, Serialize, Deserialize)]
719#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
720#[serde(rename_all = "camelCase", deny_unknown_fields)]
721pub struct DeleteCookiesCommand {
722 #[serde(default)]
724 pub urls: Vec<String>,
725 #[serde(default)]
727 pub names: Vec<String>,
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize)]
731#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
732#[serde(rename_all = "camelCase")]
733pub struct ClickAndWaitForPopupCommand {
734 pub selector: String,
735 pub target: Option<TargetSpec>,
736 pub timeout_ms: u64,
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
740#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
741#[serde(rename_all = "camelCase")]
742pub struct ClickAndWaitForDownloadCommand {
743 pub selector: String,
744 pub target: Option<TargetSpec>,
745 pub timeout_ms: u64,
746}
747
748#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
749#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
750#[serde(rename_all = "camelCase")]
751pub struct TargetSpec {
752 pub css: Option<String>,
753 pub test_id: Option<String>,
754 pub role: Option<String>,
755 pub accessible_name: Option<String>,
756 pub label: Option<String>,
757 pub text: Option<TextMatch>,
758 #[serde(default)]
759 pub attributes: BTreeMap<String, String>,
760 #[serde(default)]
761 pub frame_path: Vec<Box<TargetSpec>>,
762 #[serde(default)]
763 pub shadow_path: Vec<Box<TargetSpec>>,
764 pub ordinal: Option<usize>,
765 #[serde(default)]
766 pub allow_best_match: bool,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
770#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
771#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
772pub enum TextMatch {
773 Exact(String),
774 Contains(String),
775 Regex(String),
776}
777
778#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
780#[serde(rename_all = "camelCase")]
781pub enum ElementState {
782 Attached,
783 Detached,
784 Visible,
785 Hidden,
786 Enabled,
787 Disabled,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
791#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
792#[serde(rename_all = "PascalCase")]
793pub enum NetworkResourceType {
794 Document,
795 Stylesheet,
796 Image,
797 Media,
798 Font,
799 Script,
800 TextTrack,
801 #[serde(rename = "XHR", alias = "Xhr")]
802 Xhr,
803 Fetch,
804 Prefetch,
805 EventSource,
806 WebSocket,
807 Manifest,
808 SignedExchange,
809 Ping,
810 #[serde(rename = "CSPViolationReport")]
811 CspViolationReport,
812 Preflight,
813 #[serde(rename = "FedCM")]
814 FedCm,
815 Other,
816}
817
818impl NetworkResourceType {
819 pub fn as_str(&self) -> &'static str {
820 match self {
821 Self::Document => "Document",
822 Self::Stylesheet => "Stylesheet",
823 Self::Image => "Image",
824 Self::Media => "Media",
825 Self::Font => "Font",
826 Self::Script => "Script",
827 Self::TextTrack => "TextTrack",
828 Self::Xhr => "XHR",
829 Self::Fetch => "Fetch",
830 Self::Prefetch => "Prefetch",
831 Self::EventSource => "EventSource",
832 Self::WebSocket => "WebSocket",
833 Self::Manifest => "Manifest",
834 Self::SignedExchange => "SignedExchange",
835 Self::Ping => "Ping",
836 Self::CspViolationReport => "CSPViolationReport",
837 Self::Preflight => "Preflight",
838 Self::FedCm => "FedCM",
839 Self::Other => "Other",
840 }
841 }
842}
843
844impl std::str::FromStr for NetworkResourceType {
845 type Err = String;
846
847 fn from_str(s: &str) -> Result<Self, Self::Err> {
848 match s {
849 "Document" | "document" => Ok(Self::Document),
850 "Stylesheet" | "stylesheet" => Ok(Self::Stylesheet),
851 "Image" | "image" => Ok(Self::Image),
852 "Media" | "media" => Ok(Self::Media),
853 "Font" | "font" => Ok(Self::Font),
854 "Script" | "script" => Ok(Self::Script),
855 "TextTrack" | "textTrack" | "texttrack" => Ok(Self::TextTrack),
856 "XHR" | "Xhr" | "xhr" => Ok(Self::Xhr),
857 "Fetch" | "fetch" => Ok(Self::Fetch),
858 "Prefetch" | "prefetch" => Ok(Self::Prefetch),
859 "EventSource" | "eventSource" | "eventsource" => Ok(Self::EventSource),
860 "WebSocket" | "webSocket" | "websocket" => Ok(Self::WebSocket),
861 "Manifest" | "manifest" => Ok(Self::Manifest),
862 "SignedExchange" | "signedExchange" => Ok(Self::SignedExchange),
863 "Ping" | "ping" => Ok(Self::Ping),
864 "CSPViolationReport" | "cspViolationReport" => Ok(Self::CspViolationReport),
865 "Preflight" | "preflight" => Ok(Self::Preflight),
866 "FedCM" | "FedCm" | "fedCM" => Ok(Self::FedCm),
867 "Other" | "other" => Ok(Self::Other),
868 other => Err(format!("unknown network resource type: {other}")),
869 }
870 }
871}
872
873#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
874#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
875#[serde(tag = "kind", rename_all = "camelCase")]
876pub enum WaitCondition {
877 Element {
878 target: Box<TargetSpec>,
879 state: ElementState,
880 },
881 Text {
882 target: Box<TargetSpec>,
883 matcher: TextMatch,
884 },
885 Value {
886 target: Box<TargetSpec>,
887 matcher: TextMatch,
888 },
889 Url {
890 matcher: TextMatch,
891 },
892 Document {
893 ready: WaitUntil,
894 },
895 NetworkQuiet {
896 #[serde(rename = "idleMs", alias = "idle_ms")]
897 idle_ms: u64,
898 #[serde(rename = "maxInFlight", alias = "max_in_flight")]
899 max_in_flight: usize,
900 #[serde(
901 default,
902 rename = "ignoreUrlSubstrings",
903 alias = "ignore_url_substrings",
904 skip_serializing_if = "Vec::is_empty"
905 )]
906 ignore_url_substrings: Vec<String>,
907 #[serde(
908 default,
909 rename = "ignoreResourceTypes",
910 alias = "ignore_resource_types",
911 skip_serializing_if = "Vec::is_empty"
912 )]
913 ignore_resource_types: Vec<NetworkResourceType>,
914 #[serde(
915 default,
916 rename = "ignoreLongLived",
917 alias = "ignore_long_lived",
918 skip_serializing_if = "std::ops::Not::not"
919 )]
920 ignore_long_lived: bool,
921 },
922}
923
924#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
925#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
926#[serde(rename_all = "camelCase")]
927pub struct WaitForCommand {
928 pub condition: WaitCondition,
929 pub timeout_ms: u64,
930}
931
932#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
933#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
934#[serde(tag = "kind", rename_all = "camelCase")]
935pub enum ScreenshotMode {
936 Viewport,
937 FullPage,
938 Element {
939 target: Box<TargetSpec>,
940 },
941 Clip {
942 x: f64,
943 y: f64,
944 width: f64,
945 height: f64,
946 },
947}
948
949#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
950#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
951#[serde(rename_all = "camelCase")]
952pub struct CaptureScreenshotCommand {
953 pub mode: ScreenshotMode,
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
957#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
958#[serde(rename_all = "camelCase")]
959pub struct SetFocusEmulationCommand {
960 pub enabled: bool,
961}
962
963#[derive(Debug, Clone, Serialize, Deserialize)]
964#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
965#[serde(rename_all = "camelCase")]
966pub struct SetEmulatedMediaCommand {
967 pub media: String,
968 pub features: BTreeMap<String, String>,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
972#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
973#[serde(rename_all = "camelCase")]
974pub struct EvaluateJavaScriptCommand {
975 pub expression: String,
976 pub timeout_ms: u64,
977 #[serde(default)]
978 pub await_promise: bool,
979}
980
981#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
984#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
985#[serde(rename_all = "camelCase")]
986pub struct ExecutionPolicy {
987 #[serde(default)]
988 pub javascript_evaluation: bool,
989 #[serde(default)]
990 pub vision_assist: bool,
991 #[serde(default)]
996 pub fingerprint: bool,
997 #[serde(default)]
1004 pub humanize: bool,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub vision_node: Option<String>,
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1016#[serde(rename_all = "camelCase")]
1017pub struct CreateSessionRequest {
1018 pub profile: String,
1019 pub proxy: Option<String>,
1020 #[serde(default)]
1021 pub execution_policy: ExecutionPolicy,
1022}
1023
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1027pub struct OpenPageRequest {
1028 pub session_id: SessionId,
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1033pub struct NavigationRequest {
1034 pub page_id: PageId,
1035 pub url: String,
1036 pub wait_until: Option<String>,
1037 pub timeout_ms: Option<u64>,
1038}
1039
1040#[derive(Debug, Clone, Serialize, Deserialize)]
1041#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1042pub struct ExtractRequest {
1043 pub page_id: PageId,
1044 pub fields: serde_json::Value,
1045}