1use std::cmp::Ordering;
2use std::collections::{BTreeMap, VecDeque};
3use std::error::Error as StdError;
4use std::fmt;
5
6use bt_dom::{
7 DomStore, ElementData, HTML_NAMESPACE_URI, MATHML_NAMESPACE_URI, NodeId, NodeKind,
8 SVG_NAMESPACE_URI,
9};
10use bt_script::{
11 ElementHandle, EventPhase, HostBindings, HtmlCollectionScope, HtmlCollectionTarget,
12 KeyboardEventInit, ListenerTarget, MediaQueryListState, NodeHandle, RadioNodeListTarget,
13 ScreenOrientationState, ScriptError, ScriptEventHandle, ScriptFunction, ScriptRuntime,
14 ScriptValue, StorageTarget,
15};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct SessionConfig {
19 pub url: String,
20 pub html: Option<String>,
21 pub local_storage: BTreeMap<String, String>,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25enum DocumentReadyState {
26 Loading,
27 Complete,
28}
29
30const MATCH_MEDIA_SEED_PREFIX: &str = "__browser_tester_match_media__";
31const OPEN_FAILURE_SEED_KEY: &str = "__browser_tester_open_failure__";
32const CLOSE_FAILURE_SEED_KEY: &str = "__browser_tester_close_failure__";
33const PRINT_FAILURE_SEED_KEY: &str = "__browser_tester_print_failure__";
34const SCROLL_FAILURE_SEED_KEY: &str = "__browser_tester_scroll_failure__";
35
36impl Default for SessionConfig {
37 fn default() -> Self {
38 Self {
39 url: "https://app.local/".to_string(),
40 html: None,
41 local_storage: BTreeMap::new(),
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ScheduledTimer {
48 pub id: u64,
49 pub at_ms: i64,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Scheduler {
54 now_ms: i64,
55 timers: Vec<ScheduledTimer>,
56 microtasks: usize,
57 next_timer_id: u64,
58 step_limit: usize,
59}
60
61impl Default for Scheduler {
62 fn default() -> Self {
63 Self {
64 now_ms: 0,
65 timers: Vec::new(),
66 microtasks: 0,
67 next_timer_id: 1,
68 step_limit: 10_000,
69 }
70 }
71}
72
73impl Scheduler {
74 pub fn now_ms(&self) -> i64 {
75 self.now_ms
76 }
77
78 pub fn advance_time(&mut self, delta_ms: i64) {
79 self.now_ms += delta_ms;
80 let _ = self.run_due_timers();
81 }
82
83 pub fn advance_time_to(&mut self, target_ms: i64) {
84 self.now_ms = self.now_ms.max(target_ms);
85 let _ = self.run_due_timers();
86 }
87
88 pub fn queue_timer(&mut self, at_ms: i64) -> u64 {
89 let id = self.next_timer_id;
90 self.next_timer_id += 1;
91 self.timers.push(ScheduledTimer { id, at_ms });
92 self.timers.sort_by_key(|timer| (timer.at_ms, timer.id));
93 id
94 }
95
96 pub fn cancel_timer(&mut self, id: u64) -> bool {
97 let before = self.timers.len();
98 self.timers.retain(|timer| timer.id != id);
99 before != self.timers.len()
100 }
101
102 pub fn pending_timers(&self) -> &[ScheduledTimer] {
103 &self.timers
104 }
105
106 pub fn run_due_timers(&mut self) -> Vec<ScheduledTimer> {
107 let split = self
108 .timers
109 .iter()
110 .position(|timer| timer.at_ms > self.now_ms)
111 .unwrap_or(self.timers.len());
112 self.timers.drain(..split).collect()
113 }
114
115 pub fn queue_microtask(&mut self) {
116 self.microtasks += 1;
117 }
118
119 pub fn microtask_count(&self) -> usize {
120 self.microtasks
121 }
122
123 pub fn flush(&mut self) {
124 while let Some(next_due) = self.timers.first().map(|timer| timer.at_ms) {
125 self.now_ms = self.now_ms.max(next_due);
126 let _ = self.run_due_timers();
127 }
128 self.microtasks = 0;
129 }
130
131 pub fn step_limit(&self) -> usize {
132 self.step_limit
133 }
134}
135
136#[derive(Clone, Debug, Default, PartialEq, Eq)]
137pub struct FetchResponseRule {
138 pub url: String,
139 pub status: u16,
140 pub body: String,
141}
142
143#[derive(Clone, Debug, Default, PartialEq, Eq)]
144pub struct FetchErrorRule {
145 pub url: String,
146 pub message: String,
147}
148
149#[derive(Clone, Debug, Default, PartialEq, Eq)]
150pub struct FetchCall {
151 pub url: String,
152}
153
154#[derive(Clone, Debug, Default, PartialEq, Eq)]
155pub struct FetchResponse {
156 pub url: String,
157 pub status: u16,
158 pub body: String,
159}
160
161#[derive(Clone, Debug, Default, PartialEq, Eq)]
162pub struct FetchMocks {
163 responses: Vec<FetchResponseRule>,
164 errors: Vec<FetchErrorRule>,
165 calls: Vec<FetchCall>,
166}
167
168impl FetchMocks {
169 pub fn respond_text(&mut self, url: impl Into<String>, status: u16, body: impl Into<String>) {
170 self.responses.push(FetchResponseRule {
171 url: url.into(),
172 status,
173 body: body.into(),
174 });
175 }
176
177 pub fn fail(&mut self, url: impl Into<String>, message: impl Into<String>) {
178 self.errors.push(FetchErrorRule {
179 url: url.into(),
180 message: message.into(),
181 });
182 }
183
184 pub fn record_call(&mut self, url: impl Into<String>) {
185 self.calls.push(FetchCall { url: url.into() });
186 }
187
188 pub fn responses(&self) -> &[FetchResponseRule] {
189 &self.responses
190 }
191
192 pub fn errors(&self) -> &[FetchErrorRule] {
193 &self.errors
194 }
195
196 pub fn calls(&self) -> &[FetchCall] {
197 &self.calls
198 }
199
200 pub fn reset(&mut self) {
201 self.responses.clear();
202 self.errors.clear();
203 self.calls.clear();
204 }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq)]
208pub struct DialogMocks {
209 confirm_queue: Vec<bool>,
210 prompt_queue: Vec<Option<String>>,
211 alert_messages: Vec<String>,
212 confirm_messages: Vec<String>,
213 prompt_messages: Vec<String>,
214}
215
216impl DialogMocks {
217 pub fn push_confirm(&mut self, value: bool) {
218 self.confirm_queue.push(value);
219 }
220
221 pub fn push_prompt(&mut self, value: Option<impl Into<String>>) {
222 self.prompt_queue.push(value.map(Into::into));
223 }
224
225 pub fn record_alert(&mut self, message: impl Into<String>) {
226 self.alert_messages.push(message.into());
227 }
228
229 pub fn record_confirm(&mut self, message: impl Into<String>) {
230 self.confirm_messages.push(message.into());
231 }
232
233 pub fn record_prompt(&mut self, message: impl Into<String>) {
234 self.prompt_messages.push(message.into());
235 }
236
237 pub fn confirm_queue(&self) -> &[bool] {
238 &self.confirm_queue
239 }
240
241 pub fn prompt_queue(&self) -> &[Option<String>] {
242 &self.prompt_queue
243 }
244
245 pub fn alert_messages(&self) -> &[String] {
246 &self.alert_messages
247 }
248
249 pub fn confirm_messages(&self) -> &[String] {
250 &self.confirm_messages
251 }
252
253 pub fn prompt_messages(&self) -> &[String] {
254 &self.prompt_messages
255 }
256
257 pub fn reset(&mut self) {
258 self.confirm_queue.clear();
259 self.prompt_queue.clear();
260 self.alert_messages.clear();
261 self.confirm_messages.clear();
262 self.prompt_messages.clear();
263 }
264}
265
266#[derive(Clone, Debug, Default, PartialEq, Eq)]
267pub struct ClipboardMocks {
268 seeded_text: Option<String>,
269 writes: Vec<String>,
270 read_error: Option<String>,
271 write_error: Option<String>,
272}
273
274impl ClipboardMocks {
275 pub fn seed_text(&mut self, value: impl Into<String>) {
276 self.seeded_text = Some(value.into());
277 }
278
279 pub fn seeded_text(&self) -> Option<&str> {
280 self.seeded_text.as_deref()
281 }
282
283 pub fn set_read_error(&mut self, error: Option<&str>) {
284 self.read_error = error.map(std::string::ToString::to_string);
285 }
286
287 pub fn set_write_error(&mut self, error: Option<&str>) {
288 self.write_error = error.map(std::string::ToString::to_string);
289 }
290
291 pub fn clear_errors(&mut self) {
292 self.read_error = None;
293 self.write_error = None;
294 }
295
296 pub fn read_error(&self) -> Option<&str> {
297 self.read_error.as_deref()
298 }
299
300 pub fn write_error(&self) -> Option<&str> {
301 self.write_error.as_deref()
302 }
303
304 pub fn record_write(&mut self, value: impl Into<String>) {
305 self.writes.push(value.into());
306 }
307
308 pub fn writes(&self) -> &[String] {
309 &self.writes
310 }
311
312 pub fn reset(&mut self) {
313 self.seeded_text = None;
314 self.writes.clear();
315 self.read_error = None;
316 self.write_error = None;
317 }
318}
319
320#[derive(Clone, Debug, Default, PartialEq, Eq)]
321pub struct LocationMocks {
322 current_url: Option<String>,
323 navigations: Vec<String>,
324}
325
326impl LocationMocks {
327 pub fn set_current(&mut self, url: impl Into<String>) {
328 self.current_url = Some(url.into());
329 }
330
331 pub fn record_navigation(&mut self, url: impl Into<String>) {
332 self.navigations.push(url.into());
333 }
334
335 pub fn current_url(&self) -> Option<&str> {
336 self.current_url.as_deref()
337 }
338
339 pub fn navigations(&self) -> &[String] {
340 &self.navigations
341 }
342
343 pub fn reset(&mut self) {
344 self.current_url = None;
345 self.navigations.clear();
346 }
347}
348
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub struct MatchMediaCall {
351 pub query: String,
352}
353
354#[derive(Clone, Debug, PartialEq, Eq)]
355pub struct MatchMediaListenerCall {
356 pub query: String,
357 pub method: String,
358}
359
360#[derive(Clone, Debug, Default, PartialEq, Eq)]
361pub struct MatchMediaMocks {
362 matches: Vec<(String, bool)>,
363 calls: Vec<MatchMediaCall>,
364 listener_calls: Vec<MatchMediaListenerCall>,
365}
366
367impl MatchMediaMocks {
368 pub fn respond_matches(&mut self, query: impl Into<String>, matches: bool) {
369 self.matches.push((query.into(), matches));
370 }
371
372 pub fn record_call(&mut self, query: impl Into<String>) {
373 self.calls.push(MatchMediaCall {
374 query: query.into(),
375 });
376 }
377
378 pub fn record_listener_call(&mut self, query: impl Into<String>, method: impl Into<String>) {
379 self.listener_calls.push(MatchMediaListenerCall {
380 query: query.into(),
381 method: method.into(),
382 });
383 }
384
385 pub fn resolve(&mut self, query: &str) -> Result<MediaQueryListState, String> {
386 let query = query.trim();
387 if query.is_empty() {
388 return Err("matchMedia() requires a non-empty media query".to_string());
389 }
390
391 self.record_call(query.to_string());
392
393 if let Some((_, matches)) = self
394 .matches
395 .iter()
396 .rev()
397 .find(|(rule_query, _)| rule_query == query)
398 {
399 return Ok(MediaQueryListState::new(query, *matches));
400 }
401
402 Err(format!("no matchMedia mock configured for `{query}`"))
403 }
404
405 pub fn calls(&self) -> &[MatchMediaCall] {
406 &self.calls
407 }
408
409 pub fn take_calls(&mut self) -> Vec<MatchMediaCall> {
410 std::mem::take(&mut self.calls)
411 }
412
413 pub fn listener_calls(&self) -> &[MatchMediaListenerCall] {
414 &self.listener_calls
415 }
416
417 pub fn reset(&mut self) {
418 self.matches.clear();
419 self.calls.clear();
420 self.listener_calls.clear();
421 }
422}
423
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct OpenCall {
426 pub url: Option<String>,
427 pub target: Option<String>,
428 pub features: Option<String>,
429}
430
431impl Default for OpenCall {
432 fn default() -> Self {
433 Self {
434 url: None,
435 target: None,
436 features: None,
437 }
438 }
439}
440
441#[derive(Clone, Debug, Default, PartialEq, Eq)]
442pub struct OpenMocks {
443 failure: Option<String>,
444 calls: Vec<OpenCall>,
445}
446
447impl OpenMocks {
448 pub fn fail(&mut self, message: impl Into<String>) {
449 self.failure = Some(message.into());
450 }
451
452 pub fn clear_failure(&mut self) {
453 self.failure = None;
454 }
455
456 fn invoke(
457 &mut self,
458 url: Option<&str>,
459 target: Option<&str>,
460 features: Option<&str>,
461 ) -> Result<(), String> {
462 self.record_call(url, target, features);
463 if let Some(message) = &self.failure {
464 return Err(message.clone());
465 }
466
467 Ok(())
468 }
469
470 pub fn record_call(&mut self, url: Option<&str>, target: Option<&str>, features: Option<&str>) {
471 self.calls.push(OpenCall {
472 url: url.map(str::to_string),
473 target: target.map(str::to_string),
474 features: features.map(str::to_string),
475 });
476 }
477
478 pub fn calls(&self) -> &[OpenCall] {
479 &self.calls
480 }
481
482 pub fn reset(&mut self) {
483 self.failure = None;
484 self.calls.clear();
485 }
486}
487
488#[derive(Clone, Debug, Default, PartialEq, Eq)]
489pub struct CloseCall;
490
491#[derive(Clone, Debug, Default, PartialEq, Eq)]
492pub struct CloseMocks {
493 failure: Option<String>,
494 calls: Vec<CloseCall>,
495}
496
497impl CloseMocks {
498 pub fn fail(&mut self, message: impl Into<String>) {
499 self.failure = Some(message.into());
500 }
501
502 pub fn clear_failure(&mut self) {
503 self.failure = None;
504 }
505
506 fn invoke(&mut self) -> Result<(), String> {
507 self.record_call();
508 if let Some(message) = &self.failure {
509 return Err(message.clone());
510 }
511
512 Ok(())
513 }
514
515 pub fn record_call(&mut self) {
516 self.calls.push(CloseCall);
517 }
518
519 pub fn calls(&self) -> &[CloseCall] {
520 &self.calls
521 }
522
523 pub fn reset(&mut self) {
524 self.failure = None;
525 self.calls.clear();
526 }
527}
528
529#[derive(Clone, Debug, Default, PartialEq, Eq)]
530pub struct PrintCall;
531
532#[derive(Clone, Debug, Default, PartialEq, Eq)]
533pub struct PrintMocks {
534 failure: Option<String>,
535 calls: Vec<PrintCall>,
536}
537
538impl PrintMocks {
539 pub fn fail(&mut self, message: impl Into<String>) {
540 self.failure = Some(message.into());
541 }
542
543 pub fn clear_failure(&mut self) {
544 self.failure = None;
545 }
546
547 fn invoke(&mut self) -> Result<(), String> {
548 self.record_call();
549 if let Some(message) = &self.failure {
550 return Err(message.clone());
551 }
552
553 Ok(())
554 }
555
556 pub fn record_call(&mut self) {
557 self.calls.push(PrintCall);
558 }
559
560 pub fn calls(&self) -> &[PrintCall] {
561 &self.calls
562 }
563
564 pub fn take(&mut self) -> Vec<PrintCall> {
565 std::mem::take(&mut self.calls)
566 }
567
568 pub fn reset(&mut self) {
569 self.failure = None;
570 self.calls.clear();
571 }
572}
573
574#[derive(Clone, Copy, Debug, PartialEq, Eq)]
575pub enum ScrollMethod {
576 To,
577 By,
578}
579
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub struct ScrollCall {
582 pub method: ScrollMethod,
583 pub x: i64,
584 pub y: i64,
585}
586
587#[derive(Clone, Debug, Default, PartialEq, Eq)]
588pub struct ScrollMocks {
589 failure: Option<String>,
590 calls: Vec<ScrollCall>,
591}
592
593impl ScrollMocks {
594 pub fn fail(&mut self, message: impl Into<String>) {
595 self.failure = Some(message.into());
596 }
597
598 pub fn clear_failure(&mut self) {
599 self.failure = None;
600 }
601
602 fn invoke(&mut self, method: ScrollMethod, x: i64, y: i64) -> Result<(), String> {
603 self.record_call(method, x, y);
604 if let Some(message) = &self.failure {
605 return Err(message.clone());
606 }
607
608 Ok(())
609 }
610
611 pub fn record_call(&mut self, method: ScrollMethod, x: i64, y: i64) {
612 self.calls.push(ScrollCall { method, x, y });
613 }
614
615 pub fn calls(&self) -> &[ScrollCall] {
616 &self.calls
617 }
618
619 pub fn reset(&mut self) {
620 self.failure = None;
621 self.calls.clear();
622 }
623}
624
625#[derive(Clone, Debug, Default, PartialEq, Eq)]
626pub struct DownloadCapture {
627 pub file_name: String,
628 pub filename: Option<String>,
629 pub mime_type: Option<String>,
630 pub bytes: Vec<u8>,
631}
632
633#[derive(Clone, Debug, Default, PartialEq, Eq)]
634pub struct DownloadMocks {
635 artifacts: Vec<DownloadCapture>,
636}
637
638impl DownloadMocks {
639 pub fn capture(&mut self, file_name: impl Into<String>, bytes: impl Into<Vec<u8>>) {
640 let file_name = file_name.into();
641 self.artifacts.push(DownloadCapture {
642 filename: Some(file_name.clone()),
643 mime_type: None,
644 file_name,
645 bytes: bytes.into(),
646 });
647 }
648
649 pub fn artifacts(&self) -> &[DownloadCapture] {
650 &self.artifacts
651 }
652
653 pub fn take(&mut self) -> Vec<DownloadCapture> {
654 std::mem::take(&mut self.artifacts)
655 }
656
657 pub fn reset(&mut self) {
658 self.artifacts.clear();
659 }
660}
661
662#[derive(Clone, Debug, Default, PartialEq, Eq)]
663pub struct FileInputSelection {
664 pub selector: String,
665 pub files: Vec<String>,
666}
667
668#[derive(Clone, Debug, Default, PartialEq, Eq)]
669pub struct FileInputMocks {
670 selections: Vec<FileInputSelection>,
671}
672
673impl FileInputMocks {
674 pub fn set_files(
675 &mut self,
676 selector: impl Into<String>,
677 files: impl IntoIterator<Item = impl Into<String>>,
678 ) {
679 self.selections.push(FileInputSelection {
680 selector: selector.into(),
681 files: files.into_iter().map(Into::into).collect(),
682 });
683 }
684
685 pub fn selections(&self) -> &[FileInputSelection] {
686 &self.selections
687 }
688
689 pub fn reset(&mut self) {
690 self.selections.clear();
691 }
692}
693
694#[derive(Clone, Debug, Default, PartialEq, Eq)]
695pub struct StorageSeeds {
696 local: BTreeMap<String, String>,
697 session: BTreeMap<String, String>,
698}
699
700impl StorageSeeds {
701 pub fn seed_local(&mut self, key: impl Into<String>, value: impl Into<String>) {
702 self.local.insert(key.into(), value.into());
703 }
704
705 pub fn seed_session(&mut self, key: impl Into<String>, value: impl Into<String>) {
706 self.session.insert(key.into(), value.into());
707 }
708
709 pub fn local(&self) -> &BTreeMap<String, String> {
710 &self.local
711 }
712
713 pub fn session(&self) -> &BTreeMap<String, String> {
714 &self.session
715 }
716
717 pub fn reset(&mut self) {
718 self.local.clear();
719 self.session.clear();
720 }
721}
722
723#[derive(Clone, Debug, PartialEq, Eq)]
724pub enum SessionError {
725 HtmlParse(String),
726 Script(ScriptError),
727 Selector(String),
728 Dom(String),
729 Event(String),
730 Mock(String),
731}
732
733impl fmt::Display for SessionError {
734 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735 match self {
736 Self::HtmlParse(message) => write!(f, "HTML parse error: {message}"),
737 Self::Script(err) => write!(f, "Script error: {err}"),
738 Self::Selector(message) => write!(f, "Selector error: {message}"),
739 Self::Dom(message) => write!(f, "DOM error: {message}"),
740 Self::Event(message) => write!(f, "Event error: {message}"),
741 Self::Mock(message) => write!(f, "Mock error: {message}"),
742 }
743 }
744}
745
746impl StdError for SessionError {
747 fn source(&self) -> Option<&(dyn StdError + 'static)> {
748 match self {
749 Self::HtmlParse(_) => None,
750 Self::Script(err) => Some(err),
751 Self::Selector(_) | Self::Dom(_) | Self::Event(_) | Self::Mock(_) => None,
752 }
753 }
754}
755
756impl From<ScriptError> for SessionError {
757 fn from(value: ScriptError) -> Self {
758 Self::Script(value)
759 }
760}
761
762#[derive(Clone, Debug, PartialEq)]
763struct ScriptListenerRecord {
764 target: SessionEventTarget,
765 event_type: String,
766 capture: bool,
767 handler: ScriptFunction,
768}
769
770#[derive(Clone, Debug, PartialEq)]
771enum ScheduledScriptTimerKind {
772 AnimationFrame,
773 Timeout,
774 Interval { interval_ms: i64 },
775}
776
777#[derive(Clone, Debug, PartialEq)]
778struct ScheduledScriptTimerRecord {
779 kind: ScheduledScriptTimerKind,
780 handler: ScriptFunction,
781}
782
783#[derive(Clone, Copy, Debug, PartialEq, Eq)]
784enum SessionEventTarget {
785 Window,
786 Document,
787 Element(NodeId),
788}
789
790#[derive(Clone, Debug, PartialEq, Eq)]
791enum DefaultActionKind {
792 CheckboxToggle,
793 SubmitButton,
794}
795
796#[derive(Clone, Debug, Default, PartialEq, Eq)]
797struct DispatchOutcome {
798 default_prevented: bool,
799}
800
801#[derive(Clone, Debug, Default, PartialEq, Eq)]
802pub struct MockRegistry {
803 fetch: FetchMocks,
804 dialogs: DialogMocks,
805 clipboard: ClipboardMocks,
806 location: LocationMocks,
807 match_media: MatchMediaMocks,
808 open: OpenMocks,
809 close: CloseMocks,
810 print: PrintMocks,
811 scroll: ScrollMocks,
812 downloads: DownloadMocks,
813 file_input: FileInputMocks,
814 storage: StorageSeeds,
815}
816
817impl MockRegistry {
818 pub fn fetch(&self) -> &FetchMocks {
819 &self.fetch
820 }
821
822 pub fn fetch_mut(&mut self) -> &mut FetchMocks {
823 &mut self.fetch
824 }
825
826 pub fn dialogs(&self) -> &DialogMocks {
827 &self.dialogs
828 }
829
830 pub fn dialogs_mut(&mut self) -> &mut DialogMocks {
831 &mut self.dialogs
832 }
833
834 pub fn clipboard(&self) -> &ClipboardMocks {
835 &self.clipboard
836 }
837
838 pub fn clipboard_mut(&mut self) -> &mut ClipboardMocks {
839 &mut self.clipboard
840 }
841
842 pub fn location(&self) -> &LocationMocks {
843 &self.location
844 }
845
846 pub fn location_mut(&mut self) -> &mut LocationMocks {
847 &mut self.location
848 }
849
850 pub fn match_media(&self) -> &MatchMediaMocks {
851 &self.match_media
852 }
853
854 pub fn match_media_mut(&mut self) -> &mut MatchMediaMocks {
855 &mut self.match_media
856 }
857
858 pub fn open(&self) -> &OpenMocks {
859 &self.open
860 }
861
862 pub fn open_mut(&mut self) -> &mut OpenMocks {
863 &mut self.open
864 }
865
866 pub fn close(&self) -> &CloseMocks {
867 &self.close
868 }
869
870 pub fn close_mut(&mut self) -> &mut CloseMocks {
871 &mut self.close
872 }
873
874 pub fn print(&self) -> &PrintMocks {
875 &self.print
876 }
877
878 pub fn print_mut(&mut self) -> &mut PrintMocks {
879 &mut self.print
880 }
881
882 pub fn scroll(&self) -> &ScrollMocks {
883 &self.scroll
884 }
885
886 pub fn scroll_mut(&mut self) -> &mut ScrollMocks {
887 &mut self.scroll
888 }
889
890 pub fn downloads(&self) -> &DownloadMocks {
891 &self.downloads
892 }
893
894 pub fn downloads_mut(&mut self) -> &mut DownloadMocks {
895 &mut self.downloads
896 }
897
898 pub fn file_input(&self) -> &FileInputMocks {
899 &self.file_input
900 }
901
902 pub fn file_input_mut(&mut self) -> &mut FileInputMocks {
903 &mut self.file_input
904 }
905
906 pub fn storage(&self) -> &StorageSeeds {
907 &self.storage
908 }
909
910 pub fn storage_mut(&mut self) -> &mut StorageSeeds {
911 &mut self.storage
912 }
913
914 pub fn reset_all(&mut self) {
915 self.fetch.reset();
916 self.dialogs.reset();
917 self.clipboard.reset();
918 self.location.reset();
919 self.match_media.reset();
920 self.open.reset();
921 self.close.reset();
922 self.print.reset();
923 self.scroll.reset();
924 self.downloads.reset();
925 self.file_input.reset();
926 self.storage.reset();
927 }
928}
929
930#[derive(Clone, Debug, PartialEq, Eq)]
931pub struct DebugState {
932 trace_enabled: bool,
933 trace_events: bool,
934 trace_timers: bool,
935 trace_logs: VecDeque<String>,
936 trace_log_limit: usize,
937 trace_to_stderr: bool,
938}
939
940impl Default for DebugState {
941 fn default() -> Self {
942 Self {
943 trace_enabled: false,
944 trace_events: true,
945 trace_timers: true,
946 trace_logs: VecDeque::new(),
947 trace_log_limit: 10_000,
948 trace_to_stderr: true,
949 }
950 }
951}
952
953impl DebugState {
954 pub fn enable_trace(&mut self) {
955 self.trace_enabled = true;
956 }
957
958 pub fn set_trace_enabled(&mut self, enabled: bool) {
959 self.trace_enabled = enabled;
960 }
961
962 pub fn set_trace_stderr(&mut self, enabled: bool) {
963 self.trace_to_stderr = enabled;
964 }
965
966 pub fn set_trace_events(&mut self, enabled: bool) {
967 self.trace_events = enabled;
968 }
969
970 pub fn set_trace_timers(&mut self, enabled: bool) {
971 self.trace_timers = enabled;
972 }
973
974 pub fn set_trace_log_limit(&mut self, max_entries: usize) -> Result<(), ScriptError> {
975 if max_entries == 0 {
976 return Err(ScriptError::new(
977 "set_trace_log_limit requires at least 1 entry",
978 ));
979 }
980 self.trace_log_limit = max_entries;
981 while self.trace_logs.len() > self.trace_log_limit {
982 self.trace_logs.pop_front();
983 }
984 Ok(())
985 }
986
987 pub fn take_trace_logs(&mut self) -> Vec<String> {
988 self.trace_logs.drain(..).collect()
989 }
990
991 pub fn trace_enabled(&self) -> bool {
992 self.trace_enabled
993 }
994
995 pub fn trace_log_limit(&self) -> usize {
996 self.trace_log_limit
997 }
998}
999
1000#[derive(Clone, Debug)]
1001pub struct Session {
1002 dom: DomStore,
1003 scheduler: Scheduler,
1004 mocks: MockRegistry,
1005 script: ScriptRuntime,
1006 config: SessionConfig,
1007 rng_state: u64,
1008 debug: DebugState,
1009 script_event_listeners: Vec<ScriptListenerRecord>,
1010 scheduled_script_timers: BTreeMap<u64, ScheduledScriptTimerRecord>,
1011 default_actions: Vec<DefaultActionKind>,
1012 focused_node: Option<NodeId>,
1013 current_script: Option<NodeId>,
1014 document_ready_state: DocumentReadyState,
1015 document_design_mode: String,
1016 window_name: String,
1017 cookie_jar: BTreeMap<String, String>,
1018 history_entries: Vec<String>,
1019 history_states: Vec<Option<String>>,
1020 history_index: usize,
1021 history_scroll_restoration: String,
1022 scroll_x: i64,
1023 scroll_y: i64,
1024}
1025
1026impl Session {
1027 pub fn new(config: SessionConfig) -> Result<Self, SessionError> {
1028 let mut dom = DomStore::new_empty();
1029 if let Some(html) = &config.html {
1030 dom.bootstrap_html(html.clone())
1031 .map_err(SessionError::HtmlParse)?;
1032 }
1033
1034 let mut mocks = MockRegistry::default();
1035 for (key, value) in &config.local_storage {
1036 if let Some(query) = key.strip_prefix(MATCH_MEDIA_SEED_PREFIX) {
1037 let matches = value.parse::<bool>().map_err(|_| {
1038 SessionError::Mock(format!(
1039 "invalid matchMedia seed value for `{query}`: {value}"
1040 ))
1041 })?;
1042 mocks
1043 .match_media_mut()
1044 .respond_matches(query.to_string(), matches);
1045 } else if key == OPEN_FAILURE_SEED_KEY {
1046 mocks.open_mut().fail(value.clone());
1047 } else if key == CLOSE_FAILURE_SEED_KEY {
1048 mocks.close_mut().fail(value.clone());
1049 } else if key == PRINT_FAILURE_SEED_KEY {
1050 mocks.print_mut().fail(value.clone());
1051 } else if key == SCROLL_FAILURE_SEED_KEY {
1052 mocks.scroll_mut().fail(value.clone());
1053 } else {
1054 mocks.storage_mut().seed_local(key.clone(), value.clone());
1055 }
1056 }
1057 let initial_url = config.url.clone();
1058 mocks.location_mut().set_current(config.url.clone());
1059 dom.set_target_fragment(Self::fragment_identifier_from_url(&config.url));
1060
1061 let mut session = Self {
1062 dom,
1063 scheduler: Scheduler::default(),
1064 mocks,
1065 script: ScriptRuntime::default(),
1066 config,
1067 rng_state: 0x9E37_79B9_7F4A_7C15,
1068 debug: DebugState::default(),
1069 script_event_listeners: Vec::new(),
1070 scheduled_script_timers: BTreeMap::new(),
1071 default_actions: vec![
1072 DefaultActionKind::CheckboxToggle,
1073 DefaultActionKind::SubmitButton,
1074 ],
1075 focused_node: None,
1076 current_script: None,
1077 document_ready_state: DocumentReadyState::Loading,
1078 document_design_mode: "off".to_string(),
1079 window_name: String::new(),
1080 cookie_jar: BTreeMap::new(),
1081 history_entries: vec![initial_url],
1082 history_states: vec![None],
1083 history_index: 0,
1084 history_scroll_restoration: "auto".to_string(),
1085 scroll_x: 0,
1086 scroll_y: 0,
1087 };
1088 session.bootstrap_inline_scripts()?;
1089 session.document_ready_state = DocumentReadyState::Complete;
1090 Ok(session)
1091 }
1092
1093 pub fn dom(&self) -> &DomStore {
1094 &self.dom
1095 }
1096
1097 pub fn dom_mut(&mut self) -> &mut DomStore {
1098 &mut self.dom
1099 }
1100
1101 pub fn scheduler(&self) -> &Scheduler {
1102 &self.scheduler
1103 }
1104
1105 pub fn scheduler_mut(&mut self) -> &mut Scheduler {
1106 &mut self.scheduler
1107 }
1108
1109 pub fn advance_time(&mut self, delta_ms: i64) -> Result<usize, SessionError> {
1110 self.scheduler.now_ms = self.scheduler.now_ms.saturating_add(delta_ms);
1111 let due_timers = self.scheduler.run_due_timers();
1112 self.run_due_script_timers(due_timers)
1113 }
1114
1115 pub fn flush(&mut self) -> Result<usize, SessionError> {
1116 let mut executed = 0usize;
1117 let mut steps = 0usize;
1118 while let Some(next_due) = self.scheduler.timers.first().map(|timer| timer.at_ms) {
1119 self.scheduler.now_ms = self.scheduler.now_ms.max(next_due);
1120 let due_timers = self.scheduler.run_due_timers();
1121 executed = executed.saturating_add(self.run_due_script_timers(due_timers)?);
1122 steps += 1;
1123 if steps > self.scheduler.step_limit() {
1124 return Err(SessionError::Mock(
1125 "timer flush exceeded the scheduler step limit".to_string(),
1126 ));
1127 }
1128 }
1129 self.scheduler.microtasks = 0;
1130 Ok(executed)
1131 }
1132
1133 pub fn run_due_timers(&mut self) -> Result<usize, SessionError> {
1134 let due_timers = self.scheduler.run_due_timers();
1135 self.run_due_script_timers(due_timers)
1136 }
1137
1138 pub fn mocks(&self) -> &MockRegistry {
1139 &self.mocks
1140 }
1141
1142 pub fn mocks_mut(&mut self) -> &mut MockRegistry {
1143 &mut self.mocks
1144 }
1145
1146 pub fn script(&self) -> &ScriptRuntime {
1147 &self.script
1148 }
1149
1150 pub fn script_mut(&mut self) -> &mut ScriptRuntime {
1151 &mut self.script
1152 }
1153
1154 pub fn config(&self) -> &SessionConfig {
1155 &self.config
1156 }
1157
1158 pub fn debug(&self) -> &DebugState {
1159 &self.debug
1160 }
1161
1162 pub fn debug_mut(&mut self) -> &mut DebugState {
1163 &mut self.debug
1164 }
1165
1166 pub fn enable_trace(&mut self, enabled: bool) {
1167 self.debug.set_trace_enabled(enabled);
1168 }
1169
1170 pub fn set_trace_stderr(&mut self, enabled: bool) {
1171 self.debug.set_trace_stderr(enabled);
1172 }
1173
1174 pub fn set_trace_events(&mut self, enabled: bool) {
1175 self.debug.set_trace_events(enabled);
1176 }
1177
1178 pub fn set_trace_timers(&mut self, enabled: bool) {
1179 self.debug.set_trace_timers(enabled);
1180 }
1181
1182 pub fn set_trace_log_limit(&mut self, max_entries: usize) -> Result<(), SessionError> {
1183 self.debug
1184 .set_trace_log_limit(max_entries)
1185 .map_err(SessionError::Script)
1186 }
1187
1188 pub fn take_trace_logs(&mut self) -> Vec<String> {
1189 self.debug.take_trace_logs()
1190 }
1191
1192 pub fn set_random_seed(&mut self, seed: u64) {
1193 self.rng_state = if seed == 0 {
1194 0xA5A5_A5A5_A5A5_A5A5
1195 } else {
1196 seed
1197 };
1198 }
1199
1200 pub fn set_timer_step_limit(&mut self, max_steps: usize) -> Result<(), SessionError> {
1201 if max_steps == 0 {
1202 return Err(SessionError::Script(ScriptError::new(
1203 "set_timer_step_limit requires at least 1 step",
1204 )));
1205 }
1206 self.scheduler.step_limit = max_steps;
1207 Ok(())
1208 }
1209
1210 pub fn dispatch_node(&mut self, node_id: NodeId, event_type: &str) -> Result<(), SessionError> {
1211 let event_type = event_type.trim();
1212 if event_type.is_empty() {
1213 return Err(SessionError::Event(
1214 "event type must not be empty".to_string(),
1215 ));
1216 }
1217
1218 self.dispatch_dom_event_for_target(
1219 SessionEventTarget::Element(node_id),
1220 event_type,
1221 true,
1222 true,
1223 None,
1224 )?;
1225 Ok(())
1226 }
1227
1228 pub fn dispatch_keyboard(
1229 &mut self,
1230 selector: &str,
1231 event_type: &str,
1232 init: KeyboardEventInit,
1233 ) -> Result<(), SessionError> {
1234 let event_type = event_type.trim();
1235 if event_type.is_empty() {
1236 return Err(SessionError::Event(
1237 "event type must not be empty".to_string(),
1238 ));
1239 }
1240
1241 let target = self.resolve_keyboard_target(selector)?;
1242 let bubbles = matches!(target, SessionEventTarget::Element(_));
1243 self.dispatch_dom_event_for_target(target, event_type, bubbles, false, Some(&init))?;
1244 Ok(())
1245 }
1246
1247 pub fn click_node(&mut self, node_id: NodeId) -> Result<(), SessionError> {
1248 self.ensure_element_node(node_id)?;
1249 let outcome = self.dispatch_dom_event(node_id, "click", true, true)?;
1250 if !outcome.default_prevented {
1251 self.run_click_default_actions(node_id)?;
1252 }
1253 Ok(())
1254 }
1255
1256 pub fn type_text_node(&mut self, node_id: NodeId, text: &str) -> Result<(), SessionError> {
1257 self.ensure_element_node(node_id)?;
1258 self.dom
1259 .set_form_control_value(node_id, text)
1260 .map_err(SessionError::Dom)?;
1261 self.dispatch_dom_event(node_id, "input", true, false)?;
1262 Ok(())
1263 }
1264
1265 pub fn set_checked_node(&mut self, node_id: NodeId, checked: bool) -> Result<(), SessionError> {
1266 self.ensure_element_node(node_id)?;
1267 self.dom
1268 .set_form_control_checked(node_id, checked)
1269 .map_err(SessionError::Dom)?;
1270 self.dispatch_dom_event(node_id, "input", true, false)?;
1271 self.dispatch_dom_event(node_id, "change", true, false)?;
1272 Ok(())
1273 }
1274
1275 pub fn set_select_value_node(
1276 &mut self,
1277 node_id: NodeId,
1278 value: &str,
1279 ) -> Result<(), SessionError> {
1280 self.ensure_element_node(node_id)?;
1281 self.dom
1282 .set_select_value(node_id, value)
1283 .map_err(SessionError::Dom)?;
1284 self.dispatch_dom_event(node_id, "input", true, false)?;
1285 self.dispatch_dom_event(node_id, "change", true, false)?;
1286 Ok(())
1287 }
1288
1289 pub fn focus_node(&mut self, node_id: NodeId) -> Result<(), SessionError> {
1290 self.ensure_element_node(node_id)?;
1291 if self.focused_node == Some(node_id) {
1292 return Ok(());
1293 }
1294
1295 if let Some(previous) = self.focused_node.take() {
1296 self.dom.set_focused_node(None);
1297 self.dispatch_dom_event(previous, "blur", false, false)?;
1298 }
1299
1300 self.focused_node = Some(node_id);
1301 self.dom.set_focused_node(Some(node_id));
1302 self.dispatch_dom_event(node_id, "focus", false, false)?;
1303 Ok(())
1304 }
1305
1306 pub fn blur_node(&mut self, node_id: NodeId) -> Result<(), SessionError> {
1307 self.ensure_element_node(node_id)?;
1308 if self.focused_node != Some(node_id) {
1309 return Ok(());
1310 }
1311
1312 self.focused_node = None;
1313 self.dom.set_focused_node(None);
1314 self.dispatch_dom_event(node_id, "blur", false, false)?;
1315 Ok(())
1316 }
1317
1318 pub fn submit_node(&mut self, node_id: NodeId) -> Result<(), SessionError> {
1319 self.ensure_element_node(node_id)?;
1320 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
1321 return Err(SessionError::Dom(format!("invalid node id: {:?}", node_id)));
1322 };
1323 if matches!(&node.kind, NodeKind::Element(element) if element.tag_name == "form") {
1324 self.dispatch_dom_event(node_id, "submit", true, true)?;
1325 return Ok(());
1326 }
1327
1328 let Some(form_id) = self.find_associated_form(node_id) else {
1329 return Err(SessionError::Dom(format!(
1330 "submit is only supported on <form> elements or submit controls with an associated form, not {:?}",
1331 node_id
1332 )));
1333 };
1334
1335 self.dispatch_dom_event(form_id, "submit", true, true)?;
1336 Ok(())
1337 }
1338
1339 pub fn text_content_for_node(&self, node_id: NodeId) -> String {
1340 self.dom.text_content_for_node(node_id)
1341 }
1342
1343 pub fn value_for_node(&self, node_id: NodeId) -> String {
1344 self.dom.value_for_node(node_id)
1345 }
1346
1347 pub fn checked_for_node(&self, node_id: NodeId) -> Option<bool> {
1348 self.dom.checked_for_node(node_id)
1349 }
1350
1351 pub fn alert(&mut self, message: &str) {
1352 self.mocks.dialogs_mut().record_alert(message.to_string());
1353 }
1354
1355 pub fn confirm(&mut self, message: &str) -> Result<bool, SessionError> {
1356 let dialogs = self.mocks.dialogs_mut();
1357 dialogs.record_confirm(message.to_string());
1358 if dialogs.confirm_queue.is_empty() {
1359 return Err(SessionError::Mock(
1360 "confirm() requires a queued response".to_string(),
1361 ));
1362 }
1363 Ok(dialogs.confirm_queue.remove(0))
1364 }
1365
1366 pub fn prompt(&mut self, message: &str) -> Result<Option<String>, SessionError> {
1367 let dialogs = self.mocks.dialogs_mut();
1368 dialogs.record_prompt(message.to_string());
1369 if dialogs.prompt_queue.is_empty() {
1370 return Err(SessionError::Mock(
1371 "prompt() requires a queued response".to_string(),
1372 ));
1373 }
1374 Ok(dialogs.prompt_queue.remove(0))
1375 }
1376
1377 pub fn read_clipboard(&self) -> Result<String, SessionError> {
1378 if let Some(reason) = self.mocks.clipboard().read_error() {
1379 return Err(SessionError::Mock(reason.to_string()));
1380 }
1381 self.mocks
1382 .clipboard()
1383 .seeded_text()
1384 .map(ToString::to_string)
1385 .ok_or_else(|| SessionError::Mock("clipboard text has not been seeded".to_string()))
1386 }
1387
1388 pub fn write_clipboard(&mut self, text: &str) -> Result<(), SessionError> {
1389 if let Some(reason) = self.mocks.clipboard().write_error() {
1390 return Err(SessionError::Mock(reason.to_string()));
1391 }
1392 let clipboard = self.mocks.clipboard_mut();
1393 clipboard.record_write(text.to_string());
1394 clipboard.seed_text(text.to_string());
1395 Ok(())
1396 }
1397
1398 pub fn capture_download(
1399 &mut self,
1400 file_name: &str,
1401 bytes: impl Into<Vec<u8>>,
1402 ) -> Result<(), SessionError> {
1403 if file_name.trim().is_empty() {
1404 return Err(SessionError::Mock(
1405 "capture_download() requires a non-empty file name".to_string(),
1406 ));
1407 }
1408
1409 self.mocks
1410 .downloads_mut()
1411 .capture(file_name.to_string(), bytes);
1412 Ok(())
1413 }
1414
1415 pub fn take_downloads(&mut self) -> Vec<DownloadCapture> {
1416 self.mocks.downloads_mut().take()
1417 }
1418
1419 pub fn print(&mut self) -> Result<(), SessionError> {
1420 self.mocks.print_mut().invoke().map_err(SessionError::Mock)
1421 }
1422
1423 pub fn scroll_to(&mut self, x: i64, y: i64) -> Result<(), SessionError> {
1424 self.mocks
1425 .scroll_mut()
1426 .invoke(ScrollMethod::To, x, y)
1427 .map_err(SessionError::Mock)?;
1428 self.scroll_x = x;
1429 self.scroll_y = y;
1430 Ok(())
1431 }
1432
1433 pub fn scroll_by(&mut self, x: i64, y: i64) -> Result<(), SessionError> {
1434 self.mocks
1435 .scroll_mut()
1436 .invoke(ScrollMethod::By, x, y)
1437 .map_err(SessionError::Mock)?;
1438 self.scroll_x += x;
1439 self.scroll_y += y;
1440 Ok(())
1441 }
1442
1443 pub fn close(&mut self) -> Result<(), SessionError> {
1444 self.mocks.close_mut().invoke().map_err(SessionError::Mock)
1445 }
1446
1447 pub fn open(
1448 &mut self,
1449 url: Option<&str>,
1450 target: Option<&str>,
1451 features: Option<&str>,
1452 ) -> Result<(), SessionError> {
1453 self.mocks
1454 .open_mut()
1455 .invoke(url, target, features)
1456 .map_err(SessionError::Mock)
1457 }
1458
1459 pub fn fetch(&mut self, url: &str) -> Result<FetchResponse, SessionError> {
1460 let url = url.trim();
1461 if url.is_empty() {
1462 return Err(SessionError::Mock(
1463 "fetch() requires a non-empty URL".to_string(),
1464 ));
1465 }
1466
1467 self.mocks.fetch_mut().record_call(url.to_string());
1468
1469 if let Some(error) = self
1470 .mocks
1471 .fetch()
1472 .errors()
1473 .iter()
1474 .rev()
1475 .find(|rule| rule.url == url)
1476 {
1477 return Err(SessionError::Mock(error.message.clone()));
1478 }
1479
1480 if let Some(response) = self
1481 .mocks
1482 .fetch()
1483 .responses()
1484 .iter()
1485 .rev()
1486 .find(|rule| rule.url == url)
1487 {
1488 return Ok(FetchResponse {
1489 url: url.to_string(),
1490 status: response.status,
1491 body: response.body.clone(),
1492 });
1493 }
1494
1495 Err(SessionError::Mock(format!(
1496 "no fetch mock configured for `{url}`"
1497 )))
1498 }
1499
1500 pub fn navigate(&mut self, url: &str) -> Result<(), SessionError> {
1501 let url = url.trim();
1502 if url.is_empty() {
1503 return Err(SessionError::Mock(
1504 "navigate() requires a non-empty URL".to_string(),
1505 ));
1506 }
1507
1508 if self.history_entries.is_empty() {
1509 self.history_entries.push(url.to_string());
1510 self.history_states.push(None);
1511 self.history_index = 0;
1512 } else {
1513 let next_index = self.history_index.saturating_add(1);
1514 self.history_entries.truncate(next_index);
1515 self.history_states.truncate(next_index);
1516 self.history_entries.push(url.to_string());
1517 self.history_states.push(None);
1518 self.history_index = self.history_entries.len().saturating_sub(1);
1519 }
1520
1521 self.apply_history_navigation(url);
1522 Ok(())
1523 }
1524
1525 pub fn window_history_push_state(
1526 &mut self,
1527 state: Option<&str>,
1528 url: Option<&str>,
1529 ) -> Result<(), SessionError> {
1530 let next_url = self.resolve_history_url(url);
1531 let next_state = state.map(str::to_string);
1532
1533 if self.history_entries.is_empty() {
1534 self.history_entries.push(next_url.clone());
1535 self.history_states.push(next_state);
1536 self.history_index = 0;
1537 } else {
1538 let next_index = self.history_index.saturating_add(1);
1539 self.history_entries.truncate(next_index);
1540 self.history_states.truncate(next_index);
1541 self.history_entries.push(next_url.clone());
1542 self.history_states.push(next_state);
1543 self.history_index = self.history_entries.len().saturating_sub(1);
1544 }
1545
1546 self.apply_history_url_update(&next_url);
1547 Ok(())
1548 }
1549
1550 pub fn window_history_replace_state(
1551 &mut self,
1552 state: Option<&str>,
1553 url: Option<&str>,
1554 ) -> Result<(), SessionError> {
1555 let next_url = self.resolve_history_url(url);
1556 let next_state = state.map(str::to_string);
1557
1558 if self.history_entries.is_empty() {
1559 self.history_entries.push(next_url.clone());
1560 self.history_states.push(next_state);
1561 self.history_index = 0;
1562 } else {
1563 self.history_entries[self.history_index] = next_url.clone();
1564 self.history_states[self.history_index] = next_state;
1565 }
1566
1567 self.apply_history_url_update(&next_url);
1568 Ok(())
1569 }
1570
1571 pub fn window_history_back(&mut self) -> Result<(), SessionError> {
1572 if self.history_index == 0 {
1573 return Ok(());
1574 }
1575
1576 let target_index = self.history_index - 1;
1577 self.history_index = target_index;
1578 let url = self.history_entries[target_index].clone();
1579 self.apply_history_navigation(&url);
1580 Ok(())
1581 }
1582
1583 pub fn window_history_forward(&mut self) -> Result<(), SessionError> {
1584 if self.history_index + 1 >= self.history_entries.len() {
1585 return Ok(());
1586 }
1587
1588 let target_index = self.history_index + 1;
1589 self.history_index = target_index;
1590 let url = self.history_entries[target_index].clone();
1591 self.apply_history_navigation(&url);
1592 Ok(())
1593 }
1594
1595 pub fn window_history_go(&mut self, delta: i64) -> Result<(), SessionError> {
1596 if delta == 0 || self.history_entries.is_empty() {
1597 return Ok(());
1598 }
1599
1600 let current = self.history_index as i64;
1601 let max_index = (self.history_entries.len() - 1) as i64;
1602 let target = current.saturating_add(delta).clamp(0, max_index) as usize;
1603 if target == self.history_index {
1604 return Ok(());
1605 }
1606
1607 self.history_index = target;
1608 let url = self.history_entries[target].clone();
1609 self.apply_history_navigation(&url);
1610 Ok(())
1611 }
1612
1613 pub fn set_files_node(
1614 &mut self,
1615 node_id: NodeId,
1616 selector: &str,
1617 files: impl IntoIterator<Item = impl Into<String>>,
1618 ) -> Result<(), SessionError> {
1619 self.ensure_element_node(node_id)?;
1620 let files: Vec<String> = files.into_iter().map(Into::into).collect();
1621 self.dom
1622 .set_file_input_files(node_id, files.clone())
1623 .map_err(SessionError::Dom)?;
1624 self.mocks
1625 .file_input_mut()
1626 .set_files(selector.to_string(), files);
1627 self.dispatch_dom_event(node_id, "input", true, false)?;
1628 self.dispatch_dom_event(node_id, "change", true, false)?;
1629 Ok(())
1630 }
1631
1632 fn ensure_element_node(&self, node_id: NodeId) -> Result<(), SessionError> {
1633 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
1634 return Err(SessionError::Dom(format!("invalid node id: {:?}", node_id)));
1635 };
1636
1637 match &node.kind {
1638 NodeKind::Element(_) => Ok(()),
1639 _ => Err(SessionError::Dom(format!(
1640 "node {:?} is not an element",
1641 node_id
1642 ))),
1643 }
1644 }
1645
1646 fn apply_history_navigation(&mut self, url: &str) {
1647 self.apply_history_url_update(url);
1648 self.scroll_x = 0;
1649 self.scroll_y = 0;
1650 }
1651
1652 fn apply_history_url_update(&mut self, url: &str) {
1653 let location = self.mocks.location_mut();
1654 location.set_current(url.to_string());
1655 location.record_navigation(url.to_string());
1656 self.dom
1657 .set_target_fragment(Self::fragment_identifier_from_url(url));
1658 }
1659
1660 fn resolve_history_url(&self, url: Option<&str>) -> String {
1661 match url.map(str::trim) {
1662 Some(url) if !url.is_empty() => url.to_string(),
1663 _ => self
1664 .history_entries
1665 .get(self.history_index)
1666 .cloned()
1667 .unwrap_or_else(|| self.config.url.clone()),
1668 }
1669 }
1670
1671 fn dispatch_dom_event(
1672 &mut self,
1673 node_id: NodeId,
1674 event_type: &str,
1675 bubbles: bool,
1676 cancelable: bool,
1677 ) -> Result<DispatchOutcome, SessionError> {
1678 self.dispatch_dom_event_for_target(
1679 SessionEventTarget::Element(node_id),
1680 event_type,
1681 bubbles,
1682 cancelable,
1683 None,
1684 )
1685 }
1686
1687 fn dispatch_dom_event_for_target(
1688 &mut self,
1689 target: SessionEventTarget,
1690 event_type: &str,
1691 bubbles: bool,
1692 cancelable: bool,
1693 keyboard_init: Option<&KeyboardEventInit>,
1694 ) -> Result<DispatchOutcome, SessionError> {
1695 let event = match keyboard_init {
1696 Some(init) => ScriptEventHandle::new_keyboard(
1697 event_type.to_string(),
1698 Self::script_listener_target(target),
1699 bubbles,
1700 cancelable,
1701 init,
1702 ),
1703 None => ScriptEventHandle::new(
1704 event_type.to_string(),
1705 Self::script_listener_target(target),
1706 bubbles,
1707 cancelable,
1708 ),
1709 };
1710 let ancestors = self.event_ancestor_targets_for_target(target);
1711
1712 for target in ancestors.iter().rev() {
1713 self.run_event_listeners(*target, event_type, true, EventPhase::Capturing, &event)?;
1714 if event.immediate_propagation_stopped() || event.propagation_stopped() {
1715 event.set_current_target(None);
1716 event.set_phase(EventPhase::None);
1717 let outcome = DispatchOutcome {
1718 default_prevented: event.default_prevented(),
1719 };
1720 self.trace_event_line(format!("[event] {event_type} done {event_type}"));
1721 return Ok(outcome);
1722 }
1723 }
1724
1725 self.run_event_listeners(target, event_type, true, EventPhase::AtTarget, &event)?;
1726 if event.immediate_propagation_stopped() {
1727 event.set_current_target(None);
1728 event.set_phase(EventPhase::None);
1729 let outcome = DispatchOutcome {
1730 default_prevented: event.default_prevented(),
1731 };
1732 self.trace_event_line(format!("[event] {event_type} done {event_type}"));
1733 return Ok(outcome);
1734 }
1735
1736 self.run_event_listeners(target, event_type, false, EventPhase::AtTarget, &event)?;
1737 if event.immediate_propagation_stopped() || event.propagation_stopped() {
1738 event.set_current_target(None);
1739 event.set_phase(EventPhase::None);
1740 let outcome = DispatchOutcome {
1741 default_prevented: event.default_prevented(),
1742 };
1743 self.trace_event_line(format!("[event] {event_type} done {event_type}"));
1744 return Ok(outcome);
1745 }
1746
1747 if bubbles {
1748 for target in &ancestors {
1749 self.run_event_listeners(*target, event_type, false, EventPhase::Bubbling, &event)?;
1750 if event.immediate_propagation_stopped() || event.propagation_stopped() {
1751 break;
1752 }
1753 }
1754 }
1755
1756 event.set_current_target(None);
1757 event.set_phase(EventPhase::None);
1758 let outcome = DispatchOutcome {
1759 default_prevented: event.default_prevented(),
1760 };
1761 self.trace_event_line(format!("[event] {event_type} done {event_type}"));
1762 Ok(outcome)
1763 }
1764
1765 fn run_event_listeners(
1766 &mut self,
1767 target: SessionEventTarget,
1768 event_type: &str,
1769 capture: bool,
1770 phase: EventPhase,
1771 event: &ScriptEventHandle,
1772 ) -> Result<(), SessionError> {
1773 let listeners: Vec<ScriptListenerRecord> = self
1774 .script_event_listeners
1775 .iter()
1776 .filter(|listener| {
1777 listener.target == target
1778 && listener.event_type == event_type
1779 && listener.capture == capture
1780 })
1781 .cloned()
1782 .collect();
1783
1784 for (index, listener) in listeners.iter().enumerate() {
1785 if event.immediate_propagation_stopped() {
1786 break;
1787 }
1788
1789 event.set_current_target(Some(Self::script_listener_target(target)));
1790 event.set_phase(phase);
1791 let source_name = format!("event:{event_type}:{}:{index}", Self::phase_label(phase));
1792 let bindings = Self::listener_bindings(target, &listener.handler, event);
1793 self.eval_script_source_with_bindings(
1794 &listener.handler.body_source,
1795 &source_name,
1796 bindings,
1797 )?;
1798 }
1799
1800 Ok(())
1801 }
1802
1803 fn eval_script_source(&mut self, source: &str, source_name: &str) -> Result<(), SessionError> {
1804 self.eval_script_source_with_bindings(source, source_name, BTreeMap::new())
1805 }
1806
1807 fn eval_script_source_with_bindings(
1808 &mut self,
1809 source: &str,
1810 source_name: &str,
1811 initial_bindings: BTreeMap<String, ScriptValue>,
1812 ) -> Result<(), SessionError> {
1813 let mut script = std::mem::take(&mut self.script);
1814 let result = script
1815 .eval_program_with_bindings(source, source_name, self, initial_bindings)
1816 .map_err(SessionError::Script);
1817 self.script = script;
1818 result
1819 }
1820
1821 fn run_click_default_actions(&mut self, node_id: NodeId) -> Result<(), SessionError> {
1822 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
1823 return Err(SessionError::Dom(format!("invalid node id: {:?}", node_id)));
1824 };
1825 let NodeKind::Element(element) = &node.kind else {
1826 return Err(SessionError::Dom(format!(
1827 "node {:?} is not an element",
1828 node_id
1829 )));
1830 };
1831
1832 let tag_name = element.tag_name.clone();
1833 let input_type = element.attributes.get("type").cloned();
1834
1835 let actions = self.default_actions.clone();
1836 for action in actions {
1837 match action {
1838 DefaultActionKind::CheckboxToggle
1839 if tag_name == "input" && is_checkable_input_type(input_type.as_deref()) =>
1840 {
1841 let checked = self.checked_for_node(node_id).unwrap_or(false);
1842 self.dom
1843 .set_form_control_checked(node_id, !checked)
1844 .map_err(SessionError::Dom)?;
1845 if matches!(input_type.as_deref(), Some("checkbox")) {
1846 self.dom
1847 .set_form_control_indeterminate(node_id, false)
1848 .map_err(SessionError::Dom)?;
1849 }
1850 self.dispatch_dom_event(node_id, "input", true, false)?;
1851 self.dispatch_dom_event(node_id, "change", true, false)?;
1852 }
1853 DefaultActionKind::SubmitButton
1854 if is_submit_control(tag_name.as_str(), input_type.as_deref()) =>
1855 {
1856 if let Some(form_id) = self.find_associated_form(node_id) {
1857 self.dispatch_dom_event(form_id, "submit", true, true)?;
1858 }
1859 }
1860 _ => {}
1861 }
1862 }
1863
1864 Ok(())
1865 }
1866
1867 fn find_associated_form(&self, mut node_id: NodeId) -> Option<NodeId> {
1868 loop {
1869 let node = self.dom.nodes().get(node_id.index() as usize)?;
1870 match &node.kind {
1871 NodeKind::Element(element) if element.tag_name == "form" => return Some(node_id),
1872 NodeKind::Element(_) | NodeKind::Text(_) | NodeKind::Comment(_) => {
1873 node_id = node.parent?;
1874 }
1875 NodeKind::Document => return None,
1876 }
1877 }
1878 }
1879
1880 fn resolve_keyboard_target(&self, selector: &str) -> Result<SessionEventTarget, SessionError> {
1881 let selector = selector.trim();
1882 if selector.is_empty() {
1883 return Err(SessionError::Selector(
1884 "selector must not be empty".to_string(),
1885 ));
1886 }
1887
1888 match selector {
1889 "window" => Ok(SessionEventTarget::Window),
1890 "document" => Ok(SessionEventTarget::Document),
1891 _ => {
1892 let matches = self.dom.select(selector).map_err(SessionError::Selector)?;
1893 let Some(node_id) = matches.first().copied() else {
1894 return Err(SessionError::Dom(format!(
1895 "selector `{selector}` did not match any elements"
1896 )));
1897 };
1898 self.ensure_element_node(node_id)?;
1899 Ok(SessionEventTarget::Element(node_id))
1900 }
1901 }
1902 }
1903
1904 fn event_ancestor_targets_for_target(
1905 &self,
1906 target: SessionEventTarget,
1907 ) -> Vec<SessionEventTarget> {
1908 match target {
1909 SessionEventTarget::Window => Vec::new(),
1910 SessionEventTarget::Document => vec![SessionEventTarget::Window],
1911 SessionEventTarget::Element(node_id) => self.event_ancestor_targets(node_id),
1912 }
1913 }
1914
1915 fn event_ancestor_targets(&self, node_id: NodeId) -> Vec<SessionEventTarget> {
1916 let mut targets = Vec::new();
1917 let mut current = self
1918 .dom
1919 .nodes()
1920 .get(node_id.index() as usize)
1921 .and_then(|node| node.parent);
1922
1923 while let Some(parent_id) = current {
1924 let Some(parent) = self.dom.nodes().get(parent_id.index() as usize) else {
1925 break;
1926 };
1927
1928 match &parent.kind {
1929 NodeKind::Document => {
1930 targets.push(SessionEventTarget::Document);
1931 break;
1932 }
1933 NodeKind::Element(_) | NodeKind::Text(_) | NodeKind::Comment(_) => {
1934 targets.push(SessionEventTarget::Element(parent_id));
1935 current = parent.parent;
1936 }
1937 }
1938 }
1939
1940 targets.push(SessionEventTarget::Window);
1941 targets
1942 }
1943
1944 fn listener_bindings(
1945 target: SessionEventTarget,
1946 handler: &ScriptFunction,
1947 event: &ScriptEventHandle,
1948 ) -> BTreeMap<String, ScriptValue> {
1949 let mut bindings = (*handler.captured_bindings).clone();
1950 bindings.insert("event".to_string(), ScriptValue::Event(event.clone()));
1951 bindings.insert("this".to_string(), Self::listener_this_value(target));
1952
1953 for (index, param) in handler.params.iter().enumerate() {
1954 if index == 0 {
1955 bindings.insert(param.clone(), ScriptValue::Event(event.clone()));
1956 } else {
1957 bindings.insert(param.clone(), ScriptValue::Undefined);
1958 }
1959 }
1960
1961 bindings
1962 }
1963
1964 fn listener_this_value(target: SessionEventTarget) -> ScriptValue {
1965 match target {
1966 SessionEventTarget::Window => ScriptValue::Window,
1967 SessionEventTarget::Document => ScriptValue::Document,
1968 SessionEventTarget::Element(node_id) => {
1969 ScriptValue::Element(Self::node_id_to_handle(node_id))
1970 }
1971 }
1972 }
1973
1974 fn script_listener_target(target: SessionEventTarget) -> ListenerTarget {
1975 match target {
1976 SessionEventTarget::Window => ListenerTarget::Window,
1977 SessionEventTarget::Document => ListenerTarget::Document,
1978 SessionEventTarget::Element(node_id) => {
1979 ListenerTarget::Element(Self::node_id_to_handle(node_id))
1980 }
1981 }
1982 }
1983
1984 fn phase_label(phase: EventPhase) -> &'static str {
1985 match phase {
1986 EventPhase::None => "none",
1987 EventPhase::Capturing => "capture",
1988 EventPhase::AtTarget => "target",
1989 EventPhase::Bubbling => "bubble",
1990 }
1991 }
1992
1993 fn bootstrap_inline_scripts(&mut self) -> Result<(), SessionError> {
1994 let sources = self.collect_inline_script_sources()?;
1995 for (index, (script_node_id, source)) in sources.iter().enumerate() {
1996 let previous_current_script = self.current_script;
1997 self.current_script = Some(*script_node_id);
1998 let result = self.eval_script_source(source, &format!("inline-script-{index}"));
1999 self.current_script = previous_current_script;
2000 result?;
2001 }
2002 Ok(())
2003 }
2004
2005 fn collect_inline_script_sources(&self) -> Result<Vec<(NodeId, String)>, SessionError> {
2006 let mut sources = Vec::new();
2007 self.collect_inline_script_sources_from(self.dom.document_id(), &mut sources)?;
2008 Ok(sources)
2009 }
2010
2011 fn collect_inline_script_sources_from(
2012 &self,
2013 node_id: NodeId,
2014 sources: &mut Vec<(NodeId, String)>,
2015 ) -> Result<(), SessionError> {
2016 let node = &self.dom.nodes()[node_id.index() as usize];
2017 if let NodeKind::Element(element) = &node.kind {
2018 if element.tag_name == "script" {
2019 if element.attributes.contains_key("src") {
2020 return Err(SessionError::Script(ScriptError::new(
2021 "external <script src=...> tags are not supported in this workspace yet",
2022 )));
2023 }
2024
2025 let source = self.dom.text_content_for_node(node_id);
2026 if !source.trim().is_empty() {
2027 sources.push((node_id, source));
2028 }
2029 }
2030 }
2031
2032 for child in &node.children {
2033 self.collect_inline_script_sources_from(*child, sources)?;
2034 }
2035
2036 Ok(())
2037 }
2038
2039 fn node_id_to_handle(node_id: NodeId) -> ElementHandle {
2040 ElementHandle::new(((node_id.generation() as u64) << 32) | node_id.index() as u64)
2041 }
2042
2043 fn node_id_to_node_handle(node_id: NodeId) -> NodeHandle {
2044 NodeHandle::new(((node_id.generation() as u64) << 32) | node_id.index() as u64)
2045 }
2046
2047 fn element_handle_to_node_id(handle: ElementHandle) -> NodeId {
2048 let raw = handle.raw();
2049 NodeId::new(raw as u32, (raw >> 32) as u32)
2050 }
2051
2052 fn node_contains_id(dom: &DomStore, container: NodeId, candidate: NodeId) -> bool {
2053 let mut current = Some(candidate);
2054 while let Some(node_id) = current {
2055 if node_id == container {
2056 return true;
2057 }
2058 current = dom
2059 .nodes()
2060 .get(node_id.index() as usize)
2061 .and_then(|record| record.parent);
2062 }
2063 false
2064 }
2065
2066 fn node_root_id(dom: &DomStore, node: NodeId) -> Option<NodeId> {
2067 let mut current = Some(node);
2068 while let Some(node_id) = current {
2069 let Some(record) = dom.nodes().get(node_id.index() as usize) else {
2070 return None;
2071 };
2072 current = record.parent;
2073 if current.is_none() {
2074 return Some(node_id);
2075 }
2076 }
2077 None
2078 }
2079
2080 fn node_preorder_index_id(dom: &DomStore, root: NodeId, target: NodeId) -> Option<usize> {
2081 let mut index = 0usize;
2082 let mut stack = vec![root];
2083
2084 while let Some(node_id) = stack.pop() {
2085 if node_id == target {
2086 return Some(index);
2087 }
2088
2089 index += 1;
2090 let Some(record) = dom.nodes().get(node_id.index() as usize) else {
2091 continue;
2092 };
2093
2094 for child in record.children.iter().rev() {
2095 stack.push(*child);
2096 }
2097 }
2098
2099 None
2100 }
2101
2102 fn node_compare_document_position_id(dom: &DomStore, node: NodeId, other: NodeId) -> u16 {
2103 const DISCONNECTED: u16 = 0x01;
2104 const PRECEDING: u16 = 0x02;
2105 const FOLLOWING: u16 = 0x04;
2106 const CONTAINS: u16 = 0x08;
2107 const CONTAINED_BY: u16 = 0x10;
2108 const IMPLEMENTATION_SPECIFIC: u16 = 0x20;
2109
2110 if node == other {
2111 return 0;
2112 }
2113
2114 let Some(node_root) = Self::node_root_id(dom, node) else {
2115 return DISCONNECTED | IMPLEMENTATION_SPECIFIC | FOLLOWING;
2116 };
2117 let Some(other_root) = Self::node_root_id(dom, other) else {
2118 return DISCONNECTED | IMPLEMENTATION_SPECIFIC | PRECEDING;
2119 };
2120
2121 if node_root != other_root {
2122 return match node_root.cmp(&other_root) {
2123 Ordering::Less => DISCONNECTED | IMPLEMENTATION_SPECIFIC | FOLLOWING,
2124 Ordering::Greater => DISCONNECTED | IMPLEMENTATION_SPECIFIC | PRECEDING,
2125 Ordering::Equal => 0,
2126 };
2127 }
2128
2129 if Self::node_contains_id(dom, node, other) {
2130 return CONTAINED_BY | FOLLOWING;
2131 }
2132
2133 if Self::node_contains_id(dom, other, node) {
2134 return CONTAINS | PRECEDING;
2135 }
2136
2137 let node_index = Self::node_preorder_index_id(dom, node_root, node);
2138 let other_index = Self::node_preorder_index_id(dom, node_root, other);
2139 match (node_index, other_index) {
2140 (Some(node_index), Some(other_index)) if node_index < other_index => FOLLOWING,
2141 (Some(_), Some(_)) => PRECEDING,
2142 _ => DISCONNECTED | IMPLEMENTATION_SPECIFIC | FOLLOWING,
2143 }
2144 }
2145
2146 fn node_has_child_nodes_id(dom: &DomStore, node: NodeId) -> bool {
2147 dom.nodes()
2148 .get(node.index() as usize)
2149 .is_some_and(|record| !record.children.is_empty())
2150 }
2151
2152 fn node_is_equal_node_id(dom: &DomStore, node: NodeId, other: NodeId) -> bool {
2153 let Some(node_record) = dom.nodes().get(node.index() as usize) else {
2154 return false;
2155 };
2156 let Some(other_record) = dom.nodes().get(other.index() as usize) else {
2157 return false;
2158 };
2159
2160 match (&node_record.kind, &other_record.kind) {
2161 (NodeKind::Document, NodeKind::Document) => {
2162 Self::node_children_equal_id(dom, &node_record.children, &other_record.children)
2163 }
2164 (NodeKind::Element(left), NodeKind::Element(right)) => {
2165 left == right
2166 && Self::node_children_equal_id(
2167 dom,
2168 &node_record.children,
2169 &other_record.children,
2170 )
2171 }
2172 (NodeKind::Text(left), NodeKind::Text(right)) => left == right,
2173 (NodeKind::Comment(left), NodeKind::Comment(right)) => left == right,
2174 _ => false,
2175 }
2176 }
2177
2178 fn node_children_equal_id(dom: &DomStore, left: &[NodeId], right: &[NodeId]) -> bool {
2179 left.len() == right.len()
2180 && left
2181 .iter()
2182 .zip(right.iter())
2183 .all(|(left_child, right_child)| {
2184 Self::node_is_equal_node_id(dom, *left_child, *right_child)
2185 })
2186 }
2187
2188 fn template_content_is_equal_node_id(dom: &DomStore, left: NodeId, right: NodeId) -> bool {
2189 let Some(left_record) = dom.nodes().get(left.index() as usize) else {
2190 return false;
2191 };
2192 let Some(right_record) = dom.nodes().get(right.index() as usize) else {
2193 return false;
2194 };
2195
2196 matches!(&left_record.kind, NodeKind::Element(_))
2197 && matches!(&right_record.kind, NodeKind::Element(_))
2198 && Self::node_children_equal_id(dom, &left_record.children, &right_record.children)
2199 }
2200
2201 fn fragment_identifier_from_url(url: &str) -> Option<String> {
2202 let fragment = url.split_once('#')?.1;
2203 if fragment.is_empty() {
2204 None
2205 } else {
2206 Some(fragment.to_string())
2207 }
2208 }
2209
2210 fn query_selector_handle(
2211 &self,
2212 scope: Option<NodeId>,
2213 selector: &str,
2214 ) -> Result<Option<ElementHandle>, ScriptError> {
2215 Ok(self
2216 .query_selector_handles(scope, selector)?
2217 .into_iter()
2218 .next())
2219 }
2220
2221 fn query_selector_handles(
2222 &self,
2223 scope: Option<NodeId>,
2224 selector: &str,
2225 ) -> Result<Vec<ElementHandle>, ScriptError> {
2226 let scope_root = scope.or_else(|| self.dom.root_element_id());
2227 let matches = self
2228 .dom
2229 .select_with_scope(selector, scope_root)
2230 .map_err(ScriptError::new)?;
2231 Ok(matches
2232 .into_iter()
2233 .filter(|node_id| match scope {
2234 None => true,
2235 Some(scope_id) => self.is_descendant_of(*node_id, scope_id),
2236 })
2237 .map(Self::node_id_to_handle)
2238 .collect())
2239 }
2240
2241 fn element_matches_selector(
2242 &self,
2243 node_id: NodeId,
2244 selector: &str,
2245 ) -> Result<bool, ScriptError> {
2246 let matches = self
2247 .dom
2248 .select_with_scope(selector, Some(node_id))
2249 .map_err(ScriptError::new)?;
2250 Ok(matches.contains(&node_id))
2251 }
2252
2253 fn element_closest_selector(
2254 &self,
2255 node_id: NodeId,
2256 selector: &str,
2257 ) -> Result<Option<ElementHandle>, ScriptError> {
2258 let matches = self
2259 .dom
2260 .select_with_scope(selector, Some(node_id))
2261 .map_err(ScriptError::new)?;
2262 let mut current = Some(node_id);
2263
2264 while let Some(candidate) = current {
2265 if matches.contains(&candidate) {
2266 return Ok(Some(Self::node_id_to_handle(candidate)));
2267 }
2268
2269 current = self
2270 .dom
2271 .nodes()
2272 .get(candidate.index() as usize)
2273 .and_then(|node| node.parent);
2274 }
2275
2276 Ok(None)
2277 }
2278
2279 fn collection_root_for_scope(
2280 &self,
2281 scope: &HtmlCollectionScope,
2282 ) -> Result<NodeId, ScriptError> {
2283 match scope {
2284 HtmlCollectionScope::Document => Ok(self.dom.document_id()),
2285 HtmlCollectionScope::Element(element) => self.node_id_for_handle(*element),
2286 HtmlCollectionScope::Node(node) => self.node_id_for_node_handle(*node),
2287 }
2288 }
2289
2290 fn collect_descendant_elements_matching<F>(
2291 &self,
2292 node_id: NodeId,
2293 collected: &mut Vec<NodeId>,
2294 matches: &F,
2295 ) where
2296 F: Fn(&ElementData) -> bool,
2297 {
2298 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2299 return;
2300 };
2301
2302 for child in &node.children {
2303 self.collect_descendant_elements_matching_inner(*child, collected, matches);
2304 }
2305 }
2306
2307 fn collect_descendant_elements_matching_inner<F>(
2308 &self,
2309 node_id: NodeId,
2310 collected: &mut Vec<NodeId>,
2311 matches: &F,
2312 ) where
2313 F: Fn(&ElementData) -> bool,
2314 {
2315 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2316 return;
2317 };
2318
2319 if let NodeKind::Element(element) = &node.kind {
2320 if matches(element) {
2321 collected.push(node_id);
2322 }
2323 }
2324
2325 for child in &node.children {
2326 self.collect_descendant_elements_matching_inner(*child, collected, matches);
2327 }
2328 }
2329
2330 fn direct_child_elements_matching<F>(
2331 &self,
2332 node_id: NodeId,
2333 matches: &F,
2334 ) -> Result<Vec<NodeId>, ScriptError>
2335 where
2336 F: Fn(&ElementData) -> bool,
2337 {
2338 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2339 return Err(ScriptError::new("invalid node id"));
2340 };
2341
2342 Ok(node
2343 .children
2344 .iter()
2345 .copied()
2346 .filter_map(|child_id| {
2347 let Some(child) = self.dom.nodes().get(child_id.index() as usize) else {
2348 return None;
2349 };
2350 let NodeKind::Element(element) = &child.kind else {
2351 return None;
2352 };
2353 matches(element).then_some(child_id)
2354 })
2355 .collect())
2356 }
2357
2358 fn elements_by_tag_name(
2359 &self,
2360 scope: &HtmlCollectionScope,
2361 tag_name: &str,
2362 ) -> Result<Vec<ElementHandle>, ScriptError> {
2363 let root = self.collection_root_for_scope(scope)?;
2364 let mut collected = Vec::new();
2365 self.collect_descendant_elements_matching(
2366 root,
2367 &mut collected,
2368 &|element: &ElementData| {
2369 tag_name == "*" || element.tag_name.eq_ignore_ascii_case(tag_name)
2370 },
2371 );
2372 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2373 }
2374
2375 fn elements_by_tag_name_ns(
2376 &self,
2377 scope: &HtmlCollectionScope,
2378 namespace_uri: &str,
2379 local_name: &str,
2380 ) -> Result<Vec<ElementHandle>, ScriptError> {
2381 let root = self.collection_root_for_scope(scope)?;
2382 let mut collected = Vec::new();
2383 self.collect_descendant_elements_matching(
2384 root,
2385 &mut collected,
2386 &|element: &ElementData| {
2387 Self::matches_namespace_uri(element, namespace_uri)
2388 && (local_name == "*" || element.local_name.eq_ignore_ascii_case(local_name))
2389 },
2390 );
2391 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2392 }
2393
2394 fn named_item_for_tag_name_collection(
2395 &self,
2396 scope: &HtmlCollectionScope,
2397 tag_name: &str,
2398 name: &str,
2399 ) -> Result<Option<ElementHandle>, ScriptError> {
2400 let root = self.collection_root_for_scope(scope)?;
2401 let mut collected = Vec::new();
2402 self.collect_descendant_elements_matching(
2403 root,
2404 &mut collected,
2405 &|element: &ElementData| {
2406 tag_name == "*" || element.tag_name.eq_ignore_ascii_case(tag_name)
2407 },
2408 );
2409 Ok(self.first_named_item_in_nodes(&collected, name))
2410 }
2411
2412 fn named_item_for_tag_name_ns_collection(
2413 &self,
2414 scope: &HtmlCollectionScope,
2415 namespace_uri: &str,
2416 local_name: &str,
2417 name: &str,
2418 ) -> Result<Option<ElementHandle>, ScriptError> {
2419 let root = self.collection_root_for_scope(scope)?;
2420 let mut collected = Vec::new();
2421 self.collect_descendant_elements_matching(
2422 root,
2423 &mut collected,
2424 &|element: &ElementData| {
2425 Self::matches_namespace_uri(element, namespace_uri)
2426 && (local_name == "*" || element.local_name.eq_ignore_ascii_case(local_name))
2427 },
2428 );
2429 Ok(self.first_named_item_in_nodes(&collected, name))
2430 }
2431
2432 fn elements_by_class_name(
2433 &self,
2434 scope: &HtmlCollectionScope,
2435 class_names: &str,
2436 ) -> Result<Vec<ElementHandle>, ScriptError> {
2437 let class_tokens = Self::ordered_class_names(class_names);
2438 if class_tokens.is_empty() {
2439 return Ok(Vec::new());
2440 }
2441
2442 let root = self.collection_root_for_scope(scope)?;
2443 let mut collected = Vec::new();
2444 self.collect_descendant_elements_matching(
2445 root,
2446 &mut collected,
2447 &|element: &ElementData| {
2448 let Some(value) = element.attributes.get("class") else {
2449 return false;
2450 };
2451
2452 class_tokens.iter().all(|class_name| {
2453 value
2454 .split_ascii_whitespace()
2455 .any(|candidate| candidate == class_name)
2456 })
2457 },
2458 );
2459 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2460 }
2461
2462 fn named_item_for_class_name_collection(
2463 &self,
2464 scope: &HtmlCollectionScope,
2465 class_names: &str,
2466 name: &str,
2467 ) -> Result<Option<ElementHandle>, ScriptError> {
2468 let class_tokens = Self::ordered_class_names(class_names);
2469 if class_tokens.is_empty() {
2470 return Ok(None);
2471 }
2472
2473 let root = self.collection_root_for_scope(scope)?;
2474 let mut collected = Vec::new();
2475 self.collect_descendant_elements_matching(
2476 root,
2477 &mut collected,
2478 &|element: &ElementData| {
2479 let Some(value) = element.attributes.get("class") else {
2480 return false;
2481 };
2482
2483 class_tokens.iter().all(|class_name| {
2484 value
2485 .split_ascii_whitespace()
2486 .any(|candidate| candidate == class_name)
2487 })
2488 },
2489 );
2490 Ok(self.first_named_item_in_nodes(&collected, name))
2491 }
2492
2493 fn elements_by_name(&self, name: &str) -> Result<Vec<ElementHandle>, ScriptError> {
2494 let root = self.dom.document_id();
2495 let mut collected = Vec::new();
2496 self.collect_descendant_elements_matching(
2497 root,
2498 &mut collected,
2499 &|element: &ElementData| {
2500 element
2501 .attributes
2502 .get("name")
2503 .is_some_and(|value| value == name)
2504 },
2505 );
2506 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2507 }
2508
2509 fn element_labels(&self, element: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
2510 let node_id = self.node_id_for_handle(element)?;
2511 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2512 return Err(ScriptError::new("invalid element handle"));
2513 };
2514
2515 let NodeKind::Element(element_data) = &node.kind else {
2516 return Err(ScriptError::new("node is not a labelable element"));
2517 };
2518
2519 if !Self::is_labelable_element(element_data) {
2520 return Err(ScriptError::new("node is not a labelable element"));
2521 }
2522
2523 let target_id = element_data.attributes.get("id").cloned();
2524 let root = self.dom.document_id();
2525 let mut collected = Vec::new();
2526 self.collect_descendant_elements_matching(
2527 root,
2528 &mut collected,
2529 &|element: &ElementData| element.tag_name == "label",
2530 );
2531
2532 let mut labels = Vec::new();
2533 for label_id in collected {
2534 let Some(label_node) = self.dom.nodes().get(label_id.index() as usize) else {
2535 continue;
2536 };
2537 let NodeKind::Element(label_element) = &label_node.kind else {
2538 continue;
2539 };
2540
2541 let explicit = target_id.as_deref().is_some_and(|target_id| {
2542 label_element
2543 .attributes
2544 .get("for")
2545 .is_some_and(|value| value == target_id)
2546 });
2547 let implicit = self.is_descendant_of(node_id, label_id);
2548 if explicit || implicit {
2549 labels.push(Self::node_id_to_handle(label_id));
2550 }
2551 }
2552
2553 Ok(labels)
2554 }
2555
2556 fn form_elements(&self, form: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
2557 let node_id = self.node_id_for_handle(form)?;
2558 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2559 return Err(ScriptError::new("invalid element handle"));
2560 };
2561
2562 let NodeKind::Element(element) = &node.kind else {
2563 return Err(ScriptError::new("node is not a form element"));
2564 };
2565
2566 if !matches!(element.tag_name.as_str(), "form" | "fieldset") {
2567 return Err(ScriptError::new("node is not a form element"));
2568 }
2569
2570 let mut collected = Vec::new();
2571 self.collect_descendant_elements_matching(
2572 node_id,
2573 &mut collected,
2574 &|element: &ElementData| Self::is_form_control_element(element),
2575 );
2576 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2577 }
2578
2579 fn form_elements_named_item(
2580 &self,
2581 form: ElementHandle,
2582 name: &str,
2583 ) -> Result<Option<ElementHandle>, ScriptError> {
2584 let items = self.form_elements_named_items(form, name)?;
2585 Ok(items.first().copied())
2586 }
2587
2588 fn form_elements_named_items(
2589 &self,
2590 form: ElementHandle,
2591 name: &str,
2592 ) -> Result<Vec<ElementHandle>, ScriptError> {
2593 let node_id = self.node_id_for_handle(form)?;
2594 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2595 return Err(ScriptError::new("invalid element handle"));
2596 };
2597
2598 let NodeKind::Element(element) = &node.kind else {
2599 return Err(ScriptError::new("node is not a form element"));
2600 };
2601
2602 if !matches!(element.tag_name.as_str(), "form" | "fieldset") {
2603 return Err(ScriptError::new("node is not a form element"));
2604 }
2605
2606 let mut collected = Vec::new();
2607 self.collect_descendant_elements_matching(
2608 node_id,
2609 &mut collected,
2610 &|element: &ElementData| Self::is_form_control_element(element),
2611 );
2612 Ok(self
2613 .named_items_in_nodes(&collected, name)
2614 .into_iter()
2615 .map(Self::node_id_to_handle)
2616 .collect())
2617 }
2618
2619 fn select_options(&self, select: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
2620 let node_id = self.node_id_for_handle(select)?;
2621 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2622 return Err(ScriptError::new("invalid element handle"));
2623 };
2624
2625 let NodeKind::Element(element) = &node.kind else {
2626 return Err(ScriptError::new("node is not a select element"));
2627 };
2628
2629 if !matches!(element.tag_name.as_str(), "select" | "datalist") {
2630 return Err(ScriptError::new("node is not a select element"));
2631 }
2632
2633 let mut collected = Vec::new();
2634 self.collect_descendant_elements_matching(
2635 node_id,
2636 &mut collected,
2637 &|element: &ElementData| element.tag_name == "option",
2638 );
2639 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2640 }
2641
2642 fn select_options_named_item(
2643 &self,
2644 select: ElementHandle,
2645 name: &str,
2646 ) -> Result<Option<ElementHandle>, ScriptError> {
2647 let node_id = self.node_id_for_handle(select)?;
2648 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2649 return Err(ScriptError::new("invalid element handle"));
2650 };
2651
2652 let NodeKind::Element(element) = &node.kind else {
2653 return Err(ScriptError::new("node is not a select element"));
2654 };
2655
2656 if !matches!(element.tag_name.as_str(), "select" | "datalist") {
2657 return Err(ScriptError::new("node is not a select element"));
2658 }
2659
2660 let mut collected = Vec::new();
2661 self.collect_descendant_elements_matching(
2662 node_id,
2663 &mut collected,
2664 &|element: &ElementData| element.tag_name == "option",
2665 );
2666 Ok(self.first_named_item_in_nodes(&collected, name))
2667 }
2668
2669 fn selected_options(&self, select: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
2670 let node_id = self.node_id_for_handle(select)?;
2671 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
2672 return Err(ScriptError::new("invalid element handle"));
2673 };
2674
2675 let NodeKind::Element(element) = &node.kind else {
2676 return Err(ScriptError::new("node is not a select element"));
2677 };
2678
2679 if element.tag_name != "select" {
2680 return Err(ScriptError::new("node is not a select element"));
2681 }
2682
2683 let mut collected = Vec::new();
2684 self.collect_descendant_elements_matching(
2685 node_id,
2686 &mut collected,
2687 &|element: &ElementData| {
2688 element.tag_name == "option" && element.attributes.contains_key("selected")
2689 },
2690 );
2691 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2692 }
2693
2694 fn selected_options_named_item(
2695 &self,
2696 select: ElementHandle,
2697 name: &str,
2698 ) -> Result<Option<ElementHandle>, ScriptError> {
2699 let selected = self.selected_options(select)?;
2700 let selected_ids: Vec<NodeId> = selected
2701 .into_iter()
2702 .map(|handle| self.node_id_for_handle(handle))
2703 .collect::<Result<_, _>>()?;
2704 Ok(self.first_named_item_in_nodes(&selected_ids, name))
2705 }
2706
2707 fn select_options_add(
2708 &mut self,
2709 select: ElementHandle,
2710 option: ElementHandle,
2711 ) -> Result<(), ScriptError> {
2712 let select_id = self.node_id_for_handle(select)?;
2713 let Some(node) = self.dom.nodes().get(select_id.index() as usize) else {
2714 return Err(ScriptError::new("invalid element handle"));
2715 };
2716 let NodeKind::Element(element) = &node.kind else {
2717 return Err(ScriptError::new("node is not a select element"));
2718 };
2719 if element.tag_name != "select" {
2720 return Err(ScriptError::new("node is not a select element"));
2721 }
2722
2723 let option_id = self.node_id_for_handle(option)?;
2724 let Some(node) = self.dom.nodes().get(option_id.index() as usize) else {
2725 return Err(ScriptError::new("invalid element handle"));
2726 };
2727 let NodeKind::Element(element) = &node.kind else {
2728 return Err(ScriptError::new("node is not an option element"));
2729 };
2730 if element.tag_name != "option" {
2731 return Err(ScriptError::new("node is not an option element"));
2732 }
2733
2734 self.dom
2735 .append_child(select_id, option_id)
2736 .map_err(ScriptError::new)
2737 }
2738
2739 fn select_options_remove(
2740 &mut self,
2741 select: ElementHandle,
2742 index: usize,
2743 ) -> Result<(), ScriptError> {
2744 let select_id = self.node_id_for_handle(select)?;
2745 let Some(node) = self.dom.nodes().get(select_id.index() as usize) else {
2746 return Err(ScriptError::new("invalid element handle"));
2747 };
2748 let NodeKind::Element(element) = &node.kind else {
2749 return Err(ScriptError::new("node is not a select element"));
2750 };
2751 if element.tag_name != "select" {
2752 return Err(ScriptError::new("node is not a select element"));
2753 }
2754
2755 let options = self.select_options(select)?;
2756 let Some(option_handle) = options.get(index).copied() else {
2757 return Ok(());
2758 };
2759 let option_id = self.node_id_for_handle(option_handle)?;
2760 self.dom.remove_node(option_id).map_err(ScriptError::new)
2761 }
2762
2763 fn document_links(&self) -> Result<Vec<ElementHandle>, ScriptError> {
2764 let root = self.dom.document_id();
2765 let mut collected = Vec::new();
2766 self.collect_descendant_elements_matching(
2767 root,
2768 &mut collected,
2769 &|element: &ElementData| Self::is_document_link_element(element),
2770 );
2771 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2772 }
2773
2774 fn document_links_named_item(&self, name: &str) -> Result<Option<ElementHandle>, ScriptError> {
2775 let root = self.dom.document_id();
2776 let mut collected = Vec::new();
2777 self.collect_descendant_elements_matching(
2778 root,
2779 &mut collected,
2780 &|element: &ElementData| Self::is_document_link_element(element),
2781 );
2782 Ok(self.first_named_item_in_nodes(&collected, name))
2783 }
2784
2785 fn document_style_sheets(&self) -> Result<Vec<ElementHandle>, ScriptError> {
2786 let root = self.dom.document_id();
2787 let mut collected = Vec::new();
2788 self.collect_descendant_elements_matching(
2789 root,
2790 &mut collected,
2791 &|element: &ElementData| Self::is_document_style_sheet_element(element),
2792 );
2793 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
2794 }
2795
2796 fn document_style_sheets_named_item(
2797 &self,
2798 name: &str,
2799 ) -> Result<Option<ElementHandle>, ScriptError> {
2800 let root = self.dom.document_id();
2801 let mut collected = Vec::new();
2802 self.collect_descendant_elements_matching(
2803 root,
2804 &mut collected,
2805 &|element: &ElementData| Self::is_document_style_sheet_element(element),
2806 );
2807 Ok(self.first_named_item_in_nodes(&collected, name))
2808 }
2809
2810 fn document_document_element(&self) -> Result<Option<ElementHandle>, ScriptError> {
2811 Ok(self.dom.document_element_id().map(Self::node_id_to_handle))
2812 }
2813
2814 fn document_head(&self) -> Result<Option<ElementHandle>, ScriptError> {
2815 Ok(self.dom.head_element_id().map(Self::node_id_to_handle))
2816 }
2817
2818 fn document_body(&self) -> Result<Option<ElementHandle>, ScriptError> {
2819 Ok(self.dom.body_element_id().map(Self::node_id_to_handle))
2820 }
2821
2822 pub fn document_scrolling_element(&self) -> Result<Option<ElementHandle>, ScriptError> {
2823 self.document_document_element()
2824 }
2825
2826 fn document_active_element(&self) -> Result<Option<ElementHandle>, ScriptError> {
2827 if let Some(node_id) = self.focused_node {
2828 return Ok(Some(Self::node_id_to_handle(node_id)));
2829 }
2830
2831 if let Some(body) = self.dom.body_element_id() {
2832 return Ok(Some(Self::node_id_to_handle(body)));
2833 }
2834
2835 Ok(self.dom.document_element_id().map(Self::node_id_to_handle))
2836 }
2837
2838 pub fn document_has_focus(&self) -> bool {
2839 self.focused_node.is_some()
2840 }
2841
2842 pub fn document_visibility_state(&self) -> &'static str {
2843 "visible"
2844 }
2845
2846 pub fn document_hidden(&self) -> bool {
2847 false
2848 }
2849
2850 pub fn document_title(&self) -> String {
2851 self.dom.document_title()
2852 }
2853
2854 pub fn set_document_title(&mut self, value: &str) -> Result<(), SessionError> {
2855 self.dom
2856 .set_document_title(value.to_string())
2857 .map_err(SessionError::Dom)
2858 }
2859
2860 pub fn document_write(&mut self, html: &str) -> Result<(), SessionError> {
2861 self.dom
2862 .append_html_to_document(html)
2863 .map_err(SessionError::Dom)
2864 }
2865
2866 pub fn document_writeln(&mut self, html: &str) -> Result<(), SessionError> {
2867 let mut html = html.to_string();
2868 html.push('\n');
2869 self.document_write(&html)
2870 }
2871
2872 pub fn document_open(&mut self) -> Result<(), SessionError> {
2873 self.dom.document_open().map_err(SessionError::Dom)
2874 }
2875
2876 pub fn document_close(&mut self) -> Result<(), SessionError> {
2877 Ok(())
2878 }
2879
2880 pub fn document_location(&self) -> String {
2881 self.mocks
2882 .location()
2883 .current_url()
2884 .unwrap_or(self.config.url.as_str())
2885 .to_string()
2886 }
2887
2888 pub fn document_url(&self) -> String {
2889 self.document_location()
2890 }
2891
2892 pub fn document_document_uri(&self) -> String {
2893 self.document_location()
2894 }
2895
2896 pub fn document_base_uri(&self) -> String {
2897 self.document_location()
2898 }
2899
2900 pub fn document_origin(&self) -> String {
2901 Self::origin_from_url(&self.document_location())
2902 }
2903
2904 pub fn document_domain(&self) -> String {
2905 Self::domain_from_url(&self.document_location())
2906 }
2907
2908 pub fn document_referrer(&self) -> String {
2909 String::new()
2910 }
2911
2912 fn document_cookie(&self) -> String {
2913 self.cookie_jar
2914 .iter()
2915 .map(|(name, value)| format!("{name}={value}"))
2916 .collect::<Vec<_>>()
2917 .join("; ")
2918 }
2919
2920 fn set_document_cookie(&mut self, value: &str) -> Result<(), SessionError> {
2921 let trimmed = value.trim();
2922 if trimmed.is_empty() {
2923 return Err(SessionError::Mock(
2924 "document.cookie requires a non-empty cookie string".to_string(),
2925 ));
2926 }
2927
2928 let pair = trimmed
2929 .split_once(';')
2930 .map(|(pair, _)| pair)
2931 .unwrap_or(trimmed);
2932 let Some((name, cookie_value)) = pair.split_once('=') else {
2933 return Err(SessionError::Mock(
2934 "document.cookie requires `name=value`".to_string(),
2935 ));
2936 };
2937
2938 let name = name.trim();
2939 if name.is_empty() {
2940 return Err(SessionError::Mock(
2941 "document.cookie requires a non-empty cookie name".to_string(),
2942 ));
2943 }
2944
2945 self.cookie_jar
2946 .insert(name.to_string(), cookie_value.trim_start().to_string());
2947 Ok(())
2948 }
2949
2950 pub fn window_name(&self) -> &str {
2951 &self.window_name
2952 }
2953
2954 pub fn set_window_name(&mut self, value: &str) {
2955 self.window_name = value.to_string();
2956 }
2957
2958 pub fn window_navigator_user_agent(&self) -> &'static str {
2959 "browser_tester"
2960 }
2961
2962 pub fn window_navigator_app_code_name(&self) -> &'static str {
2963 "browser_tester"
2964 }
2965
2966 pub fn window_navigator_app_name(&self) -> &'static str {
2967 "browser_tester"
2968 }
2969
2970 pub fn window_navigator_app_version(&self) -> &'static str {
2971 "browser_tester"
2972 }
2973
2974 pub fn window_navigator_product(&self) -> &'static str {
2975 "browser_tester"
2976 }
2977
2978 pub fn window_navigator_product_sub(&self) -> &'static str {
2979 "browser_tester"
2980 }
2981
2982 pub fn window_navigator_platform(&self) -> &'static str {
2983 "unknown"
2984 }
2985
2986 pub fn window_navigator_language(&self) -> &'static str {
2987 "en-US"
2988 }
2989
2990 pub fn window_navigator_oscpu(&self) -> &'static str {
2991 "unknown"
2992 }
2993
2994 fn window_navigator_user_language(&self) -> &'static str {
2995 self.window_navigator_language()
2996 }
2997
2998 fn window_navigator_browser_language(&self) -> &'static str {
2999 self.window_navigator_language()
3000 }
3001
3002 pub fn window_navigator_system_language(&self) -> &'static str {
3003 self.window_navigator_language()
3004 }
3005
3006 pub fn window_navigator_languages(&self) -> Vec<String> {
3007 vec![self.window_navigator_language().to_string()]
3008 }
3009
3010 pub fn window_navigator_mime_types(&self) -> Vec<String> {
3011 Vec::new()
3012 }
3013
3014 pub fn window_navigator_cookie_enabled(&self) -> bool {
3015 true
3016 }
3017
3018 pub fn window_navigator_on_line(&self) -> bool {
3019 true
3020 }
3021
3022 pub fn window_navigator_webdriver(&self) -> bool {
3023 false
3024 }
3025
3026 pub fn window_navigator_vendor(&self) -> &'static str {
3027 "browser_tester"
3028 }
3029
3030 pub fn window_navigator_vendor_sub(&self) -> &'static str {
3031 "browser_tester"
3032 }
3033
3034 pub fn window_navigator_pdf_viewer_enabled(&self) -> bool {
3035 false
3036 }
3037
3038 pub fn window_navigator_do_not_track(&self) -> &'static str {
3039 "unspecified"
3040 }
3041
3042 pub fn window_navigator_java_enabled(&self) -> bool {
3043 false
3044 }
3045
3046 pub fn window_navigator_hardware_concurrency(&self) -> i64 {
3047 8
3048 }
3049
3050 pub fn window_navigator_max_touch_points(&self) -> i64 {
3051 0
3052 }
3053
3054 pub fn window_history_length(&self) -> usize {
3055 self.history_entries.len()
3056 }
3057
3058 pub fn window_history_state(&self) -> Option<&str> {
3059 self.history_states
3060 .get(self.history_index)
3061 .and_then(|state| state.as_deref())
3062 }
3063
3064 pub fn window_history_scroll_restoration(&self) -> &str {
3065 &self.history_scroll_restoration
3066 }
3067
3068 pub fn set_window_history_scroll_restoration(
3069 &mut self,
3070 value: &str,
3071 ) -> Result<(), SessionError> {
3072 match value {
3073 "auto" | "manual" => {
3074 self.history_scroll_restoration = value.to_string();
3075 Ok(())
3076 }
3077 other => Err(SessionError::Mock(format!(
3078 "unsupported history scroll restoration value: {other}"
3079 ))),
3080 }
3081 }
3082
3083 pub fn window_scroll_x(&self) -> i64 {
3084 self.scroll_x
3085 }
3086
3087 pub fn window_scroll_y(&self) -> i64 {
3088 self.scroll_y
3089 }
3090
3091 pub fn window_page_x_offset(&self) -> i64 {
3092 self.scroll_x
3093 }
3094
3095 pub fn window_page_y_offset(&self) -> i64 {
3096 self.scroll_y
3097 }
3098
3099 pub fn window_device_pixel_ratio(&self) -> f64 {
3100 1.0
3101 }
3102
3103 pub fn window_inner_width(&self) -> i64 {
3104 1024
3105 }
3106
3107 pub fn window_inner_height(&self) -> i64 {
3108 768
3109 }
3110
3111 pub fn window_outer_width(&self) -> i64 {
3112 1024
3113 }
3114
3115 pub fn window_outer_height(&self) -> i64 {
3116 768
3117 }
3118
3119 pub fn window_screen_x(&self) -> i64 {
3120 0
3121 }
3122
3123 pub fn window_screen_y(&self) -> i64 {
3124 0
3125 }
3126
3127 pub fn window_screen_left(&self) -> i64 {
3128 self.window_screen_x()
3129 }
3130
3131 pub fn window_screen_top(&self) -> i64 {
3132 self.window_screen_y()
3133 }
3134
3135 pub fn window_screen_width(&self) -> i64 {
3136 1024
3137 }
3138
3139 pub fn window_screen_height(&self) -> i64 {
3140 768
3141 }
3142
3143 pub fn window_screen_avail_width(&self) -> i64 {
3144 1024
3145 }
3146
3147 pub fn window_screen_avail_height(&self) -> i64 {
3148 768
3149 }
3150
3151 pub fn window_screen_avail_left(&self) -> i64 {
3152 0
3153 }
3154
3155 pub fn window_screen_avail_top(&self) -> i64 {
3156 0
3157 }
3158
3159 pub fn window_screen_color_depth(&self) -> i64 {
3160 24
3161 }
3162
3163 pub fn window_screen_pixel_depth(&self) -> i64 {
3164 24
3165 }
3166
3167 pub fn window_screen_orientation(&self) -> ScreenOrientationState {
3168 ScreenOrientationState::new("landscape-primary", 0)
3169 }
3170
3171 fn storage_map(&self, target: StorageTarget) -> &BTreeMap<String, String> {
3172 match target {
3173 StorageTarget::Local => &self.mocks.storage.local,
3174 StorageTarget::Session => &self.mocks.storage.session,
3175 }
3176 }
3177
3178 fn storage_map_mut(&mut self, target: StorageTarget) -> &mut BTreeMap<String, String> {
3179 match target {
3180 StorageTarget::Local => &mut self.mocks.storage.local,
3181 StorageTarget::Session => &mut self.mocks.storage.session,
3182 }
3183 }
3184
3185 pub fn storage_length(&self, target: StorageTarget) -> usize {
3186 self.storage_map(target).len()
3187 }
3188
3189 pub fn storage_get_item(&self, target: StorageTarget, key: &str) -> Option<String> {
3190 self.storage_map(target).get(key).cloned()
3191 }
3192
3193 pub fn storage_set_item(&mut self, target: StorageTarget, key: &str, value: &str) {
3194 self.storage_map_mut(target)
3195 .insert(key.to_string(), value.to_string());
3196 }
3197
3198 pub fn storage_remove_item(&mut self, target: StorageTarget, key: &str) {
3199 self.storage_map_mut(target).remove(key);
3200 }
3201
3202 pub fn storage_clear(&mut self, target: StorageTarget) {
3203 self.storage_map_mut(target).clear();
3204 }
3205
3206 pub fn storage_key(&self, target: StorageTarget, index: usize) -> Option<String> {
3207 self.storage_map(target).keys().nth(index).cloned()
3208 }
3209
3210 pub fn document_ready_state(&self) -> &'static str {
3211 match self.document_ready_state {
3212 DocumentReadyState::Loading => "loading",
3213 DocumentReadyState::Complete => "complete",
3214 }
3215 }
3216
3217 pub fn document_compat_mode(&self) -> &'static str {
3218 "CSS1Compat"
3219 }
3220
3221 pub fn document_character_set(&self) -> &'static str {
3222 "UTF-8"
3223 }
3224
3225 pub fn document_content_type(&self) -> &'static str {
3226 "text/html"
3227 }
3228
3229 pub fn document_design_mode(&self) -> &str {
3230 &self.document_design_mode
3231 }
3232
3233 pub fn set_document_design_mode(&mut self, value: &str) -> Result<(), SessionError> {
3234 if value.eq_ignore_ascii_case("on") {
3235 self.document_design_mode = "on".to_string();
3236 Ok(())
3237 } else if value.eq_ignore_ascii_case("off") {
3238 self.document_design_mode = "off".to_string();
3239 Ok(())
3240 } else {
3241 Err(SessionError::Mock(format!(
3242 "unsupported document designMode value: {value}"
3243 )))
3244 }
3245 }
3246
3247 pub fn document_dir(&self) -> String {
3248 self.dom
3249 .document_element_id()
3250 .and_then(|node_id| self.dom.get_attribute(node_id, "dir").ok().flatten())
3251 .unwrap_or_default()
3252 }
3253
3254 pub fn set_document_dir(&mut self, value: &str) -> Result<(), SessionError> {
3255 let Some(document_element) = self.dom.document_element_id() else {
3256 return Ok(());
3257 };
3258
3259 self.dom
3260 .set_attribute(document_element, "dir", value.to_string())
3261 .map_err(SessionError::Dom)
3262 }
3263
3264 pub fn document_current_script(&self) -> Option<ElementHandle> {
3265 let script_node_id = self.current_script?;
3266 let node = self.dom.nodes().get(script_node_id.index() as usize)?;
3267 let NodeKind::Element(element) = &node.kind else {
3268 return None;
3269 };
3270 if element.tag_name != "script" {
3271 return None;
3272 }
3273
3274 Some(Self::node_id_to_handle(script_node_id))
3275 }
3276
3277 fn origin_from_url(url: &str) -> String {
3278 let Some((scheme, rest)) = url.split_once(':') else {
3279 return "null".to_string();
3280 };
3281
3282 let scheme = scheme.to_ascii_lowercase();
3283 let Some(after_slashes) = rest.strip_prefix("//") else {
3284 return "null".to_string();
3285 };
3286
3287 let authority_end = after_slashes
3288 .find(['/', '?', '#'])
3289 .unwrap_or(after_slashes.len());
3290 let authority = &after_slashes[..authority_end];
3291 if authority.is_empty() {
3292 return "null".to_string();
3293 }
3294
3295 format!("{scheme}://{authority}")
3296 }
3297
3298 fn domain_from_url(url: &str) -> String {
3299 let Some((_, rest)) = url.split_once(':') else {
3300 return "null".to_string();
3301 };
3302
3303 let Some(after_slashes) = rest.strip_prefix("//") else {
3304 return "null".to_string();
3305 };
3306
3307 let authority_end = after_slashes
3308 .find(['/', '?', '#'])
3309 .unwrap_or(after_slashes.len());
3310 let mut authority = &after_slashes[..authority_end];
3311 if authority.is_empty() {
3312 return "null".to_string();
3313 }
3314
3315 if let Some((_, host)) = authority.rsplit_once('@') {
3316 authority = host;
3317 }
3318
3319 let host = if authority.starts_with('[') {
3320 let Some(end_bracket) = authority.find(']') else {
3321 return "null".to_string();
3322 };
3323 &authority[1..end_bracket]
3324 } else {
3325 authority
3326 .split_once(':')
3327 .map(|(host, _)| host)
3328 .unwrap_or(authority)
3329 };
3330
3331 if host.is_empty() {
3332 "null".to_string()
3333 } else {
3334 host.to_ascii_lowercase()
3335 }
3336 }
3337
3338 pub fn set_document_location(&mut self, value: &str) -> Result<(), SessionError> {
3339 self.document_location_assign(value)
3340 }
3341
3342 pub fn document_location_assign(&mut self, value: &str) -> Result<(), SessionError> {
3343 self.navigate(value)
3344 }
3345
3346 pub fn document_location_replace(&mut self, value: &str) -> Result<(), SessionError> {
3347 let url = value.trim();
3348 if url.is_empty() {
3349 return Err(SessionError::Mock(
3350 "document.location.replace() requires a non-empty URL".to_string(),
3351 ));
3352 }
3353
3354 if self.history_entries.is_empty() {
3355 return self.navigate(url);
3356 }
3357
3358 self.history_entries[self.history_index] = url.to_string();
3359 self.apply_history_navigation(url);
3360 Ok(())
3361 }
3362
3363 pub fn document_location_reload(&mut self) -> Result<(), SessionError> {
3364 let url = Session::document_location(self);
3365 self.apply_history_navigation(&url);
3366 Ok(())
3367 }
3368
3369 fn document_children(&self) -> Result<Vec<ElementHandle>, ScriptError> {
3370 let root = self.dom.document_id();
3371 let Some(node) = self.dom.nodes().get(root.index() as usize) else {
3372 return Err(ScriptError::new("invalid document node"));
3373 };
3374
3375 let children = node
3376 .children
3377 .iter()
3378 .copied()
3379 .filter(|child_id| {
3380 matches!(
3381 self.dom
3382 .nodes()
3383 .get(child_id.index() as usize)
3384 .map(|node| &node.kind),
3385 Some(NodeKind::Element(_))
3386 )
3387 })
3388 .map(Self::node_id_to_handle)
3389 .collect();
3390
3391 Ok(children)
3392 }
3393
3394 fn document_children_named_item(
3395 &self,
3396 name: &str,
3397 ) -> Result<Option<ElementHandle>, ScriptError> {
3398 let root = self.dom.document_id();
3399 let Some(node) = self.dom.nodes().get(root.index() as usize) else {
3400 return Err(ScriptError::new("invalid document node"));
3401 };
3402
3403 let children: Vec<NodeId> = node
3404 .children
3405 .iter()
3406 .copied()
3407 .filter(|child_id| {
3408 matches!(
3409 self.dom
3410 .nodes()
3411 .get(child_id.index() as usize)
3412 .map(|node| &node.kind),
3413 Some(NodeKind::Element(_))
3414 )
3415 })
3416 .collect();
3417
3418 Ok(self.first_named_item_in_nodes(&children, name))
3419 }
3420
3421 fn window_frames(&self) -> Result<Vec<ElementHandle>, ScriptError> {
3422 let root = self.dom.document_id();
3423 let mut collected = Vec::new();
3424 self.collect_descendant_elements_matching(
3425 root,
3426 &mut collected,
3427 &|element: &ElementData| matches!(element.tag_name.as_str(), "frame" | "iframe"),
3428 );
3429 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3430 }
3431
3432 fn window_frames_named_item(&self, name: &str) -> Result<Option<ElementHandle>, ScriptError> {
3433 let root = self.dom.document_id();
3434 let mut collected = Vec::new();
3435 self.collect_descendant_elements_matching(
3436 root,
3437 &mut collected,
3438 &|element: &ElementData| matches!(element.tag_name.as_str(), "frame" | "iframe"),
3439 );
3440 Ok(self.first_named_item_in_nodes(&collected, name))
3441 }
3442
3443 fn node_child_nodes(&self, scope: HtmlCollectionScope) -> Result<Vec<NodeHandle>, ScriptError> {
3444 let root = self.collection_root_for_scope(&scope)?;
3445 let Some(node) = self.dom.nodes().get(root.index() as usize) else {
3446 return Err(ScriptError::new("invalid node id"));
3447 };
3448
3449 Ok(node
3450 .children
3451 .iter()
3452 .copied()
3453 .map(Self::node_id_to_node_handle)
3454 .collect())
3455 }
3456
3457 fn map_areas(&self, map: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
3458 let node_id = self.node_id_for_handle(map)?;
3459 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3460 return Err(ScriptError::new("invalid element handle"));
3461 };
3462
3463 let NodeKind::Element(element) = &node.kind else {
3464 return Err(ScriptError::new(
3465 "node is not a supported map.areas host element",
3466 ));
3467 };
3468
3469 if element.tag_name != "map" {
3470 return Err(ScriptError::new(
3471 "node is not a supported map.areas host element",
3472 ));
3473 }
3474
3475 let mut collected = Vec::new();
3476 self.collect_descendant_elements_matching(
3477 node_id,
3478 &mut collected,
3479 &|element: &ElementData| element.tag_name == "area",
3480 );
3481 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3482 }
3483
3484 fn map_areas_named_item(
3485 &self,
3486 map: ElementHandle,
3487 name: &str,
3488 ) -> Result<Option<ElementHandle>, ScriptError> {
3489 let node_id = self.node_id_for_handle(map)?;
3490 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3491 return Err(ScriptError::new("invalid element handle"));
3492 };
3493
3494 let NodeKind::Element(element) = &node.kind else {
3495 return Err(ScriptError::new(
3496 "node is not a supported map.areas host element",
3497 ));
3498 };
3499
3500 if element.tag_name != "map" {
3501 return Err(ScriptError::new(
3502 "node is not a supported map.areas host element",
3503 ));
3504 }
3505
3506 let mut collected = Vec::new();
3507 self.collect_descendant_elements_matching(
3508 node_id,
3509 &mut collected,
3510 &|element: &ElementData| element.tag_name == "area",
3511 );
3512 Ok(self.first_named_item_in_nodes(&collected, name))
3513 }
3514
3515 fn table_bodies(&self, table: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
3516 let node_id = self.node_id_for_handle(table)?;
3517 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3518 return Err(ScriptError::new("invalid element handle"));
3519 };
3520
3521 let NodeKind::Element(element) = &node.kind else {
3522 return Err(ScriptError::new(
3523 "node is not a supported table.tBodies host element",
3524 ));
3525 };
3526
3527 if element.tag_name != "table" {
3528 return Err(ScriptError::new(
3529 "node is not a supported table.tBodies host element",
3530 ));
3531 }
3532
3533 let collected = self
3534 .direct_child_elements_matching(node_id, &|element: &ElementData| {
3535 element.tag_name == "tbody"
3536 })?;
3537 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3538 }
3539
3540 fn table_bodies_named_item(
3541 &self,
3542 table: ElementHandle,
3543 name: &str,
3544 ) -> Result<Option<ElementHandle>, ScriptError> {
3545 let node_id = self.node_id_for_handle(table)?;
3546 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3547 return Err(ScriptError::new("invalid element handle"));
3548 };
3549
3550 let NodeKind::Element(element) = &node.kind else {
3551 return Err(ScriptError::new(
3552 "node is not a supported table.tBodies host element",
3553 ));
3554 };
3555
3556 if element.tag_name != "table" {
3557 return Err(ScriptError::new(
3558 "node is not a supported table.tBodies host element",
3559 ));
3560 }
3561
3562 let bodies = self.table_bodies(table)?;
3563 let body_ids: Vec<NodeId> = bodies
3564 .into_iter()
3565 .map(|handle| self.node_id_for_handle(handle))
3566 .collect::<Result<_, _>>()?;
3567 Ok(self.first_named_item_in_nodes(&body_ids, name))
3568 }
3569
3570 fn table_rows(&self, table: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
3571 let node_id = self.node_id_for_handle(table)?;
3572 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3573 return Err(ScriptError::new("invalid element handle"));
3574 };
3575
3576 let NodeKind::Element(element) = &node.kind else {
3577 return Err(ScriptError::new("node is not a table.rows host element"));
3578 };
3579
3580 let collected = match element.tag_name.as_str() {
3581 "table" => {
3582 let mut collected = Vec::new();
3583 for child_id in &node.children {
3584 let Some(child) = self.dom.nodes().get(child_id.index() as usize) else {
3585 continue;
3586 };
3587 let NodeKind::Element(child_element) = &child.kind else {
3588 continue;
3589 };
3590
3591 match child_element.tag_name.as_str() {
3592 "tr" => collected.push(*child_id),
3593 "thead" | "tbody" | "tfoot" => {
3594 collected.extend(self.direct_child_elements_matching(
3595 *child_id,
3596 &|element: &ElementData| element.tag_name == "tr",
3597 )?);
3598 }
3599 _ => {}
3600 }
3601 }
3602 collected
3603 }
3604 "thead" | "tbody" | "tfoot" => self
3605 .direct_child_elements_matching(node_id, &|element: &ElementData| {
3606 element.tag_name == "tr"
3607 })?,
3608 _ => {
3609 return Err(ScriptError::new(
3610 "node is not a supported table.rows host element",
3611 ));
3612 }
3613 };
3614
3615 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3616 }
3617
3618 fn table_rows_named_item(
3619 &self,
3620 table: ElementHandle,
3621 name: &str,
3622 ) -> Result<Option<ElementHandle>, ScriptError> {
3623 let node_id = self.node_id_for_handle(table)?;
3624 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3625 return Err(ScriptError::new("invalid element handle"));
3626 };
3627
3628 let NodeKind::Element(element) = &node.kind else {
3629 return Err(ScriptError::new("node is not a table.rows host element"));
3630 };
3631
3632 match element.tag_name.as_str() {
3633 "table" | "thead" | "tbody" | "tfoot" => {
3634 let rows = self.table_rows(table)?;
3635 let row_ids: Vec<NodeId> = rows
3636 .into_iter()
3637 .map(|handle| self.node_id_for_handle(handle))
3638 .collect::<Result<_, _>>()?;
3639 Ok(self.first_named_item_in_nodes(&row_ids, name))
3640 }
3641 _ => Err(ScriptError::new(
3642 "node is not a supported table.rows host element",
3643 )),
3644 }
3645 }
3646
3647 fn row_cells(&self, row: ElementHandle) -> Result<Vec<ElementHandle>, ScriptError> {
3648 let node_id = self.node_id_for_handle(row)?;
3649 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3650 return Err(ScriptError::new("invalid element handle"));
3651 };
3652
3653 let NodeKind::Element(element) = &node.kind else {
3654 return Err(ScriptError::new("node is not a tr.cells host element"));
3655 };
3656
3657 if element.tag_name != "tr" {
3658 return Err(ScriptError::new(
3659 "node is not a supported tr.cells host element",
3660 ));
3661 }
3662
3663 let collected = self
3664 .direct_child_elements_matching(node_id, &|element: &ElementData| {
3665 matches!(element.tag_name.as_str(), "td" | "th")
3666 })?;
3667
3668 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3669 }
3670
3671 fn row_cells_named_item(
3672 &self,
3673 row: ElementHandle,
3674 name: &str,
3675 ) -> Result<Option<ElementHandle>, ScriptError> {
3676 let node_id = self.node_id_for_handle(row)?;
3677 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3678 return Err(ScriptError::new("invalid element handle"));
3679 };
3680
3681 let NodeKind::Element(element) = &node.kind else {
3682 return Err(ScriptError::new("node is not a tr.cells host element"));
3683 };
3684
3685 if element.tag_name != "tr" {
3686 return Err(ScriptError::new(
3687 "node is not a supported tr.cells host element",
3688 ));
3689 }
3690
3691 let cells = self.row_cells(row)?;
3692 let cell_ids: Vec<NodeId> = cells
3693 .into_iter()
3694 .map(|handle| self.node_id_for_handle(handle))
3695 .collect::<Result<_, _>>()?;
3696 Ok(self.first_named_item_in_nodes(&cell_ids, name))
3697 }
3698
3699 fn document_anchors(&self) -> Result<Vec<ElementHandle>, ScriptError> {
3700 let root = self.dom.document_id();
3701 let mut collected = Vec::new();
3702 self.collect_descendant_elements_matching(
3703 root,
3704 &mut collected,
3705 &|element: &ElementData| Self::is_document_anchor_element(element),
3706 );
3707 Ok(collected.into_iter().map(Self::node_id_to_handle).collect())
3708 }
3709
3710 fn document_anchors_named_item(
3711 &self,
3712 name: &str,
3713 ) -> Result<Option<ElementHandle>, ScriptError> {
3714 let root = self.dom.document_id();
3715 let mut collected = Vec::new();
3716 self.collect_descendant_elements_matching(
3717 root,
3718 &mut collected,
3719 &|element: &ElementData| Self::is_document_anchor_element(element),
3720 );
3721 Ok(self.first_named_item_in_nodes(&collected, name))
3722 }
3723
3724 fn first_named_item_in_nodes(&self, collected: &[NodeId], name: &str) -> Option<ElementHandle> {
3725 for node_id in collected {
3726 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3727 continue;
3728 };
3729 let NodeKind::Element(element) = &node.kind else {
3730 continue;
3731 };
3732
3733 if element
3734 .attributes
3735 .get("id")
3736 .is_some_and(|value| value == name)
3737 || element
3738 .attributes
3739 .get("name")
3740 .is_some_and(|value| value == name)
3741 {
3742 return Some(Self::node_id_to_handle(*node_id));
3743 }
3744 }
3745
3746 None
3747 }
3748
3749 fn named_items_in_nodes(&self, collected: &[NodeId], name: &str) -> Vec<NodeId> {
3750 let mut matches = Vec::new();
3751 for node_id in collected {
3752 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
3753 continue;
3754 };
3755 let NodeKind::Element(element) = &node.kind else {
3756 continue;
3757 };
3758
3759 if element
3760 .attributes
3761 .get("id")
3762 .is_some_and(|value| value == name)
3763 || element
3764 .attributes
3765 .get("name")
3766 .is_some_and(|value| value == name)
3767 {
3768 matches.push(*node_id);
3769 }
3770 }
3771
3772 matches
3773 }
3774
3775 fn is_form_control_element(element: &ElementData) -> bool {
3776 matches!(
3777 element.tag_name.as_str(),
3778 "input" | "select" | "textarea" | "button"
3779 )
3780 }
3781
3782 fn is_labelable_element(element: &ElementData) -> bool {
3783 match element.tag_name.as_str() {
3784 "button" | "fieldset" | "meter" | "output" | "progress" | "select" | "textarea" => true,
3785 "input" => !element
3786 .attributes
3787 .get("type")
3788 .is_some_and(|value| value.eq_ignore_ascii_case("hidden")),
3789 _ => false,
3790 }
3791 }
3792
3793 fn is_document_link_element(element: &ElementData) -> bool {
3794 matches!(element.tag_name.as_str(), "a" | "area") && element.attributes.contains_key("href")
3795 }
3796
3797 fn is_document_style_sheet_element(element: &ElementData) -> bool {
3798 if element.tag_name == "style" {
3799 return true;
3800 }
3801
3802 if element.tag_name != "link" {
3803 return false;
3804 }
3805
3806 let Some(rel) = element.attributes.get("rel") else {
3807 return false;
3808 };
3809
3810 rel.split_ascii_whitespace()
3811 .any(|token| token.eq_ignore_ascii_case("stylesheet"))
3812 }
3813
3814 fn is_document_anchor_element(element: &ElementData) -> bool {
3815 element.tag_name == "a" && element.attributes.contains_key("name")
3816 }
3817
3818 fn matches_namespace_uri(element: &ElementData, namespace_uri: &str) -> bool {
3819 match namespace_uri {
3820 "*" => true,
3821 HTML_NAMESPACE_URI | SVG_NAMESPACE_URI | MATHML_NAMESPACE_URI => {
3822 element.namespace_uri == namespace_uri
3823 }
3824 _ => false,
3825 }
3826 }
3827
3828 fn ordered_class_names(class_names: &str) -> Vec<String> {
3829 class_names
3830 .split_ascii_whitespace()
3831 .filter(|class_name| !class_name.is_empty())
3832 .map(str::to_string)
3833 .collect()
3834 }
3835
3836 fn is_descendant_of(&self, node_id: NodeId, ancestor_id: NodeId) -> bool {
3837 let mut current = self
3838 .dom
3839 .nodes()
3840 .get(node_id.index() as usize)
3841 .and_then(|node| node.parent);
3842
3843 while let Some(parent_id) = current {
3844 if parent_id == ancestor_id {
3845 return true;
3846 }
3847
3848 current = self
3849 .dom
3850 .nodes()
3851 .get(parent_id.index() as usize)
3852 .and_then(|node| node.parent);
3853 }
3854
3855 false
3856 }
3857
3858 fn node_id_for_handle(&self, handle: ElementHandle) -> Result<NodeId, ScriptError> {
3859 let raw = handle.raw();
3860 let index = (raw & 0xffff_ffff) as u32;
3861 let generation = (raw >> 32) as u32;
3862 let node_id = NodeId::new(index, generation);
3863 let Some(record) = self.dom.nodes().get(index as usize) else {
3864 return Err(ScriptError::new("invalid element handle"));
3865 };
3866 if record.id.generation() != generation {
3867 return Err(ScriptError::new("invalid element handle"));
3868 }
3869 Ok(node_id)
3870 }
3871
3872 fn node_id_for_node_handle(&self, handle: NodeHandle) -> Result<NodeId, ScriptError> {
3873 let raw = handle.raw();
3874 let index = (raw & 0xffff_ffff) as u32;
3875 let generation = (raw >> 32) as u32;
3876 let node_id = NodeId::new(index, generation);
3877 let Some(record) = self.dom.nodes().get(index as usize) else {
3878 return Err(ScriptError::new("invalid node handle"));
3879 };
3880 if record.id.generation() != generation {
3881 return Err(ScriptError::new("invalid node handle"));
3882 }
3883 Ok(node_id)
3884 }
3885
3886 fn register_script_listener(
3887 &mut self,
3888 target: SessionEventTarget,
3889 event_type: String,
3890 capture: bool,
3891 handler: ScriptFunction,
3892 ) {
3893 self.script_event_listeners.push(ScriptListenerRecord {
3894 target,
3895 event_type,
3896 capture,
3897 handler,
3898 });
3899 }
3900
3901 fn schedule_script_timer(
3902 &mut self,
3903 kind: ScheduledScriptTimerKind,
3904 delay_ms: i64,
3905 handler: ScriptFunction,
3906 ) -> u64 {
3907 let due_at = self.scheduler.now_ms.saturating_add(delay_ms.max(0));
3908 let id = self.scheduler.queue_timer(due_at);
3909 let trace_kind = match &kind {
3910 ScheduledScriptTimerKind::AnimationFrame => "animationFrame",
3911 ScheduledScriptTimerKind::Timeout => "timeout",
3912 ScheduledScriptTimerKind::Interval { .. } => "interval",
3913 };
3914 self.scheduled_script_timers
3915 .insert(id, ScheduledScriptTimerRecord { kind, handler });
3916 self.trace_timer_line(format!("[timer] schedule {trace_kind}"));
3917 id
3918 }
3919
3920 fn cancel_script_timer(&mut self, id: u64) {
3921 self.scheduler.cancel_timer(id);
3922 self.scheduled_script_timers.remove(&id);
3923 }
3924
3925 fn run_timer_callback(
3926 &mut self,
3927 callback: &ScriptFunction,
3928 this_value: ScriptValue,
3929 args: &[ScriptValue],
3930 source_name: &str,
3931 ) -> Result<(), SessionError> {
3932 let mut bindings = (*callback.captured_bindings).clone();
3933
3934 bindings.insert("this".to_string(), this_value);
3935
3936 for (index, param) in callback.params.iter().enumerate() {
3937 let value = args.get(index).cloned().unwrap_or(ScriptValue::Undefined);
3938 bindings.insert(param.clone(), value);
3939 }
3940
3941 self.eval_script_source_with_bindings(&callback.body_source, source_name, bindings)
3942 }
3943
3944 fn run_due_script_timers(
3945 &mut self,
3946 due_timers: Vec<ScheduledTimer>,
3947 ) -> Result<usize, SessionError> {
3948 let mut executed = 0usize;
3949
3950 for timer in due_timers {
3951 let Some(record) = self.scheduled_script_timers.get(&timer.id).cloned() else {
3952 continue;
3953 };
3954
3955 executed = executed.saturating_add(1);
3956 let source_name = match &record.kind {
3957 ScheduledScriptTimerKind::AnimationFrame => {
3958 format!("timer:requestAnimationFrame:{}", timer.id)
3959 }
3960 ScheduledScriptTimerKind::Timeout => format!("timer:setTimeout:{}", timer.id),
3961 ScheduledScriptTimerKind::Interval { .. } => {
3962 format!("timer:setInterval:{}", timer.id)
3963 }
3964 };
3965 let args = match &record.kind {
3966 ScheduledScriptTimerKind::AnimationFrame => {
3967 vec![ScriptValue::Number(self.scheduler.now_ms as f64)]
3968 }
3969 ScheduledScriptTimerKind::Timeout | ScheduledScriptTimerKind::Interval { .. } => {
3970 Vec::new()
3971 }
3972 };
3973
3974 self.run_timer_callback(&record.handler, ScriptValue::Window, &args, &source_name)?;
3975
3976 match &record.kind {
3977 ScheduledScriptTimerKind::AnimationFrame | ScheduledScriptTimerKind::Timeout => {
3978 self.scheduled_script_timers.remove(&timer.id);
3979 }
3980 ScheduledScriptTimerKind::Interval { interval_ms } => {
3981 if self.scheduled_script_timers.contains_key(&timer.id) {
3982 self.scheduled_script_timers.remove(&timer.id);
3983 self.schedule_script_timer(
3984 ScheduledScriptTimerKind::Interval {
3985 interval_ms: *interval_ms,
3986 },
3987 *interval_ms,
3988 record.handler,
3989 );
3990 }
3991 }
3992 }
3993 }
3994
3995 Ok(executed)
3996 }
3997
3998 fn trace_line(&mut self, line: String) {
3999 if !self.debug.trace_enabled {
4000 return;
4001 }
4002 if self.debug.trace_to_stderr {
4003 eprintln!("{line}");
4004 }
4005 if self.debug.trace_logs.len() >= self.debug.trace_log_limit {
4006 self.debug.trace_logs.pop_front();
4007 }
4008 self.debug.trace_logs.push_back(line);
4009 }
4010
4011 fn trace_event_line(&mut self, line: String) {
4012 if self.debug.trace_enabled && self.debug.trace_events {
4013 self.trace_line(line);
4014 }
4015 }
4016
4017 fn trace_timer_line(&mut self, line: String) {
4018 if self.debug.trace_enabled && self.debug.trace_timers {
4019 self.trace_line(line);
4020 }
4021 }
4022
4023 fn next_random_f64(&mut self) -> f64 {
4024 let mut x = self.rng_state;
4025 x ^= x >> 12;
4026 x ^= x << 25;
4027 x ^= x >> 27;
4028 self.rng_state = if x == 0 {
4029 0xA5A5_A5A5_A5A5_A5A5
4030 } else {
4031 x
4032 };
4033 let out = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
4034 let mantissa = out >> 11;
4035 (mantissa as f64) * (1.0 / ((1u64 << 53) as f64))
4036 }
4037}
4038
4039impl HostBindings for Session {
4040 fn document_get_element_by_id(&mut self, id: &str) -> bt_script::Result<Option<ElementHandle>> {
4041 let Some(node_id) = self.dom.indexes().id_index.get(id).copied() else {
4042 return Ok(None);
4043 };
4044
4045 Ok(Some(Self::node_id_to_handle(node_id)))
4046 }
4047
4048 fn document_create_element(&mut self, tag_name: &str) -> bt_script::Result<ElementHandle> {
4049 let node_id = self
4050 .dom
4051 .create_element(tag_name)
4052 .map_err(ScriptError::new)?;
4053 Ok(Self::node_id_to_handle(node_id))
4054 }
4055
4056 fn document_create_element_ns(
4057 &mut self,
4058 namespace_uri: &str,
4059 tag_name: &str,
4060 ) -> bt_script::Result<ElementHandle> {
4061 let node_id = self
4062 .dom
4063 .create_element_ns(namespace_uri, tag_name)
4064 .map_err(ScriptError::new)?;
4065 Ok(Self::node_id_to_handle(node_id))
4066 }
4067
4068 fn document_create_text_node(&mut self, text: &str) -> bt_script::Result<NodeHandle> {
4069 let node_id = self.dom.create_text_node(text).map_err(ScriptError::new)?;
4070 Ok(Self::node_id_to_node_handle(node_id))
4071 }
4072
4073 fn document_create_comment(&mut self, text: &str) -> bt_script::Result<NodeHandle> {
4074 let node_id = self.dom.create_comment(text).map_err(ScriptError::new)?;
4075 Ok(Self::node_id_to_node_handle(node_id))
4076 }
4077
4078 fn document_normalize(&mut self) -> bt_script::Result<()> {
4079 self.dom
4080 .normalize_node(self.dom.document_id())
4081 .map_err(ScriptError::new)
4082 }
4083
4084 fn document_document_element(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4085 Session::document_document_element(self)
4086 }
4087
4088 fn document_head(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4089 Session::document_head(self)
4090 }
4091
4092 fn document_body(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4093 Session::document_body(self)
4094 }
4095
4096 fn document_scrolling_element(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4097 Session::document_scrolling_element(self)
4098 }
4099
4100 fn document_active_element(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4101 Session::document_active_element(self)
4102 }
4103
4104 fn document_has_focus(&mut self) -> bt_script::Result<bool> {
4105 Ok(Session::document_has_focus(self))
4106 }
4107
4108 fn element_click(&mut self, element: ElementHandle) -> bt_script::Result<()> {
4109 Session::click_node(self, Self::element_handle_to_node_id(element))
4110 .map_err(|error| ScriptError::new(error.to_string()))
4111 }
4112
4113 fn element_focus(&mut self, element: ElementHandle) -> bt_script::Result<()> {
4114 Session::focus_node(self, Self::element_handle_to_node_id(element))
4115 .map_err(|error| ScriptError::new(error.to_string()))
4116 }
4117
4118 fn element_blur(&mut self, element: ElementHandle) -> bt_script::Result<()> {
4119 Session::blur_node(self, Self::element_handle_to_node_id(element))
4120 .map_err(|error| ScriptError::new(error.to_string()))
4121 }
4122
4123 fn document_visibility_state(&mut self) -> bt_script::Result<String> {
4124 Ok(Session::document_visibility_state(self).to_string())
4125 }
4126
4127 fn document_hidden(&mut self) -> bt_script::Result<bool> {
4128 Ok(Session::document_hidden(self))
4129 }
4130
4131 fn document_title(&mut self) -> bt_script::Result<String> {
4132 Ok(Session::document_title(self))
4133 }
4134
4135 fn document_set_title(&mut self, value: &str) -> bt_script::Result<()> {
4136 Session::set_document_title(self, value)
4137 .map_err(|error| ScriptError::new(error.to_string()))
4138 }
4139
4140 fn document_write(&mut self, html: &str) -> bt_script::Result<()> {
4141 Session::document_write(self, html).map_err(|error| ScriptError::new(error.to_string()))
4142 }
4143
4144 fn document_writeln(&mut self, html: &str) -> bt_script::Result<()> {
4145 Session::document_writeln(self, html).map_err(|error| ScriptError::new(error.to_string()))
4146 }
4147
4148 fn document_open(&mut self) -> bt_script::Result<()> {
4149 Session::document_open(self).map_err(|error| ScriptError::new(error.to_string()))
4150 }
4151
4152 fn document_close(&mut self) -> bt_script::Result<()> {
4153 Session::document_close(self).map_err(|error| ScriptError::new(error.to_string()))
4154 }
4155
4156 fn document_location(&mut self) -> bt_script::Result<String> {
4157 Ok(Session::document_location(self))
4158 }
4159
4160 fn document_set_location(&mut self, value: &str) -> bt_script::Result<()> {
4161 Session::set_document_location(self, value)
4162 .map_err(|error| ScriptError::new(error.to_string()))
4163 }
4164
4165 fn document_location_assign(&mut self, value: &str) -> bt_script::Result<()> {
4166 Session::document_location_assign(self, value)
4167 .map_err(|error| ScriptError::new(error.to_string()))
4168 }
4169
4170 fn document_location_replace(&mut self, value: &str) -> bt_script::Result<()> {
4171 Session::document_location_replace(self, value)
4172 .map_err(|error| ScriptError::new(error.to_string()))
4173 }
4174
4175 fn document_location_reload(&mut self) -> bt_script::Result<()> {
4176 Session::document_location_reload(self).map_err(|error| ScriptError::new(error.to_string()))
4177 }
4178
4179 fn document_url(&mut self) -> bt_script::Result<String> {
4180 Ok(Session::document_url(self))
4181 }
4182
4183 fn document_document_uri(&mut self) -> bt_script::Result<String> {
4184 Ok(Session::document_document_uri(self))
4185 }
4186
4187 fn document_base_uri(&mut self) -> bt_script::Result<String> {
4188 Ok(Session::document_base_uri(self))
4189 }
4190
4191 fn document_origin(&mut self) -> bt_script::Result<String> {
4192 Ok(Session::document_origin(self))
4193 }
4194
4195 fn document_domain(&mut self) -> bt_script::Result<String> {
4196 Ok(Session::document_domain(self))
4197 }
4198
4199 fn document_referrer(&mut self) -> bt_script::Result<String> {
4200 Ok(Session::document_referrer(self))
4201 }
4202
4203 fn document_cookie(&mut self) -> bt_script::Result<String> {
4204 Ok(Session::document_cookie(self))
4205 }
4206
4207 fn document_set_cookie(&mut self, value: &str) -> bt_script::Result<()> {
4208 Session::set_document_cookie(self, value)
4209 .map_err(|err| bt_script::ScriptError::new(err.to_string()))
4210 }
4211
4212 fn match_media(&mut self, query: &str) -> bt_script::Result<MediaQueryListState> {
4213 self.mocks
4214 .match_media_mut()
4215 .resolve(query)
4216 .map_err(ScriptError::new)
4217 }
4218
4219 fn match_media_add_listener(&mut self, query: &str) -> bt_script::Result<()> {
4220 self.mocks
4221 .match_media_mut()
4222 .record_listener_call(query, "addListener");
4223 Ok(())
4224 }
4225
4226 fn match_media_remove_listener(&mut self, query: &str) -> bt_script::Result<()> {
4227 self.mocks
4228 .match_media_mut()
4229 .record_listener_call(query, "removeListener");
4230 Ok(())
4231 }
4232
4233 fn window_open(
4234 &mut self,
4235 url: Option<&str>,
4236 target: Option<&str>,
4237 features: Option<&str>,
4238 ) -> bt_script::Result<()> {
4239 Session::open(self, url, target, features)
4240 .map_err(|error| ScriptError::new(error.to_string()))
4241 }
4242
4243 fn window_close(&mut self) -> bt_script::Result<()> {
4244 Session::close(self).map_err(|error| ScriptError::new(error.to_string()))
4245 }
4246
4247 fn window_print(&mut self) -> bt_script::Result<()> {
4248 Session::print(self).map_err(|error| ScriptError::new(error.to_string()))
4249 }
4250
4251 fn window_request_animation_frame(
4252 &mut self,
4253 callback: ScriptFunction,
4254 ) -> bt_script::Result<u64> {
4255 Ok(self.schedule_script_timer(ScheduledScriptTimerKind::AnimationFrame, 16, callback))
4256 }
4257
4258 fn window_cancel_animation_frame(&mut self, handle: u64) -> bt_script::Result<()> {
4259 self.cancel_script_timer(handle);
4260 Ok(())
4261 }
4262
4263 fn window_set_timeout(
4264 &mut self,
4265 callback: ScriptFunction,
4266 delay_ms: i64,
4267 ) -> bt_script::Result<u64> {
4268 Ok(self.schedule_script_timer(ScheduledScriptTimerKind::Timeout, delay_ms, callback))
4269 }
4270
4271 fn window_clear_timeout(&mut self, handle: u64) -> bt_script::Result<()> {
4272 self.cancel_script_timer(handle);
4273 Ok(())
4274 }
4275
4276 fn window_set_interval(
4277 &mut self,
4278 callback: ScriptFunction,
4279 delay_ms: i64,
4280 ) -> bt_script::Result<u64> {
4281 Ok(self.schedule_script_timer(
4282 ScheduledScriptTimerKind::Interval {
4283 interval_ms: delay_ms.max(0),
4284 },
4285 delay_ms,
4286 callback,
4287 ))
4288 }
4289
4290 fn window_clear_interval(&mut self, handle: u64) -> bt_script::Result<()> {
4291 self.cancel_script_timer(handle);
4292 Ok(())
4293 }
4294
4295 fn window_alert(&mut self, message: &str) -> bt_script::Result<()> {
4296 Session::alert(self, message);
4297 Ok(())
4298 }
4299
4300 fn window_confirm(&mut self, message: &str) -> bt_script::Result<bool> {
4301 Session::confirm(self, message).map_err(|error| ScriptError::new(error.to_string()))
4302 }
4303
4304 fn window_prompt(
4305 &mut self,
4306 message: &str,
4307 _default_text: Option<&str>,
4308 ) -> bt_script::Result<Option<String>> {
4309 Session::prompt(self, message).map_err(|error| ScriptError::new(error.to_string()))
4310 }
4311
4312 fn window_navigator_user_agent(&mut self) -> bt_script::Result<String> {
4313 Ok(Session::window_navigator_user_agent(self).to_string())
4314 }
4315
4316 fn window_navigator_app_code_name(&mut self) -> bt_script::Result<String> {
4317 Ok(Session::window_navigator_app_code_name(self).to_string())
4318 }
4319
4320 fn window_navigator_app_name(&mut self) -> bt_script::Result<String> {
4321 Ok(Session::window_navigator_app_name(self).to_string())
4322 }
4323
4324 fn window_navigator_app_version(&mut self) -> bt_script::Result<String> {
4325 Ok(Session::window_navigator_app_version(self).to_string())
4326 }
4327
4328 fn window_navigator_product(&mut self) -> bt_script::Result<String> {
4329 Ok(Session::window_navigator_product(self).to_string())
4330 }
4331
4332 fn window_navigator_product_sub(&mut self) -> bt_script::Result<String> {
4333 Ok(Session::window_navigator_product_sub(self).to_string())
4334 }
4335
4336 fn window_navigator_platform(&mut self) -> bt_script::Result<String> {
4337 Ok(Session::window_navigator_platform(self).to_string())
4338 }
4339
4340 fn window_navigator_language(&mut self) -> bt_script::Result<String> {
4341 Ok(Session::window_navigator_language(self).to_string())
4342 }
4343
4344 fn window_navigator_oscpu(&mut self) -> bt_script::Result<String> {
4345 Ok(Session::window_navigator_oscpu(self).to_string())
4346 }
4347
4348 fn window_navigator_user_language(&mut self) -> bt_script::Result<String> {
4349 Ok(Session::window_navigator_user_language(self).to_string())
4350 }
4351
4352 fn window_navigator_browser_language(&mut self) -> bt_script::Result<String> {
4353 Ok(Session::window_navigator_browser_language(self).to_string())
4354 }
4355
4356 fn window_navigator_system_language(&mut self) -> bt_script::Result<String> {
4357 Ok(Session::window_navigator_system_language(self).to_string())
4358 }
4359
4360 fn window_navigator_languages(&mut self) -> bt_script::Result<Vec<String>> {
4361 Ok(Session::window_navigator_languages(self))
4362 }
4363
4364 fn window_navigator_mime_types(&mut self) -> bt_script::Result<Vec<String>> {
4365 Ok(Session::window_navigator_mime_types(self))
4366 }
4367
4368 fn clipboard_write_text(&mut self, text: &str) -> bt_script::Result<()> {
4369 Session::write_clipboard(self, text).map_err(|error| ScriptError::new(error.to_string()))
4370 }
4371
4372 fn clipboard_read_text(&mut self) -> bt_script::Result<String> {
4373 Session::read_clipboard(self).map_err(|error| ScriptError::new(error.to_string()))
4374 }
4375
4376 fn random_f64(&mut self) -> bt_script::Result<f64> {
4377 Ok(self.next_random_f64())
4378 }
4379
4380 fn window_navigator_cookie_enabled(&mut self) -> bt_script::Result<bool> {
4381 Ok(Session::window_navigator_cookie_enabled(self))
4382 }
4383
4384 fn window_navigator_on_line(&mut self) -> bt_script::Result<bool> {
4385 Ok(Session::window_navigator_on_line(self))
4386 }
4387
4388 fn window_navigator_webdriver(&mut self) -> bt_script::Result<bool> {
4389 Ok(Session::window_navigator_webdriver(self))
4390 }
4391
4392 fn window_navigator_vendor(&mut self) -> bt_script::Result<String> {
4393 Ok(Session::window_navigator_vendor(self).to_string())
4394 }
4395
4396 fn window_navigator_vendor_sub(&mut self) -> bt_script::Result<String> {
4397 Ok(Session::window_navigator_vendor_sub(self).to_string())
4398 }
4399
4400 fn window_navigator_pdf_viewer_enabled(&mut self) -> bt_script::Result<bool> {
4401 Ok(Session::window_navigator_pdf_viewer_enabled(self))
4402 }
4403
4404 fn window_navigator_do_not_track(&mut self) -> bt_script::Result<String> {
4405 Ok(Session::window_navigator_do_not_track(self).to_string())
4406 }
4407
4408 fn window_navigator_java_enabled(&mut self) -> bt_script::Result<bool> {
4409 Ok(Session::window_navigator_java_enabled(self))
4410 }
4411
4412 fn window_navigator_hardware_concurrency(&mut self) -> bt_script::Result<i64> {
4413 Ok(Session::window_navigator_hardware_concurrency(self))
4414 }
4415
4416 fn window_navigator_max_touch_points(&mut self) -> bt_script::Result<i64> {
4417 Ok(Session::window_navigator_max_touch_points(self))
4418 }
4419
4420 fn window_history_length(&mut self) -> bt_script::Result<usize> {
4421 Ok(Session::window_history_length(self))
4422 }
4423
4424 fn window_history_scroll_restoration(&mut self) -> bt_script::Result<String> {
4425 Ok(Session::window_history_scroll_restoration(self).to_string())
4426 }
4427
4428 fn set_window_history_scroll_restoration(&mut self, value: &str) -> bt_script::Result<()> {
4429 Session::set_window_history_scroll_restoration(self, value)
4430 .map_err(|error| ScriptError::new(error.to_string()))
4431 }
4432
4433 fn window_history_state(&mut self) -> bt_script::Result<Option<String>> {
4434 Ok(Session::window_history_state(self).map(str::to_string))
4435 }
4436
4437 fn window_history_push_state(
4438 &mut self,
4439 state: Option<&str>,
4440 url: Option<&str>,
4441 ) -> bt_script::Result<()> {
4442 Session::window_history_push_state(self, state, url)
4443 .map_err(|error| ScriptError::new(error.to_string()))
4444 }
4445
4446 fn window_history_replace_state(
4447 &mut self,
4448 state: Option<&str>,
4449 url: Option<&str>,
4450 ) -> bt_script::Result<()> {
4451 Session::window_history_replace_state(self, state, url)
4452 .map_err(|error| ScriptError::new(error.to_string()))
4453 }
4454
4455 fn window_history_back(&mut self) -> bt_script::Result<()> {
4456 Session::window_history_back(self).map_err(|error| ScriptError::new(error.to_string()))
4457 }
4458
4459 fn window_history_forward(&mut self) -> bt_script::Result<()> {
4460 Session::window_history_forward(self).map_err(|error| ScriptError::new(error.to_string()))
4461 }
4462
4463 fn window_history_go(&mut self, delta: i64) -> bt_script::Result<()> {
4464 Session::window_history_go(self, delta).map_err(|error| ScriptError::new(error.to_string()))
4465 }
4466
4467 fn window_scroll_x(&mut self) -> bt_script::Result<i64> {
4468 Ok(Session::window_scroll_x(self))
4469 }
4470
4471 fn window_scroll_y(&mut self) -> bt_script::Result<i64> {
4472 Ok(Session::window_scroll_y(self))
4473 }
4474
4475 fn window_page_x_offset(&mut self) -> bt_script::Result<i64> {
4476 Ok(Session::window_page_x_offset(self))
4477 }
4478
4479 fn window_page_y_offset(&mut self) -> bt_script::Result<i64> {
4480 Ok(Session::window_page_y_offset(self))
4481 }
4482
4483 fn window_device_pixel_ratio(&mut self) -> bt_script::Result<f64> {
4484 Ok(Session::window_device_pixel_ratio(self))
4485 }
4486
4487 fn window_inner_width(&mut self) -> bt_script::Result<i64> {
4488 Ok(Session::window_inner_width(self))
4489 }
4490
4491 fn window_inner_height(&mut self) -> bt_script::Result<i64> {
4492 Ok(Session::window_inner_height(self))
4493 }
4494
4495 fn window_outer_width(&mut self) -> bt_script::Result<i64> {
4496 Ok(Session::window_outer_width(self))
4497 }
4498
4499 fn window_outer_height(&mut self) -> bt_script::Result<i64> {
4500 Ok(Session::window_outer_height(self))
4501 }
4502
4503 fn window_screen_x(&mut self) -> bt_script::Result<i64> {
4504 Ok(Session::window_screen_x(self))
4505 }
4506
4507 fn window_screen_y(&mut self) -> bt_script::Result<i64> {
4508 Ok(Session::window_screen_y(self))
4509 }
4510
4511 fn window_screen_left(&mut self) -> bt_script::Result<i64> {
4512 Ok(Session::window_screen_left(self))
4513 }
4514
4515 fn window_screen_top(&mut self) -> bt_script::Result<i64> {
4516 Ok(Session::window_screen_top(self))
4517 }
4518
4519 fn window_screen_width(&mut self) -> bt_script::Result<i64> {
4520 Ok(Session::window_screen_width(self))
4521 }
4522
4523 fn window_screen_height(&mut self) -> bt_script::Result<i64> {
4524 Ok(Session::window_screen_height(self))
4525 }
4526
4527 fn window_screen_avail_width(&mut self) -> bt_script::Result<i64> {
4528 Ok(Session::window_screen_avail_width(self))
4529 }
4530
4531 fn window_screen_avail_height(&mut self) -> bt_script::Result<i64> {
4532 Ok(Session::window_screen_avail_height(self))
4533 }
4534
4535 fn window_screen_avail_left(&mut self) -> bt_script::Result<i64> {
4536 Ok(Session::window_screen_avail_left(self))
4537 }
4538
4539 fn window_screen_avail_top(&mut self) -> bt_script::Result<i64> {
4540 Ok(Session::window_screen_avail_top(self))
4541 }
4542
4543 fn window_screen_color_depth(&mut self) -> bt_script::Result<i64> {
4544 Ok(Session::window_screen_color_depth(self))
4545 }
4546
4547 fn window_screen_pixel_depth(&mut self) -> bt_script::Result<i64> {
4548 Ok(Session::window_screen_pixel_depth(self))
4549 }
4550
4551 fn window_screen_orientation(&mut self) -> bt_script::Result<ScreenOrientationState> {
4552 Ok(Session::window_screen_orientation(self))
4553 }
4554
4555 fn window_scroll_to(&mut self, x: i64, y: i64) -> bt_script::Result<()> {
4556 Session::scroll_to(self, x, y).map_err(|error| ScriptError::new(error.to_string()))
4557 }
4558
4559 fn window_scroll_by(&mut self, x: i64, y: i64) -> bt_script::Result<()> {
4560 Session::scroll_by(self, x, y).map_err(|error| ScriptError::new(error.to_string()))
4561 }
4562
4563 fn window_name(&mut self) -> bt_script::Result<String> {
4564 Ok(Session::window_name(self).to_string())
4565 }
4566
4567 fn set_window_name(&mut self, value: &str) -> bt_script::Result<()> {
4568 Session::set_window_name(self, value);
4569 Ok(())
4570 }
4571
4572 fn storage_length(&mut self, target: StorageTarget) -> bt_script::Result<usize> {
4573 Ok(Session::storage_length(self, target))
4574 }
4575
4576 fn storage_get_item(
4577 &mut self,
4578 target: StorageTarget,
4579 key: &str,
4580 ) -> bt_script::Result<Option<String>> {
4581 Ok(Session::storage_get_item(self, target, key))
4582 }
4583
4584 fn storage_set_item(
4585 &mut self,
4586 target: StorageTarget,
4587 key: &str,
4588 value: &str,
4589 ) -> bt_script::Result<()> {
4590 Session::storage_set_item(self, target, key, value);
4591 Ok(())
4592 }
4593
4594 fn storage_remove_item(&mut self, target: StorageTarget, key: &str) -> bt_script::Result<()> {
4595 Session::storage_remove_item(self, target, key);
4596 Ok(())
4597 }
4598
4599 fn storage_clear(&mut self, target: StorageTarget) -> bt_script::Result<()> {
4600 Session::storage_clear(self, target);
4601 Ok(())
4602 }
4603
4604 fn storage_key(
4605 &mut self,
4606 target: StorageTarget,
4607 index: usize,
4608 ) -> bt_script::Result<Option<String>> {
4609 Ok(Session::storage_key(self, target, index))
4610 }
4611
4612 fn document_current_script(&mut self) -> bt_script::Result<Option<ElementHandle>> {
4613 Ok(Session::document_current_script(self))
4614 }
4615
4616 fn document_ready_state(&mut self) -> bt_script::Result<String> {
4617 Ok(Session::document_ready_state(self).to_string())
4618 }
4619
4620 fn document_compat_mode(&mut self) -> bt_script::Result<String> {
4621 Ok(Session::document_compat_mode(self).to_string())
4622 }
4623
4624 fn document_character_set(&mut self) -> bt_script::Result<String> {
4625 Ok(Session::document_character_set(self).to_string())
4626 }
4627
4628 fn document_content_type(&mut self) -> bt_script::Result<String> {
4629 Ok(Session::document_content_type(self).to_string())
4630 }
4631
4632 fn document_design_mode(&mut self) -> bt_script::Result<String> {
4633 Ok(Session::document_design_mode(self).to_string())
4634 }
4635
4636 fn document_set_design_mode(&mut self, value: &str) -> bt_script::Result<()> {
4637 Session::set_document_design_mode(self, value)
4638 .map_err(|error| ScriptError::new(error.to_string()))
4639 }
4640
4641 fn document_dir(&mut self) -> bt_script::Result<String> {
4642 Ok(Session::document_dir(self))
4643 }
4644
4645 fn document_set_dir(&mut self, value: &str) -> bt_script::Result<()> {
4646 Session::set_document_dir(self, value).map_err(|error| ScriptError::new(error.to_string()))
4647 }
4648
4649 fn document_query_selector(
4650 &mut self,
4651 selector: &str,
4652 ) -> bt_script::Result<Option<ElementHandle>> {
4653 self.query_selector_handle(None, selector)
4654 }
4655
4656 fn document_query_selector_all(
4657 &mut self,
4658 selector: &str,
4659 ) -> bt_script::Result<Vec<ElementHandle>> {
4660 self.query_selector_handles(None, selector)
4661 }
4662
4663 fn document_get_elements_by_name(
4664 &mut self,
4665 name: &str,
4666 ) -> bt_script::Result<Vec<ElementHandle>> {
4667 self.elements_by_name(name)
4668 }
4669
4670 fn document_style_sheets_items(&mut self) -> bt_script::Result<Vec<ElementHandle>> {
4671 Session::document_style_sheets(self)
4672 }
4673
4674 fn document_style_sheets_named_item(
4675 &mut self,
4676 name: &str,
4677 ) -> bt_script::Result<Option<ElementHandle>> {
4678 Session::document_style_sheets_named_item(self, name)
4679 }
4680
4681 fn node_child_nodes_items(
4682 &mut self,
4683 scope: HtmlCollectionScope,
4684 ) -> bt_script::Result<Vec<NodeHandle>> {
4685 self.node_child_nodes(scope)
4686 }
4687
4688 fn node_clone(&mut self, node: NodeHandle, deep: bool) -> bt_script::Result<NodeHandle> {
4689 let node_id = self.node_id_for_node_handle(node)?;
4690 let cloned_id = self
4691 .dom
4692 .clone_node(node_id, deep)
4693 .map_err(ScriptError::new)?;
4694 Ok(Self::node_id_to_node_handle(cloned_id))
4695 }
4696
4697 fn node_normalize(&mut self, node: NodeHandle) -> bt_script::Result<()> {
4698 let node_id = self.node_id_for_node_handle(node)?;
4699 self.dom.normalize_node(node_id).map_err(ScriptError::new)
4700 }
4701
4702 fn node_replace_with(
4703 &mut self,
4704 node: NodeHandle,
4705 children: Vec<NodeHandle>,
4706 ) -> bt_script::Result<()> {
4707 let node_id = self.node_id_for_node_handle(node)?;
4708 let Some(parent_id) = self
4709 .dom
4710 .nodes()
4711 .get(node_id.index() as usize)
4712 .and_then(|record| record.parent)
4713 else {
4714 return Ok(());
4715 };
4716 let children = children
4717 .into_iter()
4718 .map(|child| self.node_id_for_node_handle(child))
4719 .collect::<Result<Vec<_>, _>>()?;
4720 self.dom
4721 .insert_children_before(parent_id, node_id, children)
4722 .map_err(ScriptError::new)?;
4723 self.dom.remove_node(node_id).map_err(ScriptError::new)
4724 }
4725
4726 fn node_before(
4727 &mut self,
4728 node: NodeHandle,
4729 children: Vec<NodeHandle>,
4730 ) -> bt_script::Result<()> {
4731 let node_id = self.node_id_for_node_handle(node)?;
4732 let Some(parent_id) = self
4733 .dom
4734 .nodes()
4735 .get(node_id.index() as usize)
4736 .and_then(|record| record.parent)
4737 else {
4738 return Ok(());
4739 };
4740 let children = children
4741 .into_iter()
4742 .map(|child| self.node_id_for_node_handle(child))
4743 .collect::<Result<Vec<_>, _>>()?;
4744 self.dom
4745 .insert_children_before(parent_id, node_id, children)
4746 .map_err(ScriptError::new)
4747 }
4748
4749 fn node_after(&mut self, node: NodeHandle, children: Vec<NodeHandle>) -> bt_script::Result<()> {
4750 let node_id = self.node_id_for_node_handle(node)?;
4751 let Some(parent_id) = self
4752 .dom
4753 .nodes()
4754 .get(node_id.index() as usize)
4755 .and_then(|record| record.parent)
4756 else {
4757 return Ok(());
4758 };
4759 let children = children
4760 .into_iter()
4761 .map(|child| self.node_id_for_node_handle(child))
4762 .collect::<Result<Vec<_>, _>>()?;
4763 self.dom
4764 .insert_children_after(parent_id, node_id, children)
4765 .map_err(ScriptError::new)
4766 }
4767
4768 fn document_contains(&mut self, node: NodeHandle) -> bt_script::Result<bool> {
4769 let node_id = self.node_id_for_node_handle(node)?;
4770 Ok(Self::node_contains_id(
4771 &self.dom,
4772 self.dom.document_id(),
4773 node_id,
4774 ))
4775 }
4776
4777 fn node_contains(&mut self, node: NodeHandle, other: NodeHandle) -> bt_script::Result<bool> {
4778 let node_id = self.node_id_for_node_handle(node)?;
4779 let other_id = self.node_id_for_node_handle(other)?;
4780 Ok(Self::node_contains_id(&self.dom, node_id, other_id))
4781 }
4782
4783 fn node_compare_document_position(
4784 &mut self,
4785 node: NodeHandle,
4786 other: NodeHandle,
4787 ) -> bt_script::Result<u16> {
4788 let node_id = self.node_id_for_node_handle(node)?;
4789 let other_id = self.node_id_for_node_handle(other)?;
4790 Ok(Self::node_compare_document_position_id(
4791 &self.dom, node_id, other_id,
4792 ))
4793 }
4794
4795 fn node_is_equal_node(
4796 &mut self,
4797 node: NodeHandle,
4798 other: NodeHandle,
4799 ) -> bt_script::Result<bool> {
4800 let node_id = self.node_id_for_node_handle(node)?;
4801 let other_id = self.node_id_for_node_handle(other)?;
4802 Ok(Self::node_is_equal_node_id(&self.dom, node_id, other_id))
4803 }
4804
4805 fn template_content_is_equal_node(
4806 &mut self,
4807 fragment: ElementHandle,
4808 other: ElementHandle,
4809 ) -> bt_script::Result<bool> {
4810 let fragment_id = self.node_id_for_handle(fragment)?;
4811 let other_id = self.node_id_for_handle(other)?;
4812 Ok(Self::template_content_is_equal_node_id(
4813 &self.dom,
4814 fragment_id,
4815 other_id,
4816 ))
4817 }
4818
4819 fn document_has_child_nodes(&mut self) -> bt_script::Result<bool> {
4820 Ok(Self::node_has_child_nodes_id(
4821 &self.dom,
4822 self.dom.document_id(),
4823 ))
4824 }
4825
4826 fn node_has_child_nodes(&mut self, node: NodeHandle) -> bt_script::Result<bool> {
4827 let node_id = self.node_id_for_node_handle(node)?;
4828 Ok(Self::node_has_child_nodes_id(&self.dom, node_id))
4829 }
4830
4831 fn node_parent(&mut self, node: NodeHandle) -> bt_script::Result<Option<NodeHandle>> {
4832 let node_id = self.node_id_for_node_handle(node)?;
4833 let Some(record) = self.dom.nodes().get(node_id.index() as usize) else {
4834 return Err(ScriptError::new("invalid node handle"));
4835 };
4836
4837 Ok(record.parent.map(Self::node_id_to_node_handle))
4838 }
4839
4840 fn node_text_content(&mut self, node: NodeHandle) -> bt_script::Result<String> {
4841 let node_id = self.node_id_for_node_handle(node)?;
4842 Ok(self.dom.text_content_for_node(node_id))
4843 }
4844
4845 fn node_type(&mut self, node: NodeHandle) -> bt_script::Result<u8> {
4846 let node_id = self.node_id_for_node_handle(node)?;
4847 let Some(record) = self.dom.nodes().get(node_id.index() as usize) else {
4848 return Err(ScriptError::new("invalid node handle"));
4849 };
4850
4851 let node_type = match &record.kind {
4852 NodeKind::Document => 9,
4853 NodeKind::Element(_) => 1,
4854 NodeKind::Text(_) => 3,
4855 NodeKind::Comment(_) => 8,
4856 };
4857 Ok(node_type)
4858 }
4859
4860 fn node_name(&mut self, node: NodeHandle) -> bt_script::Result<String> {
4861 let node_id = self.node_id_for_node_handle(node)?;
4862 let Some(record) = self.dom.nodes().get(node_id.index() as usize) else {
4863 return Err(ScriptError::new("invalid node handle"));
4864 };
4865
4866 let node_name = match &record.kind {
4867 NodeKind::Document => "#document".to_string(),
4868 NodeKind::Element(element) => element.tag_name.clone(),
4869 NodeKind::Text(_) => "#text".to_string(),
4870 NodeKind::Comment(_) => "#comment".to_string(),
4871 };
4872 Ok(node_name)
4873 }
4874
4875 fn node_namespace_uri(&mut self, node: NodeHandle) -> bt_script::Result<Option<String>> {
4876 let node_id = self.node_id_for_node_handle(node)?;
4877 let Some(record) = self.dom.nodes().get(node_id.index() as usize) else {
4878 return Err(ScriptError::new("invalid node handle"));
4879 };
4880
4881 let namespace_uri = match &record.kind {
4882 NodeKind::Document => None,
4883 NodeKind::Element(element) => Some(element.namespace_uri.clone()),
4884 NodeKind::Text(_) | NodeKind::Comment(_) => None,
4885 };
4886 Ok(namespace_uri)
4887 }
4888
4889 fn html_collection_tag_name_items(
4890 &mut self,
4891 collection: HtmlCollectionTarget,
4892 ) -> bt_script::Result<Vec<ElementHandle>> {
4893 match collection {
4894 HtmlCollectionTarget::Children(element) => self.element_children(element),
4895 HtmlCollectionTarget::ByTagName { scope, tag_name } => {
4896 self.elements_by_tag_name(&scope, &tag_name)
4897 }
4898 HtmlCollectionTarget::ByTagNameNs {
4899 scope,
4900 namespace_uri,
4901 local_name,
4902 } => self.elements_by_tag_name_ns(&scope, &namespace_uri, &local_name),
4903 HtmlCollectionTarget::ByClassName { scope, class_names } => {
4904 self.elements_by_class_name(&scope, &class_names)
4905 }
4906 HtmlCollectionTarget::FormElements(element) => self.form_elements(element),
4907 HtmlCollectionTarget::SelectOptions(element) => self.select_options(element),
4908 HtmlCollectionTarget::SelectSelectedOptions(element) => self.selected_options(element),
4909 HtmlCollectionTarget::DocumentPlugins => {
4910 self.elements_by_tag_name(&HtmlCollectionScope::Document, "embed")
4911 }
4912 HtmlCollectionTarget::DocumentLinks => self.document_links(),
4913 HtmlCollectionTarget::DocumentAnchors => self.document_anchors(),
4914 HtmlCollectionTarget::DocumentChildren => self.document_children(),
4915 HtmlCollectionTarget::WindowFrames => self.window_frames(),
4916 HtmlCollectionTarget::MapAreas(element) => self.map_areas(element),
4917 HtmlCollectionTarget::TableTBodies(element) => self.table_bodies(element),
4918 HtmlCollectionTarget::TableRows(element) => self.table_rows(element),
4919 HtmlCollectionTarget::RowCells(element) => self.row_cells(element),
4920 }
4921 }
4922
4923 fn html_collection_tag_name_named_item(
4924 &mut self,
4925 collection: HtmlCollectionTarget,
4926 name: &str,
4927 ) -> bt_script::Result<Option<ElementHandle>> {
4928 match collection {
4929 HtmlCollectionTarget::Children(element) => {
4930 self.html_collection_named_item(element, name)
4931 }
4932 HtmlCollectionTarget::ByTagName { scope, tag_name } => {
4933 self.named_item_for_tag_name_collection(&scope, &tag_name, name)
4934 }
4935 HtmlCollectionTarget::ByTagNameNs {
4936 scope,
4937 namespace_uri,
4938 local_name,
4939 } => self.named_item_for_tag_name_ns_collection(
4940 &scope,
4941 &namespace_uri,
4942 &local_name,
4943 name,
4944 ),
4945 HtmlCollectionTarget::ByClassName { scope, class_names } => {
4946 self.named_item_for_class_name_collection(&scope, &class_names, name)
4947 }
4948 HtmlCollectionTarget::FormElements(element) => {
4949 self.form_elements_named_item(element, name)
4950 }
4951 HtmlCollectionTarget::SelectOptions(element) => {
4952 self.select_options_named_item(element, name)
4953 }
4954 HtmlCollectionTarget::SelectSelectedOptions(element) => {
4955 self.selected_options_named_item(element, name)
4956 }
4957 HtmlCollectionTarget::DocumentPlugins => self.named_item_for_tag_name_collection(
4958 &HtmlCollectionScope::Document,
4959 "embed",
4960 name,
4961 ),
4962 HtmlCollectionTarget::DocumentLinks => self.document_links_named_item(name),
4963 HtmlCollectionTarget::DocumentAnchors => self.document_anchors_named_item(name),
4964 HtmlCollectionTarget::DocumentChildren => self.document_children_named_item(name),
4965 HtmlCollectionTarget::WindowFrames => self.window_frames_named_item(name),
4966 HtmlCollectionTarget::MapAreas(element) => self.map_areas_named_item(element, name),
4967 HtmlCollectionTarget::TableTBodies(element) => {
4968 self.table_bodies_named_item(element, name)
4969 }
4970 HtmlCollectionTarget::TableRows(element) => self.table_rows_named_item(element, name),
4971 HtmlCollectionTarget::RowCells(element) => self.row_cells_named_item(element, name),
4972 }
4973 }
4974
4975 fn html_collection_class_name_items(
4976 &mut self,
4977 collection: HtmlCollectionTarget,
4978 ) -> bt_script::Result<Vec<ElementHandle>> {
4979 match collection {
4980 HtmlCollectionTarget::Children(element) => self.element_children(element),
4981 HtmlCollectionTarget::ByTagName { scope, tag_name } => {
4982 self.elements_by_tag_name(&scope, &tag_name)
4983 }
4984 HtmlCollectionTarget::ByTagNameNs {
4985 scope,
4986 namespace_uri,
4987 local_name,
4988 } => self.elements_by_tag_name_ns(&scope, &namespace_uri, &local_name),
4989 HtmlCollectionTarget::ByClassName { scope, class_names } => {
4990 self.elements_by_class_name(&scope, &class_names)
4991 }
4992 HtmlCollectionTarget::FormElements(element) => self.form_elements(element),
4993 HtmlCollectionTarget::SelectOptions(element) => self.select_options(element),
4994 HtmlCollectionTarget::SelectSelectedOptions(element) => self.selected_options(element),
4995 HtmlCollectionTarget::DocumentPlugins => {
4996 self.elements_by_tag_name(&HtmlCollectionScope::Document, "embed")
4997 }
4998 HtmlCollectionTarget::DocumentLinks => self.document_links(),
4999 HtmlCollectionTarget::DocumentAnchors => self.document_anchors(),
5000 HtmlCollectionTarget::DocumentChildren => self.document_children(),
5001 HtmlCollectionTarget::WindowFrames => self.window_frames(),
5002 HtmlCollectionTarget::MapAreas(element) => self.map_areas(element),
5003 HtmlCollectionTarget::TableTBodies(element) => self.table_bodies(element),
5004 HtmlCollectionTarget::TableRows(element) => self.table_rows(element),
5005 HtmlCollectionTarget::RowCells(element) => self.row_cells(element),
5006 }
5007 }
5008
5009 fn html_collection_class_name_named_item(
5010 &mut self,
5011 collection: HtmlCollectionTarget,
5012 name: &str,
5013 ) -> bt_script::Result<Option<ElementHandle>> {
5014 match collection {
5015 HtmlCollectionTarget::Children(element) => {
5016 self.html_collection_named_item(element, name)
5017 }
5018 HtmlCollectionTarget::ByTagName { scope, tag_name } => {
5019 self.named_item_for_tag_name_collection(&scope, &tag_name, name)
5020 }
5021 HtmlCollectionTarget::ByTagNameNs {
5022 scope,
5023 namespace_uri,
5024 local_name,
5025 } => self.named_item_for_tag_name_ns_collection(
5026 &scope,
5027 &namespace_uri,
5028 &local_name,
5029 name,
5030 ),
5031 HtmlCollectionTarget::ByClassName { scope, class_names } => {
5032 self.named_item_for_class_name_collection(&scope, &class_names, name)
5033 }
5034 HtmlCollectionTarget::FormElements(element) => {
5035 self.form_elements_named_item(element, name)
5036 }
5037 HtmlCollectionTarget::SelectOptions(element) => {
5038 self.select_options_named_item(element, name)
5039 }
5040 HtmlCollectionTarget::SelectSelectedOptions(element) => {
5041 self.selected_options_named_item(element, name)
5042 }
5043 HtmlCollectionTarget::DocumentPlugins => self.named_item_for_tag_name_collection(
5044 &HtmlCollectionScope::Document,
5045 "embed",
5046 name,
5047 ),
5048 HtmlCollectionTarget::DocumentLinks => self.document_links_named_item(name),
5049 HtmlCollectionTarget::DocumentAnchors => self.document_anchors_named_item(name),
5050 HtmlCollectionTarget::DocumentChildren => self.document_children_named_item(name),
5051 HtmlCollectionTarget::WindowFrames => self.window_frames_named_item(name),
5052 HtmlCollectionTarget::MapAreas(element) => self.map_areas_named_item(element, name),
5053 HtmlCollectionTarget::TableTBodies(element) => {
5054 self.table_bodies_named_item(element, name)
5055 }
5056 HtmlCollectionTarget::TableRows(element) => self.table_rows_named_item(element, name),
5057 HtmlCollectionTarget::RowCells(element) => self.row_cells_named_item(element, name),
5058 }
5059 }
5060
5061 fn html_collection_form_elements_items(
5062 &mut self,
5063 element: ElementHandle,
5064 ) -> bt_script::Result<Vec<ElementHandle>> {
5065 self.form_elements(element)
5066 }
5067
5068 fn html_collection_tag_name_ns_items(
5069 &mut self,
5070 collection: HtmlCollectionTarget,
5071 ) -> bt_script::Result<Vec<ElementHandle>> {
5072 match collection {
5073 HtmlCollectionTarget::ByTagNameNs {
5074 scope,
5075 namespace_uri,
5076 local_name,
5077 } => self.elements_by_tag_name_ns(&scope, &namespace_uri, &local_name),
5078 HtmlCollectionTarget::DocumentAnchors => self.document_anchors(),
5079 other => self.html_collection_tag_name_items(other),
5080 }
5081 }
5082
5083 fn html_collection_tag_name_ns_named_item(
5084 &mut self,
5085 collection: HtmlCollectionTarget,
5086 name: &str,
5087 ) -> bt_script::Result<Option<ElementHandle>> {
5088 match collection {
5089 HtmlCollectionTarget::ByTagNameNs {
5090 scope,
5091 namespace_uri,
5092 local_name,
5093 } => self.named_item_for_tag_name_ns_collection(
5094 &scope,
5095 &namespace_uri,
5096 &local_name,
5097 name,
5098 ),
5099 HtmlCollectionTarget::DocumentAnchors => self.document_anchors_named_item(name),
5100 other => self.html_collection_tag_name_named_item(other, name),
5101 }
5102 }
5103
5104 fn html_collection_form_elements_named_item(
5105 &mut self,
5106 element: ElementHandle,
5107 name: &str,
5108 ) -> bt_script::Result<Option<ElementHandle>> {
5109 Session::form_elements_named_item(self, element, name)
5110 }
5111
5112 fn html_collection_form_elements_named_items(
5113 &mut self,
5114 element: ElementHandle,
5115 name: &str,
5116 ) -> bt_script::Result<Vec<ElementHandle>> {
5117 Session::form_elements_named_items(self, element, name)
5118 }
5119
5120 fn radio_node_list_set_value(
5121 &mut self,
5122 target: &RadioNodeListTarget,
5123 value: &str,
5124 ) -> bt_script::Result<()> {
5125 let RadioNodeListTarget::FormElements { element, name } = target;
5126 let items = self.html_collection_form_elements_named_items(*element, name)?;
5127 let mut matched = false;
5128
5129 for item in items {
5130 if self.element_tag_name(item)? != "input" {
5131 continue;
5132 }
5133
5134 let Some(input_type) = self.element_get_attribute(item, "type")? else {
5135 continue;
5136 };
5137 if !input_type.eq_ignore_ascii_case("radio") {
5138 continue;
5139 }
5140
5141 let should_check = !matched && self.element_value(item)? == value;
5142 if should_check {
5143 matched = true;
5144 }
5145 self.element_set_checked(item, should_check)?;
5146 }
5147
5148 Ok(())
5149 }
5150
5151 fn html_collection_select_options_items(
5152 &mut self,
5153 element: ElementHandle,
5154 ) -> bt_script::Result<Vec<ElementHandle>> {
5155 Session::select_options(self, element)
5156 }
5157
5158 fn html_collection_select_options_named_item(
5159 &mut self,
5160 element: ElementHandle,
5161 name: &str,
5162 ) -> bt_script::Result<Option<ElementHandle>> {
5163 Session::select_options_named_item(self, element, name)
5164 }
5165
5166 fn html_collection_select_options_add(
5167 &mut self,
5168 element: ElementHandle,
5169 option: ElementHandle,
5170 ) -> bt_script::Result<()> {
5171 Session::select_options_add(self, element, option)
5172 }
5173
5174 fn html_collection_select_options_remove(
5175 &mut self,
5176 element: ElementHandle,
5177 index: usize,
5178 ) -> bt_script::Result<()> {
5179 Session::select_options_remove(self, element, index)
5180 }
5181
5182 fn html_collection_select_selected_options_items(
5183 &mut self,
5184 element: ElementHandle,
5185 ) -> bt_script::Result<Vec<ElementHandle>> {
5186 Session::selected_options(self, element)
5187 }
5188
5189 fn html_collection_select_selected_options_named_item(
5190 &mut self,
5191 element: ElementHandle,
5192 name: &str,
5193 ) -> bt_script::Result<Option<ElementHandle>> {
5194 Session::selected_options_named_item(self, element, name)
5195 }
5196
5197 fn html_collection_document_links_items(&mut self) -> bt_script::Result<Vec<ElementHandle>> {
5198 Session::document_links(self)
5199 }
5200
5201 fn html_collection_document_links_named_item(
5202 &mut self,
5203 name: &str,
5204 ) -> bt_script::Result<Option<ElementHandle>> {
5205 Session::document_links_named_item(self, name)
5206 }
5207
5208 fn html_collection_document_anchors_items(&mut self) -> bt_script::Result<Vec<ElementHandle>> {
5209 Session::document_anchors(self)
5210 }
5211
5212 fn html_collection_document_anchors_named_item(
5213 &mut self,
5214 name: &str,
5215 ) -> bt_script::Result<Option<ElementHandle>> {
5216 Session::document_anchors_named_item(self, name)
5217 }
5218
5219 fn html_collection_document_children_items(&mut self) -> bt_script::Result<Vec<ElementHandle>> {
5220 Session::document_children(self)
5221 }
5222
5223 fn html_collection_document_children_named_item(
5224 &mut self,
5225 name: &str,
5226 ) -> bt_script::Result<Option<ElementHandle>> {
5227 Session::document_children_named_item(self, name)
5228 }
5229
5230 fn html_collection_window_frames_items(&mut self) -> bt_script::Result<Vec<ElementHandle>> {
5231 Session::window_frames(self)
5232 }
5233
5234 fn html_collection_window_frames_named_item(
5235 &mut self,
5236 name: &str,
5237 ) -> bt_script::Result<Option<ElementHandle>> {
5238 Session::window_frames_named_item(self, name)
5239 }
5240
5241 fn html_collection_map_areas_items(
5242 &mut self,
5243 element: ElementHandle,
5244 ) -> bt_script::Result<Vec<ElementHandle>> {
5245 Session::map_areas(self, element)
5246 }
5247
5248 fn html_collection_map_areas_named_item(
5249 &mut self,
5250 element: ElementHandle,
5251 name: &str,
5252 ) -> bt_script::Result<Option<ElementHandle>> {
5253 Session::map_areas_named_item(self, element, name)
5254 }
5255
5256 fn html_collection_table_bodies_items(
5257 &mut self,
5258 element: ElementHandle,
5259 ) -> bt_script::Result<Vec<ElementHandle>> {
5260 Session::table_bodies(self, element)
5261 }
5262
5263 fn html_collection_table_bodies_named_item(
5264 &mut self,
5265 element: ElementHandle,
5266 name: &str,
5267 ) -> bt_script::Result<Option<ElementHandle>> {
5268 Session::table_bodies_named_item(self, element, name)
5269 }
5270
5271 fn html_collection_table_rows_items(
5272 &mut self,
5273 element: ElementHandle,
5274 ) -> bt_script::Result<Vec<ElementHandle>> {
5275 Session::table_rows(self, element)
5276 }
5277
5278 fn html_collection_table_rows_named_item(
5279 &mut self,
5280 element: ElementHandle,
5281 name: &str,
5282 ) -> bt_script::Result<Option<ElementHandle>> {
5283 Session::table_rows_named_item(self, element, name)
5284 }
5285
5286 fn html_collection_row_cells_items(
5287 &mut self,
5288 element: ElementHandle,
5289 ) -> bt_script::Result<Vec<ElementHandle>> {
5290 Session::row_cells(self, element)
5291 }
5292
5293 fn html_collection_row_cells_named_item(
5294 &mut self,
5295 element: ElementHandle,
5296 name: &str,
5297 ) -> bt_script::Result<Option<ElementHandle>> {
5298 Session::row_cells_named_item(self, element, name)
5299 }
5300
5301 fn element_text_content(&mut self, element: ElementHandle) -> bt_script::Result<String> {
5302 let node_id = self.node_id_for_handle(element)?;
5303 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5304 return Err(ScriptError::new("invalid element handle"));
5305 };
5306
5307 match &node.kind {
5308 NodeKind::Element(_)
5309 | NodeKind::Document
5310 | NodeKind::Text(_)
5311 | NodeKind::Comment(_) => Ok(self.dom.text_content_for_node(node_id)),
5312 }
5313 }
5314
5315 fn element_set_text_content(
5316 &mut self,
5317 element: ElementHandle,
5318 value: &str,
5319 ) -> bt_script::Result<()> {
5320 let node_id = self.node_id_for_handle(element)?;
5321 self.dom
5322 .set_text_content(node_id, value)
5323 .map_err(ScriptError::new)
5324 }
5325
5326 fn element_inner_html(&mut self, element: ElementHandle) -> bt_script::Result<String> {
5327 let node_id = self.node_id_for_handle(element)?;
5328 self.dom
5329 .inner_html_for_node(node_id)
5330 .map_err(ScriptError::new)
5331 }
5332
5333 fn element_set_inner_html(
5334 &mut self,
5335 element: ElementHandle,
5336 value: &str,
5337 ) -> bt_script::Result<()> {
5338 let node_id = self.node_id_for_handle(element)?;
5339 self.dom
5340 .set_inner_html(node_id, value)
5341 .map_err(ScriptError::new)
5342 }
5343
5344 fn element_outer_html(&mut self, element: ElementHandle) -> bt_script::Result<String> {
5345 let node_id = self.node_id_for_handle(element)?;
5346 self.dom
5347 .outer_html_for_node(node_id)
5348 .map_err(ScriptError::new)
5349 }
5350
5351 fn element_set_outer_html(
5352 &mut self,
5353 element: ElementHandle,
5354 value: &str,
5355 ) -> bt_script::Result<()> {
5356 let node_id = self.node_id_for_handle(element)?;
5357 self.dom
5358 .set_outer_html(node_id, value)
5359 .map_err(ScriptError::new)
5360 }
5361
5362 fn element_insert_adjacent_html(
5363 &mut self,
5364 element: ElementHandle,
5365 position: &str,
5366 value: &str,
5367 ) -> bt_script::Result<()> {
5368 let node_id = self.node_id_for_handle(element)?;
5369 self.dom
5370 .insert_adjacent_html(node_id, position, value)
5371 .map_err(ScriptError::new)
5372 }
5373
5374 fn element_value(&mut self, element: ElementHandle) -> bt_script::Result<String> {
5375 let node_id = self.node_id_for_handle(element)?;
5376 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5377 return Err(ScriptError::new("invalid element handle"));
5378 };
5379 match &node.kind {
5380 NodeKind::Element(_)
5381 | NodeKind::Document
5382 | NodeKind::Text(_)
5383 | NodeKind::Comment(_) => Ok(self.dom.value_for_node(node_id)),
5384 }
5385 }
5386
5387 fn element_children(
5388 &mut self,
5389 element: ElementHandle,
5390 ) -> bt_script::Result<Vec<ElementHandle>> {
5391 let node_id = self.node_id_for_handle(element)?;
5392 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5393 return Err(ScriptError::new("invalid element handle"));
5394 };
5395
5396 let children = node
5397 .children
5398 .iter()
5399 .copied()
5400 .filter(|child_id| {
5401 matches!(
5402 self.dom
5403 .nodes()
5404 .get(child_id.index() as usize)
5405 .map(|node| &node.kind),
5406 Some(NodeKind::Element(_))
5407 )
5408 })
5409 .map(Self::node_id_to_handle)
5410 .collect();
5411
5412 Ok(children)
5413 }
5414
5415 fn element_tag_name(&mut self, element: ElementHandle) -> bt_script::Result<String> {
5416 let node_id = self.node_id_for_handle(element)?;
5417 let Some(record) = self.dom.nodes().get(node_id.index() as usize) else {
5418 return Err(ScriptError::new("invalid element handle"));
5419 };
5420
5421 let NodeKind::Element(element) = &record.kind else {
5422 return Err(ScriptError::new("invalid element handle"));
5423 };
5424
5425 Ok(element.tag_name.clone())
5426 }
5427
5428 fn element_base_uri(&mut self, _element: ElementHandle) -> bt_script::Result<String> {
5429 Ok(Session::document_base_uri(self))
5430 }
5431
5432 fn element_origin(&mut self, _element: ElementHandle) -> bt_script::Result<String> {
5433 Ok(Session::document_origin(self))
5434 }
5435
5436 fn element_is_content_editable(&mut self, element: ElementHandle) -> bt_script::Result<bool> {
5437 let node_id = self.node_id_for_handle(element)?;
5438 Ok(self.dom.is_content_editable(node_id))
5439 }
5440
5441 fn element_labels(&mut self, element: ElementHandle) -> bt_script::Result<Vec<ElementHandle>> {
5442 Session::element_labels(self, element)
5443 }
5444
5445 fn html_collection_named_item(
5446 &mut self,
5447 element: ElementHandle,
5448 name: &str,
5449 ) -> bt_script::Result<Option<ElementHandle>> {
5450 let node_id = self.node_id_for_handle(element)?;
5451 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5452 return Err(ScriptError::new("invalid element handle"));
5453 };
5454
5455 for child_id in &node.children {
5456 let Some(child) = self.dom.nodes().get(child_id.index() as usize) else {
5457 continue;
5458 };
5459 let NodeKind::Element(element) = &child.kind else {
5460 continue;
5461 };
5462 if element
5463 .attributes
5464 .get("id")
5465 .is_some_and(|value| value == name)
5466 || element
5467 .attributes
5468 .get("name")
5469 .is_some_and(|value| value == name)
5470 {
5471 return Ok(Some(Self::node_id_to_handle(*child_id)));
5472 }
5473 }
5474
5475 Ok(None)
5476 }
5477
5478 fn element_set_value(&mut self, element: ElementHandle, value: &str) -> bt_script::Result<()> {
5479 let node_id = self.node_id_for_handle(element)?;
5480 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5481 return Err(ScriptError::new("invalid element handle"));
5482 };
5483
5484 match &node.kind {
5485 NodeKind::Element(element) if element.tag_name == "select" => self
5486 .dom
5487 .set_select_value(node_id, value)
5488 .map_err(ScriptError::new),
5489 NodeKind::Element(_) => self
5490 .dom
5491 .set_form_control_value(node_id, value)
5492 .map_err(ScriptError::new),
5493 _ => Err(ScriptError::new("invalid element handle")),
5494 }
5495 }
5496
5497 fn element_checked(&mut self, element: ElementHandle) -> bt_script::Result<bool> {
5498 let node_id = self.node_id_for_handle(element)?;
5499 Ok(self.dom.checked_for_node(node_id).unwrap_or(false))
5500 }
5501
5502 fn element_set_checked(
5503 &mut self,
5504 element: ElementHandle,
5505 checked: bool,
5506 ) -> bt_script::Result<()> {
5507 let node_id = self.node_id_for_handle(element)?;
5508 self.dom
5509 .set_form_control_checked(node_id, checked)
5510 .map_err(ScriptError::new)
5511 }
5512
5513 fn element_indeterminate(&mut self, element: ElementHandle) -> bt_script::Result<bool> {
5514 let node_id = self.node_id_for_handle(element)?;
5515 Ok(self.dom.indeterminate_for_node(node_id).unwrap_or(false))
5516 }
5517
5518 fn element_set_indeterminate(
5519 &mut self,
5520 element: ElementHandle,
5521 indeterminate: bool,
5522 ) -> bt_script::Result<()> {
5523 let node_id = self.node_id_for_handle(element)?;
5524 self.dom
5525 .set_form_control_indeterminate(node_id, indeterminate)
5526 .map_err(ScriptError::new)
5527 }
5528
5529 fn element_get_attribute(
5530 &mut self,
5531 element: ElementHandle,
5532 name: &str,
5533 ) -> bt_script::Result<Option<String>> {
5534 let node_id = self.node_id_for_handle(element)?;
5535 self.dom
5536 .get_attribute(node_id, name)
5537 .map_err(ScriptError::new)
5538 }
5539
5540 fn element_set_attribute(
5541 &mut self,
5542 element: ElementHandle,
5543 name: &str,
5544 value: &str,
5545 ) -> bt_script::Result<()> {
5546 let node_id = self.node_id_for_handle(element)?;
5547 self.dom
5548 .set_attribute(node_id, name, value)
5549 .map_err(ScriptError::new)
5550 }
5551
5552 fn element_remove_attribute(
5553 &mut self,
5554 element: ElementHandle,
5555 name: &str,
5556 ) -> bt_script::Result<()> {
5557 let node_id = self.node_id_for_handle(element)?;
5558 self.dom
5559 .remove_attribute(node_id, name)
5560 .map_err(ScriptError::new)?;
5561 Ok(())
5562 }
5563
5564 fn element_has_attribute(
5565 &mut self,
5566 element: ElementHandle,
5567 name: &str,
5568 ) -> bt_script::Result<bool> {
5569 let node_id = self.node_id_for_handle(element)?;
5570 self.dom
5571 .has_attribute(node_id, name)
5572 .map_err(ScriptError::new)
5573 }
5574
5575 fn element_attribute_names(
5576 &mut self,
5577 element: ElementHandle,
5578 ) -> bt_script::Result<Vec<String>> {
5579 let node_id = self.node_id_for_handle(element)?;
5580 let Some(node) = self.dom.nodes().get(node_id.index() as usize) else {
5581 return Err(ScriptError::new("invalid element handle"));
5582 };
5583 let NodeKind::Element(element) = &node.kind else {
5584 return Err(ScriptError::new("invalid element handle"));
5585 };
5586
5587 Ok(element.attributes.keys().cloned().collect())
5588 }
5589
5590 fn element_toggle_attribute(
5591 &mut self,
5592 element: ElementHandle,
5593 name: &str,
5594 force: Option<bool>,
5595 ) -> bt_script::Result<bool> {
5596 let node_id = self.node_id_for_handle(element)?;
5597 self.dom
5598 .toggle_attribute(node_id, name, force)
5599 .map_err(ScriptError::new)
5600 }
5601
5602 fn element_append_child(
5603 &mut self,
5604 parent: ElementHandle,
5605 child: NodeHandle,
5606 ) -> bt_script::Result<()> {
5607 let parent_id = self.node_id_for_handle(parent)?;
5608 let child_id = self.node_id_for_node_handle(child)?;
5609 self.dom
5610 .append_child(parent_id, child_id)
5611 .map_err(ScriptError::new)
5612 }
5613
5614 fn element_insert_before(
5615 &mut self,
5616 parent: ElementHandle,
5617 child: NodeHandle,
5618 reference: Option<NodeHandle>,
5619 ) -> bt_script::Result<()> {
5620 let parent_id = self.node_id_for_handle(parent)?;
5621 let child_id = self.node_id_for_node_handle(child)?;
5622 match reference {
5623 Some(reference) => {
5624 let reference_id = self.node_id_for_node_handle(reference)?;
5625 self.dom
5626 .insert_before(parent_id, child_id, reference_id)
5627 .map_err(ScriptError::new)
5628 }
5629 None => self
5630 .dom
5631 .append_child(parent_id, child_id)
5632 .map_err(ScriptError::new),
5633 }
5634 }
5635
5636 fn element_replace_child(
5637 &mut self,
5638 parent: ElementHandle,
5639 new_child: NodeHandle,
5640 old_child: NodeHandle,
5641 ) -> bt_script::Result<()> {
5642 let parent_id = self.node_id_for_handle(parent)?;
5643 let new_child_id = self.node_id_for_node_handle(new_child)?;
5644 let old_child_id = self.node_id_for_node_handle(old_child)?;
5645 self.dom
5646 .replace_child(parent_id, new_child_id, old_child_id)
5647 .map_err(ScriptError::new)
5648 }
5649
5650 fn element_replace_children(
5651 &mut self,
5652 parent: ElementHandle,
5653 children: Vec<NodeHandle>,
5654 ) -> bt_script::Result<()> {
5655 let parent_id = self.node_id_for_handle(parent)?;
5656 let children = children
5657 .into_iter()
5658 .map(|child| self.node_id_for_node_handle(child))
5659 .collect::<Result<Vec<_>, _>>()?;
5660 self.dom
5661 .replace_children(parent_id, children)
5662 .map_err(ScriptError::new)
5663 }
5664
5665 fn element_append(
5666 &mut self,
5667 element: ElementHandle,
5668 children: Vec<NodeHandle>,
5669 ) -> bt_script::Result<()> {
5670 let parent_id = self.node_id_for_handle(element)?;
5671 let children = children
5672 .into_iter()
5673 .map(|child| self.node_id_for_node_handle(child))
5674 .collect::<Result<Vec<_>, _>>()?;
5675 self.dom
5676 .append_children(parent_id, children)
5677 .map_err(ScriptError::new)
5678 }
5679
5680 fn element_prepend(
5681 &mut self,
5682 element: ElementHandle,
5683 children: Vec<NodeHandle>,
5684 ) -> bt_script::Result<()> {
5685 let parent_id = self.node_id_for_handle(element)?;
5686 let children = children
5687 .into_iter()
5688 .map(|child| self.node_id_for_node_handle(child))
5689 .collect::<Result<Vec<_>, _>>()?;
5690 self.dom
5691 .prepend_children(parent_id, children)
5692 .map_err(ScriptError::new)
5693 }
5694
5695 fn element_before(
5696 &mut self,
5697 element: ElementHandle,
5698 children: Vec<NodeHandle>,
5699 ) -> bt_script::Result<()> {
5700 let node_id = self.node_id_for_handle(element)?;
5701 let Some(parent_id) = self
5702 .dom
5703 .nodes()
5704 .get(node_id.index() as usize)
5705 .and_then(|node| node.parent)
5706 else {
5707 return Ok(());
5708 };
5709 let children = children
5710 .into_iter()
5711 .map(|child| self.node_id_for_node_handle(child))
5712 .collect::<Result<Vec<_>, _>>()?;
5713 self.dom
5714 .insert_children_before(parent_id, node_id, children)
5715 .map_err(ScriptError::new)
5716 }
5717
5718 fn element_after(
5719 &mut self,
5720 element: ElementHandle,
5721 children: Vec<NodeHandle>,
5722 ) -> bt_script::Result<()> {
5723 let node_id = self.node_id_for_handle(element)?;
5724 let Some(parent_id) = self
5725 .dom
5726 .nodes()
5727 .get(node_id.index() as usize)
5728 .and_then(|node| node.parent)
5729 else {
5730 return Ok(());
5731 };
5732 let children = children
5733 .into_iter()
5734 .map(|child| self.node_id_for_node_handle(child))
5735 .collect::<Result<Vec<_>, _>>()?;
5736 self.dom
5737 .insert_children_after(parent_id, node_id, children)
5738 .map_err(ScriptError::new)
5739 }
5740
5741 fn element_remove(&mut self, element: ElementHandle) -> bt_script::Result<()> {
5742 let node_id = self.node_id_for_handle(element)?;
5743 self.dom.remove_node(node_id).map_err(ScriptError::new)
5744 }
5745
5746 fn element_query_selector(
5747 &mut self,
5748 element: ElementHandle,
5749 selector: &str,
5750 ) -> bt_script::Result<Option<ElementHandle>> {
5751 let node_id = self.node_id_for_handle(element)?;
5752 self.query_selector_handle(Some(node_id), selector)
5753 }
5754
5755 fn element_query_selector_all(
5756 &mut self,
5757 element: ElementHandle,
5758 selector: &str,
5759 ) -> bt_script::Result<Vec<ElementHandle>> {
5760 let node_id = self.node_id_for_handle(element)?;
5761 self.query_selector_handles(Some(node_id), selector)
5762 }
5763
5764 fn element_matches(
5765 &mut self,
5766 element: ElementHandle,
5767 selector: &str,
5768 ) -> bt_script::Result<bool> {
5769 let node_id = self.node_id_for_handle(element)?;
5770 self.element_matches_selector(node_id, selector)
5771 }
5772
5773 fn element_closest(
5774 &mut self,
5775 element: ElementHandle,
5776 selector: &str,
5777 ) -> bt_script::Result<Option<ElementHandle>> {
5778 let node_id = self.node_id_for_handle(element)?;
5779 self.element_closest_selector(node_id, selector)
5780 }
5781
5782 fn register_event_listener_with_capture(
5783 &mut self,
5784 target: ListenerTarget,
5785 event_type: &str,
5786 capture: bool,
5787 handler: ScriptFunction,
5788 ) -> bt_script::Result<()> {
5789 let target = match target {
5790 ListenerTarget::Window => SessionEventTarget::Window,
5791 ListenerTarget::Document => SessionEventTarget::Document,
5792 ListenerTarget::Element(handle) => {
5793 let node_id = self.node_id_for_handle(handle)?;
5794 if self.dom.nodes().get(node_id.index() as usize).is_none() {
5795 return Err(ScriptError::new("invalid element handle"));
5796 }
5797 SessionEventTarget::Element(node_id)
5798 }
5799 };
5800
5801 self.register_script_listener(target, event_type.to_string(), capture, handler);
5802 Ok(())
5803 }
5804}
5805
5806fn is_checkable_input_type(input_type: Option<&str>) -> bool {
5807 matches!(input_type.unwrap_or("text"), "checkbox" | "radio")
5808}
5809
5810fn is_submit_control(tag_name: &str, input_type: Option<&str>) -> bool {
5811 match tag_name {
5812 "button" => !matches!(input_type, Some("button") | Some("reset")),
5813 "input" => matches!(input_type.unwrap_or("text"), "submit" | "image"),
5814 _ => false,
5815 }
5816}
5817
5818#[cfg(test)]
5819mod tests {
5820 use std::collections::BTreeMap;
5821
5822 use super::Session;
5823 use super::SessionConfig;
5824 use super::SessionEventTarget;
5825
5826 #[test]
5827 fn session_bootstraps_empty_dom_and_storage_seed() {
5828 let mut local_storage = BTreeMap::new();
5829 local_storage.insert("token".to_string(), "abc".to_string());
5830 let config = SessionConfig {
5831 url: "https://app.local/".to_string(),
5832 html: Some("<main id='app'></main>".to_string()),
5833 local_storage,
5834 };
5835
5836 let session = Session::new(config).expect("session should parse HTML");
5837 assert_eq!(session.dom().source_html(), Some("<main id='app'></main>"));
5838 assert_eq!(session.dom().node_count(), 2);
5839 assert_eq!(
5840 session
5841 .mocks()
5842 .storage()
5843 .local()
5844 .get("token")
5845 .map(String::as_str),
5846 Some("abc")
5847 );
5848 }
5849
5850 #[test]
5851 fn session_rejects_malformed_html() {
5852 let config = SessionConfig {
5853 url: "https://app.local/".to_string(),
5854 html: Some("<main><span></main>".to_string()),
5855 local_storage: BTreeMap::new(),
5856 };
5857
5858 let error = Session::new(config).expect_err("malformed HTML should fail");
5859 assert!(error.to_string().contains("mismatched closing tag"));
5860 }
5861
5862 #[test]
5863 fn session_bootstraps_inline_scripts_in_document_order() {
5864 let config = SessionConfig {
5865 url: "https://app.local/".to_string(),
5866 html: Some(
5867 "<main id='out'></main><script>document.getElementById('out').textContent = 'Hello';</script>"
5868 .to_string(),
5869 ),
5870 local_storage: BTreeMap::new(),
5871 };
5872
5873 let session = Session::new(config).expect("session should execute inline scripts");
5874 assert_eq!(
5875 session.dom().dump_dom(),
5876 "#document\n <main id=\"out\">\n \"Hello\"\n </main>\n <script>\n \"document.getElementById('out').textContent = 'Hello';\"\n </script>"
5877 );
5878 }
5879
5880 #[test]
5881 fn session_registers_event_listeners_from_inline_scripts() {
5882 let config = SessionConfig {
5883 url: "https://app.local/".to_string(),
5884 html: Some(
5885 "<button id='run'></button><script>document.getElementById('run').addEventListener('click', () => { document.getElementById('run').textContent = 'clicked'; });</script>"
5886 .to_string(),
5887 ),
5888 local_storage: BTreeMap::new(),
5889 };
5890
5891 let session = Session::new(config).expect("session should register listeners");
5892 assert_eq!(session.script_event_listeners.len(), 1);
5893 assert_eq!(session.script_event_listeners[0].event_type, "click");
5894 assert!(!session.script_event_listeners[0].capture);
5895 match &session.script_event_listeners[0].target {
5896 SessionEventTarget::Element(node_id) => assert_eq!(node_id.index(), 1),
5897 other => panic!("unexpected listener target: {:?}", other),
5898 }
5899 assert!(
5900 session.script_event_listeners[0]
5901 .handler
5902 .body_source
5903 .contains("textContent = 'clicked'")
5904 );
5905 }
5906
5907 #[test]
5908 fn session_bubbles_click_events_beyond_target_phase() {
5909 let config = SessionConfig {
5910 url: "https://app.local/".to_string(),
5911 html: Some(
5912 "<div id='parent'><div id='child'></div></div><div id='out'></div><script>document.getElementById('child').addEventListener('click', () => { document.getElementById('out').textContent = 'target'; }); document.getElementById('parent').addEventListener('click', () => { document.getElementById('out').textContent += ':parent'; }); document.addEventListener('click', () => { document.getElementById('out').textContent += ':document'; }); window.addEventListener('click', () => { document.getElementById('out').textContent += ':window'; });</script>"
5913 .to_string(),
5914 ),
5915 local_storage: BTreeMap::new(),
5916 };
5917
5918 let mut session = Session::new(config).expect("session should register listeners");
5919 let child_id = session.dom().select("#child").unwrap()[0];
5920 let out_id = session.dom().select("#out").unwrap()[0];
5921
5922 session.click_node(child_id).expect("click should bubble");
5923
5924 assert_eq!(
5925 session.dom().text_content_for_node(out_id),
5926 "target:parent:document:window"
5927 );
5928 }
5929
5930 #[test]
5931 fn session_stop_propagation_blocks_ancestor_listeners() {
5932 let config = SessionConfig {
5933 url: "https://app.local/".to_string(),
5934 html: Some(
5935 "<div id='parent'><div id='child'></div></div><div id='out'></div><script>document.getElementById('child').addEventListener('click', (event) => { event.stopPropagation(); document.getElementById('out').textContent = 'target'; }); document.getElementById('parent').addEventListener('click', () => { document.getElementById('out').textContent += ':parent'; }); document.addEventListener('click', () => { document.getElementById('out').textContent += ':document'; });</script>"
5936 .to_string(),
5937 ),
5938 local_storage: BTreeMap::new(),
5939 };
5940
5941 let mut session = Session::new(config).expect("session should register listeners");
5942 let child_id = session.dom().select("#child").unwrap()[0];
5943 let out_id = session.dom().select("#out").unwrap()[0];
5944
5945 session
5946 .click_node(child_id)
5947 .expect("click should still succeed");
5948
5949 assert_eq!(session.dom().text_content_for_node(out_id), "target");
5950 }
5951
5952 #[test]
5953 fn session_click_default_action_is_cancelable() {
5954 let config = SessionConfig {
5955 url: "https://app.local/".to_string(),
5956 html: Some(
5957 "<input id='agree' type='checkbox'><div id='out'></div><script>document.getElementById('agree').addEventListener('click', (event) => { event.preventDefault(); }); document.getElementById('agree').addEventListener('change', () => { document.getElementById('out').textContent = String(document.getElementById('agree').checked); });</script>"
5958 .to_string(),
5959 ),
5960 local_storage: BTreeMap::new(),
5961 };
5962
5963 let mut session = Session::new(config).expect("session should register listeners");
5964 let agree_id = session.dom().select("#agree").unwrap()[0];
5965 let out_id = session.dom().select("#out").unwrap()[0];
5966
5967 session
5968 .click_node(agree_id)
5969 .expect("canceling click should still succeed");
5970
5971 assert_eq!(session.dom().checked_for_node(agree_id), Some(false));
5972 assert_eq!(session.dom().text_content_for_node(out_id), "");
5973 }
5974
5975 #[test]
5976 fn session_focus_and_blur_dispatch_in_order() {
5977 let config = SessionConfig {
5978 url: "https://app.local/".to_string(),
5979 html: Some(
5980 "<input id='first'><input id='second'><div id='out'></div><script>document.getElementById('first').addEventListener('blur', () => { document.getElementById('second').textContent = 'after-blur'; }); document.getElementById('second').addEventListener('focus', () => { document.getElementById('out').textContent = document.getElementById('second').textContent; });</script>"
5981 .to_string(),
5982 ),
5983 local_storage: BTreeMap::new(),
5984 };
5985
5986 let mut session = Session::new(config).expect("session should register listeners");
5987 let first_id = session.dom().select("#first").unwrap()[0];
5988 let second_id = session.dom().select("#second").unwrap()[0];
5989 let out_id = session.dom().select("#out").unwrap()[0];
5990
5991 session.focus_node(first_id).expect("focus should work");
5992 session
5993 .focus_node(second_id)
5994 .expect("focus should blur the previous element");
5995
5996 assert_eq!(session.dom().text_content_for_node(second_id), "after-blur");
5997 assert_eq!(session.dom().text_content_for_node(out_id), "after-blur");
5998 }
5999
6000 #[test]
6001 fn session_set_select_value_updates_option_state_and_dispatches_change() {
6002 let config = SessionConfig {
6003 url: "https://app.local/".to_string(),
6004 html: Some(
6005 "<select id='mode'><option value='a'>A</option><option value='b'>B</option></select><div id='out'></div><script>document.getElementById('mode').addEventListener('change', () => { document.getElementById('out').textContent = document.getElementById('mode').value; });</script>"
6006 .to_string(),
6007 ),
6008 local_storage: BTreeMap::new(),
6009 };
6010
6011 let mut session = Session::new(config).expect("session should register listeners");
6012 let mode_id = session.dom().select("#mode").unwrap()[0];
6013 let option_ids = session.dom().select("option").unwrap();
6014 let out_id = session.dom().select("#out").unwrap()[0];
6015
6016 session
6017 .set_select_value_node(mode_id, "b")
6018 .expect("select should accept a matching value");
6019
6020 assert_eq!(session.dom().value_for_node(mode_id), "b");
6021 assert_eq!(
6022 session.dom().select("[selected]").unwrap(),
6023 vec![option_ids[1]]
6024 );
6025 assert_eq!(session.dom().text_content_for_node(out_id), "b");
6026 }
6027
6028 #[test]
6029 fn session_bootstraps_form_control_state_through_script_bindings() {
6030 let config = SessionConfig {
6031 url: "https://app.local/".to_string(),
6032 html: Some(
6033 "<input id='name'><input id='agree' type='checkbox'><div id='out'></div><script>document.getElementById('name').value = 'Alice'; document.getElementById('agree').checked = true; document.getElementById('out').textContent = document.getElementById('name').value + ':' + String(document.getElementById('agree').checked);</script>"
6034 .to_string(),
6035 ),
6036 local_storage: BTreeMap::new(),
6037 };
6038
6039 let session = Session::new(config).expect("session should execute form-control scripts");
6040 let name_id = session.dom().select("#name").unwrap()[0];
6041 let agree_id = session.dom().select("#agree").unwrap()[0];
6042 let out_id = session.dom().select("#out").unwrap()[0];
6043
6044 assert_eq!(session.dom().value_for_node(name_id), "Alice");
6045 assert_eq!(session.dom().checked_for_node(agree_id), Some(true));
6046 assert_eq!(session.dom().text_content_for_node(out_id), "Alice:true");
6047 }
6048
6049 #[test]
6050 fn session_click_toggles_checkbox_and_dispatches_input_listener() {
6051 let config = SessionConfig {
6052 url: "https://app.local/".to_string(),
6053 html: Some(
6054 "<input id='agree' type='checkbox'><div id='out'></div><script>document.getElementById('agree').addEventListener('input', () => { document.getElementById('out').textContent = String(document.getElementById('agree').checked); });</script>"
6055 .to_string(),
6056 ),
6057 local_storage: BTreeMap::new(),
6058 };
6059
6060 let mut session = Session::new(config).expect("session should register listeners");
6061 let agree_id = session.dom().select("#agree").unwrap()[0];
6062 let out_id = session.dom().select("#out").unwrap()[0];
6063
6064 session
6065 .click_node(agree_id)
6066 .expect("click should toggle checkbox");
6067
6068 assert_eq!(session.dom().checked_for_node(agree_id), Some(true));
6069 assert_eq!(session.dom().text_content_for_node(out_id), "true");
6070 }
6071
6072 #[test]
6073 fn session_clicking_submit_button_dispatches_form_submit_listener() {
6074 let config = SessionConfig {
6075 url: "https://app.local/".to_string(),
6076 html: Some(
6077 "<form id='profile'><input id='name'><button id='submit' type='submit'>Save</button></form><div id='out'></div><script>document.getElementById('profile').addEventListener('submit', () => { document.getElementById('out').textContent = document.getElementById('name').value; });</script>"
6078 .to_string(),
6079 ),
6080 local_storage: BTreeMap::new(),
6081 };
6082
6083 let mut session = Session::new(config).expect("session should register submit listener");
6084 let name_id = session.dom().select("#name").unwrap()[0];
6085 let submit_id = session.dom().select("#submit").unwrap()[0];
6086 let out_id = session.dom().select("#out").unwrap()[0];
6087
6088 session
6089 .type_text_node(name_id, "Alice")
6090 .expect("typing should update the input");
6091 session
6092 .click_node(submit_id)
6093 .expect("clicking submit should dispatch submit");
6094
6095 assert_eq!(session.dom().value_for_node(name_id), "Alice");
6096 assert_eq!(session.dom().text_content_for_node(out_id), "Alice");
6097 }
6098}