use crate::content_security_policy::ContentSecurityPolicy; use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, rc::Weak, sync::Arc, }; use url::Url; mod devtools_mutations; mod document_write; mod dom_facade; mod events; mod inspector_issues; mod lifecycle; mod main_parser_continuation; mod meta_refresh; mod mutation_commands; mod native_events; pub(crate) use native_events::NativeEventDispatch; mod parser_blocking; mod parser_modulepreload; mod query_facade; mod runtime_core; mod script_lifecycle; mod script_scheduling; mod security_policy; #[path = "stylesheet_runtime/mod.rs"] mod stylesheet_runtime; mod timing; pub(crate) use timing::{DocumentLifecycleTiming, DocumentTimingSnapshot}; #[cfg(test)] pub(crate) use mutation_commands::ParserPostStepRuntimeWorkForTest; use mutation_commands::{ParserPostStepRuntimeWork, TreeAdoptionPlan}; pub(super) use super::host::EventTargetHandle; use super::{ context_bootstrap, custom_elements, host::{ HostDocumentState, HostEventTargetRegistry, HostScriptScheduler, HostTimeoutRunResult, HostTimeoutScheduler, SelectorDebugCounters, SelectorDebugSnapshot, create_host_event, dispatch_public_event, dispatch_public_event_with_original_target, event_target_value, host_event_defaults, }, mutation_coordinator::{MutationCoordinator, RuntimeMutationOptions}, native_bridge::{self, PageScriptHost}, page_task_queue::{PageTask, PageTaskQueue, PostParseLifecycleWork, PostParsePageOwnedWork}, planning::PreparedScript, runtime::RendererBrowserContextRuntime, stylesheet_blocking::{ DocumentBlockingStylesheetSignature, StylesheetBlockingState, StylesheetBlockingStatus, }, util::v8str, }; #[cfg(test)] use crate::dom::native::ShadowRootBindingSnapshot; pub(crate) use crate::meta_refresh_scheduler::MetaRefreshNavigation; use crate::{ dom::{ NodeId, native::{ DocumentReadyState, DomHost, DomMutationEffects, DomStylesheetOwnerChangeKind, NativeDom, NativeNodeId, Node, }, }, frame_owner_model::FrameDocumentTaskOwner, live_document_parser::{ DocumentParserLifetime, DocumentParserRunState, DocumentParserSession, DocumentParserSessionControlHandle, ParserResumeOwner, ParserResumePermit, ParserSuspensionCause, }, module_runtime::ModuleMapKey, network::ResourceRequestClient, parser::{HtmlParser, ParserInputContext, ParserInputSession}, selector::{QueryEngine, SelectorError}, service_worker_runtime::ServiceWorkerClientId, types::SubresourceResourceType, }; pub(crate) use devtools_mutations::{ DevToolsDomChildListMutationFact, DevToolsDomMutationFact, DevToolsDomPrepublishedRemoval, }; pub(crate) use inspector_issues::PendingInspectorIssue; use parser_blocking::{ParserInsertionWork, PendingParserBlockingWork, PendingParserInsertion}; pub(crate) use parser_modulepreload::MainDocumentModulepreloadFetchOutcome; pub(crate) use script_lifecycle::{ DeferredPageTask, DeferredPageTaskLane, DeferredPageTaskState, DocumentScriptLifecycle, FollowupPageTaskDisposition, PendingMainParserDeferredScriptStart, RuntimeScriptWorkPauseKind, RuntimeScriptWorkState, SharedRuntimeScriptWorkState, parser_prepared_script_page_owned_work, parser_script_preparation_failure_page_owned_work, }; pub(crate) use security_policy::{ DocumentConnectPolicySnapshot, DocumentContentSecurityPolicyCheck, DocumentContentSecurityPolicyViolation, DocumentNavigationEmbeddingContext, DocumentSubresourceCspKind, create_content_security_policy_violation_event, document_content_security_policy_error_message, }; pub(crate) use stylesheet_runtime::attribute_reprocesses_connected_stylesheet; #[cfg(test)] use stylesheet_runtime::{ConnectedLinkReadinessFetchOptions, ConnectedLoadParameters}; pub(crate) use stylesheet_runtime::{ ConnectedLoadCompletion, LinkedStylesheetImportGraphCompletion, LiveStylesheetImportLoadCompletion, StylesheetImportCompletionAuthority, fetch_complete_stylesheet_import_graph, fetch_complete_stylesheet_import_graph_from_root, live_stylesheet_import_responses, }; use stylesheet_runtime::{ ConnectedLoadOperation, LinkedStylesheetImportGraphs, QueuedConnectedStyleLoad, StylesheetLinkClientIndex, StylesheetOwnerCspDisposition, StylesheetOwnerRuntimeStates, }; pub(crate) use stylesheet_runtime::{ ConnectedStyleEventElementKind, ConnectedStyleLoadEventAdmission, ConnectedStyleLoadEventPlan, ConnectedStyleLoadPrimeResult, InstallLinkedStylesheet, NativeModulepreloadLinkFetchOutcome, OwnerlessStylesheetAdmissionError, PendingNativeModulepreloadLinkEvent, PreparedConnectedStyleLoad, PreparedLinkedStylesheetResource, ReadyConnectedStyleLoad, StylesheetLinkClientTerminal, }; pub(super) type DomHandle = NativeNodeId; pub(crate) type ParserStreamHandle = crate::live_document_parser::DocumentParserStreamHandle; type ParserStreamWeakHandle = Weak>; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum HtmlFragmentCustomElementUpgradeTiming { InReturnedFragment, AfterInsertion, } #[derive(Debug)] pub(crate) enum DocumentProcessingAction { PostParsePageOwnedWork(Box), DispatchConnectedStyleLoad(ReadyConnectedStyleLoad), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum DocumentProcessingWakeSource { InjectedPageTask, TaskSourceLoadCompletion, } #[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ParseTimeWakeSource { InjectedPageTask, TaskSourceLoadCompletion, } #[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ParseTimeWakeObservation { ReadyNow, Arrived(ParseTimeWakeSource), TimedOutNoReady, } #[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum DocumentProcessingWakeObservation { ReadyNow, Arrived(DocumentProcessingWakeSource), NoWake, } #[derive(Clone, Copy)] pub(crate) struct PostParseOwnerReadiness { pub(crate) should_poll_document_processing: bool, pub(crate) blocks_page_task_pop: bool, pub(crate) has_pending_progress_source: bool, } #[derive(Debug)] pub(crate) enum PostParseOwnerDriverStep { Ready(Box), NeedsContinuation, AwaitProgress, Idle, } #[derive(Debug)] struct ParserConnectedScriptContext { bridge: ParserConnectedScriptBridge, _input_context: ParserInputContext, } #[derive(Debug)] struct CurrentScriptContext { handle: Option, parser_connected: Option, } #[derive(Debug, Clone)] pub(crate) struct CurrentScriptContextSpec { pub(crate) handle: Option, pub(crate) parser_write_insertion_point_active: bool, pub(crate) parser_bridge: Option, } #[derive(Clone)] pub(crate) struct ParserInsertionController { input_session: ParserInputSession, parser_stream: ParserStreamWeakHandle, } impl std::fmt::Debug for ParserInsertionController { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ParserInsertionController") .field("input_session", &self.input_session) .finish() } } impl ParserInsertionController { pub(crate) fn for_stream(stream: &ParserStreamHandle) -> Self { let input_session = stream.borrow().script_input_session(); Self { input_session, parser_stream: std::rc::Rc::downgrade(stream), } } pub(crate) fn input_session(&self) -> ParserInputSession { self.input_session.clone() } pub(crate) fn with_parser_stream( &self, operation: impl FnOnce(&crate::parser::DocumentStream) -> R, ) -> R { let stream = self .parser_stream .upgrade() .expect("synchronous parser insertion permission outlived its parser session"); let stream = stream.borrow(); operation(&stream) } } /// The explicit bridge between parser-connected script execution and the /// phase-owned parser session. /// /// The insertion controller is deliberately limited to synchronous input /// insertion. Pump/suspend/script-nesting transitions are reported through /// the separate session control handle so retaining insertion permission can /// never itself authorize parser finalization. #[derive(Clone)] pub(crate) struct ParserConnectedScriptBridge { insertion_controller: ParserInsertionController, parser_control: DocumentParserSessionControlHandle, } impl std::fmt::Debug for ParserConnectedScriptBridge { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ParserConnectedScriptBridge") .field("insertion_controller", &self.insertion_controller) .field("session_id", &self.parser_control.session_id()) .field("run_state", &self.parser_control.run_state()) .field( "pump_session_nesting_level", &self.parser_control.pump_session_nesting_level(), ) .field( "parser_script_nesting_level", &self.parser_control.parser_script_nesting_level(), ) .field( "finish_request_state", &self.parser_control.finish_request_state(), ) .finish() } } impl ParserConnectedScriptBridge { #[cfg(test)] pub(crate) fn for_stream(stream: &ParserStreamHandle) -> Self { Self { insertion_controller: ParserInsertionController::for_stream(stream), parser_control: DocumentParserSessionControlHandle::new(), } } pub(crate) fn for_session(parser: &DocumentParserSession) -> Option { let stream = parser.html_stream_handle()?; Some(Self { insertion_controller: ParserInsertionController::for_stream(&stream), parser_control: parser.control_handle(), }) } pub(crate) fn insertion_controller(&self) -> &ParserInsertionController { &self.insertion_controller } pub(crate) fn enter_parser_script_nesting( &self, ) -> crate::live_document_parser::DocumentParserScriptNestingGuard { self.parser_control.enter_parser_script_nesting() } pub(crate) fn run_state(&self) -> DocumentParserRunState { self.parser_control.run_state() } pub(crate) fn suspend(&self, cause: ParserSuspensionCause) -> ParserResumePermit { self.parser_control .suspend(cause, ParserResumeOwner::DocumentWrite) } pub(crate) fn resume(&self, permit: ParserResumePermit) -> bool { self.parser_control .resume(permit, ParserResumeOwner::DocumentWrite) } pub(crate) fn begin_pump(&self) -> crate::live_document_parser::DocumentParserPumpGuard { self.parser_control.begin_pump() } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] struct StylesheetLinkPromotionTrace { promotion_count: u64, inspected_link_states: u64, promoted_clients: u64, max_indexed_link_clients: usize, total_elapsed_us: u128, } struct StylesheetLifecycleState { pending_connected_loads: VecDeque>, /// Synthetic ready events used only by low-level ordering tests. /// /// Async stylesheet work never enters this queue; it always publishes a /// typed DOM-manipulation task through `task_producer`. #[cfg(test)] injected_ready_connected_loads: VecDeque, /// Owners already processed through a connected mutation before the /// one-time post-parse discovery scan. /// /// Their event tasks may finish before that scan now that DOM work has a /// stable Page source, so transient event residence cannot be used as the /// discovery deduplication fact. pre_initial_scan_processed_owners: HashSet, ready_connected_load_network_results: VecDeque, /// Client completions accepted while applying the selected physical /// stylesheet Networking task. ready_stylesheet_link_client_terminals: VecDeque, linked_stylesheet_import_graphs: LinkedStylesheetImportGraphs, // One canonical entry owns each stylesheet-related owner's operation, // completion/event disposition, and link state. owner_states: StylesheetOwnerRuntimeStates, link_client_index: StylesheetLinkClientIndex, link_promotion_trace: StylesheetLinkPromotionTrace, fetches: StylesheetBlockingState, task_sender: crate::page_task_queue::RendererPageStylesheetTaskSender, task_producer: Option, service_worker_connected_link_context: Option, #[cfg(test)] task_test_residence: Option, } impl StylesheetLifecycleState { fn new(task_sender: crate::page_task_queue::RendererPageStylesheetTaskSender) -> Self { Self { pending_connected_loads: VecDeque::new(), #[cfg(test)] injected_ready_connected_loads: VecDeque::new(), pre_initial_scan_processed_owners: HashSet::new(), ready_connected_load_network_results: VecDeque::new(), ready_stylesheet_link_client_terminals: VecDeque::new(), linked_stylesheet_import_graphs: LinkedStylesheetImportGraphs::default(), owner_states: StylesheetOwnerRuntimeStates::default(), link_client_index: StylesheetLinkClientIndex::default(), link_promotion_trace: StylesheetLinkPromotionTrace::default(), fetches: StylesheetBlockingState::default(), task_sender, task_producer: None, service_worker_connected_link_context: None, #[cfg(test)] task_test_residence: None, } } fn set_service_worker_connected_link_context( &mut self, browser_context_runtime: RendererBrowserContextRuntime, client_id: ServiceWorkerClientId, ) { self.service_worker_connected_link_context = Some(ServiceWorkerConnectedLinkContext { browser_context_runtime, client_id, }); } } #[derive(Clone)] struct ServiceWorkerConnectedLinkContext { browser_context_runtime: RendererBrowserContextRuntime, client_id: ServiceWorkerClientId, } #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct ConnectedStyleImportRoot { pub(crate) owner: DomHandle, pub(crate) stylesheet_id: crate::live_stylesheet::StylesheetId, pub(crate) contents_revision: u64, pub(crate) import_generation: u64, /// Original resource URL; the live stylesheet retains the final parser base. /// Inline roots have no resource identity to exclude from their imports. pub(crate) root_resource_url: Option, } impl ConnectedStyleImportRoot { pub(crate) fn new( owner: DomHandle, stylesheet: &crate::live_stylesheet::LiveStylesheetRef, root_resource_url: Option, ) -> Self { Self { owner, stylesheet_id: stylesheet.id(), contents_revision: stylesheet.contents_revision(), import_generation: stylesheet.import_generation(), root_resource_url, } } pub(crate) fn matches_stylesheet( &self, stylesheet: &crate::live_stylesheet::LiveStylesheetRef, ) -> bool { stylesheet.id() == self.stylesheet_id && stylesheet.contents_revision() == self.contents_revision && stylesheet.import_generation() == self.import_generation } } #[derive(Debug, Clone)] pub(crate) struct ConnectedLoadNetworkResult { pub(crate) stylesheet_fetch: Option, pub(crate) blocking_operation: Option, pub(crate) source_operation: Option>, pub(crate) import_roots: Vec, pub(crate) document_url: Url, pub(crate) request_url: Url, pub(crate) source_owners: Vec, pub(crate) resource_type: SubresourceResourceType, pub(crate) start_unix_millis: Option, pub(crate) origin_clean: bool, pub(crate) result: std::result::Result, } #[derive(Debug)] struct DocumentWriteExternalScriptStart { node: DomHandle, host_script_handle: String, script: PreparedScript, } #[derive(Debug)] struct SuspendedDocumentWriteInsertion { document_handle: DomHandle, document_incarnation: DocumentRuntimeIncarnationIdentity, parser_bridge: ParserConnectedScriptBridge, resume_permit: ParserResumePermit, resume_permit_consumed: bool, } #[derive(Debug)] enum SuspendedDocumentWriteContinuation { ResumeAfterCompleted { start: DocumentWriteExternalScriptStart, insertion: SuspendedDocumentWriteInsertion, }, StartExternal { start: DocumentWriteExternalScriptStart, insertion: SuspendedDocumentWriteInsertion, blocking_signatures_before: HashSet, }, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum DocumentWriteExternalScriptLoadApplication { Applied, SupersededDuringApplication, RejectedStaleTarget, } #[derive(Debug)] struct DocumentWriteScriptPreload { request: crate::runtime::BufferedScriptPreloadRequest, target: crate::types::DocumentWriteExternalScriptFetchTarget, ready_completion: Option, } #[derive(Debug)] enum DocumentWriteScriptRunOutcome { Complete, Suspend(Box), } impl std::fmt::Debug for StylesheetLifecycleState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut debug = f.debug_struct("StylesheetLifecycleState"); debug.field( "pending_connected_loads_len", &self.pending_connected_loads.len(), ); #[cfg(test)] debug.field( "injected_ready_connected_loads_len", &self.injected_ready_connected_loads.len(), ); debug .field("owner_states_len", &self.owner_states.len()) .field( "indexed_stylesheet_link_clients", &self.link_client_index.len(), ) .field("link_promotion_trace", &self.link_promotion_trace) .finish_non_exhaustive() } } #[derive(Clone)] enum DocumentRuntimeIncarnationIdentity { MainFrame(FrameDocumentTaskOwner), Standalone(Arc<()>), } impl DocumentRuntimeIncarnationIdentity { fn standalone() -> Self { Self::Standalone(Arc::new(())) } } impl std::fmt::Debug for DocumentRuntimeIncarnationIdentity { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::MainFrame(owner) => formatter.debug_tuple("MainFrame").field(owner).finish(), Self::Standalone(token) => formatter .debug_tuple("Standalone") .field(&Arc::as_ptr(token)) .finish(), } } } impl PartialEq for DocumentRuntimeIncarnationIdentity { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::MainFrame(left), Self::MainFrame(right)) => left == right, (Self::Standalone(left), Self::Standalone(right)) => Arc::ptr_eq(left, right), (Self::MainFrame(_), Self::Standalone(_)) | (Self::Standalone(_), Self::MainFrame(_)) => false, } } } impl Eq for DocumentRuntimeIncarnationIdentity {} #[derive(Debug, Clone, PartialEq, Eq)] struct ParserStepOwnerGuard { document_incarnation: DocumentRuntimeIncarnationIdentity, depth: usize, } #[derive(Debug, Default)] struct ParserReentryState { active_step_owner_guard: Option, custom_element_reaction_queue_active: bool, // Counts nested synchronous parser-blocking script execution scopes, // including load/error completion dispatched before parser resume. This // does not describe parser-inserted ownership, deferred-script execution, // parser pause depth, or document.write insertion-point availability. script_nesting_level: usize, pause_depth: usize, insertion_session_depth: usize, dynamic_markup_insertion_counters: HashMap, } pub(crate) struct ParserScriptNestingGuard { runtime: *mut DocumentRuntime, _parser_session: Option, } pub(crate) struct ParserPauseGuard { runtime: *mut DocumentRuntime, } pub(crate) struct ParserInsertionSessionGuard { runtime: *mut DocumentRuntime, } impl Drop for ParserScriptNestingGuard { fn drop(&mut self) { // SAFETY: The guard is created from `DocumentRuntime::enter_parser_script_nesting` // and is only used on the renderer owner thread while the owning // `DocumentRuntime` remains alive. The raw pointer avoids holding a // broad mutable borrow across V8 script execution, which can re-enter // the runtime through DOM callbacks. unsafe { &mut *self.runtime }.exit_parser_script_nesting(); } } impl Drop for ParserPauseGuard { fn drop(&mut self) { // SAFETY: The guard is created from `DocumentRuntime::enter_parser_pause` // and is scoped to the renderer owner thread. The raw pointer avoids // holding a broad mutable borrow while V8 executes user constructor code. unsafe { &mut *self.runtime }.exit_parser_pause(); } } impl Drop for ParserInsertionSessionGuard { fn drop(&mut self) { // SAFETY: The guard is created from `DocumentRuntime::enter_parser_insertion_session` // and stays on the renderer owner thread. The raw pointer avoids // holding a broad mutable borrow across parser pumping and script handoff. unsafe { &mut *self.runtime }.exit_parser_insertion_session(); } } struct StructuralMutationGuard { runtime: *mut DocumentRuntime, } impl Drop for StructuralMutationGuard { fn drop(&mut self) { // SAFETY: The guard is created from `DocumentRuntime::enter_structural_mutation` // and is scoped to the renderer owner thread. The raw pointer avoids // holding a broad mutable borrow while the runtime mutates its native // DOM storage. unsafe { &mut *self.runtime }.exit_structural_mutation(); } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct DocumentPolicyContainer { pub(crate) document_referrer: String, pub(crate) referrer_policy: Option, pub(crate) cross_origin_embedder_policy: crate::cross_origin_isolation::CrossOriginEmbedderPolicy, pub(crate) document_isolation_policy: crate::cross_origin_isolation::DocumentIsolationPolicy, pub(crate) cross_origin_isolated: bool, pub(crate) document_content_security_policies: Vec, pub(crate) response_content_security_policies: Vec, pub(crate) response_content_security_report_only_policies: Vec, pub(crate) content_security_reporting_endpoints: crate::content_security_policy::ContentSecurityPolicyReportingEndpoints, pub(crate) credentialless: bool, pub(crate) credentialless_storage_nonce: Option, pub(crate) sandbox: DocumentSandboxPolicy, pub(crate) permissions_policy: crate::permissions_policy::DocumentPermissionsPolicy, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct DocumentSandboxPolicy { pub(crate) forces_opaque_origin: bool, pub(crate) allows_scripts: bool, pub(crate) allows_modals: bool, pub(crate) allows_top_navigation: bool, pub(crate) allows_popups_to_escape: bool, pub(crate) sandboxes_document_domain: bool, } impl Default for DocumentSandboxPolicy { fn default() -> Self { Self { forces_opaque_origin: false, allows_scripts: true, allows_modals: true, allows_top_navigation: true, allows_popups_to_escape: false, sandboxes_document_domain: false, } } } impl DocumentSandboxPolicy { pub(crate) fn from_response_content_security_policies( policies: &[ContentSecurityPolicy], ) -> Self { Self { forces_opaque_origin: crate::content_security_policy::content_security_policy_forces_opaque_origin( policies, ), allows_scripts: crate::content_security_policy::content_security_policy_sandbox_allows_scripts( policies, ), allows_modals: crate::content_security_policy::content_security_policy_sandbox_allows_modals( policies, ), allows_top_navigation: crate::content_security_policy::content_security_policy_sandbox_allows_top_navigation( policies, ), allows_popups_to_escape: crate::content_security_policy::content_security_policy_sandbox_allows_popups_to_escape( policies, ), sandboxes_document_domain: crate::content_security_policy::content_security_policy_sandboxes_document_domain( policies, ), } } pub(crate) fn with_response_content_security_policy(mut self, response: Self) -> Self { if response.sandboxes_document_domain { self.forces_opaque_origin |= response.forces_opaque_origin; self.allows_scripts &= response.allows_scripts; self.allows_modals &= response.allows_modals; self.allows_top_navigation &= response.allows_top_navigation; self.allows_popups_to_escape = if self.sandboxes_document_domain { self.allows_popups_to_escape && response.allows_popups_to_escape } else { response.allows_popups_to_escape }; } self.sandboxes_document_domain |= response.sandboxes_document_domain; self } } #[derive(Debug)] pub(super) struct DocumentRuntime { dom_host: LiveRuntimeDomHost, parser_reentry: ParserReentryState, pending_parser_post_step_runtime_work: ParserPostStepRuntimeWork, /// Used when temporarily parking live shadow-root bindings across compat replacement paths. #[cfg(test)] parked_live_shadow_root_bindings: Option>, selector_engine: QueryEngine, selector_debug: SelectorDebugCounters, document: HostDocumentState, timing: timing::DocumentTiming, script_execution_control: crate::script_execution_control::RendererScriptExecutionControl, author_styles_disabled: bool, bypass_content_security_policy: bool, policy_container: DocumentPolicyContainer, delivered_meta_content_security_policies: Arc>>>, processed_meta_content_security_policy_handles: RefCell>, resource_loader_binding: Option, script_context_stack: Vec, root_document_parser: Option, post_parse_schedule_invalidated: bool, stylesheet_lifecycle: StylesheetLifecycleState, main_parser_continuation: main_parser_continuation::MainParserContinuationState, pending_stylesheet_source_css_projection_owners: Vec, pending_connected_style_load_prime_result: ConnectedStyleLoadPrimeResult, initial_connected_style_loads_queued: bool, late_preload_stylesheet_handles: HashSet, in_document_image_priority_boost_count: usize, parser_discovered_modulepreloads: HashSet, modulepreload_invalid_as_link_errors: HashSet, style_source_document_sync_pending: bool, pending_devtools_dom_mutations: Vec, #[cfg(test)] pending_runtime_binding_calls: Vec, pending_inspector_issues: Vec, quirks_mode_issue_reported: bool, script_lifecycle: DocumentScriptLifecycle, parser_script_start_positions: HashMap, timeouts: HostTimeoutScheduler, events: HostEventTargetRegistry, mutations: MutationCoordinator, meta_refresh_scheduler: crate::meta_refresh_scheduler::MetaRefreshScheduler, custom_element_reaction_depth: usize, structural_mutation_depth: usize, dom_content_loaded_dispatched: bool, document_incarnation: DocumentRuntimeIncarnationIdentity, document_input_stream_opened: bool, next_document_write_external_script_load_id: u64, document_write_script_preload_scanner: Option>, /// In-flight classic script loads started by the main-document scanner. /// The Document shares this resource residence with parser re-entry while /// scanner/tokenizer state remains owned by phase one. main_document_script_preloads: crate::runtime::DocumentScriptPreloadStore, document_write_script_preloads: HashMap, pending_parser_blocking_work: Option, } #[derive(Clone, Debug)] struct DocumentResourceLoaderBinding { registry: crate::network::context::DocumentResourceLoaderRegistry, owner: crate::native_bridge::WindowDocumentOwner, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct ParserScriptStartPosition { pub(crate) line: u64, pub(crate) column: u64, } /// Wraps the runtime-owned live `DomHost`. /// /// The runtime always owns the `DomHost`; parser steps do not move it out and /// do not put the runtime into a separate DOM-access mode. Parser callbacks /// call renderer-owned sinks, and each sink short-borrows this same native DOM /// object graph through `DocumentRuntime`. /// /// The live DOM also owns the canonical media settings, shared by native parser /// reads and Window bindings. The wrapper centralizes direct `DomHost` borrowing /// so the parser path avoids carrying a `DomHost` pointer or borrow token through /// helper layers. #[derive(Debug)] struct LiveRuntimeDomHost { dom_host: DomHost, emulated_media: crate::protocol_types::EmulatedMediaOverrides, viewport_surface: Option, } impl LiveRuntimeDomHost { fn style_viewport(&self) -> crate::style_engine::StyleViewport { self.viewport_surface .map(crate::style_engine::StyleViewport::from_viewport_surface) .unwrap_or_else(|| { crate::style_engine::StyleViewport::new( Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.inner_width), Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.inner_height), ) .with_screen_size( Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.screen_width), Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.screen_height), ) }) } fn from_dom_host(dom_host: DomHost) -> Self { Self { dom_host, emulated_media: Default::default(), viewport_surface: None, } } #[track_caller] fn borrow(&self) -> &DomHost { &self.dom_host } #[track_caller] fn borrow_mut(&mut self) -> &mut DomHost { &mut self.dom_host } fn into_dom_host(self) -> DomHost { self.dom_host } } impl DocumentRuntime { #[cfg(test)] /// Exposes the synchronous parser-blocking execution depth to contract /// tests; it is not a general measure of parser ownership or activity. pub(crate) fn parser_script_nesting_level(&self) -> usize { self.parser_reentry.script_nesting_level } #[cfg(test)] pub(crate) fn parser_pause_depth(&self) -> usize { self.parser_reentry.pause_depth } #[cfg(test)] pub(crate) fn is_parser_paused_for_reentry(&self) -> bool { self.parser_reentry.pause_depth > 0 } #[cfg(test)] pub(crate) fn parser_insertion_session_depth(&self) -> usize { self.parser_reentry.insertion_session_depth } pub(crate) fn is_parser_insertion_session_active(&self) -> bool { self.parser_reentry.insertion_session_depth > 0 } #[cfg(test)] fn active_parser_step_document_incarnation( &self, ) -> Option { self.parser_reentry .active_step_owner_guard .as_ref() .map(|guard| guard.document_incarnation.clone()) } #[cfg(test)] pub(crate) fn active_parser_step_depth(&self) -> Option { self.parser_reentry .active_step_owner_guard .as_ref() .map(|guard| guard.depth) } pub(crate) fn should_checkpoint_before_parser_custom_element_constructor(&self) -> bool { self.parser_reentry.script_nesting_level == 0 } pub(crate) fn enter_parser_script_nesting(&mut self) -> ParserScriptNestingGuard { let parser_session = self .current_parser_bridge() .map(|bridge| bridge.enter_parser_script_nesting()); self.parser_reentry.script_nesting_level = self .parser_reentry .script_nesting_level .checked_add(1) .expect("parser script nesting level overflow"); ParserScriptNestingGuard { runtime: self as *mut DocumentRuntime, _parser_session: parser_session, } } fn exit_parser_script_nesting(&mut self) { assert!( self.parser_reentry.script_nesting_level > 0, "parser script nesting guard exited without matching enter" ); self.parser_reentry.script_nesting_level -= 1; } pub(crate) fn enter_parser_pause(&mut self) -> ParserPauseGuard { self.parser_reentry.pause_depth = self .parser_reentry .pause_depth .checked_add(1) .expect("parser pause depth overflow"); ParserPauseGuard { runtime: self as *mut DocumentRuntime, } } fn exit_parser_pause(&mut self) { assert!( self.parser_reentry.pause_depth > 0, "parser pause guard exited without matching enter" ); self.parser_reentry.pause_depth -= 1; } pub(crate) fn enter_parser_insertion_session(&mut self) -> ParserInsertionSessionGuard { self.parser_reentry.insertion_session_depth = self .parser_reentry .insertion_session_depth .checked_add(1) .expect("parser insertion session depth overflow"); ParserInsertionSessionGuard { runtime: self as *mut DocumentRuntime, } } fn exit_parser_insertion_session(&mut self) { assert!( self.parser_reentry.insertion_session_depth > 0, "parser insertion session guard exited without matching enter" ); self.parser_reentry.insertion_session_depth -= 1; } pub(crate) fn enter_throw_on_dynamic_markup_insertion(&mut self, document: DomHandle) { *self .parser_reentry .dynamic_markup_insertion_counters .entry(document) .or_insert(0) += 1; } pub(crate) fn exit_throw_on_dynamic_markup_insertion(&mut self, document: DomHandle) { let Some(counter) = self .parser_reentry .dynamic_markup_insertion_counters .get_mut(&document) else { return; }; *counter = counter.saturating_sub(1); if *counter == 0 { self.parser_reentry .dynamic_markup_insertion_counters .remove(&document); } } pub(crate) fn has_throw_on_dynamic_markup_insertion_counter( &self, document: DomHandle, ) -> bool { self.parser_reentry .dynamic_markup_insertion_counters .get(&document) .is_some_and(|counter| *counter > 0) } /// Runs a synchronous parser step against the runtime-owned live DOM. /// /// This keeps the active Document-owner/depth guard owned by `DocumentRuntime` /// and guarantees the guard is released when the step returns or unwinds. #[track_caller] pub(crate) fn with_dom_host_parse_step(&mut self, step: impl FnOnce(&mut Self) -> R) -> R { struct FinishParserStepOnDrop<'a> { runtime: &'a mut DocumentRuntime, } impl Drop for FinishParserStepOnDrop<'_> { fn drop(&mut self) { self.runtime.finish_dom_host_parse_step(); } } self.begin_dom_host_parse_step(); let guard = FinishParserStepOnDrop { runtime: self }; step(&mut *guard.runtime) } /// Starts one synchronous parser step against the runtime-owned live DOM. /// /// Parser sinks do not receive this owner identity. They call back into /// `DocumentRuntime`, and each callback short-borrows the current DomHost /// after this runtime-private active step guard rejects a replaced /// Document incarnation. #[track_caller] pub(crate) fn begin_dom_host_parse_step(&mut self) { let document_incarnation = self.document_incarnation.clone(); if let Some(active) = self.parser_reentry.active_step_owner_guard.as_mut() { assert_eq!( active.document_incarnation, document_incarnation, "nested parser steps must target the current Document incarnation" ); active.depth = active .depth .checked_add(1) .expect("parser step depth overflow"); return; } self.parser_reentry.active_step_owner_guard = Some(ParserStepOwnerGuard { document_incarnation, depth: 1, }); } /// Finishes one synchronous parser step and marks parser-discovered work dirty. #[track_caller] pub(crate) fn finish_dom_host_parse_step(&mut self) { let guard = self .parser_reentry .active_step_owner_guard .as_mut() .expect("parser step must be active before finishing"); assert!( guard.depth > 0, "parser step must be active before finishing" ); guard.depth -= 1; if guard.depth == 0 { self.parser_reentry.active_step_owner_guard.take(); } } #[track_caller] fn assert_active_parser_document_incarnation(&self) { let active = self .parser_reentry .active_step_owner_guard .as_ref() .expect("parser step must be active"); assert_eq!( active.document_incarnation, self.document_incarnation, "parser step must target the current Document incarnation" ); } #[track_caller] fn dom_host_mut_for_active_parser_step(&mut self) -> &mut DomHost { self.assert_active_parser_document_incarnation(); self.dom_host.borrow_mut() } fn enter_structural_mutation(&mut self) -> StructuralMutationGuard { self.structural_mutation_depth = self .structural_mutation_depth .checked_add(1) .expect("structural mutation depth overflow"); StructuralMutationGuard { runtime: self as *mut DocumentRuntime, } } fn exit_structural_mutation(&mut self) { assert!( self.structural_mutation_depth > 0, "structural mutation guard exited without matching enter" ); self.structural_mutation_depth -= 1; } pub(crate) fn is_structural_mutation_active(&self) -> bool { self.structural_mutation_depth > 0 } pub(crate) fn debug_assert_not_in_structural_mutation(&self, action: &str) { debug_assert!( !self.is_structural_mutation_active(), "{action} must run after structural mutation scope exits" ); } #[track_caller] fn append_child_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, ) -> DomMutationEffects { let _guard = self.enter_structural_mutation(); self.dom_host.append_child_effects(parent, child) } #[track_caller] fn insert_before_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, reference_child: Option, ) -> DomMutationEffects { self.insert_before_effects_in_structural_scope_with_state_preservation( parent, child, reference_child, false, ) } #[track_caller] fn insert_before_effects_in_structural_scope_with_state_preservation( &mut self, parent: DomHandle, child: DomHandle, reference_child: Option, is_atomic_move: bool, ) -> DomMutationEffects { let _guard = self.enter_structural_mutation(); if is_atomic_move { self.dom_host .insert_before_preserving_state_effects(parent, child, reference_child) } else { self.dom_host .insert_before_effects(parent, child, reference_child) } } #[track_caller] fn remove_child_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, ) -> DomMutationEffects { self.remove_child_effects_in_structural_scope_with_state_preservation(parent, child, false) } #[track_caller] fn remove_child_effects_in_structural_scope_with_state_preservation( &mut self, parent: DomHandle, child: DomHandle, is_atomic_move: bool, ) -> DomMutationEffects { let _guard = self.enter_structural_mutation(); if is_atomic_move { self.dom_host .remove_child_preserving_state_effects(parent, child) } else { self.dom_host.remove_child_effects(parent, child) } } #[track_caller] fn replace_child_with_self_effects_in_structural_scope( &mut self, parent: DomHandle, old_child: DomHandle, ) -> DomMutationEffects { let _guard = self.enter_structural_mutation(); self.dom_host .replace_child_with_self_effects(parent, old_child) } #[track_caller] fn parser_append_child_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, ) -> DomMutationEffects { self.assert_active_parser_document_incarnation(); self.append_child_effects_in_structural_scope(parent, child) } #[track_caller] fn parser_insert_before_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, reference_child: Option, ) -> DomMutationEffects { self.assert_active_parser_document_incarnation(); self.insert_before_effects_in_structural_scope(parent, child, reference_child) } #[track_caller] fn parser_remove_child_effects_in_structural_scope( &mut self, parent: DomHandle, child: DomHandle, ) -> DomMutationEffects { self.assert_active_parser_document_incarnation(); self.remove_child_effects_in_structural_scope(parent, child) } pub(crate) fn node_is_connected_for_web_api(&self, handle: DomHandle) -> bool { custom_elements::is_shadow_including_rooted_in_document(self.dom_host.borrow(), handle) } } impl std::ops::Deref for LiveRuntimeDomHost { type Target = DomHost; #[track_caller] fn deref(&self) -> &Self::Target { self.borrow() } } impl std::ops::DerefMut for LiveRuntimeDomHost { #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { self.borrow_mut() } } impl super::planning::ParserPlanningReadView for LiveRuntimeDomHost { fn parser_script_read( &self, node_id: NativeNodeId, ) -> Option { self.borrow().parser_script_read(node_id) } fn is_connected(&self, node_id: NativeNodeId) -> bool { self.borrow().is_connected(node_id) } fn script_handles(&self) -> Vec { self.borrow().script_handles() } fn document_order_script_handles(&self) -> Vec { self.borrow().document_order_script_handles() } fn document_order_position(&self, node_id: NativeNodeId) -> Option { self.borrow().document_order_position(node_id) } fn final_url_clone(&self) -> Option { self.borrow().final_url_clone() } fn document_base_url_clone(&self) -> Option { ::document_base_url_clone(self.borrow()) } } impl super::stylesheet_blocking::StylesheetBlockingReadView for LiveRuntimeDomHost { fn media_query_matches(&self, media: &str) -> bool { let document = self .borrow() .node(self.borrow().document_handle()) .and_then(Node::as_document) .expect("live Document"); crate::style_engine::media_list::evaluate_media_query_list_with_context( media, Some(&self.emulated_media), self.style_viewport(), document.base_url(), document.quirks_mode(), ) } fn stylesheet_element( &self, node_id: NativeNodeId, ) -> Option { ::stylesheet_element( self.borrow(), node_id, ) } fn child_ids(&self, node_id: NativeNodeId) -> Vec { ::child_ids( self.borrow(), node_id, ) } fn text_content(&self, node_id: NativeNodeId) -> Option { self.borrow().text_content(node_id) } fn final_url_clone(&self) -> Option { ::final_url_clone( self.borrow(), ) } fn document_base_url_clone(&self) -> Option { ::document_base_url_clone( self.borrow(), ) } fn stylesheet_base_url_clone(&self, node_id: NativeNodeId) -> Option { ::stylesheet_base_url_clone(self.borrow(), node_id) } fn document_node_id(&self) -> NativeNodeId { self.borrow().document_node_id() } fn document_is_quirks_mode(&self) -> bool { ::document_is_quirks_mode( self.borrow(), ) } fn document_order_stylesheet_candidate_ids_before( &self, target_node_id: Option, ) -> Vec { ::document_order_stylesheet_candidate_ids_before( self.borrow(), target_node_id, ) } } #[cfg(test)] mod tests { use std::{collections::HashSet, sync::Arc}; use url::Url; use crate::stylesheet_blocking::StylesheetFetchOptions; use crate::{ dom::{ NodeId, native::{Element, Node}, }, network::ResourceRequestClient, parser::HtmlParser, types::SubresourceResourceType, { document_runtime::DocumentProcessingWakeObservation, document_script_scheduler::{DocumentScriptExecutionLane, PageOwnedDocumentScriptWork}, native_bridge::PendingRuntimeBindingCall, page_task_queue::{PageTask, PostParsePageOwnedWork}, }, }; use moli_fetch::FetchConfig; use super::{ ConnectedLinkReadinessFetchOptions, ConnectedLoadCompletion, ConnectedLoadNetworkResult, ConnectedLoadOperation, ConnectedLoadParameters, ConnectedStyleEventElementKind, DocumentRuntime, DomHandle, DomHost, LiveRuntimeDomHost, ParseTimeWakeObservation, ParseTimeWakeSource, }; fn first_element_handle(document: &crate::dom::native::NativeDom, tag_name: &str) -> DomHandle { let mut stack = vec![document.document_node_id()]; while let Some(handle) = stack.pop() { if document .node(handle) .and_then(Node::as_element) .is_some_and(|element| element.is_html_element(tag_name)) { return handle; } let mut children = document.child_nodes(handle).unwrap_or_default(); children.reverse(); stack.extend(children); } panic!("expected {tag_name} element in test document") } fn parse_document_with_stylesheet_inputs( final_url: Url, html: impl Into, ) -> ( crate::dom::native::NativeDom, Vec, ) { let (document, _, inputs) = crate::parse_html_test_fixture_with_parser_outputs(final_url, html.into()); (document, inputs) } fn preload_load_parameters(url: Url) -> ConnectedLoadParameters { ConnectedLoadParameters::PreloadLikeLink { url, options: Arc::new(ConnectedLinkReadinessFetchOptions { resource_type: SubresourceResourceType::Stylesheet, request_resource_type: Some(moli_fetch::RequestResourceType::CssStyleSheet), script_fetch_metadata: None, request_mode: moli_fetch::RequestMode::NoCors, credentials_mode: moli_fetch::RequestCredentialsMode::Include, fetch_priority_hint: None, link_preload: true, link_fetch_options: StylesheetFetchOptions::default(), }), } } #[test] fn live_runtime_dom_host_forwards_the_canonical_document_base_url() { let document = HtmlParser::SCRIPTING_ENABLED.parse( Url::parse("https://example.test/path/page.html").unwrap(), "".to_owned(), ); let host = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document)); let expected = Url::parse("https://example.test/assets/").unwrap(); assert_eq!( ::document_base_url_clone( &host, ), Some(expected.clone()) ); assert_eq!( ::document_base_url_clone( &host, ), Some(expected) ); } #[test] fn stylesheet_discovery_reads_the_canonical_document_media_environment() { let document = HtmlParser::SCRIPTING_ENABLED.parse( Url::parse("https://example.test/media").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let mut runtime = DocumentRuntime::from_document(document); let blocks = |runtime: &DocumentRuntime| { crate::stylesheet_blocking::stylesheet_link_disposition( &runtime.dom_host, NodeId::new(link.index()), ) .expect("a nonmatching stylesheet must still have a load operation") .is_blocking() }; assert!(blocks(&runtime)); runtime.set_viewport_surface(Some(crate::protocol_types::ViewportSurface { inner_width: 320, inner_height: 200, device_pixel_ratio: 2.0, ..Default::default() })); assert!(!blocks(&runtime)); runtime.dom_host.set_attribute( link, "media", "screen and (width: 320px) and (device-width: 1920px) and (resolution: 2dppx)", ); assert!(blocks(&runtime)); runtime .dom_host .set_attribute(link, "media", "print and (prefers-color-scheme: dark)"); assert!(!blocks(&runtime)); runtime.set_emulated_media(&crate::protocol_types::EmulatedMediaOverrides { media: Some("print".into()), color_scheme: Some("dark".into()), ..Default::default() }); assert!(blocks(&runtime)); runtime.set_emulated_media(&Default::default()); assert!(!blocks(&runtime)); } #[test] fn live_runtime_dom_host_forwards_quirks_mode_for_stylesheet_processing() { let document = HtmlParser::SCRIPTING_ENABLED.parse( Url::parse("https://example.test/page.html").unwrap(), "".to_owned(), ); let link = first_element_handle(&document, "link"); let host = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document)); let disposition = crate::stylesheet_blocking::stylesheet_link_disposition( &host, NodeId::new(link.index()), ) .expect("stylesheet disposition"); assert!(disposition.options().quirks_mode_mime_compatibility()); } #[test] fn dom_host_builds_from_parsed_document_and_updates_text_content() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let div_handle = host.create_element("div"); assert!(host.set_attribute(div_handle, "data-kind", "native")); assert!(host.set_text_content(div_handle, "child")); assert!(host.append_child(body_handle, div_handle)); assert_eq!( host.get_attribute(div_handle, "data-kind"), Some("native".to_owned()) ); assert_eq!(host.text_content(body_handle).as_deref(), Some("seedchild")); } #[test] fn live_runtime_dom_host_direct_borrow_does_not_block_runtime_access() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
".to_owned(), ); let mut live = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document)); let document_handle = live.document_handle(); let child = live.create_element("div"); assert!(live.append_child(document_handle, child)); assert_eq!(live.borrow_mut().document_handle(), document_handle); } #[test] fn parser_reentry_state_tracks_script_pause_insertion_and_dynamic_markup() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut runtime = DocumentRuntime::from_document(document); let document_handle = runtime.document_handle(); assert_eq!(runtime.parser_script_nesting_level(), 0); assert_eq!(runtime.parser_pause_depth(), 0); assert_eq!(runtime.parser_insertion_session_depth(), 0); assert!(!runtime.is_parser_paused_for_reentry()); assert!(!runtime.is_parser_insertion_session_active()); assert!(!runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle)); assert!(runtime.should_checkpoint_before_parser_custom_element_constructor()); { let _outer = runtime.enter_parser_script_nesting(); assert_eq!(runtime.parser_script_nesting_level(), 1); assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor()); { let _inner = runtime.enter_parser_script_nesting(); assert_eq!(runtime.parser_script_nesting_level(), 2); assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor()); } assert_eq!(runtime.parser_script_nesting_level(), 1); assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor()); } assert_eq!(runtime.parser_script_nesting_level(), 0); assert!(runtime.should_checkpoint_before_parser_custom_element_constructor()); { let _pause = runtime.enter_parser_pause(); assert_eq!(runtime.parser_pause_depth(), 1); assert!(runtime.is_parser_paused_for_reentry()); assert!(runtime.should_checkpoint_before_parser_custom_element_constructor()); { let _nested_pause = runtime.enter_parser_pause(); assert_eq!(runtime.parser_pause_depth(), 2); assert!(runtime.is_parser_paused_for_reentry()); } assert_eq!(runtime.parser_pause_depth(), 1); } assert_eq!(runtime.parser_pause_depth(), 0); assert!(!runtime.is_parser_paused_for_reentry()); { let _session = runtime.enter_parser_insertion_session(); assert_eq!(runtime.parser_insertion_session_depth(), 1); assert!(runtime.is_parser_insertion_session_active()); assert!(runtime.should_checkpoint_before_parser_custom_element_constructor()); { let _nested_session = runtime.enter_parser_insertion_session(); assert_eq!(runtime.parser_insertion_session_depth(), 2); assert!(runtime.is_parser_insertion_session_active()); } assert_eq!(runtime.parser_insertion_session_depth(), 1); } assert_eq!(runtime.parser_insertion_session_depth(), 0); assert!(!runtime.is_parser_insertion_session_active()); runtime.enter_throw_on_dynamic_markup_insertion(document_handle); assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle)); runtime.enter_throw_on_dynamic_markup_insertion(document_handle); assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle)); runtime.exit_throw_on_dynamic_markup_insertion(document_handle); assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle)); runtime.exit_throw_on_dynamic_markup_insertion(document_handle); assert!(!runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle)); } #[test] fn structural_mutation_guard_tracks_nested_raw_tree_splices() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut runtime = DocumentRuntime::from_document(document); assert!(!runtime.is_structural_mutation_active()); { let _outer = runtime.enter_structural_mutation(); assert!(runtime.is_structural_mutation_active()); { let _inner = runtime.enter_structural_mutation(); assert!(runtime.is_structural_mutation_active()); } assert!(runtime.is_structural_mutation_active()); } assert!(!runtime.is_structural_mutation_active()); let body = runtime .dom_host .document_body_handle() .expect("body handle"); let child = runtime.dom_host_mut().create_element("div"); let effects = runtime.append_child_effects_in_structural_scope(body, child); assert!(effects.did_change()); assert!(!runtime.is_structural_mutation_active()); } #[test] fn parser_runtime_dom_owner_allows_nested_steps_on_same_document_incarnation() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
".to_owned(), ); let mut runtime = DocumentRuntime::from_document(document); runtime.begin_dom_host_parse_step(); let outer_incarnation = runtime.active_parser_step_document_incarnation(); runtime.begin_dom_host_parse_step(); assert_eq!( runtime.active_parser_step_document_incarnation(), outer_incarnation ); assert_eq!(runtime.active_parser_step_depth(), Some(2)); runtime.finish_dom_host_parse_step(); assert_eq!( runtime.active_parser_step_document_incarnation(), outer_incarnation ); assert_eq!(runtime.active_parser_step_depth(), Some(1)); runtime.finish_dom_host_parse_step(); assert_eq!(runtime.active_parser_step_document_incarnation(), None); assert_eq!(runtime.active_parser_step_depth(), None); } #[test] #[should_panic(expected = "current Document incarnation")] fn parser_runtime_dom_owner_rejects_document_replacement() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
".to_owned(), ); let mut runtime = DocumentRuntime::from_document(document); runtime.begin_dom_host_parse_step(); runtime.open_document(); runtime.parser_runtime_dom_node_exists(runtime.document_handle()); } #[test] fn dom_host_updates_tree_navigation_and_contains_relations() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
one
two
" .to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let children = host.child_nodes(body_handle).expect("body children"); let first = children[0]; let second = children[1]; let inserted = host.create_element("aside"); assert_eq!( host.node(body_handle).and_then(Node::owner_document), Some(host.document_handle()) ); assert_eq!( host.node(first).and_then(Node::parent_node), Some(body_handle) ); assert_eq!( host.node(body_handle).and_then(Node::first_child), Some(first) ); assert_eq!( host.node(body_handle).and_then(Node::last_child), Some(second) ); assert_eq!(host.node(first).and_then(Node::next_sibling), Some(second)); assert_eq!(host.node(second).and_then(Node::prev_sibling), Some(first)); assert!( host.node(body_handle) .is_some_and(|node| node.contains(host.dom(), second)) ); assert!( host.node(first) .is_some_and(|node| node.contains(host.dom(), first)) ); assert!(host.insert_before(body_handle, inserted, Some(second))); assert_eq!( host.node(first).and_then(Node::next_sibling), Some(inserted) ); assert_eq!( host.node(second).and_then(Node::prev_sibling), Some(inserted) ); assert_eq!( host.child_nodes(body_handle) .expect("updated body children"), vec![first, inserted, second] ); assert!(host.remove_child(body_handle, inserted)); assert_eq!( host.child_nodes(body_handle) .expect("body children after removal"), vec![first, second] ); assert_eq!(host.node(inserted).and_then(Node::parent_node), None); assert!( !host .node(inserted) .is_some_and(|node| node.contains(host.dom(), first)) ); } #[test] fn dom_host_reports_unified_mutation_effects() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let fragment = host.create_document_fragment(); let script = host.create_element("script"); let insert_script = host.append_child_effects(fragment, script); assert!(insert_script.did_change()); assert!(insert_script.scripts().connected_roots().is_empty()); assert_eq!(insert_script.scripts().updated_nodes(), &[script]); assert_eq!(insert_script.scripts().prepare_triggers().len(), 1); assert_eq!( insert_script.scripts().prepare_triggers()[0].handle(), script ); assert!(insert_script.tree().disconnected_roots().is_empty()); let connect_fragment = host.append_child_effects(body_handle, fragment); assert!(connect_fragment.did_change()); assert!(connect_fragment.scripts().connected_roots().is_empty()); assert_eq!(connect_fragment.scripts().updated_nodes(), &[script]); assert_eq!(connect_fragment.scripts().prepare_triggers().len(), 1); assert_eq!( connect_fragment.scripts().prepare_triggers()[0].handle(), script ); assert!(connect_fragment.tree().disconnected_roots().is_empty()); let update_script = host.set_attribute_effects(script, "src", "/dynamic.js"); assert!(update_script.did_change()); assert!(update_script.scripts().connected_roots().is_empty()); assert_eq!(update_script.scripts().updated_nodes(), &[script]); assert_eq!(update_script.scripts().prepare_triggers().len(), 1); assert_eq!( update_script.scripts().prepare_triggers()[0].handle(), script ); let disconnect_script = host.remove_child_effects(body_handle, script); assert!(disconnect_script.did_change()); assert!(disconnect_script.scripts().connected_roots().is_empty()); assert!(disconnect_script.scripts().updated_nodes().is_empty()); assert!(disconnect_script.scripts().prepare_triggers().is_empty()); assert_eq!(disconnect_script.tree().disconnected_roots(), &[script]); } #[test] fn dom_host_uses_connected_script_roots_only_for_non_script_subtrees() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let wrapper = host.create_element("div"); let script = host.create_element("script"); let insert_script = host.append_child_effects(wrapper, script); assert!(insert_script.scripts().connected_roots().is_empty()); assert_eq!(insert_script.scripts().prepare_triggers().len(), 1); assert_eq!( insert_script.scripts().prepare_triggers()[0].handle(), script ); let connect_wrapper = host.append_child_effects(body_handle, wrapper); assert_eq!(connect_wrapper.scripts().connected_roots(), &[wrapper]); assert!(connect_wrapper.scripts().prepare_triggers().is_empty()); } #[test] fn dom_host_scans_connected_shadow_trees_for_scripts() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let shadow_host = host.create_element("div"); let shadow_root = host .attach_shadow_root(shadow_host, "open") .expect("shadow root"); let script = host.create_element("script"); let insert_script = host.append_child_effects(shadow_root, script); assert!(insert_script.did_change()); assert_eq!(insert_script.scripts().prepare_triggers().len(), 1); assert_eq!( insert_script.scripts().prepare_triggers()[0].handle(), script ); let connect_host = host.append_child_effects(body_handle, shadow_host); assert_eq!(connect_host.scripts().connected_roots(), &[shadow_host]); assert_eq!(host.connected_script_handles(shadow_host), vec![script]); } #[test] fn dom_host_flattens_nested_slot_fallback_without_losing_siblings() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let shadow_host = host.create_element("section"); assert!(host.append_child(body_handle, shadow_host)); let shadow_root = host .attach_shadow_root(shadow_host, "open") .expect("shadow root"); let outer_slot = host.create_element("slot"); let inner_slot = host.create_element("slot"); let fallback_text = host.create_text_node("hello"); let fallback_span = host.create_element("span"); assert!(host.append_child(shadow_root, outer_slot)); assert!(host.append_child(outer_slot, inner_slot)); assert!(host.append_child(inner_slot, fallback_text)); assert!(host.append_child(outer_slot, fallback_span)); assert_eq!( host.assigned_nodes_for_slot_with_options(outer_slot, false), Vec::new() ); assert_eq!( host.assigned_nodes_for_slot_with_options(outer_slot, true), vec![fallback_text, fallback_span] ); assert_eq!( host.assigned_nodes_for_slot_with_options(inner_slot, false), Vec::new() ); assert_eq!( host.assigned_nodes_for_slot_with_options(inner_slot, true), vec![fallback_text] ); } #[test] fn dom_host_slot_mutation_effects_mark_old_and_new_slots() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let shadow_host = host.create_element("section"); assert!(host.append_child(body_handle, shadow_host)); let shadow_root = host .attach_shadow_root(shadow_host, "open") .expect("shadow root"); let slot_a = host.create_element("slot"); let slot_b = host.create_element("slot"); let child = host.create_element("div"); assert!(host.set_attribute(slot_a, "name", "a")); assert!(host.set_attribute(slot_b, "name", "b")); assert!(host.append_child(shadow_root, slot_a)); assert!(host.append_child(shadow_root, slot_b)); assert!(host.set_attribute(child, "slot", "a")); let insert_effects = host.append_child_effects(shadow_host, child); assert_eq!(insert_effects.slots().changed_slots(), &[slot_a]); assert_eq!(host.assigned_slot_for_node(child), Some(slot_a)); let retarget_effects = host.set_attribute_effects(child, "slot", "b"); let retargeted: HashSet<_> = retarget_effects .slots() .changed_slots() .iter() .map(|handle| handle.index()) .collect(); assert_eq!(retargeted, HashSet::from([slot_a.index(), slot_b.index()])); assert_eq!(host.assigned_slot_for_node(child), Some(slot_b)); let remove_effects = host.remove_child_effects(shadow_host, child); assert_eq!(remove_effects.slots().changed_slots(), &[slot_b]); assert_eq!(host.assigned_slot_for_node(child), None); } #[test] fn dom_host_move_effects_mark_shadow_slot_changes() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let shadow_host = host.create_element("section"); assert!(host.append_child(body_handle, shadow_host)); let shadow_root = host .attach_shadow_root(shadow_host, "open") .expect("shadow root"); let slot = host.create_element("slot"); let slottable = host.create_element("p"); assert!(host.set_attribute(slot, "name", "content")); assert!(host.set_attribute(slottable, "slot", "content")); assert!(host.append_child(shadow_root, slot)); assert!(host.append_child(shadow_host, slottable)); assert_eq!( host.assigned_nodes_for_slot_with_options(slot, false), vec![slottable] ); let move_out_effects = host.insert_before_effects(body_handle, slottable, None); assert_eq!(move_out_effects.slots().changed_slots(), &[slot]); assert_eq!( host.assigned_nodes_for_slot_with_options(slot, false), Vec::new() ); let move_in_effects = host.insert_before_effects(shadow_host, slottable, None); assert_eq!(move_in_effects.slots().changed_slots(), &[slot]); assert_eq!( host.assigned_nodes_for_slot_with_options(slot, false), vec![slottable] ); let fallback = host.create_element("span"); assert!(host.append_child(shadow_root, fallback)); let fallback_in_effects = host.insert_before_effects(slot, fallback, None); assert_eq!(fallback_in_effects.slots().changed_slots(), &[slot]); let fallback_out_effects = host.insert_before_effects(shadow_root, fallback, None); assert_eq!(fallback_out_effects.slots().changed_slots(), &[slot]); let slot_out_effects = host.insert_before_effects(body_handle, slot, None); assert_eq!(slot_out_effects.slots().changed_slots(), &[slot]); let slot_in_effects = host.insert_before_effects(shadow_root, slot, None); assert_eq!(slot_in_effects.slots().changed_slots(), &[slot]); } #[test] fn document_runtime_replace_live_document_preserves_shadow_root_bindings() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let body_handle = document.document_body_handle().expect("body handle"); let shadow_host = runtime.dom_host_mut().create_element("section"); assert!( runtime .dom_host_mut() .append_child(body_handle, shadow_host) ); let shadow_root = runtime .dom_host_mut() .attach_shadow_root(shadow_host, "open") .expect("shadow root"); let shadow_child = runtime.dom_host_mut().create_element("span"); assert!( runtime .dom_host_mut() .append_child(shadow_root, shadow_child) ); let parser_snapshot = runtime.snapshot_document(); runtime.replace_live_document(&parser_snapshot); assert_eq!( runtime.dom_host().shadow_root_handle(shadow_host), Some(shadow_root) ); assert_eq!( runtime .dom_host() .child_handles(shadow_root) .collect::>(), vec![shadow_child] ); } #[test] fn document_runtime_replace_live_document_preserves_script_already_started() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let script = document .script_handles() .first() .copied() .expect("parser-created script handle"); assert!( runtime .dom_host_mut() .set_script_already_started(script, true), "live runtime should allow marking parser-created script as already-started" ); let replacement_document = document.clone(); runtime.replace_live_document(&replacement_document); assert!( runtime .snapshot_document() .node(script) .and_then(|node| node.as_element()) .is_some_and(|element| element.script_already_started()), "document replacement should preserve runtime-owned already-started state" ); } #[test] fn document_runtime_can_mark_script_already_started_by_node_id() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let script = document .script_handles() .first() .copied() .expect("parser-created script handle"); let node_id = NodeId::new(script.index()); assert!( runtime.mark_script_already_started_by_node_id(node_id), "marking by node id should resolve the live runtime script handle" ); assert!( runtime .snapshot_document() .node(script) .and_then(|node| node.as_element()) .is_some_and(|element| element.script_already_started()), "marking by node id should project into the live DOM snapshot" ); } #[test] fn document_runtime_sets_parser_script_force_async_like_chrome() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let parser_scripts = document.script_handles(); let parser_script = parser_scripts .first() .copied() .expect("parser-created script handle"); let parser_async_script = parser_scripts .get(1) .copied() .expect("parser-created async script handle"); let dynamic_script = runtime.dom_host_mut().create_element("script"); assert!( !runtime .dom_host() .node(parser_script) .and_then(Node::as_element) .is_some_and(Element::script_async) ); assert!( runtime .dom_host() .node(parser_async_script) .and_then(Node::as_element) .is_some_and(Element::script_async) ); assert!( runtime .dom_host() .node(dynamic_script) .and_then(Node::as_element) .is_some_and(Element::script_async) ); } #[test] fn document_runtime_snapshot_projects_live_dom_without_reparse() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
" .to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let body_handle = document.document_body_handle().expect("body handle"); let script = runtime.dom_host_mut().create_element("script"); let text = runtime.dom_host_mut().create_text_node("console.log('ok')"); let section = runtime.dom_host_mut().create_element("section"); assert!( runtime .dom_host_mut() .set_attribute(section, "data-state", "live") ); assert!(runtime.dom_host_mut().set_text_content(section, "fresh")); assert!(runtime.dom_host_mut().append_child(body_handle, section)); assert!(runtime.dom_host_mut().append_child(script, text)); assert!(runtime.dom_host_mut().append_child(body_handle, script)); let snapshot = runtime.snapshot_document(); let body_id = snapshot.body_node_id().expect("snapshot body"); let body_html = snapshot.outer_html(body_id).expect("snapshot body html"); assert!(snapshot.parse_errors().is_empty()); assert!(body_html.contains("
fresh
")); assert!(body_html.contains("")); assert_eq!(snapshot.script_node_ids().len(), 1); } #[test] fn parser_roundtrip_through_live_runtime_keeps_style_import_text_before_later_script() { let parser = HtmlParser::SCRIPTING_ENABLED; let stream = parser.start_document(Url::parse("https://example.com/").unwrap()); let html = "
late
"; let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) = stream.pump_parser_step(html).result else { panic!("expected first script handoff"); }; let first_document = stream.take_parser_stream_dom_host().into_dom(); let mut runtime = DocumentRuntime::new(&first_document); runtime.replace_live_document_with_document(first_document); stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document())); let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) = stream.pump_parser_step("").result else { panic!("expected second script handoff"); }; let second_document = stream.take_parser_stream_dom_host().into_dom(); runtime.replace_live_document_with_document(second_document); stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document())); let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(third_handoff)) = stream.pump_parser_step("").result else { panic!("expected third script handoff"); }; let third_handoff = third_handoff.node_id(); let third_snapshot = stream.snapshot_parser_stream_document(); let style_node_id = third_snapshot .nodes() .iter() .find(|node| node.is_html_element_named("style")) .map(|node| node.id()) .expect("expected style element"); assert_eq!( third_snapshot.text_content(style_node_id).as_deref(), Some("@import url('/slow.css');") ); assert_eq!( third_snapshot.script_src(third_handoff), Some("/blocking.js") ); assert!( third_snapshot .node(third_handoff) .is_some_and(|node| node.flags().parser_created()), "external script should remain parser-created after live roundtrip" ); assert!( third_snapshot.document_body_handle().is_none(), "later body content should still be hidden at the external script handoff" ); } #[test] fn parser_roundtrip_after_live_dom_mutation_keeps_later_body_hidden() { let parser = HtmlParser::SCRIPTING_ENABLED; let stream = parser.start_document(Url::parse("https://example.com/").unwrap()); let html = "
late
"; let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) = stream.pump_parser_step(html).result else { panic!("expected first script handoff"); }; let first_document = stream.take_parser_stream_dom_host().into_dom(); let mut runtime = DocumentRuntime::new(&first_document); runtime.replace_live_document_with_document(first_document); let head = runtime .dom_host() .document_head_handle() .expect("head should exist before runtime mutation"); let link = runtime.dom_host_mut().create_element("link"); assert!( runtime .dom_host_mut() .set_attribute(link, "rel", "stylesheet") ); assert!( runtime .dom_host_mut() .set_attribute(link, "href", "/runtime.css") ); assert!(runtime.dom_host_mut().append_child(head, link)); stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document())); let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script( second_handoff, )) = stream.pump_parser_step("").result else { panic!("expected second script handoff"); }; let second_handoff = second_handoff.node_id(); let second_snapshot = stream.snapshot_parser_stream_document(); assert_eq!( second_snapshot.script_text(second_handoff).as_deref(), Some("window.after = 1;") ); assert!( second_snapshot.document_body_handle().is_none(), "later body content should remain hidden after live DOM mutation roundtrip" ); } #[test] fn precedes_in_document_order_handles_disconnected_roots_without_underflow() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
".to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let live = runtime .dom_host() .element_handle_by_id("live") .expect("live node in document"); let fragment = runtime.dom_host_mut().create_document_fragment(); let detached = runtime.dom_host_mut().create_element("div"); assert!(runtime.dom_host_mut().append_child(fragment, detached)); assert!(!runtime.precedes_in_document_order(live, detached)); assert!(!runtime.precedes_in_document_order(detached, live)); } #[test] fn dom_host_import_and_adopt_node_preserve_owner_document_and_parentage() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
seed
" .to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let document_handle = document.document_node_id(); let container = host.element_handle_by_id("container").expect("container"); let seed = host.element_handle_by_id("seed").expect("seed"); let imported = host .import_node(document_handle, container, true) .expect("imported clone"); assert_ne!(imported, container); assert_eq!( host.node(imported).and_then(Node::owner_document), Some(document_handle) ); assert_eq!(host.node(imported).and_then(Node::parent_node), None); let imported_children = host.child_nodes(imported).expect("imported children"); assert_eq!(imported_children.len(), 1); assert_ne!(imported_children[0], seed); assert_eq!( host.node(imported_children[0]) .and_then(Node::owner_document), Some(document_handle) ); assert!(host.remove_child(container, seed)); let adopted = host .adopt_node(document_handle, seed) .expect("adopted node"); assert_eq!(adopted, seed); assert_eq!(host.node(seed).and_then(Node::parent_node), None); assert_eq!( host.node(seed).and_then(Node::owner_document), Some(document_handle) ); } #[test] fn dom_host_rebuilds_id_lookup_after_id_attribute_mutation() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
one
two
" .to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let children = host.child_nodes(body_handle).expect("body children"); let first = children[0]; let second = children[1]; assert_eq!(host.element_handle_by_id("one"), Some(first)); assert!(host.set_attribute(second, "id", "two")); assert_eq!(host.element_handle_by_id("two"), Some(second)); assert!(host.remove_attribute(first, "id")); assert_eq!(host.element_handle_by_id("one"), None); } #[test] fn dom_host_invalidates_live_collection_cache_on_tree_and_attribute_mutations() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let body_handle = document.document_body_handle().expect("body handle"); let main = host .child_nodes(body_handle) .expect("body children") .into_iter() .next() .expect("seed child"); assert_eq!( host.resolve_live_collection(body_handle, "children", None, false) .expect("children collection") .len(), 1 ); let aside = host.create_element("aside"); assert!(host.append_child(body_handle, aside)); assert_eq!( host.resolve_live_collection(body_handle, "children", None, false) .expect("children collection after append") .len(), 2 ); assert_eq!( host.resolve_live_collection(body_handle, "className", Some("active"), false) .expect("class collection before attr change") .len(), 0 ); assert!(host.set_attribute(main, "class", "active")); assert_eq!( host.resolve_live_collection(body_handle, "className", Some("active"), false) .expect("class collection after attr change") .len(), 1 ); } #[test] fn dom_host_normalize_merges_adjacent_text_nodes_recursively() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "
".to_owned(), ); let mut host = DomHost::from_dom(document.clone()); let container = host.element_handle_by_id("container").expect("container"); let first = host.create_text_node("a"); let empty = host.create_text_node(""); let second = host.create_text_node("b"); let nested = host.create_element("section"); let nested_first = host.create_text_node("x"); let nested_empty = host.create_text_node(""); let nested_second = host.create_text_node("y"); assert!(host.append_child(container, first)); assert!(host.append_child(container, empty)); assert!(host.append_child(container, second)); assert!(host.append_child(container, nested)); assert!(host.append_child(nested, nested_first)); assert!(host.append_child(nested, nested_empty)); assert!(host.append_child(nested, nested_second)); let effects = host.normalize_effects(container); assert!(effects.did_change()); let children = host.child_nodes(container).expect("container children"); assert_eq!(children.len(), 2); assert_eq!(host.text_content(children[0]).as_deref(), Some("ab")); let nested_children = host.child_nodes(children[1]).expect("nested children"); assert_eq!(nested_children.len(), 1); assert_eq!(host.text_content(nested_children[0]).as_deref(), Some("xy")); } #[tokio::test] async fn stylesheet_typed_task_is_ready_for_document_processing() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new(); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let link = first_element_handle(&document, "link"); let stylesheet_url = Url::parse("data:text/css,body%7B%7D").unwrap(); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); runtime .stylesheet_lifecycle .fetches .enqueue_completion_for_testing( crate::dom::NodeId::new(link.index()), stylesheet_url, true, ); assert_eq!( runtime .observe_document_processing_wake(&mut task_queue) .await, DocumentProcessingWakeObservation::ReadyNow, "a stylesheet completion published before observation must remain visible as typed Networking work" ); } #[tokio::test] async fn injected_page_task_can_wake_parse_time_turn_without_timeout_fallback() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/").unwrap(), "".to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new(); let sender = task_queue.parser_boundary_sender(); tokio::spawn(async move { let _ = sender.send(PageTask::DispatchDomContentLoaded); }); assert_eq!( runtime .wait_for_parse_time_turn_arrival(&mut task_queue) .await, ParseTimeWakeObservation::Arrived(ParseTimeWakeSource::InjectedPageTask) ); task_queue.accept_ready_parse_time_wakes(); assert!(matches!( task_queue.parse_time_pop_front(), Some(PageTask::DispatchDomContentLoaded) )); } #[tokio::test] async fn stylesheet_completion_with_link_owner_surfaces_ready_parse_time_work() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new(); let head = document.document_head_handle().expect("head handle"); let link = document .child_nodes(head) .expect("head children") .into_iter() .find(|handle| { document .node(*handle) .and_then(Node::as_element) .is_some_and(|element| element.is_html_element("link")) }) .expect("stylesheet link"); let link_node_id = crate::dom::NodeId::new(link.index()); let stylesheet_url = Url::parse("data:text/css,body%7B%7D").unwrap(); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); runtime .stylesheet_lifecycle .fetches .enqueue_completion_for_testing(link_node_id, stylesheet_url, true); assert_eq!( runtime .wait_for_parse_time_turn_arrival(&mut task_queue) .await, ParseTimeWakeObservation::ReadyNow, "the resource wake is not a parse-time source, but draining it makes the link event ready" ); } #[tokio::test] async fn draining_blocking_stylesheet_completion_persists_resolved_state() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); assert!(!runtime.has_all_blocking_stylesheets_resolved()); let node_id = NodeId::new(first_element_handle(&document, "link").index()); let stylesheet_url = Url::parse("https://example.com/app.css").unwrap(); runtime .stylesheet_lifecycle .fetches .enqueue_completion_for_testing(node_id, stylesheet_url, true); runtime.drain_blocking_stylesheet_completions(); assert!( runtime.has_all_blocking_stylesheets_resolved(), "drained blocking stylesheet completions should persist as resolved state" ); } #[tokio::test(flavor = "current_thread")] async fn response_csp_blocks_ownerless_stylesheet_admission() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "".to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.set_response_content_security_policies(&["style-src 'none'".to_owned()]); assert!( runtime .preload_stylesheet( Url::parse("https://example.com/speculative.css").unwrap(), crate::stylesheet_blocking::StylesheetFetchOptions::default(), ) .is_none(), "response policy admission must run before creating an ownerless resource" ); assert!(runtime.take_ready_stylesheet_network_results().is_empty()); } #[tokio::test] async fn typed_stylesheet_claim_preserves_blocking_resolution_state() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); let node_id = NodeId::new(first_element_handle(&document, "link").index()); let stylesheet_url = Url::parse("https://example.com/app.css").unwrap(); runtime .stylesheet_lifecycle .fetches .enqueue_completion_for_testing(node_id, stylesheet_url, true); assert!( runtime.wait_for_stylesheet_networking_task_for_test().await, "the exact blocking terminal must remain resident in the typed Networking source" ); assert!( runtime.apply_next_stylesheet_networking_task_for_test(), "the test must claim the same typed task consumed by a live Page" ); assert!( runtime.has_all_blocking_stylesheets_resolved(), "claiming the typed task must persist the canonical blocking resolution" ); } #[test] fn stale_connected_stylesheet_network_result_releases_source_owner() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let stale_url = Url::parse("https://example.com/old.css").unwrap(); let mut runtime = DocumentRuntime::new(&document); runtime .stylesheet_lifecycle .ready_connected_load_network_results .push_back(ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document.final_url().unwrap().clone(), request_url: stale_url.clone(), source_owners: vec![link], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("stale connected stylesheet result".to_owned()), }); let results = runtime.take_ready_stylesheet_network_results(); assert_eq!(results.len(), 1); let result = results.into_iter().next().unwrap(); assert_eq!(result.request_url, stale_url); assert!( result.source_owners.is_empty(), "stale connected stylesheet results must not retain a source owner" ); } #[tokio::test] async fn active_link_load_keeps_physical_observation_separate_from_client_terminal() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let current_url = Url::parse("https://example.com/current.css").unwrap(); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); let active_load = runtime .active_stylesheet_link_client_for_test(link) .expect("stylesheet processing should create an owner-bound load"); runtime .stylesheet_lifecycle .ready_connected_load_network_results .push_back(ConnectedLoadNetworkResult { stylesheet_fetch: Some(active_load.fetch().clone()), blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document.final_url().unwrap().clone(), request_url: current_url.clone(), source_owners: vec![link], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("current connected stylesheet result".to_owned()), }); let results = runtime.take_ready_stylesheet_network_results(); assert_eq!(results.len(), 1); let result = results.into_iter().next().unwrap(); assert_eq!(result.request_url, current_url); assert!( result.source_owners.is_empty(), "typed physical observations must not double as stylesheet client delivery" ); } #[tokio::test] async fn removed_link_rejects_its_detached_load_network_source_owner() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); let detached_load = runtime .active_stylesheet_link_client_for_test(link) .expect("active stylesheet load"); let parent = runtime .dom_host() .parent_node(link) .expect("link should have a parent"); assert!(runtime.dom_host_mut().remove_child(parent, link)); drop(runtime.invalidate_style_related_state(link)); runtime .stylesheet_lifecycle .ready_connected_load_network_results .push_back(ConnectedLoadNetworkResult { stylesheet_fetch: Some(detached_load.fetch().clone()), blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: detached_load.fetch().document_url().clone(), request_url: detached_load.request_url().clone(), source_owners: vec![link], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("detached load result".to_owned()), }); let result = runtime .take_ready_stylesheet_network_results() .into_iter() .next() .expect("resource observation result"); assert!(result.source_owners.is_empty()); } #[tokio::test] async fn disabled_link_rejects_its_detached_load_network_source_owner() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); let detached_load = runtime .active_stylesheet_link_client_for_test(link) .expect("active stylesheet load"); assert!(runtime.dom_host_mut().set_attribute(link, "disabled", "")); drop(runtime.invalidate_style_related_state(link)); runtime.queue_connected_style_loads(link); runtime.prime_document_lifecycle_processing(); assert!( runtime .active_stylesheet_link_client_for_test(link) .is_none() ); runtime .stylesheet_lifecycle .ready_connected_load_network_results .push_back(ConnectedLoadNetworkResult { stylesheet_fetch: Some(detached_load.fetch().clone()), blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: detached_load.fetch().document_url().clone(), request_url: detached_load.request_url().clone(), source_owners: vec![link], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("disabled load result".to_owned()), }); let result = runtime .take_ready_stylesheet_network_results() .into_iter() .next() .expect("resource observation result"); assert!(result.source_owners.is_empty()); } #[tokio::test] async fn same_url_reprocess_rejects_the_detached_owner_bound_load_object() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); let first_a_load = runtime .active_stylesheet_link_client_for_test(link) .expect("first A processing load"); drop(runtime.invalidate_style_related_state(link)); assert!(runtime.dom_host_mut().set_attribute(link, "href", "/b.css")); runtime.queue_connected_style_loads(link); runtime.prime_document_lifecycle_processing(); let b_load = runtime .active_stylesheet_link_client_for_test(link) .expect("B processing load"); drop(runtime.invalidate_style_related_state(link)); assert!(runtime.dom_host_mut().set_attribute(link, "href", "/a.css")); runtime.queue_connected_style_loads(link); runtime.prime_document_lifecycle_processing(); let second_a_load = runtime .active_stylesheet_link_client_for_test(link) .expect("second A processing load"); assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq( &first_a_load, &b_load )); assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq( &first_a_load, &second_a_load )); assert_eq!(first_a_load.request_url(), second_a_load.request_url()); assert!( first_a_load.fetch().ptr_eq(second_a_load.fetch()), "a new owner processing client should reuse the compatible document resource" ); assert!( !runtime.accept_stylesheet_link_client_completion_for_test(link, &first_a_load, true,), "an old A load object must not complete the new A processing" ); } #[tokio::test] async fn linked_stylesheet_request_attribute_reprocess_captures_new_fetch_options() { let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( Url::parse("https://example.com/page").unwrap(), "" .to_owned(), ); let link = first_element_handle(&document, "link"); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); let first_load = runtime .active_stylesheet_link_client_for_test(link) .expect("first processing load"); assert_eq!( first_load.fetch().options().cross_origin(), Some("anonymous") ); assert_eq!( first_load.fetch().options().integrity(), Some("sha256-first") ); drop(runtime.invalidate_style_related_state(link)); assert!( runtime .dom_host_mut() .set_attribute(link, "crossorigin", "use-credentials") ); assert!( runtime .dom_host_mut() .set_attribute(link, "integrity", "sha256-second") ); runtime.queue_connected_style_loads(link); runtime.prime_document_lifecycle_processing(); let second_load = runtime .active_stylesheet_link_client_for_test(link) .expect("reprocessed load"); assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq( &first_load, &second_load )); assert!(!first_load.fetch().ptr_eq(second_load.fetch())); assert_eq!( second_load.fetch().options().cross_origin(), Some("use-credentials") ); assert_eq!( second_load.fetch().options().integrity(), Some("sha256-second") ); } #[tokio::test(flavor = "multi_thread")] async fn link_blocking_stylesheet_fetch_is_shared_across_document_lifecycle_state() { use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, time::{Duration, timeout}, }; let hits = Arc::new(AtomicUsize::new(0)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server_hits = Arc::clone(&hits); let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.unwrap(); let mut buffer = [0_u8; 1024]; let _ = socket.read(&mut buffer).await.unwrap(); server_hits.fetch_add(1, Ordering::SeqCst); socket .write_all( b"HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nContent-Length: 20\r\nConnection: close\r\n\r\nbody { color: red; }", ) .await .unwrap(); }); let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse(&format!("http://{addr}/page")).unwrap(), "".to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); let node_id = NodeId::new(first_element_handle(&document, "link").index()); let stylesheet_url = Url::parse(&format!("http://{addr}/app.css")).unwrap(); timeout(Duration::from_secs(2), async { loop { runtime.drain_blocking_stylesheet_completions(); let shared_resolved = runtime .stylesheet_lifecycle .fetches .status(node_id, &stylesheet_url) .is_some(); if shared_resolved && runtime.has_all_blocking_stylesheets_resolved() { break; } tokio::task::yield_now().await; } }) .await .expect("stylesheet fetch should resolve"); assert_eq!( hits.load(Ordering::SeqCst), 1, "link stylesheet fetch should be shared between connected-style and document-owned blocker state" ); server.abort(); } #[tokio::test(flavor = "multi_thread")] async fn link_stylesheet_fetch_respects_nosniff_mime_blocking() { use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, time::{Duration, timeout}, }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { const BODY: &str = "body { color: red; }"; let (mut socket, _) = listener.accept().await.unwrap(); let mut buffer = [0_u8; 1024]; let _ = socket.read(&mut buffer).await.unwrap(); socket .write_all( format!( "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Content-Type-Options: nosniff\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", BODY.len(), BODY ) .as_bytes(), ) .await .unwrap(); }); let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse(&format!("http://{addr}/page")).unwrap(), "".to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); let node_id = NodeId::new(first_element_handle(&document, "link").index()); let stylesheet_url = Url::parse(&format!("http://{addr}/app.css")).unwrap(); let status = timeout(Duration::from_secs(2), async { loop { runtime.drain_blocking_stylesheet_completions(); if let Some(status) = runtime .stylesheet_lifecycle .fetches .status(node_id, &stylesheet_url) { break status; } tokio::task::yield_now().await; } }) .await .expect("stylesheet fetch should resolve"); runtime.drain_blocking_stylesheet_completions(); assert!( !status, "nosniff text/html stylesheet should fail readiness" ); assert!( runtime.has_all_blocking_stylesheets_resolved(), "failed stylesheet readiness must unblock script scheduling" ); server.abort(); } #[tokio::test(flavor = "multi_thread")] async fn parser_created_style_import_network_results_are_drained() { use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, time::{Duration, timeout}, }; const STYLESHEET_BODY: &str = "body { color: blue; }"; let hits = Arc::new(AtomicUsize::new(0)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server_hits = Arc::clone(&hits); let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.unwrap(); let mut buffer = [0_u8; 1024]; let _ = socket.read(&mut buffer).await.unwrap(); server_hits.fetch_add(1, Ordering::SeqCst); socket .write_all( format!( "HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", STYLESHEET_BODY.len(), STYLESHEET_BODY ) .as_bytes(), ) .await .unwrap(); }); let document_url = Url::parse(&format!("http://{addr}/page")).unwrap(); let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( document_url.clone(), "".to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let script_node_id = NodeId::new(document.script_handles()[0].index()); let inputs = moli_stylesheet_blocking::collect_document_owned_blocking_stylesheets_before( &document, script_node_id, ) .into_iter() .map(|blocker| crate::DocumentOwnedStylesheetDiscoveryInput::from(&blocker)) .collect::>(); assert_eq!( inputs.len(), 1, "expected one parser-created style import blocker" ); runtime.note_discovered_document_owned_stylesheet_inputs(inputs.iter()); let results = timeout(Duration::from_secs(2), async { loop { let results = runtime.take_ready_stylesheet_network_results(); if !results.is_empty() { break results; } tokio::task::yield_now().await; } }) .await .expect("parser-created style import should resolve"); assert_eq!(results.len(), 1); let result = results.into_iter().next().unwrap(); let result_document_url = result.document_url; let request_url = result.request_url; let source_owners = result.source_owners; let result = result.result; assert_eq!(result_document_url, document_url); assert_eq!( request_url, Url::parse(&format!("http://{addr}/imported.css")).unwrap() ); assert!( source_owners.is_empty(), "physical parser-blocking terminals must not carry opportunistic live-owner authority" ); let response = result.expect("stylesheet import should succeed"); assert_eq!(response.status, 200); assert_eq!(response.body_text(), STYLESHEET_BODY); assert_eq!(hits.load(Ordering::SeqCst), 1); server.abort(); } #[tokio::test(flavor = "multi_thread")] async fn parser_created_style_import_respects_nosniff_mime_blocking() { use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, time::{Duration, timeout}, }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { const BODY: &str = "body { color: blue; }"; let (mut socket, _) = listener.accept().await.unwrap(); let mut buffer = [0_u8; 1024]; let _ = socket.read(&mut buffer).await.unwrap(); socket .write_all( format!( "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Content-Type-Options: nosniff\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", BODY.len(), BODY ) .as_bytes(), ) .await .unwrap(); }); let document_url = Url::parse(&format!("http://{addr}/page")).unwrap(); let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( document_url.clone(), "".to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let script_node_id = NodeId::new(document.script_handles()[0].index()); let inputs = moli_stylesheet_blocking::collect_document_owned_blocking_stylesheets_before( &document, script_node_id, ) .into_iter() .map(|blocker| crate::DocumentOwnedStylesheetDiscoveryInput::from(&blocker)) .collect::>(); assert_eq!( inputs.len(), 1, "expected one parser-created style import blocker" ); runtime.note_discovered_document_owned_stylesheet_inputs(inputs.iter()); let results = timeout(Duration::from_secs(2), async { loop { let results = runtime.take_ready_stylesheet_network_results(); if !results.is_empty() { break results; } tokio::task::yield_now().await; } }) .await .expect("parser-created style import should resolve"); assert_eq!(results.len(), 1); let result = results.into_iter().next().unwrap(); let blocking_operation = result .blocking_operation .expect("parser-created import should retain its blocking operation"); let request_url = result.request_url; let source_owners = result.source_owners; let result = result.result; assert_eq!( request_url, Url::parse(&format!("http://{addr}/imported.css")).unwrap() ); assert!( source_owners.is_empty(), "an unusable physical terminal must remain separate from its one-shot install authority" ); let response = result.expect("the HTTP response remains a physical network success"); assert_eq!(response.status, 200); assert_eq!( runtime .stylesheet_lifecycle .fetches .status_for_blocking_operation(&blocking_operation), Some(crate::stylesheet_blocking::StylesheetBlockingStatus::Failed), "nosniff text/html must fail stylesheet usability while retaining the HTTP response" ); assert!( runtime.has_all_blocking_stylesheets_resolved(), "failed import readiness must unblock script scheduling" ); server.abort(); } #[tokio::test(flavor = "multi_thread")] async fn connected_style_import_failure_marks_style_load_error() { use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::TcpListener, time::{Duration, timeout}, }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { let (mut socket, _) = listener.accept().await.unwrap(); let mut buffer = [0_u8; 1024]; let _ = socket.read(&mut buffer).await.unwrap(); const BODY: &str = "missing"; socket .write_all( format!( "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", BODY.len(), BODY ) .as_bytes(), ) .await .unwrap(); }); let document_url = Url::parse(&format!("http://{addr}/page")).unwrap(); let parser = HtmlParser::SCRIPTING_ENABLED; let document = parser.parse( document_url.clone(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new(&document); let document_loader = crate::network::context::DocumentResourceLoader::new( loader.clone(), crate::network::RendererResourceTaskRunner::from_current_tokio() .expect("stylesheet test runtime"), crate::network::context::DocumentFetchContext::new( crate::native_bridge::WindowDocumentOwner::Frame( super::runtime_core::test_stylesheet_document_owner(), ), document_url.clone(), document_url.clone(), moli_url::origin_ascii_serialization(&document_url), ), ); runtime.install_standalone_document_resource_loader(&document_loader); let style = document .document_head_handle() .and_then(|head| document.child_nodes(head)) .and_then(|children| { children.into_iter().find(|handle| { document .node(*handle) .and_then(Node::as_element) .is_some_and(|element| element.is_html_element("style")) }) }) .expect("style handle"); runtime.queue_initial_connected_style_loads(); runtime.prime_document_lifecycle_processing(); assert!( timeout( Duration::from_secs(2), runtime.wait_for_stylesheet_networking_task_for_test() ) .await .expect("connected style import should resolve"), "the stylesheet Networking source must remain open" ); assert!( runtime.apply_ready_stylesheet_networking_tasks_for_test(), "the completed import must publish typed Networking work" ); let ready = runtime .pop_ready_connected_style_load() .expect("the import completion must publish its typed style event"); assert_eq!(ready.owner(), style); assert!(!ready.successful()); let results = runtime.take_ready_stylesheet_network_results(); assert_eq!(results.len(), 1); let result = results.into_iter().next().unwrap(); assert_eq!( result.request_url, Url::parse(&format!("http://{addr}/missing.css")).unwrap() ); let response = result .result .expect("HTTP failure must retain its physical response"); assert_eq!(response.status, 404); assert_eq!(response.body_text(), "missing"); server.abort(); } #[test] fn drained_preload_like_network_result_still_drives_load_event_result() { let parser = HtmlParser::SCRIPTING_ENABLED; let document_url = Url::parse("https://example.com/page").unwrap(); let request_url = Url::parse("https://example.com/chunk.js").unwrap(); let document = parser.parse( document_url.clone(), "" .to_owned(), ); let mut runtime = DocumentRuntime::new(&document); let link = document .document_head_handle() .and_then(|head| document.child_nodes(head)) .and_then(|children| { children.into_iter().find(|handle| { document .node(*handle) .and_then(Node::as_element) .is_some_and(|element| element.is_html_element("link")) }) }) .expect("link handle"); let operation = ConnectedLoadOperation::new_with_load_event_binding( link, crate::document_runtime::ConnectedStyleEventElementKind::Link, preload_load_parameters(request_url.clone()), None, Some( crate::frame_owner_model::MainDocumentStyleLoadEventBinding::unowned_for_document_runtime_test( link, ), ), ); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(&operation)); runtime.apply_connected_style_load_completion(ConnectedLoadCompletion { operation, successful: false, network_results: vec![ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document_url.clone(), request_url: request_url.clone(), source_owners: vec![link], resource_type: SubresourceResourceType::Script, start_unix_millis: None, origin_clean: true, result: Err("preload failed".to_owned()), }], }); runtime.prime_document_lifecycle_processing(); let ready = runtime .pop_ready_connected_style_load() .expect("failed preload event task"); assert_eq!(ready.owner(), link); assert!(!ready.successful()); let results = runtime.take_ready_stylesheet_network_results(); assert_eq!(results.len(), 1); assert!( runtime .stylesheet_lifecycle .ready_connected_load_network_results .is_empty() ); } #[test] fn same_parameter_connected_completion_cannot_cross_operation_identity() { let parser = HtmlParser::SCRIPTING_ENABLED; let document_url = Url::parse("https://example.com/page").unwrap(); let request_url = Url::parse("https://example.com/a.css").unwrap(); let document = parser.parse( document_url.clone(), "" .to_owned(), ); let owner = first_element_handle(&document, "link"); let inline_source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new( owner, "@import url(a.css);".to_owned(), document_url.clone(), )); for parameters in [ preload_load_parameters(request_url.clone()), ConnectedLoadParameters::StyleImports { source: super::stylesheet_runtime::InlineStyleImportSource::new(Arc::clone( &inline_source, )), urls: vec![request_url.clone()], roots: Vec::new(), }, ] { let mut runtime = DocumentRuntime::new(&document); let first_a = ConnectedLoadOperation::new_for_test( owner, ConnectedStyleEventElementKind::Link, parameters.clone(), None, ); let middle_b = ConnectedLoadOperation::new_for_test( owner, ConnectedStyleEventElementKind::Link, preload_load_parameters(Url::parse("https://example.com/b.css").unwrap()), None, ); let current_a = ConnectedLoadOperation::new_for_test( owner, ConnectedStyleEventElementKind::Link, parameters, None, ); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(&first_a)); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(middle_b); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(¤t_a)); runtime.apply_connected_style_load_completion(ConnectedLoadCompletion { operation: first_a, successful: true, network_results: vec![ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document_url.clone(), request_url: request_url.clone(), source_owners: vec![owner], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("stale A completion".to_owned()), }], }); assert!( runtime .stylesheet_lifecycle .owner_states .pending_operation(owner) .is_some_and(|pending| ConnectedLoadOperation::ptr_eq(pending, ¤t_a)), "the new A processing object must remain pending" ); assert!( runtime .stylesheet_lifecycle .injected_ready_connected_loads .is_empty() ); let stale_result = runtime .stylesheet_lifecycle .ready_connected_load_network_results .pop_front() .expect("stale completion still records network observation"); assert!(stale_result.source_owners.is_empty()); } } #[test] fn accepted_style_import_result_installs_only_for_its_exact_processing_operation() { let parser = HtmlParser::SCRIPTING_ENABLED; let document_url = Url::parse("https://example.com/page").unwrap(); let request_url = Url::parse("https://example.com/shared.css").unwrap(); let document = parser.parse( document_url.clone(), "" .to_owned(), ); let owner = first_element_handle(&document, "style"); let source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new( owner, "@import url('/shared.css');".to_owned(), document_url.clone(), )); let operation = ConnectedLoadOperation::new_for_test( owner, ConnectedStyleEventElementKind::Style, ConnectedLoadParameters::StyleImports { source: super::stylesheet_runtime::InlineStyleImportSource::new(source), urls: vec![request_url.clone()], roots: Vec::new(), }, None, ); let mut runtime = DocumentRuntime::new(&document); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(&operation)); runtime.apply_connected_style_load_completion(ConnectedLoadCompletion { operation, successful: true, network_results: vec![ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document_url.clone(), request_url: request_url.clone(), source_owners: vec![owner], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("accepted source observation".to_owned()), }], }); let result = runtime .take_ready_stylesheet_network_results() .into_iter() .next() .expect("accepted source result"); assert_eq!(result.source_owners, vec![owner]); } #[test] fn accepted_style_import_result_loses_install_authority_after_same_url_aba() { let parser = HtmlParser::SCRIPTING_ENABLED; let document_url = Url::parse("https://example.com/page").unwrap(); let request_url = Url::parse("https://example.com/shared.css").unwrap(); let document = parser.parse( document_url.clone(), "" .to_owned(), ); let owner = first_element_handle(&document, "style"); let operation_for = |css_text: &str| { let source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new( owner, css_text.to_owned(), document_url.clone(), )); ConnectedLoadOperation::new_for_test( owner, ConnectedStyleEventElementKind::Style, ConnectedLoadParameters::StyleImports { source: super::stylesheet_runtime::InlineStyleImportSource::new(source), urls: vec![request_url.clone()], roots: Vec::new(), }, None, ) }; let first_a = operation_for("@import url('/shared.css'); /* first A */"); let mut runtime = DocumentRuntime::new(&document); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(&first_a)); runtime.apply_connected_style_load_completion(ConnectedLoadCompletion { operation: first_a, successful: true, network_results: vec![ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document_url.clone(), request_url: request_url.clone(), source_owners: vec![owner], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("first A source observation".to_owned()), }], }); drop(runtime.invalidate_style_related_state(owner)); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(operation_for( "@import url('/middle.css'); /* B */ @import url('/shared.css');", )); drop(runtime.invalidate_style_related_state(owner)); let current_a = operation_for("@import url('/shared.css'); /* current A */"); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(¤t_a)); let stale = runtime .take_ready_stylesheet_network_results() .into_iter() .next() .expect("stale first A observation remains reportable"); assert!(stale.source_owners.is_empty()); assert!( runtime .stylesheet_lifecycle .owner_states .pending_operation(owner) .is_some_and(|pending| ConnectedLoadOperation::ptr_eq(pending, ¤t_a)), "the current A operation must retain its pending authority" ); } #[tokio::test] async fn blocked_front_page_task_does_not_count_as_ready_document_processing_wake() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new(); let script = document.script_handles()[0]; let script_work = PageOwnedDocumentScriptWork::script( DocumentScriptExecutionLane::ClassicDefer, crate::planning::PreparedScript { position: 0, node_id: crate::dom::NodeId::new(script.index()), kind: crate::types::ScriptKind::Classic, mode: crate::types::ScriptMode::Defer, source_kind: crate::types::ScriptSourceKind::External, fetch_metadata: crate::planning::ScriptFetchMetadata::default(), source: crate::planning::ScriptSource::External, initiator_url: Url::parse("https://example.com/app.js").unwrap(), base_url: Url::parse("https://example.com/app.js").unwrap(), url: Url::parse("https://example.com/app.js").unwrap(), host_script_handle: None, }, ); task_queue.extend_post_parse_work([ PostParsePageOwnedWork::document_script_work_with_blocking_signatures( script_work, std::collections::HashSet::from([ crate::DocumentBlockingStylesheetSignature::Link { url: Url::parse("https://example.com/app.css").unwrap(), options: crate::stylesheet_blocking::StylesheetFetchOptions::default(), }, ]), ), ]); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); assert!(!runtime.has_ready_document_processing_wake(&mut task_queue)); } #[tokio::test] async fn open_document_resets_document_owned_lifecycle_state() { let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs( Url::parse("https://example.com/").unwrap(), "" .to_owned(), ); let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); let mut runtime = DocumentRuntime::new_networked(&document, &loader); let handle = first_element_handle(&document, "link"); runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter()); runtime.enqueue_pending_connected_style_load_for_test(handle); runtime .stylesheet_lifecycle .injected_ready_connected_loads .push_back(crate::document_runtime::ReadyConnectedStyleLoad::for_owner( handle, false, crate::document_runtime::ConnectedStyleEventElementKind::Link, )); let pending_operation = ConnectedLoadOperation::new_for_test( handle, ConnectedStyleEventElementKind::Link, preload_load_parameters(Url::parse("https://example.com/app.css").unwrap()), None, ); runtime .stylesheet_lifecycle .owner_states .install_pending_operation(Arc::clone(&pending_operation)); let node_id = crate::dom::NodeId::new(handle.index()); let stylesheet_url = Url::parse("https://example.com/app.css").unwrap(); runtime .stylesheet_lifecycle .fetches .enqueue_completion_for_testing(node_id, stylesheet_url.clone(), true); let _ = runtime .stylesheet_lifecycle .fetches .drain_ready_completions(); runtime .stylesheet_lifecycle .ready_connected_load_network_results .push_back(ConnectedLoadNetworkResult { stylesheet_fetch: None, blocking_operation: None, source_operation: None, import_roots: Vec::new(), document_url: document.final_url().unwrap().clone(), request_url: stylesheet_url.clone(), source_owners: vec![handle], resource_type: SubresourceResourceType::Stylesheet, start_unix_millis: Some(1.0), origin_clean: true, result: Err("stale connected stylesheet result".to_owned()), }); runtime.absorb_runtime_binding_calls(vec![PendingRuntimeBindingCall { source: crate::protocol_types::RuntimeBindingCallSourceIdentity::new(1, 1), name: "binding".to_owned(), payload: "{}".to_owned(), execution_context_id: 1, }]); runtime.open_document(); assert!( runtime .stylesheet_lifecycle .pending_connected_loads .is_empty() ); assert!( runtime .stylesheet_lifecycle .injected_ready_connected_loads .is_empty() ); assert!(runtime.stylesheet_lifecycle.owner_states.is_empty()); assert!( !runtime .stylesheet_lifecycle .fetches .has_any_pending_entries() ); assert!(runtime.pop_ready_connected_style_load().is_none()); assert!( runtime .stylesheet_lifecycle .ready_connected_load_network_results .is_empty() ); let stale_results = runtime.take_ready_stylesheet_network_results(); assert_eq!( stale_results.len(), 1, "the retired typed terminal remains a resource observation" ); assert_eq!(stale_results[0].request_url, stylesheet_url); assert!( stale_results[0].source_owners.is_empty(), "document.open() must revoke the retired terminal's stylesheet install authority" ); assert_eq!( runtime.pending_runtime_binding_call_count(), 1, "document.open() must retain a Runtime binding observation accepted before Document replacement" ); } }