Skip to main content

bt_runtime/
lib.rs

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}