1use crate::engine::html::{DomTree, HtmlNodeType};
11use crate::engine::js::web_apis::dom::document::IframeDocument;
12use crate::engine::layouter::dom_snapshot::DomSnapshot;
13use crate::engine::tree::NodeRef;
14use pixi_byte::value::JSArray;
15use pixi_byte::value::jsobject::JSObject;
16use pixi_byte::{JSError, JSValue};
17use std::cell::{Cell, RefCell};
18use std::collections::{HashMap, HashSet};
19use std::rc::Rc;
20use std::time::{Duration, Instant};
21
22pub mod devtools;
23pub mod processor;
24pub use devtools::JsDevToolsRequest;
25pub use processor::{JsProcessor, JsTask, JsTaskResult};
26
27mod common;
28pub(crate) mod runtime;
29pub(crate) mod web_apis;
30
31pub(crate) use common::{
33 host_read_only_property, is_callable, node_dom_id, with_host, with_host_mut,
34};
35pub(crate) use web_apis::dom::document::expose_node;
36pub(crate) use web_apis::dom::events::{event_flag, make_event};
37pub(crate) use web_apis::network::{make_fetch_response, resolve_xml_http_request};
38
39pub(crate) struct JsTimer {
44 id: u64,
45 callback: JSValue,
46 arguments: Vec<JSValue>,
47 deadline: Instant,
48 interval: Option<Duration>,
49}
50
51pub(crate) struct JsFetchCapability {
52 resolve: JSValue,
53 reject: JSValue,
54}
55
56#[derive(Debug)]
58pub struct JsFetchRequest {
59 pub(crate) id: u64,
60 pub(crate) url: String,
61 pub(crate) method: String,
62 pub(crate) headers: Vec<(String, String)>,
63 pub(crate) body: Vec<u8>,
64}
65
66#[derive(Debug)]
68pub struct JsIframeFetchRequest {
69 pub dom_id: u64,
71 pub url: String,
73}
74
75#[derive(Debug)]
78pub struct IframeContentSnapshot {
79 pub iframe_dom_id: u64,
81 pub content: DomSnapshot,
83}
84
85#[derive(Debug)]
87pub(crate) struct JsDynamicScriptRequest {
88 pub(crate) node_id: u64,
89 pub(crate) source: JsDynamicScriptSource,
90}
91
92#[derive(Debug)]
93pub(crate) enum JsDynamicScriptSource {
94 Inline(String),
95 External(String),
96}
97
98#[derive(Debug)]
99pub(crate) struct JsDynamicStyleRequest {
100 pub(crate) node_id: u64,
101 pub(crate) url: String,
102}
103
104#[derive(Debug)]
106pub(crate) struct JsDynamicImageRequest {
108 pub(crate) source: String,
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq)]
113pub struct JsLayoutMetrics {
114 pub offset_left: f64,
115 pub offset_top: f64,
116 pub offset_width: f64,
117 pub offset_height: f64,
118 pub client_width: f64,
119 pub client_height: f64,
120 pub rect_left: f64,
121 pub rect_top: f64,
122 pub rect_width: f64,
123 pub rect_height: f64,
124}
125
126#[derive(Debug)]
128pub struct JsFetchResponse {
129 pub(crate) url: String,
130 pub(crate) status: u16,
131 pub(crate) status_text: String,
132 pub(crate) redirected: bool,
133 pub(crate) body: Vec<u8>,
134 pub(crate) headers: Vec<(String, String)>,
135}
136
137#[derive(Clone)]
139pub(crate) struct CustomElementDefinition {
140 pub(crate) constructor: JSValue,
141 pub(crate) connected_callback: Option<JSValue>,
142 pub(crate) disconnected_callback: Option<JSValue>,
143 pub(crate) attribute_changed_callback: Option<JSValue>,
144 pub(crate) observed_attributes: Vec<String>,
145 pub(crate) when_defined_resolvers: Vec<JSValue>,
147}
148
149pub struct JsHost {
155 pub(crate) dom: Rc<DomTree>,
156 pub(crate) refs: HashMap<
157 u64,
158 std::rc::Weak<std::cell::RefCell<crate::engine::tree::TreeNode<HtmlNodeType>>>,
159 >,
160 pub(crate) objects: HashMap<u64, Rc<RefCell<JSObject>>>,
163 pub(crate) styles: HashMap<u64, Rc<RefCell<JSObject>>>,
165 pub(crate) canvas_contexts: HashMap<u64, Rc<RefCell<JSObject>>>,
167 pub(crate) namespaces: HashMap<u64, String>,
169 pub(crate) element_prototype: Rc<RefCell<JSObject>>,
170 pub(crate) element_constructor: Rc<RefCell<JSObject>>,
171 pub(crate) document: Option<Rc<RefCell<JSObject>>>,
172 pub(crate) document_implementation: Option<Rc<RefCell<JSObject>>>,
173 pub(crate) iframe_documents: HashMap<u64, Rc<RefCell<IframeDocument>>>,
176 pub(crate) failed_iframe_fetches: HashSet<u64>,
179 pub(crate) document_event_listeners: HashMap<String, Vec<JSValue>>,
180 pub(crate) element_event_listeners: HashMap<u64, HashMap<String, Vec<(JSValue, bool)>>>,
184 pub(crate) window_inline_event_handlers: HashMap<String, String>,
189 pub(crate) active_element: Option<u64>,
190 pub(crate) detached_nodes: HashMap<u64, NodeRef<HtmlNodeType>>,
192 pub(crate) timers: Vec<JsTimer>,
193 pub(crate) fetch_requests: Vec<JsFetchRequest>,
194 pub(crate) iframe_fetch_requests: Vec<JsIframeFetchRequest>,
195 pub(crate) pending_iframe_fetches: std::collections::HashSet<u64>,
198 pub(crate) dynamic_script_requests: Vec<JsDynamicScriptRequest>,
199 pub(crate) queued_dynamic_scripts: HashSet<u64>,
200 pub(crate) dynamic_style_requests: Vec<JsDynamicStyleRequest>,
201 pub(crate) queued_dynamic_styles: HashSet<u64>,
202 pub(crate) dynamic_image_requests: Vec<JsDynamicImageRequest>,
203 pub(crate) queued_dynamic_images: HashSet<u64>,
204 pub(crate) fetch_capabilities: HashMap<u64, JsFetchCapability>,
205 pub(crate) xhr_requests: HashMap<u64, Rc<RefCell<JSObject>>>,
206 pub(crate) constructing_fetch_capability: Option<JsFetchCapability>,
207 pub(crate) devtools_requests: Vec<JsDevToolsRequest>,
208 pub(crate) devtools_capabilities: HashMap<u64, devtools::JsDevToolsCapability>,
209 pub(crate) constructing_devtools_capability: Option<devtools::JsDevToolsCapability>,
210 pub(crate) next_devtools_id: u64,
211 pub(crate) custom_elements: HashMap<String, CustomElementDefinition>,
213 pub(crate) shadow_roots: HashMap<u64, u64>,
215 pub(crate) local_storage: HashMap<String, String>,
217 pub(crate) session_storage: HashMap<String, String>,
218 pub(crate) document_cookies: HashMap<String, String>,
220 pub(crate) document_url: String,
221 pub(crate) origin: String,
223 pub(crate) viewport: (f64, f64),
224 pub(crate) layout_metrics: HashMap<usize, JsLayoutMetrics>,
226 pub(crate) layout_metrics_by_dom_id: HashMap<u64, JsLayoutMetrics>,
228 pub(crate) next_fetch_id: u64,
229 pub(crate) next_timer_id: u64,
230 pub(crate) time_origin: Instant,
231 pub(crate) dom_content_loaded_fired: bool,
232 pub(crate) window_load_fired: bool,
233 pub(crate) next_id: u64,
234 pub(crate) needs_redraw: Rc<Cell<bool>>,
235}
236
237impl JsHost {
238 pub(crate) fn dom_id_for_node(&self, node: &NodeRef<HtmlNodeType>) -> Option<u64> {
240 self.refs.iter().find_map(|(&dom_id, weak)| {
241 if std::ptr::eq(weak.as_ptr(), Rc::as_ptr(node)) {
242 Some(dom_id)
243 } else {
244 None
245 }
246 })
247 }
248}
249
250pub struct JsRuntime {
252 engine: pixi_byte::JSEngine,
253 needs_redraw: Rc<Cell<bool>>,
254}
255
256impl JsRuntime {
257 pub fn new(dom: Rc<DomTree>) -> Self {
259 let needs_redraw = Rc::new(Cell::new(false));
260 let (element_prototype, element_constructor) =
261 web_apis::dom::element::make_element_interface();
262 let mut host = JsHost {
263 dom,
264 refs: HashMap::new(),
265 objects: HashMap::new(),
266 styles: HashMap::new(),
267 canvas_contexts: HashMap::new(),
268 namespaces: HashMap::new(),
269 element_prototype,
270 element_constructor,
271 document: None,
272 document_implementation: None,
273 iframe_documents: HashMap::new(),
274 failed_iframe_fetches: HashSet::new(),
275 document_event_listeners: HashMap::new(),
276 element_event_listeners: HashMap::new(),
277 window_inline_event_handlers: HashMap::new(),
278 active_element: None,
279 detached_nodes: HashMap::new(),
280 timers: Vec::new(),
281 fetch_requests: Vec::new(),
282 iframe_fetch_requests: Vec::new(),
283 pending_iframe_fetches: std::collections::HashSet::new(),
284 dynamic_script_requests: Vec::new(),
285 queued_dynamic_scripts: HashSet::new(),
286 dynamic_style_requests: Vec::new(),
287 queued_dynamic_styles: HashSet::new(),
288 dynamic_image_requests: Vec::new(),
289 queued_dynamic_images: HashSet::new(),
290 fetch_capabilities: HashMap::new(),
291 xhr_requests: HashMap::new(),
292 constructing_fetch_capability: None,
293 devtools_requests: Vec::new(),
294 devtools_capabilities: HashMap::new(),
295 constructing_devtools_capability: None,
296 next_devtools_id: 0,
297 custom_elements: HashMap::new(),
298 shadow_roots: HashMap::new(),
299 local_storage: HashMap::new(),
300 session_storage: HashMap::new(),
301 document_cookies: HashMap::new(),
302 document_url: "about:blank".to_string(),
303 origin: "null".to_string(),
304 viewport: (800.0, 600.0),
305 layout_metrics: HashMap::new(),
306 layout_metrics_by_dom_id: HashMap::new(),
307 next_fetch_id: 0,
308 next_timer_id: 0,
309 time_origin: Instant::now(),
310 dom_content_loaded_fired: false,
311 window_load_fired: false,
312 next_id: 0,
313 needs_redraw: Rc::clone(&needs_redraw),
314 };
315 Self::register_window_event_handlers(&mut host);
316
317 let host = Rc::new(RefCell::new(host));
318
319 let mut engine = pixi_byte::JSEngine::new();
320 engine.set_host(host);
321
322 web_apis::console::install_console(&mut engine);
323 web_apis::dom::document::install_document(&mut engine);
324
325 web_apis::observers::install_mutation_observer(&mut engine);
326 web_apis::observers::install_resize_observer(&mut engine);
327 web_apis::observers::install_intersection_observer(&mut engine);
328 web_apis::timers::install_timers(&mut engine);
329 web_apis::performance::install_performance(&mut engine);
330 runtime::microtasks::install_microtasks(&mut engine);
331 web_apis::network::install_headers(&mut engine);
332 web_apis::network::install_request(&mut engine);
333 web_apis::network::install_fetch(&mut engine);
334 web_apis::network::install_xml_http_request(&mut engine);
335 devtools::install(&mut engine);
336 web_apis::url::install_url_apis(&mut engine);
337 web_apis::encoding::install_encoding_apis(&mut engine);
338 web_apis::browser_env::install_browser_environment(&mut engine);
339 web_apis::browser_env::install_global_aliases(&mut engine);
340 web_apis::dom::custom_elements::install_custom_elements(&mut engine);
341
342 Self {
343 engine,
344 needs_redraw,
345 }
346 }
347
348 pub fn set_viewport(&mut self, width: f32, height: f32) {
350 let width = width.max(0.0) as f64;
351 let height = height.max(0.0) as f64;
352 let mut global = self.engine.global_mut().borrow_mut();
353 global.set("innerWidth".to_string(), JSValue::from_number(width));
354 global.set("innerHeight".to_string(), JSValue::from_number(height));
355 global.set("outerWidth".to_string(), JSValue::from_number(width));
356 global.set("outerHeight".to_string(), JSValue::from_number(height));
357 drop(global);
358 with_host_mut(self.engine.vm(), |host| host.viewport = (width, height));
359 }
360
361 #[cfg(test)]
364 pub(crate) fn set_layout_metrics(&mut self, metrics: HashMap<usize, JsLayoutMetrics>) {
365 with_host_mut(self.engine.vm(), |host| host.layout_metrics = metrics);
366 }
367
368 pub(crate) fn set_layout_metrics_by_dom_id(&mut self, metrics: HashMap<u64, JsLayoutMetrics>) {
371 with_host_mut(self.engine.vm(), |host| {
372 host.layout_metrics_by_dom_id = metrics
373 });
374 }
375
376 pub fn set_language(&mut self, language: &str) {
378 let language = language.trim();
379 if language.is_empty() {
380 return;
381 }
382 let mut languages = vec![JSValue::from_string(language.to_string())];
383 if let Some(base) = language.split('-').next()
384 && !base.eq_ignore_ascii_case(language)
385 {
386 languages.push(JSValue::from_string(base.to_string()));
387 }
388 if !language.eq_ignore_ascii_case("en-US") {
389 languages.push(JSValue::from_string("en-US".to_string()));
390 }
391
392 let global = self.engine.global_mut().borrow_mut();
393 let Some(navigator) = global.get("navigator").as_object() else {
394 return;
395 };
396 drop(global);
397 let mut navigator = navigator.borrow_mut();
398 navigator.define_property(
399 "language".to_string(),
400 host_read_only_property(JSValue::from_string(language.to_string())),
401 );
402 navigator.define_property(
403 "languages".to_string(),
404 host_read_only_property(JSArray::from_vec(languages).to_object()),
405 );
406 }
407
408 pub fn run_script(&mut self, source: &str) {
410 match self.engine.eval(source) {
411 Ok(_) => {}
412 Err(err) => {
413 if let JSError::Thrown(value) = &err
414 && let Some(object) = value.as_object()
415 {
416 let object = object.borrow();
417 let details = object
418 .keys()
419 .into_iter()
420 .map(|key| format!("{key}={}", object.get(&key).to_console_string()))
421 .collect::<Vec<_>>()
422 .join(", ");
423 log::info!("JS error: uncaught object ({details})");
424 }
425 log::info!("JS error: {}", err);
426 }
427 }
428 self.perform_microtask_checkpoint();
429 }
430
431 pub fn eval_value(&mut self, source: &str) -> JSValue {
433 match self.engine.eval(source) {
434 Ok(value) => value,
435 Err(err) => {
436 log::info!("JS error evaluating {source:?}: {err}");
437 JSValue::undefined()
438 }
439 }
440 }
441
442 pub fn set_document_url(&mut self, url: &str) {
444 let _ = with_host_mut(self.engine.vm(), |host| {
445 host.document_url = url.to_string();
446 });
447 }
448
449 pub fn set_page_origin(&mut self, origin: &str) {
452 let _ = with_host_mut(self.engine.vm(), |host| {
453 host.origin = origin.to_string();
454 });
455 }
456
457 pub fn dispatch_dom_content_loaded(&mut self) -> bool {
462 let Some((document, listeners)) = with_host_mut(self.engine.vm(), |host| {
463 if host.dom_content_loaded_fired {
464 return None;
465 }
466
467 host.dom_content_loaded_fired = true;
468 Some((
469 host.document.as_ref().cloned(),
470 host.document_event_listeners
471 .get("DOMContentLoaded")
472 .cloned()
473 .unwrap_or_default(),
474 ))
475 })
476 .flatten() else {
477 return false;
478 };
479
480 let Some(document) = document else {
481 return true;
482 };
483 for listener in listeners {
484 let event = make_event(
485 "DOMContentLoaded",
486 Rc::clone(&document),
487 Rc::clone(&document),
488 );
489 if let Err(err) = self.engine.call(
490 listener,
491 JSValue::from_object(Rc::clone(&document)),
492 vec![JSValue::from_object(event)],
493 ) {
494 log::info!("JS error on DOMContentLoaded: {}", err);
495 }
496 }
497 self.perform_microtask_checkpoint();
498 true
499 }
500
501 pub fn dispatch_window_load(&mut self) -> bool {
507 let state = with_host_mut(self.engine.vm(), |host| {
508 if host.window_load_fired {
509 return None;
510 }
511 host.window_load_fired = true;
512 Some((
513 host.window_inline_event_handlers
514 .get("load")
515 .cloned()
516 .unwrap_or_default(),
517 host.document_event_listeners
518 .get("load")
519 .cloned()
520 .unwrap_or_default(),
521 ))
522 });
523 let Some((body_onload, listeners)) = state.flatten() else {
524 return false;
525 };
526
527 let window_object = Rc::clone(self.engine.global_mut());
528
529 if !body_onload.trim().is_empty() {
532 self.call_inline_load_handler(&window_object, &body_onload, "body onload");
533 }
534
535 let onload = window_object.borrow().get("onload");
536 if is_callable(&onload) {
537 self.call_load_handler(&window_object, onload, "window.onload");
538 }
539
540 for listener in listeners {
541 self.call_load_handler(&window_object, listener, "window load listener");
542 }
543
544 self.perform_microtask_checkpoint();
545 true
546 }
547
548 fn call_inline_load_handler(
552 &mut self,
553 window: &Rc<RefCell<JSObject>>,
554 code: &str,
555 label: &str,
556 ) {
557 let event = make_event("load", Rc::clone(window), Rc::clone(window));
558 let wrapped = format!("(function(event) {{ {code} \n}})");
562 let handler = self.engine.eval(&wrapped).unwrap_or(JSValue::undefined());
563 if is_callable(&handler)
564 && let Err(err) = self.engine.call(
565 handler,
566 JSValue::from_object(Rc::clone(window)),
567 vec![JSValue::from_object(event)],
568 )
569 {
570 log::info!("JS error in {label}: {err}");
571 }
572 }
573
574 fn call_load_handler(&mut self, window: &Rc<RefCell<JSObject>>, handler: JSValue, label: &str) {
576 let event = make_event("load", Rc::clone(window), Rc::clone(window));
577 if let Err(err) = self.engine.call(
578 handler,
579 JSValue::from_object(Rc::clone(window)),
580 vec![JSValue::from_object(event)],
581 ) {
582 log::info!("JS error in {label}: {err}");
583 }
584 }
585
586 pub fn run_due_timers(&mut self) -> bool {
591 let invocations = with_host_mut(self.engine.vm(), |host| {
592 let now = Instant::now();
593 let mut invocations = Vec::new();
594 let mut index = 0;
595 while index < host.timers.len() {
596 if host.timers[index].deadline > now {
597 index += 1;
598 continue;
599 }
600
601 let callback = host.timers[index].callback.clone();
602 let arguments = host.timers[index].arguments.clone();
603 if let Some(interval) = host.timers[index].interval {
604 host.timers[index].deadline = now + interval;
605 index += 1;
606 } else {
607 host.timers.remove(index);
608 }
609 invocations.push((callback, arguments));
610 }
611 invocations
612 })
613 .unwrap_or_default();
614
615 let ran_callback = !invocations.is_empty();
616 for (callback, arguments) in invocations {
617 if let Err(err) = self.engine.call(callback, JSValue::undefined(), arguments) {
618 log::info!("JS error in timer callback: {}", err);
619 }
620 self.perform_microtask_checkpoint();
621 }
622 ran_callback
623 }
624
625 pub fn needs_redraw(&self) -> bool {
627 self.needs_redraw.get()
628 }
629
630 pub fn take_needs_redraw(&self) -> bool {
632 self.needs_redraw.replace(false)
633 }
634
635 pub(crate) fn mark_needs_redraw(&self) {
639 self.needs_redraw.set(true);
640 }
641
642 pub(crate) fn take_fetch_requests(&mut self) -> Vec<JsFetchRequest> {
644 with_host_mut(self.engine.vm(), |host| {
645 std::mem::take(&mut host.fetch_requests)
646 })
647 .unwrap_or_default()
648 }
649
650 pub fn take_iframe_fetch_requests(&mut self) -> Vec<JsIframeFetchRequest> {
653 with_host_mut(self.engine.vm(), |host| {
654 std::mem::take(&mut host.iframe_fetch_requests)
655 })
656 .unwrap_or_default()
657 }
658
659 pub fn resolve_iframe_fetch(&mut self, dom_id: u64, html: String) {
662 let installed = with_host_mut(self.engine.vm(), |host| {
663 host.pending_iframe_fetches.remove(&dom_id);
664 host.failed_iframe_fetches.remove(&dom_id);
665 web_apis::dom::document::install_parsed_iframe_document(host, dom_id, &html)
666 });
667 if installed.unwrap_or(false) {
668 self.dispatch_element_event(dom_id, "load");
669 self.mark_needs_redraw();
674 }
675 }
676
677 pub fn reject_iframe_fetch(&mut self, dom_id: u64) {
680 with_host_mut(self.engine.vm(), |host| {
681 host.pending_iframe_fetches.remove(&dom_id);
682 host.failed_iframe_fetches.insert(dom_id);
683 });
684 }
685
686 pub(crate) fn queue_markup_iframe_loads(&mut self) -> usize {
695 with_host_mut(self.engine.vm(), |host| {
696 let iframes = host.dom.find_all(|node| node.tag_name() == Some("iframe"));
697 let mut queued = 0;
698 for node in iframes {
699 let src = {
700 let node_ref = node.borrow();
701 node_ref
702 .value
703 .get_attr("src")
704 .map(str::trim)
705 .unwrap_or("")
706 .to_string()
707 };
708 if src.is_empty() {
709 continue;
710 }
711 let Some(dom_id) = host.dom_id_for_node(&node) else {
712 continue;
713 };
714 let before = host.pending_iframe_fetches.len();
715 web_apis::dom::element::queue_iframe_fetch_if_needed(host, dom_id, &src);
716 if host.pending_iframe_fetches.len() > before {
717 queued += 1;
718 }
719 }
720 queued
721 })
722 .unwrap_or(0)
723 }
724
725 pub(crate) fn take_dynamic_script_requests(&mut self) -> Vec<JsDynamicScriptRequest> {
726 with_host_mut(self.engine.vm(), |host| {
727 std::mem::take(&mut host.dynamic_script_requests)
728 })
729 .unwrap_or_default()
730 }
731
732 pub(crate) fn take_dynamic_style_requests(&mut self) -> Vec<JsDynamicStyleRequest> {
733 with_host_mut(self.engine.vm(), |host| {
734 std::mem::take(&mut host.dynamic_style_requests)
735 })
736 .unwrap_or_default()
737 }
738
739 pub(crate) fn take_dynamic_image_requests(&mut self) -> Vec<JsDynamicImageRequest> {
740 with_host_mut(self.engine.vm(), |host| {
741 std::mem::take(&mut host.dynamic_image_requests)
742 })
743 .unwrap_or_default()
744 }
745
746 pub(crate) fn dispatch_element_event(&mut self, node_id: u64, event_type: &str) {
748 let Some((target, listeners)) = with_host(self.engine.vm(), |host| {
749 let target = host.objects.get(&node_id).cloned()?;
750 let listeners = host
751 .element_event_listeners
752 .get(&node_id)
753 .and_then(|events| events.get(event_type))
754 .cloned()
755 .unwrap_or_default();
756 Some((target, listeners))
757 })
758 .flatten() else {
759 return;
760 };
761 let handler = target.borrow().get(&format!("on{event_type}"));
762 let event = make_event(event_type, Rc::clone(&target), Rc::clone(&target));
763 if is_callable(&handler)
764 && let Err(error) = self.engine.call(
765 handler,
766 JSValue::from_object(Rc::clone(&target)),
767 vec![JSValue::from_object(Rc::clone(&event))],
768 )
769 {
770 log::info!("JS error in on{event_type}: {error}");
771 }
772 for listener in listeners {
773 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
774 break;
775 }
776 if let Err(error) = self.engine.call(
777 listener.0,
778 JSValue::from_object(Rc::clone(&target)),
779 vec![JSValue::from_object(Rc::clone(&event))],
780 ) {
781 log::info!("JS error in {event_type} listener: {error}");
782 }
783 }
784 }
785
786 pub(crate) fn resolve_fetch(&mut self, id: u64, response: JsFetchResponse) {
788 let capability =
789 with_host_mut(self.engine.vm(), |host| host.fetch_capabilities.remove(&id)).flatten();
790 if let Some(capability) = capability {
791 let response = make_fetch_response(response);
792 if let Err(err) = self.engine.call(
793 capability.resolve,
794 JSValue::undefined(),
795 vec![JSValue::from_object(response)],
796 ) {
797 log::info!("JS error while resolving fetch: {}", err);
798 }
799 self.perform_microtask_checkpoint();
800 return;
801 }
802 let xhr = with_host_mut(self.engine.vm(), |host| host.xhr_requests.remove(&id)).flatten();
803 let Some(xhr) = xhr else { return };
804 resolve_xml_http_request(&mut self.engine, xhr, response);
805 self.perform_microtask_checkpoint();
806 }
807
808 pub(crate) fn reject_fetch(&mut self, id: u64, reason: String) {
810 let capability =
811 with_host_mut(self.engine.vm(), |host| host.fetch_capabilities.remove(&id)).flatten();
812 if let Some(capability) = capability {
813 if let Err(err) = self.engine.call(
814 capability.reject,
815 JSValue::undefined(),
816 vec![JSValue::from_string(reason)],
817 ) {
818 log::info!("JS error while rejecting fetch: {}", err);
819 }
820 self.perform_microtask_checkpoint();
821 return;
822 }
823 let xhr = with_host_mut(self.engine.vm(), |host| host.xhr_requests.remove(&id)).flatten();
824 let Some(xhr) = xhr else { return };
825 let handler = xhr.borrow().get("onerror");
826 if is_callable(&handler) {
827 let _ = self.engine.call(
828 handler,
829 JSValue::from_object(Rc::clone(&xhr)),
830 vec![JSValue::from_string(reason)],
831 );
832 }
833 self.perform_microtask_checkpoint();
834 }
835
836 pub fn snapshot(&self) -> DomSnapshot {
841 let Some((root, dom_ids)) = with_host(self.engine.vm(), |host| {
842 let mut reverse = HashMap::new();
843 for (dom_id, weak) in &host.refs {
844 if let Some(node) = weak.upgrade() {
845 reverse.insert(Rc::as_ptr(&node) as usize, *dom_id);
846 }
847 }
848 (Rc::clone(&host.dom.root), reverse)
849 }) else {
850 return DomSnapshot::default();
851 };
852 DomSnapshot::from_mirror(&root, &dom_ids)
853 }
854
855 pub fn snapshot_iframe_documents(&self) -> Vec<IframeContentSnapshot> {
858 let Some(docs) = with_host(self.engine.vm(), |host| {
859 let mut out: Vec<(u64, Rc<DomTree>)> = Vec::new();
860 for (dom_id, doc) in &host.iframe_documents {
861 out.push((*dom_id, Rc::clone(&doc.borrow().tree)));
862 }
863 Some(out)
864 })
865 .flatten() else {
866 return Vec::new();
867 };
868 let mut snapshots = Vec::with_capacity(docs.len());
869 for (dom_id, tree) in docs {
870 let (content, _refs) = DomSnapshot::from_tree(&tree.root);
871 snapshots.push(IframeContentSnapshot {
872 iframe_dom_id: dom_id,
873 content,
874 });
875 }
876 snapshots
877 }
878
879 pub fn apply_dom(&mut self, snapshot: &DomSnapshot) {
886 let (tree, dom_ids) = snapshot.into_tree();
887 with_host_mut(self.engine.vm(), |host| {
888 host.dom = Rc::new(tree);
889 let mut refs = std::mem::take(&mut host.refs);
890 host.dom.traverse(|node| {
891 if let Some(&dom_id) = dom_ids.get(&(Rc::as_ptr(node) as usize)) {
892 refs.insert(dom_id, Rc::downgrade(node));
893 }
894 });
895 for (&dom_id, node) in &host.detached_nodes {
896 refs.entry(dom_id).or_insert_with(|| Rc::downgrade(node));
897 }
898 host.refs = refs;
899 if let Some(max_id) = dom_ids.values().max() {
900 host.next_id = host.next_id.max(*max_id);
901 }
902 Self::register_window_event_handlers(host);
903 });
904 }
905
906 fn register_window_event_handlers(host: &mut JsHost) {
914 for node in host.dom.find_all(|node| node.tag_name() == Some("body")) {
915 if let Some(code) = node.borrow().value.get_attr("onload") {
916 host.window_inline_event_handlers
917 .insert("load".to_string(), code.to_string());
918 }
919 }
920 }
921
922 pub fn click_dom_id(&mut self, dom_id: u64) -> bool {
925 let Some(node) = with_host(self.engine.vm(), |host| {
926 host.refs.get(&dom_id).and_then(|w| w.upgrade())
927 })
928 .flatten() else {
929 return false;
930 };
931 self.click(&node)
932 }
933
934 pub fn click(&mut self, node: &NodeRef<HtmlNodeType>) -> bool {
941 let mut path = Vec::new();
942 let mut current = Some(Rc::clone(node));
943 while let Some(node) = current {
944 current = node.borrow().parent();
945 if let Some(object) = expose_node(self.engine.vm(), node).and_then(|v| v.as_object()) {
946 path.push(object);
947 }
948 }
949 let Some(target) = path.first().cloned() else {
950 return false;
951 };
952
953 let mut ran_handler = false;
954 for current_target in path {
955 let Some(dom_id) = node_dom_id(&JSValue::from_object(Rc::clone(¤t_target)))
956 else {
957 continue;
958 };
959 let onclick = current_target.borrow().get("onclick");
960 let listeners = with_host(self.engine.vm(), |host| {
961 host.element_event_listeners
962 .get(&dom_id)
963 .and_then(|events| events.get("click"))
964 .cloned()
965 .unwrap_or_default()
966 })
967 .unwrap_or_default();
968 let has_onclick = is_callable(&onclick);
969 if !has_onclick && listeners.is_empty() {
970 continue;
971 }
972
973 ran_handler = true;
974 let event = make_event("click", Rc::clone(&target), Rc::clone(¤t_target));
975 if has_onclick
976 && let Err(err) = self.engine.call(
977 onclick,
978 JSValue::from_object(Rc::clone(¤t_target)),
979 vec![JSValue::from_object(Rc::clone(&event))],
980 )
981 {
982 log::info!("JS error in onclick: {}", err);
983 }
984 if !event_flag(&event, "__orinium_immediate_propagation_stopped") {
985 for listener in listeners {
986 if let Err(err) = self.engine.call(
987 listener.0,
988 JSValue::from_object(Rc::clone(¤t_target)),
989 vec![JSValue::from_object(Rc::clone(&event))],
990 ) {
991 log::info!("JS error in click listener: {}", err);
992 }
993 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
994 break;
995 }
996 }
997 }
998 if event_flag(&event, "cancelBubble") {
999 break;
1000 }
1001 }
1002 if ran_handler {
1003 self.perform_microtask_checkpoint();
1004 }
1005 ran_handler
1006 }
1007
1008 pub fn scroll_dom_id(&mut self, dom_id: u64) -> bool {
1012 let Some(node) = with_host(self.engine.vm(), |host| {
1013 host.refs.get(&dom_id).and_then(|w| w.upgrade())
1014 })
1015 .flatten() else {
1016 return false;
1017 };
1018 self.scroll(&node)
1019 }
1020
1021 pub fn scroll(&mut self, node: &NodeRef<HtmlNodeType>) -> bool {
1027 let mut path = Vec::new();
1028 let mut current = Some(Rc::clone(node));
1029 while let Some(node) = current {
1030 current = node.borrow().parent();
1031 if let Some(object) = expose_node(self.engine.vm(), node).and_then(|v| v.as_object()) {
1032 path.push(object);
1033 }
1034 }
1035 let Some(target) = path.first().cloned() else {
1036 return false;
1037 };
1038
1039 let mut ran_handler = false;
1040 for current_target in path {
1041 let Some(dom_id) = node_dom_id(&JSValue::from_object(Rc::clone(¤t_target)))
1042 else {
1043 continue;
1044 };
1045 let onscroll = current_target.borrow().get("onscroll");
1046 let listeners = with_host(self.engine.vm(), |host| {
1047 host.element_event_listeners
1048 .get(&dom_id)
1049 .and_then(|events| events.get("scroll"))
1050 .cloned()
1051 .unwrap_or_default()
1052 })
1053 .unwrap_or_default();
1054 let has_onscroll = is_callable(&onscroll);
1055 if !has_onscroll && listeners.is_empty() {
1056 continue;
1057 }
1058
1059 ran_handler = true;
1060 let event = make_event("scroll", Rc::clone(&target), Rc::clone(¤t_target));
1061 if has_onscroll
1062 && let Err(err) = self.engine.call(
1063 onscroll,
1064 JSValue::from_object(Rc::clone(¤t_target)),
1065 vec![JSValue::from_object(Rc::clone(&event))],
1066 )
1067 {
1068 log::info!("JS error in onscroll: {}", err);
1069 }
1070 if !event_flag(&event, "__orinium_immediate_propagation_stopped") {
1071 for listener in listeners {
1072 if let Err(err) = self.engine.call(
1073 listener.0,
1074 JSValue::from_object(Rc::clone(¤t_target)),
1075 vec![JSValue::from_object(Rc::clone(&event))],
1076 ) {
1077 log::info!("JS error in scroll listener: {}", err);
1078 }
1079 if event_flag(&event, "__orinium_immediate_propagation_stopped") {
1080 break;
1081 }
1082 }
1083 }
1084 if event_flag(&event, "cancelBubble") {
1085 break;
1086 }
1087 }
1088 if ran_handler {
1089 self.perform_microtask_checkpoint();
1090 }
1091 ran_handler
1092 }
1093 fn perform_microtask_checkpoint(&mut self) {
1095 while let Err(err) = self.engine.run_jobs() {
1096 if let JSError::Thrown(value) = &err
1097 && let Some(object) = value.as_object()
1098 {
1099 let object = object.borrow();
1100 let details = object
1101 .keys()
1102 .into_iter()
1103 .map(|key| format!("{key}={}", object.get(&key).to_console_string()))
1104 .collect::<Vec<_>>()
1105 .join(", ");
1106 log::info!("JS error in microtask: {} ({details})", err);
1107 } else {
1108 log::info!("JS error in microtask: {}", err);
1109 }
1110 }
1111 }
1112}
1113
1114#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use crate::engine::html::Parser as HtmlParser;
1122 use web_apis::dom::element::{HTML_NAMESPACE, SVG_NAMESPACE, style_property_name};
1123
1124 fn runtime_from_html(html: &str) -> (JsRuntime, Rc<DomTree>) {
1125 let mut parser = HtmlParser::new(html);
1126 let dom = Rc::new(parser.parse());
1127 let runtime = JsRuntime::new(Rc::clone(&dom));
1128 (runtime, dom)
1129 }
1130
1131 #[test]
1132 fn set_text_content_mutates_dom_and_marks_dirty() {
1133 let (mut runtime, dom) = runtime_from_html(r#"<div id="hello">before</div>"#);
1134 runtime.run_script(
1135 r#"const el = document.getElementById("hello"); el.textContent = "hello from js";"#,
1136 );
1137 assert!(runtime.needs_redraw());
1138 assert!(runtime.take_needs_redraw());
1139
1140 let node = dom.get_element_by_id("hello").unwrap();
1141 assert_eq!(DomTree::inner_text(&node), "hello from js");
1142 }
1143
1144 #[test]
1145 fn viewport_dimensions_follow_browser_resizes() {
1146 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1147 runtime.set_viewport(1280.0, 720.0);
1148 runtime.run_script(
1149 r#"document.getElementById("result").setAttribute("data-size", innerWidth + ":" + innerHeight);"#,
1150 );
1151 assert_eq!(
1152 dom.get_element_by_id("result")
1153 .unwrap()
1154 .borrow()
1155 .value
1156 .get_attr("data-size"),
1157 Some("1280:720")
1158 );
1159 runtime.run_script(
1160 r#"document.getElementById("result").setAttribute("data-root", document.body.clientWidth + ":" + document.body.clientHeight + ":" + outerWidth + ":" + outerHeight);"#,
1161 );
1162 assert_eq!(
1163 dom.get_element_by_id("result")
1164 .unwrap()
1165 .borrow()
1166 .value
1167 .get_attr("data-root"),
1168 Some("1280:720:1280:720")
1169 );
1170 }
1171
1172 #[test]
1173 fn set_attribute_mutates_dom() {
1174 let (mut runtime, dom) = runtime_from_html(r#"<div id="hello"></div>"#);
1175 runtime.run_script(
1176 r#"const el = document.getElementById("hello"); el.setAttribute("data-run", "1");"#,
1177 );
1178
1179 let node = dom.get_element_by_id("hello").unwrap();
1180 assert_eq!(node.borrow().value.get_attr("data-run"), Some("1"));
1181 }
1182
1183 #[test]
1184 fn browser_environment_exposes_react_bootstrap_apis() {
1185 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1186 runtime.set_document_url("https://scratch.mit.edu/projects/editor/?tutorial=1#stage");
1187 runtime.run_script(
1188 r#"
1189 localStorage.setItem("answer", 42);
1190 sessionStorage.setItem("temporary", "yes");
1191 const query = matchMedia("(prefers-color-scheme: dark)");
1192 query.addEventListener("change", function () {});
1193 const event = new CustomEvent("ready", {detail: "loaded", cancelable: true});
1194 event.preventDefault();
1195 const frame = requestAnimationFrame(function () {});
1196 cancelAnimationFrame(frame);
1197 document.getElementById("result").setAttribute(
1198 "data-environment",
1199 navigator.language + ":" + localStorage.getItem("answer") + ":" +
1200 localStorage.length + ":" + query.matches + ":" + (frame > 0) + ":" +
1201 location.pathname + ":" + event.detail + ":" + event.defaultPrevented + ":" +
1202 (typeof Intl === "object") + ":" + ("Locale" in Intl) + ":" +
1203 Intl.getCanonicalLocales(["EN-us", "ja"])[0] + ":" +
1204 new Intl.Locale("und-x-private").toString()
1205 );
1206 "#,
1207 );
1208
1209 let node = dom.get_element_by_id("result").unwrap();
1210 assert_eq!(
1211 node.borrow().value.get_attr("data-environment"),
1212 Some(
1213 "en-US:42:1:false:true:/projects/editor/:loaded:true:true:true:en-US:und-x-private"
1214 )
1215 );
1216 }
1217
1218 #[test]
1219 fn browser_origin_exposed_consistently_across_window_location_and_document() {
1220 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1221 runtime.set_page_origin("https://example.test");
1222 runtime.run_script(
1223 r#"document.getElementById("result").setAttribute(
1224 "data-origins",
1225 window.origin + ":" + location.origin + ":" + document.origin
1226 );"#,
1227 );
1228
1229 let node = dom.get_element_by_id("result").unwrap();
1230 assert_eq!(
1231 node.borrow().value.get_attr("data-origins"),
1232 Some("https://example.test:https://example.test:https://example.test")
1233 );
1234 }
1235
1236 #[test]
1237 fn opaque_page_reports_null_origin_everywhere() {
1238 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1239 runtime.set_page_origin("null");
1240 runtime.run_script(
1241 r#"document.getElementById("result").setAttribute(
1242 "data-origins",
1243 window.origin + ":" + location.origin + ":" + document.origin
1244 );"#,
1245 );
1246
1247 let node = dom.get_element_by_id("result").unwrap();
1248 assert_eq!(
1249 node.borrow().value.get_attr("data-origins"),
1250 Some("null:null:null")
1251 );
1252 }
1253
1254 #[test]
1255 fn browser_language_preferences_follow_the_host() {
1256 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1257 runtime.set_language("ja-JP");
1258 runtime.run_script(
1259 r#"
1260 document.getElementById("result").setAttribute(
1261 "data-languages",
1262 navigator.language + ":" + navigator.languages[0] + ":" +
1263 navigator.languages[1] + ":" + navigator.languages[2]
1264 );
1265 "#,
1266 );
1267
1268 assert_eq!(
1269 dom.get_element_by_id("result")
1270 .unwrap()
1271 .borrow()
1272 .value
1273 .get_attr("data-languages"),
1274 Some("ja-JP:ja-JP:ja:en-US")
1275 );
1276 }
1277
1278 #[test]
1279 fn document_cookie_is_a_string_and_supports_assignment_and_expiry() {
1280 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1281 runtime.run_script(
1282 r#"
1283 const result = document.getElementById("result");
1284 result.setAttribute("data-empty-cookie", typeof document.cookie + ":" + document.cookie);
1285 document.cookie = "scratchlanguage=ja; Path=/";
1286 result.setAttribute("data-cookie", document.cookie);
1287 document.cookie = "scratchlanguage=; Max-Age=0; Path=/";
1288 result.setAttribute("data-expired-cookie", document.cookie);
1289 "#,
1290 );
1291
1292 let result = dom.get_element_by_id("result").unwrap();
1293 let result = result.borrow();
1294 assert_eq!(result.value.get_attr("data-empty-cookie"), Some("string:"));
1295 assert_eq!(
1296 result.value.get_attr("data-cookie"),
1297 Some("scratchlanguage=ja")
1298 );
1299 assert_eq!(result.value.get_attr("data-expired-cookie"), Some(""));
1300 }
1301
1302 #[test]
1303 fn url_apis_resolve_assets_and_manage_query_parameters() {
1304 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1305 runtime.run_script(
1306 r#"
1307 const asset = new URL("../assets/stage.svg?locale=ja#costume", "https://scratch.mit.edu/projects/editor/");
1308 const params = new URLSearchParams("project=123&mode=editor");
1309 params.set("mode", "fullscreen");
1310 params.append("cloud", "on");
1311 params.delete("project");
1312 document.getElementById("result").setAttribute(
1313 "data-url",
1314 asset.origin + ":" + asset.pathname + ":" + asset.searchParams.get("locale") +
1315 ":" + params.has("cloud") + ":" + params.toString()
1316 );
1317 "#,
1318 );
1319
1320 let result = dom.get_element_by_id("result").unwrap();
1321 assert_eq!(
1322 result.borrow().value.get_attr("data-url"),
1323 Some(
1324 "https://scratch.mit.edu:/projects/assets/stage.svg:ja:true:mode=fullscreen&cloud=on"
1325 )
1326 );
1327 }
1328
1329 #[test]
1330 fn encoding_apis_round_trip_utf8_and_base64() {
1331 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1332 runtime.run_script(
1333 r#"
1334 const encoder = new TextEncoder();
1335 const decoder = new TextDecoder("utf-8");
1336 const bytes = encoder.encode("Scratch 日本");
1337 document.getElementById("result").setAttribute(
1338 "data-encoding",
1339 decoder.decode(bytes) + ":" + bytes.length + ":" + atob(btoa("Scratch")) +
1340 ":" + decodeURIComponent(encodeURIComponent("日本 語"))
1341 );
1342 "#,
1343 );
1344
1345 let result = dom.get_element_by_id("result").unwrap();
1346 assert_eq!(
1347 result.borrow().value.get_attr("data-encoding"),
1348 Some("Scratch 日本:14:Scratch:日本 語")
1349 );
1350 }
1351
1352 #[test]
1353 fn layout_measurement_and_resize_observer_report_element_size() {
1354 let (mut runtime, dom) = runtime_from_html(
1355 r#"<canvas id="stage" width="480" height="360"></canvas><div id="result"></div>"#,
1356 );
1357 runtime.run_script(
1358 r#"
1359 const stage = document.getElementById("stage");
1360 const rect = stage.getBoundingClientRect();
1361 const observer = new ResizeObserver(function (entries) {
1362 const observed = entries[0].contentRect;
1363 document.getElementById("result").setAttribute(
1364 "data-resize",
1365 observed.width + ":" + observed.height
1366 );
1367 });
1368 observer.observe(stage);
1369 document.getElementById("result").setAttribute(
1370 "data-measure",
1371 rect.width + ":" + rect.height + ":" + stage.clientWidth + ":" + stage.offsetHeight
1372 );
1373 "#,
1374 );
1375
1376 let result = dom.get_element_by_id("result").unwrap();
1377 let result = result.borrow();
1378 assert_eq!(
1379 result.value.get_attr("data-measure"),
1380 Some("480:360:480:360")
1381 );
1382 assert_eq!(result.value.get_attr("data-resize"), Some("480:360"));
1383 }
1384
1385 #[test]
1386 fn layout_offsets_have_a_numeric_fallback() {
1387 let (mut runtime, dom) = runtime_from_html(
1388 r#"<div id="list" class="carousel slick-list"></div><div id="plain"></div><div id="result"></div>"#,
1389 );
1390 runtime.run_script(
1391 r#"
1392 document.getElementById("result").setAttribute(
1393 "data-widths",
1394 document.getElementById("list").offsetWidth + ":" +
1395 document.getElementById("plain").offsetWidth + ":" +
1396 document.getElementById("list").offsetLeft + ":" +
1397 document.getElementById("list").offsetTop
1398 );
1399 "#,
1400 );
1401
1402 assert_eq!(
1403 dom.get_element_by_id("result")
1404 .unwrap()
1405 .borrow()
1406 .value
1407 .get_attr("data-widths"),
1408 Some("800:0:0:0")
1409 );
1410 }
1411
1412 #[test]
1413 fn dom_measurements_prefer_committed_layout_geometry() {
1414 let (mut runtime, dom) = runtime_from_html(
1415 r#"<div id="target" style="width: 1px; height: 2px"></div><div id="result"></div>"#,
1416 );
1417 let target = dom.get_element_by_id("target").unwrap();
1418 runtime.set_layout_metrics(HashMap::from([(
1419 Rc::as_ptr(&target) as usize,
1420 JsLayoutMetrics {
1421 offset_left: 12.0,
1422 offset_top: 34.0,
1423 offset_width: 222.0,
1424 offset_height: 111.0,
1425 client_width: 218.0,
1426 client_height: 107.0,
1427 rect_left: 42.5,
1428 rect_top: 64.25,
1429 rect_width: 222.0,
1430 rect_height: 111.0,
1431 },
1432 )]));
1433 runtime.run_script(
1434 r#"
1435 const target = document.getElementById("target");
1436 const rect = target.getBoundingClientRect();
1437 document.getElementById("result").setAttribute(
1438 "data-layout",
1439 target.offsetLeft + ":" + target.offsetTop + ":" +
1440 target.offsetWidth + ":" + target.offsetHeight + ":" +
1441 target.clientWidth + ":" + target.clientHeight + ":" +
1442 rect.left + ":" + rect.top + ":" + rect.right + ":" + rect.bottom
1443 );
1444 "#,
1445 );
1446
1447 assert_eq!(
1448 dom.get_element_by_id("result")
1449 .unwrap()
1450 .borrow()
1451 .value
1452 .get_attr("data-layout"),
1453 Some("12:34:222:111:218:107:42.5:64.25:264.5:175.25")
1454 );
1455 }
1456
1457 #[test]
1458 fn inserting_script_element_queues_dynamic_resource_load() {
1459 let (mut runtime, _dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
1460 runtime.run_script(
1461 r#"
1462 const script = document.createElement("script");
1463 script.src = "/static/chunks/editor.js";
1464 script.async = true;
1465 document.head.appendChild(script);
1466 "#,
1467 );
1468
1469 let requests = runtime.take_dynamic_script_requests();
1470 assert_eq!(requests.len(), 1);
1471 assert!(requests[0].node_id > 0);
1472 match &requests[0].source {
1473 JsDynamicScriptSource::External(source) => {
1474 assert_eq!(source, "/static/chunks/editor.js")
1475 }
1476 JsDynamicScriptSource::Inline(_) => panic!("expected an external script request"),
1477 }
1478 }
1479
1480 #[test]
1481 fn inserting_stylesheet_link_queues_dynamic_resource_load() {
1482 let (mut runtime, _dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
1483 runtime.run_script(
1484 r#"
1485 const link = document.createElement("link");
1486 link.rel = "stylesheet";
1487 link.href = "/static/css/editor.css";
1488 document.head.appendChild(link);
1489 "#,
1490 );
1491
1492 let requests = runtime.take_dynamic_style_requests();
1493 assert_eq!(requests.len(), 1);
1494 assert!(requests[0].node_id > 0);
1495 assert_eq!(requests[0].url, "/static/css/editor.css");
1496 }
1497
1498 #[test]
1499 fn inserting_image_queues_dynamic_resource_load_once() {
1500 let (mut runtime, _dom) = runtime_from_html(r#"<html><body></body></html>"#);
1501 runtime.run_script(
1502 r#"
1503 const image = document.createElement("img");
1504 image.src = "/images/scratch-logo.svg";
1505 document.body.appendChild(image);
1506 "#,
1507 );
1508
1509 let requests = runtime.take_dynamic_image_requests();
1510 assert_eq!(requests.len(), 1);
1511 assert_eq!(requests[0].source, "/images/scratch-logo.svg");
1512 }
1513
1514 #[test]
1515 fn canvas_2d_context_records_visible_rectangle_commands() {
1516 let (mut runtime, dom) = runtime_from_html(r#"<canvas id="stage"></canvas>"#);
1517 runtime.run_script(
1518 r##"
1519 const canvas = document.getElementById("stage");
1520 canvas.width = 480;
1521 canvas.height = 360;
1522 const context = canvas.getContext("2d");
1523 context.fillStyle = "#ff8800";
1524 context.fillRect(10, 20, 30, 40);
1525 context.strokeStyle = "blue";
1526 context.strokeRect(0, 0, 480, 360);
1527 canvas.setAttribute("data-metrics", context.measureText("Scratch").width);
1528 "##,
1529 );
1530
1531 let canvas = dom.get_element_by_id("stage").unwrap();
1532 let canvas = canvas.borrow();
1533 assert_eq!(canvas.value.get_attr("width"), Some("480"));
1534 assert_eq!(canvas.value.get_attr("height"), Some("360"));
1535 assert_eq!(canvas.value.get_attr("data-metrics"), Some("42"));
1536 assert_eq!(
1537 canvas.value.get_attr("data-orinium-canvas-commands"),
1538 Some("fillRect|#ff8800|10|20|30|40\nstrokeRect|blue|0|0|480|360")
1539 );
1540 }
1541
1542 #[test]
1543 fn canvas_exposes_webgl_capability_surface() {
1544 let (mut runtime, dom) =
1545 runtime_from_html(r#"<canvas id="stage" width="480" height="360"></canvas>"#);
1546 runtime.run_script(
1547 r#"
1548 const canvas = document.getElementById("stage");
1549 const gl = canvas.getContext("webgl");
1550 const shader = gl.createShader(gl.VERTEX_SHADER);
1551 gl.shaderSource(shader, "void main() {}");
1552 gl.compileShader(shader);
1553 const program = gl.createProgram();
1554 gl.attachShader(program, shader);
1555 gl.linkProgram(program);
1556 canvas.setAttribute(
1557 "data-webgl",
1558 gl.getShaderParameter(shader, gl.COMPILE_STATUS) + ":" +
1559 gl.getProgramParameter(program, gl.LINK_STATUS) + ":" +
1560 gl.getParameter(gl.MAX_TEXTURE_SIZE) + ":" + gl.drawingBufferWidth
1561 );
1562 "#,
1563 );
1564
1565 let canvas = dom.get_element_by_id("stage").unwrap();
1566 assert_eq!(
1567 canvas.borrow().value.get_attr("data-webgl"),
1568 Some("true:true:4096:480")
1569 );
1570 }
1571
1572 #[test]
1573 fn mutation_observer_can_register_for_dom_changes() {
1574 let (mut runtime, dom) =
1575 runtime_from_html(r#"<html><body><div id="target"></div></body></html>"#);
1576 runtime.run_script(
1577 r#"
1578 const observer = new MutationObserver(function () {
1579 document.getElementById("target").setAttribute("data-observed", "yes");
1580 });
1581 observer.observe(document.documentElement, { childList: true, subtree: true });
1582 const records = observer.takeRecords();
1583 observer.disconnect();
1584 document.getElementById("target").setAttribute("data-records", records.length);
1585 "#,
1586 );
1587
1588 let node = dom.get_element_by_id("target").unwrap();
1589 assert_eq!(node.borrow().value.get_attr("data-records"), Some("0"));
1590 assert_eq!(node.borrow().value.get_attr("data-observed"), Some("yes"));
1591 }
1592
1593 #[test]
1594 fn element_id_is_a_live_reflected_property() {
1595 let (mut runtime, dom) = runtime_from_html(r#"<main id="root"></main>"#);
1596 runtime.run_script(
1597 r#"
1598 const child = document.createElement("button");
1599 child.id = "first";
1600 document.getElementById("root").appendChild(child);
1601 child.setAttribute("id", "second");
1602 child.setAttribute("data-current-id", child.id);
1603 "#,
1604 );
1605
1606 let child = dom.get_element_by_id("second").unwrap();
1607 assert_eq!(
1608 child.borrow().value.get_attr("data-current-id"),
1609 Some("second")
1610 );
1611 assert!(runtime.needs_redraw());
1612 }
1613
1614 #[test]
1615 fn form_properties_reflect_to_dom_attributes() {
1616 let (mut runtime, dom) = runtime_from_html(
1617 r#"<input id="field"><option id="option"></option><select id="select"></select>"#,
1618 );
1619 runtime.run_script(
1620 r#"
1621 const field = document.getElementById("field");
1622 field.value = "hello";
1623 field.checked = true;
1624 field.disabled = true;
1625 field.checked = false;
1626 const option = document.getElementById("option");
1627 option.selected = true;
1628 const select = document.getElementById("select");
1629 select.multiple = true;
1630 "#,
1631 );
1632
1633 let field = dom.get_element_by_id("field").unwrap();
1634 let field = field.borrow();
1635 assert_eq!(field.value.get_attr("value"), Some("hello"));
1636 assert_eq!(field.value.get_attr("checked"), None);
1637 assert_eq!(field.value.get_attr("disabled"), Some(""));
1638 drop(field);
1639 let option = dom.get_element_by_id("option").unwrap();
1640 assert_eq!(option.borrow().value.get_attr("selected"), Some(""));
1641 let select = dom.get_element_by_id("select").unwrap();
1642 assert_eq!(select.borrow().value.get_attr("multiple"), Some(""));
1643 assert!(runtime.needs_redraw());
1644 }
1645
1646 #[test]
1647 fn form_properties_are_accessors_on_the_element_prototype() {
1648 let (mut runtime, dom) = runtime_from_html(r#"<input id="field">"#);
1649 runtime.run_script(
1650 r#"
1651 const field = document.getElementById("field");
1652 const prototype = field.constructor.prototype;
1653 const descriptor = Object.getOwnPropertyDescriptor(prototype, "value");
1654 field.setAttribute("data-prototype", Object.getPrototypeOf(field) === prototype);
1655 field.setAttribute("data-interface", field.constructor === HTMLElement && field instanceof Element);
1656 field.setAttribute("data-own-value", field.hasOwnProperty("value"));
1657 field.setAttribute("data-accessor", typeof descriptor.get + ":" + typeof descriptor.set);
1658 descriptor.set.call(field, "tracked");
1659 field.setAttribute("data-read", descriptor.get.call(field));
1660 "#,
1661 );
1662
1663 let field = dom.get_element_by_id("field").unwrap();
1664 let field = field.borrow();
1665 assert_eq!(field.value.get_attr("data-prototype"), Some("true"));
1666 assert_eq!(field.value.get_attr("data-interface"), Some("true"));
1667 assert_eq!(field.value.get_attr("data-own-value"), Some("false"));
1668 assert_eq!(
1669 field.value.get_attr("data-accessor"),
1670 Some("function:function")
1671 );
1672 assert_eq!(field.value.get_attr("data-read"), Some("tracked"));
1673 assert_eq!(field.value.get_attr("value"), Some("tracked"));
1674 }
1675
1676 #[test]
1677 fn exposes_document_and_node_metadata_used_by_react_dom() {
1678 let (mut runtime, dom) = runtime_from_html(
1679 r#"<html><body><main id="root"><span id="child">text</span></main></body></html>"#,
1680 );
1681 runtime.run_script(
1682 r#"
1683 const root = document.getElementById("root");
1684 const child = document.getElementById("child");
1685 child.setAttribute("data-default-view", document.defaultView === window);
1686 child.setAttribute("data-ready-before", document.readyState);
1687 child.setAttribute("data-local-name", child.localName);
1688 child.setAttribute("data-parent-element", child.parentElement === root);
1689 child.setAttribute("data-connected", child.isConnected);
1690 child.setAttribute("data-text-connected", child.firstChild.isConnected);
1691 "#,
1692 );
1693
1694 let child = dom.get_element_by_id("child").unwrap();
1695 let child = child.borrow();
1696 assert_eq!(child.value.get_attr("data-default-view"), Some("true"));
1697 assert_eq!(child.value.get_attr("data-ready-before"), Some("loading"));
1698 assert_eq!(child.value.get_attr("data-local-name"), Some("span"));
1699 assert_eq!(child.value.get_attr("data-parent-element"), Some("true"));
1700 assert_eq!(child.value.get_attr("data-connected"), Some("true"));
1701 assert_eq!(child.value.get_attr("data-text-connected"), Some("true"));
1702 drop(child);
1703
1704 assert!(runtime.dispatch_dom_content_loaded());
1705 runtime.run_script(
1706 r#"
1707 document.getElementById("child").setAttribute(
1708 "data-ready-after",
1709 document.readyState
1710 );
1711 "#,
1712 );
1713 assert_eq!(
1714 dom.get_element_by_id("child")
1715 .unwrap()
1716 .borrow()
1717 .value
1718 .get_attr("data-ready-after"),
1719 Some("complete")
1720 );
1721 }
1722
1723 #[test]
1724 fn style_declaration_mutates_inline_style() {
1725 let (mut runtime, dom) = runtime_from_html(r#"<div id="target"></div>"#);
1726 runtime.run_script(
1727 r#"
1728 const target = document.getElementById("target");
1729 target.style.backgroundColor = "red";
1730 target.style.setProperty("--accent", "blue");
1731 target.style.marginTop = "4px";
1732 target.style.removeProperty("background-color");
1733 "#,
1734 );
1735
1736 let node = dom.get_element_by_id("target").unwrap();
1737 assert_eq!(
1738 node.borrow().value.get_attr("style"),
1739 Some("--accent: blue; margin-top: 4px;")
1740 );
1741 assert!(runtime.needs_redraw());
1742 }
1743
1744 #[test]
1745 fn inner_html_parses_replaces_and_serializes_children() {
1746 let (mut runtime, dom) = runtime_from_html(
1747 r#"<div id="target"><em id="old">old</em></div><div id="result"></div>"#,
1748 );
1749 runtime.run_script(
1750 r#"
1751 const target = document.getElementById("target");
1752 const old = document.getElementById("old");
1753 target.innerHTML = '<span id="child" data-label="a&b">hello</span><br>';
1754 old.setAttribute("data-detached", "yes");
1755 document.getElementById("result").setAttribute("data-html", target.innerHTML);
1756 "#,
1757 );
1758
1759 let target = dom.get_element_by_id("target").unwrap();
1760 assert_eq!(target.borrow().children().len(), 2);
1761 assert!(dom.get_element_by_id("old").is_none());
1762 assert_eq!(
1763 dom.get_element_by_id("result")
1764 .unwrap()
1765 .borrow()
1766 .value
1767 .get_attr("data-html"),
1768 Some("<span id=\"child\" data-label=\"a&b\">hello</span><br>")
1769 );
1770 assert!(runtime.needs_redraw());
1771 }
1772
1773 #[test]
1774 fn style_property_names_follow_cssom_spelling() {
1775 assert_eq!(style_property_name("backgroundColor"), "background-color");
1776 assert_eq!(style_property_name("msTransition"), "-ms-transition");
1777 assert_eq!(style_property_name("WebkitTransform"), "-webkit-transform");
1778 assert_eq!(style_property_name("cssFloat"), "float");
1779 assert_eq!(style_property_name("--accent"), "--accent");
1780 }
1781
1782 #[test]
1783 fn get_attribute_reads_dom() {
1784 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello" data-x="v"></div>"#);
1785 runtime.run_script(
1786 r#"const el = document.getElementById("hello"); if (el.getAttribute("data-x") !== "v") { throw new Error("mismatch"); }"#,
1787 );
1788 }
1789
1790 #[test]
1791 fn missing_id_returns_null() {
1792 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello"></div>"#);
1793 runtime.run_script(
1794 r#"const el = document.getElementById("missing"); if (el !== null) { throw new Error("expected null"); }"#,
1795 );
1796 }
1797
1798 #[test]
1799 fn console_log_does_not_panic() {
1800 let (mut runtime, _dom) = runtime_from_html(r#"<html></html>"#);
1801 runtime.run_script(
1802 r#"console.log("a", 1, undefined); console.warn("w"); console.error("e");"#,
1803 );
1804 }
1805
1806 #[test]
1807 fn syntax_error_is_logged_not_panicked() {
1808 let (mut runtime, _dom) = runtime_from_html(r#"<html></html>"#);
1809 runtime.run_script("this is not valid js ((");
1810 }
1811
1812 #[test]
1813 fn accessor_reads_text_content() {
1814 let (mut runtime, _dom) = runtime_from_html(r#"<div id="hello">hi</div>"#);
1815 runtime.run_script(
1816 r#"const el = document.getElementById("hello"); if (el.textContent !== "hi") { throw new Error("mismatch"); }"#,
1817 );
1818 }
1819
1820 #[test]
1821 fn click_invokes_onclick_and_mutates_dom() {
1822 let (mut runtime, dom) =
1823 runtime_from_html(r#"<button id="b">click me</button><p id="result">not clicked</p>"#);
1824 runtime.run_script(
1825 r#"
1826 const button = document.getElementById("b");
1827 const result = document.getElementById("result");
1828 button.onclick = function () {
1829 result.textContent = "clicked!";
1830 result.setAttribute("data-clicked", "true");
1831 };
1832 "#,
1833 );
1834
1835 let button = dom.get_element_by_id("b").unwrap();
1836 assert!(runtime.click(&button));
1837 assert!(runtime.needs_redraw());
1838 assert!(runtime.take_needs_redraw());
1839
1840 let result = dom.get_element_by_id("result").unwrap();
1841 assert_eq!(DomTree::inner_text(&result), "clicked!");
1842 assert_eq!(result.borrow().value.get_attr("data-clicked"), Some("true"));
1843 }
1844
1845 #[test]
1846 fn click_without_handler_is_noop() {
1847 let (mut runtime, dom) = runtime_from_html(r#"<div id="x"></div>"#);
1848 runtime.run_script(r#"document.getElementById("x");"#);
1849 let node = dom.get_element_by_id("x").unwrap();
1850 assert!(!runtime.click(&node));
1851 assert!(!runtime.needs_redraw());
1852 }
1853
1854 #[test]
1855 fn click_invokes_element_event_listeners_in_registration_order() {
1856 let (mut runtime, dom) =
1857 runtime_from_html(r#"<button id="button">click</button><div id="result"></div>"#);
1858 runtime.run_script(
1859 r#"
1860 const button = document.getElementById("button");
1861 const result = document.getElementById("result");
1862 let order = "";
1863 button.addEventListener("click", function (event) {
1864 order = order + "a";
1865 result.setAttribute("data-event-type", event.type);
1866 });
1867 button.addEventListener("click", function () {
1868 order = order + "b";
1869 result.setAttribute("data-order", order);
1870 });
1871 "#,
1872 );
1873
1874 let button = dom.get_element_by_id("button").unwrap();
1875 assert!(runtime.click(&button));
1876
1877 let result = dom.get_element_by_id("result").unwrap();
1878 assert_eq!(result.borrow().value.get_attr("data-order"), Some("ab"));
1879 assert_eq!(
1880 result.borrow().value.get_attr("data-event-type"),
1881 Some("click")
1882 );
1883 assert!(runtime.needs_redraw());
1884 }
1885
1886 #[test]
1887 fn event_listeners_are_deduplicated_and_removable() {
1888 let (mut runtime, dom) =
1889 runtime_from_html(r#"<button id="button">click</button><div id="result"></div>"#);
1890 runtime.run_script(
1891 r#"
1892 const button = document.getElementById("button");
1893 const result = document.getElementById("result");
1894 function listener() {
1895 const count = result.getAttribute("data-count");
1896 result.setAttribute("data-count", count === null ? 1 : Number(count) + 1);
1897 }
1898 button.addEventListener("click", listener);
1899 button.addEventListener("click", listener);
1900 "#,
1901 );
1902
1903 let button = dom.get_element_by_id("button").unwrap();
1904 assert!(runtime.click(&button));
1905 let result = dom.get_element_by_id("result").unwrap();
1906 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
1907
1908 runtime.run_script(
1909 r#"
1910 button.removeEventListener("click", listener);
1911 window.addEventListener("test", listener);
1912 window.removeEventListener("test", listener);
1913 "#,
1914 );
1915 assert!(!runtime.click(&button));
1916 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
1917 }
1918
1919 #[test]
1920 fn document_event_listeners_can_be_removed() {
1921 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
1922 runtime.run_script(
1923 r#"
1924 const result = document.getElementById("result");
1925 function listener() { result.setAttribute("data-ran", "yes"); }
1926 document.addEventListener("DOMContentLoaded", listener);
1927 document.removeEventListener("DOMContentLoaded", listener);
1928 "#,
1929 );
1930
1931 assert!(runtime.dispatch_dom_content_loaded());
1932 let result = dom.get_element_by_id("result").unwrap();
1933 assert_eq!(result.borrow().value.get_attr("data-ran"), None);
1934 }
1935
1936 #[test]
1937 fn click_bubbles_to_delegated_ancestor_listeners() {
1938 let (mut runtime, dom) = runtime_from_html(
1939 r#"<main id="root"><button id="button">click</button></main><div id="result"></div>"#,
1940 );
1941 runtime.run_script(
1942 r#"
1943 const root = document.getElementById("root");
1944 const result = document.getElementById("result");
1945 root.addEventListener("click", function (event) {
1946 result.setAttribute("data-target", event.target.id);
1947 result.setAttribute("data-current", event.currentTarget.id);
1948 result.setAttribute("data-this", this.id);
1949 event.preventDefault();
1950 result.setAttribute("data-prevented", event.defaultPrevented);
1951 });
1952 "#,
1953 );
1954
1955 let button = dom.get_element_by_id("button").unwrap();
1956 assert!(runtime.click(&button));
1957 let result = dom.get_element_by_id("result").unwrap();
1958 let result = result.borrow();
1959 assert_eq!(result.value.get_attr("data-target"), Some("button"));
1960 assert_eq!(result.value.get_attr("data-current"), Some("root"));
1961 assert_eq!(result.value.get_attr("data-this"), Some("root"));
1962 assert_eq!(result.value.get_attr("data-prevented"), Some("true"));
1963 }
1964
1965 #[test]
1966 fn click_propagation_can_be_stopped() {
1967 let (mut runtime, dom) = runtime_from_html(
1968 r#"<main id="root"><button id="button">click</button></main><div id="result"></div>"#,
1969 );
1970 runtime.run_script(
1971 r#"
1972 const root = document.getElementById("root");
1973 const button = document.getElementById("button");
1974 const result = document.getElementById("result");
1975 button.addEventListener("click", function (event) {
1976 result.setAttribute("data-child", "ran");
1977 event.stopPropagation();
1978 });
1979 root.addEventListener("click", function () {
1980 result.setAttribute("data-root", "ran");
1981 });
1982 "#,
1983 );
1984
1985 let button = dom.get_element_by_id("button").unwrap();
1986 assert!(runtime.click(&button));
1987 let result = dom.get_element_by_id("result").unwrap();
1988 let result = result.borrow();
1989 assert_eq!(result.value.get_attr("data-child"), Some("ran"));
1990 assert_eq!(result.value.get_attr("data-root"), None);
1991 }
1992
1993 #[test]
1994 fn click_does_not_invoke_other_event_types() {
1995 let (mut runtime, dom) = runtime_from_html(r#"<button id="button">click</button>"#);
1996 runtime.run_script(
1997 r#"
1998 const button = document.getElementById("button");
1999 button.addEventListener("mouseover", function () {
2000 button.setAttribute("data-ran", "yes");
2001 });
2002 "#,
2003 );
2004
2005 let button = dom.get_element_by_id("button").unwrap();
2006 assert!(!runtime.click(&button));
2007 assert_eq!(button.borrow().value.get_attr("data-ran"), None);
2008 }
2009
2010 #[test]
2011 fn scroll_invokes_onscroll_and_mutates_dom() {
2012 let (mut runtime, dom) =
2013 runtime_from_html(r#"<div id="s">scroll</div><p id="result">not scrolled</p>"#);
2014 runtime.run_script(
2015 r#"
2016 const s = document.getElementById("s");
2017 const result = document.getElementById("result");
2018 window.__scrolls = 0;
2019 s.onscroll = function () {
2020 window.__scrolls = (window.__scrolls || 0) + 1;
2021 result.textContent = "scrolled!";
2022 };
2023 "#,
2024 );
2025
2026 let s = dom.get_element_by_id("s").unwrap();
2027 assert!(runtime.scroll(&s));
2028 let result = dom.get_element_by_id("result").unwrap();
2029 assert_eq!(DomTree::inner_text(&result), "scrolled!");
2030 }
2031
2032 #[test]
2033 fn scroll_without_handler_is_noop() {
2034 let (mut runtime, dom) = runtime_from_html(r#"<div id="x"></div>"#);
2035 runtime.run_script(r#"document.getElementById("x");"#);
2036 let node = dom.get_element_by_id("x").unwrap();
2037 assert!(!runtime.scroll(&node));
2038 }
2039
2040 #[test]
2041 fn scroll_invokes_element_listeners_in_registration_order() {
2042 let (mut runtime, dom) =
2043 runtime_from_html(r#"<div id="s">scroll</div><div id="result"></div>"#);
2044 runtime.run_script(
2045 r#"
2046 const s = document.getElementById("s");
2047 const result = document.getElementById("result");
2048 let order = "";
2049 s.addEventListener("scroll", function (event) {
2050 order = order + "a";
2051 result.setAttribute("data-event-type", event.type);
2052 });
2053 s.addEventListener("scroll", function () {
2054 order = order + "b";
2055 result.setAttribute("data-order", order);
2056 });
2057 "#,
2058 );
2059
2060 let s = dom.get_element_by_id("s").unwrap();
2061 assert!(runtime.scroll(&s));
2062
2063 let result = dom.get_element_by_id("result").unwrap();
2064 assert_eq!(result.borrow().value.get_attr("data-order"), Some("ab"));
2065 assert_eq!(
2066 result.borrow().value.get_attr("data-event-type"),
2067 Some("scroll")
2068 );
2069 }
2070
2071 #[test]
2072 fn scroll_bubbles_to_delegated_ancestor_listeners() {
2073 let (mut runtime, dom) = runtime_from_html(
2074 r#"<main id="root"><div id="s">scroll</div></main><div id="result"></div>"#,
2075 );
2076 runtime.run_script(
2077 r#"
2078 const root = document.getElementById("root");
2079 const result = document.getElementById("result");
2080 root.addEventListener("scroll", function (event) {
2081 result.setAttribute("data-target", event.target.id);
2082 });
2083 "#,
2084 );
2085
2086 let s = dom.get_element_by_id("s").unwrap();
2087 assert!(runtime.scroll(&s));
2088
2089 let result = dom.get_element_by_id("result").unwrap();
2090 assert_eq!(result.borrow().value.get_attr("data-target"), Some("s"));
2091 }
2092
2093 #[test]
2094 fn scroll_dom_id_dispatches_to_the_named_element() {
2095 let (mut runtime, dom) = runtime_from_html(r#"<div id="s">scroll</div>"#);
2096 runtime.run_script(
2097 r#"
2098 const s = document.getElementById("s");
2099 s.addEventListener("scroll", function () {
2100 s.setAttribute("data-ran", "yes");
2101 });
2102 "#,
2103 );
2104 let s = dom.get_element_by_id("s").unwrap();
2105 let dom_id = with_host(runtime.engine.vm(), |host| {
2108 host.refs
2109 .iter()
2110 .find(|(_, weak)| {
2111 weak.upgrade()
2112 .is_some_and(|n| Rc::as_ptr(&n) == Rc::as_ptr(&s))
2113 })
2114 .map(|(id, _)| *id)
2115 })
2116 .flatten()
2117 .expect("element must be registered in the host refs");
2118 assert!(runtime.scroll_dom_id(dom_id));
2119 assert_eq!(s.borrow().value.get_attr("data-ran"), Some("yes"));
2120 }
2121
2122 #[test]
2123 fn scroll_does_not_invoke_other_event_types() {
2124 let (mut runtime, dom) = runtime_from_html(r#"<div id="s">scroll</div>"#);
2125 runtime.run_script(
2126 r#"
2127 const s = document.getElementById("s");
2128 s.addEventListener("mouseover", function () {
2129 s.setAttribute("data-ran", "yes");
2130 });
2131 "#,
2132 );
2133
2134 let s = dom.get_element_by_id("s").unwrap();
2135 assert!(!runtime.scroll(&s));
2136 assert_eq!(s.borrow().value.get_attr("data-ran"), None);
2137 }
2138
2139 #[test]
2140 fn get_element_by_id_reuses_the_same_object() {
2141 let (mut runtime, _dom) = runtime_from_html(r#"<div id="x"></div>"#);
2142 runtime.run_script(
2143 r#"
2144 const a = document.getElementById("x");
2145 const b = document.getElementById("x");
2146 a.onclick = function () {};
2147 if (a !== b) { throw new Error("expected the same object"); }
2148 "#,
2149 );
2150 }
2151
2152 #[test]
2153 fn dom_content_loaded_listener_runs_when_dispatched() {
2154 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2155 runtime.run_script(
2156 r#"
2157 document.addEventListener("DOMContentLoaded", function (event) {
2158 const result = document.getElementById("result");
2159 result.setAttribute("data-ready", "yes");
2160 result.setAttribute("data-event-type", event.type);
2161 });
2162 "#,
2163 );
2164
2165 let result = dom.get_element_by_id("result").unwrap();
2166 assert_eq!(result.borrow().value.get_attr("data-ready"), None);
2167 assert!(runtime.dispatch_dom_content_loaded());
2168 assert_eq!(result.borrow().value.get_attr("data-ready"), Some("yes"));
2169 assert_eq!(
2170 result.borrow().value.get_attr("data-event-type"),
2171 Some("DOMContentLoaded")
2172 );
2173 assert!(runtime.needs_redraw());
2174 }
2175
2176 #[test]
2177 fn dom_content_loaded_is_dispatched_only_once() {
2178 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2179 runtime.run_script(
2180 r#"
2181 let dispatchCount = 0;
2182 document.addEventListener("DOMContentLoaded", function () {
2183 dispatchCount = dispatchCount + 1;
2184 document.getElementById("result").setAttribute("data-count", dispatchCount);
2185 });
2186 "#,
2187 );
2188
2189 assert!(runtime.dispatch_dom_content_loaded());
2190 assert!(!runtime.dispatch_dom_content_loaded());
2191 let result = dom.get_element_by_id("result").unwrap();
2192 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
2193 }
2194
2195 #[test]
2196 fn window_onload_runs_when_dispatched() {
2197 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2198 runtime.run_script(
2199 r#"
2200 window.onload = function (event) {
2201 const result = document.getElementById("result");
2202 result.setAttribute("data-ready", "yes");
2203 result.setAttribute("data-event-type", event.type);
2204 };
2205 "#,
2206 );
2207
2208 let result = dom.get_element_by_id("result").unwrap();
2209 assert_eq!(result.borrow().value.get_attr("data-ready"), None);
2210 assert!(runtime.dispatch_window_load());
2211 assert_eq!(result.borrow().value.get_attr("data-ready"), Some("yes"));
2212 assert_eq!(
2213 result.borrow().value.get_attr("data-event-type"),
2214 Some("load")
2215 );
2216 }
2217
2218 #[test]
2219 fn window_load_listener_runs_when_dispatched_and_only_once() {
2220 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2221 runtime.run_script(
2222 r#"
2223 let dispatchCount = 0;
2224 window.addEventListener("load", function () {
2225 dispatchCount = dispatchCount + 1;
2226 document.getElementById("result").setAttribute("data-count", dispatchCount);
2227 });
2228 "#,
2229 );
2230
2231 assert!(runtime.dispatch_window_load());
2232 assert!(!runtime.dispatch_window_load());
2233 let result = dom.get_element_by_id("result").unwrap();
2234 assert_eq!(result.borrow().value.get_attr("data-count"), Some("1"));
2235 }
2236
2237 #[test]
2238 fn body_onload_registered_at_setup_runs_on_dispatch() {
2239 let (mut runtime, dom) = runtime_from_html(
2240 r#"<body onload="document.getElementById('result').setAttribute('data-onload', 'yes')"><div id="result"></div></body>"#,
2241 );
2242
2243 let result = dom.get_element_by_id("result").unwrap();
2244 assert_eq!(result.borrow().value.get_attr("data-onload"), None);
2245 assert!(runtime.dispatch_window_load());
2246 assert_eq!(result.borrow().value.get_attr("data-onload"), Some("yes"));
2247 assert!(!runtime.dispatch_window_load());
2249 }
2250
2251 #[test]
2252 fn document_query_selector_and_query_selector_all_expose_elements() {
2253 let (mut runtime, dom) = runtime_from_html(
2254 r#"
2255 <div id="result"></div>
2256 <main><p class="item">first</p><p class="item featured">second</p></main>
2257 "#,
2258 );
2259 runtime.run_script(
2260 r#"
2261 const featured = document.querySelector("main > p.featured");
2262 featured.setAttribute("data-selected", "yes");
2263 const items = document.querySelectorAll("p.item");
2264 const classified = document.querySelectorAll("[class]");
2265 items[0].setAttribute("data-first", "yes");
2266 items.forEach(function (item, index) {
2267 item.setAttribute("data-index", index);
2268 });
2269 document.getElementById("result").setAttribute("data-count", items.length);
2270 document.getElementById("result").setAttribute("data-class-count", classified.length);
2271 "#,
2272 );
2273
2274 let items = dom.get_elements_by_class_name("item");
2275 assert_eq!(items[0].borrow().value.get_attr("data-first"), Some("yes"));
2276 assert_eq!(items[0].borrow().value.get_attr("data-index"), Some("0"));
2277 assert_eq!(items[1].borrow().value.get_attr("data-index"), Some("1"));
2278 assert_eq!(
2279 items[1].borrow().value.get_attr("data-selected"),
2280 Some("yes")
2281 );
2282 let result = dom.get_element_by_id("result").unwrap();
2283 assert_eq!(result.borrow().value.get_attr("data-count"), Some("2"));
2284 assert_eq!(
2285 result.borrow().value.get_attr("data-class-count"),
2286 Some("2")
2287 );
2288 }
2289
2290 #[test]
2291 fn element_query_selectors_are_scoped_to_descendants() {
2292 let (mut runtime, dom) = runtime_from_html(
2293 r#"
2294 <section id="scope"><span class="item">one</span><span class="item">two</span></section>
2295 <span class="item" id="outside">outside</span>
2296 "#,
2297 );
2298 runtime.run_script(
2299 r##"
2300 const scope = document.querySelector("#scope");
2301 scope.querySelector(".item").setAttribute("data-first", "yes");
2302 const items = scope.querySelectorAll(".item");
2303 items[1].setAttribute("data-second", "yes");
2304 scope.setAttribute("data-count", items.length);
2305 "##,
2306 );
2307
2308 let scope = dom.get_element_by_id("scope").unwrap();
2309 assert_eq!(scope.borrow().value.get_attr("data-count"), Some("2"));
2310 let items = DomTree::query_selector_all_within(&scope, ".item");
2311 assert_eq!(items[0].borrow().value.get_attr("data-first"), Some("yes"));
2312 assert_eq!(items[1].borrow().value.get_attr("data-second"), Some("yes"));
2313 let outside = dom.get_element_by_id("outside").unwrap();
2314 assert_eq!(outside.borrow().value.get_attr("data-first"), None);
2315 assert_eq!(outside.borrow().value.get_attr("data-second"), None);
2316 }
2317
2318 #[test]
2319 fn react_dom_collection_and_event_apis_are_available() {
2320 let (mut runtime, dom) = runtime_from_html(
2321 r#"
2322 <main id="root">
2323 <button class="control primary">one</button>
2324 <button class="control">two</button>
2325 <span class="primary">label</span>
2326 </main>
2327 <div id="result"></div>
2328 "#,
2329 );
2330 runtime.run_script(
2331 r#"
2332 const root = document.getElementById("root");
2333 const button = root.getElementsByTagName("button")[0];
2334 let received = "no";
2335 button.addEventListener("scratch-ready", function (event) {
2336 received = event.detail + ":" + (event.target === button);
2337 event.preventDefault();
2338 });
2339 const accepted = button.dispatchEvent(new CustomEvent(
2340 "scratch-ready", {detail: "yes", cancelable: true}
2341 ));
2342 document.getElementById("result").setAttribute(
2343 "data-dom-apis",
2344 document.getElementsByTagName("button").length + ":" +
2345 document.getElementsByClassName("control primary").length + ":" +
2346 root.getElementsByClassName("primary").length + ":" + received + ":" + accepted
2347 );
2348 "#,
2349 );
2350
2351 let result = dom.get_element_by_id("result").unwrap();
2352 assert_eq!(
2353 result.borrow().value.get_attr("data-dom-apis"),
2354 Some("2:1:2:yes:true:false")
2355 );
2356 }
2357
2358 #[test]
2359 fn create_and_append_element_and_text_nodes() {
2360 let (mut runtime, dom) = runtime_from_html(r#"<ul id="list"></ul>"#);
2361 runtime.run_script(
2362 r##"
2363 const item = document.createElement("li");
2364 item.setAttribute("class", "dynamic");
2365 const text = document.createTextNode("created by JavaScript");
2366 item.appendChild(text);
2367 document.querySelector("#list").appendChild(item);
2368 "##,
2369 );
2370
2371 let item = dom.query_selector("li.dynamic").unwrap();
2372 assert_eq!(DomTree::inner_text(&item), "created by JavaScript");
2373 assert!(runtime.needs_redraw());
2374 }
2375
2376 #[test]
2377 fn document_head_and_element_append_insert_dynamic_styles() {
2378 let (mut runtime, dom) = runtime_from_html(r#"<html><head></head><body></body></html>"#);
2379 runtime.run_script(
2380 r#"
2381 const style = document.createElement("style");
2382 style.append("body { color: red; }");
2383 document.head.append(style);
2384 "#,
2385 );
2386
2387 let style = dom.query_selector("head style").unwrap();
2388 assert_eq!(DomTree::inner_text(&style), "body { color: red; }");
2389 assert!(runtime.needs_redraw());
2390 }
2391
2392 #[test]
2393 fn namespace_dom_apis_create_svg_elements_and_attributes() {
2394 let (mut runtime, dom) =
2395 runtime_from_html(r#"<main id="root"></main><div id="result"></div>"#);
2396 runtime.run_script(
2397 r##"
2398 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2399 const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2400 path.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#shape");
2401 svg.appendChild(path);
2402 document.querySelector("#root").appendChild(svg);
2403
2404 const result = document.querySelector("#result");
2405 result.setAttribute("data-svg-ns", svg.namespaceURI);
2406 result.setAttribute("data-path-ns", path.namespaceURI);
2407 result.setAttribute("data-html-ns", result.namespaceURI);
2408 "##,
2409 );
2410
2411 let path = dom.query_selector("path").unwrap();
2412 assert_eq!(path.borrow().value.get_attr("xlink:href"), Some("#shape"));
2413 let result = dom.get_element_by_id("result").unwrap();
2414 let result = result.borrow();
2415 assert_eq!(result.value.get_attr("data-svg-ns"), Some(SVG_NAMESPACE));
2416 assert_eq!(result.value.get_attr("data-path-ns"), Some(SVG_NAMESPACE));
2417 assert_eq!(result.value.get_attr("data-html-ns"), Some(HTML_NAMESPACE));
2418 assert!(runtime.needs_redraw());
2419 }
2420
2421 #[test]
2422 fn element_contains_checks_self_descendants_and_unrelated_nodes() {
2423 let (mut runtime, dom) = runtime_from_html(
2424 r#"<main id="root"><section id="child"><span id="nested"></span></section></main><aside id="other"></aside>"#,
2425 );
2426 runtime.run_script(
2427 r##"
2428 const root = document.querySelector("#root");
2429 const child = document.querySelector("#child");
2430 const nested = document.querySelector("#nested");
2431 const other = document.querySelector("#other");
2432 root.setAttribute("data-self", root.contains(root));
2433 root.setAttribute("data-child", root.contains(child));
2434 root.setAttribute("data-nested", root.contains(nested));
2435 root.setAttribute("data-other", root.contains(other));
2436 root.setAttribute("data-null", root.contains(null));
2437 "##,
2438 );
2439
2440 let root = dom.get_element_by_id("root").unwrap();
2441 let root = root.borrow();
2442 assert_eq!(root.value.get_attr("data-self"), Some("true"));
2443 assert_eq!(root.value.get_attr("data-child"), Some("true"));
2444 assert_eq!(root.value.get_attr("data-nested"), Some("true"));
2445 assert_eq!(root.value.get_attr("data-other"), Some("false"));
2446 assert_eq!(root.value.get_attr("data-null"), Some("false"));
2447 }
2448
2449 #[test]
2450 fn document_tracks_the_focused_element() {
2451 let (mut runtime, dom) = runtime_from_html(
2452 r#"<body><input id="field"><button id="other"></button><div id="result"></div></body>"#,
2453 );
2454 runtime.run_script(
2455 r##"
2456 const field = document.querySelector("#field");
2457 const other = document.querySelector("#other");
2458 const result = document.querySelector("#result");
2459 result.setAttribute("data-initial", document.activeElement === document.body);
2460 field.focus();
2461 result.setAttribute("data-field", document.activeElement === field);
2462 other.focus();
2463 result.setAttribute("data-other", document.activeElement === other);
2464 other.blur();
2465 result.setAttribute("data-blurred", document.activeElement === document.body);
2466 result.setAttribute("data-has-focus", document.hasFocus());
2467 "##,
2468 );
2469
2470 let result = dom.get_element_by_id("result").unwrap();
2471 let result = result.borrow();
2472 assert_eq!(result.value.get_attr("data-initial"), Some("true"));
2473 assert_eq!(result.value.get_attr("data-field"), Some("true"));
2474 assert_eq!(result.value.get_attr("data-other"), Some("true"));
2475 assert_eq!(result.value.get_attr("data-blurred"), Some("true"));
2476 assert_eq!(result.value.get_attr("data-has-focus"), Some("true"));
2477 }
2478
2479 #[test]
2480 fn react_dom_node_primitives_identify_and_reorder_nodes() {
2481 let (mut runtime, dom) =
2482 runtime_from_html(r#"<main id="root"></main><div id="result"></div>"#);
2483 runtime.run_script(
2484 r##"
2485 const root = document.querySelector("#root");
2486 const first = document.createElement("span");
2487 first.setAttribute("data-name", "first");
2488 const second = document.createElement("span");
2489 second.setAttribute("data-name", "second");
2490 const text = document.createTextNode("before");
2491 text.nodeValue = "after";
2492 second.appendChild(text);
2493 root.appendChild(first);
2494 root.insertBefore(second, first);
2495 root.removeChild(first);
2496 second.className = "react-node";
2497 second.setAttribute("data-remove", "yes");
2498 second.removeAttribute("data-remove");
2499
2500 const result = document.querySelector("#result");
2501 result.setAttribute("data-document-type", document.nodeType);
2502 result.setAttribute("data-root-type", root.nodeType);
2503 result.setAttribute("data-root-name", root.nodeName);
2504 result.setAttribute("data-owner", root.ownerDocument === document);
2505 result.setAttribute("data-first", root.firstChild.getAttribute("data-name"));
2506 result.setAttribute("data-last", root.lastChild.getAttribute("data-name"));
2507 result.setAttribute("data-count", root.childNodes.length);
2508 result.setAttribute("data-text", root.firstChild.firstChild.data);
2509 result.setAttribute("data-class", root.firstChild.className);
2510 result.setAttribute("data-removed", root.firstChild.hasAttribute("data-remove"));
2511 "##,
2512 );
2513
2514 let result = dom.get_element_by_id("result").unwrap();
2515 let result = result.borrow();
2516 assert_eq!(result.value.get_attr("data-document-type"), Some("9"));
2517 assert_eq!(result.value.get_attr("data-root-type"), Some("1"));
2518 assert_eq!(result.value.get_attr("data-root-name"), Some("MAIN"));
2519 assert_eq!(result.value.get_attr("data-owner"), Some("true"));
2520 assert_eq!(result.value.get_attr("data-first"), Some("second"));
2521 assert_eq!(result.value.get_attr("data-last"), Some("second"));
2522 assert_eq!(result.value.get_attr("data-count"), Some("1"));
2523 assert_eq!(result.value.get_attr("data-text"), Some("after"));
2524 assert_eq!(result.value.get_attr("data-class"), Some("react-node"));
2525 assert_eq!(result.value.get_attr("data-removed"), Some("false"));
2526
2527 let root = dom.get_element_by_id("root").unwrap();
2528 assert_eq!(root.borrow().children().len(), 1);
2529 assert_eq!(
2530 root.borrow().children()[0]
2531 .borrow()
2532 .value
2533 .get_attr("data-remove"),
2534 None
2535 );
2536 }
2537
2538 #[test]
2539 fn html_iframe_element_supports_host_instance_checks() {
2540 let (mut runtime, dom) = runtime_from_html(
2541 r#"<body><iframe id="frame"></iframe><div id="result"></div></body>"#,
2542 );
2543 runtime.run_script(
2544 r#"
2545 const frame = document.getElementById("frame");
2546 const result = document.getElementById("result");
2547 result.setAttribute("data-frame", frame instanceof HTMLIFrameElement);
2548 result.setAttribute("data-body", document.body instanceof HTMLIFrameElement);
2549 "#,
2550 );
2551
2552 let result = dom.get_element_by_id("result").unwrap();
2553 let result = result.borrow();
2554 assert_eq!(result.value.get_attr("data-frame"), Some("true"));
2555 assert_eq!(result.value.get_attr("data-body"), Some("false"));
2556 }
2557
2558 #[test]
2559 fn markup_declared_iframes_queue_loads_once_and_failures_are_not_retried() {
2560 let (mut runtime, dom) = runtime_from_html(
2561 r#"<html><body>
2562 <iframe src="https://example.test/frame-a.html"></iframe>
2563 <iframe src="frames/frame-b.html"></iframe>
2564 <iframe id="placeholder"></iframe>
2565 </body></html>"#,
2566 );
2567 runtime.set_document_url("https://example.test/dir/page.html");
2568
2569 let ids: HashMap<usize, u64> = {
2572 let mut ids = HashMap::new();
2573 let mut next_id = 1u64;
2574 dom.traverse(|node| {
2575 ids.insert(Rc::as_ptr(node) as usize, next_id);
2576 next_id += 1;
2577 });
2578 ids
2579 };
2580 let snapshot = DomSnapshot::from_mirror(&dom.root, &ids);
2581 runtime.apply_dom(&snapshot);
2582
2583 assert_eq!(runtime.queue_markup_iframe_loads(), 2);
2586 let requests = runtime.take_iframe_fetch_requests();
2587 assert_eq!(requests.len(), 2);
2588 let by_url: HashMap<String, u64> = requests
2589 .into_iter()
2590 .map(|req| (req.url, req.dom_id))
2591 .collect();
2592 assert_eq!(
2593 by_url.len(),
2594 2,
2595 "one request per src-ified iframe, absolute or relative"
2596 );
2597 assert!(by_url.contains_key("https://example.test/frame-a.html"));
2599 assert!(by_url.contains_key("https://example.test/dir/frames/frame-b.html"));
2600 let frame_a = by_url["https://example.test/frame-a.html"];
2601 let frame_b = by_url["https://example.test/dir/frames/frame-b.html"];
2602
2603 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
2605 assert!(runtime.take_iframe_fetch_requests().is_empty());
2606
2607 runtime.resolve_iframe_fetch(
2609 frame_a,
2610 r#"<html><body><p>frame content</p></body></html>"#.to_string(),
2611 );
2612 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
2613 assert!(runtime.take_iframe_fetch_requests().is_empty());
2614
2615 runtime.reject_iframe_fetch(frame_b);
2617 assert_eq!(runtime.queue_markup_iframe_loads(), 0);
2618 assert!(runtime.take_iframe_fetch_requests().is_empty());
2619 }
2620
2621 #[test]
2622 fn remove_detaches_node_but_keeps_it_available_for_reappend() {
2623 let (mut runtime, dom) = runtime_from_html(
2624 r#"<div id="first"><span id="moving">move</span></div><div id="second"></div>"#,
2625 );
2626 runtime.run_script(
2627 r##"
2628 const moving = document.querySelector("#moving");
2629 moving.remove();
2630 document.querySelector("#second").appendChild(moving);
2631 "##,
2632 );
2633
2634 let first = dom.get_element_by_id("first").unwrap();
2635 let second = dom.get_element_by_id("second").unwrap();
2636 assert!(DomTree::query_selector_within(&first, "#moving").is_none());
2637 assert!(DomTree::query_selector_within(&second, "#moving").is_some());
2638 assert!(runtime.needs_redraw());
2639 }
2640
2641 #[test]
2642 fn parent_node_and_children_expose_tree_relationships() {
2643 let (mut runtime, dom) = runtime_from_html(
2644 r#"<div id="parent">text<span id="first"></span><span id="second"></span></div>"#,
2645 );
2646 runtime.run_script(
2647 r##"
2648 const first = document.querySelector("#first");
2649 first.parentNode.setAttribute("data-parent", "yes");
2650 const children = first.parentNode.children;
2651 children[1].setAttribute("data-second", "yes");
2652 first.parentNode.setAttribute("data-child-count", children.length);
2653
2654 const text = document.createTextNode("dynamic");
2655 first.appendChild(text);
2656 text.parentNode.setAttribute("data-text-parent", "yes");
2657 "##,
2658 );
2659
2660 let parent = dom.get_element_by_id("parent").unwrap();
2661 assert_eq!(parent.borrow().value.get_attr("data-parent"), Some("yes"));
2662 assert_eq!(
2663 parent.borrow().value.get_attr("data-child-count"),
2664 Some("2")
2665 );
2666 let first = dom.get_element_by_id("first").unwrap();
2667 assert_eq!(
2668 first.borrow().value.get_attr("data-text-parent"),
2669 Some("yes")
2670 );
2671 let second = dom.get_element_by_id("second").unwrap();
2672 assert_eq!(second.borrow().value.get_attr("data-second"), Some("yes"));
2673 }
2674
2675 #[test]
2676 fn class_list_mutates_class_attribute_and_reports_membership() {
2677 let (mut runtime, dom) = runtime_from_html(r#"<div id="target" class="one two"></div>"#);
2678 runtime.run_script(
2679 r##"
2680 const target = document.querySelector("#target");
2681 let initial = "";
2682 for (const token of target.classList) initial += token + ",";
2683 target.setAttribute("data-initial-classes", initial);
2684 target.classList.add("two", "three");
2685 target.classList.remove("one", "missing");
2686 target.setAttribute("data-has-three", target.classList.contains("three"));
2687 target.setAttribute("data-removed-three", target.classList.toggle("three"));
2688 target.setAttribute("data-added-four", target.classList.toggle("four"));
2689 target.setAttribute("data-forced-off", target.classList.toggle("four", false));
2690 target.setAttribute("data-forced-on", target.classList.toggle("five", true));
2691 "##,
2692 );
2693
2694 let target = dom.get_element_by_id("target").unwrap();
2695 let target = target.borrow();
2696 assert_eq!(target.value.get_attr("class"), Some("two five"));
2697 assert_eq!(
2698 target.value.get_attr("data-initial-classes"),
2699 Some("one,two,")
2700 );
2701 assert_eq!(target.value.get_attr("data-has-three"), Some("true"));
2702 assert_eq!(target.value.get_attr("data-removed-three"), Some("false"));
2703 assert_eq!(target.value.get_attr("data-added-four"), Some("true"));
2704 assert_eq!(target.value.get_attr("data-forced-off"), Some("false"));
2705 assert_eq!(target.value.get_attr("data-forced-on"), Some("true"));
2706 assert!(runtime.needs_redraw());
2707 }
2708
2709 #[test]
2710 fn timeout_runs_once_with_additional_arguments_and_can_be_cancelled() {
2711 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2712 runtime.run_script(
2713 r##"
2714 setTimeout(function (value) {
2715 document.querySelector("#result").setAttribute("data-value", value);
2716 }, 0, "done");
2717 const cancelled = setTimeout(function () {
2718 document.querySelector("#result").setAttribute("data-cancelled", "no");
2719 }, 0);
2720 clearTimeout(cancelled);
2721 "##,
2722 );
2723
2724 assert!(runtime.run_due_timers());
2725 assert!(!runtime.run_due_timers());
2726 let result = dom.get_element_by_id("result").unwrap();
2727 assert_eq!(result.borrow().value.get_attr("data-value"), Some("done"));
2728 assert_eq!(result.borrow().value.get_attr("data-cancelled"), None);
2729 }
2730
2731 #[test]
2732 fn interval_can_clear_itself() {
2733 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2734 runtime.run_script(
2735 r##"
2736 const intervalId = setInterval(function () {
2737 document.querySelector("#result").setAttribute("data-ran", "once");
2738 clearInterval(intervalId);
2739 }, 0);
2740 "##,
2741 );
2742
2743 assert!(runtime.run_due_timers());
2744 assert!(!runtime.run_due_timers());
2745 let result = dom.get_element_by_id("result").unwrap();
2746 assert_eq!(result.borrow().value.get_attr("data-ran"), Some("once"));
2747 }
2748
2749 #[test]
2750 fn performance_now_exposes_monotonic_runtime_time() {
2751 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2752 runtime.run_script(
2753 r#"
2754 const first = performance.now();
2755 const second = performance.now();
2756 document.getElementById("result").setAttribute(
2757 "data-monotonic",
2758 typeof first === "number" && second >= first
2759 );
2760 "#,
2761 );
2762
2763 let result = dom.get_element_by_id("result").unwrap();
2764 assert_eq!(
2765 result.borrow().value.get_attr("data-monotonic"),
2766 Some("true")
2767 );
2768 }
2769
2770 #[test]
2771 fn microtasks_run_in_fifo_order_after_script_evaluation() {
2772 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2773 runtime.run_script(
2774 r##"
2775 const result = document.querySelector("#result");
2776 queueMicrotask(function () {
2777 result.setAttribute("data-order", result.getAttribute("data-order") + "-first");
2778 queueMicrotask(function () {
2779 result.setAttribute("data-order", result.getAttribute("data-order") + "-second");
2780 });
2781 });
2782 result.setAttribute("data-order", "sync");
2783 "##,
2784 );
2785
2786 let result = dom.get_element_by_id("result").unwrap();
2787 assert_eq!(
2788 result.borrow().value.get_attr("data-order"),
2789 Some("sync-first-second")
2790 );
2791 }
2792
2793 #[test]
2794 fn timer_microtasks_run_before_the_next_timer_task() {
2795 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2796 runtime.run_script(
2797 r##"
2798 const result = document.querySelector("#result");
2799 setTimeout(function () {
2800 result.setAttribute("data-order", "timer");
2801 queueMicrotask(function () {
2802 result.setAttribute("data-order", "timer-microtask");
2803 });
2804 }, 0);
2805 setTimeout(function () {
2806 result.setAttribute("data-observed", result.getAttribute("data-order"));
2807 }, 0);
2808 "##,
2809 );
2810
2811 assert!(runtime.run_due_timers());
2812 let result = dom.get_element_by_id("result").unwrap();
2813 assert_eq!(
2814 result.borrow().value.get_attr("data-observed"),
2815 Some("timer-microtask")
2816 );
2817 }
2818
2819 #[test]
2820 fn promise_reactions_share_fifo_order_with_queued_microtasks() {
2821 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2822 runtime.run_script(
2823 r##"
2824 const result = document.querySelector("#result");
2825 result.setAttribute("data-order", "sync");
2826 queueMicrotask(function () {
2827 result.setAttribute("data-order", result.getAttribute("data-order") + "-first");
2828 });
2829 new Promise(function (resolve) {
2830 resolve("promise");
2831 }).then(function (value) {
2832 result.setAttribute("data-order", result.getAttribute("data-order") + "-" + value);
2833 });
2834 queueMicrotask(function () {
2835 result.setAttribute("data-order", result.getAttribute("data-order") + "-last");
2836 });
2837 "##,
2838 );
2839
2840 let result = dom.get_element_by_id("result").unwrap();
2841 assert_eq!(
2842 result.borrow().value.get_attr("data-order"),
2843 Some("sync-first-promise-last")
2844 );
2845 }
2846
2847 #[test]
2848 fn a_failed_microtask_does_not_block_later_jobs() {
2849 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2850 runtime.run_script(
2851 r##"
2852 queueMicrotask(function () {
2853 missingFunction();
2854 });
2855 queueMicrotask(function () {
2856 document.querySelector("#result").setAttribute("data-ran", "yes");
2857 });
2858 "##,
2859 );
2860
2861 let result = dom.get_element_by_id("result").unwrap();
2862 assert_eq!(result.borrow().value.get_attr("data-ran"), Some("yes"));
2863 }
2864
2865 #[test]
2866 fn promise_static_methods_complete_during_script_checkpoint() {
2867 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2868 runtime.run_script(
2869 r##"
2870 const result = document.querySelector("#result");
2871 Promise.all([Promise.resolve("first"), "second"])
2872 .then(function (values) {
2873 result.setAttribute("data-all", values[0] + "-" + values[1]);
2874 return Promise.reject("expected");
2875 })
2876 .catch(function (reason) {
2877 result.setAttribute("data-catch", reason);
2878 });
2879 "##,
2880 );
2881
2882 let result = dom.get_element_by_id("result").unwrap();
2883 assert_eq!(
2884 result.borrow().value.get_attr("data-all"),
2885 Some("first-second")
2886 );
2887 assert_eq!(
2888 result.borrow().value.get_attr("data-catch"),
2889 Some("expected")
2890 );
2891 }
2892
2893 #[test]
2894 fn arrow_callbacks_work_with_promises_and_lexical_this() {
2895 let (mut runtime, dom) =
2896 runtime_from_html(r#"<button id="target"></button><div id="other"></div>"#);
2897 runtime.run_script(
2898 r##"
2899 const target = document.querySelector("#target");
2900 Promise.resolve("promise").then(value => {
2901 target.setAttribute("data-promise", value);
2902 });
2903 target.addEventListener("click", function () {
2904 const update = () => this.setAttribute("data-this", "target");
2905 update.call(document.querySelector("#other"));
2906 });
2907 "##,
2908 );
2909
2910 let target = dom.get_element_by_id("target").unwrap();
2911 assert!(runtime.click(&target));
2912 assert_eq!(
2913 target.borrow().value.get_attr("data-promise"),
2914 Some("promise")
2915 );
2916 assert_eq!(target.borrow().value.get_attr("data-this"), Some("target"));
2917 let other = dom.get_element_by_id("other").unwrap();
2918 assert_eq!(other.borrow().value.get_attr("data-this"), None);
2919 }
2920
2921 #[test]
2922 fn queue_microtask_accepts_an_arrow_callback() {
2923 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2924 runtime.run_script(
2925 r##"
2926 const result = document.querySelector("#result");
2927 queueMicrotask(() => {
2928 result.setAttribute("data-microtask", "yes");
2929 });
2930 "##,
2931 );
2932
2933 let result = dom.get_element_by_id("result").unwrap();
2934 assert_eq!(
2935 result.borrow().value.get_attr("data-microtask"),
2936 Some("yes")
2937 );
2938 }
2939
2940 #[test]
2941 fn browser_global_aliases_share_window_properties() {
2942 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2943 runtime.run_script(
2944 r##"
2945 const result = window.document.querySelector("#result");
2946 result.setAttribute("data-same-self", window === self);
2947 result.setAttribute("data-same-global", window === globalThis);
2948 result.setAttribute("data-document", window.document === document);
2949 window.queueMicrotask(() => {
2950 result.setAttribute("data-microtask", "yes");
2951 });
2952 "##,
2953 );
2954
2955 let result = dom.get_element_by_id("result").unwrap();
2956 let result = result.borrow();
2957 assert_eq!(result.value.get_attr("data-same-self"), Some("true"));
2958 assert_eq!(result.value.get_attr("data-same-global"), Some("true"));
2959 assert_eq!(result.value.get_attr("data-document"), Some("true"));
2960 assert_eq!(result.value.get_attr("data-microtask"), Some("yes"));
2961 }
2962
2963 #[test]
2964 fn fetch_resolves_response_metadata_and_text_promise() {
2965 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
2966 runtime.run_script(
2967 r##"
2968 fetch("data:text/plain,hello").then(response => {
2969 const result = document.querySelector("#result");
2970 result.setAttribute("data-ok", response.ok);
2971 result.setAttribute("data-status", response.status);
2972 result.setAttribute("data-status-text", response.statusText);
2973 result.setAttribute("data-url", response.url);
2974 result.setAttribute("data-redirected", response.redirected);
2975 result.setAttribute("data-body-used-before", response.bodyUsed);
2976 const body = response.text();
2977 result.setAttribute("data-body-used-after", response.bodyUsed);
2978 return body;
2979 }).then(text => {
2980 document.querySelector("#result").setAttribute("data-text", text);
2981 });
2982 "##,
2983 );
2984
2985 let requests = runtime.take_fetch_requests();
2986 assert_eq!(requests.len(), 1);
2987 assert_eq!(requests[0].url, "data:text/plain,hello");
2988 runtime.resolve_fetch(
2989 requests[0].id,
2990 JsFetchResponse {
2991 url: "data:text/plain,hello".to_string(),
2992 status: 200,
2993 status_text: "All Good".to_string(),
2994 redirected: true,
2995 body: b"hello".to_vec(),
2996 headers: Vec::new(),
2997 },
2998 );
2999
3000 let result = dom.get_element_by_id("result").unwrap();
3001 let result = result.borrow();
3002 assert_eq!(result.value.get_attr("data-ok"), Some("true"));
3003 assert_eq!(result.value.get_attr("data-status"), Some("200"));
3004 assert_eq!(result.value.get_attr("data-status-text"), Some("All Good"));
3005 assert_eq!(result.value.get_attr("data-redirected"), Some("true"));
3006 assert_eq!(
3007 result.value.get_attr("data-body-used-before"),
3008 Some("false")
3009 );
3010 assert_eq!(result.value.get_attr("data-body-used-after"), Some("true"));
3011 assert_eq!(
3012 result.value.get_attr("data-url"),
3013 Some("data:text/plain,hello")
3014 );
3015 assert_eq!(result.value.get_attr("data-text"), Some("hello"));
3016 }
3017
3018 #[test]
3019 fn fetch_array_buffer_preserves_binary_bytes() {
3020 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3021 runtime.run_script(
3022 r##"
3023 fetch("https://assets.scratch.mit.edu/project.sb3")
3024 .then(response => response.arrayBuffer())
3025 .then(buffer => {
3026 const bytes = new Uint8Array(buffer);
3027 document.querySelector("#result").setAttribute(
3028 "data-bytes",
3029 buffer.byteLength + ":" + bytes.length + ":" + bytes[0] + ":" + bytes[3]
3030 );
3031 });
3032 "##,
3033 );
3034
3035 let requests = runtime.take_fetch_requests();
3036 runtime.resolve_fetch(
3037 requests[0].id,
3038 JsFetchResponse {
3039 url: requests[0].url.clone(),
3040 status: 200,
3041 status_text: "OK".to_string(),
3042 redirected: false,
3043 body: vec![0, 127, 128, 255],
3044 headers: Vec::new(),
3045 },
3046 );
3047
3048 let result = dom.get_element_by_id("result").unwrap();
3049 assert_eq!(
3050 result.borrow().value.get_attr("data-bytes"),
3051 Some("4:4:0:255")
3052 );
3053 }
3054
3055 #[test]
3056 fn response_body_cannot_be_consumed_twice() {
3057 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3058 runtime.run_script(
3059 r##"
3060 fetch("data:text/plain,hello").then(response => {
3061 return response.text().then(() => response.text());
3062 }).catch(reason => {
3063 document.querySelector("#result").setAttribute("data-error", reason);
3064 });
3065 "##,
3066 );
3067
3068 let requests = runtime.take_fetch_requests();
3069 runtime.resolve_fetch(
3070 requests[0].id,
3071 JsFetchResponse {
3072 url: "data:text/plain,hello".to_string(),
3073 status: 200,
3074 status_text: "OK".to_string(),
3075 redirected: false,
3076 body: b"hello".to_vec(),
3077 headers: Vec::new(),
3078 },
3079 );
3080
3081 let result = dom.get_element_by_id("result").unwrap();
3082 assert_eq!(
3083 result.borrow().value.get_attr("data-error"),
3084 Some("Response body has already been consumed")
3085 );
3086 }
3087
3088 #[test]
3089 fn fetch_captures_method_headers_and_body() {
3090 let (mut runtime, _dom) = runtime_from_html("<div></div>");
3091 runtime.run_script(
3092 r#"
3093 const headers = {};
3094 headers["Content-Type"] = "application/json";
3095 headers["X-Test"] = "yes";
3096 fetch("https://example.test/messages", {
3097 method: "post",
3098 headers: headers,
3099 body: "{\"message\":\"hello\"}"
3100 });
3101 "#,
3102 );
3103
3104 let requests = runtime.take_fetch_requests();
3105 assert_eq!(requests.len(), 1);
3106 assert_eq!(requests[0].method, "POST");
3107 assert!(
3108 requests[0]
3109 .headers
3110 .contains(&("Content-Type".to_string(), "application/json".to_string()))
3111 );
3112 assert!(
3113 requests[0]
3114 .headers
3115 .contains(&("X-Test".to_string(), "yes".to_string()))
3116 );
3117 assert_eq!(requests[0].body, br#"{"message":"hello"}"#);
3118 }
3119
3120 #[test]
3121 fn xml_http_request_captures_request_and_dispatches_load() {
3122 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3123 runtime.run_script(
3124 r##"
3125 const request = new XMLHttpRequest();
3126 request.open("post", "https://example.test/messages");
3127 request.setRequestHeader("Content-Type", "text/plain");
3128 request.onload = function () {
3129 const result = document.querySelector("#result");
3130 result.setAttribute("data-state", this.readyState);
3131 result.setAttribute("data-status", this.status);
3132 result.setAttribute("data-text", this.responseText);
3133 result.setAttribute("data-headers", this.getAllResponseHeaders());
3134 };
3135 request.send("hello");
3136 "##,
3137 );
3138
3139 let requests = runtime.take_fetch_requests();
3140 assert_eq!(requests.len(), 1);
3141 assert_eq!(requests[0].url, "https://example.test/messages");
3142 assert_eq!(requests[0].method, "POST");
3143 assert_eq!(requests[0].body, b"hello");
3144 assert!(
3145 requests[0]
3146 .headers
3147 .contains(&("Content-Type".to_string(), "text/plain".to_string()))
3148 );
3149
3150 runtime.resolve_fetch(
3151 requests[0].id,
3152 JsFetchResponse {
3153 url: requests[0].url.clone(),
3154 status: 201,
3155 status_text: "Created".to_string(),
3156 redirected: false,
3157 body: b"saved".to_vec(),
3158 headers: vec![("X-Test".to_string(), "yes".to_string())],
3159 },
3160 );
3161
3162 let result = dom.get_element_by_id("result").unwrap();
3163 let result = result.borrow();
3164 assert_eq!(result.value.get_attr("data-state"), Some("4"));
3165 assert_eq!(result.value.get_attr("data-status"), Some("201"));
3166 assert_eq!(result.value.get_attr("data-text"), Some("saved"));
3167 assert_eq!(
3168 result.value.get_attr("data-headers"),
3169 Some("X-Test: yes\r\n")
3170 );
3171 }
3172
3173 #[test]
3174 fn headers_are_case_insensitive_and_mutable() {
3175 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3176 runtime.run_script(
3177 r##"
3178 const headers = new Headers({ Accept: "application/json" });
3179 headers.append("X-Test", "one");
3180 headers.append("x-test", "two");
3181 headers.set("X-Replace", "before");
3182 headers.set("x-replace", "after");
3183 headers.delete("ACCEPT");
3184
3185 const result = document.querySelector("#result");
3186 result.setAttribute("data-test", headers.get("X-TEST"));
3187 result.setAttribute("data-replace", headers.get("X-Replace"));
3188 result.setAttribute("data-has-accept", headers.has("accept"));
3189 result.setAttribute("data-missing", headers.get("missing") === null);
3190
3191 fetch("https://example.test/", { headers: headers });
3192 "##,
3193 );
3194
3195 let result = dom.get_element_by_id("result").unwrap();
3196 let result = result.borrow();
3197 assert_eq!(result.value.get_attr("data-test"), Some("one, two"));
3198 assert_eq!(result.value.get_attr("data-replace"), Some("after"));
3199 assert_eq!(result.value.get_attr("data-has-accept"), Some("false"));
3200 assert_eq!(result.value.get_attr("data-missing"), Some("true"));
3201
3202 let requests = runtime.take_fetch_requests();
3203 assert!(
3204 requests[0]
3205 .headers
3206 .contains(&("x-test".to_string(), "one, two".to_string()))
3207 );
3208 assert!(
3209 requests[0]
3210 .headers
3211 .contains(&("x-replace".to_string(), "after".to_string()))
3212 );
3213 }
3214
3215 #[test]
3216 fn response_exposes_read_only_headers() {
3217 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3218 runtime.run_script(
3219 r##"
3220 fetch("data:text/plain,hello").then(response => {
3221 const result = document.querySelector("#result");
3222 result.setAttribute("data-type", response.headers.get("Content-Type"));
3223 result.setAttribute("data-has", response.headers.has("X-Test"));
3224 });
3225 "##,
3226 );
3227
3228 let requests = runtime.take_fetch_requests();
3229 runtime.resolve_fetch(
3230 requests[0].id,
3231 JsFetchResponse {
3232 url: "data:text/plain,hello".to_string(),
3233 status: 200,
3234 status_text: "OK".to_string(),
3235 redirected: false,
3236 body: b"hello".to_vec(),
3237 headers: vec![
3238 ("content-type".to_string(), "text/plain".to_string()),
3239 ("X-Test".to_string(), "yes".to_string()),
3240 ],
3241 },
3242 );
3243
3244 let result = dom.get_element_by_id("result").unwrap();
3245 let result = result.borrow();
3246 assert_eq!(result.value.get_attr("data-type"), Some("text/plain"));
3247 assert_eq!(result.value.get_attr("data-has"), Some("true"));
3248 }
3249
3250 #[test]
3251 fn request_objects_can_be_copied_and_passed_to_fetch() {
3252 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3253 runtime.run_script(
3254 r##"
3255 const headers = new Headers({ Accept: "application/json" });
3256 const original = new Request("https://example.test/messages", {
3257 method: "post",
3258 headers: headers,
3259 body: "hello"
3260 });
3261 const copied = new Request(original);
3262 const result = document.querySelector("#result");
3263 result.setAttribute("data-url", copied.url);
3264 result.setAttribute("data-method", copied.method);
3265 result.setAttribute("data-accept", copied.headers.get("accept"));
3266 fetch(copied, { method: "put" });
3267 "##,
3268 );
3269
3270 let result = dom.get_element_by_id("result").unwrap();
3271 let result = result.borrow();
3272 assert_eq!(
3273 result.value.get_attr("data-url"),
3274 Some("https://example.test/messages")
3275 );
3276 assert_eq!(result.value.get_attr("data-method"), Some("POST"));
3277 assert_eq!(
3278 result.value.get_attr("data-accept"),
3279 Some("application/json")
3280 );
3281
3282 let requests = runtime.take_fetch_requests();
3283 assert_eq!(requests[0].url, "https://example.test/messages");
3284 assert_eq!(requests[0].method, "PUT");
3285 assert_eq!(requests[0].body, b"hello");
3286 assert!(
3287 requests[0]
3288 .headers
3289 .contains(&("accept".to_string(), "application/json".to_string()))
3290 );
3291 }
3292
3293 #[test]
3294 fn response_json_resolves_objects_and_arrays() {
3295 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3296 runtime.run_script(
3297 r##"
3298 fetch("data:application/json,pending").then(response => response.json()).then(value => {
3299 const result = document.querySelector("#result");
3300 result.setAttribute("data-name", value.name);
3301 result.setAttribute("data-second", value.items[1]);
3302 result.setAttribute("data-enabled", value.enabled);
3303 result.setAttribute("data-empty", value.empty === null);
3304 });
3305 "##,
3306 );
3307
3308 let requests = runtime.take_fetch_requests();
3309 runtime.resolve_fetch(
3310 requests[0].id,
3311 JsFetchResponse {
3312 url: "data:application/json,pending".to_string(),
3313 status: 200,
3314 status_text: "OK".to_string(),
3315 redirected: false,
3316 body: br#"{"name":"Orinium","items":[1,2],"enabled":true,"empty":null}"#.to_vec(),
3317 headers: Vec::new(),
3318 },
3319 );
3320
3321 let result = dom.get_element_by_id("result").unwrap();
3322 let result = result.borrow();
3323 assert_eq!(result.value.get_attr("data-name"), Some("Orinium"));
3324 assert_eq!(result.value.get_attr("data-second"), Some("2"));
3325 assert_eq!(result.value.get_attr("data-enabled"), Some("true"));
3326 assert_eq!(result.value.get_attr("data-empty"), Some("true"));
3327 }
3328
3329 #[test]
3330 fn response_json_rejects_invalid_json() {
3331 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3332 runtime.run_script(
3333 r##"
3334 fetch("data:application/json,invalid")
3335 .then(response => response.json())
3336 .catch(reason => {
3337 document.querySelector("#result").setAttribute("data-error", reason);
3338 });
3339 "##,
3340 );
3341
3342 let requests = runtime.take_fetch_requests();
3343 runtime.resolve_fetch(
3344 requests[0].id,
3345 JsFetchResponse {
3346 url: "data:application/json,invalid".to_string(),
3347 status: 200,
3348 status_text: "OK".to_string(),
3349 redirected: false,
3350 body: b"not json".to_vec(),
3351 headers: Vec::new(),
3352 },
3353 );
3354
3355 let result = dom.get_element_by_id("result").unwrap();
3356 assert!(
3357 result
3358 .borrow()
3359 .value
3360 .get_attr("data-error")
3361 .unwrap()
3362 .starts_with("Failed to parse JSON:")
3363 );
3364 }
3365
3366 #[test]
3367 fn fetch_rejection_runs_catch_reaction() {
3368 let (mut runtime, dom) = runtime_from_html(r#"<div id="result"></div>"#);
3369 runtime.run_script(
3370 r##"
3371 fetch("https://invalid.test/").catch(reason => {
3372 document.querySelector("#result").setAttribute("data-error", reason);
3373 });
3374 "##,
3375 );
3376
3377 let requests = runtime.take_fetch_requests();
3378 runtime.reject_fetch(requests[0].id, "network failed".to_string());
3379 let result = dom.get_element_by_id("result").unwrap();
3380 assert_eq!(
3381 result.borrow().value.get_attr("data-error"),
3382 Some("network failed")
3383 );
3384 }
3385
3386 #[test]
3387 fn intersection_observer_fires_on_observe() {
3388 let (mut runtime, dom) = runtime_from_html(
3389 r#"<div id="target" style="width: 100px; height: 50px"></div><div id="result"></div>"#,
3390 );
3391 runtime.run_script(
3392 r#"
3393 const target = document.getElementById("target");
3394 const result = document.getElementById("result");
3395 const observer = new IntersectionObserver(function (entries) {
3396 const entry = entries[0];
3397 result.setAttribute("data-target", entry.target === target);
3398 result.setAttribute("data-is-intersecting", entry.isIntersecting);
3399 result.setAttribute("data-ratio", entry.intersectionRatio);
3400 result.setAttribute("data-has-root-bounds", entry.rootBounds !== null);
3401 result.setAttribute("data-bcr-width", entry.boundingClientRect.width);
3402 });
3403 observer.observe(target);
3404 "#,
3405 );
3406
3407 let result = dom.get_element_by_id("result").unwrap();
3408 let result = result.borrow();
3409 assert_eq!(result.value.get_attr("data-target"), Some("true"));
3410 assert_eq!(result.value.get_attr("data-is-intersecting"), Some("true"));
3412 assert_eq!(result.value.get_attr("data-ratio"), Some("1"));
3413 assert_eq!(result.value.get_attr("data-has-root-bounds"), Some("true"));
3414 assert_eq!(result.value.get_attr("data-bcr-width"), Some("100"));
3415 }
3416
3417 #[test]
3418 fn custom_elements_define_and_connect() {
3419 let (mut runtime, dom) =
3420 runtime_from_html(r#"<html><body><div id="result"></div></body></html>"#);
3421 runtime.run_script(
3422 r#"
3423 class MyElement extends HTMLElement {
3424 connectedCallback() {
3425 document.getElementById("result").setAttribute("data-connected", "yes");
3426 }
3427 disconnectedCallback() {
3428 document.getElementById("result").setAttribute("data-disconnected", "yes");
3429 }
3430 }
3431 customElements.define("my-element", MyElement);
3432 document.getElementById("result").setAttribute(
3433 "data-proto",
3434 typeof MyElement.prototype.connectedCallback
3435 );
3436 const el = document.createElement("my-element");
3437 document.body.appendChild(el);
3438 document.body.removeChild(el);
3439 "#,
3440 );
3441
3442 let result = dom.get_element_by_id("result").unwrap();
3443 let result = result.borrow();
3444 assert_eq!(result.value.get_attr("data-proto"), Some("function"));
3446 assert_eq!(result.value.get_attr("data-connected"), Some("yes"));
3448 assert_eq!(result.value.get_attr("data-disconnected"), Some("yes"));
3449 }
3450
3451 #[test]
3452 fn custom_elements_define_getters_work() {
3453 let (mut runtime, _dom) = runtime_from_html(r#"<html><body></body></html>"#);
3454 runtime.run_script(
3455 r#"
3456 class MyEl extends HTMLElement {}
3457 customElements.define("my-el", MyEl);
3458 if (customElements.get("my-el") === undefined) throw new Error("get failed");
3459 if (customElements.get("no-such") !== undefined) throw new Error("get should be undefined");
3460 "#,
3461 );
3462 }
3463
3464 #[test]
3465 fn custom_elements_attribute_changed_and_when_defined() {
3466 let (mut runtime, dom) =
3467 runtime_from_html(r#"<html><body><div id="result"></div></body></html>"#);
3468 runtime.run_script(
3469 r#"
3470 globalThis.__attrLog = [];
3471 class AttrEl extends HTMLElement {
3472 attributeChangedCallback(name, oldVal, newVal) {
3473 globalThis.__attrLog.push(name + ":" + (oldVal === null ? "null" : oldVal) + ":" + (newVal === null ? "null" : newVal));
3474 }
3475 }
3476 AttrEl.observedAttributes = ["data-val"];
3477 customElements.define("attr-el", AttrEl);
3478 const el = document.createElement("attr-el");
3479 document.body.appendChild(el);
3480 el.setAttribute("data-val", "first");
3481 el.setAttribute("data-val", "second");
3482 el.removeAttribute("data-val");
3483 // whenDefined resolves immediately for an already-defined name.
3484 let wdResolved = false;
3485 customElements.whenDefined("attr-el").then(function () {
3486 wdResolved = true;
3487 });
3488 document.getElementById("result").setAttribute(
3489 "data-wd", wdResolved
3490 );
3491 "#,
3492 );
3493
3494 runtime.run_script(
3496 r#"document.getElementById("result").setAttribute(
3497 "data-log", globalThis.__attrLog.join("|")
3498 );"#,
3499 );
3500
3501 let result = dom.get_element_by_id("result").unwrap();
3502 let result = result.borrow();
3503 assert_eq!(result.value.get_attr("data-wd"), Some("true"));
3506 }
3507
3508 #[test]
3509 fn shadow_dom_attach_and_query() {
3510 let (mut runtime, dom) =
3511 runtime_from_html(r#"<html><body><div id="host"></div></body></html>"#);
3512 runtime.run_script(
3514 r##"
3515 var host = document.getElementById("host");
3516 host.setAttribute("data-step1", "ready");
3517 "##,
3518 );
3519 let result = dom.get_element_by_id("host").unwrap();
3520 assert_eq!(result.borrow().value.get_attr("data-step1"), Some("ready"));
3521
3522 runtime.run_script(
3524 r##"
3525 var host = document.getElementById("host");
3526 host.attachShadow({ mode: "open" });
3527 host.setAttribute("data-step2", "shadow-attached");
3528 "##,
3529 );
3530 assert_eq!(
3531 result.borrow().value.get_attr("data-step2"),
3532 Some("shadow-attached")
3533 );
3534
3535 runtime.run_script(
3537 r##"
3538 var host = document.getElementById("host");
3539 try {
3540 var sr = host.shadowRoot;
3541 host.setAttribute("data-sr", sr !== null ? "true" : "false");
3542 var span = document.createElement("span");
3543 span.id = "inner";
3544 span.textContent = "shadow text";
3545 sr.appendChild(span);
3546 var found = sr.querySelector("#inner");
3547 host.setAttribute("data-found", found !== null ? found.textContent : "NOT_FOUND");
3548 var notFound = host.querySelector("#inner");
3549 host.setAttribute("data-boundary", notFound === null ? "true" : "false");
3550 host.setAttribute("data-text", host.textContent.trim() === "" ? "true" : "false");
3551 } catch(e) {
3552 host.setAttribute("data-error", e.toString());
3553 }
3554 "##,
3555 );
3556 let result = dom.get_element_by_id("host").unwrap();
3557 let result = result.borrow();
3558 assert_eq!(result.value.get_attr("data-sr"), Some("true"));
3559 assert_eq!(result.value.get_attr("data-found"), Some("shadow text"));
3560 assert_eq!(result.value.get_attr("data-boundary"), Some("true"));
3561 assert_eq!(result.value.get_attr("data-text"), Some("true"));
3562 }
3563
3564 #[test]
3565 fn shadow_dom_closed_root() {
3566 let (mut runtime, dom) =
3567 runtime_from_html(r#"<html><body><div id="host"></div></body></html>"#);
3568 runtime.run_script(
3569 r##"
3570 var host = document.getElementById("host");
3571 host.attachShadow({ mode: "closed" });
3572 host.setAttribute("data-closed", host.shadowRoot === null ? "true" : "false");
3573 "##,
3574 );
3575 let result = dom.get_element_by_id("host").unwrap();
3576 let result = result.borrow();
3577 assert_eq!(result.value.get_attr("data-closed"), Some("true"));
3578 }
3579
3580 #[test]
3581 fn document_write_inserts_parsed_html_into_body() {
3582 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
3583 runtime.run_script(r#"document.write("<p>Hello</p>");"#);
3584 let p = dom.query_selector("body p").unwrap();
3585 assert_eq!(DomTree::inner_text(&p), "Hello");
3586 assert!(runtime.needs_redraw());
3587 }
3588
3589 #[test]
3590 fn document_writeln_appends_content_with_newline() {
3591 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
3592 runtime.run_script(r#"document.writeln("<span>A</span>");"#);
3593 let span = dom.query_selector("body span").unwrap();
3594 assert_eq!(DomTree::inner_text(&span), "A");
3595 }
3596
3597 #[test]
3598 fn dom_exception_has_name_message_and_code() {
3599 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3600 runtime.run_script(
3601 r#"
3602 try {
3603 throw new DOMException("test error", "SyntaxError");
3604 } catch (e) {
3605 document.getElementById("r").setAttribute("data-name", e.name);
3606 document.getElementById("r").setAttribute("data-msg", e.message);
3607 document.getElementById("r").setAttribute("data-code", e.code);
3608 }
3609 "#,
3610 );
3611 let r = dom.get_element_by_id("r").unwrap();
3612 let r = r.borrow();
3613 assert_eq!(r.value.get_attr("data-name"), Some("SyntaxError"));
3614 assert_eq!(r.value.get_attr("data-msg"), Some("test error"));
3615 assert_eq!(r.value.get_attr("data-code"), Some("12"));
3616 }
3617
3618 #[test]
3619 fn dom_exception_static_constants_are_exposed() {
3620 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3621 runtime.run_script(
3622 r#"
3623 document.getElementById("r").setAttribute(
3624 "data-codes",
3625 DOMException.SYNTAX_ERR + ":" +
3626 DOMException.HIERARCHY_REQUEST_ERR + ":" +
3627 DOMException.NOT_FOUND_ERR + ":" +
3628 DOMException.INVALID_STATE_ERR
3629 );
3630 "#,
3631 );
3632 let r = dom.get_element_by_id("r").unwrap();
3633 assert_eq!(r.borrow().value.get_attr("data-codes"), Some("12:3:8:11"));
3634 }
3635
3636 #[test]
3637 fn dom_exception_to_string_formats_name_and_message() {
3638 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3639 runtime.run_script(
3640 r#"
3641 var e = new DOMException("oops", "NotFoundError");
3642 document.getElementById("r").setAttribute("data-str", e.toString());
3643 "#,
3644 );
3645 let r = dom.get_element_by_id("r").unwrap();
3646 assert_eq!(
3647 r.borrow().value.get_attr("data-str"),
3648 Some("NotFoundError: oops")
3649 );
3650 }
3651
3652 #[test]
3653 fn create_element_throws_invalid_character_error_for_empty_name() {
3654 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3655 runtime.run_script(
3656 r#"
3657 try {
3658 document.createElement("");
3659 document.getElementById("r").setAttribute("data-error", "no-throw");
3660 } catch (e) {
3661 document.getElementById("r").setAttribute("data-error", e.name);
3662 }
3663 "#,
3664 );
3665 let r = dom.get_element_by_id("r").unwrap();
3666 assert_eq!(
3667 r.borrow().value.get_attr("data-error"),
3668 Some("InvalidCharacterError")
3669 );
3670 }
3671
3672 #[test]
3673 fn create_element_throws_invalid_character_error_for_invalid_name() {
3674 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3675 runtime.run_script(
3676 r#"
3677 try {
3678 document.createElement("123bad");
3679 document.getElementById("r").setAttribute("data-error", "no-throw");
3680 } catch (e) {
3681 document.getElementById("r").setAttribute("data-error", e.name);
3682 }
3683 "#,
3684 );
3685 let r = dom.get_element_by_id("r").unwrap();
3686 assert_eq!(
3687 r.borrow().value.get_attr("data-error"),
3688 Some("InvalidCharacterError")
3689 );
3690 }
3691
3692 #[test]
3693 fn create_element_valid_name_works() {
3694 let (mut runtime, dom) = runtime_from_html(r#"<html><body></body></html>"#);
3695 runtime.run_script(
3696 r#"
3697 var el = document.createElement("div");
3698 el.id = "created";
3699 document.body.appendChild(el);
3700 "#,
3701 );
3702 assert!(dom.get_element_by_id("created").is_some());
3703 }
3704
3705 #[test]
3706 fn create_element_ns_throws_invalid_character_error_for_invalid_name() {
3707 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3708 runtime.run_script(
3709 r#"
3710 try {
3711 document.createElementNS("http://www.w3.org/2000/svg", "123bad");
3712 document.getElementById("r").setAttribute("data-error", "no-throw");
3713 } catch (e) {
3714 document.getElementById("r").setAttribute("data-error", e.name);
3715 }
3716 "#,
3717 );
3718 let r = dom.get_element_by_id("r").unwrap();
3719 assert_eq!(
3720 r.borrow().value.get_attr("data-error"),
3721 Some("InvalidCharacterError")
3722 );
3723 }
3724
3725 #[test]
3726 fn node_constants_are_exposed_on_global() {
3727 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3728 runtime.run_script(
3729 r#"
3730 document.getElementById("r").setAttribute("data-elem", Node.ELEMENT_NODE);
3731 document.getElementById("r").setAttribute("data-text", Node.TEXT_NODE);
3732 document.getElementById("r").setAttribute("data-doc", Node.DOCUMENT_NODE);
3733 document.getElementById("r").setAttribute("data-frag", Node.DOCUMENT_FRAGMENT_NODE);
3734 "#,
3735 );
3736 let r = dom.get_element_by_id("r").unwrap();
3737 let r = r.borrow();
3738 assert_eq!(r.value.get_attr("data-elem"), Some("1"));
3739 assert_eq!(r.value.get_attr("data-text"), Some("3"));
3740 assert_eq!(r.value.get_attr("data-doc"), Some("9"));
3741 assert_eq!(r.value.get_attr("data-frag"), Some("11"));
3742 }
3743
3744 #[test]
3745 fn node_constants_are_exposed_on_instance_nodes() {
3746 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3747 runtime.run_script(
3748 r#"
3749 document.getElementById("r").setAttribute("data-doc-frag", document.DOCUMENT_FRAGMENT_NODE);
3750 document.getElementById("r").setAttribute("data-cmt", document.body.COMMENT_NODE);
3751 document.getElementById("r").setAttribute("data-txt", document.createTextNode("").ELEMENT_NODE);
3752 document.getElementById("r").setAttribute("data-frag", document.createElement("div").DOCUMENT_FRAGMENT_NODE);
3753 "#,
3754 );
3755 let r = dom.get_element_by_id("r").unwrap();
3756 let r = r.borrow();
3757 assert_eq!(r.value.get_attr("data-doc-frag"), Some("11"));
3758 assert_eq!(r.value.get_attr("data-cmt"), Some("8"));
3759 assert_eq!(r.value.get_attr("data-txt"), Some("1"));
3760 assert_eq!(r.value.get_attr("data-frag"), Some("11"));
3761 }
3762
3763 #[test]
3764 fn document_first_child_is_doctype_node() {
3765 let (mut runtime, dom) = runtime_from_html(
3766 r#"<!DOCTYPE html><html><body><div id="r"><span>x</span></div></body></html>"#,
3767 );
3768 runtime.run_script(
3769 r#"
3770 document.getElementById("r").setAttribute("data-doc-type", document.nodeType);
3771 document.getElementById("r").setAttribute("data-doctype-node", document.firstChild.nodeType);
3772 document.getElementById("r").setAttribute("data-doctype-name", document.firstChild.nodeName);
3773 var span = document.getElementById("r").firstChild;
3774 document.getElementById("r").setAttribute("data-text-type", span.firstChild.nodeType);
3775 "#,
3776 );
3777 let r = dom.get_element_by_id("r").unwrap();
3778 let r = r.borrow();
3779 assert_eq!(r.value.get_attr("data-doc-type"), Some("9"));
3780 assert_eq!(r.value.get_attr("data-doctype-node"), Some("10"));
3781 assert_eq!(r.value.get_attr("data-doctype-name"), Some("html"));
3782 assert_eq!(r.value.get_attr("data-text-type"), Some("3"));
3783 }
3784
3785 #[test]
3786 fn create_element_ns_preserves_qualified_name() {
3787 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3788 runtime.run_script(
3789 r#"
3790 var el = document.createElementNS("http://ns.example.com/", "prefix:localname");
3791 document.getElementById("r").setAttribute("data-tag", el.tagName);
3792 document.getElementById("r").setAttribute("data-local", el.localName);
3793 document.getElementById("r").setAttribute("data-prefix", el.prefix);
3794 document.getElementById("r").setAttribute("data-ns", el.namespaceURI);
3795 "#,
3796 );
3797 let r = dom.get_element_by_id("r").unwrap();
3798 let r = r.borrow();
3799 assert_eq!(r.value.get_attr("data-tag"), Some("prefix:localname"));
3800 assert_eq!(r.value.get_attr("data-local"), Some("localname"));
3801 assert_eq!(r.value.get_attr("data-prefix"), Some("prefix"));
3802 assert_eq!(r.value.get_attr("data-ns"), Some("http://ns.example.com/"));
3803 }
3804
3805 #[test]
3806 fn document_close_returns_undefined() {
3807 let (mut runtime, _) = runtime_from_html(r#"<html><body></body></html>"#);
3808 runtime.run_script(
3809 r#"
3810 var result = document.close();
3811 document.getElementById("r").setAttribute("data-close", typeof result);
3812 "#,
3813 );
3814 let (mut runtime2, dom2) = runtime_from_html(r#"<div id="r"></div>"#);
3817 runtime2.run_script(
3818 r#"
3819 document.close();
3820 document.getElementById("r").setAttribute("data-close", "ok");
3821 "#,
3822 );
3823 let r = dom2.get_element_by_id("r").unwrap();
3824 assert_eq!(r.borrow().value.get_attr("data-close"), Some("ok"));
3825 }
3826
3827 #[test]
3828 fn dom_exception_instanceof_error() {
3829 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3830 runtime.run_script(
3831 r#"
3832 var e = new DOMException("test", "SyntaxError");
3833 document.getElementById("r").setAttribute("data-is-error", e instanceof Error);
3834 document.getElementById("r").setAttribute("data-is-domexc", e instanceof DOMException);
3835 "#,
3836 );
3837 let r = dom.get_element_by_id("r").unwrap();
3838 let r = r.borrow();
3839 assert_eq!(r.value.get_attr("data-is-error"), Some("true"));
3840 assert_eq!(r.value.get_attr("data-is-domexc"), Some("true"));
3841 }
3842
3843 #[test]
3844 fn create_comment_returns_comment_node() {
3845 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3846 runtime.run_script(
3847 r#"
3848 var c = document.createComment("hello");
3849 document.getElementById("r").setAttribute("data-type", c.nodeType);
3850 document.getElementById("r").setAttribute("data-name", c.nodeName);
3851 document.getElementById("r").setAttribute("data-data", c.data);
3852 "#,
3853 );
3854 let r = dom.get_element_by_id("r").unwrap();
3855 let r = r.borrow();
3856 assert_eq!(r.value.get_attr("data-type"), Some("8"));
3857 assert_eq!(r.value.get_attr("data-name"), Some("#comment"));
3858 assert_eq!(r.value.get_attr("data-data"), Some("hello"));
3859 }
3860
3861 #[test]
3862 fn create_processing_instruction_returns_pi_node() {
3863 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3864 runtime.run_script(
3865 r#"
3866 var pi = document.createProcessingInstruction("xml-stylesheet", "href=\"style.css\"");
3867 document.getElementById("r").setAttribute("data-type", pi.nodeType);
3868 document.getElementById("r").setAttribute("data-data", pi.data);
3869 "#,
3870 );
3871 let r = dom.get_element_by_id("r").unwrap();
3872 let r = r.borrow();
3873 assert_eq!(r.value.get_attr("data-type"), Some("7"));
3874 assert_eq!(r.value.get_attr("data-data"), Some("href=\"style.css\""));
3875 }
3876
3877 #[test]
3878 fn create_processing_instruction_empty_target_throws() {
3879 let (mut runtime, dom) = runtime_from_html(r#"<div id="r"></div>"#);
3880 runtime.run_script(
3881 r#"
3882 try {
3883 document.createProcessingInstruction("", "data");
3884 document.getElementById("r").setAttribute("data-error", "no-throw");
3885 } catch (e) {
3886 document.getElementById("r").setAttribute("data-error", e.name);
3887 }
3888 "#,
3889 );
3890 let r = dom.get_element_by_id("r").unwrap();
3891 assert_eq!(r.borrow().value.get_attr("data-error"), Some("SyntaxError"));
3892 }
3893
3894 #[test]
3895 fn innerhtml_setter_uses_parser_and_replaces_children() {
3896 let (mut runtime, dom) = runtime_from_html(r#"<div id="target"></div><div id="r"></div>"#);
3897 runtime.run_script(r#"
3898 var t = document.getElementById("target");
3899 t.innerHTML = "<p>A</p><span>B</span>";
3900 document.getElementById("r").setAttribute("data-count", t.childNodes.length);
3901 document.getElementById("r").setattr || document.getElementById("r").setAttribute("data-tags",
3902 t.children[0].tagName + ":" + t.children[1].tagName
3903 );
3904 "#);
3905 let r = dom.get_element_by_id("r").unwrap();
3906 let r = r.borrow();
3907 assert_eq!(r.value.get_attr("data-count"), Some("2"));
3908 }
3909}