diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs index 722b191e5d..ab64a402e1 100644 --- a/moli-core/tests/scripts.rs +++ b/moli-core/tests/scripts.rs @@ -37,6 +37,9 @@ mod parser_cleanup; #[path = "scripts/parser_preparation.rs"] mod parser_preparation; +#[path = "scripts/lifecycle_task_order.rs"] +mod lifecycle_task_order; + fn diagnostic_global<'a>( page: &'a moli_core::page::Page, name: &str, diff --git a/moli-core/tests/scripts/lifecycle_task_order.rs b/moli-core/tests/scripts/lifecycle_task_order.rs new file mode 100644 index 0000000000..59e5c2d352 --- /dev/null +++ b/moli-core/tests/scripts/lifecycle_task_order.rs @@ -0,0 +1,230 @@ +use super::*; + +async fn parser_rejection_notification_precedes_lifecycle_events(child: bool) -> Result<()> { + let markup = r#"

parser tail

"#; + let result = observe_lifecycle_markup(child, markup).await?; + assert_eq!( + result["value"].as_str(), + Some(r#"["interactive","rejection","DOMContentLoaded","complete","load"]"#), + "child={child}" + ); + Ok(()) +} + +async fn observe_lifecycle_markup(child: bool, markup: &str) -> Result { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let mut target = url::Url::parse(&server.url("/compat/child-dynamic-markup-document"))?; + target.query_pairs_mut().append_pair("markup", markup); + if child { + let parent = format!( + "", + target.as_str().replace('&', "&") + ); + target.set_query(None); + target.query_pairs_mut().append_pair("markup", &parent); + } + let mut page = browser.fetch(target.as_str()).await?; + let result = page + .evaluate_runtime_expression_with_await_async( + "JSON.stringify((document.getElementById('target')?.contentWindow || window).events)", + true, + ) + .await?; + server.shutdown().await; + Ok(result) +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> { + parser_rejection_notification_precedes_lifecycle_events(false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> { + parser_rejection_notification_precedes_lifecycle_events(true).await +} + +async fn deferred_rejection_keeps_dom_fifo_position(child: bool, module: bool) -> Result<()> { + let attribute = if module { "type=module" } else { "defer" }; + let markup = format!( + r#" +

parser tail

"# + ); + let result = observe_lifecycle_markup(child, &markup).await?; + let events: Vec = serde_json::from_str(result["value"].as_str().unwrap())?; + let at = |name: &str| events.iter().position(|event| event == name).unwrap(); + assert!(at("interactive") < at("rejection"), "{events:?}"); + assert!(at("rejection") < at("DOMContentLoaded"), "{events:?}"); + assert!(at("DOMContentLoaded") < at("later DOM task"), "{events:?}"); + assert!(at("rejection") < at("complete"), "{events:?}"); + assert!(at("complete") < at("load"), "{events:?}"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> { + deferred_rejection_keeps_dom_fifo_position(false, false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> { + deferred_rejection_keeps_dom_fifo_position(true, false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> { + deferred_rejection_keeps_dom_fifo_position(false, true).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> { + deferred_rejection_keeps_dom_fifo_position(true, true).await +} + +async fn earlier_dom_task_replaces_document_before_queued_dcl(child: bool) -> Result<()> { + let markup = r#"

old parser tail

"#; + let result = observe_lifecycle_markup(child, markup).await?; + assert_eq!(result["value"].as_str(), Some(r#"["new DCL"]"#)); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> { + earlier_dom_task_replaces_document_before_queued_dcl(false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> { + earlier_dom_task_replaces_document_before_queued_dcl(true).await +} + +async fn dom_callback_before_load_does_not_lose_load(child: bool) -> Result<()> { + let markup = r#"

parser tail

