1use crate::engine::html::{DomTree, HtmlNodeType};
11use crate::engine::js::web_apis::dom::computed_style;
12use crate::engine::js::web_apis::dom::document::IframeDocument;
13use crate::engine::layouter::dom_snapshot::DomSnapshot;
14use crate::engine::permissions::Permissions;
15use crate::engine::tree::NodeRef;
16use pixi_byte::value::jsobject::JSObject;
17use pixi_byte::{JSError, JSValue};
18use std::cell::{Cell, RefCell};
19use std::collections::{HashMap, HashSet};
20use std::rc::Rc;
21use std::time::{Duration, Instant};
22
23pub mod devtools;
24pub mod processor;
25pub use devtools::JsDevToolsRequest;
26pub use processor::{JsProcessor, JsTask, JsTaskResult};
27
28mod common;
29pub(crate) mod runtime;
30mod trace;
31pub use trace::{JsActiveTrace, JsTraceBatch, JsTraceEvent, JsTraceKind};
32pub(crate) mod web_apis;
33pub use web_apis::missing_api_detector;
34
35pub fn default_trace_enabled() -> bool {
37 trace::default_enabled()
38}
39
40pub(crate) use common::{
42 host_read_only_property, is_callable, node_dom_id, with_host, with_host_mut,
43};
44pub(crate) use web_apis::dom::document::expose_node;
45pub(crate) use web_apis::dom::events::{event_flag, make_event};
46pub(crate) use web_apis::network::{make_fetch_response, resolve_xml_http_request};
47
48pub(crate) struct JsTimer {
53 id: u64,
54 callback: JSValue,
55 arguments: Vec<JSValue>,
56 deadline: Instant,
57 interval: Option<Duration>,
58}
59
60pub(crate) struct JsFetchCapability {
61 resolve: JSValue,
62 reject: JSValue,
63}
64
65#[derive(Debug)]
67pub struct JsFetchRequest {
68 pub(crate) id: u64,
69 pub(crate) url: String,
70 pub(crate) method: String,
71 pub(crate) headers: Vec<(String, String)>,
72 pub(crate) body: Vec<u8>,
73}
74
75#[derive(Debug)]
77pub struct JsIframeFetchRequest {
78 pub dom_id: u64,
80 pub url: String,
82}
83
84#[derive(Debug)]
87pub struct IframeContentSnapshot {
88 pub iframe_dom_id: u64,
90 pub content: DomSnapshot,
92}
93
94#[derive(Debug)]
96pub(crate) struct JsDynamicScriptRequest {
97 pub(crate) node_id: u64,
98 pub(crate) source: JsDynamicScriptSource,
99}
100
101#[derive(Debug)]
102pub(crate) enum JsDynamicScriptSource {
103 Inline(String),
104 External(String),
105}
106
107#[derive(Debug)]
108pub(crate) struct JsDynamicStyleRequest {
109 pub(crate) node_id: u64,
110 pub(crate) url: String,
111}
112
113#[derive(Debug)]
115pub(crate) struct JsDynamicImageRequest {
117 pub(crate) source: String,
118}
119
120#[derive(Debug, Clone, Copy, Default, PartialEq)]
122pub struct JsLayoutMetrics {
123 pub offset_left: f64,
124 pub offset_top: f64,
125 pub offset_width: f64,
126 pub offset_height: f64,
127 pub client_width: f64,
128 pub client_height: f64,
129 pub rect_left: f64,
130 pub rect_top: f64,
131 pub rect_width: f64,
132 pub rect_height: f64,
133}
134
135#[derive(Debug)]
137pub struct JsFetchResponse {
138 pub(crate) url: String,
139 pub(crate) status: u16,
140 pub(crate) status_text: String,
141 pub(crate) redirected: bool,
142 pub(crate) body: Vec<u8>,
143 pub(crate) headers: Vec<(String, String)>,
144}
145
146#[derive(Clone)]
148pub(crate) struct CustomElementDefinition {
149 pub(crate) constructor: JSValue,
150 pub(crate) connected_callback: Option<JSValue>,
151 pub(crate) disconnected_callback: Option<JSValue>,
152 pub(crate) attribute_changed_callback: Option<JSValue>,
153 pub(crate) observed_attributes: Vec<String>,
154 pub(crate) when_defined_resolvers: Vec<JSValue>,
156}
157
158pub struct JsHost {
164 pub(crate) dom: Rc<DomTree>,
165 pub(crate) refs: HashMap<
166 u64,
167 std::rc::Weak<std::cell::RefCell<crate::engine::tree::TreeNode<HtmlNodeType>>>,
168 >,
169 pub(crate) objects: HashMap<u64, Rc<RefCell<JSObject>>>,
172 pub(crate) styles: HashMap<u64, Rc<RefCell<JSObject>>>,
174 pub(crate) canvas_contexts: HashMap<u64, Rc<RefCell<JSObject>>>,
176 pub(crate) namespaces: HashMap<u64, String>,
178 pub(crate) element_prototype: Rc<RefCell<JSObject>>,
179 pub(crate) element_constructor: Rc<RefCell<JSObject>>,
180 pub(crate) node_prototype: Option<Rc<RefCell<JSObject>>>,
181 pub(crate) fragment_prototype: Option<Rc<RefCell<JSObject>>>,
182 pub(crate) shadow_root_prototype: Option<Rc<RefCell<JSObject>>>,
185 pub(crate) document: Option<Rc<RefCell<JSObject>>>,
186 pub(crate) document_implementation: Option<Rc<RefCell<JSObject>>>,
187 pub(crate) iframe_documents: HashMap<u64, Rc<RefCell<IframeDocument>>>,
190 pub(crate) failed_iframe_fetches: HashSet<u64>,
193 pub(crate) document_event_listeners: HashMap<String, Vec<JSValue>>,
194 pub(crate) document_selection: Option<Rc<RefCell<JSObject>>>,
196 pub(crate) element_event_listeners: HashMap<u64, HashMap<String, Vec<(JSValue, bool)>>>,
200 pub(crate) window_inline_event_handlers: HashMap<String, String>,
205 pub(crate) active_element: Option<u64>,
206 pub(crate) fullscreen_element: Option<u64>,
209 pub(crate) detached_nodes: HashMap<u64, NodeRef<HtmlNodeType>>,
211 pub(crate) detached_documents: Vec<Rc<RefCell<JSObject>>>,
214 pub(crate) main_document_dom_id: Option<u64>,
217 pub(crate) timers: Vec<JsTimer>,
218 pub(crate) fetch_requests: Vec<JsFetchRequest>,
219 pub(crate) iframe_fetch_requests: Vec<JsIframeFetchRequest>,
220 pub(crate) navigation_requests: Vec<String>,
223 pub(crate) pending_iframe_fetches: std::collections::HashSet<u64>,
226 pub(crate) dynamic_script_requests: Vec<JsDynamicScriptRequest>,
227 pub(crate) queued_dynamic_scripts: HashSet<u64>,
228 pub(crate) dynamic_style_requests: Vec<JsDynamicStyleRequest>,
229 pub(crate) queued_dynamic_styles: HashSet<u64>,
230 pub(crate) dynamic_image_requests: Vec<JsDynamicImageRequest>,
231 pub(crate) queued_dynamic_images: HashSet<u64>,
232 pub(crate) fetch_capabilities: HashMap<u64, JsFetchCapability>,
233 pub(crate) fetch_urls: HashMap<u64, String>,
234 pub(crate) fetch_signals: HashMap<u64, Rc<RefCell<JSObject>>>,
240 pub(crate) xhr_requests: HashMap<u64, Rc<RefCell<JSObject>>>,
241 pub(crate) constructing_fetch_capability: Option<JsFetchCapability>,
242 pub(crate) devtools_requests: Vec<JsDevToolsRequest>,
243 pub(crate) devtools_capabilities: HashMap<u64, devtools::JsDevToolsCapability>,
244 pub(crate) constructing_devtools_capability: Option<devtools::JsDevToolsCapability>,
245 pub(crate) next_devtools_id: u64,
246 pub(crate) console_results: Vec<(u64, String)>,
247 pub(crate) custom_elements: HashMap<String, CustomElementDefinition>,
249 pub(crate) shadow_roots: HashMap<u64, u64>,
251 pub(crate) local_storage: HashMap<String, String>,
253 pub(crate) session_storage: HashMap<String, String>,
254 pub(crate) document_cookies: HashMap<String, String>,
256 pub(crate) current_script_dom_id: Option<u64>,
259 pub(crate) adopted_style_sheets: Option<JSValue>,
261 pub(crate) document_fonts: Option<Rc<RefCell<JSObject>>>,
263 pub(crate) document_url: String,
264 pub(crate) origin: String,
266 pub(crate) permissions: Permissions,
269 pub(crate) viewport: (f64, f64),
270 pub(crate) layout_metrics: HashMap<usize, JsLayoutMetrics>,
272 pub(crate) layout_metrics_by_dom_id: HashMap<u64, JsLayoutMetrics>,
274 pub(crate) computed_styles: HashMap<u64, Vec<(String, String)>>,
279 pub(crate) computed_style_declarations: HashMap<u64, Rc<RefCell<JSObject>>>,
282 pub(crate) computed_style_resolver: computed_style::Resolver,
285 pub(crate) next_fetch_id: u64,
286 pub(crate) next_timer_id: u64,
287 pub(crate) time_origin: Instant,
288 pub(crate) dom_content_loaded_fired: bool,
289 pub(crate) window_load_fired: bool,
290 pub(crate) next_id: u64,
291 pub(crate) needs_redraw: Rc<Cell<bool>>,
292 pub(crate) trace: trace::JsTraceSession,
294}
295
296impl JsHost {
297 pub(crate) fn dom_id_for_node(&self, node: &NodeRef<HtmlNodeType>) -> Option<u64> {
299 self.refs.iter().find_map(|(&dom_id, weak)| {
300 if std::ptr::eq(weak.as_ptr(), Rc::as_ptr(node)) {
301 Some(dom_id)
302 } else {
303 None
304 }
305 })
306 }
307}
308
309pub struct JsRuntime {
311 engine: pixi_byte::JSEngine,
312 needs_redraw: Rc<Cell<bool>>,
313}
314
315impl JsRuntime {
316 pub fn new(dom: Rc<DomTree>) -> Self {
318 let needs_redraw = Rc::new(Cell::new(false));
319 let (element_prototype, element_constructor) =
320 web_apis::dom::element::make_element_interface();
321 let mut host = JsHost {
322 dom,
323 refs: HashMap::new(),
324 objects: HashMap::new(),
325 styles: HashMap::new(),
326 canvas_contexts: HashMap::new(),
327 namespaces: HashMap::new(),
328 element_prototype,
329 element_constructor,
330 node_prototype: None,
331 fragment_prototype: None,
332 shadow_root_prototype: None,
333 document: None,
334 document_implementation: None,
335 iframe_documents: HashMap::new(),
336 failed_iframe_fetches: HashSet::new(),
337 document_event_listeners: HashMap::new(),
338 document_selection: None,
339 element_event_listeners: HashMap::new(),
340 window_inline_event_handlers: HashMap::new(),
341 active_element: None,
342 fullscreen_element: None,
343 detached_nodes: HashMap::new(),
344 timers: Vec::new(),
345 fetch_requests: Vec::new(),
346 iframe_fetch_requests: Vec::new(),
347 navigation_requests: Vec::new(),
348 pending_iframe_fetches: std::collections::HashSet::new(),
349 dynamic_script_requests: Vec::new(),
350 queued_dynamic_scripts: HashSet::new(),
351 dynamic_style_requests: Vec::new(),
352 queued_dynamic_styles: HashSet::new(),
353 dynamic_image_requests: Vec::new(),
354 queued_dynamic_images: HashSet::new(),
355 fetch_capabilities: HashMap::new(),
356 fetch_urls: HashMap::new(),
357 fetch_signals: HashMap::new(),
358 xhr_requests: HashMap::new(),
359 constructing_fetch_capability: None,
360 devtools_requests: Vec::new(),
361 devtools_capabilities: HashMap::new(),
362 constructing_devtools_capability: None,
363 next_devtools_id: 0,
364 console_results: Vec::new(),
365 custom_elements: HashMap::new(),
366 shadow_roots: HashMap::new(),
367 local_storage: HashMap::new(),
368 session_storage: HashMap::new(),
369 document_cookies: HashMap::new(),
370 current_script_dom_id: None,
371 adopted_style_sheets: None,
372 document_fonts: None,
373 document_url: "about:blank".to_string(),
374 origin: "null".to_string(),
375 permissions: Permissions::none(),
376 viewport: (800.0, 600.0),
377 layout_metrics: HashMap::new(),
378 layout_metrics_by_dom_id: HashMap::new(),
379 computed_styles: HashMap::new(),
380 computed_style_declarations: HashMap::new(),
381 computed_style_resolver: computed_style::Resolver::new(),
382 next_fetch_id: 0,
383 next_timer_id: 0,
384 time_origin: Instant::now(),
385 dom_content_loaded_fired: false,
386 window_load_fired: false,
387 next_id: 0,
388 needs_redraw: Rc::clone(&needs_redraw),
389 detached_documents: Vec::new(),
390 main_document_dom_id: None,
391 trace: trace::JsTraceSession::new(),
392 };
393 Self::register_window_event_handlers(&mut host);
394
395 let host = Rc::new(RefCell::new(host));
396
397 let mut engine = pixi_byte::JSEngine::new();
398 engine.set_host(host);
399
400 web_apis::console::install_console(&mut engine);
401 trace::install_caught_exception_hook(&mut engine);
402 web_apis::dom::document::install_document(&mut engine);
403
404 web_apis::observers::install_mutation_observer(&mut engine);
405 web_apis::observers::install_resize_observer(&mut engine);
406 web_apis::observers::install_intersection_observer(&mut engine);
407 web_apis::timers::install_timers(&mut engine);
408 web_apis::performance::install_performance(&mut engine);
409 web_apis::message_channel::install_message_channel(&mut engine);
410 runtime::microtasks::install_microtasks(&mut engine);
411 web_apis::network::install_headers(&mut engine);
412 web_apis::network::install_request(&mut engine);
413 web_apis::network::install_fetch(&mut engine);
414 web_apis::network::install_xml_http_request(&mut engine);
415 web_apis::network::install_response(&mut engine);
416 web_apis::abort::install_abort_apis(&mut engine);
417 web_apis::file::install_blob_apis(&mut engine);
418 devtools::install(&mut engine);
419 web_apis::url::install_url_apis(&mut engine);
420 web_apis::encoding::install_encoding_apis(&mut engine);
421 web_apis::browser_env::install_browser_environment(&mut engine);
422 web_apis::browser_env::install_global_aliases(&mut engine);
423 web_apis::misc::install_misc_apis(&mut engine);
424 web_apis::dom::custom_elements::install_custom_elements(&mut engine);
425
426 Self {
427 engine,
428 needs_redraw,
429 }
430 }
431
432 pub fn set_viewport(&mut self, width: f32, height: f32) {
434 let width = width.max(0.0) as f64;
435 let height = height.max(0.0) as f64;
436 let mut global = self.engine.global_mut().borrow_mut();
437 global.set("innerWidth".to_string(), JSValue::from_number(width));
438 global.set("innerHeight".to_string(), JSValue::from_number(height));
439 global.set("outerWidth".to_string(), JSValue::from_number(width));
440 global.set("outerHeight".to_string(), JSValue::from_number(height));
441 drop(global);
442 with_host_mut(self.engine.vm(), |host| host.viewport = (width, height));
443 }
444
445 #[cfg(test)]
448 pub(crate) fn set_layout_metrics(&mut self, metrics: HashMap<usize, JsLayoutMetrics>) {
449 with_host_mut(self.engine.vm(), |host| host.layout_metrics = metrics);
450 }
451
452 pub(crate) fn set_layout_metrics_by_dom_id(&mut self, metrics: HashMap<u64, JsLayoutMetrics>) {
455 with_host_mut(self.engine.vm(), |host| {
456 host.layout_metrics_by_dom_id = metrics
457 });
458 }
459
460 pub(crate) fn set_computed_styles_by_dom_id(
462 &mut self,
463 computed_styles: HashMap<u64, Vec<(String, String)>>,
464 ) {
465 with_host_mut(self.engine.vm(), |host| {
466 host.computed_style_resolver.invalidate();
469 host.computed_styles = computed_styles
470 });
471 }
472
473 pub fn set_language(&mut self, language: &str) {
475 let language = language.trim();
476 if language.is_empty() {
477 return;
478 }
479 let mut languages = vec![JSValue::from_string(language.to_string())];
480 if let Some(base) = language.split('-').next()
481 && !base.eq_ignore_ascii_case(language)
482 {
483 languages.push(JSValue::from_string(base.to_string()));
484 }
485 if !language.eq_ignore_ascii_case("en-US") {
486 languages.push(JSValue::from_string("en-US".to_string()));
487 }
488
489 let global = self.engine.global_mut().borrow_mut();
490 let Some(navigator) = global.get("navigator").as_object() else {
491 return;
492 };
493 drop(global);
494 let mut navigator = navigator.borrow_mut();
495 navigator.define_property(
496 "language".to_string(),
497 host_read_only_property(JSValue::from_string(language.to_string())),
498 );
499 navigator.define_property(
500 "languages".to_string(),
501 host_read_only_property(self.engine.vm().array_from_values(languages)),
502 );
503 }
504
505 fn begin_trace(
507 &self,
508 kind: JsTraceKind,
509 label: String,
510 detail: String,
511 source: Option<String>,
512 ) -> Option<trace::JsTraceSpan> {
513 if !trace::is_enabled(self.engine.vm()) {
514 return None;
515 }
516 let active_id = trace::begin_active(
517 self.engine.vm(),
518 kind,
519 label.clone(),
520 detail.clone(),
521 source.clone(),
522 );
523 Some(trace::JsTraceSpan {
524 active_id,
525 started: Instant::now(),
526 kind,
527 label,
528 detail,
529 source,
530 })
531 }
532
533 fn end_trace(
535 &mut self,
536 span: Option<trace::JsTraceSpan>,
537 error: Option<String>,
538 stack: Vec<String>,
539 ) {
540 let Some(span) = span else {
541 return;
542 };
543 trace::end_active(self.engine.vm(), span.active_id);
544 let duration_us = span.started.elapsed().as_micros() as u64;
545 let variables = if error.is_some() {
546 self.engine.last_error_variables().to_vec()
547 } else {
548 Vec::new()
549 };
550 let program_counter = error.as_ref().and_then(|_| self.engine.last_error_pc());
551 trace::record(
552 self.engine.vm(),
553 span.kind,
554 span.label,
555 span.detail,
556 duration_us,
557 error,
558 stack,
559 span.source,
560 variables,
561 program_counter,
562 );
563 }
564
565 fn trace_error_stack(&self, error: &Option<String>) -> Vec<String> {
567 if error.is_some() {
568 self.engine.last_error_stack().to_vec()
569 } else {
570 Vec::new()
571 }
572 }
573
574 pub(crate) fn take_trace_batch(&mut self) -> JsTraceBatch {
576 trace::take_batch(self.engine.vm())
577 }
578
579 pub(crate) fn set_js_tracing(&mut self, enabled: bool) {
581 trace::set_enabled(self.engine.vm(), enabled);
582 }
583
584 pub(crate) fn clear_js_trace(&mut self) {
586 trace::clear(self.engine.vm());
587 }
588
589 pub(crate) fn evaluate_console(&mut self, id: u64, source: &str) {
591 let result = match self.engine.eval(source) {
592 Ok(value) => trace::format_console_value(&value),
593 Err(error) => format!("Uncaught {error}"),
594 };
595 with_host_mut(self.engine.vm(), |host| {
596 host.console_results.push((id, result))
597 });
598 }
599
600 pub(crate) fn take_console_results(&mut self) -> Vec<(u64, String)> {
602 with_host_mut(self.engine.vm(), |host| {
603 std::mem::take(&mut host.console_results)
604 })
605 .unwrap_or_default()
606 }
607
608 pub fn run_script(&mut self, source: &str, label: Option<&str>) {
610 let display_label = label.unwrap_or("script").to_string();
611 let span = self.begin_trace(
612 JsTraceKind::Script,
613 display_label,
614 trace::source_summary(source),
615 Some(trace::source_preview(source)),
616 );
617 let result = self.engine.eval(source);
618 let error = result.as_ref().err().map(ToString::to_string);
619 match result {
620 Ok(_) => {}
621 Err(err) => {
622 if let JSError::Thrown(value) = &err
623 && let Some(object) = value.as_object()
624 {
625 let object = object.borrow();
626 let details = object
627 .keys()
628 .into_iter()
629 .map(|key| format!("{key}={}", object.get(&key).to_console_string()))
630 .collect::<Vec<_>>()
631 .join(", ");
632 log::info!("JS error: uncaught object ({details})");
633 }
634 log::info!(
635 "JS error: {} (label {:?}, source {:?}, pc={:?}, fn={:?}, stack={:?})",
636 err,
637 label,
638 source.chars().take(60).collect::<String>(),
639 self.engine.last_error_pc(),
640 self.engine.last_error_fn(),
641 self.engine
642 .last_error_stack()
643 .iter()
644 .take(10)
645 .collect::<Vec<_>>(),
646 );
647 }
648 }
649 let stack = self.trace_error_stack(&error);
650 self.end_trace(span, error, stack);
651 self.perform_microtask_checkpoint();
652 }
653
654 pub fn try_run_script(&mut self, source: &str, label: Option<&str>) -> Result<(), String> {
659 let display_label = label.unwrap_or("script").to_string();
660 let span = self.begin_trace(
661 JsTraceKind::Script,
662 display_label,
663 trace::source_summary(source),
664 Some(trace::source_preview(source)),
665 );
666 let result = self.engine.eval(source);
667 let error = result.as_ref().err().map(ToString::to_string);
668 let stack = self.trace_error_stack(&error);
669 self.end_trace(span, error, stack);
670 self.perform_microtask_checkpoint();
671 match result {
672 Ok(_) => Ok(()),
673 Err(err) => Err(err.to_string()),
674 }
675 }
676
677 pub fn eval_value(&mut self, source: &str) -> JSValue {
679 match self.engine.eval(source) {
680 Ok(value) => value,
681 Err(err) => {
682 log::info!("JS error evaluating {source:?}: {err}");
683 JSValue::undefined()
684 }
685 }
686 }
687
688 #[doc(hidden)]
690 pub fn host_stats(&self) -> String {
691 with_host(self.engine.vm(), |host| {
692 let doc_listeners: usize = host
693 .document_event_listeners
694 .values()
695 .map(std::vec::Vec::len)
696 .sum();
697 let elem_listeners: usize = host
698 .element_event_listeners
699 .values()
700 .flat_map(|by_type| by_type.values().map(std::vec::Vec::len))
701 .sum();
702 format!(
703 "timers={} doc_listeners={} elem_listener_types={} elem_listeners={} \
704 objects={} styles={} canvas={} namespaces={} detached_nodes={} \
705 detached_docs={} fetch_signals={} xhr={} fetch_requests={} iframes={} \
706 layout_metrics={} computed_styles={} computed_decls={} custom_elements={}",
707 host.timers.len(),
708 doc_listeners,
709 host.element_event_listeners.len(),
710 elem_listeners,
711 host.objects.len(),
712 host.styles.len(),
713 host.canvas_contexts.len(),
714 host.namespaces.len(),
715 host.detached_nodes.len(),
716 host.detached_documents.len(),
717 host.fetch_signals.len(),
718 host.xhr_requests.len(),
719 host.fetch_requests.len(),
720 host.iframe_documents.len(),
721 host.layout_metrics.len() + host.layout_metrics_by_dom_id.len(),
722 host.computed_styles.len(),
723 host.computed_style_declarations.len(),
724 host.custom_elements.len(),
725 )
726 })
727 .unwrap_or_default()
728 }
729
730 pub fn set_document_url(&mut self, url: &str) {
732 let _ = with_host_mut(self.engine.vm(), |host| {
733 host.document_url = url.to_string();
734 });
735 }
736
737 pub fn set_page_origin(&mut self, origin: &str) {
740 let _ = with_host_mut(self.engine.vm(), |host| {
741 host.origin = origin.to_string();
742 });
743 }
744
745 pub fn set_page_permissions(&mut self, permissions: Permissions) {
750 let _ = with_host_mut(self.engine.vm(), |host| {
751 host.permissions = permissions;
752 });
753 }
754
755 pub fn dispatch_dom_content_loaded(&mut self) -> bool {
760 let Some((document, listeners)) = with_host_mut(self.engine.vm(), |host| {
761 if host.dom_content_loaded_fired {
762 return None;
763 }
764
765 host.dom_content_loaded_fired = true;
766 Some((
767 host.document.as_ref().cloned(),
768 host.document_event_listeners
769 .get("DOMContentLoaded")
770 .cloned()
771 .unwrap_or_default(),
772 ))
773 })
774 .flatten() else {
775 return false;
776 };
777
778 let Some(document) = document else {
779 return true;
780 };
781 let span = self.begin_trace(
782 JsTraceKind::Event,
783 "DOMContentLoaded".to_string(),
784 format!("{} listener(s)", listeners.len()),
785 None,
786 );
787 let mut first_error = None;
788 for listener in listeners {
789 let event = make_event(
790 "DOMContentLoaded",
791 Rc::clone(&document),
792 Rc::clone(&document),
793 );
794 if let Err(err) = self.engine.call(
795 listener,
796 JSValue::from_object(Rc::clone(&document)),
797 vec![JSValue::from_object(event)],
798 ) {
799 log::info!("JS error on DOMContentLoaded: {}", err);
800 first_error.get_or_insert_with(|| err.to_string());
801 }
802 }
803 let stack = self.trace_error_stack(&first_error);
804 self.end_trace(span, first_error, stack);
805 self.perform_microtask_checkpoint();
806 true
807 }
808
809 pub fn dispatch_window_load(&mut self) -> bool {
815 let state = with_host_mut(self.engine.vm(), |host| {
816 if host.window_load_fired {
817 return None;
818 }
819 host.window_load_fired = true;
820 Some((
821 host.window_inline_event_handlers
822 .get("load")
823 .cloned()
824 .unwrap_or_default(),
825 host.document_event_listeners
826 .get("load")
827 .cloned()
828 .unwrap_or_default(),
829 ))
830 });
831 let Some((body_onload, listeners)) = state.flatten() else {
832 return false;
833 };
834
835 let window_object = Rc::clone(self.engine.global_mut());
836 let span = self.begin_trace(
837 JsTraceKind::Event,
838 "load".to_string(),
839 format!("{} listener(s)", listeners.len()),
840 None,
841 );
842 let mut first_error = None;
843
844 if !body_onload.trim().is_empty()
847 && let Some(error) =
848 self.call_inline_load_handler(&window_object, &body_onload, "body onload")
849 {
850 first_error.get_or_insert(error);
851 }
852
853 let onload = window_object.borrow().get("onload");
854 if is_callable(&onload)
855 && let Some(error) = self.call_load_handler(&window_object, onload, "window.onload")
856 {
857 first_error.get_or_insert(error);
858 }
859
860 for listener in listeners {
861 if let Some(error) =
862 self.call_load_handler(&window_object, listener, "window load listener")
863 {
864 first_error.get_or_insert(error);
865 }
866 }
867
868 let stack = self.trace_error_stack(&first_error);
869 self.end_trace(span, first_error, stack);
870 self.perform_microtask_checkpoint();
871 true
872 }
873
874 fn call_inline_load_handler(
878 &mut self,
879 window: &Rc<RefCell<JSObject>>,
880 code: &str,
881 label: &str,
882 ) -> Option<String> {
883 let span = self.begin_trace(
884 JsTraceKind::Script,
885 label.to_string(),
886 "inline handler".to_string(),
887 Some(trace::source_preview(code)),
888 );
889 let event = make_event("load", Rc::clone(window), Rc::clone(window));
890 let wrapped = format!("(function(event) {{ {code} \n}})");
894 let handler = match self.engine.eval(&wrapped) {
895 Ok(handler) => handler,
896 Err(err) => {
897 let error = err.to_string();
898 log::info!("JS error in {label}: {err}");
899 let stack = self.trace_error_stack(&Some(error.clone()));
900 self.end_trace(span, Some(error.clone()), stack);
901 return Some(error);
902 }
903 };
904 let error = if is_callable(&handler) {
905 match self.engine.call(
906 handler,
907 JSValue::from_object(Rc::clone(window)),
908 vec![JSValue::from_object(event)],
909 ) {
910 Ok(_) => None,
911 Err(err) => {
912 log::info!("JS error in {label}: {err}");
913 Some(err.to_string())
914 }
915 }
916 } else {
917 None
918 };
919 let stack = self.trace_error_stack(&error);
920 self.end_trace(span, error.clone(), stack);
921 error
922 }
923
924 fn call_load_handler(
926 &mut self,
927 window: &Rc<RefCell<JSObject>>,
928 handler: JSValue,
929 label: &str,
930 ) -> Option<String> {
931 let span = self.begin_trace(
932 JsTraceKind::Event,
933 label.to_string(),
934 "load listener".to_string(),
935 None,
936 );
937 let event = make_event("load", Rc::clone(window), Rc::clone(window));
938 let error = match self.engine.call(
939 handler,
940 JSValue::from_object(Rc::clone(window)),
941 vec![JSValue::from_object(event)],
942 ) {
943 Ok(_) => None,
944 Err(err) => {
945 log::info!("JS error in {label}: {err}");
946 Some(err.to_string())
947 }
948 };
949 let stack = self.trace_error_stack(&error);
950 self.end_trace(span, error.clone(), stack);
951 error
952 }
953
954 pub fn run_due_timers(&mut self) -> bool {
959 let invocations = with_host_mut(self.engine.vm(), |host| {
960 let now = Instant::now();
961 let mut invocations = Vec::new();
962 let mut index = 0;
963 while index < host.timers.len() {
964 if host.timers[index].deadline > now {
965 index += 1;
966 continue;
967 }
968
969 let callback = host.timers[index].callback.clone();
970 let arguments = host.timers[index].arguments.clone();
971 let is_interval = host.timers[index].interval.is_some();
972 if let Some(interval) = host.timers[index].interval {
973 host.timers[index].deadline = now + interval;
974 index += 1;
975 } else {
976 host.timers.remove(index);
977 }
978 invocations.push((callback, arguments, is_interval));
979 }
980 invocations
981 })
982 .unwrap_or_default();
983
984 let ran_callback = !invocations.is_empty();
985 for (callback, arguments, is_interval) in invocations {
986 let label = if is_interval {
987 "setInterval"
988 } else {
989 "setTimeout"
990 };
991 let span = self.begin_trace(
992 JsTraceKind::Timer,
993 label.to_string(),
994 format!("{} argument(s)", arguments.len()),
995 None,
996 );
997 let error = match self.engine.call(callback, JSValue::undefined(), arguments) {
998 Ok(_) => None,
999 Err(err) => {
1000 log::info!("JS error in timer callback: {}", err);
1001 Some(err.to_string())
1002 }
1003 };
1004 let stack = self.trace_error_stack(&error);
1005 self.end_trace(span, error, stack);
1006 self.perform_microtask_checkpoint();
1007 }
1008 ran_callback
1009 }
1010
1011 pub fn needs_redraw(&self) -> bool {
1013 self.needs_redraw.get()
1014 }
1015
1016 pub fn take_needs_redraw(&self) -> bool {
1018 self.needs_redraw.replace(false)
1019 }
1020
1021 pub(crate) fn mark_needs_redraw(&self) {
1025 self.needs_redraw.set(true);
1026 }
1027
1028 pub(crate) fn take_fetch_requests(&mut self) -> Vec<JsFetchRequest> {
1030 with_host_mut(self.engine.vm(), |host| {
1031 std::mem::take(&mut host.fetch_requests)
1032 })
1033 .unwrap_or_default()
1034 }
1035
1036 pub fn take_navigation_requests(&mut self) -> Vec<String> {
1039 with_host_mut(self.engine.vm(), |host| {
1040 std::mem::take(&mut host.navigation_requests)
1041 })
1042 .unwrap_or_default()
1043 }
1044
1045 pub fn take_iframe_fetch_requests(&mut self) -> Vec<JsIframeFetchRequest> {
1048 with_host_mut(self.engine.vm(), |host| {
1049 std::mem::take(&mut host.iframe_fetch_requests)
1050 })
1051 .unwrap_or_default()
1052 }
1053
1054 pub fn resolve_iframe_fetch(&mut self, dom_id: u64, html: String, url: String) {
1057 let installed = with_host_mut(self.engine.vm(), |host| {
1058 host.pending_iframe_fetches.remove(&dom_id);
1059 host.failed_iframe_fetches.remove(&dom_id);
1060 web_apis::dom::document::install_parsed_iframe_document(host, dom_id, &html, &url)
1061 });
1062 if installed.unwrap_or(false) {
1063 self.dispatch_element_event(dom_id, "load");
1064 self.mark_needs_redraw();
1069 }
1070 }
1071
1072 pub fn reject_iframe_fetch(&mut self, dom_id: u64) {
1075 with_host_mut(self.engine.vm(), |host| {
1076 host.pending_iframe_fetches.remove(&dom_id);
1077 host.failed_iframe_fetches.insert(dom_id);
1078 });
1079 }
1080
1081 pub(crate) fn queue_markup_iframe_loads(&mut self) -> usize {
1090 with_host_mut(self.engine.vm(), |host| {
1091 let iframes = host.dom.find_all(|node| node.tag_name() == Some("iframe"));
1092 let mut queued = 0;
1093 for node in iframes {
1094 let src = {
1095 let node_ref = node.borrow();
1096 node_ref
1097 .value
1098 .get_attr("src")
1099 .map(str::trim)
1100 .unwrap_or("")
1101 .to_string()
1102 };
1103 if src.is_empty() {
1104 continue;
1105 }
1106 let Some(dom_id) = host.dom_id_for_node(&node) else {
1107 continue;
1108 };
1109 let before = host.pending_iframe_fetches.len();
1110 web_apis::dom::element::queue_iframe_fetch_if_needed(host, dom_id, &src);
1111 if host.pending_iframe_fetches.len() > before {
1112 queued += 1;
1113 }
1114 }
1115 queued
1116 })
1117 .unwrap_or(0)
1118 }
1119
1120 pub(crate) fn take_dynamic_script_requests(&mut self) -> Vec<JsDynamicScriptRequest> {
1121 with_host_mut(self.engine.vm(), |host| {
1122 std::mem::take(&mut host.dynamic_script_requests)
1123 })
1124 .unwrap_or_default()
1125 }
1126
1127 pub(crate) fn take_dynamic_style_requests(&mut self) -> Vec<JsDynamicStyleRequest> {
1128 with_host_mut(self.engine.vm(), |host| {
1129 std::mem::take(&mut host.dynamic_style_requests)
1130 })
1131 .unwrap_or_default()
1132 }
1133
1134 pub(crate) fn take_dynamic_image_requests(&mut self) -> Vec<JsDynamicImageRequest> {
1135 with_host_mut(self.engine.vm(), |host| {
1136 std::mem::take(&mut host.dynamic_image_requests)
1137 })
1138 .unwrap_or_default()
1139 }
1140
1141 pub(crate) fn dispatch_element_event(&mut self, node_id: u64, event_type: &str) {
1143 let Some((target, listeners)) = with_host(self.engine.vm(), |host| {
1144 let target = host.objects.get(&node_id).cloned()?;
1145 let listeners = host
1146 .element_event_listeners
1147 .get(&node_id)
1148 .and_then(|events| events.get(event_type))
1149 .cloned()
1150 .unwrap_or_default();
1151 Some((target, listeners))
1152 })
1153 .flatten() else {
1154 return;
1155 };
1156 let handler = target.borrow().get(&format!("on{event_type}"));
1157 let event = make_event(event_type, Rc::clone(&target), Rc::clone(&target));
1158 let span = self.begin_trace(
1159 JsTraceKind::Event,
1160 event_type.to_string(),
1161 format!("{} listener(s)", listeners.len()),
1162 None,
1163 );
1164 let mut first_error = None;
1165 if is_callable(&handler)
1166 && let Err(error) = self.engine.call(
1167 handler,
1168 JSValue::from_object(Rc::clone(&target)),
1169 vec![JSValue::from_object(Rc::clone(&event))],
1170 )
1171 {
1172 log::info!("JS error in on{event_type}: {error}");
1173 first_error.get_or_insert_with(|| error.to_string());
1174 }
1175 for listener in listeners {
1176 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
1177 break;
1178 }
1179 if let Err(error) = self.engine.call(
1180 listener.0,
1181 JSValue::from_object(Rc::clone(&target)),
1182 vec![JSValue::from_object(Rc::clone(&event))],
1183 ) {
1184 log::info!("JS error in {event_type} listener: {error}");
1185 first_error.get_or_insert_with(|| error.to_string());
1186 }
1187 }
1188 let stack = self.trace_error_stack(&first_error);
1189 self.end_trace(span, first_error, stack);
1190 }
1191
1192 pub(crate) fn resolve_fetch(&mut self, id: u64, response: JsFetchResponse) {
1194 let url = with_host_mut(self.engine.vm(), |host| host.fetch_urls.remove(&id))
1195 .flatten()
1196 .unwrap_or_default();
1197 with_host_mut(self.engine.vm(), |host| {
1198 host.fetch_signals.remove(&id);
1199 });
1200 let capability =
1201 with_host_mut(self.engine.vm(), |host| host.fetch_capabilities.remove(&id)).flatten();
1202 if let Some(capability) = capability {
1203 let span = self.begin_trace(
1204 JsTraceKind::Fetch,
1205 "fetch resolve".to_string(),
1206 format!("{url} — HTTP {} {}", response.status, response.status_text),
1207 None,
1208 );
1209 let response = make_fetch_response(response);
1210 let error = match self.engine.call(
1211 capability.resolve,
1212 JSValue::undefined(),
1213 vec![JSValue::from_object(response)],
1214 ) {
1215 Ok(_) => None,
1216 Err(err) => {
1217 log::info!("JS error while resolving fetch: {}", err);
1218 Some(err.to_string())
1219 }
1220 };
1221 let stack = self.trace_error_stack(&error);
1222 self.end_trace(span, error, stack);
1223 self.perform_microtask_checkpoint();
1224 return;
1225 }
1226 let xhr = with_host_mut(self.engine.vm(), |host| host.xhr_requests.remove(&id)).flatten();
1227 let Some(xhr) = xhr else { return };
1228 let span = self.begin_trace(
1229 JsTraceKind::Fetch,
1230 "XHR load".to_string(),
1231 format!("request {id}"),
1232 None,
1233 );
1234 resolve_xml_http_request(&mut self.engine, xhr, response);
1235 self.end_trace(span, None, Vec::new());
1236 self.perform_microtask_checkpoint();
1237 }
1238
1239 pub(crate) fn reject_fetch(&mut self, id: u64, reason: String) {
1241 let url = with_host_mut(self.engine.vm(), |host| host.fetch_urls.remove(&id))
1242 .flatten()
1243 .unwrap_or_default();
1244 with_host_mut(self.engine.vm(), |host| {
1245 host.fetch_signals.remove(&id);
1246 });
1247 let capability =
1248 with_host_mut(self.engine.vm(), |host| host.fetch_capabilities.remove(&id)).flatten();
1249 if let Some(capability) = capability {
1250 let span = self.begin_trace(
1251 JsTraceKind::Fetch,
1252 "fetch reject".to_string(),
1253 format!("{url} — {reason}"),
1254 None,
1255 );
1256 let reason_value = JSValue::from_string(reason.clone());
1257 let error =
1258 match self
1259 .engine
1260 .call(capability.reject, JSValue::undefined(), vec![reason_value])
1261 {
1262 Ok(_) => None,
1263 Err(err) => {
1264 log::info!("JS error while rejecting fetch: {}", err);
1265 Some(err.to_string())
1266 }
1267 };
1268 let stack = self.trace_error_stack(&error);
1269 self.end_trace(span, Some(error.unwrap_or(reason)), stack);
1270 self.perform_microtask_checkpoint();
1271 return;
1272 }
1273 let xhr = with_host_mut(self.engine.vm(), |host| host.xhr_requests.remove(&id)).flatten();
1274 let Some(xhr) = xhr else { return };
1275 let span = self.begin_trace(
1276 JsTraceKind::Fetch,
1277 "XHR error".to_string(),
1278 format!("request {id}"),
1279 None,
1280 );
1281 let handler = xhr.borrow().get("onerror");
1282 let error = if is_callable(&handler) {
1283 match self.engine.call(
1284 handler,
1285 JSValue::from_object(Rc::clone(&xhr)),
1286 vec![JSValue::from_string(reason)],
1287 ) {
1288 Ok(_) => None,
1289 Err(err) => Some(err.to_string()),
1290 }
1291 } else {
1292 None
1293 };
1294 self.end_trace(span, error, Vec::new());
1295 self.perform_microtask_checkpoint();
1296 }
1297
1298 pub fn snapshot(&self) -> DomSnapshot {
1303 let Some((root, dom_ids)) = with_host(self.engine.vm(), |host| {
1304 let mut reverse = HashMap::new();
1305 for (dom_id, weak) in &host.refs {
1306 if let Some(node) = weak.upgrade() {
1307 reverse.insert(Rc::as_ptr(&node) as usize, *dom_id);
1308 }
1309 }
1310 (Rc::clone(&host.dom.root), reverse)
1311 }) else {
1312 return DomSnapshot::default();
1313 };
1314 DomSnapshot::from_mirror(&root, &dom_ids)
1315 }
1316
1317 pub fn snapshot_iframe_documents(&self) -> Vec<IframeContentSnapshot> {
1320 let Some(docs) = with_host(self.engine.vm(), |host| {
1321 let mut out: Vec<(u64, Rc<DomTree>)> = Vec::new();
1322 for (dom_id, doc) in &host.iframe_documents {
1323 out.push((*dom_id, Rc::clone(&doc.borrow().tree)));
1324 }
1325 Some(out)
1326 })
1327 .flatten() else {
1328 return Vec::new();
1329 };
1330 let mut snapshots = Vec::with_capacity(docs.len());
1331 for (dom_id, tree) in docs {
1332 let (content, _refs) = DomSnapshot::from_tree(&tree.root);
1333 snapshots.push(IframeContentSnapshot {
1334 iframe_dom_id: dom_id,
1335 content,
1336 });
1337 }
1338 snapshots
1339 }
1340
1341 pub fn apply_dom(&mut self, snapshot: &DomSnapshot) {
1348 let (tree, dom_ids) = snapshot.into_tree();
1349 with_host_mut(self.engine.vm(), |host| {
1350 host.dom = Rc::new(tree);
1351 let mut refs = std::mem::take(&mut host.refs);
1352 host.dom.traverse(|node| {
1353 if let Some(&dom_id) = dom_ids.get(&(Rc::as_ptr(node) as usize)) {
1354 refs.insert(dom_id, Rc::downgrade(node));
1355 }
1356 });
1357 for (&dom_id, node) in &host.detached_nodes {
1358 refs.entry(dom_id).or_insert_with(|| Rc::downgrade(node));
1359 }
1360 host.refs = refs;
1361 if let Some(max_id) = dom_ids.values().max() {
1362 host.next_id = host.next_id.max(*max_id);
1363 }
1364 Self::register_window_event_handlers(host);
1365 });
1366 }
1367
1368 fn register_window_event_handlers(host: &mut JsHost) {
1376 for node in host.dom.find_all(|node| node.tag_name() == Some("body")) {
1377 if let Some(code) = node.borrow().value.get_attr("onload") {
1378 host.window_inline_event_handlers
1379 .insert("load".to_string(), code.to_string());
1380 }
1381 }
1382 }
1383
1384 pub fn click_dom_id(&mut self, dom_id: u64) -> bool {
1387 let Some(node) = with_host(self.engine.vm(), |host| {
1388 host.refs.get(&dom_id).and_then(|w| w.upgrade())
1389 })
1390 .flatten() else {
1391 return false;
1392 };
1393 self.click(&node)
1394 }
1395
1396 pub fn click(&mut self, node: &NodeRef<HtmlNodeType>) -> bool {
1403 let mut path = Vec::new();
1404 let mut current = Some(Rc::clone(node));
1405 while let Some(node) = current {
1406 current = node.borrow().parent();
1407 if let Some(object) = expose_node(self.engine.vm(), node).and_then(|v| v.as_object()) {
1408 path.push(object);
1409 }
1410 }
1411 let Some(target) = path.first().cloned() else {
1412 return false;
1413 };
1414
1415 let span = self.begin_trace(
1416 JsTraceKind::Event,
1417 "click".to_string(),
1418 format!("{} target(s)", path.len()),
1419 None,
1420 );
1421 let mut first_error = None;
1422 let mut ran_handler = false;
1423 for current_target in path {
1424 let Some(dom_id) = node_dom_id(&JSValue::from_object(Rc::clone(¤t_target)))
1425 else {
1426 continue;
1427 };
1428 let onclick = current_target.borrow().get("onclick");
1429 let listeners = with_host(self.engine.vm(), |host| {
1430 host.element_event_listeners
1431 .get(&dom_id)
1432 .and_then(|events| events.get("click"))
1433 .cloned()
1434 .unwrap_or_default()
1435 })
1436 .unwrap_or_default();
1437 let has_onclick = is_callable(&onclick);
1438 if !has_onclick && listeners.is_empty() {
1439 continue;
1440 }
1441
1442 ran_handler = true;
1443 let event = make_event("click", Rc::clone(&target), Rc::clone(¤t_target));
1444 if has_onclick
1445 && let Err(err) = self.engine.call(
1446 onclick,
1447 JSValue::from_object(Rc::clone(¤t_target)),
1448 vec![JSValue::from_object(Rc::clone(&event))],
1449 )
1450 {
1451 log::info!("JS error in onclick: {}", err);
1452 first_error.get_or_insert_with(|| err.to_string());
1453 }
1454 if !event_flag(&event, "__orinium_immediate_propagation_stopped") {
1455 for listener in listeners {
1456 if let Err(err) = self.engine.call(
1457 listener.0,
1458 JSValue::from_object(Rc::clone(¤t_target)),
1459 vec![JSValue::from_object(Rc::clone(&event))],
1460 ) {
1461 log::info!("JS error in click listener: {}", err);
1462 first_error.get_or_insert_with(|| err.to_string());
1463 }
1464 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
1465 break;
1466 }
1467 }
1468 }
1469 if event_flag(&event, "cancelBubble") {
1470 break;
1471 }
1472 }
1473 let stack = self.trace_error_stack(&first_error);
1474 self.end_trace(span, first_error, stack);
1475 if ran_handler {
1476 self.perform_microtask_checkpoint();
1477 }
1478 ran_handler
1479 }
1480
1481 pub fn scroll_dom_id(&mut self, dom_id: u64) -> bool {
1485 let Some(node) = with_host(self.engine.vm(), |host| {
1486 host.refs.get(&dom_id).and_then(|w| w.upgrade())
1487 })
1488 .flatten() else {
1489 return false;
1490 };
1491 self.scroll(&node)
1492 }
1493
1494 pub fn scroll(&mut self, node: &NodeRef<HtmlNodeType>) -> bool {
1500 let mut path = Vec::new();
1501 let mut current = Some(Rc::clone(node));
1502 while let Some(node) = current {
1503 current = node.borrow().parent();
1504 if let Some(object) = expose_node(self.engine.vm(), node).and_then(|v| v.as_object()) {
1505 path.push(object);
1506 }
1507 }
1508 let Some(target) = path.first().cloned() else {
1509 return false;
1510 };
1511
1512 let span = self.begin_trace(
1513 JsTraceKind::Event,
1514 "scroll".to_string(),
1515 format!("{} target(s)", path.len()),
1516 None,
1517 );
1518 let mut first_error = None;
1519 let mut ran_handler = false;
1520 for current_target in path {
1521 let Some(dom_id) = node_dom_id(&JSValue::from_object(Rc::clone(¤t_target)))
1522 else {
1523 continue;
1524 };
1525 let onscroll = current_target.borrow().get("onscroll");
1526 let listeners = with_host(self.engine.vm(), |host| {
1527 host.element_event_listeners
1528 .get(&dom_id)
1529 .and_then(|events| events.get("scroll"))
1530 .cloned()
1531 .unwrap_or_default()
1532 })
1533 .unwrap_or_default();
1534 let has_onscroll = is_callable(&onscroll);
1535 if !has_onscroll && listeners.is_empty() {
1536 continue;
1537 }
1538
1539 ran_handler = true;
1540 let event = make_event("scroll", Rc::clone(&target), Rc::clone(¤t_target));
1541 if has_onscroll
1542 && let Err(err) = self.engine.call(
1543 onscroll,
1544 JSValue::from_object(Rc::clone(¤t_target)),
1545 vec![JSValue::from_object(Rc::clone(&event))],
1546 )
1547 {
1548 log::info!("JS error in onscroll: {}", err);
1549 first_error.get_or_insert_with(|| err.to_string());
1550 }
1551 if !event_flag(&event, "__orinium_immediate_propagation_stopped") {
1552 for listener in listeners {
1553 if let Err(err) = self.engine.call(
1554 listener.0,
1555 JSValue::from_object(Rc::clone(¤t_target)),
1556 vec![JSValue::from_object(Rc::clone(&event))],
1557 ) {
1558 log::info!("JS error in scroll listener: {}", err);
1559 first_error.get_or_insert_with(|| err.to_string());
1560 }
1561 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
1562 break;
1563 }
1564 }
1565 }
1566 if event_flag(&event, "cancelBubble") {
1567 break;
1568 }
1569 }
1570 let stack = self.trace_error_stack(&first_error);
1571 self.end_trace(span, first_error, stack);
1572 if ran_handler {
1573 self.perform_microtask_checkpoint();
1574 }
1575 ran_handler
1576 }
1577 #[cfg(test)]
1580 pub(crate) fn perform_microtask_checkpoint_public(&mut self) {
1581 self.perform_microtask_checkpoint();
1582 }
1583
1584 pub fn drain_microtasks(&mut self) {
1589 self.perform_microtask_checkpoint();
1590 }
1591
1592 fn perform_microtask_checkpoint(&mut self) {
1594 let span = if self.engine.has_pending_jobs() {
1595 self.begin_trace(
1596 JsTraceKind::Microtask,
1597 "microtasks".to_string(),
1598 "checkpoint".to_string(),
1599 None,
1600 )
1601 } else {
1602 None
1603 };
1604 let mut first_error = None;
1605 const MAX_ROUNDS: usize = 50;
1606 let mut rounds = 0;
1607 loop {
1608 rounds += 1;
1609 if rounds > MAX_ROUNDS {
1610 break;
1611 }
1612 match self.engine.run_jobs() {
1613 Ok(()) => break,
1614 Err(err) => {
1615 if let JSError::Thrown(value) = &err
1616 && let Some(object) = value.as_object()
1617 {
1618 let object = object.borrow();
1619 let details = object
1620 .keys()
1621 .into_iter()
1622 .map(|key| format!("{key}={}", object.get(&key).to_console_string()))
1623 .collect::<Vec<_>>()
1624 .join(", ");
1625 log::info!("JS error in microtask: {} ({details})", err);
1626 } else {
1627 log::info!("JS error in microtask: {}", err);
1628 }
1629 first_error.get_or_insert_with(|| err.to_string());
1630 }
1631 }
1632 }
1633 let stack = self.trace_error_stack(&first_error);
1634 self.end_trace(span, first_error, stack);
1635 }
1636}
1637
1638#[cfg(test)]
1643mod tests {
1644 use super::*;
1645 use crate::engine::html::Parser as HtmlParser;
1646 use crate::test_support::assert_send;
1647 use web_apis::dom::element::{HTML_NAMESPACE, SVG_NAMESPACE, style_property_name};
1648
1649 fn runtime_from_html(html: &str) -> (JsRuntime, Rc<DomTree>) {
1650 let mut parser = HtmlParser::new(html);
1651 let dom = Rc::new(parser.parse());
1652 let runtime = JsRuntime::new(Rc::clone(&dom));
1653 (runtime, dom)
1654 }
1655
1656 #[test]
1662 fn task_result_is_send() {
1663 assert_send::<crate::engine::js::processor::JsTaskResult>();
1664 assert_send::<JsFetchRequest>();
1665 assert_send::<JsFetchResponse>();
1666 }
1667
1668 #[test]
1669 fn misc_globals_behave_like_the_platform() {
1670 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1671 runtime.run_script(
1672 r##"
1673 const results = [];
1674
1675 // Object.defineProperties
1676 const target = {a: 1};
1677 Object.defineProperties(target, {
1678 b: {value: 2, enumerable: true},
1679 c: {get: function () { return 3; }, enumerable: true},
1680 });
1681 results.push("defineProperties:" + (target.a + target.b + target.c));
1682
1683 // Reflect
1684 results.push("reflect.has:" + Reflect.has(target, "b"));
1685 results.push("reflect.get:" + Reflect.get(target, "a"));
1686 results.push("reflect.ownKeys:" + Reflect.ownKeys(target).length);
1687 results.push(
1688 "reflect.apply:" + Reflect.apply(function (x) { return x * 2; }, null, [21])
1689 );
1690
1691 // crypto.getRandomValues
1692 const bytes = new Uint8Array(8);
1693 crypto.getRandomValues(bytes);
1694 const filled = Array.prototype.every.call(bytes, function (b) { return b >= 0; });
1695 results.push("crypto:" + filled + ":" + bytes.length + ":" + bytes.byteLength);
1696 results.push("uuid:" + /^[0-9a-f-]{36}$/.test(crypto.randomUUID()));
1697
1698 // FormData
1699 const form = new FormData();
1700 form.append("q", "hello world");
1701 form.append("lang", "ja");
1702 form.append("q", "second");
1703 results.push("form.get:" + form.get("q"));
1704 results.push("form.getAll:" + form.getAll("q").join("|"));
1705 results.push("form.has:" + form.has("lang"));
1706 form.set("lang", "en");
1707 results.push("form.afterSet:" + form.get("lang"));
1708 form.delete("lang");
1709 results.push("form.afterDelete:" + form.get("lang"));
1710 results.push("form.toString:" + form.toString());
1711
1712 // DOMParser
1713 const doc = new DOMParser().parseFromString(
1714 '<html><body><p id="x">hi</p><span class="k">there</span></body></html>',
1715 "text/html"
1716 );
1717 results.push("parser.body:" + (doc.body !== null));
1718 results.push("parser.qs:" + doc.querySelector("#x").textContent);
1719 results.push("parser.tag:" + doc.getElementsByTagName("span").length);
1720
1721 document.getElementById("result").setAttribute("data-misc", results.join(";"));
1722 "##,
1723 None,
1724 );
1725
1726 let node = dom.get_element_by_id("result").unwrap();
1727 let data = node
1728 .borrow()
1729 .value
1730 .get_attr("data-misc")
1731 .unwrap_or_default()
1732 .to_string();
1733 let parts: Vec<&str> = data.split(';').collect();
1734
1735 assert_eq!(parts[0], "defineProperties:6");
1736 assert_eq!(parts[1], "reflect.has:true");
1737 assert_eq!(parts[2], "reflect.get:1");
1738 assert_eq!(parts[3], "reflect.ownKeys:3");
1739 assert_eq!(parts[4], "reflect.apply:42");
1740 assert!(parts[5].starts_with("crypto:true:8:8"), "got {}", parts[5]);
1741 assert_eq!(parts[6], "uuid:true");
1742 assert_eq!(parts[7], "form.get:hello world");
1743 assert_eq!(parts[8], "form.getAll:hello world|second");
1744 assert_eq!(parts[9], "form.has:true");
1745 assert_eq!(parts[10], "form.afterSet:en");
1746 assert_eq!(parts[11], "form.afterDelete:null");
1747 assert_eq!(parts[12], "form.toString:q=hello+world&q=second");
1748 assert_eq!(parts[13], "parser.body:true");
1749 assert_eq!(parts[14], "parser.qs:hi");
1750 assert_eq!(parts[15], "parser.tag:1");
1751 }
1752
1753 #[test]
1754 fn event_target_and_window_constructors_supported() {
1755 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1756 runtime.run_script(
1757 r##"
1758 const results = [];
1759
1760 // window.EventTarget / window.Window / window.XMLHttpRequest
1761 // must be constructors with an object `.prototype` for the
1762 // webcomponents-sd `ab()` feature detection to pass.
1763 results.push("et:" + typeof window.EventTarget);
1764 results.push("et.proto:" + (typeof EventTarget.prototype === "object"));
1765 results.push("win:" + (typeof Window === "function" || typeof Window === "object"));
1766 results.push("win.proto:" + (typeof Window.prototype === "object"));
1767 results.push("xhr:" + (typeof XMLHttpRequest));
1768 results.push("xhr.proto:" + (typeof XMLHttpRequest.prototype === "object"));
1769
1770 // Mimic wc-sd's E(): getOwnPropertyDescriptor on each prototype.
1771 function e(a, b) {
1772 b = b === undefined ? [] : b;
1773 const out = [];
1774 for (let c = 0; c < b.length; c++) {
1775 const d = b[c];
1776 out.push(!!Object.getOwnPropertyDescriptor(a, d));
1777 }
1778 return out.join(",");
1779 }
1780 const keys = ["dispatchEvent", "addEventListener", "removeEventListener"];
1781 results.push("et." + e(window.EventTarget.prototype, keys));
1782 results.push("win." + e(window.Window.prototype, keys));
1783 results.push("node." + e(Node.prototype, keys));
1784 results.push("xr." + e(XMLHttpRequest.prototype, keys));
1785
1786 // Node instances are EventTargets too.
1787 results.push("node.methods:" + typeof Node.prototype.addEventListener + ":" + typeof Node.prototype.dispatchEvent);
1788
1789 // wc-sd ab() also runs E() over Element/DocumentFragment/Document.
1790 results.push("el:" + (typeof Element.prototype));
1791 results.push("df:" + (typeof DocumentFragment.prototype));
1792 results.push("doc:" + (typeof Document.prototype));
1793
1794 // new EventTarget() instances inherit the methods from the prototype
1795 const t = new EventTarget();
1796 results.push("inst:" + typeof t.addEventListener + ":" + typeof t.dispatchEvent);
1797
1798 document.getElementById("result").setAttribute("data-eventtarget", results.join(";"));
1799 "##, None);
1800
1801 let node = dom.get_element_by_id("result").unwrap();
1802 let data = node
1803 .borrow()
1804 .value
1805 .get_attr("data-eventtarget")
1806 .unwrap_or_default()
1807 .to_string();
1808 assert_eq!(
1809 data,
1810 "et:object;et.proto:true;win:true;win.proto:true;xhr:object;xhr.proto:true;et.true,true,true;win.true,true,true;node.true,true,true;xr.true,true,true;node.methods:function:function;el:object;df:object;doc:object;inst:function:function",
1811 "got: {data}"
1812 );
1813 }
1814
1815 #[test]
1816 fn template_content_exposes_parsed_fragment() {
1817 let (mut runtime, dom) = runtime_from_html(
1818 r#"<template id="t"><div id="x">hi</div></template><div id="result"></div>"#,
1819 );
1820 runtime.run_script(
1821 r##"
1822 const results = [];
1823 results.push("tmpl:" + typeof window.HTMLTemplateElement);
1824 results.push("tmpl.proto:" + (typeof HTMLTemplateElement.prototype === "object"));
1825 const t = document.getElementById("t");
1826 const content = t.content;
1827 results.push("content:" + (content instanceof DocumentFragment));
1828 results.push("content.firstChild:" + (content.firstChild && content.firstChild.tagName === "DIV"));
1829 const x = content.querySelector("#x");
1830 results.push("x:" + (x && x.textContent === "hi"));
1831 if (x) {
1832 x.setAttribute("data-stamped", "yes");
1833 }
1834 document.getElementById("result").setAttribute("data-template", results.join(";"));
1835 "##, None);
1836 let node = dom.get_element_by_id("result").unwrap();
1837 let data = node
1838 .borrow()
1839 .value
1840 .get_attr("data-template")
1841 .unwrap_or_default()
1842 .to_string();
1843 assert_eq!(
1844 data, "tmpl:function;tmpl.proto:true;content:true;content.firstChild:true;x:true",
1845 "got: {data}"
1846 );
1847 }
1848
1849 #[test]
1850 fn template_inertness_and_lazy_content() {
1851 let (mut runtime, dom) = runtime_from_html(
1852 r#"<template id="t"><div id="inner" class="c">hi</div><span>two</span></template><div id="result"></div>"#,
1853 );
1854 runtime.run_script(
1855 r##"
1856 const results = [];
1857 const t = document.getElementById("t");
1858
1859 // Instance-of checks against the real constructors.
1860 results.push("inst:" + (t instanceof HTMLTemplateElement));
1861 results.push("elem:" + (t instanceof Element) + ":" + (t instanceof HTMLElement));
1862
1863 // `content` exposes the parsed children as a DocumentFragment and
1864 // is live: repeated accesses return the same fragment.
1865 const content = t.content;
1866 results.push("content:" + (content instanceof DocumentFragment));
1867 results.push("live:" + (t.content === content));
1868 results.push("count:" + content.childNodes.length);
1869 const inner = content.querySelector("#inner");
1870 results.push("inner:" + (inner && inner.textContent === "hi"));
1871 results.push("span:" + content.querySelectorAll("span").length);
1872
1873 // Inertness: template contents are unreachable from document-wide
1874 // queries (matching browsers) but mutations inside `content` work.
1875 results.push("qs:" + (document.querySelector("#inner") === null));
1876 results.push("id:" + (document.getElementById("inner") === null));
1877 results.push("btn:" + document.getElementsByTagName("span").length);
1878 results.push("cls:" + document.getElementsByClassName("c").length);
1879 inner.setAttribute("data-stamped", "yes");
1880 results.push("stamp:" + (content.querySelector("#inner").getAttribute("data-stamped") === "yes"));
1881
1882 // `document.createElement("template")` gets a lazily created,
1883 // usable, stable `content` fragment.
1884 const t2 = document.createElement("template");
1885 results.push("lazy:" + (t2.content instanceof DocumentFragment));
1886 t2.content.appendChild(document.createElement("b"));
1887 results.push("append:" + t2.content.childNodes.length);
1888 results.push("stable:" + (t2.content === t2.content));
1889
1890 document.getElementById("result").setAttribute("data-template", results.join(";"));
1891 "##, None);
1892 let node = dom.get_element_by_id("result").unwrap();
1893 let data = node
1894 .borrow()
1895 .value
1896 .get_attr("data-template")
1897 .unwrap_or_default()
1898 .to_string();
1899 assert_eq!(
1900 data,
1901 "inst:true;elem:true:true;content:true;live:true;count:2;inner:true;span:1;qs:true;id:true;btn:0;cls:0;stamp:true;lazy:true;append:1;stable:true",
1902 "got: {data}"
1903 );
1904 }
1905
1906 #[test]
1907 fn template_content_instantiation_via_import_node_and_clone_node() {
1908 let (mut runtime, dom) = runtime_from_html(
1909 r#"<div id="host"></div><ul id="list"></ul>\
1910 <template id="row"><li class="row"><b class="name"></b></li></template>\
1911 <template id="nested"><div><span>deep</span><template><i>inner</i></template></div></template>\
1912 <div id="result"></div>"#,
1913 );
1914 runtime.run_script(
1915 r##"
1916 const results = [];
1917 const host = document.getElementById("host");
1918
1919 // importNode(template.content, true) deep-clones into a detached
1920 // fragment without touching the original template.
1921 const t = document.getElementById("row");
1922 const frag = document.importNode(t.content, true);
1923 results.push("frag:" + (frag instanceof DocumentFragment) + ":" + frag.childNodes.length);
1924 results.push("orig-intact:" + (t.content.querySelectorAll("li").length === 1));
1925
1926 // Appending the fragment instantiates the cloned rows into the
1927 // document, where document-wide queries can now reach them.
1928 host.appendChild(frag);
1929 results.push("appended:" + host.querySelectorAll("li.row").length);
1930 results.push("doc-visible:" + document.querySelectorAll("li.row").length);
1931 results.push("deep:" + (host.querySelector("li.row b.name") !== null));
1932
1933 // Instantiated copies are independent of the template: mutations
1934 // on the clone never leak back into `template.content`.
1935 host.querySelector("b.name").textContent = "cloned!";
1936 results.push("independent:" + (t.content.querySelector("b.name").textContent === ""));
1937
1938 // cloneNode(true) on `content` works as an alternative.
1939 const frag2 = t.content.cloneNode(true);
1940 results.push("clone-frag:" + (frag2 instanceof DocumentFragment) + ":" + frag2.querySelectorAll("li").length);
1941
1942 // Repeated instantiation yields fresh copies each time.
1943 const frag3 = document.importNode(t.content, true);
1944 document.getElementById("list").appendChild(frag3);
1945 results.push("repeat:" + document.querySelectorAll("li.row").length);
1946
1947 // Nested templates: importing the outer content keeps the inner
1948 // `<template>` inert — its content stays inside its own fragment.
1949 const nested = document.getElementById("nested");
1950 const nfrag = document.importNode(nested.content, true);
1951 host.appendChild(nfrag);
1952 results.push("nested-span:" + (host.querySelector("span") !== null));
1953 const innerTemplate = host.querySelector("template");
1954 results.push("nested-tmpl:" + (innerTemplate !== null) + ":" + (innerTemplate && innerTemplate.content.querySelector("i") !== null));
1955
1956 // Shallow import clones the fragment but none of its children.
1957 const shallow = document.importNode(t.content, false);
1958 results.push("shallow:" + (shallow instanceof DocumentFragment) + ":" + shallow.childNodes.length);
1959
1960 // `importNode(node)` defaults deep to true (per spec).
1961 const def = document.importNode(t.content);
1962 results.push("default-deep:" + def.querySelectorAll("li").length);
1963
1964 document.getElementById("result").setAttribute("data-r", results.join(";"));
1965 "##, None);
1966 let node = dom.get_element_by_id("result").unwrap();
1967 let data = node
1968 .borrow()
1969 .value
1970 .get_attr("data-r")
1971 .unwrap_or_default()
1972 .to_string();
1973 assert_eq!(
1974 data,
1975 "frag:true:1;orig-intact:true;appended:1;doc-visible:1;deep:true;independent:true;clone-frag:true:1;repeat:2;nested-span:true;nested-tmpl:true:true;shallow:true:0;default-deep:1",
1976 "got: {data}"
1977 );
1978 }
1979
1980 #[test]
1981 fn implementation_create_document_is_independent() {
1982 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1983 runtime.run_script(
1984 r##"
1985 const results = [];
1986
1987 // A created document is a distinct object with its own tree.
1988 const d = document.implementation.createDocument(null, null, null);
1989 results.push("distinct:" + (d !== document));
1990 results.push("nodeType:" + d.nodeType);
1991 results.push("children:" + d.childNodes.length);
1992
1993 // Appending to the document makes the element its documentElement.
1994 const root = d.createElement("root");
1995 d.appendChild(root);
1996 results.push("documentElement:" + d.documentElement.tagName);
1997 results.push("sameNode:" + (d.documentElement === root));
1998
1999 // ownerDocument is correct for a node that is in no tree yet, and
2000 // for one that has been inserted.
2001 const e1 = d.createElement("test");
2002 results.push("detachedOwner:" + (e1.ownerDocument === d));
2003 d.documentElement.appendChild(e1);
2004 results.push("insertedOwner:" + (e1.parentNode.ownerDocument === d));
2005 results.push("ownerType:" + e1.parentNode.ownerDocument.nodeType);
2006
2007 // The top-level document is untouched by any of the above.
2008 results.push("mainDocElement:" + document.documentElement.tagName);
2009 results.push("mainUntouched:" + (document.getElementById("result") !== null));
2010
2011 // createDocument with a qualified name builds an XHTML-ish document.
2012 const x = document.implementation.createDocument(
2013 "http://www.w3.org/1999/xhtml", "html", null);
2014 results.push("xhtmlRoot:" + x.documentElement.tagName);
2015 results.push("xhtmlTitle:" + JSON.stringify(x.title));
2016 x.documentElement.appendChild(
2017 x.createElementNS("http://www.w3.org/1999/xhtml", "head"));
2018 x.documentElement.appendChild(
2019 x.createElementNS("http://www.w3.org/1999/xhtml", "body"));
2020 const title = x.createElementNS("http://www.w3.org/1999/xhtml", "title");
2021 x.documentElement.firstChild.appendChild(title);
2022 results.push("xhtmlBody:" + x.body.tagName);
2023 results.push("xhtmlForms:" + x.forms.length);
2024 title.textContent = "Sparrow";
2025 results.push("xhtmlTitleSet:" + x.title);
2026
2027 document.getElementById("result").setAttribute("data-r", results.join(";"));
2028 "##,
2029 None,
2030 );
2031 let node = dom.get_element_by_id("result").unwrap();
2032 let data = node
2033 .borrow()
2034 .value
2035 .get_attr("data-r")
2036 .unwrap_or_default()
2037 .to_string();
2038 assert_eq!(
2039 data,
2040 "distinct:true;nodeType:9;children:0;documentElement:ROOT;sameNode:true;\
2041 detachedOwner:true;insertedOwner:true;ownerType:9;mainDocElement:HTML;\
2042 mainUntouched:true;xhtmlRoot:HTML;xhtmlTitle:\"\";xhtmlBody:BODY;xhtmlForms:0;\
2043 xhtmlTitleSet:Sparrow",
2044 "got: {data}"
2045 );
2046 }
2047
2048 #[test]
2049 fn node_interface_and_html_document_helpers() {
2050 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2051 runtime.run_script(
2052 r##"
2053 const results = [];
2054
2055 // Node is now a constructor with a prototype
2056 results.push("node.typeof:" + typeof Node);
2057 results.push("node.prototype:" + (typeof Node.prototype === "object"));
2058
2059 // wc-sd gate: firstChild must be a configurable accessor with a getter
2060 const d = Object.getOwnPropertyDescriptor(Node.prototype, "firstChild");
2061 results.push("fc.desc:" + (!!d && d.configurable === true && typeof d.get === "function"));
2062 results.push("fc.enum:" + d.enumerable + ":" + d.writable);
2063
2064 // getRootNode gate for ShadyDOM detection
2065 results.push("rootNode:" + typeof Node.prototype.getRootNode);
2066 results.push("rootIsDoc:" + (document.documentElement.getRootNode() === document));
2067
2068 // textNode rides Node.prototype accessors? (engine: instance props win)
2069
2070 // document.implementation.createHTMLDocument
2071 const doc = document.implementation.createHTMLDocument("inert");
2072 results.push("htmlDoc.doctype:" + (doc !== null));
2073 results.push("htmlDoc.ns:" + doc.namespaceURI);
2074 results.push("htmlDoc.html:" + doc.documentElement.tagName);
2075 results.push("htmlDoc.body:" + doc.body.tagName);
2076 results.push("htmlDoc.title:" + doc.title);
2077 const p = doc.createElement("p");
2078 p.innerHTML = "<b>hi</b>";
2079 results.push("htmlDoc.el:" + p.tagName + ":" + p.firstChild.tagName);
2080
2081 // createEvent('CustomEvent') + initCustomEvent
2082 const ev = document.createEvent("CustomEvent");
2083 results.push("ce.has:" + typeof ev.initCustomEvent);
2084 ev.initCustomEvent("yt:run", true, false, {x: 1});
2085 results.push("ce.prop:" + ev.type + ":" + ev.bubbles + ":" + ev.cancelable + ":" + ev.detail.x);
2086
2087 document.getElementById("result").setAttribute("data-node", results.join(";"));
2088 "##, None);
2089
2090 let node = dom.get_element_by_id("result").unwrap();
2091 let data = node
2092 .borrow()
2093 .value
2094 .get_attr("data-node")
2095 .unwrap_or_default()
2096 .to_string();
2097 let parts: Vec<&str> = data.split(';').collect();
2098
2099 assert_eq!(parts[0], "node.typeof:object");
2100 assert_eq!(parts[1], "node.prototype:true");
2101 assert_eq!(parts[2], "fc.desc:true");
2102 assert_eq!(parts[3], "fc.enum:false:undefined");
2103 assert_eq!(parts[4], "rootNode:function");
2104 assert_eq!(parts[5], "rootIsDoc:true");
2105 assert_eq!(parts[6], "htmlDoc.doctype:true");
2106 assert_eq!(parts[7], "htmlDoc.ns:http://www.w3.org/1999/xhtml");
2107 assert_eq!(parts[8], "htmlDoc.html:HTML");
2108 assert_eq!(parts[9], "htmlDoc.body:BODY");
2109 assert_eq!(parts[10], "htmlDoc.title:inert");
2110 assert_eq!(parts[11], "htmlDoc.el:P:B");
2111 assert_eq!(parts[12], "ce.has:function");
2112 assert_eq!(parts[13], "ce.prop:yt:run:true:false:1");
2113 }
2114
2115 #[test]
2116 fn set_text_content_mutates_dom_and_marks_dirty() {
2117 let (mut runtime, dom) = runtime_from_html(r#"<div id="hello">before</div>"#);
2118 runtime.run_script(
2119 r#"const el = document.getElementById("hello"); el.textContent = "hello from js";"#,
2120 None,
2121 );
2122 assert!(runtime.needs_redraw());
2123 assert!(runtime.take_needs_redraw());
2124
2125 let node = dom.get_element_by_id("hello").unwrap();
2126 assert_eq!(DomTree::inner_text(&node), "hello from js");
2127 }
2128
2129 #[test]
2130 fn viewport_dimensions_follow_browser_resizes() {
2131 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2132 runtime.set_viewport(1280.0, 720.0);
2133 runtime.run_script(
2134 r#"document.getElementById("result").setAttribute("data-size", innerWidth + ":" + innerHeight);"#, None);
2135 assert_eq!(
2136 dom.get_element_by_id("result")
2137 .unwrap()
2138 .borrow()
2139 .value
2140 .get_attr("data-size"),
2141 Some("1280:720")
2142 );
2143 runtime.run_script(
2144 r#"document.getElementById("result").setAttribute("data-root", document.body.clientWidth + ":" + document.body.clientHeight + ":" + outerWidth + ":" + outerHeight);"#, None);
2145 assert_eq!(
2146 dom.get_element_by_id("result")
2147 .unwrap()
2148 .borrow()
2149 .value
2150 .get_attr("data-root"),
2151 Some("1280:720:1280:720")
2152 );
2153 }
2154
2155 #[test]
2156 fn set_attribute_mutates_dom() {
2157 let (mut runtime, dom) = runtime_from_html(r#"<div id="hello"></div>"#);
2158 runtime.run_script(
2159 r#"const el = document.getElementById("hello"); el.setAttribute("data-run", "1");"#,
2160 None,
2161 );
2162
2163 let node = dom.get_element_by_id("hello").unwrap();
2164 assert_eq!(node.borrow().value.get_attr("data-run"), Some("1"));
2165 }
2166
2167 #[test]
2168 fn match_media_evaluates_width_and_type_queries_against_viewport() {
2169 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2170 runtime.set_viewport(1000.0, 600.0);
2171 runtime.run_script(
2172 r#"
2173 const results = [];
2174 results.push(matchMedia("(max-width: 1000px)").matches);
2175 results.push(matchMedia("(max-width: 999px)").matches);
2176 results.push(matchMedia("(min-width: 1000px)").matches);
2177 results.push(matchMedia("screen").matches);
2178 results.push(matchMedia("print").matches);
2179 results.push(matchMedia("all").matches);
2180 results.push(matchMedia("").matches);
2181 results.push(matchMedia("screen and (min-width: 500px) and (max-width: 1200px)").matches);
2182 results.push(matchMedia("only screen and (max-width: 600px)").matches);
2183 document.getElementById("result").setAttribute("data-media", results.join(","));
2184 "#, None);
2185 let data = dom
2186 .get_element_by_id("result")
2187 .unwrap()
2188 .borrow()
2189 .value
2190 .get_attr("data-media")
2191 .unwrap_or_default()
2192 .to_string();
2193 assert_eq!(
2194 data, "true,false,true,true,false,true,false,true,false",
2195 "got: {data}"
2196 );
2197 }
2198
2199 #[test]
2200 fn computed_style_reflects_committed_layout_values() {
2201 let (mut runtime, dom) =
2202 runtime_from_html(r#"<div id="xm" style="color: red"></div><div id="result"></div>"#);
2203 runtime.run_script(
2204 r#"document.getElementById("result").setAttribute("data-id", String(document.getElementById("xm").__orinium_dom_id));"#, None);
2205 let dom_id: u64 = dom
2206 .get_element_by_id("result")
2207 .unwrap()
2208 .borrow()
2209 .value
2210 .get_attr("data-id")
2211 .unwrap_or_default()
2212 .parse()
2213 .expect("JS-facing dom id");
2214 runtime.set_computed_styles_by_dom_id(std::collections::HashMap::from([(
2215 dom_id,
2216 vec![
2217 ("display".to_string(), "flex".to_string()),
2218 ("opacity".to_string(), "0.5".to_string()),
2219 ("background-color".to_string(), "#3366ff".to_string()),
2220 ("width".to_string(), "240px".to_string()),
2221 ],
2222 )]));
2223 runtime.run_script(
2224 r#"
2225 const xm = document.getElementById("xm");
2226 const cs = getComputedStyle(xm);
2227 const results = [];
2228 results.push(cs.opacity);
2229 results.push(cs.getPropertyValue("display"));
2230 results.push(cs.width);
2231 results.push(cs.backgroundColor);
2232 results.push(cs.getPropertyValue("color"));
2233 results.push(String(cs));
2234 results.push(cs.cssText);
2235 results.push(String(xm.style));
2236 results.push(String(cs) === cs.cssText);
2237 document.getElementById("result").setAttribute("data-computed", results.join("|"));
2238 "#,
2239 None,
2240 );
2241 let data = dom
2242 .get_element_by_id("result")
2243 .unwrap()
2244 .borrow()
2245 .value
2246 .get_attr("data-computed")
2247 .unwrap_or_default()
2248 .to_string();
2249 assert_eq!(
2250 data,
2251 "0.5|flex|240px|#3366ff|red|display: flex; opacity: 0.5; background-color: #3366ff; width: 240px;|display: flex; opacity: 0.5; background-color: #3366ff; width: 240px;|color: red|true",
2252 "got: {data}"
2253 );
2254 }
2255
2256 #[test]
2257 fn computed_style_writes_are_ignored_and_resolve_back_to_inline_when_unlaid_out() {
2258 let (mut runtime, dom) = runtime_from_html(
2259 r#"<div id="xm" style="margin-left: 3px"></div><div id="result"></div>"#,
2260 );
2261 runtime.run_script(
2262 r#"
2263 const cs = getComputedStyle(document.getElementById("xm"));
2264 cs.opacity = "0.99";
2265 cs.cssText = "color: blue";
2266 cs.setProperty("color", "green");
2267 cs.removeProperty("color");
2268 document.getElementById("result").setAttribute(
2269 "data-inline",
2270 cs.opacity + ":" + cs.getPropertyValue("margin-left") + ":" + cs.getPropertyValue("color")
2271 );
2272 "#, None);
2273 let data = dom
2274 .get_element_by_id("result")
2275 .unwrap()
2276 .borrow()
2277 .value
2278 .get_attr("data-inline")
2279 .unwrap_or_default()
2280 .to_string();
2281 assert_eq!(data, ":3px:", "got: {data}");
2284 }
2285
2286 #[test]
2287 fn browser_environment_exposes_react_bootstrap_apis() {
2288 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2289 runtime.set_document_url("https://scratch.mit.edu/projects/editor/?tutorial=1#stage");
2290 runtime.run_script(
2291 r#"
2292 localStorage.setItem("answer", 42);
2293 sessionStorage.setItem("temporary", "yes");
2294 const query = matchMedia("(prefers-color-scheme: dark)");
2295 query.addEventListener("change", function () {});
2296 const event = new CustomEvent("ready", {detail: "loaded", cancelable: true});
2297 event.preventDefault();
2298 const frame = requestAnimationFrame(function () {});
2299 cancelAnimationFrame(frame);
2300 document.getElementById("result").setAttribute(
2301 "data-environment",
2302 navigator.language + ":" + localStorage.getItem("answer") + ":" +
2303 localStorage.length + ":" + query.matches + ":" + (frame > 0) + ":" +
2304 location.pathname + ":" + event.detail + ":" + event.defaultPrevented + ":" +
2305 (typeof Intl === "object") + ":" + ("Locale" in Intl) + ":" +
2306 Intl.getCanonicalLocales(["EN-us", "ja"])[0] + ":" +
2307 new Intl.Locale("und-x-private").toString()
2308 );
2309 "#,
2310 None,
2311 );
2312
2313 let node = dom.get_element_by_id("result").unwrap();
2314 assert_eq!(
2315 node.borrow().value.get_attr("data-environment"),
2316 Some(
2317 "en-US:42:1:false:true:/projects/editor/:loaded:true:true:true:en-US:und-x-private"
2318 )
2319 );
2320 }
2321
2322 #[test]
2323 fn browser_origin_exposed_consistently_across_window_location_and_document() {
2324 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2325 runtime.set_page_origin("https://example.test");
2326 runtime.run_script(
2327 r#"document.getElementById("result").setAttribute(
2328 "data-origins",
2329 window.origin + ":" + location.origin + ":" + document.origin
2330 );"#,
2331 None,
2332 );
2333
2334 let node = dom.get_element_by_id("result").unwrap();
2335 assert_eq!(
2336 node.borrow().value.get_attr("data-origins"),
2337 Some("https://example.test:https://example.test:https://example.test")
2338 );
2339 }
2340
2341 #[test]
2342 fn opaque_page_reports_null_origin_everywhere() {
2343 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2344 runtime.set_page_origin("null");
2345 runtime.run_script(
2346 r#"document.getElementById("result").setAttribute(
2347 "data-origins",
2348 window.origin + ":" + location.origin + ":" + document.origin
2349 );"#,
2350 None,
2351 );
2352
2353 let node = dom.get_element_by_id("result").unwrap();
2354 assert_eq!(
2355 node.borrow().value.get_attr("data-origins"),
2356 Some("null:null:null")
2357 );
2358 }
2359
2360 #[test]
2361 fn browser_language_preferences_follow_the_host() {
2362 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2363 runtime.set_language("ja-JP");
2364 runtime.run_script(
2365 r#"
2366 document.getElementById("result").setAttribute(
2367 "data-languages",
2368 navigator.language + ":" + navigator.languages[0] + ":" +
2369 navigator.languages[1] + ":" + navigator.languages[2]
2370 );
2371 "#,
2372 None,
2373 );
2374
2375 assert_eq!(
2376 dom.get_element_by_id("result")
2377 .unwrap()
2378 .borrow()
2379 .value
2380 .get_attr("data-languages"),
2381 Some("ja-JP:ja-JP:ja:en-US")
2382 );
2383 }
2384
2385 #[test]
2386 fn document_cookie_is_a_string_and_supports_assignment_and_expiry() {
2387 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2388 runtime.run_script(
2389 r#"
2390 const result = document.getElementById("result");
2391 result.setAttribute("data-empty-cookie", typeof document.cookie + ":" + document.cookie);
2392 document.cookie = "scratchlanguage=ja; Path=/";
2393 result.setAttribute("data-cookie", document.cookie);
2394 document.cookie = "scratchlanguage=; Max-Age=0; Path=/";
2395 result.setAttribute("data-expired-cookie", document.cookie);
2396 "#, None);
2397
2398 let result = dom.get_element_by_id("result").unwrap();
2399 let result = result.borrow();
2400 assert_eq!(result.value.get_attr("data-empty-cookie"), Some("string:"));
2401 assert_eq!(
2402 result.value.get_attr("data-cookie"),
2403 Some("scratchlanguage=ja")
2404 );
2405 assert_eq!(result.value.get_attr("data-expired-cookie"), Some(""));
2406 }
2407
2408 #[test]
2409 fn url_apis_resolve_assets_and_manage_query_parameters() {
2410 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2411 runtime.run_script(
2412 r#"
2413 const asset = new URL("../assets/stage.svg?locale=ja#costume", "https://scratch.mit.edu/projects/editor/");
2414 const params = new URLSearchParams("project=123&mode=editor");
2415 params.set("mode", "fullscreen");
2416 params.append("cloud", "on");
2417 params.delete("project");
2418 document.getElementById("result").setAttribute(
2419 "data-url",
2420 asset.origin + ":" + asset.pathname + ":" + asset.searchParams.get("locale") +
2421 ":" + params.has("cloud") + ":" + params.toString()
2422 );
2423 "#, None);
2424
2425 let result = dom.get_element_by_id("result").unwrap();
2426 assert_eq!(
2427 result.borrow().value.get_attr("data-url"),
2428 Some(
2429 "https://scratch.mit.edu:/projects/assets/stage.svg:ja:true:mode=fullscreen&cloud=on"
2430 )
2431 );
2432 }
2433
2434 #[test]
2435 fn encoding_apis_round_trip_utf8_and_base64() {
2436 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2437 runtime.run_script(
2438 r#"
2439 const encoder = new TextEncoder();
2440 const decoder = new TextDecoder("utf-8");
2441 const bytes = encoder.encode("Scratch 日本");
2442 document.getElementById("result").setAttribute(
2443 "data-encoding",
2444 decoder.decode(bytes) + ":" + bytes.length + ":" + atob(btoa("Scratch")) +
2445 ":" + decodeURIComponent(encodeURIComponent("日本 語"))
2446 );
2447 "#,
2448 None,
2449 );
2450
2451 let result = dom.get_element_by_id("result").unwrap();
2452 assert_eq!(
2453 result.borrow().value.get_attr("data-encoding"),
2454 Some("Scratch 日本:14:Scratch:日本 語")
2455 );
2456 }
2457
2458 #[test]
2459 fn layout_measurement_and_resize_observer_report_element_size() {
2460 let (mut runtime, dom) = runtime_from_html(
2461 r#"<canvas id="stage" width="480" height="360"></canvas><div id="result"></div>"#,
2462 );
2463 runtime.run_script(
2464 r#"
2465 const stage = document.getElementById("stage");
2466 const rect = stage.getBoundingClientRect();
2467 const observer = new ResizeObserver(function (entries) {
2468 const observed = entries[0].contentRect;
2469 document.getElementById("result").setAttribute(
2470 "data-resize",
2471 observed.width + ":" + observed.height
2472 );
2473 });
2474 observer.observe(stage);
2475 document.getElementById("result").setAttribute(
2476 "data-measure",
2477 rect.width + ":" + rect.height + ":" + stage.clientWidth + ":" + stage.offsetHeight
2478 );
2479 "#,
2480 None,
2481 );
2482
2483 let result = dom.get_element_by_id("result").unwrap();
2484 let result = result.borrow();
2485 assert_eq!(
2486 result.value.get_attr("data-measure"),
2487 Some("480:360:480:360")
2488 );
2489 assert_eq!(result.value.get_attr("data-resize"), Some("480:360"));
2490 }
2491
2492 #[test]
2493 fn layout_offsets_have_a_numeric_fallback() {
2494 let (mut runtime, dom) = runtime_from_html(
2495 r#"<div id="list" class="carousel slick-list"></div><div id="plain"></div><div id="result"></div>"#,
2496 );
2497 runtime.run_script(
2498 r#"
2499 document.getElementById("result").setAttribute(
2500 "data-widths",
2501 document.getElementById("list").offsetWidth + ":" +
2502 document.getElementById("plain").offsetWidth + ":" +
2503 document.getElementById("list").offsetLeft + ":" +
2504 document.getElementById("list").offsetTop
2505 );
2506 "#,
2507 None,
2508 );
2509
2510 assert_eq!(
2511 dom.get_element_by_id("result")
2512 .unwrap()
2513 .borrow()
2514 .value
2515 .get_attr("data-widths"),
2516 Some("800:0:0:0")
2517 );
2518 }
2519
2520 #[test]
2521 fn dom_measurements_prefer_committed_layout_geometry() {
2522 let (mut runtime, dom) = runtime_from_html(
2523 r#"<div id="target" style="width: 1px; height: 2px"></div><div id="result"></div>"#,
2524 );
2525 let target = dom.get_element_by_id("target").unwrap();
2526 runtime.set_layout_metrics(HashMap::from([(
2527 Rc::as_ptr(&target) as usize,
2528 JsLayoutMetrics {
2529 offset_left: 12.0,
2530 offset_top: 34.0,
2531 offset_width: 222.0,
2532 offset_height: 111.0,
2533 client_width: 218.0,
2534 client_height: 107.0,
2535 rect_left: 42.5,
2536 rect_top: 64.25,
2537 rect_width: 222.0,
2538 rect_height: 111.0,
2539 },
2540 )]));
2541 runtime.run_script(
2542 r#"
2543 const target = document.getElementById("target");
2544 const rect = target.getBoundingClientRect();
2545 document.getElementById("result").setAttribute(
2546 "data-layout",
2547 target.offsetLeft + ":" + target.offsetTop + ":" +
2548 target.offsetWidth + ":" + target.offsetHeight + ":" +
2549 target.clientWidth + ":" + target.clientHeight + ":" +
2550 rect.left + ":" + rect.top + ":" + rect.right + ":" + rect.bottom
2551 );
2552 "#,
2553 None,
2554 );
2555
2556 assert_eq!(
2557 dom.get_element_by_id("result")
2558 .unwrap()
2559 .borrow()
2560 .value
2561 .get_attr("data-layout"),
2562 Some("12:34:222:111:218:107:42.5:64.25:264.5:175.25")
2563 );
2564 }
2565
2566 #[test]
2567 fn inserting_script_element_queues_dynamic_resource_load() {
2568 let (mut runtime, _dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
2569 runtime.run_script(
2570 r#"
2571 const script = document.createElement("script");
2572 script.src = "/static/chunks/editor.js";
2573 script.async = true;
2574 document.head.appendChild(script);
2575 "#,
2576 None,
2577 );
2578
2579 let requests = runtime.take_dynamic_script_requests();
2580 assert_eq!(requests.len(), 1);
2581 assert!(requests[0].node_id > 0);
2582 match &requests[0].source {
2583 JsDynamicScriptSource::External(source) => {
2584 assert_eq!(source, "/static/chunks/editor.js")
2585 }
2586 JsDynamicScriptSource::Inline(_) => panic!("expected an external script request"),
2587 }
2588 }
2589
2590 #[test]
2591 fn inserting_stylesheet_link_queues_dynamic_resource_load() {
2592 let (mut runtime, _dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
2593 runtime.run_script(
2594 r#"
2595 const link = document.createElement("link");
2596 link.rel = "stylesheet";
2597 link.href = "/static/css/editor.css";
2598 document.head.appendChild(link);
2599 "#,
2600 None,
2601 );
2602
2603 let requests = runtime.take_dynamic_style_requests();
2604 assert_eq!(requests.len(), 1);
2605 assert!(requests[0].node_id > 0);
2606 assert_eq!(requests[0].url, "/static/css/editor.css");
2607 }
2608
2609 #[test]
2610 fn inserting_image_queues_dynamic_resource_load_once() {
2611 let (mut runtime, _dom) = runtime_from_html(r#"<html><body></body></html>"#);
2612 runtime.run_script(
2613 r#"
2614 const image = document.createElement("img");
2615 image.src = "/images/scratch-logo.svg";
2616 document.body.appendChild(image);
2617 "#,
2618 None,
2619 );
2620
2621 let requests = runtime.take_dynamic_image_requests();
2622 assert_eq!(requests.len(), 1);
2623 assert_eq!(requests[0].source, "/images/scratch-logo.svg");
2624 }
2625
2626 #[test]
2627 fn canvas_2d_context_records_visible_rectangle_commands() {
2628 let (mut runtime, dom) = runtime_from_html(r#"<canvas id="stage"></canvas>"#);
2629 runtime.run_script(
2630 r##"
2631 const canvas = document.getElementById("stage");
2632 canvas.width = 480;
2633 canvas.height = 360;
2634 const context = canvas.getContext("2d");
2635 context.fillStyle = "#ff8800";
2636 context.fillRect(10, 20, 30, 40);
2637 context.strokeStyle = "blue";
2638 context.strokeRect(0, 0, 480, 360);
2639 canvas.setAttribute("data-metrics", context.measureText("Scratch").width);
2640 "##,
2641 None,
2642 );
2643
2644 let canvas = dom.get_element_by_id("stage").unwrap();
2645 let canvas = canvas.borrow();
2646 assert_eq!(canvas.value.get_attr("width"), Some("480"));
2647 assert_eq!(canvas.value.get_attr("height"), Some("360"));
2648 assert_eq!(canvas.value.get_attr("data-metrics"), Some("42"));
2649 assert_eq!(
2650 canvas.value.get_attr("data-orinium-canvas-commands"),
2651 Some("fillRect|#ff8800|10|20|30|40\nstrokeRect|blue|0|0|480|360")
2652 );
2653 }
2654
2655 #[test]
2656 fn canvas_exposes_webgl_capability_surface() {
2657 let (mut runtime, dom) =
2658 runtime_from_html(r#"<canvas id="stage" width="480" height="360"></canvas>"#);
2659 runtime.run_script(
2660 r#"
2661 const canvas = document.getElementById("stage");
2662 const gl = canvas.getContext("webgl");
2663 const shader = gl.createShader(gl.VERTEX_SHADER);
2664 gl.shaderSource(shader, "void main() {}");
2665 gl.compileShader(shader);
2666 const program = gl.createProgram();
2667 gl.attachShader(program, shader);
2668 gl.linkProgram(program);
2669 canvas.setAttribute(
2670 "data-webgl",
2671 gl.getShaderParameter(shader, gl.COMPILE_STATUS) + ":" +
2672 gl.getProgramParameter(program, gl.LINK_STATUS) + ":" +
2673 gl.getParameter(gl.MAX_TEXTURE_SIZE) + ":" + gl.drawingBufferWidth
2674 );
2675 "#,
2676 None,
2677 );
2678
2679 let canvas = dom.get_element_by_id("stage").unwrap();
2680 assert_eq!(
2681 canvas.borrow().value.get_attr("data-webgl"),
2682 Some("true:true:4096:480")
2683 );
2684 }
2685
2686 #[test]
2687 fn mutation_observer_can_register_for_dom_changes() {
2688 let (mut runtime, dom) =
2689 runtime_from_html(r#"<html><body><div id="target"></div></body></html>"#);
2690 runtime.run_script(
2691 r#"
2692 const observer = new MutationObserver(function () {
2693 document.getElementById("target").setAttribute("data-observed", "yes");
2694 });
2695 observer.observe(document.documentElement, { childList: true, subtree: true });
2696 const records = observer.takeRecords();
2697 observer.disconnect();
2698 document.getElementById("target").setAttribute("data-records", records.length);
2699 "#,
2700 None,
2701 );
2702
2703 let node = dom.get_element_by_id("target").unwrap();
2704 assert_eq!(node.borrow().value.get_attr("data-records"), Some("0"));
2705 assert_eq!(node.borrow().value.get_attr("data-observed"), Some("yes"));
2706 }
2707
2708 #[test]
2709 fn element_id_is_a_live_reflected_property() {
2710 let (mut runtime, dom) = runtime_from_html(r#"<main id="root"></main>"#);
2711 runtime.run_script(
2712 r#"
2713 const child = document.createElement("button");
2714 child.id = "first";
2715 document.getElementById("root").appendChild(child);
2716 child.setAttribute("id", "second");
2717 child.setAttribute("data-current-id", child.id);
2718 "#,
2719 None,
2720 );
2721
2722 let child = dom.get_element_by_id("second").unwrap();
2723 assert_eq!(
2724 child.borrow().value.get_attr("data-current-id"),
2725 Some("second")
2726 );
2727 assert!(runtime.needs_redraw());
2728 }
2729
2730 #[test]
2731 fn form_properties_reflect_to_dom_attributes() {
2732 let (mut runtime, dom) = runtime_from_html(
2733 r#"<input id="field"><option id="option"></option><select id="select"></select>"#,
2734 );
2735 runtime.run_script(
2736 r#"
2737 const field = document.getElementById("field");
2738 field.value = "hello";
2739 field.checked = true;
2740 field.disabled = true;
2741 field.checked = false;
2742 const option = document.getElementById("option");
2743 option.selected = true;
2744 const select = document.getElementById("select");
2745 select.multiple = true;
2746 "#,
2747 None,
2748 );
2749
2750 let field = dom.get_element_by_id("field").unwrap();
2751 let field = field.borrow();
2752 assert_eq!(field.value.get_attr("value"), Some("hello"));
2753 assert_eq!(field.value.get_attr("checked"), None);
2754 assert_eq!(field.value.get_attr("disabled"), Some(""));
2755 drop(field);
2756 let option = dom.get_element_by_id("option").unwrap();
2757 assert_eq!(option.borrow().value.get_attr("selected"), Some(""));
2758 let select = dom.get_element_by_id("select").unwrap();
2759 assert_eq!(select.borrow().value.get_attr("multiple"), Some(""));
2760 assert!(runtime.needs_redraw());
2761 }
2762
2763 #[test]
2764 fn form_properties_are_accessors_on_the_element_prototype() {
2765 let (mut runtime, dom) = runtime_from_html(r#"<input id="field">"#);
2766 runtime.run_script(
2767 r#"
2768 const field = document.getElementById("field");
2769 const prototype = field.constructor.prototype;
2770 const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
2771 field.setAttribute("data-prototype", Object.getPrototypeOf(field) === prototype);
2772 field.setAttribute("data-interface", field.constructor === HTMLElement && field instanceof Element);
2773 field.setAttribute("data-own-value", field.hasOwnProperty("value"));
2774 field.setAttribute("data-accessor", typeof descriptor.get + ":" + typeof descriptor.set);
2775 descriptor.set.call(field, "tracked");
2776 field.setAttribute("data-read", descriptor.get.call(field));
2777 "#, None);
2778
2779 let field = dom.get_element_by_id("field").unwrap();
2780 let field = field.borrow();
2781 assert_eq!(field.value.get_attr("data-prototype"), Some("true"));
2782 assert_eq!(field.value.get_attr("data-interface"), Some("true"));
2783 assert_eq!(field.value.get_attr("data-own-value"), Some("false"));
2784 assert_eq!(
2785 field.value.get_attr("data-accessor"),
2786 Some("function:function")
2787 );
2788 assert_eq!(field.value.get_attr("data-read"), Some("tracked"));
2789 assert_eq!(field.value.get_attr("value"), Some("tracked"));
2790 }
2791
2792 #[test]
2793 fn exposes_document_and_node_metadata_used_by_react_dom() {
2794 let (mut runtime, dom) = runtime_from_html(
2795 r#"<html><body><main id="root"><span id="child">text</span></main></body></html>"#,
2796 );
2797 runtime.run_script(
2798 r#"
2799 const root = document.getElementById("root");
2800 const child = document.getElementById("child");
2801 child.setAttribute("data-default-view", document.defaultView === window);
2802 child.setAttribute("data-ready-before", document.readyState);
2803 child.setAttribute("data-local-name", child.localName);
2804 child.setAttribute("data-parent-element", child.parentElement === root);
2805 child.setAttribute("data-connected", child.isConnected);
2806 child.setAttribute("data-text-connected", child.firstChild.isConnected);
2807 "#,
2808 None,
2809 );
2810
2811 let child = dom.get_element_by_id("child").unwrap();
2812 let child = child.borrow();
2813 assert_eq!(child.value.get_attr("data-default-view"), Some("true"));
2814 assert_eq!(child.value.get_attr("data-ready-before"), Some("loading"));
2815 assert_eq!(child.value.get_attr("data-local-name"), Some("span"));
2816 assert_eq!(child.value.get_attr("data-parent-element"), Some("true"));
2817 assert_eq!(child.value.get_attr("data-connected"), Some("true"));
2818 assert_eq!(child.value.get_attr("data-text-connected"), Some("true"));
2819 drop(child);
2820
2821 assert!(runtime.dispatch_dom_content_loaded());
2822 runtime.run_script(
2823 r#"
2824 document.getElementById("child").setAttribute(
2825 "data-ready-after",
2826 document.readyState
2827 );
2828 "#,
2829 None,
2830 );
2831 assert_eq!(
2832 dom.get_element_by_id("child")
2833 .unwrap()
2834 .borrow()
2835 .value
2836 .get_attr("data-ready-after"),
2837 Some("complete")
2838 );
2839 }
2840
2841 #[test]
2842 fn style_declaration_mutates_inline_style() {
2843 let (mut runtime, dom) = runtime_from_html(r#"<div id="target"></div>"#);
2844 runtime.run_script(
2845 r#"
2846 const target = document.getElementById("target");
2847 target.style.backgroundColor = "red";
2848 target.style.setProperty("--accent", "blue");
2849 target.style.marginTop = "4px";
2850 target.style.removeProperty("background-color");
2851 "#,
2852 None,
2853 );
2854
2855 let node = dom.get_element_by_id("target").unwrap();
2856 assert_eq!(
2857 node.borrow().value.get_attr("style"),
2858 Some("--accent: blue; margin-top: 4px;")
2859 );
2860 assert!(runtime.needs_redraw());
2861 }
2862
2863 #[test]
2864 fn inner_html_parses_replaces_and_serializes_children() {
2865 let (mut runtime, dom) = runtime_from_html(
2866 r#"<div id="target"><em id="old">old</em></div><div id="result"></div>"#,
2867 );
2868 runtime.run_script(
2869 r#"
2870 const target = document.getElementById("target");
2871 const old = document.getElementById("old");
2872 target.innerHTML = '<span id="child" data-label="a&b">hello</span><br>';
2873 old.setAttribute("data-detached", "yes");
2874 document.getElementById("result").setAttribute("data-html", target.innerHTML);
2875 "#,
2876 None,
2877 );
2878
2879 let target = dom.get_element_by_id("target").unwrap();
2880 assert_eq!(target.borrow().children().len(), 2);
2881 assert!(dom.get_element_by_id("old").is_none());
2882 assert_eq!(
2883 dom.get_element_by_id("result")
2884 .unwrap()
2885 .borrow()
2886 .value
2887 .get_attr("data-html"),
2888 Some("<span id=\"child\" data-label=\"a&b\">hello</span><br>")
2889 );
2890 assert!(runtime.needs_redraw());
2891 }
2892
2893 #[test]
2894 fn style_property_names_follow_cssom_spelling() {
2895 assert_eq!(style_property_name("backgroundColor"), "background-color");
2896 assert_eq!(style_property_name("msTransition"), "-ms-transition");
2897 assert_eq!(style_property_name("WebkitTransform"), "-webkit-transform");
2898 assert_eq!(style_property_name("cssFloat"), "float");
2899 assert_eq!(style_property_name("--accent"), "--accent");
2900 }
2901
2902 #[test]
2903 fn get_attribute_reads_dom() {
2904 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello" data-x="v"></div>"#);
2905 runtime.run_script(
2906 r#"const el = document.getElementById("hello"); if (el.getAttribute("data-x") !== "v") { throw new Error("mismatch"); }"#, None);
2907 }
2908
2909 #[test]
2910 fn missing_id_returns_null() {
2911 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello"></div>"#);
2912 runtime.run_script(
2913 r#"const el = document.getElementById("missing"); if (el !== null) { throw new Error("expected null"); }"#, None);
2914 }
2915
2916 #[test]
2917 fn console_log_does_not_panic() {
2918 let (mut runtime, _dom) = runtime_from_html(r#"<html></html>"#);
2919 runtime.run_script(
2920 r#"console.log("a", 1, undefined); console.warn("w"); console.error("e");"#,
2921 None,
2922 );
2923 }
2924
2925 #[test]
2926 fn syntax_error_is_logged_not_panicked() {
2927 let (mut runtime, _dom) = runtime_from_html(r#"<html></html>"#);
2928 runtime.run_script("this is not valid js ((", None);
2929 }
2930
2931 #[test]
2932 fn accessor_reads_text_content() {
2933 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello">hi</div>"#);
2934 runtime.run_script(
2935 r#"const el = document.getElementById("hello"); if (el.textContent !== "hi") { throw new Error("mismatch"); }"#, None);
2936 }
2937
2938 #[test]
2939 fn click_invokes_onclick_and_mutates_dom() {
2940 let (mut runtime, dom) =
2941 runtime_from_html(r#"<button id="b">click me</button><p id="result">not clicked</p>"#);
2942 runtime.run_script(
2943 r#"
2944 const button = document.getElementById("b");
2945 const result = document.getElementById("result");
2946 button.onclick = function () {
2947 result.textContent = "clicked!";
2948 result.setAttribute("data-clicked", "true");
2949 };
2950 "#,
2951 None,
2952 );
2953
2954 let button = dom.get_element_by_id("b").unwrap();
2955 assert!(runtime.click(&button));
2956 assert!(runtime.needs_redraw());
2957 assert!(runtime.take_needs_redraw());
2958
2959 let result = dom.get_element_by_id("result").unwrap();
2960 assert_eq!(DomTree::inner_text(&result), "clicked!");
2961 assert_eq!(result.borrow().value.get_attr("data-clicked"), Some("true"));
2962 }
2963
2964 #[test]
2965 fn click_without_handler_is_noop() {
2966 let (mut runtime, dom) = runtime_from_html(r#"<div id="x"></div>"#);
2967 runtime.run_script(r#"document.getElementById("x");"#, None);
2968 let node = dom.get_element_by_id("x").unwrap();
2969 assert!(!runtime.click(&node));
2970 assert!(!runtime.needs_redraw());
2971 }
2972
2973 #[test]
2974 fn click_invokes_element_event_listeners_in_registration_order() {
2975 let (mut runtime, dom) =
2976 runtime_from_html(r#"<button id="button">click</button><div id="result"></div>"#);
2977 runtime.run_script(
2978 r#"
2979 const button = document.getElementById("button");
2980 const result = document.getElementById("result");
2981 let order = "";
2982 button.addEventListener("click", function (event) {
2983 order = order + "a";
2984 result.setAttribute("data-event-type", event.type);
2985 });
2986 button.addEventListener("click", function () {
2987 order = order + "b";
2988 result.setAttribute("data-order", order);
2989 });
2990 "#,
2991 None,
2992 );
2993
2994 let button = dom.get_element_by_id("button").unwrap();
2995 assert!(runtime.click(&button));
2996
2997 let result = dom.get_element_by_id("result").unwrap();
2998 assert_eq!(result.borrow().value.get_attr("data-order"), Some("ab"));
2999 assert_eq!(
3000 result.borrow().value.get_attr("data-event-type"),
3001 Some("click")
3002 );
3003 assert!(runtime.needs_redraw());
3004 }
3005
3006 #[test]
3007 fn event_listeners_are_deduplicated_and_removable() {
3008 let (mut runtime, dom) =
3009 runtime_from_html(r#"<button id="button">click</button><div id="result"></div>"#);
3010 runtime.run_script(
3011 r#"
3012 const button = document.getElementById("button");
3013 const result = document.getElementById("result");
3014 function listener() {
3015 const count = result.getAttribute("data-count");
3016 result.setAttribute("data-count", count === null ? 1 : Number(count) + 1);
3017 }
3018 button.addEventListener("click", listener);
3019 button.addEventListener("click", listener);
3020 "#,
3021 None,
3022 );
3023
3024 let button = dom.get_element_by_id("button").unwrap();
3025 assert!(runtime.click(&button));
3026 let result = dom.get_element_by_id("result").unwrap();
3027 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
3028
3029 runtime.run_script(
3030 r#"
3031 button.removeEventListener("click", listener);
3032 window.addEventListener("test", listener);
3033 window.removeEventListener("test", listener);
3034 "#,
3035 None,
3036 );
3037 assert!(!runtime.click(&button));
3038 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
3039 }
3040
3041 #[test]
3042 fn document_event_listeners_can_be_removed() {
3043 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3044 runtime.run_script(
3045 r#"
3046 const result = document.getElementById("result");
3047 function listener() { result.setAttribute("data-ran", "yes"); }
3048 document.addEventListener("DOMContentLoaded", listener);
3049 document.removeEventListener("DOMContentLoaded", listener);
3050 "#,
3051 None,
3052 );
3053
3054 assert!(runtime.dispatch_dom_content_loaded());
3055 let result = dom.get_element_by_id("result").unwrap();
3056 assert_eq!(result.borrow().value.get_attr("data-ran"), None);
3057 }
3058
3059 #[test]
3060 fn click_bubbles_to_delegated_ancestor_listeners() {
3061 let (mut runtime, dom) = runtime_from_html(
3062 r#"<main id="root"><button id="button">click</button></main><div id="result"></div>"#,
3063 );
3064 runtime.run_script(
3065 r#"
3066 const root = document.getElementById("root");
3067 const result = document.getElementById("result");
3068 root.addEventListener("click", function (event) {
3069 result.setAttribute("data-target", event.target.id);
3070 result.setAttribute("data-current", event.currentTarget.id);
3071 result.setAttribute("data-this", this.id);
3072 event.preventDefault();
3073 result.setAttribute("data-prevented", event.defaultPrevented);
3074 });
3075 "#,
3076 None,
3077 );
3078
3079 let button = dom.get_element_by_id("button").unwrap();
3080 assert!(runtime.click(&button));
3081 let result = dom.get_element_by_id("result").unwrap();
3082 let result = result.borrow();
3083 assert_eq!(result.value.get_attr("data-target"), Some("button"));
3084 assert_eq!(result.value.get_attr("data-current"), Some("root"));
3085 assert_eq!(result.value.get_attr("data-this"), Some("root"));
3086 assert_eq!(result.value.get_attr("data-prevented"), Some("true"));
3087 }
3088
3089 #[test]
3090 fn click_propagation_can_be_stopped() {
3091 let (mut runtime, dom) = runtime_from_html(
3092 r#"<main id="root"><button id="button">click</button></main><div id="result"></div>"#,
3093 );
3094 runtime.run_script(
3095 r#"
3096 const root = document.getElementById("root");
3097 const button = document.getElementById("button");
3098 const result = document.getElementById("result");
3099 button.addEventListener("click", function (event) {
3100 result.setAttribute("data-child", "ran");
3101 event.stopPropagation();
3102 });
3103 root.addEventListener("click", function () {
3104 result.setAttribute("data-root", "ran");
3105 });
3106 "#,
3107 None,
3108 );
3109
3110 let button = dom.get_element_by_id("button").unwrap();
3111 assert!(runtime.click(&button));
3112 let result = dom.get_element_by_id("result").unwrap();
3113 let result = result.borrow();
3114 assert_eq!(result.value.get_attr("data-child"), Some("ran"));
3115 assert_eq!(result.value.get_attr("data-root"), None);
3116 }
3117
3118 #[test]
3119 fn click_does_not_invoke_other_event_types() {
3120 let (mut runtime, dom) = runtime_from_html(r#"<button id="button">click</button>"#);
3121 runtime.run_script(
3122 r#"
3123 const button = document.getElementById("button");
3124 button.addEventListener("mouseover", function () {
3125 button.setAttribute("data-ran", "yes");
3126 });
3127 "#,
3128 None,
3129 );
3130
3131 let button = dom.get_element_by_id("button").unwrap();
3132 assert!(!runtime.click(&button));
3133 assert_eq!(button.borrow().value.get_attr("data-ran"), None);
3134 }
3135
3136 #[test]
3137 fn scroll_invokes_onscroll_and_mutates_dom() {
3138 let (mut runtime, dom) =
3139 runtime_from_html(r#"<div id="s">scroll</div><p id="result">not scrolled</p>"#);
3140 runtime.run_script(
3141 r#"
3142 const s = document.getElementById("s");
3143 const result = document.getElementById("result");
3144 window.__scrolls = 0;
3145 s.onscroll = function () {
3146 window.__scrolls = (window.__scrolls || 0) + 1;
3147 result.textContent = "scrolled!";
3148 };
3149 "#,
3150 None,
3151 );
3152
3153 let s = dom.get_element_by_id("s").unwrap();
3154 assert!(runtime.scroll(&s));
3155 let result = dom.get_element_by_id("result").unwrap();
3156 assert_eq!(DomTree::inner_text(&result), "scrolled!");
3157 }
3158
3159 #[test]
3160 fn scroll_without_handler_is_noop() {
3161 let (mut runtime, dom) = runtime_from_html(r#"<div id="x"></div>"#);
3162 runtime.run_script(r#"document.getElementById("x");"#, None);
3163 let node = dom.get_element_by_id("x").unwrap();
3164 assert!(!runtime.scroll(&node));
3165 }
3166
3167 #[test]
3168 fn scroll_invokes_element_listeners_in_registration_order() {
3169 let (mut runtime, dom) =
3170 runtime_from_html(r#"<div id="s">scroll</div><div id="result"></div>"#);
3171 runtime.run_script(
3172 r#"
3173 const s = document.getElementById("s");
3174 const result = document.getElementById("result");
3175 let order = "";
3176 s.addEventListener("scroll", function (event) {
3177 order = order + "a";
3178 result.setAttribute("data-event-type", event.type);
3179 });
3180 s.addEventListener("scroll", function () {
3181 order = order + "b";
3182 result.setAttribute("data-order", order);
3183 });
3184 "#,
3185 None,
3186 );
3187
3188 let s = dom.get_element_by_id("s").unwrap();
3189 assert!(runtime.scroll(&s));
3190
3191 let result = dom.get_element_by_id("result").unwrap();
3192 assert_eq!(result.borrow().value.get_attr("data-order"), Some("ab"));
3193 assert_eq!(
3194 result.borrow().value.get_attr("data-event-type"),
3195 Some("scroll")
3196 );
3197 }
3198
3199 #[test]
3200 fn scroll_bubbles_to_delegated_ancestor_listeners() {
3201 let (mut runtime, dom) = runtime_from_html(
3202 r#"<main id="root"><div id="s">scroll</div></main><div id="result"></div>"#,
3203 );
3204 runtime.run_script(
3205 r#"
3206 const root = document.getElementById("root");
3207 const result = document.getElementById("result");
3208 root.addEventListener("scroll", function (event) {
3209 result.setAttribute("data-target", event.target.id);
3210 });
3211 "#,
3212 None,
3213 );
3214
3215 let s = dom.get_element_by_id("s").unwrap();
3216 assert!(runtime.scroll(&s));
3217
3218 let result = dom.get_element_by_id("result").unwrap();
3219 assert_eq!(result.borrow().value.get_attr("data-target"), Some("s"));
3220 }
3221
3222 #[test]
3223 fn scroll_dom_id_dispatches_to_the_named_element() {
3224 let (mut runtime, dom) = runtime_from_html(r#"<div id="s">scroll</div>"#);
3225 runtime.run_script(
3226 r#"
3227 const s = document.getElementById("s");
3228 s.addEventListener("scroll", function () {
3229 s.setAttribute("data-ran", "yes");
3230 });
3231 "#,
3232 None,
3233 );
3234 let s = dom.get_element_by_id("s").unwrap();
3235 let dom_id = with_host(runtime.engine.vm(), |host| {
3238 host.refs
3239 .iter()
3240 .find(|(_, weak)| {
3241 weak.upgrade()
3242 .is_some_and(|n| Rc::as_ptr(&n) == Rc::as_ptr(&s))
3243 })
3244 .map(|(id, _)| *id)
3245 })
3246 .flatten()
3247 .expect("element must be registered in the host refs");
3248 assert!(runtime.scroll_dom_id(dom_id));
3249 assert_eq!(s.borrow().value.get_attr("data-ran"), Some("yes"));
3250 }
3251
3252 #[test]
3253 fn scroll_does_not_invoke_other_event_types() {
3254 let (mut runtime, dom) = runtime_from_html(r#"<div id="s">scroll</div>"#);
3255 runtime.run_script(
3256 r#"
3257 const s = document.getElementById("s");
3258 s.addEventListener("mouseover", function () {
3259 s.setAttribute("data-ran", "yes");
3260 });
3261 "#,
3262 None,
3263 );
3264
3265 let s = dom.get_element_by_id("s").unwrap();
3266 assert!(!runtime.scroll(&s));
3267 assert_eq!(s.borrow().value.get_attr("data-ran"), None);
3268 }
3269
3270 #[test]
3271 fn get_element_by_id_reuses_the_same_object() {
3272 let (mut runtime, _dom) = runtime_from_html(r#"<div id="x"></div>"#);
3273 runtime.run_script(
3274 r#"
3275 const a = document.getElementById("x");
3276 const b = document.getElementById("x");
3277 a.onclick = function () {};
3278 if (a !== b) { throw new Error("expected the same object"); }
3279 "#,
3280 None,
3281 );
3282 }
3283
3284 #[test]
3285 fn dom_content_loaded_listener_runs_when_dispatched() {
3286 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3287 runtime.run_script(
3288 r#"
3289 document.addEventListener("DOMContentLoaded", function (event) {
3290 const result = document.getElementById("result");
3291 result.setAttribute("data-ready", "yes");
3292 result.setAttribute("data-event-type", event.type);
3293 });
3294 "#,
3295 None,
3296 );
3297
3298 let result = dom.get_element_by_id("result").unwrap();
3299 assert_eq!(result.borrow().value.get_attr("data-ready"), None);
3300 assert!(runtime.dispatch_dom_content_loaded());
3301 assert_eq!(result.borrow().value.get_attr("data-ready"), Some("yes"));
3302 assert_eq!(
3303 result.borrow().value.get_attr("data-event-type"),
3304 Some("DOMContentLoaded")
3305 );
3306 assert!(runtime.needs_redraw());
3307 }
3308
3309 #[test]
3310 fn dom_content_loaded_is_dispatched_only_once() {
3311 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3312 runtime.run_script(
3313 r#"
3314 let dispatchCount = 0;
3315 document.addEventListener("DOMContentLoaded", function () {
3316 dispatchCount = dispatchCount + 1;
3317 document.getElementById("result").setAttribute("data-count", dispatchCount);
3318 });
3319 "#,
3320 None,
3321 );
3322
3323 assert!(runtime.dispatch_dom_content_loaded());
3324 assert!(!runtime.dispatch_dom_content_loaded());
3325 let result = dom.get_element_by_id("result").unwrap();
3326 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
3327 }
3328
3329 #[test]
3330 fn window_onload_runs_when_dispatched() {
3331 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3332 runtime.run_script(
3333 r#"
3334 window.onload = function (event) {
3335 const result = document.getElementById("result");
3336 result.setAttribute("data-ready", "yes");
3337 result.setAttribute("data-event-type", event.type);
3338 };
3339 "#,
3340 None,
3341 );
3342
3343 let result = dom.get_element_by_id("result").unwrap();
3344 assert_eq!(result.borrow().value.get_attr("data-ready"), None);
3345 assert!(runtime.dispatch_window_load());
3346 assert_eq!(result.borrow().value.get_attr("data-ready"), Some("yes"));
3347 assert_eq!(
3348 result.borrow().value.get_attr("data-event-type"),
3349 Some("load")
3350 );
3351 }
3352
3353 #[test]
3354 fn window_load_listener_runs_when_dispatched_and_only_once() {
3355 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3356 runtime.run_script(
3357 r#"
3358 let dispatchCount = 0;
3359 window.addEventListener("load", function () {
3360 dispatchCount = dispatchCount + 1;
3361 document.getElementById("result").setAttribute("data-count", dispatchCount);
3362 });
3363 "#,
3364 None,
3365 );
3366
3367 assert!(runtime.dispatch_window_load());
3368 assert!(!runtime.dispatch_window_load());
3369 let result = dom.get_element_by_id("result").unwrap();
3370 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
3371 }
3372
3373 #[test]
3374 fn body_onload_registered_at_setup_runs_on_dispatch() {
3375 let (mut runtime, dom) = runtime_from_html(
3376 r#"<body onload="document.getElementById('result').setAttribute('data-onload', 'yes')"><div id="result"></div></body>"#,
3377 );
3378
3379 let result = dom.get_element_by_id("result").unwrap();
3380 assert_eq!(result.borrow().value.get_attr("data-onload"), None);
3381 assert!(runtime.dispatch_window_load());
3382 assert_eq!(result.borrow().value.get_attr("data-onload"), Some("yes"));
3383 assert!(!runtime.dispatch_window_load());
3385 }
3386
3387 #[test]
3388 fn document_query_selector_and_query_selector_all_expose_elements() {
3389 let (mut runtime, dom) = runtime_from_html(
3390 r#"
3391 <div id="result"></div>
3392 <main><p class="item">first</p><p class="item featured">second</p></main>
3393 "#,
3394 );
3395 runtime.run_script(
3396 r#"
3397 const featured = document.querySelector("main > p.featured");
3398 featured.setAttribute("data-selected", "yes");
3399 const items = document.querySelectorAll("p.item");
3400 const classified = document.querySelectorAll("[class]");
3401 items[0].setAttribute("data-first", "yes");
3402 items.forEach(function (item, index) {
3403 item.setAttribute("data-index", index);
3404 });
3405 document.getElementById("result").setAttribute("data-count", items.length);
3406 document.getElementById("result").setAttribute("data-class-count", classified.length);
3407 "#,
3408 None,
3409 );
3410
3411 let items = dom.get_elements_by_class_name("item");
3412 assert_eq!(items[0].borrow().value.get_attr("data-first"), Some("yes"));
3413 assert_eq!(items[0].borrow().value.get_attr("data-index"), Some("0"));
3414 assert_eq!(items[1].borrow().value.get_attr("data-index"), Some("1"));
3415 assert_eq!(
3416 items[1].borrow().value.get_attr("data-selected"),
3417 Some("yes")
3418 );
3419 let result = dom.get_element_by_id("result").unwrap();
3420 assert_eq!(result.borrow().value.get_attr("data-count"), Some("2"));
3421 assert_eq!(
3422 result.borrow().value.get_attr("data-class-count"),
3423 Some("2")
3424 );
3425 }
3426
3427 #[test]
3428 fn element_query_selectors_are_scoped_to_descendants() {
3429 let (mut runtime, dom) = runtime_from_html(
3430 r#"
3431 <section id="scope"><span class="item">one</span><span class="item">two</span></section>
3432 <span class="item" id="outside">outside</span>
3433 "#,
3434 );
3435 runtime.run_script(
3436 r##"
3437 const scope = document.querySelector("#scope");
3438 scope.querySelector(".item").setAttribute("data-first", "yes");
3439 const items = scope.querySelectorAll(".item");
3440 items[1].setAttribute("data-second", "yes");
3441 scope.setAttribute("data-count", items.length);
3442 "##,
3443 None,
3444 );
3445
3446 let scope = dom.get_element_by_id("scope").unwrap();
3447 assert_eq!(scope.borrow().value.get_attr("data-count"), Some("2"));
3448 let items = DomTree::query_selector_all_within(&scope, ".item");
3449 assert_eq!(items[0].borrow().value.get_attr("data-first"), Some("yes"));
3450 assert_eq!(items[1].borrow().value.get_attr("data-second"), Some("yes"));
3451 let outside = dom.get_element_by_id("outside").unwrap();
3452 assert_eq!(outside.borrow().value.get_attr("data-first"), None);
3453 assert_eq!(outside.borrow().value.get_attr("data-second"), None);
3454 }
3455
3456 #[test]
3457 fn react_dom_collection_and_event_apis_are_available() {
3458 let (mut runtime, dom) = runtime_from_html(
3459 r#"
3460 <main id="root">
3461 <button class="control primary">one</button>
3462 <button class="control">two</button>
3463 <span class="primary">label</span>
3464 </main>
3465 <div id="result"></div>
3466 "#,
3467 );
3468 runtime.run_script(
3469 r#"
3470 const root = document.getElementById("root");
3471 const button = root.getElementsByTagName("button")[0];
3472 let received = "no";
3473 button.addEventListener("scratch-ready", function (event) {
3474 received = event.detail + ":" + (event.target === button);
3475 event.preventDefault();
3476 });
3477 const accepted = button.dispatchEvent(new CustomEvent(
3478 "scratch-ready", {detail: "yes", cancelable: true}
3479 ));
3480 document.getElementById("result").setAttribute(
3481 "data-dom-apis",
3482 document.getElementsByTagName("button").length + ":" +
3483 document.getElementsByClassName("control primary").length + ":" +
3484 root.getElementsByClassName("primary").length + ":" + received + ":" + accepted
3485 );
3486 "#,
3487 None,
3488 );
3489
3490 let result = dom.get_element_by_id("result").unwrap();
3491 assert_eq!(
3492 result.borrow().value.get_attr("data-dom-apis"),
3493 Some("2:1:2:yes:true:false")
3494 );
3495 }
3496
3497 #[test]
3498 fn create_and_append_element_and_text_nodes() {
3499 let (mut runtime, dom) = runtime_from_html(r#"<ul id="list"></ul>"#);
3500 runtime.run_script(
3501 r##"
3502 const item = document.createElement("li");
3503 item.setAttribute("class", "dynamic");
3504 const text = document.createTextNode("created by JavaScript");
3505 item.appendChild(text);
3506 document.querySelector("#list").appendChild(item);
3507 "##,
3508 None,
3509 );
3510
3511 let item = dom.query_selector("li.dynamic").unwrap();
3512 assert_eq!(DomTree::inner_text(&item), "created by JavaScript");
3513 assert!(runtime.needs_redraw());
3514 }
3515
3516 #[test]
3517 fn document_head_and_element_append_insert_dynamic_styles() {
3518 let (mut runtime, dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
3519 runtime.run_script(
3520 r#"
3521 const style = document.createElement("style");
3522 style.append("body { color: red; }");
3523 document.head.append(style);
3524 "#,
3525 None,
3526 );
3527
3528 let style = dom.query_selector("head style").unwrap();
3529 assert_eq!(DomTree::inner_text(&style), "body { color: red; }");
3530 assert!(runtime.needs_redraw());
3531 }
3532
3533 #[test]
3534 fn namespace_dom_apis_create_svg_elements_and_attributes() {
3535 let (mut runtime, dom) =
3536 runtime_from_html(r#"<main id="root"></main><div id="result"></div>"#);
3537 runtime.run_script(
3538 r##"
3539 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3540 const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3541 path.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#shape");
3542 svg.appendChild(path);
3543 document.querySelector("#root").appendChild(svg);
3544
3545 const result = document.querySelector("#result");
3546 result.setAttribute("data-svg-ns", svg.namespaceURI);
3547 result.setAttribute("data-path-ns", path.namespaceURI);
3548 result.setAttribute("data-html-ns", result.namespaceURI);
3549 "##,
3550 None,
3551 );
3552
3553 let path = dom.query_selector("path").unwrap();
3554 assert_eq!(path.borrow().value.get_attr("xlink:href"), Some("#shape"));
3555 let result = dom.get_element_by_id("result").unwrap();
3556 let result = result.borrow();
3557 assert_eq!(result.value.get_attr("data-svg-ns"), Some(SVG_NAMESPACE));
3558 assert_eq!(result.value.get_attr("data-path-ns"), Some(SVG_NAMESPACE));
3559 assert_eq!(result.value.get_attr("data-html-ns"), Some(HTML_NAMESPACE));
3560 assert!(runtime.needs_redraw());
3561 }
3562
3563 #[test]
3564 fn element_contains_checks_self_descendants_and_unrelated_nodes() {
3565 let (mut runtime, dom) = runtime_from_html(
3566 r#"<main id="root"><section id="child"><span id="nested"></span></section></main><aside id="other"></aside>"#,
3567 );
3568 runtime.run_script(
3569 r##"
3570 const root = document.querySelector("#root");
3571 const child = document.querySelector("#child");
3572 const nested = document.querySelector("#nested");
3573 const other = document.querySelector("#other");
3574 root.setAttribute("data-self", root.contains(root));
3575 root.setAttribute("data-child", root.contains(child));
3576 root.setAttribute("data-nested", root.contains(nested));
3577 root.setAttribute("data-other", root.contains(other));
3578 root.setAttribute("data-null", root.contains(null));
3579 "##,
3580 None,
3581 );
3582
3583 let root = dom.get_element_by_id("root").unwrap();
3584 let root = root.borrow();
3585 assert_eq!(root.value.get_attr("data-self"), Some("true"));
3586 assert_eq!(root.value.get_attr("data-child"), Some("true"));
3587 assert_eq!(root.value.get_attr("data-nested"), Some("true"));
3588 assert_eq!(root.value.get_attr("data-other"), Some("false"));
3589 assert_eq!(root.value.get_attr("data-null"), Some("false"));
3590 }
3591
3592 #[test]
3593 fn document_tracks_the_focused_element() {
3594 let (mut runtime, dom) = runtime_from_html(
3595 r#"<body><input id="field"><button id="other"></button><div id="result"></div></body>"#,
3596 );
3597 runtime.run_script(
3598 r##"
3599 const field = document.querySelector("#field");
3600 const other = document.querySelector("#other");
3601 const result = document.querySelector("#result");
3602 result.setAttribute("data-initial", document.activeElement === document.body);
3603 field.focus();
3604 result.setAttribute("data-field", document.activeElement === field);
3605 other.focus();
3606 result.setAttribute("data-other", document.activeElement === other);
3607 other.blur();
3608 result.setAttribute("data-blurred", document.activeElement === document.body);
3609 result.setAttribute("data-has-focus", document.hasFocus());
3610 "##,
3611 None,
3612 );
3613
3614 let result = dom.get_element_by_id("result").unwrap();
3615 let result = result.borrow();
3616 assert_eq!(result.value.get_attr("data-initial"), Some("true"));
3617 assert_eq!(result.value.get_attr("data-field"), Some("true"));
3618 assert_eq!(result.value.get_attr("data-other"), Some("true"));
3619 assert_eq!(result.value.get_attr("data-blurred"), Some("true"));
3620 assert_eq!(result.value.get_attr("data-has-focus"), Some("true"));
3621 }
3622
3623 #[test]
3624 fn react_dom_node_primitives_identify_and_reorder_nodes() {
3625 let (mut runtime, dom) =
3626 runtime_from_html(r#"<main id="root"></main><div id="result"></div>"#);
3627 runtime.run_script(
3628 r##"
3629 const root = document.querySelector("#root");
3630 const first = document.createElement("span");
3631 first.setAttribute("data-name", "first");
3632 const second = document.createElement("span");
3633 second.setAttribute("data-name", "second");
3634 const text = document.createTextNode("before");
3635 text.nodeValue = "after";
3636 second.appendChild(text);
3637 root.appendChild(first);
3638 root.insertBefore(second, first);
3639 root.removeChild(first);
3640 second.className = "react-node";
3641 second.setAttribute("data-remove", "yes");
3642 second.removeAttribute("data-remove");
3643
3644 const result = document.querySelector("#result");
3645 result.setAttribute("data-document-type", document.nodeType);
3646 result.setAttribute("data-root-type", root.nodeType);
3647 result.setAttribute("data-root-name", root.nodeName);
3648 result.setAttribute("data-owner", root.ownerDocument === document);
3649 result.setAttribute("data-first", root.firstChild.getAttribute("data-name"));
3650 result.setAttribute("data-last", root.lastChild.getAttribute("data-name"));
3651 result.setAttribute("data-count", root.childNodes.length);
3652 result.setAttribute("data-text", root.firstChild.firstChild.data);
3653 result.setAttribute("data-class", root.firstChild.className);
3654 result.setAttribute("data-removed", root.firstChild.hasAttribute("data-remove"));
3655 "##,
3656 None,
3657 );
3658
3659 let result = dom.get_element_by_id("result").unwrap();
3660 let result = result.borrow();
3661 assert_eq!(result.value.get_attr("data-document-type"), Some("9"));
3662 assert_eq!(result.value.get_attr("data-root-type"), Some("1"));
3663 assert_eq!(result.value.get_attr("data-root-name"), Some("MAIN"));
3664 assert_eq!(result.value.get_attr("data-owner"), Some("true"));
3665 assert_eq!(result.value.get_attr("data-first"), Some("second"));
3666 assert_eq!(result.value.get_attr("data-last"), Some("second"));
3667 assert_eq!(result.value.get_attr("data-count"), Some("1"));
3668 assert_eq!(result.value.get_attr("data-text"), Some("after"));
3669 assert_eq!(result.value.get_attr("data-class"), Some("react-node"));
3670 assert_eq!(result.value.get_attr("data-removed"), Some("false"));
3671
3672 let root = dom.get_element_by_id("root").unwrap();
3673 assert_eq!(root.borrow().children().len(), 1);
3674 assert_eq!(
3675 root.borrow().children()[0]
3676 .borrow()
3677 .value
3678 .get_attr("data-remove"),
3679 None
3680 );
3681 }
3682
3683 #[test]
3684 fn html_iframe_element_supports_host_instance_checks() {
3685 let (mut runtime, dom) = runtime_from_html(
3686 r#"<body><iframe id="frame"></iframe><div id="result"></div></body>"#,
3687 );
3688 runtime.run_script(
3689 r#"
3690 const frame = document.getElementById("frame");
3691 const result = document.getElementById("result");
3692 result.setAttribute("data-frame", frame instanceof HTMLIFrameElement);
3693 result.setAttribute("data-body", document.body instanceof HTMLIFrameElement);
3694 "#,
3695 None,
3696 );
3697
3698 let result = dom.get_element_by_id("result").unwrap();
3699 let result = result.borrow();
3700 assert_eq!(result.value.get_attr("data-frame"), Some("true"));
3701 assert_eq!(result.value.get_attr("data-body"), Some("false"));
3702 }
3703
3704 #[test]
3705 fn markup_declared_iframes_queue_loads_once_and_failures_are_not_retried() {
3706 let (mut runtime, dom) = runtime_from_html(
3707 r#"<html><body>
3708 <iframe src="https://example.test/frame-a.html"></iframe>
3709 <iframe src="frames/frame-b.html"></iframe>
3710 <iframe id="placeholder"></iframe>
3711 </body></html>"#,
3712 );
3713 runtime.set_document_url("https://example.test/dir/page.html");
3714
3715 let ids: HashMap<usize, u64> = {
3718 let mut ids = HashMap::new();
3719 let mut next_id = 1u64;
3720 dom.traverse(|node| {
3721 ids.insert(Rc::as_ptr(node) as usize, next_id);
3722 next_id += 1;
3723 });
3724 ids
3725 };
3726 let snapshot = DomSnapshot::from_mirror(&dom.root, &ids);
3727 runtime.apply_dom(&snapshot);
3728
3729 assert_eq!(runtime.queue_markup_iframe_loads(), 2);
3732 let requests = runtime.take_iframe_fetch_requests();
3733 assert_eq!(requests.len(), 2);
3734 let by_url: HashMap<String, u64> = requests
3735 .into_iter()
3736 .map(|req| (req.url, req.dom_id))
3737 .collect();
3738 assert_eq!(
3739 by_url.len(),
3740 2,
3741 "one request per src-ified iframe, absolute or relative"
3742 );
3743 assert!(by_url.contains_key("https://example.test/frame-a.html"));
3745 assert!(by_url.contains_key("https://example.test/dir/frames/frame-b.html"));
3746 let frame_a = by_url["https://example.test/frame-a.html"];
3747 let frame_b = by_url["https://example.test/dir/frames/frame-b.html"];
3748
3749 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
3751 assert!(runtime.take_iframe_fetch_requests().is_empty());
3752
3753 runtime.resolve_iframe_fetch(
3755 frame_a,
3756 r#"<html><body><p>frame content</p></body></html>"#.to_string(),
3757 "https://example.test/frame-a.html".to_string(),
3758 );
3759 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
3760 assert!(runtime.take_iframe_fetch_requests().is_empty());
3761
3762 runtime.reject_iframe_fetch(frame_b);
3764 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
3765 assert!(runtime.take_iframe_fetch_requests().is_empty());
3766 }
3767
3768 #[test]
3769 fn remove_detaches_node_but_keeps_it_available_for_reappend() {
3770 let (mut runtime, dom) = runtime_from_html(
3771 r#"<div id="first"><span id="moving">move</span></div><div id="second"></div>"#,
3772 );
3773 runtime.run_script(
3774 r##"
3775 const moving = document.querySelector("#moving");
3776 moving.remove();
3777 document.querySelector("#second").appendChild(moving);
3778 "##,
3779 None,
3780 );
3781
3782 let first = dom.get_element_by_id("first").unwrap();
3783 let second = dom.get_element_by_id("second").unwrap();
3784 assert!(DomTree::query_selector_within(&first, "#moving").is_none());
3785 assert!(DomTree::query_selector_within(&second, "#moving").is_some());
3786 assert!(runtime.needs_redraw());
3787 }
3788
3789 #[test]
3790 fn parent_node_and_children_expose_tree_relationships() {
3791 let (mut runtime, dom) = runtime_from_html(
3792 r#"<div id="parent">text<span id="first"></span><span id="second"></span></div>"#,
3793 );
3794 runtime.run_script(
3795 r##"
3796 const first = document.querySelector("#first");
3797 first.parentNode.setAttribute("data-parent", "yes");
3798 const children = first.parentNode.children;
3799 children[1].setAttribute("data-second", "yes");
3800 first.parentNode.setAttribute("data-child-count", children.length);
3801
3802 const text = document.createTextNode("dynamic");
3803 first.appendChild(text);
3804 text.parentNode.setAttribute("data-text-parent", "yes");
3805 "##,
3806 None,
3807 );
3808
3809 let parent = dom.get_element_by_id("parent").unwrap();
3810 assert_eq!(parent.borrow().value.get_attr("data-parent"), Some("yes"));
3811 assert_eq!(
3812 parent.borrow().value.get_attr("data-child-count"),
3813 Some("2")
3814 );
3815 let first = dom.get_element_by_id("first").unwrap();
3816 assert_eq!(
3817 first.borrow().value.get_attr("data-text-parent"),
3818 Some("yes")
3819 );
3820 let second = dom.get_element_by_id("second").unwrap();
3821 assert_eq!(second.borrow().value.get_attr("data-second"), Some("yes"));
3822 }
3823
3824 #[test]
3825 fn class_list_mutates_class_attribute_and_reports_membership() {
3826 let (mut runtime, dom) = runtime_from_html(r#"<div id="target" class="one two"></div>"#);
3827 runtime.run_script(
3828 r##"
3829 const target = document.querySelector("#target");
3830 let initial = "";
3831 for (const token of target.classList) initial += token + ",";
3832 target.setAttribute("data-initial-classes", initial);
3833 target.classList.add("two", "three");
3834 target.classList.remove("one", "missing");
3835 target.setAttribute("data-has-three", target.classList.contains("three"));
3836 target.setAttribute("data-removed-three", target.classList.toggle("three"));
3837 target.setAttribute("data-added-four", target.classList.toggle("four"));
3838 target.setAttribute("data-forced-off", target.classList.toggle("four", false));
3839 target.setAttribute("data-forced-on", target.classList.toggle("five", true));
3840 "##,
3841 None,
3842 );
3843
3844 let target = dom.get_element_by_id("target").unwrap();
3845 let target = target.borrow();
3846 assert_eq!(target.value.get_attr("class"), Some("two five"));
3847 assert_eq!(
3848 target.value.get_attr("data-initial-classes"),
3849 Some("one,two,")
3850 );
3851 assert_eq!(target.value.get_attr("data-has-three"), Some("true"));
3852 assert_eq!(target.value.get_attr("data-removed-three"), Some("false"));
3853 assert_eq!(target.value.get_attr("data-added-four"), Some("true"));
3854 assert_eq!(target.value.get_attr("data-forced-off"), Some("false"));
3855 assert_eq!(target.value.get_attr("data-forced-on"), Some("true"));
3856 assert!(runtime.needs_redraw());
3857 }
3858
3859 #[test]
3860 fn timeout_runs_once_with_additional_arguments_and_can_be_cancelled() {
3861 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3862 runtime.run_script(
3863 r##"
3864 setTimeout(function (value) {
3865 document.querySelector("#result").setAttribute("data-value", value);
3866 }, 0, "done");
3867 const cancelled = setTimeout(function () {
3868 document.querySelector("#result").setAttribute("data-cancelled", "no");
3869 }, 0);
3870 clearTimeout(cancelled);
3871 "##,
3872 None,
3873 );
3874
3875 assert!(runtime.run_due_timers());
3876 assert!(!runtime.run_due_timers());
3877 let result = dom.get_element_by_id("result").unwrap();
3878 assert_eq!(result.borrow().value.get_attr("data-value"), Some("done"));
3879 assert_eq!(result.borrow().value.get_attr("data-cancelled"), None);
3880 }
3881
3882 #[test]
3883 fn interval_can_clear_itself() {
3884 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3885 runtime.run_script(
3886 r##"
3887 const intervalId = setInterval(function () {
3888 document.querySelector("#result").setAttribute("data-ran", "once");
3889 clearInterval(intervalId);
3890 }, 0);
3891 "##,
3892 None,
3893 );
3894
3895 assert!(runtime.run_due_timers());
3896 assert!(!runtime.run_due_timers());
3897 let result = dom.get_element_by_id("result").unwrap();
3898 assert_eq!(result.borrow().value.get_attr("data-ran"), Some("once"));
3899 }
3900
3901 #[test]
3902 fn performance_now_exposes_monotonic_runtime_time() {
3903 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3904 runtime.run_script(
3905 r#"
3906 const first = performance.now();
3907 const second = performance.now();
3908 document.getElementById("result").setAttribute(
3909 "data-monotonic",
3910 typeof first === "number" && second >= first
3911 );
3912 "#,
3913 None,
3914 );
3915
3916 let result = dom.get_element_by_id("result").unwrap();
3917 assert_eq!(
3918 result.borrow().value.get_attr("data-monotonic"),
3919 Some("true")
3920 );
3921 }
3922
3923 #[test]
3924 fn microtasks_run_in_fifo_order_after_script_evaluation() {
3925 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3926 runtime.run_script(
3927 r##"
3928 const result = document.querySelector("#result");
3929 queueMicrotask(function () {
3930 result.setAttribute("data-order", result.getAttribute("data-order") + "-first");
3931 queueMicrotask(function () {
3932 result.setAttribute("data-order", result.getAttribute("data-order") + "-second");
3933 });
3934 });
3935 result.setAttribute("data-order", "sync");
3936 "##, None);
3937
3938 let result = dom.get_element_by_id("result").unwrap();
3939 assert_eq!(
3940 result.borrow().value.get_attr("data-order"),
3941 Some("sync-first-second")
3942 );
3943 }
3944
3945 #[test]
3946 fn timer_microtasks_run_before_the_next_timer_task() {
3947 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3948 runtime.run_script(
3949 r##"
3950 const result = document.querySelector("#result");
3951 setTimeout(function () {
3952 result.setAttribute("data-order", "timer");
3953 queueMicrotask(function () {
3954 result.setAttribute("data-order", "timer-microtask");
3955 });
3956 }, 0);
3957 setTimeout(function () {
3958 result.setAttribute("data-observed", result.getAttribute("data-order"));
3959 }, 0);
3960 "##,
3961 None,
3962 );
3963
3964 assert!(runtime.run_due_timers());
3965 let result = dom.get_element_by_id("result").unwrap();
3966 assert_eq!(
3967 result.borrow().value.get_attr("data-observed"),
3968 Some("timer-microtask")
3969 );
3970 }
3971
3972 #[test]
3973 fn promise_reactions_share_fifo_order_with_queued_microtasks() {
3974 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3975 runtime.run_script(
3976 r##"
3977 const result = document.querySelector("#result");
3978 result.setAttribute("data-order", "sync");
3979 queueMicrotask(function () {
3980 result.setAttribute("data-order", result.getAttribute("data-order") + "-first");
3981 });
3982 new Promise(function (resolve) {
3983 resolve("promise");
3984 }).then(function (value) {
3985 result.setAttribute("data-order", result.getAttribute("data-order") + "-" + value);
3986 });
3987 queueMicrotask(function () {
3988 result.setAttribute("data-order", result.getAttribute("data-order") + "-last");
3989 });
3990 "##,
3991 None,
3992 );
3993
3994 let result = dom.get_element_by_id("result").unwrap();
3995 assert_eq!(
3996 result.borrow().value.get_attr("data-order"),
3997 Some("sync-first-promise-last")
3998 );
3999 }
4000
4001 #[test]
4002 fn a_failed_microtask_does_not_block_later_jobs() {
4003 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4004 runtime.run_script(
4005 r##"
4006 queueMicrotask(function () {
4007 missingFunction();
4008 });
4009 queueMicrotask(function () {
4010 document.querySelector("#result").setAttribute("data-ran", "yes");
4011 });
4012 "##,
4013 None,
4014 );
4015
4016 let result = dom.get_element_by_id("result").unwrap();
4017 assert_eq!(result.borrow().value.get_attr("data-ran"), Some("yes"));
4018 }
4019
4020 #[test]
4021 fn promise_static_methods_complete_during_script_checkpoint() {
4022 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4023 runtime.run_script(
4024 r##"
4025 const result = document.querySelector("#result");
4026 Promise.all([Promise.resolve("first"), "second"])
4027 .then(function (values) {
4028 result.setAttribute("data-all", values[0] + "-" + values[1]);
4029 return Promise.reject("expected");
4030 })
4031 .catch(function (reason) {
4032 result.setAttribute("data-catch", reason);
4033 });
4034 "##,
4035 None,
4036 );
4037
4038 let result = dom.get_element_by_id("result").unwrap();
4039 assert_eq!(
4040 result.borrow().value.get_attr("data-all"),
4041 Some("first-second")
4042 );
4043 assert_eq!(
4044 result.borrow().value.get_attr("data-catch"),
4045 Some("expected")
4046 );
4047 }
4048
4049 #[test]
4050 fn arrow_callbacks_work_with_promises_and_lexical_this() {
4051 let (mut runtime, dom) =
4052 runtime_from_html(r#"<button id="target"></button><div id="other"></div>"#);
4053 runtime.run_script(
4054 r##"
4055 const target = document.querySelector("#target");
4056 Promise.resolve("promise").then(value => {
4057 target.setAttribute("data-promise", value);
4058 });
4059 target.addEventListener("click", function () {
4060 const update = () => this.setAttribute("data-this", "target");
4061 update.call(document.querySelector("#other"));
4062 });
4063 "##,
4064 None,
4065 );
4066
4067 let target = dom.get_element_by_id("target").unwrap();
4068 assert!(runtime.click(&target));
4069 assert_eq!(
4070 target.borrow().value.get_attr("data-promise"),
4071 Some("promise")
4072 );
4073 assert_eq!(target.borrow().value.get_attr("data-this"), Some("target"));
4074 let other = dom.get_element_by_id("other").unwrap();
4075 assert_eq!(other.borrow().value.get_attr("data-this"), None);
4076 }
4077
4078 #[test]
4079 fn queue_microtask_accepts_an_arrow_callback() {
4080 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4081 runtime.run_script(
4082 r##"
4083 const result = document.querySelector("#result");
4084 queueMicrotask(() => {
4085 result.setAttribute("data-microtask", "yes");
4086 });
4087 "##,
4088 None,
4089 );
4090
4091 let result = dom.get_element_by_id("result").unwrap();
4092 assert_eq!(
4093 result.borrow().value.get_attr("data-microtask"),
4094 Some("yes")
4095 );
4096 }
4097
4098 #[test]
4099 fn browser_global_aliases_share_window_properties() {
4100 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4101 runtime.run_script(
4102 r##"
4103 const result = window.document.querySelector("#result");
4104 result.setAttribute("data-same-self", window === self);
4105 result.setAttribute("data-same-global", window === globalThis);
4106 result.setAttribute("data-document", window.document === document);
4107 window.queueMicrotask(() => {
4108 result.setAttribute("data-microtask", "yes");
4109 });
4110 "##,
4111 None,
4112 );
4113
4114 let result = dom.get_element_by_id("result").unwrap();
4115 let result = result.borrow();
4116 assert_eq!(result.value.get_attr("data-same-self"), Some("true"));
4117 assert_eq!(result.value.get_attr("data-same-global"), Some("true"));
4118 assert_eq!(result.value.get_attr("data-document"), Some("true"));
4119 assert_eq!(result.value.get_attr("data-microtask"), Some("yes"));
4120 }
4121
4122 #[test]
4123 fn fetch_resolves_response_metadata_and_text_promise() {
4124 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4125 runtime.run_script(
4126 r##"
4127 fetch("data:text/plain,hello").then(response => {
4128 const result = document.querySelector("#result");
4129 result.setAttribute("data-ok", response.ok);
4130 result.setAttribute("data-status", response.status);
4131 result.setAttribute("data-status-text", response.statusText);
4132 result.setAttribute("data-url", response.url);
4133 result.setAttribute("data-redirected", response.redirected);
4134 result.setAttribute("data-body-used-before", response.bodyUsed);
4135 const body = response.text();
4136 result.setAttribute("data-body-used-after", response.bodyUsed);
4137 return body;
4138 }).then(text => {
4139 document.querySelector("#result").setAttribute("data-text", text);
4140 });
4141 "##,
4142 None,
4143 );
4144
4145 let requests = runtime.take_fetch_requests();
4146 assert_eq!(requests.len(), 1);
4147 assert_eq!(requests[0].url, "data:text/plain,hello");
4148 runtime.resolve_fetch(
4149 requests[0].id,
4150 JsFetchResponse {
4151 url: "data:text/plain,hello".to_string(),
4152 status: 200,
4153 status_text: "All Good".to_string(),
4154 redirected: true,
4155 body: b"hello".to_vec(),
4156 headers: Vec::new(),
4157 },
4158 );
4159
4160 let result = dom.get_element_by_id("result").unwrap();
4161 let result = result.borrow();
4162 assert_eq!(result.value.get_attr("data-ok"), Some("true"));
4163 assert_eq!(result.value.get_attr("data-status"), Some("200"));
4164 assert_eq!(result.value.get_attr("data-status-text"), Some("All Good"));
4165 assert_eq!(result.value.get_attr("data-redirected"), Some("true"));
4166 assert_eq!(
4167 result.value.get_attr("data-body-used-before"),
4168 Some("false")
4169 );
4170 assert_eq!(result.value.get_attr("data-body-used-after"), Some("true"));
4171 assert_eq!(
4172 result.value.get_attr("data-url"),
4173 Some("data:text/plain,hello")
4174 );
4175 assert_eq!(result.value.get_attr("data-text"), Some("hello"));
4176 }
4177
4178 #[test]
4179 fn fetch_array_buffer_preserves_binary_bytes() {
4180 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4181 runtime.run_script(
4182 r##"
4183 fetch("https://assets.scratch.mit.edu/project.sb3")
4184 .then(response => response.arrayBuffer())
4185 .then(buffer => {
4186 const bytes = new Uint8Array(buffer);
4187 document.querySelector("#result").setAttribute(
4188 "data-bytes",
4189 buffer.byteLength + ":" + bytes.length + ":" + bytes[0] + ":" + bytes[3]
4190 );
4191 });
4192 "##,
4193 None,
4194 );
4195
4196 let requests = runtime.take_fetch_requests();
4197 runtime.resolve_fetch(
4198 requests[0].id,
4199 JsFetchResponse {
4200 url: requests[0].url.clone(),
4201 status: 200,
4202 status_text: "OK".to_string(),
4203 redirected: false,
4204 body: vec![0, 127, 128, 255],
4205 headers: Vec::new(),
4206 },
4207 );
4208
4209 let result = dom.get_element_by_id("result").unwrap();
4210 assert_eq!(
4211 result.borrow().value.get_attr("data-bytes"),
4212 Some("4:4:0:255")
4213 );
4214 }
4215
4216 #[test]
4217 fn response_body_cannot_be_consumed_twice() {
4218 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4219 runtime.run_script(
4220 r##"
4221 fetch("data:text/plain,hello").then(response => {
4222 return response.text().then(() => response.text());
4223 }).catch(reason => {
4224 document.querySelector("#result").setAttribute("data-error", reason);
4225 });
4226 "##,
4227 None,
4228 );
4229
4230 let requests = runtime.take_fetch_requests();
4231 runtime.resolve_fetch(
4232 requests[0].id,
4233 JsFetchResponse {
4234 url: "data:text/plain,hello".to_string(),
4235 status: 200,
4236 status_text: "OK".to_string(),
4237 redirected: false,
4238 body: b"hello".to_vec(),
4239 headers: Vec::new(),
4240 },
4241 );
4242
4243 let result = dom.get_element_by_id("result").unwrap();
4244 assert_eq!(
4245 result.borrow().value.get_attr("data-error"),
4246 Some("Response body has already been consumed")
4247 );
4248 }
4249
4250 #[test]
4251 fn fetch_captures_method_headers_and_body() {
4252 let (mut runtime, _dom) = runtime_from_html("<div></div>");
4253 runtime.run_script(
4254 r#"
4255 const headers = {};
4256 headers["Content-Type"] = "application/json";
4257 headers["X-Test"] = "yes";
4258 fetch("https://example.test/messages", {
4259 method: "post",
4260 headers: headers,
4261 body: "{\"message\":\"hello\"}"
4262 });
4263 "#,
4264 None,
4265 );
4266
4267 let requests = runtime.take_fetch_requests();
4268 assert_eq!(requests.len(), 1);
4269 assert_eq!(requests[0].method, "POST");
4270 assert!(
4271 requests[0]
4272 .headers
4273 .contains(&("Content-Type".to_string(), "application/json".to_string()))
4274 );
4275 assert!(
4276 requests[0]
4277 .headers
4278 .contains(&("X-Test".to_string(), "yes".to_string()))
4279 );
4280 assert_eq!(requests[0].body, br#"{"message":"hello"}"#);
4281 }
4282
4283 #[test]
4284 fn xml_http_request_captures_request_and_dispatches_load() {
4285 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4286 runtime.run_script(
4287 r##"
4288 const request = new XMLHttpRequest();
4289 request.open("post", "https://example.test/messages");
4290 request.setRequestHeader("Content-Type", "text/plain");
4291 request.onload = function () {
4292 const result = document.querySelector("#result");
4293 result.setAttribute("data-state", this.readyState);
4294 result.setAttribute("data-status", this.status);
4295 result.setAttribute("data-text", this.responseText);
4296 result.setAttribute("data-headers", this.getAllResponseHeaders());
4297 };
4298 request.send("hello");
4299 "##,
4300 None,
4301 );
4302
4303 let requests = runtime.take_fetch_requests();
4304 assert_eq!(requests.len(), 1);
4305 assert_eq!(requests[0].url, "https://example.test/messages");
4306 assert_eq!(requests[0].method, "POST");
4307 assert_eq!(requests[0].body, b"hello");
4308 assert!(
4309 requests[0]
4310 .headers
4311 .contains(&("Content-Type".to_string(), "text/plain".to_string()))
4312 );
4313
4314 runtime.resolve_fetch(
4315 requests[0].id,
4316 JsFetchResponse {
4317 url: requests[0].url.clone(),
4318 status: 201,
4319 status_text: "Created".to_string(),
4320 redirected: false,
4321 body: b"saved".to_vec(),
4322 headers: vec![("X-Test".to_string(), "yes".to_string())],
4323 },
4324 );
4325
4326 let result = dom.get_element_by_id("result").unwrap();
4327 let result = result.borrow();
4328 assert_eq!(result.value.get_attr("data-state"), Some("4"));
4329 assert_eq!(result.value.get_attr("data-status"), Some("201"));
4330 assert_eq!(result.value.get_attr("data-text"), Some("saved"));
4331 assert_eq!(
4332 result.value.get_attr("data-headers"),
4333 Some("X-Test: yes\r\n")
4334 );
4335 }
4336
4337 #[test]
4338 fn headers_are_case_insensitive_and_mutable() {
4339 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4340 runtime.run_script(
4341 r##"
4342 const headers = new Headers({ Accept: "application/json" });
4343 headers.append("X-Test", "one");
4344 headers.append("x-test", "two");
4345 headers.set("X-Replace", "before");
4346 headers.set("x-replace", "after");
4347 headers.delete("ACCEPT");
4348
4349 const result = document.querySelector("#result");
4350 result.setAttribute("data-test", headers.get("X-TEST"));
4351 result.setAttribute("data-replace", headers.get("X-Replace"));
4352 result.setAttribute("data-has-accept", headers.has("accept"));
4353 result.setAttribute("data-missing", headers.get("missing") === null);
4354
4355 fetch("https://example.test/", { headers: headers });
4356 "##,
4357 None,
4358 );
4359
4360 let result = dom.get_element_by_id("result").unwrap();
4361 let result = result.borrow();
4362 assert_eq!(result.value.get_attr("data-test"), Some("one, two"));
4363 assert_eq!(result.value.get_attr("data-replace"), Some("after"));
4364 assert_eq!(result.value.get_attr("data-has-accept"), Some("false"));
4365 assert_eq!(result.value.get_attr("data-missing"), Some("true"));
4366
4367 let requests = runtime.take_fetch_requests();
4368 assert!(
4369 requests[0]
4370 .headers
4371 .contains(&("x-test".to_string(), "one, two".to_string()))
4372 );
4373 assert!(
4374 requests[0]
4375 .headers
4376 .contains(&("x-replace".to_string(), "after".to_string()))
4377 );
4378 }
4379
4380 #[test]
4381 fn response_exposes_read_only_headers() {
4382 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4383 runtime.run_script(
4384 r##"
4385 fetch("data:text/plain,hello").then(response => {
4386 const result = document.querySelector("#result");
4387 result.setAttribute("data-type", response.headers.get("Content-Type"));
4388 result.setAttribute("data-has", response.headers.has("X-Test"));
4389 });
4390 "##,
4391 None,
4392 );
4393
4394 let requests = runtime.take_fetch_requests();
4395 runtime.resolve_fetch(
4396 requests[0].id,
4397 JsFetchResponse {
4398 url: "data:text/plain,hello".to_string(),
4399 status: 200,
4400 status_text: "OK".to_string(),
4401 redirected: false,
4402 body: b"hello".to_vec(),
4403 headers: vec![
4404 ("content-type".to_string(), "text/plain".to_string()),
4405 ("X-Test".to_string(), "yes".to_string()),
4406 ],
4407 },
4408 );
4409
4410 let result = dom.get_element_by_id("result").unwrap();
4411 let result = result.borrow();
4412 assert_eq!(result.value.get_attr("data-type"), Some("text/plain"));
4413 assert_eq!(result.value.get_attr("data-has"), Some("true"));
4414 }
4415
4416 #[test]
4417 fn request_objects_can_be_copied_and_passed_to_fetch() {
4418 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4419 runtime.run_script(
4420 r##"
4421 const headers = new Headers({ Accept: "application/json" });
4422 const original = new Request("https://example.test/messages", {
4423 method: "post",
4424 headers: headers,
4425 body: "hello"
4426 });
4427 const copied = new Request(original);
4428 const result = document.querySelector("#result");
4429 result.setAttribute("data-url", copied.url);
4430 result.setAttribute("data-method", copied.method);
4431 result.setAttribute("data-accept", copied.headers.get("accept"));
4432 fetch(copied, { method: "put" });
4433 "##,
4434 None,
4435 );
4436
4437 let result = dom.get_element_by_id("result").unwrap();
4438 let result = result.borrow();
4439 assert_eq!(
4440 result.value.get_attr("data-url"),
4441 Some("https://example.test/messages")
4442 );
4443 assert_eq!(result.value.get_attr("data-method"), Some("POST"));
4444 assert_eq!(
4445 result.value.get_attr("data-accept"),
4446 Some("application/json")
4447 );
4448
4449 let requests = runtime.take_fetch_requests();
4450 assert_eq!(requests[0].url, "https://example.test/messages");
4451 assert_eq!(requests[0].method, "PUT");
4452 assert_eq!(requests[0].body, b"hello");
4453 assert!(
4454 requests[0]
4455 .headers
4456 .contains(&("accept".to_string(), "application/json".to_string()))
4457 );
4458 }
4459
4460 #[test]
4461 fn response_json_resolves_objects_and_arrays() {
4462 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4463 runtime.run_script(
4464 r##"
4465 fetch("data:application/json,pending").then(response => response.json()).then(value => {
4466 const result = document.querySelector("#result");
4467 result.setAttribute("data-name", value.name);
4468 result.setAttribute("data-second", value.items[1]);
4469 result.setAttribute("data-enabled", value.enabled);
4470 result.setAttribute("data-empty", value.empty === null);
4471 });
4472 "##,
4473 None,
4474 );
4475
4476 let requests = runtime.take_fetch_requests();
4477 runtime.resolve_fetch(
4478 requests[0].id,
4479 JsFetchResponse {
4480 url: "data:application/json,pending".to_string(),
4481 status: 200,
4482 status_text: "OK".to_string(),
4483 redirected: false,
4484 body: br#"{"name":"Orinium","items":[1,2],"enabled":true,"empty":null}"#.to_vec(),
4485 headers: Vec::new(),
4486 },
4487 );
4488
4489 let result = dom.get_element_by_id("result").unwrap();
4490 let result = result.borrow();
4491 assert_eq!(result.value.get_attr("data-name"), Some("Orinium"));
4492 assert_eq!(result.value.get_attr("data-second"), Some("2"));
4493 assert_eq!(result.value.get_attr("data-enabled"), Some("true"));
4494 assert_eq!(result.value.get_attr("data-empty"), Some("true"));
4495 }
4496
4497 #[test]
4498 fn response_json_rejects_invalid_json() {
4499 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4500 runtime.run_script(
4501 r##"
4502 fetch("data:application/json,invalid")
4503 .then(response => response.json())
4504 .catch(reason => {
4505 document.querySelector("#result").setAttribute("data-error", reason);
4506 });
4507 "##,
4508 None,
4509 );
4510
4511 let requests = runtime.take_fetch_requests();
4512 runtime.resolve_fetch(
4513 requests[0].id,
4514 JsFetchResponse {
4515 url: "data:application/json,invalid".to_string(),
4516 status: 200,
4517 status_text: "OK".to_string(),
4518 redirected: false,
4519 body: b"not json".to_vec(),
4520 headers: Vec::new(),
4521 },
4522 );
4523
4524 let result = dom.get_element_by_id("result").unwrap();
4525 assert!(
4526 result
4527 .borrow()
4528 .value
4529 .get_attr("data-error")
4530 .unwrap()
4531 .starts_with("Failed to parse JSON:")
4532 );
4533 }
4534
4535 #[test]
4536 fn fetch_rejection_runs_catch_reaction() {
4537 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
4538 runtime.run_script(
4539 r##"
4540 fetch("https://invalid.test/").catch(reason => {
4541 document.querySelector("#result").setAttribute("data-error", reason);
4542 });
4543 "##,
4544 None,
4545 );
4546
4547 let requests = runtime.take_fetch_requests();
4548 runtime.reject_fetch(requests[0].id, "network failed".to_string());
4549 let result = dom.get_element_by_id("result").unwrap();
4550 assert_eq!(
4551 result.borrow().value.get_attr("data-error"),
4552 Some("network failed")
4553 );
4554 }
4555
4556 #[test]
4557 fn intersection_observer_fires_on_observe() {
4558 let (mut runtime, dom) = runtime_from_html(
4559 r#"<div id="target" style="width: 100px; height: 50px"></div><div id="result"></div>"#,
4560 );
4561 runtime.run_script(
4562 r#"
4563 const target = document.getElementById("target");
4564 const result = document.getElementById("result");
4565 const observer = new IntersectionObserver(function (entries) {
4566 const entry = entries[0];
4567 result.setAttribute("data-target", entry.target === target);
4568 result.setAttribute("data-is-intersecting", entry.isIntersecting);
4569 result.setAttribute("data-ratio", entry.intersectionRatio);
4570 result.setAttribute("data-has-root-bounds", entry.rootBounds !== null);
4571 result.setAttribute("data-bcr-width", entry.boundingClientRect.width);
4572 });
4573 observer.observe(target);
4574 "#,
4575 None,
4576 );
4577
4578 let result = dom.get_element_by_id("result").unwrap();
4579 let result = result.borrow();
4580 assert_eq!(result.value.get_attr("data-target"), Some("true"));
4581 assert_eq!(result.value.get_attr("data-is-intersecting"), Some("true"));
4583 assert_eq!(result.value.get_attr("data-ratio"), Some("1"));
4584 assert_eq!(result.value.get_attr("data-has-root-bounds"), Some("true"));
4585 assert_eq!(result.value.get_attr("data-bcr-width"), Some("100"));
4586 }
4587
4588 #[test]
4589 fn custom_elements_define_and_connect() {
4590 let (mut runtime, dom) =
4591 runtime_from_html(r#"<html><body><div id="result"></div></body></html>"#);
4592 runtime.run_script(
4593 r#"
4594 class MyElement extends HTMLElement {
4595 connectedCallback() {
4596 document.getElementById("result").setAttribute("data-connected", "yes");
4597 }
4598 disconnectedCallback() {
4599 document.getElementById("result").setAttribute("data-disconnected", "yes");
4600 }
4601 }
4602 customElements.define("my-element", MyElement);
4603 document.getElementById("result").setAttribute(
4604 "data-proto",
4605 typeof MyElement.prototype.connectedCallback
4606 );
4607 const el = document.createElement("my-element");
4608 document.body.appendChild(el);
4609 document.body.removeChild(el);
4610 "#,
4611 None,
4612 );
4613
4614 let result = dom.get_element_by_id("result").unwrap();
4615 let result = result.borrow();
4616 assert_eq!(result.value.get_attr("data-proto"), Some("function"));
4618 assert_eq!(result.value.get_attr("data-connected"), Some("yes"));
4620 assert_eq!(result.value.get_attr("data-disconnected"), Some("yes"));
4621 }
4622
4623 #[test]
4624 fn custom_elements_define_getters_work() {
4625 let (mut runtime, _dom) = runtime_from_html(r#"<html><body></body></html>"#);
4626 runtime.run_script(
4627 r#"
4628 class MyEl extends HTMLElement {}
4629 customElements.define("my-el", MyEl);
4630 if (customElements.get("my-el") === undefined) throw new Error("get failed");
4631 if (customElements.get("no-such") !== undefined) throw new Error("get should be undefined");
4632 "#, None);
4633 }
4634
4635 #[test]
4636 fn custom_elements_attribute_changed_and_when_defined() {
4637 let (mut runtime, dom) =
4638 runtime_from_html(r#"<html><body><div id="result"></div></body></html>"#);
4639 runtime.run_script(
4640 r#"
4641 globalThis.__attrLog = [];
4642 class AttrEl extends HTMLElement {
4643 attributeChangedCallback(name, oldVal, newVal) {
4644 globalThis.__attrLog.push(name + ":" + (oldVal === null ? "null" : oldVal) + ":" + (newVal === null ? "null" : newVal));
4645 }
4646 }
4647 AttrEl.observedAttributes = ["data-val"];
4648 customElements.define("attr-el", AttrEl);
4649 const el = document.createElement("attr-el");
4650 document.body.appendChild(el);
4651 el.setAttribute("data-val", "first");
4652 el.setAttribute("data-val", "second");
4653 el.removeAttribute("data-val");
4654 // whenDefined resolves immediately for an already-defined name.
4655 let wdResolved = false;
4656 customElements.whenDefined("attr-el").then(function () {
4657 wdResolved = true;
4658 });
4659 document.getElementById("result").setAttribute(
4660 "data-wd", wdResolved
4661 );
4662 "#, None);
4663
4664 runtime.run_script(
4666 r#"document.getElementById("result").setAttribute(
4667 "data-log", globalThis.__attrLog.join("|")
4668 );"#,
4669 None,
4670 );
4671
4672 let result = dom.get_element_by_id("result").unwrap();
4673 let result = result.borrow();
4674 assert_eq!(result.value.get_attr("data-wd"), Some("true"));
4677 }
4678
4679 #[test]
4680 fn shadow_dom_attach_and_query() {
4681 let (mut runtime, dom) =
4682 runtime_from_html(r#"<html><body><div id="host"></div></body></html>"#);
4683 runtime.run_script(
4685 r##"
4686 var host = document.getElementById("host");
4687 host.setAttribute("data-step1", "ready");
4688 "##,
4689 None,
4690 );
4691 let result = dom.get_element_by_id("host").unwrap();
4692 assert_eq!(result.borrow().value.get_attr("data-step1"), Some("ready"));
4693
4694 runtime.run_script(
4696 r##"
4697 var host = document.getElementById("host");
4698 host.attachShadow({ mode: "open" });
4699 host.setAttribute("data-step2", "shadow-attached");
4700 "##,
4701 None,
4702 );
4703 assert_eq!(
4704 result.borrow().value.get_attr("data-step2"),
4705 Some("shadow-attached")
4706 );
4707
4708 runtime.run_script(
4710 r##"
4711 var host = document.getElementById("host");
4712 try {
4713 var sr = host.shadowRoot;
4714 host.setAttribute("data-sr", sr !== null ? "true" : "false");
4715 var span = document.createElement("span");
4716 span.id = "inner";
4717 span.textContent = "shadow text";
4718 sr.appendChild(span);
4719 var found = sr.querySelector("#inner");
4720 host.setAttribute("data-found", found !== null ? found.textContent : "NOT_FOUND");
4721 var notFound = host.querySelector("#inner");
4722 host.setAttribute("data-boundary", notFound === null ? "true" : "false");
4723 host.setAttribute("data-text", host.textContent.trim() === "" ? "true" : "false");
4724 } catch(e) {
4725 host.setAttribute("data-error", e.toString());
4726 }
4727 "##,
4728 None,
4729 );
4730 let result = dom.get_element_by_id("host").unwrap();
4731 let result = result.borrow();
4732 assert_eq!(result.value.get_attr("data-sr"), Some("true"));
4733 assert_eq!(result.value.get_attr("data-found"), Some("shadow text"));
4734 assert_eq!(result.value.get_attr("data-boundary"), Some("true"));
4735 assert_eq!(result.value.get_attr("data-text"), Some("true"));
4736 }
4737
4738 #[test]
4739 fn shadow_dom_closed_root() {
4740 let (mut runtime, dom) =
4741 runtime_from_html(r#"<html><body><div id="host"></div></body></html>"#);
4742 runtime.run_script(
4743 r##"
4744 var host = document.getElementById("host");
4745 host.attachShadow({ mode: "closed" });
4746 host.setAttribute("data-closed", host.shadowRoot === null ? "true" : "false");
4747 "##,
4748 None,
4749 );
4750 let result = dom.get_element_by_id("host").unwrap();
4751 let result = result.borrow();
4752 assert_eq!(result.value.get_attr("data-closed"), Some("true"));
4753 }
4754
4755 #[test]
4756 fn document_write_inserts_parsed_html_into_body() {
4757 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
4758 runtime.run_script(r#"document.write("<p>Hello</p>");"#, None);
4759 let p = dom.query_selector("body p").unwrap();
4760 assert_eq!(DomTree::inner_text(&p), "Hello");
4761 assert!(runtime.needs_redraw());
4762 }
4763
4764 #[test]
4765 fn document_writeln_appends_content_with_newline() {
4766 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
4767 runtime.run_script(r#"document.writeln("<span>A</span>");"#, None);
4768 let span = dom.query_selector("body span").unwrap();
4769 assert_eq!(DomTree::inner_text(&span), "A");
4770 }
4771
4772 #[test]
4773 fn dom_exception_has_name_message_and_code() {
4774 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4775 runtime.run_script(
4776 r#"
4777 try {
4778 throw new DOMException("test error", "SyntaxError");
4779 } catch (e) {
4780 document.getElementById("r").setAttribute("data-name", e.name);
4781 document.getElementById("r").setAttribute("data-msg", e.message);
4782 document.getElementById("r").setAttribute("data-code", e.code);
4783 }
4784 "#,
4785 None,
4786 );
4787 let r = dom.get_element_by_id("r").unwrap();
4788 let r = r.borrow();
4789 assert_eq!(r.value.get_attr("data-name"), Some("SyntaxError"));
4790 assert_eq!(r.value.get_attr("data-msg"), Some("test error"));
4791 assert_eq!(r.value.get_attr("data-code"), Some("12"));
4792 }
4793
4794 #[test]
4795 fn dom_exception_static_constants_are_exposed() {
4796 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4797 runtime.run_script(
4798 r#"
4799 document.getElementById("r").setAttribute(
4800 "data-codes",
4801 DOMException.SYNTAX_ERR + ":" +
4802 DOMException.HIERARCHY_REQUEST_ERR + ":" +
4803 DOMException.NOT_FOUND_ERR + ":" +
4804 DOMException.INVALID_STATE_ERR
4805 );
4806 "#,
4807 None,
4808 );
4809 let r = dom.get_element_by_id("r").unwrap();
4810 assert_eq!(r.borrow().value.get_attr("data-codes"), Some("12:3:8:11"));
4811 }
4812
4813 #[test]
4814 fn dom_exception_to_string_formats_name_and_message() {
4815 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4816 runtime.run_script(
4817 r#"
4818 var e = new DOMException("oops", "NotFoundError");
4819 document.getElementById("r").setAttribute("data-str", e.toString());
4820 "#,
4821 None,
4822 );
4823 let r = dom.get_element_by_id("r").unwrap();
4824 assert_eq!(
4825 r.borrow().value.get_attr("data-str"),
4826 Some("NotFoundError: oops")
4827 );
4828 }
4829
4830 #[test]
4831 fn create_element_throws_invalid_character_error_for_empty_name() {
4832 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4833 runtime.run_script(
4834 r#"
4835 try {
4836 document.createElement("");
4837 document.getElementById("r").setAttribute("data-error", "no-throw");
4838 } catch (e) {
4839 document.getElementById("r").setAttribute("data-error", e.name);
4840 }
4841 "#,
4842 None,
4843 );
4844 let r = dom.get_element_by_id("r").unwrap();
4845 assert_eq!(
4846 r.borrow().value.get_attr("data-error"),
4847 Some("InvalidCharacterError")
4848 );
4849 }
4850
4851 #[test]
4852 fn create_element_throws_invalid_character_error_for_invalid_name() {
4853 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4854 runtime.run_script(
4855 r#"
4856 try {
4857 document.createElement("123bad");
4858 document.getElementById("r").setAttribute("data-error", "no-throw");
4859 } catch (e) {
4860 document.getElementById("r").setAttribute("data-error", e.name);
4861 }
4862 "#,
4863 None,
4864 );
4865 let r = dom.get_element_by_id("r").unwrap();
4866 assert_eq!(
4867 r.borrow().value.get_attr("data-error"),
4868 Some("InvalidCharacterError")
4869 );
4870 }
4871
4872 #[test]
4873 fn create_element_valid_name_works() {
4874 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
4875 runtime.run_script(
4876 r#"
4877 var el = document.createElement("div");
4878 el.id = "created";
4879 document.body.appendChild(el);
4880 "#,
4881 None,
4882 );
4883 assert!(dom.get_element_by_id("created").is_some());
4884 }
4885
4886 #[test]
4887 fn create_element_ns_throws_invalid_character_error_for_invalid_name() {
4888 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4889 runtime.run_script(
4890 r#"
4891 try {
4892 document.createElementNS("http://www.w3.org/2000/svg", "123bad");
4893 document.getElementById("r").setAttribute("data-error", "no-throw");
4894 } catch (e) {
4895 document.getElementById("r").setAttribute("data-error", e.name);
4896 }
4897 "#,
4898 None,
4899 );
4900 let r = dom.get_element_by_id("r").unwrap();
4901 assert_eq!(
4902 r.borrow().value.get_attr("data-error"),
4903 Some("InvalidCharacterError")
4904 );
4905 }
4906
4907 #[test]
4908 fn node_constants_are_exposed_on_global() {
4909 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4910 runtime.run_script(
4911 r#"
4912 document.getElementById("r").setAttribute("data-elem", Node.ELEMENT_NODE);
4913 document.getElementById("r").setAttribute("data-text", Node.TEXT_NODE);
4914 document.getElementById("r").setAttribute("data-doc", Node.DOCUMENT_NODE);
4915 document.getElementById("r").setAttribute("data-frag", Node.DOCUMENT_FRAGMENT_NODE);
4916 "#,
4917 None,
4918 );
4919 let r = dom.get_element_by_id("r").unwrap();
4920 let r = r.borrow();
4921 assert_eq!(r.value.get_attr("data-elem"), Some("1"));
4922 assert_eq!(r.value.get_attr("data-text"), Some("3"));
4923 assert_eq!(r.value.get_attr("data-doc"), Some("9"));
4924 assert_eq!(r.value.get_attr("data-frag"), Some("11"));
4925 }
4926
4927 #[test]
4928 fn node_constants_are_exposed_on_instance_nodes() {
4929 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4930 runtime.run_script(
4931 r#"
4932 document.getElementById("r").setAttribute("data-doc-frag", document.DOCUMENT_FRAGMENT_NODE);
4933 document.getElementById("r").setAttribute("data-cmt", document.body.COMMENT_NODE);
4934 document.getElementById("r").setAttribute("data-txt", document.createTextNode("").ELEMENT_NODE);
4935 document.getElementById("r").setAttribute("data-frag", document.createElement("div").DOCUMENT_FRAGMENT_NODE);
4936 "#, None);
4937 let r = dom.get_element_by_id("r").unwrap();
4938 let r = r.borrow();
4939 assert_eq!(r.value.get_attr("data-doc-frag"), Some("11"));
4940 assert_eq!(r.value.get_attr("data-cmt"), Some("8"));
4941 assert_eq!(r.value.get_attr("data-txt"), Some("1"));
4942 assert_eq!(r.value.get_attr("data-frag"), Some("11"));
4943 }
4944
4945 #[test]
4946 fn document_first_child_is_doctype_node() {
4947 let (mut runtime, dom) = runtime_from_html(
4948 r#"<!DOCTYPE html><html><body><div id="r"><span>x</span></div></body></html>"#,
4949 );
4950 runtime.run_script(
4951 r#"
4952 document.getElementById("r").setAttribute("data-doc-type", document.nodeType);
4953 document.getElementById("r").setAttribute("data-doctype-node", document.firstChild.nodeType);
4954 document.getElementById("r").setAttribute("data-doctype-name", document.firstChild.nodeName);
4955 var span = document.getElementById("r").firstChild;
4956 document.getElementById("r").setAttribute("data-text-type", span.firstChild.nodeType);
4957 "#, None);
4958 let r = dom.get_element_by_id("r").unwrap();
4959 let r = r.borrow();
4960 assert_eq!(r.value.get_attr("data-doc-type"), Some("9"));
4961 assert_eq!(r.value.get_attr("data-doctype-node"), Some("10"));
4962 assert_eq!(r.value.get_attr("data-doctype-name"), Some("html"));
4963 assert_eq!(r.value.get_attr("data-text-type"), Some("3"));
4964 }
4965
4966 #[test]
4967 fn create_element_ns_preserves_qualified_name() {
4968 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
4969 runtime.run_script(
4970 r#"
4971 var el = document.createElementNS("http://ns.example.com/", "prefix:localname");
4972 document.getElementById("r").setAttribute("data-tag", el.tagName);
4973 document.getElementById("r").setAttribute("data-local", el.localName);
4974 document.getElementById("r").setAttribute("data-prefix", el.prefix);
4975 document.getElementById("r").setAttribute("data-ns", el.namespaceURI);
4976 "#,
4977 None,
4978 );
4979 let r = dom.get_element_by_id("r").unwrap();
4980 let r = r.borrow();
4981 assert_eq!(r.value.get_attr("data-tag"), Some("prefix:localname"));
4982 assert_eq!(r.value.get_attr("data-local"), Some("localname"));
4983 assert_eq!(r.value.get_attr("data-prefix"), Some("prefix"));
4984 assert_eq!(r.value.get_attr("data-ns"), Some("http://ns.example.com/"));
4985 }
4986
4987 #[test]
4988 fn document_close_returns_undefined() {
4989 let (mut runtime, _) = runtime_from_html(r#"<html><body></body></html>"#);
4990 runtime.run_script(
4991 r#"
4992 var result = document.close();
4993 document.getElementById("r").setAttribute("data-close", typeof result);
4994 "#,
4995 None,
4996 );
4997 let (mut runtime2, dom2) = runtime_from_html(r#"<div id="r"></div>"#);
5000 runtime2.run_script(
5001 r#"
5002 document.close();
5003 document.getElementById("r").setAttribute("data-close", "ok");
5004 "#,
5005 None,
5006 );
5007 let r = dom2.get_element_by_id("r").unwrap();
5008 assert_eq!(r.borrow().value.get_attr("data-close"), Some("ok"));
5009 }
5010
5011 #[test]
5012 fn dom_exception_instanceof_error() {
5013 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5014 runtime.run_script(
5015 r#"
5016 var e = new DOMException("test", "SyntaxError");
5017 document.getElementById("r").setAttribute("data-is-error", e instanceof Error);
5018 document.getElementById("r").setAttribute("data-is-domexc", e instanceof DOMException);
5019 "#,
5020 None,
5021 );
5022 let r = dom.get_element_by_id("r").unwrap();
5023 let r = r.borrow();
5024 assert_eq!(r.value.get_attr("data-is-error"), Some("true"));
5025 assert_eq!(r.value.get_attr("data-is-domexc"), Some("true"));
5026 }
5027
5028 #[test]
5029 fn create_comment_returns_comment_node() {
5030 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5031 runtime.run_script(
5032 r#"
5033 var c = document.createComment("hello");
5034 document.getElementById("r").setAttribute("data-type", c.nodeType);
5035 document.getElementById("r").setAttribute("data-name", c.nodeName);
5036 document.getElementById("r").setAttribute("data-data", c.data);
5037 "#,
5038 None,
5039 );
5040 let r = dom.get_element_by_id("r").unwrap();
5041 let r = r.borrow();
5042 assert_eq!(r.value.get_attr("data-type"), Some("8"));
5043 assert_eq!(r.value.get_attr("data-name"), Some("#comment"));
5044 assert_eq!(r.value.get_attr("data-data"), Some("hello"));
5045 }
5046
5047 #[test]
5048 fn create_processing_instruction_returns_pi_node() {
5049 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5050 runtime.run_script(
5051 r#"
5052 var pi = document.createProcessingInstruction("xml-stylesheet", "href=\"style.css\"");
5053 document.getElementById("r").setAttribute("data-type", pi.nodeType);
5054 document.getElementById("r").setAttribute("data-data", pi.data);
5055 "#,
5056 None,
5057 );
5058 let r = dom.get_element_by_id("r").unwrap();
5059 let r = r.borrow();
5060 assert_eq!(r.value.get_attr("data-type"), Some("7"));
5061 assert_eq!(r.value.get_attr("data-data"), Some("href=\"style.css\""));
5062 }
5063
5064 #[test]
5065 fn create_processing_instruction_empty_target_throws() {
5066 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5067 runtime.run_script(
5068 r#"
5069 try {
5070 document.createProcessingInstruction("", "data");
5071 document.getElementById("r").setAttribute("data-error", "no-throw");
5072 } catch (e) {
5073 document.getElementById("r").setAttribute("data-error", e.name);
5074 }
5075 "#,
5076 None,
5077 );
5078 let r = dom.get_element_by_id("r").unwrap();
5079 assert_eq!(r.borrow().value.get_attr("data-error"), Some("SyntaxError"));
5080 }
5081
5082 #[test]
5083 fn innerhtml_setter_uses_parser_and_replaces_children() {
5084 let (mut runtime, dom) = runtime_from_html(r#"<div id="target"></div><div id="r"></div>"#);
5085 runtime.run_script(r#"
5086 var t = document.getElementById("target");
5087 t.innerHTML = "<p>A</p><span>B</span>";
5088 document.getElementById("r").setAttribute("data-count", t.childNodes.length);
5089 document.getElementById("r").setattr || document.getElementById("r").setAttribute("data-tags",
5090 t.children[0].tagName + ":" + t.children[1].tagName
5091 );
5092 "#, None);
5093 let r = dom.get_element_by_id("r").unwrap();
5094 let r = r.borrow();
5095 assert_eq!(r.value.get_attr("data-count"), Some("2"));
5096 }
5097
5098 #[test]
5103 fn all_detector_listed_apis_are_present() {
5104 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5105 runtime.run_script(
5106 r#"
5107 var missing = [];
5108 var globals = ["AbortController", "AbortSignal", "Blob", "CustomElementRegistry",
5109 "FileReader", "Response", "ShadowRoot"];
5110 for (var i = 0; i < globals.length; i++) {
5111 if (typeof window[globals[i]] === "undefined") missing.push("window." + globals[i]);
5112 }
5113 var docMethods = ["elementFromPoint", "elementsFromPoint", "execCommand", "getSelection",
5114 "pictureInPictureElement"];
5115 for (var j = 0; j < docMethods.length; j++) {
5116 if (typeof document[docMethods[j]] === "undefined") missing.push("document." + docMethods[j]);
5117 }
5118 var elementMethods = ["getAnimations", "requestFullscreen", "scrollIntoView", "scrollTo"];
5119 for (var k = 0; k < elementMethods.length; k++) {
5120 if (typeof Element.prototype[elementMethods[k]] === "undefined")
5121 missing.push("Element.prototype." + elementMethods[k]);
5122 }
5123 document.getElementById("r").setAttribute("data-missing", missing.join(","));
5124 "#, None);
5125 let r = dom.get_element_by_id("r").unwrap();
5126 assert_eq!(r.borrow().value.get_attr("data-missing"), Some(""));
5127 }
5128
5129 #[test]
5130 fn element_prototype_methods_are_callable_noops() {
5131 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5132 runtime.run_script(
5133 r#"
5134 var el = document.getElementById("r");
5135 var out = [];
5136 out.push("anims:" + (el.getAnimations() instanceof Array));
5137 out.push("scrollIntoView:" + el.scrollIntoView());
5138 out.push("scrollTo:" + el.scrollTo(0, 10));
5139 out.push("scrollToOptions:" + el.scrollTo({left: 1, top: 2}));
5140 el.requestFullscreen().then(function () {
5141 el.setAttribute("data-fs", "resolved:" + (document.fullscreenElement === el));
5142 });
5143 el.setAttribute("data-out", out.join(";"));
5144 "#,
5145 None,
5146 );
5147 let r = dom.get_element_by_id("r").unwrap();
5148 let attrs = r.borrow();
5149 assert_eq!(
5150 attrs.value.get_attr("data-out"),
5151 Some(
5152 "anims:true;scrollIntoView:undefined;scrollTo:undefined;scrollToOptions:undefined"
5153 )
5154 );
5155 assert_eq!(attrs.value.get_attr("data-fs"), Some("resolved:true"));
5156 }
5157
5158 #[test]
5159 fn document_selection_and_point_apis_behave() {
5160 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5161 runtime.run_script(
5162 r#"
5163 var out = [];
5164 var sel = document.getSelection();
5165 out.push("selSingleton:" + (document.getSelection() === sel));
5166 out.push("rangeCount:" + sel.rangeCount);
5167 out.push("type:" + sel.type);
5168 out.push("exec:" + document.execCommand("bold"));
5169 out.push("execEmpty:" + document.execCommand(""));
5170 out.push("pipType:" + typeof document.pictureInPictureElement);
5171 out.push("pipNull:" + (document.pictureInPictureElement === null));
5172 out.push("point:" + (document.elementFromPoint(5, 5) === null));
5173 out.push("stack:" + document.elementsFromPoint(5, 5).length);
5174 document.getElementById("r").setAttribute("data-out", out.join(";"));
5175 "#,
5176 None,
5177 );
5178 let r = dom.get_element_by_id("r").unwrap();
5179 assert_eq!(
5180 r.borrow().value.get_attr("data-out"),
5181 Some(
5182 "selSingleton:true;rangeCount:0;type:None;exec:true;execEmpty:false;pipType:object;pipNull:true;point:true;stack:0"
5183 )
5184 );
5185 }
5186
5187 #[test]
5188 fn document_scripts_fonts_and_adjacent_insertion_work() {
5189 let (mut runtime, dom) = runtime_from_html(
5190 r#"<html><head><script id="s1"></script></head><body><div id="r"></div><div id="anchor"></div></body></html>"#,
5191 );
5192 runtime.run_script(
5193 r#"
5194 var out = [];
5195 out.push("scripts:" + document.scripts.length);
5196 out.push("scriptId:" + document.scripts[0].id);
5197 out.push("current:" + (document.currentScript === null));
5198 out.push("fonts:" + document.fonts.status + ":" + document.fonts.check("12px sans-serif"));
5199 out.push("fontsReady:" + typeof document.fonts.ready.then);
5200 document.adoptedStyleSheets = [];
5201 out.push("adopted:" + document.adoptedStyleSheets.length);
5202 var inserted = document.getElementById("anchor").insertAdjacentElement("beforebegin", document.createElement("span"));
5203 out.push("inserted:" + (inserted && inserted.tagName));
5204 document.getElementById("r").setAttribute("data-out", out.join(";"));
5205 "#, None);
5206 let r = dom.get_element_by_id("r").unwrap();
5207 assert_eq!(
5208 r.borrow().value.get_attr("data-out"),
5209 Some(
5210 "scripts:1;scriptId:s1;current:true;fonts:loaded:true;fontsReady:function;adopted:0;inserted:SPAN"
5211 )
5212 );
5213 }
5214
5215 #[test]
5216 fn abort_controller_and_signal_follow_platform_semantics() {
5217 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5218 runtime.run_script(
5219 r#"
5220 var out = [];
5221 var controller = new AbortController();
5222 out.push("fresh:" + controller.signal.aborted);
5223 var fired = 0;
5224 controller.signal.addEventListener("abort", function () { fired++; });
5225 controller.abort();
5226 out.push("aborted:" + controller.signal.aborted);
5227 out.push("reasonName:" + controller.signal.reason.name);
5228 out.push("fired:" + fired);
5229 var custom = new AbortController();
5230 custom.abort("because");
5231 out.push("customReason:" + custom.signal.reason);
5232 out.push("reabort:" + (controller.abort(), controller.signal.reason.name));
5233 try { controller.signal.throwIfAborted(); } catch (e) { out.push("throw:" + e.name); }
5234 var staticAborted = AbortSignal.abort();
5235 out.push("static:" + staticAborted.aborted + ":" + staticAborted.reason.name);
5236 var any = AbortSignal.any([controller.signal, new AbortController().signal]);
5237 out.push("any:" + any.aborted);
5238 document.getElementById("r").setAttribute("data-out", out.join(";"));
5239 "#,
5240 None,
5241 );
5242 runtime.perform_microtask_checkpoint_public();
5243 let r = dom.get_element_by_id("r").unwrap();
5244 assert_eq!(
5245 r.borrow().value.get_attr("data-out"),
5246 Some(
5247 "fresh:false;aborted:true;reasonName:AbortError;fired:1;customReason:because;reabort:AbortError;throw:AbortError;static:true:AbortError;any:true"
5248 )
5249 );
5250 }
5251
5252 #[test]
5253 fn fetch_with_preaborted_signal_rejects_without_request() {
5254 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5255 runtime.run_script(
5256 r#"
5257 var controller = new AbortController();
5258 controller.abort();
5259 fetch("https://example.com/data", { signal: controller.signal })
5260 .then(function () {
5261 document.getElementById("r").setAttribute("data-result", "resolved");
5262 })
5263 .catch(function (error) {
5264 document.getElementById("r").setAttribute("data-result", "rejected:" + error.name);
5265 });
5266 "#,
5267 None,
5268 );
5269 runtime.perform_microtask_checkpoint_public();
5270 let requests = runtime.take_fetch_requests();
5271 assert!(
5272 requests.is_empty(),
5273 "no fetch should be queued for an aborted signal"
5274 );
5275 let r = dom.get_element_by_id("r").unwrap();
5276 assert_eq!(
5277 r.borrow().value.get_attr("data-result"),
5278 Some("rejected:AbortError")
5279 );
5280 }
5281
5282 #[test]
5283 fn fetch_registers_signal_and_abort_rejects_pending_request() {
5284 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5285 runtime.run_script(
5286 r#"
5287 var controller = new AbortController();
5288 fetch("https://example.com/slow", { signal: controller.signal })
5289 .then(function () {
5290 document.getElementById("r").setAttribute("data-result", "resolved");
5291 })
5292 .catch(function (error) {
5293 document.getElementById("r").setAttribute("data-result", "rejected:" + error.name);
5294 });
5295 controller.abort();
5296 "#,
5297 None,
5298 );
5299 runtime.perform_microtask_checkpoint_public();
5300 let requests = runtime.take_fetch_requests();
5301 assert!(
5302 requests.is_empty(),
5303 "aborted fetch must be removed from the queue"
5304 );
5305 let r = dom.get_element_by_id("r").unwrap();
5306 assert_eq!(
5307 r.borrow().value.get_attr("data-result"),
5308 Some("rejected:AbortError")
5309 );
5310 }
5311
5312 #[test]
5313 fn blob_and_file_reader_round_trip() {
5314 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5315 runtime.run_script(
5316 r#"
5317 var out = [];
5318 var blob = new Blob(["hello", " ", "world"], { type: "text/plain" });
5319 out.push("size:" + blob.size);
5320 out.push("type:" + blob.type);
5321 blob.text().then(function (text) { out.push("text:" + text); });
5322 blob.arrayBuffer().then(function (buffer) { out.push("bytes:" + buffer.byteLength); });
5323 var sliced = blob.slice(0, 5);
5324 sliced.text().then(function (text) { out.push("slice:" + text); });
5325 var reader = new FileReader();
5326 var loaded = null;
5327 reader.onload = function (event) { loaded = event.type; };
5328 reader.readAsText(blob);
5329 out.push("read:" + reader.result);
5330 out.push("loadEvent:" + loaded);
5331 document.getElementById("r").setAttribute("data-out", out.join(";"));
5332 window.__blob_out = out;
5333 "#,
5334 None,
5335 );
5336 runtime.perform_microtask_checkpoint_public();
5337 runtime.run_script(
5339 r#"
5340 document.getElementById("r").setAttribute("data-out", window.__blob_out.join(";"));
5341 "#,
5342 None,
5343 );
5344 let r = dom.get_element_by_id("r").unwrap();
5345 let observed = r
5346 .borrow()
5347 .value
5348 .get_attr("data-out")
5349 .unwrap_or_default()
5350 .to_string();
5351 assert!(observed.contains("size:11"), "got {observed}");
5352 assert!(observed.contains("type:text/plain"), "got {observed}");
5353 assert!(observed.contains("text:hello world"), "got {observed}");
5354 assert!(observed.contains("bytes:11"), "got {observed}");
5355 assert!(observed.contains("slice:hello"), "got {observed}");
5356 assert!(observed.contains("read:hello world"), "got {observed}");
5357 assert!(observed.contains("loadEvent:load"), "got {observed}");
5358 }
5359
5360 #[test]
5361 fn response_constructor_produces_a_working_response() {
5362 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5363 runtime.run_script(
5364 r#"
5365 var out = [];
5366 var response = new Response("body text", { status: 201, statusText: "Created" });
5367 out.push("status:" + response.status);
5368 out.push("statusText:" + response.statusText);
5369 out.push("ok:" + response.ok);
5370 response.text().then(function (text) { out.push("text:" + text); });
5371 var jsonResponse = new Response("{}", { headers: { "content-type": "application/json" } });
5372 jsonResponse.json().then(function (value) { out.push("json:" + (typeof value === "object")); });
5373 window.__response_out = out;
5374 "#, None);
5375 runtime.perform_microtask_checkpoint_public();
5376 runtime.run_script(
5377 r#"
5378 document.getElementById("r").setAttribute("data-out", window.__response_out.join(";"));
5379 "#,
5380 None,
5381 );
5382 let r = dom.get_element_by_id("r").unwrap();
5383 let observed = r
5384 .borrow()
5385 .value
5386 .get_attr("data-out")
5387 .unwrap_or_default()
5388 .to_string();
5389 assert!(observed.contains("status:201"), "got {observed}");
5390 assert!(observed.contains("statusText:Created"), "got {observed}");
5391 assert!(observed.contains("ok:true"), "got {observed}");
5392 assert!(observed.contains("text:body text"), "got {observed}");
5393 assert!(observed.contains("json:true"), "got {observed}");
5394 }
5395
5396 #[test]
5397 fn custom_element_registry_and_shadow_root_are_instanciable_checks() {
5398 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5399 runtime.run_script(
5400 r#"
5401 var out = [];
5402 out.push("ceType:" + typeof CustomElementRegistry);
5403 out.push("instance:" + (customElements instanceof CustomElementRegistry));
5404 out.push("define:" + typeof customElements.define);
5405 out.push("whenDefined:" + typeof customElements.whenDefined);
5406 out.push("srType:" + typeof ShadowRoot);
5407 out.push("hostHook:" + typeof ShadowRoot.prototype);
5408 var host = document.getElementById("r");
5409 var shadow = host.attachShadow({ mode: "open" });
5410 out.push("attach:" + (shadow instanceof ShadowRoot));
5411 try { new ShadowRoot(); out.push("ctor:no-throw"); }
5412 catch (e) { out.push("ctor:" + (e instanceof TypeError)); }
5413 document.getElementById("r").setAttribute("data-out", out.join(";"));
5414 "#,
5415 None,
5416 );
5417 let r = dom.get_element_by_id("r").unwrap();
5418 assert_eq!(
5419 r.borrow().value.get_attr("data-out"),
5420 Some(
5421 "ceType:function;instance:true;define:function;whenDefined:function;srType:function;hostHook:object;attach:true;ctor:true"
5422 )
5423 );
5424 }
5425
5426 #[test]
5427 fn promise_finally_race_any_and_all_settled_compose() {
5428 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5429 runtime.run_script(
5430 r#"
5431 window.__out = [];
5432 function log(s) { window.__out.push(s); }
5433 Promise.race([new Promise(function (_, rej) { rej("early"); })])
5434 .catch(function (e) { log("race:" + e); });
5435 Promise.allSettled([Promise.resolve(1), Promise.reject("bad")])
5436 .then(function (rs) {
5437 log("settled:" + rs[0].status + ":" + rs[1].status + ":" + rs[1].reason);
5438 });
5439 Promise.any([Promise.reject("a"), Promise.resolve("ok")])
5440 .then(function (v) { log("any:" + v); });
5441 Promise.any([Promise.reject("x"), Promise.reject("y")])
5442 .catch(function (e) {
5443 log("anyReject:" + e.name + ":" + e.errors.length + ":" + e.errors[0]);
5444 });
5445 Promise.resolve("base")
5446 .finally(function () { log("finally:ran"); })
5447 .then(function (v) { log("finally:" + v); });
5448 Promise.reject("original")
5449 .finally(function () { log("finallyReject:ran"); })
5450 .catch(function (e) { log("finallyReject:" + e); });
5451 "#,
5452 None,
5453 );
5454 runtime.perform_microtask_checkpoint_public();
5455 runtime.run_script(
5456 r#"
5457 document.getElementById("r").setAttribute("data-out", window.__out.join(";"));
5458 "#,
5459 None,
5460 );
5461 let r = dom.get_element_by_id("r").unwrap();
5462 assert_eq!(
5463 r.borrow().value.get_attr("data-out"),
5464 Some(
5465 "finally:ran;finallyReject:ran;race:early;settled:fulfilled:rejected:bad;any:ok;anyReject:AggregateError:2:x;finally:base;finallyReject:original"
5466 )
5467 );
5468 }
5469
5470 #[test]
5471 fn promise_rejections_and_host_errors_preserve_error_instances() {
5472 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5473 runtime.run_script(
5474 r#"
5475 window.__out = [];
5476 function log(s) { window.__out.push(s); }
5477 Promise.resolve(1)
5478 .then(function () { throw new TypeError("boom"); })
5479 .catch(function (e) {
5480 log("thrown:" + (e instanceof TypeError) + ":" + e.name + ":" + e.message);
5481 });
5482 Promise.reject(new RangeError("nope"))
5483 .catch(function (e) {
5484 log("rejected:" + (e instanceof RangeError) + ":" + e.message);
5485 });
5486 try { JSON.parse("{not json"); }
5487 catch (e) { log("hostSyntax:" + (e instanceof SyntaxError) + ":" + e.name); }
5488 try { new ShadowRoot(); }
5489 catch (e) { log("hostType:" + (e instanceof TypeError)); }
5490 "#,
5491 None,
5492 );
5493 runtime.perform_microtask_checkpoint_public();
5494 runtime.run_script(
5495 r#"
5496 document.getElementById("r").setAttribute("data-out", window.__out.join(";"));
5497 "#,
5498 None,
5499 );
5500 let r = dom.get_element_by_id("r").unwrap();
5501 assert_eq!(
5502 r.borrow().value.get_attr("data-out"),
5503 Some(
5504 "hostSyntax:true:SyntaxError;hostType:true;rejected:true:nope;thrown:true:TypeError:boom"
5505 )
5506 );
5507 }
5508
5509 #[test]
5510 fn try_run_script_distinguishes_parse_failures_from_clean_runs() {
5511 let (mut runtime, _dom) = runtime_from_html(r#""#);
5512 assert!(runtime.try_run_script("var x = 1; 1 + 1;", None).is_ok());
5513 let err = runtime
5514 .try_run_script("var x = ;", None)
5515 .expect_err("syntax error must surface");
5516 assert!(!err.is_empty(), "parse error still needs a message");
5517 runtime
5519 .try_run_script("throw new TypeError('boom');", None)
5520 .expect_err("thrown value must surface");
5521 }
5522
5523 #[test]
5524 fn drain_microtasks_settles_job_queued_callbacks() {
5525 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5526 runtime.run_script(
5527 r#"queueMicrotask(function () {
5528 document.getElementById("r").setAttribute("data-out", "drained");
5529 });"#,
5530 None,
5531 );
5532 runtime.drain_microtasks();
5533 let r = dom.get_element_by_id("r").unwrap();
5534 assert_eq!(
5535 r.borrow().value.get_attr("data-out"),
5536 Some("drained"),
5537 "queueMicrotask work must settle without a timer"
5538 );
5539 }
5540
5541 #[test]
5542 fn element_prototype_exposes_selector_and_adjacency_helpers() {
5543 let (mut runtime, dom) = runtime_from_html(
5544 r#"<div id="host" class="outer"><span id="target" class="inner"></span></div>"#,
5545 );
5546 runtime.run_script(
5547 r#"
5548 var out = [];
5549 out.push("matches:" + Element.prototype.matches.call(document.getElementById("target"), ".inner"));
5550 out.push("closest:" + Element.prototype.closest.call(document.getElementById("target"), ".outer").id);
5551 out.push("insert:" + typeof Element.prototype.insertAdjacentElement);
5552 var added = document.getElementById("target").insertAdjacentElement("beforebegin", document.createElement("i"));
5553 out.push("inserted:" + (added && added.tagName));
5554 document.getElementById("host").setAttribute("data-out", out.join(";"));
5555 "#, None);
5556 let host = dom.get_element_by_id("host").unwrap();
5557 assert_eq!(
5558 host.borrow().value.get_attr("data-out"),
5559 Some("matches:true;closest:host;insert:function;inserted:I")
5560 );
5561 }
5562
5563 #[test]
5564 fn array_buffer_applies_to_index_length_semantics() {
5565 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5566 runtime.run_script(
5567 r#"
5568 var out = [];
5569 out.push("empty:" + new ArrayBuffer().byteLength);
5570 out.push("undefined:" + new ArrayBuffer(undefined).byteLength);
5571 out.push("nan:" + new ArrayBuffer(NaN).byteLength);
5572 out.push("trunc:" + new ArrayBuffer(2.9).byteLength);
5573 try {
5574 new ArrayBuffer(-1);
5575 out.push("negative:no-throw");
5576 } catch (e) {
5577 out.push("negative:" + e.name);
5578 }
5579 document.getElementById("r").setAttribute("data-out", out.join(";"));
5580 "#,
5581 None,
5582 );
5583 let r = dom.get_element_by_id("r").unwrap();
5584 assert_eq!(
5585 r.borrow().value.get_attr("data-out"),
5586 Some("empty:0;undefined:0;nan:0;trunc:2;negative:RangeError")
5587 );
5588 }
5589
5590 #[test]
5591 fn host_constructed_encoders_and_views_pass_instanceof() {
5592 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
5593 runtime.run_script(
5594 r#"
5595 var results = [];
5596 var enc = new TextEncoder();
5597 var dec = new TextDecoder();
5598 var buf = new ArrayBuffer(8);
5599 var view = new Uint8Array(buf);
5600 var dv = new DataView(buf);
5601 results.push("enc:" + (enc instanceof TextEncoder));
5602 results.push("dec:" + (dec instanceof TextDecoder));
5603 results.push("decNotEnc:" + (dec instanceof TextEncoder));
5604 results.push("buf:" + (buf instanceof ArrayBuffer));
5605 results.push("view:" + (view instanceof Uint8Array));
5606 results.push("dv:" + (dv instanceof DataView));
5607 results.push("plain:" + ({} instanceof TextEncoder));
5608 document.getElementById("r").setAttribute("data-out", results.join(";"));
5609 "#,
5610 None,
5611 );
5612 let r = dom.get_element_by_id("r").unwrap();
5613 assert_eq!(
5614 r.borrow().value.get_attr("data-out"),
5615 Some("enc:true;dec:true;decNotEnc:false;buf:true;view:true;dv:true;plain:false")
5616 );
5617 }
5618}