"#; + let result = observe_lifecycle_markup(child, markup).await?; + let events: Vec = serde_json::from_str(result["value"].as_str().unwrap())?; + for expected in ["DOMContentLoaded", "rejection", "async", "load"] { + assert_eq!( + events + .iter() + .filter(|event| event.as_str() == expected) + .count(), + 1, + "child={child}, {events:?}" + ); + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> { + dom_callback_before_load_does_not_lose_load(false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> { + dom_callback_before_load_does_not_lose_load(true).await +} + +async fn parser_import_map_error_keeps_dom_fifo_position( + child: bool, + document_write: bool, +) -> Result<()> { + let import_map = r#""#; + let parser_markup = if document_write { + format!( + "", + serde_json::to_string(import_map)?.replace("", "<\\/script>") + ) + } else { + import_map.to_owned() + }; + let markup = format!( + r#"{parser_markup}parser tail"# + ); + let result = observe_lifecycle_markup(child, &markup).await?; + assert_eq!( + result["value"].as_str(), + Some(r#"["parser error","dynamic error","DOMContentLoaded"]"#), + "child={child}, document_write={document_write}" + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> { + parser_import_map_error_keeps_dom_fifo_position(false, false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> { + parser_import_map_error_keeps_dom_fifo_position(true, false).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn main_written_import_map_error_precedes_later_dom_tasks() -> Result<()> { + parser_import_map_error_keeps_dom_fifo_position(false, true).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn child_written_import_map_error_precedes_later_dom_tasks() -> Result<()> { + parser_import_map_error_keeps_dom_fifo_position(true, true).await +} diff --git a/moli-renderer-v8/src/document_runtime/document_write.rs b/moli-renderer-v8/src/document_runtime/document_write.rs index 5c88788c7f..8429acdd54 100644 --- a/moli-renderer-v8/src/document_runtime/document_write.rs +++ b/moli-renderer-v8/src/document_runtime/document_write.rs @@ -2129,10 +2129,7 @@ impl DocumentRuntime { } } PreparedImportMapSource::ExternalUnsupported => { - let _ = self.enqueue_script_event_lifecycle_work( - ScriptEventKind::Error, - &host_script_handle, - ); + unsafe { &mut *host_ptr }.queue_script_preparation_error(scope, node); } } } diff --git a/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs b/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs index 3de30e81ed..98e1ec7d76 100644 --- a/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs +++ b/moli-renderer-v8/src/document_runtime/script_scheduling/post_parse_lifecycle.rs @@ -28,8 +28,9 @@ impl DocumentRuntime { .unwrap_or(work.len()); // This is an owner-queue boundary marker, not yet the HTML event-loop // queue point for DOMContentLoaded. When it reaches the front after all - // defer-like and parser-owned pre-DCL work has drained, the driver first - // runs ready timer tasks recorded during classic defer execution. + // defer-like and parser-owned pre-DCL work has drained, the driver + // admits the event to the shared DOM task source before yielding to + // ready timer tasks recorded during classic defer execution. work.insert( domcontentloaded_index, PostParsePageOwnedWork::main_document_domcontentloaded(owner), diff --git a/moli-renderer-v8/src/module_runtime.rs b/moli-renderer-v8/src/module_runtime.rs index 54e6452bc0..d064bb510e 100644 --- a/moli-renderer-v8/src/module_runtime.rs +++ b/moli-renderer-v8/src/module_runtime.rs @@ -151,11 +151,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff( start_line: u64, start_column: u64, import_map: PreparedImportMap, -) { +) -> anyhow::Result<()> { vm.document_runtime .note_parser_script_start_position(node_id, start_line, start_column); - let host_script_handle = vm - .document_runtime + vm.document_runtime .bind_parser_owned_script_handle_for_node(import_map.node_id); let _ = vm .document_runtime @@ -169,7 +168,7 @@ pub(crate) fn accept_parser_owned_import_map_handoff( let Some(source) = vm.inline_script_element_source_for_execution(node_id, &source, request) else { - return; + return Ok(()); }; if let Err(error) = register_parser_owned_import_map_source(vm, &source, &import_map.base_url) @@ -181,12 +180,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff( } } PreparedImportMapSource::ExternalUnsupported => { - let _ = vm.document_runtime.enqueue_script_event_lifecycle_work( - crate::host::ScriptEventKind::Error, - &host_script_handle, - ); + vm.queue_script_preparation_error(node_id)?; } } + Ok(()) } pub(crate) fn resolve_module_specifier( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index 0d6ba5f7b4..135c5f4317 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -63,8 +63,21 @@ impl JsContextHost { action: FrameDocumentLifecycleAction, realm_id: FrameRealmId, ) -> bool { + if !matches!(action, FrameDocumentLifecycleAction::Interactive(_)) { + return self + .page_task_capabilities + .get() + .expect("a child Document must retain its Page task capabilities") + .dom_manipulation() + .send_child_document_lifecycle( + crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new( + action, realm_id, + ), + ) + .is_ok(); + } self.page_child_frame_task_sender() - .send_document_lifecycle( + .send_interactive_lifecycle( crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new( action, realm_id, ), @@ -196,6 +209,35 @@ impl JsContextHost { self.route_child_document_lifecycle_action(action.into(), realm_request.realm_id()) } + /// Parser stop changes readiness synchronously. Only an unmaterialized + /// child realm needs an owner continuation before it can dispatch events. + pub(crate) fn finish_child_document_parser_stop( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + action: FrameDocumentInteractiveLifecycleAction, + ) -> FrameDocumentLifecycleTaskEffect { + if !self.child_document_lifecycle_action_is_current(action.into()) { + return FrameDocumentLifecycleTaskEffect::NotApplied; + } + let dispatch_scope = super::super::OwnerDispatchScope::Child(action.child_handle()); + let execution_owner = crate::native_bridge::WindowExecutionContextOwner::Frame( + action.owner().local_window_id, + ); + let Some((_, context)) = + self.window_execution_context(scope, execution_owner, dispatch_scope) + else { + return if self.queue_child_document_interactive_lifecycle_action(action) { + FrameDocumentLifecycleTaskEffect::ConsumedWithoutEvent + } else { + FrameDocumentLifecycleTaskEffect::NotApplied + }; + }; + let context = v8::Global::new(scope, context); + let context = v8::Local::new(scope, &context); + let child_scope = &mut v8::ContextScope::new(scope, context); + self.run_child_document_lifecycle_action(child_scope, action.into()) + } + pub(crate) fn queue_child_document_domcontentloaded_if_ready( &mut self, child_handle: DomHandle, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs index 8663b77289..0ed39dfe53 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs @@ -937,12 +937,11 @@ impl JsContextHost { } } PreparedImportMapSource::ExternalUnsupported => { - tracing::debug!( - child_handle = ?child_handle, - document_handle = ?document_handle, - script_handle = ?node_id, - "child parser external import map is unsupported" - ); + if !self.queue_script_preparation_error(scope, node_id) { + return ScriptDisposition::AdmissionFailed { + script_handle: node_id, + }; + } } } ScriptDisposition::Continue @@ -1143,7 +1142,7 @@ impl JsContextHost { document_handle, parser, ) { - self.queue_child_document_interactive_lifecycle_action(action); + self.finish_child_document_parser_stop(scope, action); } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs index e353720443..9176a7efee 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commit.rs @@ -749,9 +749,10 @@ impl JsContextHost { }; }; let initial_classic_ready_work = install.initial_classic_ready_work; - let parser_stop_queued = install - .parser_stop_action - .is_some_and(|action| self.queue_child_document_interactive_lifecycle_action(action)); + let parser_stop_queued = install.parser_stop_action.is_some_and(|action| { + self.finish_child_document_parser_stop(scope, action) + != crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::NotApplied + }); self.promote_pending_service_worker_child_client(handle); self.register_or_update_service_worker_child_client(handle); self.complete_pending_service_worker_child_client_navigation(handle); diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 382519c948..cba1103b88 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -781,6 +781,16 @@ impl JsContextHost { .promise_rejection() } + pub(crate) fn page_main_document_lifecycle_sender( + &self, + ) -> crate::page_task_queue::RendererPageMainDocumentLifecycleSender { + self.page_task_capabilities + .get() + .expect("a live Page Window must install its lifecycle task capability") + .dom_manipulation() + .main_document_lifecycle() + } + pub(crate) fn page_file_reading_sender( &self, ) -> crate::page_task_queue::RendererPageFileReadingSender { diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs index 0721dc2676..dfc3f8a57e 100644 --- a/moli-renderer-v8/src/page_task_queue.rs +++ b/moli-renderer-v8/src/page_task_queue.rs @@ -17,6 +17,7 @@ mod history_traversal; mod image_load_event; mod indexed_db_task; mod internal_loading; +mod main_document_lifecycle; mod main_document_post_parse; mod main_document_runtime; mod main_document_task_owner; @@ -218,12 +219,10 @@ pub(crate) use self::dedicated_worker_client_event::{ RendererPageDedicatedWorkerClientEventProducer, RendererPageDedicatedWorkerClientEventSender, RendererPageDedicatedWorkerClientEventTask, }; -#[cfg(test)] -pub(crate) use self::dom_manipulation::RendererPageDomManipulationOwner; pub(crate) use self::dom_manipulation::{ PageDomManipulationTurnAction, PageDomManipulationTurnOutcome, - RendererPageDomManipulationRoute, RendererPageDomManipulationSender, - RendererPageDomManipulationTask, + RendererPageDomManipulationOwner, RendererPageDomManipulationRoute, + RendererPageDomManipulationSender, RendererPageDomManipulationTask, }; pub(crate) use self::dynamic_import_owner_action::{ PageDynamicImportOwnerActionDocumentEffect, PageDynamicImportOwnerActionTurnAction, @@ -278,6 +277,11 @@ pub(crate) use self::internal_loading::{ RendererPageInternalLoadingOwner, RendererPageInternalLoadingSender, RendererPageInternalLoadingTask, }; +pub(crate) use self::main_document_lifecycle::{ + PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome, + RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleOwner, + RendererPageMainDocumentLifecycleSender, RendererPageMainDocumentLifecycleTask, +}; pub(crate) use self::main_document_post_parse::{ MainDocumentCompletionRecheckEffect, MainDocumentPostParseCallbackExecution, MainDocumentPostParseCallbackSettlement, MainDocumentPostParseExecution, diff --git a/moli-renderer-v8/src/page_task_queue/child_frame_task.rs b/moli-renderer-v8/src/page_task_queue/child_frame_task.rs index fecb0960bf..95cbc6355f 100644 --- a/moli-renderer-v8/src/page_task_queue/child_frame_task.rs +++ b/moli-renderer-v8/src/page_task_queue/child_frame_task.rs @@ -390,7 +390,7 @@ pub(crate) type PageChildClassicScriptSourceLoadTurnOutcome = PageOwnerTurnOutcome; impl RendererPageChildFrameTask { - fn new(owner: RendererPageChildFrameTaskOwner) -> Self { + pub(super) fn new(owner: RendererPageChildFrameTaskOwner) -> Self { debug_assert!(!matches!( owner.target(), RendererPageChildFrameTaskTarget::ParserModuleRootStart(_) @@ -566,10 +566,14 @@ impl RendererPageChildFrameTaskSender { )) } - pub(crate) fn send_document_lifecycle( + pub(crate) fn send_interactive_lifecycle( &self, target: RendererPageChildDocumentLifecycleTarget, ) -> Result<(), RendererPageChildFrameTaskRouteClosed> { + debug_assert!(matches!( + target.action(), + crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_) + )); self.send(RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) } diff --git a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs index addb597836..b25df6df0d 100644 --- a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs +++ b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs @@ -78,6 +78,8 @@ pub(crate) enum RendererPageDomManipulationOwner { ScriptPreparationError(RendererPageScriptPreparationErrorOwner), PromiseRejection(RendererPagePromiseRejectionOwner), ImageLoadEvent(RendererPageImageLoadEventOwner), + MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleOwner), + ChildDocumentLifecycle(super::RendererPageChildFrameTaskOwner), PopupLoadEvent(RendererPagePopupLoadEventOwner), PopupClose(RendererPagePopupCloseOwner), ConnectedStyleEvent(RendererPageStylesheetTaskOwner), @@ -97,6 +99,8 @@ pub(crate) enum RendererPageDomManipulationTask { ScriptPreparationError(RendererPageScriptPreparationErrorTask), PromiseRejection(RendererPagePromiseRejectionTask), ImageLoadEvent(RendererPageImageLoadEventTask), + MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleTask), + ChildDocumentLifecycle(super::RendererPageChildFrameTask), PopupLoadEvent(RendererPagePopupLoadEventTask), PopupClose(RendererPagePopupCloseTask), ConnectedStyleEvent(RendererPageConnectedStyleEventTask), @@ -130,6 +134,12 @@ impl RendererPageDomManipulationTask { Self::ImageLoadEvent(task) => { RendererPageDomManipulationOwner::ImageLoadEvent(task.owner()) } + Self::MainDocumentLifecycle(task) => { + RendererPageDomManipulationOwner::MainDocumentLifecycle(task.owner) + } + Self::ChildDocumentLifecycle(task) => { + RendererPageDomManipulationOwner::ChildDocumentLifecycle(task.owner()) + } Self::PopupLoadEvent(task) => { RendererPageDomManipulationOwner::PopupLoadEvent(task.owner()) } @@ -169,6 +179,8 @@ pub(crate) enum PageDomManipulationTurnAction { ScriptPreparationError(super::PageScriptPreparationErrorTurnAction), PromiseRejection(super::PagePromiseRejectionTurnAction), ImageLoadEvent(super::PageImageLoadEventTurnAction), + MainDocumentLifecycle(super::PageMainDocumentLifecycleTurnAction), + ChildDocumentLifecycle(super::PageChildDocumentLifecycleTurnAction), PopupLoadEvent(super::PagePopupLoadEventTurnAction), PopupClose(super::PagePopupCloseTurnAction), ConnectedStyleEvent(PageConnectedStyleEventTurnAction), @@ -234,6 +246,30 @@ impl RendererPageDomManipulationSender { RendererPageImageLoadEventSender::new(self.route.clone(), self.root_document) } + pub(crate) fn main_document_lifecycle(&self) -> super::RendererPageMainDocumentLifecycleSender { + super::RendererPageMainDocumentLifecycleSender::new(self.route.clone(), self.root_document) + } + + pub(crate) fn send_child_document_lifecycle( + &self, + target: super::RendererPageChildDocumentLifecycleTarget, + ) -> Result<(), super::child_frame_task::RendererPageChildFrameTaskRouteClosed> { + debug_assert!(!matches!( + target.action(), + crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_) + )); + self.route + .send(RendererPageDomManipulationTask::ChildDocumentLifecycle( + super::RendererPageChildFrameTask::new( + super::RendererPageChildFrameTaskOwner::new( + self.root_document, + super::RendererPageChildFrameTaskTarget::DocumentLifecycle(target), + ), + ), + )) + .map_err(|_| super::child_frame_task::RendererPageChildFrameTaskRouteClosed) + } + pub(crate) fn popup_load_event(&self) -> RendererPagePopupLoadEventSender { RendererPagePopupLoadEventSender::new(self.route.clone(), self.root_document) } @@ -292,6 +328,21 @@ pub(crate) struct RendererPageDomManipulationSource { } impl RendererPageDomManipulationSource { + pub(crate) fn has_main_document_lifecycle_task( + &mut self, + root_document: RendererDocumentToken, + document: crate::frame_owner_model::FrameDocumentTaskOwner, + ) -> bool { + self.source.has_matching_task(|ready| { + matches!( + ready.value(), + RendererPageDomManipulationTask::MainDocumentLifecycle(task) + if task.owner.root_document == root_document + && task.owner.body.owner() == document + ) + }) + } + /// Remove cancelled task closures before they become scheduler-visible. /// /// Element toggle coalescing cancels and reposts at the tail, matching the diff --git a/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs b/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs new file mode 100644 index 0000000000..dc0a3f2dec --- /dev/null +++ b/moli-renderer-v8/src/page_task_queue/main_document_lifecycle.rs @@ -0,0 +1,82 @@ +use tokio::sync::oneshot; + +use crate::{ + frame_owner_model::FrameDocumentTaskOwner, + runtime::{PageOwnerTurnOutcome, RendererDocumentToken}, + script_vm::{MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect}, +}; + +use super::dom_manipulation::{RendererPageDomManipulationRoute, RendererPageDomManipulationTask}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPageMainDocumentLifecycleOwner { + pub(crate) root_document: RendererDocumentToken, + pub(crate) body: MainDocumentLifecycleBody, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RendererPageMainDocumentLifecycleCompletion { + Executed, + /// An earlier DOM task added a load delay after this task was admitted. + /// Return the exact load boundary to its driver without completing it. + LoadBlocked { + owner: FrameDocumentTaskOwner, + }, +} + +/// An admitted HTML lifecycle task, after parser/defer and load prerequisites. +/// Its receipt releases the driver's completion token only after execution. +#[derive(Debug)] +pub(crate) struct RendererPageMainDocumentLifecycleTask { + pub(crate) owner: RendererPageMainDocumentLifecycleOwner, + pub(crate) completion: Option>, +} + +#[derive(Clone, Debug)] +pub(crate) struct RendererPageMainDocumentLifecycleSender { + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, +} + +impl RendererPageMainDocumentLifecycleSender { + pub(super) fn new( + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, + ) -> Self { + Self { + route, + root_document, + } + } + + pub(crate) fn send( + &self, + body: MainDocumentLifecycleBody, + completion: Option>, + ) -> anyhow::Result<()> { + anyhow::ensure!( + !matches!(body, MainDocumentLifecycleBody::Interactive(_)), + "interactive readiness belongs to parser completion" + ); + self.route + .send(RendererPageDomManipulationTask::MainDocumentLifecycle( + RendererPageMainDocumentLifecycleTask { + owner: RendererPageMainDocumentLifecycleOwner { + root_document: self.root_document, + body, + }, + completion, + }, + )) + .map_err(|_| anyhow::anyhow!("main document lifecycle DOM task route closed")) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PageMainDocumentLifecycleTurnAction { + pub(crate) owner: RendererPageMainDocumentLifecycleOwner, + pub(crate) target: MainDocumentLifecycleTargetEffect, +} + +pub(crate) type PageMainDocumentLifecycleTurnOutcome = + PageOwnerTurnOutcome; diff --git a/moli-renderer-v8/src/page_task_queue/owner_sources.rs b/moli-renderer-v8/src/page_task_queue/owner_sources.rs index 2b79f032c5..1d28325ae0 100644 --- a/moli-renderer-v8/src/page_task_queue/owner_sources.rs +++ b/moli-renderer-v8/src/page_task_queue/owner_sources.rs @@ -627,6 +627,15 @@ pub(crate) enum RendererPageSchedulerTask { } impl RendererPageOwnedTaskSources { + pub(crate) fn has_queued_main_document_lifecycle_dom_task( + &mut self, + root_document: crate::runtime::RendererDocumentToken, + document: crate::frame_owner_model::FrameDocumentTaskOwner, + ) -> bool { + self.dom_manipulation + .has_main_document_lifecycle_task(root_document, document) + } + pub(crate) fn new( runtime_wake: PageRuntimeWakeSignal, owner_wake: RendererOwnerWakeSender, @@ -1958,6 +1967,43 @@ impl RendererPageOwnedTaskSourcesTestHarness { }) } + /// Compare the two child semantic source heads without changing either FIFO. + pub(crate) fn next_child_semantic_task_target( + &self, + ) -> Option { + self.sources + .borrow_mut() + .ready_descriptors() + .into_iter() + .filter(|descriptor| { + matches!( + descriptor, + RendererPageReadyDescriptor::ChildFrameTask { .. } + | RendererPageReadyDescriptor::DomManipulation { + owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle( + _ + ), + .. + } + ) + }) + .min_by_key(|descriptor| { + ( + descriptor.runnable_since(), + descriptor.enqueue_order().unwrap_or(0), + descriptor.source_kind(), + ) + }) + .map(|descriptor| match descriptor { + RendererPageReadyDescriptor::ChildFrameTask { owner, .. } + | RendererPageReadyDescriptor::DomManipulation { + owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), + .. + } => owner.target(), + _ => unreachable!("child semantic selector must retain its exact target"), + }) + } + /// Dequeue one production scheduler task selected by a narrow semantic /// fixture. The selector sees the same descriptor as the owner scheduler; /// this helper adds no lane-specific consumer or alternate queue model. diff --git a/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs b/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs index 1d9e99f8d1..ff8add88d3 100644 --- a/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs +++ b/moli-renderer-v8/src/runtime/document_lifecycle_turn.rs @@ -141,12 +141,13 @@ pub(super) struct PendingDocumentLifecycleTurn { /// can immediately take another owner turn without ordinary Page work. /// /// This is stable residence state, not a wake hint. It keeps a runnable - /// parser-finish chain (notably `interactive` -> DOMContentLoaded) from - /// being displaced by an older resource terminal that became ready while - /// the parser task was still executing. + /// parser preparation and lifecycle task admission contiguous. Once an + /// event is admitted to the DOM source, the resident waits for its receipt + /// and ordinary task arbitration owns event delivery. pub(super) owner_turn_is_runnable: bool, pub(super) driver: PostParseLifecycleDriver, pub(super) completed_task: Option, + pub(super) awaiting_dom_task: Option, pub(super) completion_action: Option, /// Whether this exact resident owns a sealed main-parser defer/module /// continuation. @@ -158,3 +159,10 @@ pub(super) struct PendingDocumentLifecycleTurn { pub(super) has_sealed_main_parser_script_queue: bool, pub(super) started: Instant, } + +pub(super) struct PendingDocumentLifecycleDomTask { + pub(super) task: PostParsePageOwnedTask, + pub(super) completion: tokio::sync::oneshot::Receiver< + crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion, + >, +} diff --git a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs index 8db6c47677..dead34c08c 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/bound.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/bound.rs @@ -763,11 +763,30 @@ pub(super) fn select_page_scheduler_turn( !lifecycle_is_deferred && entry.document_lifecycle_owner_turn_is_runnable(); let has_ready_main_parser_script_continuation = !lifecycle_is_deferred && entry.has_ready_main_parser_script_continuation(); - let document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state( + let mut document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state( has_pending_document_lifecycle_turn, document_lifecycle_owner_turn_is_runnable, has_ready_main_parser_script_continuation, ); + let has_lifecycle_dom_task = entry + .page_vm() + .vm() + .current_main_document_task_owner() + .is_some_and(|owner| { + task_sources.has_queued_main_document_lifecycle_dom_task( + entry.page_vm().document_lifecycle.identity().document, + owner, + ) + }); + if has_lifecycle_dom_task + && !matches!( + document_lifecycle, + DocumentLifecycleClassReadiness::RunnableContinuation + | DocumentLifecycleClassReadiness::ReadyMainParserScriptContinuation + ) + { + document_lifecycle = DocumentLifecycleClassReadiness::QueuedDomTask; + } let gate_policy = lifecycle_gate .as_mut() .map(|gate| gate.turn_policy(entry, !snapshot.eligible.is_empty())) @@ -794,8 +813,26 @@ pub(super) fn select_page_scheduler_turn( ), }, Some(PageTurnClass::Ordinary) => { + // Preserve parser-finish priority across task sources while the + // lifecycle event occupies its real DOM FIFO position. Earlier DOM + // tasks run first; no descriptor can jump to the lifecycle payload. + let dom_head_is_eligible = snapshot.eligible.iter().any(|descriptor| { + matches!( + descriptor, + crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. } + ) + }); + // A child lifecycle head can be awaiting its exact realm. Allow + // that prerequisite source to run before restoring DOM priority. + let eligible = if has_lifecycle_dom_task && dom_head_is_eligible { + snapshot.eligible.into_iter().filter(|descriptor| { + matches!(descriptor, crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. }) + }).collect() + } else { + snapshot.eligible + }; let selected = scheduler - .select_ready_descriptor(snapshot.eligible) + .select_ready_descriptor(eligible) .expect("selected ordinary Page-turn class must retain an eligible descriptor"); let task = task_sources.take_task(selected); RendererPageScheduledTurn::Ordinary(Box::new(task)) @@ -815,6 +852,12 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task( ) -> (LivePageEntry, Result<()>) { run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| { Box::pin(async move { + let main_lifecycle_owner = match &task { + RendererPageSchedulerTask::DomManipulation( + crate::page_task_queue::RendererPageDomManipulationTask::MainDocumentLifecycle(task), + ) => Some(task.owner), + _ => None, + }; let replacement_lifecycle_snapshot = entry .page_vm() .document_replacement_lifecycle_action_snapshot(); @@ -847,6 +890,21 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task( .await }; + if application.is_ok() + && main_lifecycle_owner.is_some_and(|owner| { + owner.root_document == entry.page_vm().document_lifecycle.identity().document + && entry.page_vm().vm().current_main_document_task_owner() == Some(owner.body.owner()) + }) + { + let (_, pending) = entry.page_vm_and_document_lifecycle_turn_mut(); + if let Some(pending) = pending { + // Resume the exact driver to consume its receipt. An + // executed event publishes the reached stage before other + // sources run; a blocked load returns to its prerequisites. + pending.owner_turn_is_runnable = true; + } + } + match (application, reconciliation) { (Ok(()), Ok(_)) => Ok(()), (Err(action_error), Ok(_)) => Err(action_error), diff --git a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs index 31c70fa86f..c07e454f59 100644 --- a/moli-renderer-v8/src/runtime/page_turn_scheduler.rs +++ b/moli-renderer-v8/src/runtime/page_turn_scheduler.rs @@ -81,6 +81,8 @@ pub(super) enum PageTurnClass { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum DocumentLifecycleClassReadiness { Absent, + /// The DOM source owns the admitted event. Only its FIFO head may run. + QueuedDomTask, Available, RunnableContinuation, ReadyMainParserScriptContinuation, @@ -110,7 +112,7 @@ impl DocumentLifecycleClassReadiness { } const fn is_available(self) -> bool { - !matches!(self, Self::Absent) + !matches!(self, Self::Absent | Self::QueuedDomTask) } } diff --git a/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs b/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs index bb97d14d0a..fcc7186d95 100644 --- a/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs +++ b/moli-renderer-v8/src/runtime/page_vm/document_lifecycle_turn.rs @@ -249,6 +249,7 @@ impl PageVm { owner_turn_is_runnable: true, driver, completed_task: None, + awaiting_dom_task: None, completion_action: None, has_sealed_main_parser_script_queue, started, @@ -372,6 +373,34 @@ impl PageVm { return Ok(settled); } + let pending = pending_document_lifecycle_turn + .as_mut() + .expect("post-parse lifecycle state should remain installed"); + if let Some(waiting) = pending.awaiting_dom_task.as_mut() { + match waiting.completion.try_recv() { + Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => { + pending.completed_task = Some(pending.awaiting_dom_task.take().unwrap().task); + } + Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => { + // The attempt did not execute the load boundary. Its token + // must never tell the driver that this stage was reached. + pending.awaiting_dom_task = None; + self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![ + PostParsePageOwnedWork::main_document_window_load(owner), + ]); + } + Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + return Ok(DocumentLifecycleTurnOutcome::blocked( + DocumentLifecycleTurnAction::None, + document, + )); + } + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + anyhow::bail!("current lifecycle DOM task disappeared before completion"); + } + } + } + // Feed the previous exact task result into the driver before other // page work is admitted. `AwaitProgress` parks immediately; a producer // wake returns to owner arbitration instead of waiting in this method. @@ -412,17 +441,6 @@ impl PageVm { ) .await } - PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => { - self.run_classic_defer_timer_before_domcontentloaded(&request_client) - .await?; - self.outcome_after_exact_post_parse_action( - pending_document_lifecycle_turn, - document, - stage, - DocumentLifecycleTurnAction::Progressed, - false, - ) - } PostParseLifecycleAdvance::NeedsContinuation => self .outcome_after_exact_post_parse_action( pending_document_lifecycle_turn, @@ -570,6 +588,7 @@ impl PageVm { owner_turn_is_runnable: true, driver, completed_task: None, + awaiting_dom_task: None, completion_action: None, has_sealed_main_parser_script_queue: false, started: Instant::now(), @@ -598,6 +617,7 @@ impl PageVm { owner_turn_is_runnable: _, driver, completed_task, + awaiting_dom_task: _, completion_action, has_sealed_main_parser_script_queue: _, started: _, @@ -620,6 +640,7 @@ impl PageVm { owner_turn_is_runnable: true, driver, completed_task, + awaiting_dom_task: None, completion_action, has_sealed_main_parser_script_queue, started: Instant::now(), @@ -647,6 +668,7 @@ impl PageVm { owner_turn_is_runnable: true, driver, completed_task: None, + awaiting_dom_task: None, completion_action: None, has_sealed_main_parser_script_queue: self .vm() diff --git a/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs b/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs index 2e66209eb1..dd38cb88c7 100644 --- a/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/main_document_lifecycle_completion.rs @@ -3,11 +3,9 @@ //! The exact lifecycle resident remains the sole ordinary DCL/load authority. //! This component only reconciles an already-claimed body: typed checkpoint //! continuations, milestone journal visibility, body settlement, and lifecycle -//! priming stay in one auditable order. Parser completion may call this same -//! component after claiming the exact DCL direct successor, but the parser -//! continuation's own task-end checkpoint has already completed first. This -//! preserves the observable task boundary without opening a scheduler round in -//! which an ordinary Page task could overtake DCL. +//! priming stay in one auditable order. DCL and load reach this coordinator +//! through the shared DOM task source after parser completion has admitted +//! them; interactive readiness remains part of parser completion. use anyhow::Result; use std::time::Instant; @@ -42,17 +40,6 @@ pub(super) async fn execute_main_document_lifecycle_on_owner_local_task( execute_main_document_lifecycle_body_on_owner_local_task(page_vm, body).await } -pub(super) async fn execute_parser_exact_domcontentloaded_on_owner_local_task( - page_vm: &mut PageVm, - owner: crate::frame_owner_model::FrameDocumentTaskOwner, -) -> Result { - execute_main_document_lifecycle_body_on_owner_local_task( - page_vm, - MainDocumentLifecycleBody::DomContentLoaded { owner }, - ) - .await -} - async fn execute_main_document_lifecycle_body_on_owner_local_task( page_vm: &mut PageVm, body: MainDocumentLifecycleBody, diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index b1d9e45482..aaf56e8c04 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -105,6 +105,7 @@ mod page_image_load_event; mod page_indexed_db_task; mod page_internal_loading; mod page_internal_loading_task_completion; +mod page_main_document_lifecycle; mod page_main_document_post_parse; mod page_main_document_runtime; mod page_main_native_module_task; @@ -2119,9 +2120,23 @@ impl PageVm { ) -> bool { use crate::{ frame_owner_model::ChildFrameSemanticTurnKind, - page_task_queue::RendererPageChildFrameTaskTarget, + page_task_queue::{ + RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner, + RendererPageReadyDescriptor, + }, }; + if expected == ChildFrameSemanticTurnKind::DocumentLifecycle { + return self.page_task_executor_sources_for_test().has_scheduler_task_for_executor_test(|descriptor| { + matches!(descriptor, + RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. } + ) || matches!(descriptor, + RendererPageReadyDescriptor::ChildFrameTask { owner, .. } + if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_)) + ) + }); + } + let Some(target) = self .page_task_executor_sources_for_test() .next_child_frame_task_target() @@ -2961,7 +2976,11 @@ impl PageVm { { return Some(ChildFrameSemanticTurnKind::NavigationCommit); } - if self + if matches!( + self.page_task_executor_sources_for_test() + .next_child_semantic_task_target(), + Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)) + ) && self .run_exact_selected_page_task_for_test( PageSelectedTaskTestSelector::ChildDocumentLifecycle, &loader, @@ -3170,11 +3189,61 @@ impl PageVm { match advance { PostParseLifecycleAdvance::PageOwnedTask(mut task) => { let request_client = self.request_client.clone(); - self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane( - &request_client, - task.take_work_for_execution(), - ) - .await?; + let work = task.take_work_for_execution(); + if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work + && matches!( + **lifecycle, + PostParseLifecycleWork::DispatchDomContentLoaded { .. } + | PostParseLifecycleWork::DispatchWindowLoad { .. } + ) + { + let body = crate::script_vm::MainDocumentLifecycleBody::from_post_parse_work( + lifecycle, + ) + .unwrap(); + let (sender, mut receiver) = tokio::sync::oneshot::channel(); + self.vm() + .queue_main_document_lifecycle_dom_task(body, Some(sender))?; + if matches!( + body, + crate::script_vm::MainDocumentLifecycleBody::DomContentLoaded { .. } + ) { + self.run_ready_classic_defer_timers_before_domcontentloaded( + &request_client, + ) + .await?; + } + loop { + match receiver.try_recv() { + Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => break, + Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => { + self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![PostParsePageOwnedWork::main_document_window_load(owner)]); + return Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None))); + } + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + anyhow::bail!( + "lifecycle DOM task disappeared before its fixture completion" + ); + } + Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + anyhow::ensure!( + self.run_exact_selected_page_task_for_test( + PageSelectedTaskTestSelector::AnyDomManipulation, + &request_client, + ) + .await?, + "queued lifecycle task must remain runnable" + ); + } + } + } + } else { + self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane( + &request_client, + work, + ) + .await?; + } if self.vm().has_pending_location_navigation() { return Ok(PostParseLifecycleLoopAdvance::Complete( PostParseLifecycleCompletionAction::TriggeredNavigation, @@ -3184,12 +3253,6 @@ impl PageVm { *task, )))) } - PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => { - let request_client = self.request_client.clone(); - self.run_classic_defer_timer_before_domcontentloaded(&request_client) - .await?; - Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None))) - } PostParseLifecycleAdvance::NeedsContinuation => { Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None))) } diff --git a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs index 0f9e54154a..ba8398cf7c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_body_test_support.rs @@ -22,14 +22,22 @@ impl PageVm { matches!( descriptor, RendererPageReadyDescriptor::ChildFrameTask { owner, .. } - if matches!( + | RendererPageReadyDescriptor::DomManipulation { + owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), .. + } if matches!( owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_) ) ) })?; - let RendererPageSchedulerTask::ChildFrameTask(task) = task else { - unreachable!("DocumentLifecycle descriptor must dequeue a child-frame task") + let task = match task { + RendererPageSchedulerTask::ChildFrameTask(task) + | RendererPageSchedulerTask::DomManipulation( + crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle( + task, + ), + ) => task, + _ => unreachable!("DocumentLifecycle descriptor must dequeue its admitted task"), }; Some(self.apply_selected_page_child_document_lifecycle_turn(task)) } diff --git a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs index ee5afe3f3e..5a24762e98 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_child_document_lifecycle_task_completion.rs @@ -1,15 +1,8 @@ -//! Task-end boundary for an exact child `Document` lifecycle action. +//! Task-end boundary for an exact child Document lifecycle action. //! -//! Interactive, `DOMContentLoaded`, and complete transitions are ordinary -//! selected child-frame tasks. Their bodies synchronously dispatch the -//! corresponding document event when a wrapper still exists, but deliberately -//! leave Promise reactions pending. This module maps that execution-produced -//! fact to the sole Page task-completion authority. -//! -//! This does not govern the parser-owned direct-successor boundary where the -//! final deferred script completes parsing and synchronously dispatches DCL in -//! the same task. That separately typed parser completion path must remain -//! contiguous and must not be turned into another scheduler task here. +//! DCL and complete use the shared DOM source. Interactive readiness belongs +//! to parser stop; an unmaterialized realm may need a child owner continuation. +//! The selected dispatcher completes the event's checkpoint and follow-up. use crate::page_task_queue::{ PageChildDocumentLifecycleTargetEffect, PageChildDocumentLifecycleTurnAction, diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs index 15055e1bff..a9610941e9 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation.rs @@ -5,11 +5,20 @@ use crate::page_task_queue::{ use super::PageVm; impl PageVm { - pub(in crate::runtime) fn apply_selected_page_dom_manipulation_turn( + pub(in crate::runtime) async fn apply_selected_page_dom_manipulation_turn( &mut self, task: RendererPageDomManipulationTask, ) -> anyhow::Result { match task { + RendererPageDomManipulationTask::ChildDocumentLifecycle(task) => Ok(self + .apply_selected_page_child_document_lifecycle_turn(task) + .map_action(PageDomManipulationTurnAction::ChildDocumentLifecycle)), + RendererPageDomManipulationTask::MainDocumentLifecycle(task) => self + .apply_selected_page_main_document_lifecycle_turn(task) + .await + .map(|outcome| { + outcome.map_action(PageDomManipulationTurnAction::MainDocumentLifecycle) + }), RendererPageDomManipulationTask::BroadcastChannel(task) => self .apply_selected_page_broadcast_channel_delivery_turn(task) .map(|outcome| outcome.map_action(PageDomManipulationTurnAction::BroadcastChannel)), diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs index 00f33de262..a39139e114 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_task_completion.rs @@ -18,6 +18,12 @@ impl PageVm { loader: &crate::network::ResourceRequestClient, ) -> Result<()> { let completion = match action { + PageDomManipulationTurnAction::ChildDocumentLifecycle(action) => { + action.into_page_task_completion() + } + PageDomManipulationTurnAction::MainDocumentLifecycle(action) => { + action.into_page_task_completion() + } PageDomManipulationTurnAction::BroadcastChannel(action) => { action.into_page_task_completion() } diff --git a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs index b1871de9d9..39d215278c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_dom_manipulation_test_support.rs @@ -21,6 +21,7 @@ pub(crate) enum PageDomManipulationTestFamily { FileEntryFileCallback, ScriptPreparationError, PromiseRejection, + MainDocumentLifecycle, ImageLoadEvent, PopupLoadEvent, ConnectedStyleEvent, @@ -34,6 +35,9 @@ impl PageDomManipulationTestFamily { matches!( (self, owner), ( + Self::MainDocumentLifecycle, + RendererPageDomManipulationOwner::MainDocumentLifecycle(_) + ) | ( Self::BroadcastChannel, RendererPageDomManipulationOwner::BroadcastChannel(_) ) | ( diff --git a/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs new file mode 100644 index 0000000000..6629468123 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/page_main_document_lifecycle.rs @@ -0,0 +1,125 @@ +use crate::{ + page_task_queue::{ + PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome, + RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleTask, + }, + runtime::PendingDocumentLifecycleTurn, + script_vm::{ + MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect, + MainDocumentLifecycleTargetRejection, PostParsePageOwnedTask, + }, +}; + +use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm}; + +/// Event bodies finish their own checkpoints through the lifecycle coordinator. +/// A blocked or stale task does not enter page code. +impl IntoPageTaskCompletion for PageMainDocumentLifecycleTurnAction { + fn into_page_task_completion(self) -> PageTaskCompletion { + PageTaskCompletion::NoCompletion + } +} + +impl PageVm { + pub(super) async fn run_ready_classic_defer_timers_before_domcontentloaded( + &mut self, + loader: &crate::network::ResourceRequestClient, + ) -> anyhow::Result<()> { + // Preserve the existing cross-source timer preference, after DCL has + // acquired its FIFO position in the DOM task source. + while self + .vm() + .document_runtime + .has_ready_timeout_queued_by_classic_defer_script() + { + self.run_classic_defer_timer_before_domcontentloaded(loader) + .await?; + } + Ok(()) + } + + pub(super) fn queue_post_parse_lifecycle_dom_task( + &self, + pending: &mut PendingDocumentLifecycleTurn, + body: MainDocumentLifecycleBody, + task: PostParsePageOwnedTask, + ) -> anyhow::Result<()> { + anyhow::ensure!( + pending.awaiting_dom_task.is_none() && pending.completed_task.is_none(), + "a lifecycle resident may await only one exact task" + ); + let (sender, receiver) = tokio::sync::oneshot::channel(); + self.vm() + .queue_main_document_lifecycle_dom_task(body, Some(sender))?; + pending.awaiting_dom_task = Some( + super::super::document_lifecycle_turn::PendingDocumentLifecycleDomTask { + task, + completion: receiver, + }, + ); + Ok(()) + } + + pub(super) async fn apply_selected_page_main_document_lifecycle_turn( + &mut self, + task: RendererPageMainDocumentLifecycleTask, + ) -> anyhow::Result { + let RendererPageMainDocumentLifecycleTask { owner, completion } = task; + if owner.root_document == self.document_lifecycle.identity().document + && let MainDocumentLifecycleBody::WindowLoad { + owner: document_owner, + } = owner.body + && self + .vm_mut() + .main_document_window_load_task_is_ready(document_owner) + == Some(false) + { + // The DOM queue may have run a producer since admission. Do not + // open the milestone journal or consume its boundary token while + // the exact Document has a new load delay. + let completion = + completion.expect("queued Window load must retain its lifecycle driver"); + let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::LoadBlocked { + owner: document_owner, + }); + return Ok(PageMainDocumentLifecycleTurnOutcome::new( + PageMainDocumentLifecycleTurnAction { + owner, + target: MainDocumentLifecycleTargetEffect::NotApplied { + reason: MainDocumentLifecycleTargetRejection::TransitionRejected, + current_owner: self.vm().current_main_document_task_owner(), + }, + }, + )); + } + let target = if owner.root_document == self.document_lifecycle.identity().document { + let run = super::main_document_lifecycle_completion::execute_main_document_lifecycle_on_owner_local_task( + self, owner.body, + ).await?; + let target = run.completion.target(); + if let crate::script_vm::MainDocumentLifecycleFollowup::ScheduleInternalLoading { + task, + ready_at, + } = run.completion.into_followup() + { + self.vm() + .schedule_page_internal_loading_task(task, ready_at)?; + } + // The common selected Page owner reconciles document.open() after + // the body and its checkpoints, just as for other DOM tasks. + target + } else { + MainDocumentLifecycleTargetEffect::NotApplied { + reason: MainDocumentLifecycleTargetRejection::TransitionRejected, + current_owner: self.vm().current_main_document_task_owner(), + } + }; + if let Some(completion) = completion { + // Replacement may have retired the receiver during this task. + let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::Executed); + } + Ok(PageMainDocumentLifecycleTurnOutcome::new( + PageMainDocumentLifecycleTurnAction { owner, target }, + )) + } +} diff --git a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs index bb77c451b9..5549dd0ced 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_selected_task_test_harness.rs @@ -52,6 +52,7 @@ pub(crate) enum PageSelectedTaskTestSelector { ChildNavigationCommit, ChildModulepreloadEventAction, DomManipulation(PageDomManipulationTestFamily), + AnyDomManipulation, DedicatedWorkerClientEvent, DynamicImportOwnerAction, FileReading, @@ -89,6 +90,9 @@ impl PageSelectedTaskTestSelector { Self::ChildDocumentLifecycle => matches!( descriptor, RendererPageReadyDescriptor::ChildFrameTask { owner, .. } + | RendererPageReadyDescriptor::DomManipulation { + owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), .. + } if matches!( owner.target(), crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_) @@ -153,6 +157,10 @@ impl PageSelectedTaskTestSelector { descriptor, RendererPageReadyDescriptor::ChildModulepreloadEventAction { .. } ), + Self::AnyDomManipulation => matches!( + descriptor, + RendererPageReadyDescriptor::DomManipulation { .. } + ), Self::DomManipulation(family) => matches!( descriptor, RendererPageReadyDescriptor::DomManipulation { owner, .. } @@ -308,7 +316,13 @@ impl PageSelectedTaskTestSelector { fn matches_task(self, task: &RendererPageSchedulerTask) -> bool { match (self, task) { - (Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task)) => { + (Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task)) + | ( + Self::ChildDocumentLifecycle, + RendererPageSchedulerTask::DomManipulation( + RendererPageDomManipulationTask::ChildDocumentLifecycle(task), + ), + ) => { matches!( task.owner().target(), crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_) @@ -369,6 +383,7 @@ impl PageSelectedTaskTestSelector { Self::ChildModulepreloadEventAction, RendererPageSchedulerTask::ChildModulepreloadEventAction(_), ) => true, + (Self::AnyDomManipulation, RendererPageSchedulerTask::DomManipulation(_)) => true, (Self::DomManipulation(family), RendererPageSchedulerTask::DomManipulation(task)) => { family.matches_owner(task.owner()) } diff --git a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs index bce2e08304..48f11b8e12 100644 --- a/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs +++ b/moli-renderer-v8/src/runtime/page_vm/page_typed_immediate_source.rs @@ -74,6 +74,27 @@ impl PageVm { descriptor: RendererPageReadyDescriptor, ) -> bool { match descriptor { + RendererPageReadyDescriptor::DomManipulation { + owner: + crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle( + owner, + ), + .. + } => { + if owner.root_document() != self.document_lifecycle.identity().document { + return true; + } + let crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle( + target, + ) = owner.target() + else { + unreachable!("child lifecycle DOM carrier must retain its exact target"); + }; + // A current task retains its DOM FIFO position while its + // separately queued realm prerequisite runs. It is not stale + // merely because that realm has not materialized yet. + !self.vm().child_document_lifecycle_waits_for_realm(target) + } RendererPageReadyDescriptor::ActionWindow { .. } | RendererPageReadyDescriptor::DomManipulation { .. } | RendererPageReadyDescriptor::UserInteraction { .. } diff --git a/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs b/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs index 0b9b1e8dac..631024ffad 100644 --- a/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs +++ b/moli-renderer-v8/src/runtime/page_vm/parser_completion.rs @@ -4,8 +4,8 @@ //! owns the bounded handoff from that work to parser completion: it consumes a //! one-shot drained-queue permit, asks the lifecycle authority to claim the //! exact DOMContentLoaded successor, closes the selected parser task's -//! checkpoint boundary, and only then hands that already-claimed successor to -//! the lifecycle coordinator. The coordinator applies it to the surviving +//! checkpoint boundary, and admits the claimed successor to the shared DOM +//! task source. Its selected-task coordinator applies it to the surviving //! exact Document or stale-rejects it after replacement. //! It is not a scheduler lane and stores no durable lifecycle state. @@ -16,9 +16,7 @@ use crate::frame_owner_model::FrameDocumentTaskOwner; use crate::network::ResourceRequestClient; use crate::page_task_queue::{PostParseLifecycleWork, PostParsePageOwnedWork}; use crate::runtime::{PendingDocumentLifecycleTurn, RendererDocumentLifecycleIdentity}; -use crate::script_vm::{ - MainDocumentLifecycleBodyKind, ParserFinishDomContentLoadedTask, PostParsePageOwnedTask, -}; +use crate::script_vm::{MainDocumentLifecycleBody, PostParsePageOwnedTask}; use super::super::document_lifecycle_turn::{ DocumentLifecycleTurnAction, DocumentLifecycleTurnOutcome, @@ -164,31 +162,20 @@ impl ParserCompletion { }; } }; - // The parser continuation and DOMContentLoaded are distinct HTML task - // boundaries even though this Chromium-compatible direct successor - // deliberately does not reopen ordinary scheduler arbitration. Drain - // terminal reactions first; the lifecycle coordinator then owns DCL's - // separate task-end checkpoint. + // Finish parser reactions before admitting the DCL global task. An + // earlier DOM task keeps its FIFO position; later timer callbacks may + // enqueue DOM work only behind this already admitted lifecycle task. Self::finish_task(page_vm, task_effect)?; + page_vm.vm().queue_main_document_lifecycle_dom_task( + MainDocumentLifecycleBody::DomContentLoaded { + owner: successor_owner, + }, + None, + )?; let request_client = page_vm.request_client.clone(); - while page_vm - .vm() - .document_runtime - .has_ready_timeout_queued_by_classic_defer_script() - { - page_vm - .run_classic_defer_timer_before_domcontentloaded(&request_client) - .await?; - } - let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task( - page_vm, - successor_owner, - ) - .await?; - anyhow::ensure!( - run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded, - "parse-time DOMContentLoaded successor lost its typed lifecycle execution" - ); + page_vm + .run_ready_classic_defer_timers_before_domcontentloaded(&request_client) + .await?; Ok(()) } @@ -228,32 +215,6 @@ impl PageVm { ParserCompletion::finish_parse_time(self, completion).await } - async fn execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane( - &mut self, - mut task: ParserFinishDomContentLoadedTask, - ) -> Result { - let owner = task.owner(); - let work = task.take_work_for_execution(); - let PostParsePageOwnedWork::Lifecycle(work) = work else { - anyhow::bail!("parser-finish successor is not lifecycle work"); - }; - let successor_owner = match ParserCompletion::exact_domcontentloaded_owner(*work, owner) { - Ok(owner) => owner, - Err(message) => anyhow::bail!(message), - }; - - let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task( - self, - successor_owner, - ) - .await?; - anyhow::ensure!( - run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded, - "DOMContentLoaded direct successor lost its typed lifecycle execution" - ); - Ok(task.into_completed_task()) - } - pub(super) async fn execute_and_complete_selected_post_parse_page_owned_task( &mut self, loader: &ResourceRequestClient, @@ -262,12 +223,38 @@ impl PageVm { stage: PageVmInitStage, mut task: Box, ) -> Result { + let work = task.take_work_for_execution(); + if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work + && matches!( + **lifecycle, + PostParseLifecycleWork::DispatchDomContentLoaded { .. } + | PostParseLifecycleWork::DispatchWindowLoad { .. } + ) + { + let body = MainDocumentLifecycleBody::from_post_parse_work(lifecycle) + .expect("selected DCL/load work must have a typed lifecycle body"); + self.queue_post_parse_lifecycle_dom_task( + pending_document_lifecycle_turn + .as_mut() + .expect("lifecycle resident must remain installed"), + body, + *task, + )?; + if matches!(body, MainDocumentLifecycleBody::DomContentLoaded { .. }) { + self.run_ready_classic_defer_timers_before_domcontentloaded(loader) + .await?; + } + return self.outcome_after_exact_post_parse_action( + pending_document_lifecycle_turn, + document, + stage, + DocumentLifecycleTurnAction::Progressed, + true, + ); + } let replacement_lifecycle_snapshot = self.document_replacement_lifecycle_action_snapshot(); let execution = self - .execute_post_parse_page_owned_task_on_named_owner_lane( - loader, - task.take_work_for_execution(), - ) + .execute_post_parse_page_owned_task_on_named_owner_lane(loader, work) .await; // Generic post-parse callbacks must finish their old-realm task before // a synchronous `document.open()` replacement is admitted. MainParser @@ -416,50 +403,32 @@ impl PageVm { }; } }; - if let Some(dcl_task) = claimed_dcl { + if let Some(mut dcl_task) = claimed_dcl { ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?; - while self - .vm() - .document_runtime - .has_ready_timeout_queued_by_classic_defer_script() - { - self.run_classic_defer_timer_before_domcontentloaded(loader) - .await?; - } - let replacement_lifecycle_snapshot = - self.document_replacement_lifecycle_action_snapshot(); - let execution = self - .execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane( - dcl_task, - ) - .await; - let admission = self - .take_document_replacement_lifecycle_admission_after_action( - replacement_lifecycle_snapshot, - ); - let completed_dcl_task = match (execution, admission) { - (Ok(task), Ok(_)) => task, - (Err(execution_error), Ok(_)) => return Err(execution_error), - (Ok(_), Err(admission_error)) => return Err(admission_error), - (Err(execution_error), Err(admission_error)) => { - return Err(anyhow::anyhow!( - "parser-finish DOMContentLoaded action failed ({execution_error:#}) and its Document replacement admission also failed ({admission_error:#})" - )); - } + let owner = dcl_task.owner(); + let PostParsePageOwnedWork::Lifecycle(work) = + dcl_task.take_work_for_execution() + else { + anyhow::bail!("parser-finish successor is not lifecycle work"); }; - pending_document_lifecycle_turn - .as_mut() - .expect("post-parse lifecycle state should remain installed") - .completed_task = Some(completed_dcl_task); - if let Some(outcome) = self - .transition_lifecycle_for_pending_top_level_navigation( - pending_document_lifecycle_turn, - document, - stage, - ) - { - return Ok(outcome); - } + let owner = ParserCompletion::exact_domcontentloaded_owner(*work, owner) + .map_err(anyhow::Error::msg)?; + self.queue_post_parse_lifecycle_dom_task( + pending_document_lifecycle_turn + .as_mut() + .expect("lifecycle resident must remain installed"), + MainDocumentLifecycleBody::DomContentLoaded { owner }, + dcl_task.into_pending_task(), + )?; + self.run_ready_classic_defer_timers_before_domcontentloaded(loader) + .await?; + return self.outcome_after_exact_post_parse_action( + pending_document_lifecycle_turn, + document, + stage, + DocumentLifecycleTurnAction::Progressed, + true, + ); } else { ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?; } diff --git a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs index 35d63f8740..2d1d0136ec 100644 --- a/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs +++ b/moli-renderer-v8/src/runtime/page_vm/selected_page_task.rs @@ -23,7 +23,7 @@ impl PageVm { self.apply_selected_page_action_window_turn(deadline) } RendererPageSchedulerTask::DomManipulation(task) => { - let outcome = self.apply_selected_page_dom_manipulation_turn(task)?; + let outcome = self.apply_selected_page_dom_manipulation_turn(task).await?; self.finish_selected_page_dom_manipulation_task(outcome.action, loader) .await?; Ok(()) diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs index d5fb298a4d..62556ec9ae 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs @@ -206,6 +206,26 @@ async fn advance_unblocked_exact_lifecycle_to_stage( }, .. } => assert_eq!(*runnable_document, document), + DocumentLifecycleTurnOutcome { + readiness: DocumentLifecycleTurnReadiness::Blocked { document: waiting }, + .. + } if pending + .as_ref() + .is_some_and(|pending| pending.awaiting_dom_task.is_some()) => + { + assert_eq!(*waiting, document); + let loader = page_vm.request_client.clone(); + assert!( + page_vm + .run_exact_selected_page_task_for_test( + super::super::PageSelectedTaskTestSelector::DomManipulation( + super::super::PageDomManipulationTestFamily::MainDocumentLifecycle + ), + &loader + ) + .await? + ); + } _ => panic!( "unblocked exact lifecycle should remain runnable before {stage:?}: {outcome:?}" ), @@ -267,6 +287,47 @@ async fn prepare_parse_time_exact_domcontentloaded( Ok(owner) } +impl PageVm { + async fn advance_lifecycle_and_selected_dom_task_for_test( + &mut self, + pending: &mut Option, + document: RendererDocumentLifecycleIdentity, + ) -> anyhow::Result { + for _ in 0..64 { + let outcome = self + .advance_post_parse_lifecycle_one_owner_turn(pending, document) + .await?; + if !matches!( + outcome.readiness, + DocumentLifecycleTurnReadiness::Blocked { .. } + ) || pending + .as_ref() + .is_none_or(|pending| pending.awaiting_dom_task.is_none()) + { + return Ok(outcome); + } + let snapshot = self.document_replacement_lifecycle_action_snapshot(); + let loader = self.request_client.clone(); + assert!( + self.run_exact_selected_page_task_for_test( + super::super::PageSelectedTaskTestSelector::DomManipulation( + super::super::PageDomManipulationTestFamily::MainDocumentLifecycle, + ), + &loader, + ) + .await? + ); + if let Some(outcome) = self + .reconcile_document_replacement_lifecycle_after_owner_action(snapshot, pending) + .await? + { + return Ok(outcome); + } + } + anyhow::bail!("lifecycle fixture exceeded its bounded DOM task turns") + } +} + async fn exact_lifecycle_turn_reaches_handler_navigation( event_target: &str, event_name: &str, @@ -303,7 +364,7 @@ async fn exact_lifecycle_turn_reaches_handler_navigation( for _ in 0..128 { match page_vm - .advance_post_parse_lifecycle_one_owner_turn(&mut pending, document) + .advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document) .await? { DocumentLifecycleTurnOutcome { @@ -369,7 +430,7 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement( for _ in 0..96 { match page_vm - .advance_post_parse_lifecycle_one_owner_turn( + .advance_lifecycle_and_selected_dom_task_for_test( &mut pending_document_lifecycle_turn, document, ) @@ -401,7 +462,17 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement( }, .. } => { - assert_eq!(next_document, document); + if next_document != document { + assert_ne!(next_document, initial_document); + assert_eq!(page_vm.document_lifecycle.identity(), next_document); + assert_eq!( + pending_document_lifecycle_turn + .as_ref() + .map(|pending| pending.document), + Some(next_document) + ); + return Ok(()); + } document = next_document; } DocumentLifecycleTurnOutcome { @@ -576,7 +647,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() { for _ in 0..128 { let outcome = page_vm - .advance_post_parse_lifecycle_one_owner_turn(&mut pending, document) + .advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document) .await?; if !matches!( outcome.action, @@ -622,7 +693,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() { for _ in 0..128 { let resumed = page_vm - .advance_post_parse_lifecycle_one_owner_turn( + .advance_lifecycle_and_selected_dom_task_for_test( &mut pending, document, ) @@ -1729,6 +1800,15 @@ Promise.resolve().then(() => __parseTimeExactDclOrder.push("module-terminal:micr outcome, ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained ); + assert_eq!( + page_vm.vm_mut().eval_without_microtask_checkpoint_for_test( + "__parseTimeExactDclOrder.join('|')" + )?, + "module-terminal:microtask", + "parser completion must only enqueue DCL" + ); + let loader = page_vm.request_client.clone(); + assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?); assert_eq!( page_vm.vm_mut().eval_without_microtask_checkpoint_for_test( "__parseTimeExactDclOrder.join('|')" @@ -1789,6 +1869,8 @@ Promise.resolve().then(() => { ParseTimeMainParserBoundaryOutcome::DocumentReplaced, "the parser task-end checkpoint must report replacement before attempting the old exact DCL" ); + let loader = page_vm.request_client.clone(); + assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?); assert_ne!( page_vm.vm().current_main_document_task_owner(), Some(owner) @@ -1918,13 +2000,16 @@ document.addEventListener("DOMContentLoaded", () => { .await?; assert_eq!( outcome, - ParseTimeMainParserBoundaryOutcome::DocumentReplaced, - "phase one must stop the retired Document after an exact-DCL reaction replaces it" + ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained, + "the parser returns before the queued DCL listener can replace its Document" ); - assert_ne!( - page_vm.vm().current_main_document_task_owner(), - Some(owner) + let loader = page_vm.request_client.clone(); + assert!( + page_vm + .run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader) + .await? ); + assert_ne!(page_vm.vm().current_main_document_task_owner(), Some(owner)); assert_eq!( page_vm.vm_mut().eval( "String(document.getElementById('parse-time-replacement') !== null)" @@ -2377,7 +2462,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() { let dcl_outcome = loop { let outcome = page_vm - .advance_post_parse_lifecycle_one_owner_turn( + .advance_lifecycle_and_selected_dom_task_for_test( &mut pending_document_lifecycle_turn, document, ) @@ -2437,7 +2522,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() { loop { match page_vm - .advance_post_parse_lifecycle_one_owner_turn( + .advance_lifecycle_and_selected_dom_task_for_test( &mut pending_document_lifecycle_turn, document, ) @@ -3605,7 +3690,7 @@ fn top_level_http_location_navigation_reserves_service_worker_client_until_commi } => { loop { match page_vm - .advance_post_parse_lifecycle_one_owner_turn( + .advance_lifecycle_and_selected_dom_task_for_test( &mut pending_document_lifecycle_turn, lifecycle_document, ) diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index 6b95448ee8..c0303d9f99 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -2053,12 +2053,20 @@ async fn run_child_interactive_domcontentloaded_then_host_load_for_wait( page_vm: &mut PageVm, label: &str, ) -> ChildFrameSemanticTurnKind { - run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( - page_vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - &format!("{label} interactive transition"), - ) - .await; + if page_vm.has_ready_child_frame_semantic_turn_for_test( + ChildFrameSemanticTurnKind::RealmMaterialization, + ) { + run_expected_child_realm_materialization_for_wait(page_vm, label).await; + } + if matches!(page_vm.page_task_executor_sources_for_test().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_))) + { + run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( + page_vm, + ChildFrameSemanticTurnKind::DocumentLifecycle, + &format!("{label} queued interactive transition"), + ) + .await; + } run_child_domcontentloaded_then_host_load_for_wait(page_vm, label).await } @@ -3439,14 +3447,7 @@ globalThis.__childClassicWaitValue = 91; "external:true|current:external-classic|inline:91", "second DocumentScriptReady should run parser continuation without iframe load" ); - followup_sources.push( - run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( - &mut page_vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child classic parser EOF interactive transition", - ) - .await, - ); + assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously"); followup_sources.push( run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( &mut page_vm, @@ -3535,10 +3536,9 @@ globalThis.__childClassicWaitValue = 91; ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::DocumentLifecycle, - ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::HostLoad ], - "child classic completion should progress through script, parser continuation, interactive, DOMContentLoaded, complete, and HostLoad turns" + "child classic completion should progress through script, parser continuation, DOMContentLoaded, complete, and HostLoad turns" ); assert_eq!( final_events, @@ -3679,14 +3679,7 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet() "script:stylesheet-ready:loading", "stylesheet source must be installed before the parser-blocking script executes while the document is still loading" ); - followup_sources.push( - run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( - &mut page_vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "stylesheet-gated child interactive transition", - ) - .await, - ); + assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously"); followup_sources.push( run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( &mut page_vm, @@ -3750,7 +3743,6 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet() ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::DocumentLifecycle, - ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::HostLoad, ] ); @@ -4516,9 +4508,8 @@ globalThis.__childClassicDeferWaitValue = 73; ChildFrameSemanticTurnKind::RealmMaterialization, ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentScriptReady, - ChildFrameSemanticTurnKind::DocumentLifecycle ], - "child defer classic bootstrap should end with the document-owned interactive turn" + "child defer classic bootstrap should apply interactive at parser EOF" ); assert_eq!( followup_sources, @@ -5100,7 +5091,6 @@ globalThis.__childClassicAsyncWaitValue = 41; ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentLifecycle, - ChildFrameSemanticTurnKind::DocumentLifecycle ], "child async bootstrap should dispatch interactive and DCL before the later async completion" ); @@ -5347,7 +5337,6 @@ parent.__childClassicDeferOrderEvents.push("current:" + document.currentScript.i ChildFrameSemanticTurnKind::RealmMaterialization, ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::DocumentScriptReady, - ChildFrameSemanticTurnKind::DocumentLifecycle ], "child defer ordering bootstrap should reach interactive before either source completion" ); @@ -5782,8 +5771,8 @@ async fn page_vm_child_parser_defer_preserves_cross_kind_document_order() { "parser EOF must not execute any mixed parser-deferred script" ); assert!( - bootstrap_sources.contains(&ChildFrameSemanticTurnKind::DocumentLifecycle), - "mixed parser-deferred document should reach interactive" + page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")? == "interactive", + "mixed parser-deferred document should become interactive at parser EOF" ); assert_eq!( bootstrap_sources @@ -6493,7 +6482,6 @@ globalThis.__childParserModuleWaitValue = 188; ChildFrameSemanticTurnKind::DocumentScriptReady, ChildFrameSemanticTurnKind::ParserModuleRootStart, ChildFrameSemanticTurnKind::DocumentScriptReady, - ChildFrameSemanticTurnKind::DocumentLifecycle ], "the typed root fetch-start must preserve parser discovery FIFO, then the parser should run the following inline script and reach interactive" ); @@ -8865,8 +8853,8 @@ async fn page_vm_child_modulepreload_terminal_event_does_not_delay_complete() { .iter() .filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle) .count() - >= 3, - "interactive, DOMContentLoaded, and complete must advance while the terminal link event remains queued: {lifecycle_turns:?}" + == 2, + "DOMContentLoaded and complete must each take a queued turn while the terminal link event remains queued: {lifecycle_turns:?}" ); assert!( lifecycle_turns.contains(&ChildFrameSemanticTurnKind::HostLoad), @@ -9082,8 +9070,8 @@ async fn page_vm_child_modulepreload_fetch_does_not_delay_iframe_load() { .iter() .filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle) .count() - >= 3, - "interactive, DOMContentLoaded and complete should run while modulepreload fetch is pending: {startup_sources:?}" + == 2, + "DOMContentLoaded and complete should each take a queued turn while modulepreload fetch is pending: {startup_sources:?}" ); assert!(startup_sources.contains(&ChildFrameSemanticTurnKind::HostLoad)); assert_eq!( @@ -9565,7 +9553,7 @@ globalThis.__childDocumentLoadWaitValue = 42; first_followup_source, events_after_first_followup, lifecycle_ready_after_first_followup, - interactive_source, + interactive_state, host_load_source, final_events, ) = local_executor @@ -9644,12 +9632,7 @@ globalThis.__childDocumentLoadWaitValue = 42; ChildFrameSemanticTurnKind::DocumentLifecycle, ); - let interactive_source = run_expected_child_frame_task_source_after_realm_prerequisite_for_wait( - &mut page_vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child document parser EOF interactive transition", - ) - .await; + let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?; let host_load_source = run_child_domcontentloaded_then_host_load_for_wait( &mut page_vm, "child document iframe load", @@ -9672,7 +9655,7 @@ globalThis.__childDocumentLoadWaitValue = 42; first_followup_source, events_after_first_followup, lifecycle_ready_after_first_followup, - interactive_source, + interactive_state, host_load_source, final_events, )) @@ -9710,8 +9693,8 @@ globalThis.__childDocumentLoadWaitValue = 42; "document-script ready should make the later lifecycle turn runnable" ); assert_eq!( - interactive_source, - ChildFrameSemanticTurnKind::DocumentLifecycle, + interactive_state, + "interactive", "parser EOF should become interactive before HostLoad" ); assert_eq!( @@ -9885,8 +9868,6 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self) page_vm.run_next_child_frame_task_source_for_semantic_test().await, page_vm.run_next_child_frame_task_source_for_semantic_test().await, page_vm.run_next_child_frame_task_source_for_semantic_test().await, - page_vm.run_next_child_frame_task_source_for_semantic_test().await, - page_vm.run_next_child_frame_task_source_for_semantic_test().await, ]; let first_host_load_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await; let events_after_first_host_load = page_vm @@ -9966,8 +9947,8 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self) ); assert_eq!( lifecycle_sources, - vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 6], - "interactive, DOMContentLoaded and complete must each consume one lifecycle turn per child" + vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 4], + "DOMContentLoaded and complete must each consume one lifecycle turn per child" ); assert_eq!( first_host_load_source, @@ -10034,7 +10015,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self)); script_ready_source, events_after_script_ready, lifecycle_ready_after_script, - interactive_source, + interactive_state, host_load_source, events_after_host_load, ) = local_executor @@ -10092,7 +10073,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self)); ChildFrameSemanticTurnKind::DocumentLifecycle, ); - let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await; + let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?; let host_load_source = Some( run_child_domcontentloaded_then_host_load_for_wait( &mut page_vm, @@ -10111,7 +10092,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self)); script_ready_source, events_after_script_ready, lifecycle_ready_after_script, - interactive_source, + interactive_state, host_load_source, events_after_host_load, )) @@ -10146,8 +10127,8 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self)); "DocumentScriptReady should make the later lifecycle turn runnable" ); assert_eq!( - interactive_source, - Some(ChildFrameSemanticTurnKind::DocumentLifecycle), + interactive_state, + "interactive", "parser EOF should dispatch interactive before HostLoad" ); assert_eq!( @@ -10771,7 +10752,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script .run_next_child_frame_task_source_for_semantic_test() .await, Some(ChildFrameSemanticTurnKind::DocumentLifecycle), - "outer parser EOF should become interactive before nested document work" + "outer DOMContentLoaded must precede nested parser work admitted after it" ); assert_eq!( page_vm @@ -10792,7 +10773,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script .run_next_child_frame_task_source_for_semantic_test() .await, Some(ChildFrameSemanticTurnKind::DocumentLifecycle), - "outer DOMContentLoaded should remain a later FIFO turn after nested work already admitted by realm materialization" + "nested DOMContentLoaded must remain a later FIFO turn after its parser work" ); Ok::<_, anyhow::Error>(()) }) @@ -11344,12 +11325,12 @@ onload = () => { host_load_pending_after_completion, script_ready_source, events_after_script_ready, - interactive_source, + interactive_state, host_load_source, events_after_host_load, nested_script_ready_source, events_after_nested_script_ready, - nested_interactive_source, + nested_interactive_state, nested_host_load_source, events_after_nested_host_load, ) = local_executor @@ -11393,7 +11374,7 @@ onload = () => { let events_after_script_ready = page_vm .vm_mut() .eval("__hostLoadNestedEvents.join('|')")?; - let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await; + let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?; let host_load_source = Some( run_child_domcontentloaded_then_host_load_for_wait( &mut page_vm, @@ -11419,8 +11400,7 @@ onload = () => { let events_after_nested_script_ready = page_vm .vm_mut() .eval("__hostLoadNestedEvents.join('|')")?; - let nested_interactive_source = - page_vm.run_next_child_frame_task_source_for_semantic_test().await; + let nested_interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.querySelector('iframe').contentDocument.readyState")?; let nested_host_load_source = Some( run_child_domcontentloaded_then_host_load_for_wait( &mut page_vm, @@ -11438,12 +11418,12 @@ onload = () => { host_load_pending_after_completion, script_ready_source, events_after_script_ready, - interactive_source, + interactive_state, host_load_source, events_after_host_load, nested_script_ready_source, events_after_nested_script_ready, - nested_interactive_source, + nested_interactive_state, nested_host_load_source, events_after_nested_host_load, )) @@ -11471,8 +11451,8 @@ onload = () => { ); assert_eq!(events_after_script_ready, "child-script"); assert_eq!( - interactive_source, - Some(ChildFrameSemanticTurnKind::DocumentLifecycle), + interactive_state, + "interactive", "child parser EOF should become interactive before window load" ); assert_eq!( @@ -11494,8 +11474,8 @@ onload = () => { "nested script should run on the later DocumentScriptReady turn" ); assert_eq!( - nested_interactive_source, - Some(ChildFrameSemanticTurnKind::DocumentLifecycle), + nested_interactive_state, + "interactive", "nested parser EOF should become interactive before nested HostLoad" ); assert_eq!( @@ -11586,7 +11566,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis completion_source, script_ready_source, events_after_script_ready, - interactive_source, + interactive_state, host_load_source, events_after_host_load, pending_after_host_load, @@ -11638,7 +11618,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis .vm_mut() .eval("__childLoadNavigationEvents.join('|')")?; - let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await; + let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?; let host_load_source = Some( run_child_domcontentloaded_then_host_load_for_wait( &mut page_vm, @@ -11688,7 +11668,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis completion_source, script_ready_source, events_after_script_ready, - interactive_source, + interactive_state, host_load_source, events_after_host_load, pending_after_host_load, @@ -11723,8 +11703,8 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis "DocumentScriptReady should not dispatch child window load inline" ); assert_eq!( - interactive_source, - Some(ChildFrameSemanticTurnKind::DocumentLifecycle), + interactive_state, + "interactive", "child document should become interactive before its window load" ); assert_eq!( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs b/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs index 5e1a64abe2..029c393791 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/view_transition.rs @@ -213,7 +213,7 @@ JSON.stringify({ PageDomManipulationTestFamily::ViewTransitionUpdate, ) .expect("view-transition update callback should queue one platform task"); - let body = page_vm.apply_selected_page_dom_manipulation_turn(task)?; + let body = page_vm.apply_selected_page_dom_manipulation_turn(task).await?; let PageDomManipulationTurnAction::ViewTransitionUpdate(action) = body.action else { unreachable!("the selected view-transition task must retain its typed action") }; diff --git a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs index 8eeb9b6531..df135d8458 100644 --- a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs +++ b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs @@ -1246,7 +1246,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { start_line, start_column, import_map, - ); + )?; Ok(ScriptHandoffOutcome::NoNavigation) } ParserScriptHandoff::NoExecution { diff --git a/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs b/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs index b64d1459be..aa52bc43c2 100644 --- a/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs +++ b/moli-renderer-v8/src/runtime/phase_one/streaming_admission_tests.rs @@ -753,6 +753,16 @@ fn streaming_stylesheet_and_json_ld_reach_tail_and_post_parse_boundary() { DocumentLifecycleTurnReadiness::Runnable { .. }, .. } => {} + DocumentLifecycleTurnOutcome { + readiness: DocumentLifecycleTurnReadiness::Blocked { .. }, + .. + } if pending.as_ref().is_some_and(|pending| pending.awaiting_dom_task.is_some()) => { + let loader = page_vm.request_client.clone(); + assert!(page_vm.run_exact_selected_page_task_for_test( + crate::runtime::page_vm::PageSelectedTaskTestSelector::AnyDomManipulation, + &loader, + ).await?, "admitted DCL must advance through the real DOM source head"); + } outcome => panic!( "streaming fixture should reach DCL without parking: {outcome:?}" ), diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 52970e057d..6670eb9c44 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -5338,9 +5338,23 @@ impl ScriptVm { ) -> bool { use crate::{ frame_owner_model::ChildFrameSemanticTurnKind, - page_task_queue::RendererPageChildFrameTaskTarget, + page_task_queue::{ + RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner, + RendererPageReadyDescriptor, + }, }; + if expected == ChildFrameSemanticTurnKind::DocumentLifecycle { + return self._page_task_residence_for_executor_test.as_ref().expect("semantic fixture must retain its sources").task_sources().has_scheduler_task_for_executor_test(|descriptor| { + matches!(descriptor, + RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. } + ) || matches!(descriptor, + RendererPageReadyDescriptor::ChildFrameTask { owner, .. } + if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_)) + ) + }); + } + let Some(target) = self ._page_task_residence_for_executor_test .as_ref() @@ -5393,7 +5407,14 @@ impl ScriptVm { { return Some(ChildFrameSemanticTurnKind::NavigationCommit); } - if self + if matches!( + self._page_task_residence_for_executor_test + .as_ref() + .expect("child fixture must retain its sources") + .task_sources() + .next_child_semantic_task_target(), + Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)) + ) && self .run_child_document_lifecycle_body_for_test() .expect("typed child lifecycle executor turn should succeed") .is_some() diff --git a/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs b/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs index 4fbac866e5..edec0178c1 100644 --- a/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs +++ b/moli-renderer-v8/src/script_vm/child_document_lifecycle.rs @@ -27,15 +27,29 @@ impl<'vm> ChildDocumentLifecycleOwner<'vm> { pub(super) fn notify_parser_stop_action( &mut self, action: FrameDocumentInteractiveLifecycleAction, - ) -> bool { - self.vm - ._context_host - .borrow_mut() - .queue_child_document_interactive_lifecycle_action(action) + ) -> Result { + let context_host = self.vm._context_host.clone(); + self.vm.with_default_context_scope(move |scope, _| { + Ok(context_host + .borrow_mut() + .finish_child_document_parser_stop(scope, action)) + }) } } impl ScriptVm { + pub(crate) fn child_document_lifecycle_waits_for_realm( + &self, + target: RendererPageChildDocumentLifecycleTarget, + ) -> bool { + let host = self._context_host.borrow(); + host.child_document_lifecycle_action_is_current(target.action()) + && host.has_child_frame_realm_materialization_request( + target.child_handle(), + target.document_owner(), + ) + } + pub(crate) fn current_child_document_lifecycle_target( &self, expected: RendererPageChildDocumentLifecycleTarget, @@ -102,9 +116,9 @@ impl ScriptVm { let Some(task) = source.take_scheduler_task_for_executor_test(|descriptor| { matches!( descriptor, - crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask { - owner, - .. + crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask { owner, .. } + | crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { + owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), .. } if matches!( owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_) @@ -113,8 +127,14 @@ impl ScriptVm { }) else { return Ok(None); }; - let crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task) = task else { - unreachable!("child-frame descriptor must dequeue its own family source") + let task = match task { + crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task) + | crate::page_task_queue::RendererPageSchedulerTask::DomManipulation( + crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle( + task, + ), + ) => task, + _ => unreachable!("child lifecycle selector must dequeue its admitted task"), }; let RendererPageChildFrameTaskTarget::DocumentLifecycle(target) = task.owner().target() else { diff --git a/moli-renderer-v8/src/script_vm/child_navigation_commit.rs b/moli-renderer-v8/src/script_vm/child_navigation_commit.rs index 624e317709..df3dc06da4 100644 --- a/moli-renderer-v8/src/script_vm/child_navigation_commit.rs +++ b/moli-renderer-v8/src/script_vm/child_navigation_commit.rs @@ -60,7 +60,7 @@ impl ScriptVm { self.apply_child_document_owner_transition(transition); } if let Some(action) = parser_stop_action { - ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action); + ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action)?; } let mut ready_owner = ChildDocumentScriptSchedulerOwner::new(self); for work in ready_work { diff --git a/moli-renderer-v8/src/script_vm/native_module.rs b/moli-renderer-v8/src/script_vm/native_module.rs index 741ef9fcb7..da48abc6b3 100644 --- a/moli-renderer-v8/src/script_vm/native_module.rs +++ b/moli-renderer-v8/src/script_vm/native_module.rs @@ -5317,7 +5317,7 @@ mod tests { vm: &mut ScriptVm, label: &str, ) { - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { assert!( vm.run_child_frame_task_source_once_for_test( ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -5355,7 +5355,6 @@ mod tests { label: &str, ) { for expected in [ - ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::DocumentLifecycle, ChildFrameSemanticTurnKind::HostLoad, diff --git a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs index 6a350a9527..6a7d3d125b 100644 --- a/moli-renderer-v8/src/script_vm/page_task_enqueue.rs +++ b/moli-renderer-v8/src/script_vm/page_task_enqueue.rs @@ -110,8 +110,8 @@ impl ScriptVm { /// The main-document lifecycle coordinator owns the ordinary task-end /// checkpoint. Keeping this primitive body-only prevents DCL and /// `readystatechange` helpers from silently becoming a second completion - /// authority. Parser-finish DCL temporarily uses the explicit compatibility - /// wrapper below until P5-A3 moves that direct successor as one unit. + /// authority. Parser completion admits DCL to the DOM task source after + /// finishing its own task-end checkpoint. pub(super) fn dispatch_document_lifecycle_event_body( &mut self, event_type: &str, diff --git a/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs b/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs index a2ce28cec3..f7fdd523cd 100644 --- a/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs +++ b/moli-renderer-v8/src/script_vm/post_parse_lifecycle.rs @@ -908,6 +908,32 @@ impl ScriptVm { Some(ParserFinishDomContentLoadedWork::new(owner, *work)) } + pub(crate) fn main_document_window_load_task_is_ready( + &mut self, + owner: FrameDocumentTaskOwner, + ) -> Option { + let has_runtime_delay = self.has_post_domcontentloaded_load_delaying_runtime_work(); + self._context_host + .borrow() + .current_main_document_complete_transition_is_ready(owner) + .map(|ready| ready && !has_runtime_delay) + } + + pub(crate) fn queue_main_document_lifecycle_dom_task( + &self, + body: super::MainDocumentLifecycleBody, + completion: Option< + tokio::sync::oneshot::Sender< + crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion, + >, + >, + ) -> anyhow::Result<()> { + self._context_host + .borrow() + .page_main_document_lifecycle_sender() + .send(body, completion) + } + /// Consume the completed parser task and claim only its exact, already /// queued DOMContentLoaded successor. /// @@ -1840,21 +1866,6 @@ impl ScriptVm { reached_boundary, invalidation_policy, } = PostParseProcessingAction::from_document_processing_action(action); - if work.is_domcontentloaded_task() - && self - .document_runtime - .has_ready_timeout_queued_by_classic_defer_script() - { - // The lifecycle plan pre-seeds this boundary marker so owner work - // stays ordered. It has now reached the front after defer-like and - // parser-owned pre-DCL work, which is the point at which HTML queues - // the actual DOMContentLoaded global task. Give only ready timers - // scheduled by classic defer scripts their intervening task turn. - page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![work]); - return PostParseDriverStep::Ready(Box::new( - ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded, - )); - } if !(work.is_domcontentloaded_task() || work.is_window_load_task()) { return PostParseDriverStep::Ready(Box::new(ReadyPostParseAction::Processing( Box::new(PostParseProcessingAction { @@ -1977,11 +1988,6 @@ impl ScriptVm { ReadyPostParseAction::Processing(action) => { return Ok(PostParseProcessingStep::Action(action)); } - ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded => { - return Ok( - PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded, - ); - } }, PostParseDriverStep::NeedsContinuation => { return Ok(PostParseProcessingStep::NeedsContinuation); @@ -2020,11 +2026,6 @@ impl ScriptVm { execution.into_page_owned_task(), ))); } - PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded => { - return Ok( - PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded, - ); - } PostParseProcessingStep::NeedsContinuation => { return Ok(PostParseLifecycleAdvance::NeedsContinuation); } diff --git a/moli-renderer-v8/src/script_vm/runtime_work.rs b/moli-renderer-v8/src/script_vm/runtime_work.rs index 34acd51688..a165bfebab 100644 --- a/moli-renderer-v8/src/script_vm/runtime_work.rs +++ b/moli-renderer-v8/src/script_vm/runtime_work.rs @@ -28,7 +28,6 @@ pub(crate) enum PostParseDrainResult { pub(crate) enum PostParseLifecycleAdvance { PageOwnedTask(Box), - TimerQueuedByClassicDeferBeforeDomContentLoaded, NeedsContinuation, AwaitProgress, Complete(PostParseLifecycleCompletionAction), @@ -193,20 +192,20 @@ pub(crate) struct PostParsePageOwnedTask { pub(super) completion: PostParseTaskCompletion, } -/// The exact DOMContentLoaded action claimed as the direct successor of a +/// The exact DOMContentLoaded action claimed for DOM task admission after a /// drained main-parser queue. /// /// This value is deliberately short-lived: the existing post-parse queue and /// lifecycle driver remain the only durable authority. The wrapper merely /// prevents an already-claimed DCL action from falling back to a generic -/// page-owned task before the parser continuation commits it. +/// page-owned task before the parser continuation admits it to the DOM source. pub(crate) struct ParserFinishDomContentLoadedTask { owner: FrameDocumentTaskOwner, task: PostParsePageOwnedTask, } -/// The exact parse-time DOMContentLoaded work claimed as the direct successor -/// of a drained main-parser queue. +/// The exact parse-time DOMContentLoaded work claimed for DOM task admission +/// after a drained main-parser queue. /// /// Unlike [`ParserFinishDomContentLoadedTask`], phase one has no installed /// post-parse driver task token to complete. The lifecycle authority still @@ -244,7 +243,7 @@ impl ParserFinishDomContentLoadedTask { self.task.take_work_for_execution() } - pub(crate) fn into_completed_task(self) -> PostParsePageOwnedTask { + pub(crate) fn into_pending_task(self) -> PostParsePageOwnedTask { self.task } } @@ -323,7 +322,6 @@ impl PostParseProcessingAction { pub(crate) enum ReadyPostParseAction { Processing(Box), - TimerQueuedByClassicDeferBeforeDomContentLoaded, } pub(crate) enum PostParseRuntimeDriverStep { @@ -340,7 +338,6 @@ pub(crate) enum PostParseDriverStep { pub(crate) enum PostParseProcessingStep { Action(Box), - TimerQueuedByClassicDeferBeforeDomContentLoaded, NeedsContinuation, AwaitProgress, Idle, diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index 5ca2fb9a1e..99252e71ba 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -6068,7 +6068,7 @@ impl ScriptVm { } }; self.apply_pending_child_document_owner_retirements(); - let (application, body_activity) = application; + let (application, mut body_activity) = application; let Some(application) = application else { return Ok(CurrentChildDocumentLoadApplication::Applied { body_activity }); }; @@ -6076,9 +6076,13 @@ impl ScriptVm { if let Some(transition) = owner_transition { self.apply_child_document_owner_transition(transition); } - if let Some(action) = parser_stop_action { - super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self) - .notify_parser_stop_action(action); + if let Some(action) = parser_stop_action + && super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self) + .notify_parser_stop_action(action)? + == crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::EventDispatched + { + body_activity = + crate::native_bridge::ChildDocumentLoadBodyActivity::PageCodeOrEventDispatch; } if let Some(work) = work { super::child_document_script_scheduler::ChildDocumentScriptSchedulerOwner::new(self) diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index 5bd3a9612b..55d709ba15 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -11654,7 +11654,7 @@ async fn iframe_javascript_url_string_completion_replaces_child_document() { "javascript URL should execute on DocumentScriptReady", ) .await; - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -13644,7 +13644,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output() "old Window load listener must retain a current callback relevant realm" ); } - for context in ["old interactive", "old DOMContentLoaded", "old complete"] { + for context in ["old DOMContentLoaded", "old complete"] { expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -13696,11 +13696,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output() "replacement child load handlers should install", ) .await; - for context in [ - "replacement interactive", - "replacement DOMContentLoaded", - "replacement complete", - ] { + for context in ["replacement DOMContentLoaded", "replacement complete"] { expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -13855,7 +13851,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output() "old Window lifecycle listeners must retain current callback relevant realms" ); } - for context in ["old interactive", "old DOMContentLoaded", "old complete"] { + for context in ["old DOMContentLoaded", "old complete"] { expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -13906,11 +13902,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output() "replacement pageshow handlers should install", ) .await; - for context in [ - "replacement interactive", - "replacement DOMContentLoaded", - "replacement complete", - ] { + for context in ["replacement DOMContentLoaded", "replacement complete"] { expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -14088,13 +14080,12 @@ async fn child_static_media_delays_complete_and_iframe_load_until_loadeddata() { "child media parser script should run before lifecycle", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child media document should enter interactive and accept its media token", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_page_child_frame_task_source_after_realm_prerequisite( &mut vm, &loader, @@ -14212,13 +14203,12 @@ async fn child_media_network_failure_releases_lifecycle_before_later_host_load() "child media parser script should install listeners", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child failed media document should become interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); let request = request_rx.await.expect("child media request should arrive"); assert!( request @@ -14339,13 +14329,12 @@ async fn child_image_network_failure_releases_lifecycle_before_later_host_load() "child image parser script should install listeners", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child image document should become interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); let request = request_rx.await.expect("child image request should arrive"); assert!( request @@ -14518,13 +14507,12 @@ async fn child_dynamic_media_accepted_during_dcl_delays_later_load_turns() { "dynamic media parser script should install its DCL producer", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "dynamic media document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_page_child_frame_task_source_after_realm_prerequisite( &mut vm, &loader, @@ -14617,13 +14605,12 @@ async fn child_static_text_track_starts_at_interactive_without_own_load_token() "child track parser script should install its listener", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "interactive should start the static child track", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); assert!( vm.run_one_dom_manipulation_task_executor_turn( PageDomManipulationTestFamily::TextTrackDefaultMode, @@ -14746,12 +14733,12 @@ async fn child_document_replacement_retires_media_sequence_and_delay() { "first media document parser script should run", ) .await; - expect_child_frame_task_source_after_realm_prerequisite( - &mut vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "first media document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -14796,7 +14783,6 @@ async fn child_document_replacement_retires_media_sequence_and_delay() { ) .await; for (source, transition) in [ - (ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"), ( ChildFrameSemanticTurnKind::DocumentLifecycle, "DOMContentLoaded", @@ -14868,13 +14854,12 @@ async fn moving_pending_child_media_restarts_under_the_new_document_owner() { "moving media parser script should run", ) .await; - expect_page_child_frame_task_source_after_realm_prerequisite( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "moving media document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_page_child_frame_task_source_after_realm_prerequisite( &mut vm, &loader, @@ -15014,12 +14999,12 @@ async fn child_image_event_delay_blocks_complete_and_host_load_until_terminal() "child image parser script should run before lifecycle", ) .await; - expect_child_frame_task_source_after_realm_prerequisite( - &mut vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child image document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -15117,12 +15102,12 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() { "first image document parser script should run", ) .await; - expect_child_frame_task_source_after_realm_prerequisite( - &mut vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "first image document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -15166,8 +15151,19 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() { "replacement parser script should run", ) .await; + assert!( + vm.apply_next_image_load_event_body_for_test() + .expect("stale image DOM task"), + "the earlier image task must retire at the shared DOM FIFO head" + ); + assert_eq!( + vm.eval("__childImageReplacementEvents.join('|')") + .expect("retired image trace"), + "first-dcl", + "the stale image task must not dispatch into the replacement" + ); + for (source, transition) in [ - (ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"), ( ChildFrameSemanticTurnKind::DocumentLifecycle, "DOMContentLoaded", @@ -15233,12 +15229,12 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request( "moving image parser script should run", ) .await; - expect_child_frame_task_source_after_realm_prerequisite( - &mut vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "moving image document should enter interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -15266,6 +15262,17 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request( "#, ) .expect("pending child image should move to the parent document"); + assert!( + vm.apply_next_image_load_event_body_for_test() + .expect("old image DOM task"), + "the old image task must retire before the later complete task at the shared FIFO head" + ); + assert_eq!( + vm.eval("__movedChildImageEvents.join('|')") + .expect("old image trace"), + "child-dcl", + "retiring the old image task must leave the rebound request pending" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -16035,12 +16042,12 @@ async fn child_document_close_without_defer_queues_replacement_domcontentloaded( "child parser script should install the original DCL handler", ) .await; - expect_child_frame_task_source_after_realm_prerequisite( - &mut vm, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "original child document should become interactive", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); expect_child_frame_task_source_after_realm_prerequisite( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -16150,10 +16157,16 @@ async fn child_script_document_open_after_location_navigation_is_noop() { .await, "the blob URL assigned by the child script must commit on a later navigation turn" ); + expect_one_child_frame_task_source( + &mut vm, + ChildFrameSemanticTurnKind::RealmMaterialization, + "blob replacement must materialize its exact realm", + ) + .await; expect_one_child_frame_task_source( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, - "the queued srcdoc lifecycle task must stale-discard before blob lifecycle work", + "the older srcdoc DCL must stale-discard at the shared DOM head", ) .await; for transition in ["interactive", "DOMContentLoaded", "complete"] { diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 49b2ba5e19..bb4fe3d049 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -4752,11 +4752,16 @@ async fn run_child_document_lifecycle_and_host_load_for_test(vm: &mut ScriptVm, // Only consecutive materialization tasks at the stable family head // belong here. Never jump over an earlier DocumentScriptReady task. } - assert!( - vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle) + if matches!(vm._page_task_residence_for_executor_test.as_ref().expect("child fixture sources").task_sources().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_))) + { + assert!( + vm.run_child_frame_task_source_once_for_test( + ChildFrameSemanticTurnKind::DocumentLifecycle + ) .await, - "{message}: DocumentLifecycle should make the installed document interactive" - ); + "{message}: a child without a parser realm must apply its queued interactive transition" + ); + } assert!( vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle) .await, @@ -7147,7 +7152,7 @@ async fn child_srcdoc_inline_classic_script_runs_as_frame_script_job() { "child:true|current:inline-classic", "DocumentScriptReady should execute the inline classic script without firing iframe load" ); - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { assert!( vm.run_child_frame_task_source_once_for_test( ChildFrameSemanticTurnKind::DocumentLifecycle @@ -7264,7 +7269,7 @@ async fn child_inline_classic_script_moved_from_original_document_is_skipped() { "after:after-stale-inline", "parser continuation should run the next inline classic script without firing iframe load" ); - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -7593,7 +7598,7 @@ async fn child_external_classic_script_load_executes_as_frame_script_job() { "external:true|external-current:external-classic|external-write:true|script-load|inline-current:after-external-classic|inline:73", "ignored document.open() must leave the parser owner alive for the following inline script" ); - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { run_page_realm_prerequisite_then_expected_child_frame_semantic_turn( &mut vm, &loader, @@ -8935,10 +8940,11 @@ async fn child_inline_parser_module_executes_from_registered_pending_script() { "parser should continue past the deferred inline module after graph start", ) .await; - assert!( - vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle) - .await, - "parser EOF should make the child document interactive before defer execution" + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" ); assert!( vm.run_child_frame_task_source_once_for_test( @@ -9048,13 +9054,12 @@ async fn child_external_parser_module_executes_from_document_ready_lane() { "parser should continue past module-defer script from DocumentScriptReady after fetch start", ) .await; - run_page_realm_prerequisite_then_expected_child_frame_semantic_turn( - &mut vm, - &loader, - ChildFrameSemanticTurnKind::DocumentLifecycle, - "child parser module should enter interactive before module-defer execution", - ) - .await; + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" + ); assert!( !vm.run_one_child_frame_task_executor_turn(ChildFrameSemanticTurnKind::HostLoad, &loader) .await @@ -9194,7 +9199,7 @@ async fn child_dynamic_import_root_fetch_uses_child_import_map_and_initiator_url .expect("child dynamic import owner ready flag should evaluate"), "true" ); - for transition in ["interactive", "DOMContentLoaded", "complete"] { + for transition in ["DOMContentLoaded", "complete"] { run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test( &mut vm, ChildFrameSemanticTurnKind::DocumentLifecycle, @@ -9451,14 +9456,11 @@ async fn child_external_classic_source_error_dispatches_before_later_inline() { "script-error|after-inline", "parser continuation should execute the following inline script without firing iframe load" ); - assert!( - vm.run_one_child_frame_task_executor_turn( - ChildFrameSemanticTurnKind::DocumentLifecycle, - &loader, - ) - .await - .expect("child interactive task should use the selected-task dispatcher"), - "parser EOF should dispatch interactive before HostLoad" + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" ); assert!( vm.run_one_child_frame_task_executor_turn( @@ -9596,10 +9598,11 @@ async fn child_inline_classic_throw_reports_to_child_window_and_continues() { "child-error:Uncaught Error: child-boom:child-boom:true|after-inline:true", "parser continuation should run the following inline script without firing iframe load" ); - assert!( - vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle) - .await, - "parser EOF should dispatch interactive before HostLoad" + assert_eq!( + vm.eval("document.querySelector('iframe').contentDocument.readyState") + .expect("child readiness"), + "interactive", + "parser EOF must apply interactive synchronously" ); assert!( vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle) diff --git a/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs b/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs index 6d5d7a3d37..564b8fa312 100644 --- a/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs +++ b/moli-renderer-v8/src/script_vm/tests/script_terminal_completion.rs @@ -128,7 +128,8 @@ addEventListener("error", () => { base_url: document_url.clone(), initiator_url: document_url, }, - ); + ) + .expect("parser import-map handoff should be admitted"); assert_eq!( vm.eval_without_microtask_checkpoint_for_test("__importMapFailureOrder.join('|')") @@ -182,7 +183,8 @@ fn parser_import_map_registration_obeys_inline_script_csp() { base_url: document_url.clone(), initiator_url: document_url, }, - ); + ) + .expect("parser import-map handoff should be admitted"); } let base_url = Url::parse("https://script-terminal.test/import-map.html").unwrap();