From c66bd58e982bbab3fbabaeaaacf0ea425fe077f1 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 21:11:12 +0800 Subject: [PATCH] fix(script): queue Window promise rejection notifications as DOM tasks Promise rejection events were dispatched during microtask checkpoints or synchronously when a late handler was attached. Queue both events on the shared DOM-manipulation source, snapshot each realm's notification batch at the checkpoint, and recheck handlers immediately before notification. Keep V8 payloads in the originating Host, authorize exact Document owners through the Page dispatcher, and restore the registered Window realm for each event. Retire stale payloads and clear retained handles at teardown. Exercise FIFO order across checkpoints, late handling, realm routing, and Document replacement through the production selected-task harness. Update existing notification fixtures and assert that suppressed rejections leave no notification task queued. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - 254 related WPT cases: all 240 previously passing cases remain passing - promise-rejection-events.html: 40/43 subtests pass, up from 33/43 - cargo nextest run --no-fail-fast: 17,849 passed, 13 skipped --- .../src/native_bridge/context_host/core.rs | 12 ++ .../src/native_bridge/context_host/mod.rs | 4 + .../context_host/promise_rejection_tasks.rs | 72 +++++++ moli-renderer-v8/src/page_task_queue.rs | 7 + .../src/page_task_queue/dom_manipulation.rs | 14 ++ .../src/page_task_queue/promise_rejection.rs | 79 +++++++ moli-renderer-v8/src/runtime/mod.rs | 1 + moli-renderer-v8/src/runtime/page_vm/mod.rs | 2 + .../runtime/page_vm/page_dom_manipulation.rs | 3 + .../page_dom_manipulation_task_completion.rs | 3 + .../page_dom_manipulation_test_support.rs | 4 + .../runtime/page_vm/page_promise_rejection.rs | 64 ++++++ .../src/runtime/page_vm/tests/mod.rs | 1 + .../page_vm/tests/promise_rejection.rs | 49 +++++ moli-renderer-v8/src/script_vm.rs | 2 + .../src/script_vm/promise_rejection_task.rs | 65 ++++++ .../src/script_vm/runtime_bindings.rs | 127 +++++++++--- .../src/script_vm/tests/browser_api/misc.rs | 13 +- .../tests/browser_api/promise_rejection.rs | 193 +++++++++++++++--- .../src/script_vm/tests/streams.rs | 12 +- .../src/script_vm/tests/webidl_fetch.rs | 12 +- .../tests/window_execution_context.rs | 43 +++- 22 files changed, 714 insertions(+), 68 deletions(-) create mode 100644 moli-renderer-v8/src/native_bridge/context_host/promise_rejection_tasks.rs create mode 100644 moli-renderer-v8/src/page_task_queue/promise_rejection.rs create mode 100644 moli-renderer-v8/src/runtime/page_vm/page_promise_rejection.rs create mode 100644 moli-renderer-v8/src/runtime/page_vm/tests/promise_rejection.rs create mode 100644 moli-renderer-v8/src/script_vm/promise_rejection_task.rs 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 715e7bdb28..23b893c576 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -303,6 +303,8 @@ impl JsContextHost { super::file_entry_file_callbacks::FileEntryFileCallbackState::default(), script_preparation_errors: super::script_preparation_errors::ScriptPreparationErrorState::default(), + promise_rejection_tasks: + super::promise_rejection_tasks::PromiseRejectionTaskState::default(), pending_selectedcontent_updates: HashSet::new(), user_interaction_tasks: super::user_interaction_tasks::UserInteractionTaskState::default(), @@ -752,6 +754,16 @@ impl JsContextHost { .script_preparation_error() } + pub(crate) fn page_promise_rejection_sender( + &self, + ) -> crate::page_task_queue::RendererPagePromiseRejectionSender { + self.page_task_capabilities + .get() + .expect("a live Page Window must install its task capabilities before promise rejection admission") + .dom_manipulation() + .promise_rejection() + } + pub(crate) fn page_file_reading_sender( &self, ) -> crate::page_task_queue::RendererPageFileReadingSender { diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index c8c5c0020c..a1171ce635 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -101,6 +101,7 @@ mod host_loads; mod image_decodes; mod image_loads; mod image_resources; +mod promise_rejection_tasks; mod script_preparation_errors; pub(crate) use image_resources::{ CssImageResourceAdmission, CssImageResourceRequestIdentity, ImageResponseDescriptor, @@ -919,6 +920,7 @@ pub(crate) struct JsContextHost { misc_platform_api_tasks: misc_platform_api_tasks::MiscPlatformApiTaskState, file_entry_file_callbacks: file_entry_file_callbacks::FileEntryFileCallbackState, script_preparation_errors: script_preparation_errors::ScriptPreparationErrorState, + promise_rejection_tasks: promise_rejection_tasks::PromiseRejectionTaskState, pending_selectedcontent_updates: HashSet, user_interaction_tasks: user_interaction_tasks::UserInteractionTaskState, pending_image_load_events: HashMap, @@ -1320,6 +1322,8 @@ impl JsContextHost { return; } self.page_context_resources_closed = true; + self.promise_rejection_tasks = + promise_rejection_tasks::PromiseRejectionTaskState::default(); self.retire_all_document_resource_loaders(); self.page_default_context = None; self.v8_finalizers.clear_for_context_teardown(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/promise_rejection_tasks.rs b/moli-renderer-v8/src/native_bridge/context_host/promise_rejection_tasks.rs new file mode 100644 index 0000000000..aa49e95243 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/promise_rejection_tasks.rs @@ -0,0 +1,72 @@ +use crate::{ + page_task_queue::{RendererPagePromiseRejectionTaskId, RendererPagePromiseRejectionTaskKind}, + script_vm::PromiseRejectionTaskPayload, +}; + +use super::window_document_tasks::{ExactWindowDocumentTaskLedger, PendingExactWindowDocumentTask}; +use super::{JsContextHost, WindowDocumentTaskTarget}; + +pub(super) type PromiseRejectionTaskState = ExactWindowDocumentTaskLedger< + RendererPagePromiseRejectionTaskId, + RendererPagePromiseRejectionTaskKind, + PromiseRejectionTaskPayload, +>; + +impl JsContextHost { + pub(crate) fn queue_promise_rejection_task( + &mut self, + target: WindowDocumentTaskTarget, + kind: RendererPagePromiseRejectionTaskKind, + payload: PromiseRejectionTaskPayload, + ) -> bool { + let task_id = self + .promise_rejection_tasks + .allocate_task_id(RendererPagePromiseRejectionTaskId::from_raw); + self.promise_rejection_tasks + .push(PendingExactWindowDocumentTask::new( + task_id, target, kind, payload, + )); + if self + .page_promise_rejection_sender() + .send(target, task_id, kind) + .is_ok() + { + return true; + } + self.promise_rejection_tasks + .remove_exact(task_id, target, kind); + false + } + + pub(crate) fn current_pending_promise_rejection_target( + &self, + task_id: RendererPagePromiseRejectionTaskId, + ) -> Option<( + WindowDocumentTaskTarget, + RendererPagePromiseRejectionTaskKind, + )> { + let pending = self.promise_rejection_tasks.pending(task_id)?; + let target = self.current_window_document_task_target_for_dispatch_scope( + pending.target().dispatch_scope(), + )?; + Some((target, pending.kind())) + } + + pub(crate) fn take_pending_promise_rejection_task( + &mut self, + task_id: RendererPagePromiseRejectionTaskId, + target: WindowDocumentTaskTarget, + kind: RendererPagePromiseRejectionTaskKind, + ) -> Option { + self.promise_rejection_tasks + .remove_exact(task_id, target, kind) + .map(PendingExactWindowDocumentTask::into_payload) + } + + pub(crate) fn discard_pending_promise_rejection_task( + &mut self, + task_id: RendererPagePromiseRejectionTaskId, + ) -> bool { + self.promise_rejection_tasks.remove(task_id).is_some() + } +} diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs index be2502a2c4..037b1e6543 100644 --- a/moli-renderer-v8/src/page_task_queue.rs +++ b/moli-renderer-v8/src/page_task_queue.rs @@ -38,6 +38,7 @@ mod popup_close; mod popup_load_event; mod post_domcontentloaded_runtime; mod post_parse_owner_work; +mod promise_rejection; mod ready_signal; mod rendering_update; mod resource_completions; @@ -386,6 +387,12 @@ pub(crate) use self::post_parse_owner_work::{ PostParseLifecycleQueueStats, PostParseLifecycleWork, PostParsePageOwnedWork, post_parse_lifecycle_queue_stats, }; +pub(crate) use self::promise_rejection::{ + PagePromiseRejectionTargetEffect, PagePromiseRejectionTurnAction, + PagePromiseRejectionTurnOutcome, RendererPagePromiseRejectionOwner, + RendererPagePromiseRejectionSender, RendererPagePromiseRejectionTask, + RendererPagePromiseRejectionTaskId, RendererPagePromiseRejectionTaskKind, +}; use self::ready_signal::RendererPageTaskReadySignal; #[cfg(test)] pub(crate) use self::rendering_update::RendererPageRenderingUpdateHead; 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 08a4436647..addb597836 100644 --- a/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs +++ b/moli-renderer-v8/src/page_task_queue/dom_manipulation.rs @@ -34,6 +34,10 @@ use super::{ RendererPagePopupLoadEventOwner, RendererPagePopupLoadEventSender, RendererPagePopupLoadEventTask, }, + promise_rejection::{ + RendererPagePromiseRejectionOwner, RendererPagePromiseRejectionSender, + RendererPagePromiseRejectionTask, + }, script_preparation_error::{ RendererPageScriptPreparationErrorOwner, RendererPageScriptPreparationErrorSender, RendererPageScriptPreparationErrorTask, @@ -72,6 +76,7 @@ pub(crate) enum RendererPageDomManipulationOwner { ElementToggle(RendererPageElementToggleEventOwner), FileEntryFileCallback(RendererPageFileEntryFileCallbackOwner), ScriptPreparationError(RendererPageScriptPreparationErrorOwner), + PromiseRejection(RendererPagePromiseRejectionOwner), ImageLoadEvent(RendererPageImageLoadEventOwner), PopupLoadEvent(RendererPagePopupLoadEventOwner), PopupClose(RendererPagePopupCloseOwner), @@ -90,6 +95,7 @@ pub(crate) enum RendererPageDomManipulationTask { ElementToggle(RendererPageElementToggleEventTask), FileEntryFileCallback(RendererPageFileEntryFileCallbackTask), ScriptPreparationError(RendererPageScriptPreparationErrorTask), + PromiseRejection(RendererPagePromiseRejectionTask), ImageLoadEvent(RendererPageImageLoadEventTask), PopupLoadEvent(RendererPagePopupLoadEventTask), PopupClose(RendererPagePopupCloseTask), @@ -118,6 +124,9 @@ impl RendererPageDomManipulationTask { Self::ScriptPreparationError(task) => { RendererPageDomManipulationOwner::ScriptPreparationError(task.owner()) } + Self::PromiseRejection(task) => { + RendererPageDomManipulationOwner::PromiseRejection(task.owner()) + } Self::ImageLoadEvent(task) => { RendererPageDomManipulationOwner::ImageLoadEvent(task.owner()) } @@ -158,6 +167,7 @@ pub(crate) enum PageDomManipulationTurnAction { ElementToggle(super::PageElementToggleEventTurnAction), FileEntryFileCallback(super::PageFileEntryFileCallbackTurnAction), ScriptPreparationError(super::PageScriptPreparationErrorTurnAction), + PromiseRejection(super::PagePromiseRejectionTurnAction), ImageLoadEvent(super::PageImageLoadEventTurnAction), PopupLoadEvent(super::PagePopupLoadEventTurnAction), PopupClose(super::PagePopupCloseTurnAction), @@ -216,6 +226,10 @@ impl RendererPageDomManipulationSender { RendererPageScriptPreparationErrorSender::new(self.route.clone(), self.root_document) } + pub(crate) fn promise_rejection(&self) -> RendererPagePromiseRejectionSender { + RendererPagePromiseRejectionSender::new(self.route.clone(), self.root_document) + } + pub(crate) fn image_load_event(&self) -> RendererPageImageLoadEventSender { RendererPageImageLoadEventSender::new(self.route.clone(), self.root_document) } diff --git a/moli-renderer-v8/src/page_task_queue/promise_rejection.rs b/moli-renderer-v8/src/page_task_queue/promise_rejection.rs new file mode 100644 index 0000000000..24d589d49a --- /dev/null +++ b/moli-renderer-v8/src/page_task_queue/promise_rejection.rs @@ -0,0 +1,79 @@ +use crate::{ + native_bridge::WindowDocumentTaskTarget, + runtime::{PageOwnerTurnOutcome, RendererDocumentToken}, +}; + +use super::{ + PageWindowDocumentTaskTargetEffect, PageWindowDocumentTaskTurnAction, + RendererPageWindowDocumentTask, RendererPageWindowDocumentTaskOwner, + dom_manipulation::{RendererPageDomManipulationRoute, RendererPageDomManipulationTask}, +}; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) struct RendererPagePromiseRejectionTaskId(u64); + +impl RendererPagePromiseRejectionTaskId { + pub(crate) const fn from_raw(raw: u64) -> Self { + Self(raw) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RendererPagePromiseRejectionTaskKind { + Unhandled, + Handled, +} + +pub(crate) type RendererPagePromiseRejectionOwner = RendererPageWindowDocumentTaskOwner; +pub(crate) type RendererPagePromiseRejectionTask = RendererPageWindowDocumentTask< + RendererPagePromiseRejectionTaskId, + RendererPagePromiseRejectionTaskKind, +>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct RendererPagePromiseRejectionRouteClosed; + +/// Both promise rejection events use the HTML DOM-manipulation FIFO. The +/// checkpoint only queues a notification; it must never run its listeners. +#[derive(Clone, Debug)] +pub(crate) struct RendererPagePromiseRejectionSender { + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, +} + +impl RendererPagePromiseRejectionSender { + pub(super) fn new( + route: RendererPageDomManipulationRoute, + root_document: RendererDocumentToken, + ) -> Self { + Self { + route, + root_document, + } + } + + pub(crate) fn send( + &self, + target: WindowDocumentTaskTarget, + task_id: RendererPagePromiseRejectionTaskId, + kind: RendererPagePromiseRejectionTaskKind, + ) -> Result<(), RendererPagePromiseRejectionRouteClosed> { + self.route + .send(RendererPageDomManipulationTask::PromiseRejection( + RendererPagePromiseRejectionTask::new( + RendererPagePromiseRejectionOwner::new(self.root_document, target), + task_id, + kind, + ), + )) + .map_err(|_| RendererPagePromiseRejectionRouteClosed) + } +} + +pub(crate) type PagePromiseRejectionTargetEffect = PageWindowDocumentTaskTargetEffect; +pub(crate) type PagePromiseRejectionTurnAction = PageWindowDocumentTaskTurnAction< + RendererPagePromiseRejectionTaskId, + RendererPagePromiseRejectionTaskKind, +>; +pub(crate) type PagePromiseRejectionTurnOutcome = + PageOwnerTurnOutcome; diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index b57004d678..0b5c328091 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -113,6 +113,7 @@ pub(crate) use self::page_vm::AuthorizedCurrentPageNavigationApiTask; pub(crate) use self::page_vm::AuthorizedCurrentPageOpfsTask; pub(crate) use self::page_vm::AuthorizedCurrentPagePopupClose; pub(crate) use self::page_vm::AuthorizedCurrentPagePopupLoadEvent; +pub(crate) use self::page_vm::AuthorizedCurrentPagePromiseRejection; pub(crate) use self::page_vm::AuthorizedCurrentPageRenderingUpdate; pub(crate) use self::page_vm::AuthorizedCurrentPageScriptPreparationError; pub(crate) use self::page_vm::AuthorizedCurrentPageServiceWorkerClientMessage; diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 78cabb127d..cf62f684ce 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -133,6 +133,7 @@ mod page_parser_async_module_admission; mod page_parser_owned_module_continuation; mod page_popup_close; mod page_popup_load_event; +mod page_promise_rejection; mod page_rendering_update; #[cfg(test)] mod page_rendering_update_body_test_support; @@ -225,6 +226,7 @@ pub(crate) use page_navigation_api_task::AuthorizedCurrentPageNavigationApiTask; pub(crate) use page_opfs_task::AuthorizedCurrentPageOpfsTask; pub(crate) use page_popup_close::AuthorizedCurrentPagePopupClose; pub(crate) use page_popup_load_event::AuthorizedCurrentPagePopupLoadEvent; +pub(crate) use page_promise_rejection::AuthorizedCurrentPagePromiseRejection; pub(crate) use page_rendering_update::AuthorizedCurrentPageRenderingUpdate; pub(crate) use page_resource_completion::{ AuthorizedCurrentChildDocumentLoadCompletion, AuthorizedCurrentChildModuleFetchCompletion, 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 2b6c93544b..15055e1bff 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 @@ -32,6 +32,9 @@ impl PageVm { .map(|outcome| { outcome.map_action(PageDomManipulationTurnAction::ScriptPreparationError) }), + RendererPageDomManipulationTask::PromiseRejection(task) => self + .apply_selected_page_promise_rejection_turn(task) + .map(|outcome| outcome.map_action(PageDomManipulationTurnAction::PromiseRejection)), RendererPageDomManipulationTask::ImageLoadEvent(task) => self .apply_selected_page_image_load_event_turn(task) .map(|outcome| outcome.map_action(PageDomManipulationTurnAction::ImageLoadEvent)), 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 b4552f274f..00f33de262 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 @@ -34,6 +34,9 @@ impl PageVm { PageDomManipulationTurnAction::ScriptPreparationError(action) => { action.into_page_task_completion() } + PageDomManipulationTurnAction::PromiseRejection(action) => { + action.into_page_task_completion() + } PageDomManipulationTurnAction::ImageLoadEvent(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 bbf2d4ccb4..b1871de9d9 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 @@ -20,6 +20,7 @@ pub(crate) enum PageDomManipulationTestFamily { ElementToggle, FileEntryFileCallback, ScriptPreparationError, + PromiseRejection, ImageLoadEvent, PopupLoadEvent, ConnectedStyleEvent, @@ -50,6 +51,9 @@ impl PageDomManipulationTestFamily { ) | ( Self::ScriptPreparationError, RendererPageDomManipulationOwner::ScriptPreparationError(_) + ) | ( + Self::PromiseRejection, + RendererPageDomManipulationOwner::PromiseRejection(_) ) | ( Self::ImageLoadEvent, RendererPageDomManipulationOwner::ImageLoadEvent(_) diff --git a/moli-renderer-v8/src/runtime/page_vm/page_promise_rejection.rs b/moli-renderer-v8/src/runtime/page_vm/page_promise_rejection.rs new file mode 100644 index 0000000000..802b9cab10 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/page_promise_rejection.rs @@ -0,0 +1,64 @@ +use crate::page_task_queue::{ + PagePromiseRejectionTargetEffect, PagePromiseRejectionTurnAction, + PagePromiseRejectionTurnOutcome, RendererPagePromiseRejectionTask, +}; + +use super::{ + AuthorizedCurrentWindowDocumentTask, IntoPageTaskCompletion, PageTaskCompletion, PageVm, +}; + +impl IntoPageTaskCompletion for PagePromiseRejectionTurnAction { + fn into_page_task_completion(self) -> PageTaskCompletion { + match self.target_effect { + PagePromiseRejectionTargetEffect::DispatchedToCurrentOwner => { + PageTaskCompletion::CallbackCompletion + } + PagePromiseRejectionTargetEffect::CurrentOwnerHadNoEventTarget => { + PageTaskCompletion::CheckpointOnly + } + PagePromiseRejectionTargetEffect::DiscardedStaleOwner { .. } => { + PageTaskCompletion::NoCompletion + } + } + } +} + +pub(crate) type AuthorizedCurrentPagePromiseRejection = + AuthorizedCurrentWindowDocumentTask; + +impl PageVm { + pub(in crate::runtime) fn apply_selected_page_promise_rejection_turn( + &mut self, + task: RendererPagePromiseRejectionTask, + ) -> anyhow::Result { + let owner = task.owner(); + let task_id = task.task_id(); + let kind = task.kind(); + let current = self.vm().current_pending_promise_rejection_owner( + task_id, + self.document_lifecycle.identity().document, + ); + let target_effect = + match self.authorize_current_window_document_task(task, owner, kind, current) { + Ok(authorization) => self + .vm_mut() + .apply_current_promise_rejection_body(authorization)?, + Err(stale) => { + if stale.may_discard_local_payload() { + self.vm_mut().discard_stale_promise_rejection_task(task_id); + } + PagePromiseRejectionTargetEffect::DiscardedStaleOwner { + current_owner: stale.current_owner(), + } + } + }; + Ok(PagePromiseRejectionTurnOutcome::new( + PagePromiseRejectionTurnAction { + owner, + task_id, + kind, + target_effect, + }, + )) + } +} 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 db897b1e70..3d12a1e3d8 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -159,6 +159,7 @@ mod opfs; mod parser_written_script_residence; mod popup_document_completion; mod preferred_aspect_ratio; +mod promise_rejection; mod rendering_update; mod script_preparation_error; mod service_worker; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/promise_rejection.rs b/moli-renderer-v8/src/runtime/page_vm/tests/promise_rejection.rs new file mode 100644 index 0000000000..2e9420d4f9 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/promise_rejection.rs @@ -0,0 +1,49 @@ +use super::*; +use crate::page_task_queue::RendererPageDomManipulationTask; + +#[tokio::test(flavor = "current_thread")] +async fn promise_rejection_tasks_retire_with_their_exact_document() { + run_page_vm_async_test(async move { + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?; + for handled in [false, true] { + let (mut page_vm, _, _) = page_vm_with_bound_task_sources_and_owner_wake( + &loader, Url::parse("https://example.com/stale-rejection")?, + ); + page_vm.vm_mut().eval(r#" + globalThis.events = []; + onunhandledrejection = event => { events.push(event.type); event.preventDefault(); }; + onrejectionhandled = event => events.push(event.type); + globalThis.rejected = Promise.reject('retired document'); + "#)?; + if handled { + assert!(page_vm.run_exact_selected_page_task_for_test( + PageSelectedTaskTestSelector::DomManipulation(PageDomManipulationTestFamily::PromiseRejection), + &loader, + ).await?); + page_vm.vm_mut().eval("rejected.catch(() => {});")?; + } + let task = page_vm.take_dom_manipulation_body_task_for_test( + PageDomManipulationTestFamily::PromiseRejection, + ).expect("queued rejection event"); + let RendererPageDomManipulationTask::PromiseRejection(rejection) = &task else { + unreachable!("rejection task selector") + }; + let task_id = rejection.task_id(); + let previous = page_vm.vm().current_main_document_task_owner().unwrap(); + page_vm.vm_mut().eval(r#" + document.open(); document.write('replacement'); document.close(); + "#)?; + let current = page_vm.vm().current_main_document_task_owner().unwrap(); + assert_ne!(previous, current); + page_vm.vm_mut().eval_without_microtask_checkpoint_for_test( + "globalThis.checkpoints = 0; Promise.resolve().then(() => checkpoints++);", + )?; + page_vm.run_claimed_dom_manipulation_task_through_selected_dispatcher_for_test(task, &loader).await?; + assert_eq!(page_vm.vm_mut().eval_without_microtask_checkpoint_for_test("String(checkpoints)")?, "0", + "a retired notification must not checkpoint the replacement document"); + assert_eq!(page_vm.vm_mut().eval("String(events.length)")?, if handled { "1" } else { "0" }); + assert!(!page_vm.vm_mut().discard_stale_promise_rejection_task(task_id)); + } + Ok::<_, anyhow::Error>(()) + }).await.expect("promise rejection exact-document retirement"); +} diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index f5afac34cf..05838a41b8 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -718,6 +718,7 @@ mod indexed_db_task_body; mod input_dispatch; mod input_helpers; mod inspector; +mod promise_rejection_task; mod script_preparation_error; pub(crate) use inspector::{dispatch_inspector_io_owner_wake, dispatch_inspector_main_owner_wake}; mod isolated_worlds; @@ -869,6 +870,7 @@ pub(crate) use inspector::{ }; use isolated_worlds::*; pub(crate) use runtime_bindings::PromiseRejectDispatchSlot; +pub(crate) use runtime_bindings::PromiseRejectionTaskPayload; pub(crate) use runtime_bindings::perform_microtask_checkpoint_and_report_pending_promise_rejections; use runtime_bindings::*; pub(crate) use runtime_work::*; diff --git a/moli-renderer-v8/src/script_vm/promise_rejection_task.rs b/moli-renderer-v8/src/script_vm/promise_rejection_task.rs new file mode 100644 index 0000000000..b8011a6ba6 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/promise_rejection_task.rs @@ -0,0 +1,65 @@ +use anyhow::{Result, anyhow}; + +use super::{ScriptVm, runtime_bindings::dispatch_promise_rejection_task}; +use crate::{ + page_task_queue::{ + PagePromiseRejectionTargetEffect, RendererPagePromiseRejectionOwner, + RendererPagePromiseRejectionTaskId, RendererPagePromiseRejectionTaskKind, + }, + runtime::AuthorizedCurrentPagePromiseRejection, +}; + +impl ScriptVm { + pub(crate) fn current_pending_promise_rejection_owner( + &self, + task_id: RendererPagePromiseRejectionTaskId, + root_document: crate::runtime::RendererDocumentToken, + ) -> Option<( + RendererPagePromiseRejectionOwner, + RendererPagePromiseRejectionTaskKind, + )> { + let (target, kind) = self + ._context_host + .borrow() + .current_pending_promise_rejection_target(task_id)?; + Some(( + RendererPagePromiseRejectionOwner::new(root_document, target), + kind, + )) + } + + pub(crate) fn apply_current_promise_rejection_body( + &mut self, + authorization: AuthorizedCurrentPagePromiseRejection, + ) -> Result { + let task = authorization.into_task(); + let payload = self + ._context_host + .borrow_mut() + .take_pending_promise_rejection_task(task.task_id(), task.owner().target(), task.kind()) + .ok_or_else(|| anyhow!("authorized promise rejection lost its exact payload"))?; + self.with_default_context_scope(|scope, _| { + Ok( + if dispatch_promise_rejection_task( + scope, + task.owner().target(), + task.kind(), + payload, + ) { + PagePromiseRejectionTargetEffect::DispatchedToCurrentOwner + } else { + PagePromiseRejectionTargetEffect::CurrentOwnerHadNoEventTarget + }, + ) + }) + } + + pub(crate) fn discard_stale_promise_rejection_task( + &mut self, + task_id: RendererPagePromiseRejectionTaskId, + ) -> bool { + self._context_host + .borrow_mut() + .discard_pending_promise_rejection_task(task_id) + } +} diff --git a/moli-renderer-v8/src/script_vm/runtime_bindings.rs b/moli-renderer-v8/src/script_vm/runtime_bindings.rs index a21a982af1..6d4f4fe4a7 100644 --- a/moli-renderer-v8/src/script_vm/runtime_bindings.rs +++ b/moli-renderer-v8/src/script_vm/runtime_bindings.rs @@ -1,7 +1,8 @@ use crate::{ context_bootstrap::dispatch_window_promise_rejection_event, exception_reporting::log_unhandled_promise_rejection, - native_bridge::{JsContextHost, WindowExecutionContextBinding}, + native_bridge::{JsContextHost, WindowDocumentTaskTarget, WindowExecutionContextBinding}, + page_task_queue::RendererPagePromiseRejectionTaskKind, util::context_host_ptr_from_global_bridge, }; use std::{ @@ -23,6 +24,12 @@ struct PendingPromiseRejection { realm: WindowExecutionContextBinding, } +/// V8 handles stay on the creating Host. The Page queue carries only the +/// exact Document owner and an id for this detached notification batch. +pub(crate) struct PromiseRejectionTaskPayload { + rejections: Vec, +} + #[derive(Clone)] pub(crate) struct PromiseRejectDispatchSlot { pub(super) host_weak: Weak>, @@ -100,19 +107,83 @@ fn remember_reported_promise_rejection( reported.push(rejection); } -pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) -> usize { +fn queue_promise_rejection_notification( + state: &PromiseRejectDispatchState, + kind: RendererPagePromiseRejectionTaskKind, + rejections: Vec, +) { + let Some(first) = rejections.first() else { + return; + }; + let host_ptr: *mut JsContextHost = (*state.host).as_ptr(); + let host = unsafe { &mut *host_ptr }; + if !first.realm.is_current(host) { + return; + } + let Some(target) = + host.current_window_document_task_target_for_dispatch_scope(first.realm.dispatch_scope()) + else { + return; + }; + if !host.queue_promise_rejection_task(target, kind, PromiseRejectionTaskPayload { rejections }) + { + tracing::debug!("promise rejection task route closed"); + } +} + +fn queue_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) -> usize { let Some(state) = promise_reject_dispatch_state(scope) else { return 0; }; let pending = std::mem::take(&mut *state.pending.borrow_mut()); let pending_len = pending.len(); + // HTML snapshots and empties each global's about-to-be-notified list at + // the checkpoint. Later rejections belong to a later task, even if this + // batch has not yet been selected by the event loop. + let mut batches = indexmap::IndexMap::<_, Vec<_>>::new(); + for rejection in pending { + batches + .entry(( + rejection.realm.owner(), + rejection.realm.dispatch_scope(), + rejection.realm.realm_token(), + )) + .or_default() + .push(rejection); + } + for (_, rejections) in batches { + queue_promise_rejection_notification( + &state, + RendererPagePromiseRejectionTaskKind::Unhandled, + rejections, + ); + } + pending_len +} + +pub(super) fn dispatch_promise_rejection_task( + scope: &mut v8::PinScope<'_, '_>, + target: WindowDocumentTaskTarget, + kind: RendererPagePromiseRejectionTaskKind, + payload: PromiseRejectionTaskPayload, +) -> bool { + let Some(state) = promise_reject_dispatch_state(scope) else { + return false; + }; let host_ptr: *mut JsContextHost = (*state.host).as_ptr(); state .reported .borrow_mut() .retain(|rejection| rejection.realm.is_current(unsafe { &*host_ptr })); - for rejection in pending { + let mut dispatched = false; + for rejection in payload.rejections { + if unsafe { &*host_ptr } + .current_window_document_task_target_for_dispatch_scope(target.dispatch_scope()) + != Some(target) + { + break; + } let _ = rejection .realm .with_current_scope(scope, host_ptr, |scope, dispatch_scope| { @@ -120,7 +191,8 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) // HTML rechecks PromiseIsHandled immediately before notifying. // The host, or an earlier notification in this detached batch, // may have handled it since it was first queued. - if promise.has_handler() { + if kind == RendererPagePromiseRejectionTaskKind::Unhandled && promise.has_handler() + { return; } let reason = rejection @@ -131,7 +203,10 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) scope, host_ptr, dispatch_scope, - "unhandledrejection", + match kind { + RendererPagePromiseRejectionTaskKind::Unhandled => "unhandledrejection", + RendererPagePromiseRejectionTaskKind::Handled => "rejectionhandled", + }, promise, reason, ); @@ -139,15 +214,19 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) // synchronously turn this notification into `rejectionhandled`. // Only promises that remain unhandled after dispatch belong in // the outstanding reported-rejection set. - if !promise.has_handler() { + dispatched |= outcome.is_ok(); + if kind == RendererPagePromiseRejectionTaskKind::Unhandled && !promise.has_handler() + { remember_reported_promise_rejection(&state.reported, rejection.clone()); } - if matches!(outcome, Ok(true)) { + if kind == RendererPagePromiseRejectionTaskKind::Unhandled + && matches!(outcome, Ok(true)) + { log_unhandled_promise_rejection(scope, reason); } }); } - pending_len + dispatched } pub(crate) fn perform_microtask_checkpoint_and_report_pending_promise_rejections( @@ -223,13 +302,13 @@ pub(crate) fn perform_microtask_checkpoint_and_report_pending_promise_rejections elapsed_us = %started.elapsed().as_micros(), ); } - let flush_started = trace_enabled.then(Instant::now); - let pending_rejections = flush_pending_promise_rejections(scope); + let queue_started = trace_enabled.then(Instant::now); + let pending_rejections = queue_pending_promise_rejections(scope); crate::context_bootstrap::run_end_of_microtask_checkpoint_tasks(scope); - if let Some(started) = flush_started { + if let Some(started) = queue_started { tracing::info!( target: "moli_cdp_runtime", - stage = "microtask_checkpoint_rejection_flush_done", + stage = "microtask_checkpoint_rejection_queue_done", pending_rejections, elapsed_us = %started.elapsed().as_micros(), ); @@ -310,7 +389,7 @@ pub(super) unsafe extern "C" fn promise_reject_callback(message: v8::PromiseReje .iter() .position(|rejection| pending_promise_rejection_matches(scope, rejection, promise)) { - pending.swap_remove(index); + pending.remove(index); return; } drop(pending); @@ -327,23 +406,11 @@ pub(super) unsafe extern "C" fn promise_reject_callback(message: v8::PromiseReje let Some(rejection) = reported else { return; }; - let _ = rejection - .realm - .with_current_scope(scope, host_ptr, |scope, dispatch_scope| { - let promise = v8::Local::new(scope, &rejection.promise); - let reason = rejection - .reason - .as_ref() - .map(|reason| v8::Local::new(scope, reason)); - let _ = dispatch_window_promise_rejection_event( - scope, - host_ptr, - dispatch_scope, - "rejectionhandled", - promise, - reason, - ); - }); + queue_promise_rejection_notification( + &state, + RendererPagePromiseRejectionTaskKind::Handled, + vec![rejection], + ); } _ => {} } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 8d9e30fc89..e8432b7f29 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -1901,9 +1901,10 @@ fn font_face_declared_slots_ignore_prototype_spoofing() { assert_eq!(vm.eval("fakeLoadedResult").unwrap(), "rejected:TypeError"); } -#[test] -fn invalid_font_face_defers_loaded_rejection_until_promise_is_observed() { - let mut vm = new_storage_test_vm("https://font-face-lazy-loaded-promise.test/"); +#[tokio::test(flavor = "current_thread")] +async fn invalid_font_face_defers_loaded_rejection_until_promise_is_observed() { + let mut vm = + new_storage_page_task_executor_test_vm("https://font-face-lazy-loaded-promise.test/"); let initial = vm .eval( @@ -1938,6 +1939,12 @@ fn invalid_font_face_defers_loaded_rejection_until_promise_is_observed() { "[]", ); + assert!( + !vm.has_ready_dom_manipulation_family_for_test( + PageDomManipulationTestFamily::PromiseRejection, + ), + "no rejection notification may remain queued" + ); let observed = vm .eval( r#" diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs index 20a9179c7c..55e18517ec 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs @@ -1,8 +1,26 @@ use super::*; -#[test] -fn rejection_notification_rechecks_handlers_added_by_an_earlier_notification() { - let mut vm = new_storage_test_vm("https://promise-notification-batch.test/"); +async fn run_rejection_task( + vm: &mut crate::runtime::PageVmTaskExecutorTestHarness, + loader: &ResourceRequestClient, +) { + assert!( + vm.run_one_dom_manipulation_task_executor_turn( + PageDomManipulationTestFamily::PromiseRejection, + loader, + ) + .await + .expect("promise rejection task") + ); +} + +#[tokio::test(flavor = "current_thread")] +async fn rejection_notification_rechecks_handlers_added_by_an_earlier_notification() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://promise-notification-batch.test/", + &loader, + ); vm.eval( r#" globalThis.__notifications = []; @@ -18,15 +36,18 @@ fn rejection_notification_rechecks_handlers_added_by_an_earlier_notification() { ) .unwrap(); vm.eval("0").unwrap(); + run_rejection_task(&mut vm, &loader).await; assert_eq!( vm.eval("JSON.stringify(__notifications)").unwrap(), r#"["first"]"# ); } -#[test] -fn main_window_unhandled_rejection_dispatches_to_main_window() { - let mut vm = new_storage_test_vm("https://main-promise-rejection.test/"); +#[tokio::test(flavor = "current_thread")] +async fn main_window_unhandled_rejection_dispatches_to_main_window() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = + new_page_task_executor_test_vm_with_loader("https://main-promise-rejection.test/", &loader); vm.eval( r#" @@ -41,6 +62,12 @@ Promise.reject("main-owned"); .expect("main Window rejection setup should evaluate"); vm.eval("0") .expect("main Window rejection checkpoint should evaluate"); + assert_eq!( + vm.eval("JSON.stringify(__mainPromiseRejections)").unwrap(), + "[]", + "microtask checkpoints must leave the notification in the DOM task queue" + ); + run_rejection_task(&mut vm, &loader).await; assert_eq!( vm.eval("JSON.stringify(__mainPromiseRejections)") @@ -49,9 +76,13 @@ Promise.reject("main-owned"); ); } -#[test] -fn handler_added_during_unhandled_rejection_does_not_dispatch_rejectionhandled() { - let mut vm = new_storage_test_vm("https://handled-during-notification.test/"); +#[tokio::test(flavor = "current_thread")] +async fn handler_added_during_unhandled_rejection_does_not_dispatch_rejectionhandled() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://handled-during-notification.test/", + &loader, + ); vm.eval( r#" @@ -73,6 +104,7 @@ onrejectionhandled = event => { .expect("rejection handled during notification setup should evaluate"); vm.eval("0") .expect("unhandled rejection notification checkpoint should evaluate"); + run_rejection_task(&mut vm, &loader).await; vm.eval("0") .expect("rejection handler reaction checkpoint should evaluate"); @@ -83,9 +115,13 @@ onrejectionhandled = event => { ); } -#[test] -fn universal_isolated_world_rejection_uses_its_registry_backed_realm() { - let mut vm = new_storage_test_vm("https://isolated-promise-rejection.test/"); +#[tokio::test(flavor = "current_thread")] +async fn universal_isolated_world_rejection_uses_its_registry_backed_realm() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://isolated-promise-rejection.test/", + &loader, + ); let context_id = vm .create_isolated_world("promise-rejection-universal", true) .expect("universal isolated world should be created"); @@ -105,6 +141,7 @@ Promise.reject("isolated-owned"); .expect("isolated rejection setup should evaluate"); vm.eval_in_isolated_context(context_id, "0") .expect("isolated rejection checkpoint should evaluate"); + run_rejection_task(&mut vm, &loader).await; assert_eq!( vm.eval_in_isolated_context(context_id, "JSON.stringify(__isolatedPromiseRejections)",) @@ -114,9 +151,13 @@ Promise.reject("isolated-owned"); ); } -#[test] -fn live_child_unhandled_rejection_dispatches_only_to_child_window() { - let mut vm = new_storage_test_vm("https://child-promise-rejection.test/"); +#[tokio::test(flavor = "current_thread")] +async fn live_child_unhandled_rejection_dispatches_only_to_child_window() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://child-promise-rejection.test/", + &loader, + ); vm.eval( r#" @@ -145,6 +186,7 @@ __promiseRejectionChild.eval(` .expect("live child rejection setup should evaluate"); vm.eval("0") .expect("live child rejection checkpoint should evaluate"); + run_rejection_task(&mut vm, &loader).await; assert_eq!( vm.eval( @@ -158,9 +200,13 @@ __promiseRejectionChild.eval(` ); } -#[test] -fn live_child_rejectionhandled_dispatches_only_to_child_window() { - let mut vm = new_storage_test_vm("https://child-rejection-handled.test/"); +#[tokio::test(flavor = "current_thread")] +async fn live_child_rejectionhandled_dispatches_only_to_child_window() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://child-rejection-handled.test/", + &loader, + ); vm.eval( r#" @@ -193,8 +239,16 @@ __promiseHandledChild.eval(` .expect("live child late-handler setup should evaluate"); vm.eval("0") .expect("live child unhandled rejection checkpoint should evaluate"); + run_rejection_task(&mut vm, &loader).await; vm.eval(r#"__promiseHandledChild.__lateHandledPromise.catch(() => {})"#) .expect("parent realm should be able to attach the live child rejection handler"); + assert_eq!( + vm.eval("JSON.stringify(__promiseHandledChild.__childPromiseEvents)") + .unwrap(), + r#"["unhandledrejection"]"#, + "attaching the handler must only queue rejectionhandled" + ); + run_rejection_task(&mut vm, &loader).await; assert_eq!( vm.eval( @@ -208,9 +262,13 @@ __promiseHandledChild.eval(` ); } -#[test] -fn detached_child_late_handler_does_not_dispatch_rejectionhandled_to_parent_window() { - let mut vm = new_storage_test_vm("https://detached-rejection-handled.test/"); +#[tokio::test(flavor = "current_thread")] +async fn detached_child_late_handler_does_not_dispatch_rejectionhandled_to_parent_window() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://detached-rejection-handled.test/", + &loader, + ); vm.eval( r#" @@ -243,6 +301,7 @@ __lateHandlerChild.eval(` .expect("detached child late-handler setup should evaluate"); vm.eval("0") .expect("child unhandled rejection checkpoint should evaluate"); + run_rejection_task(&mut vm, &loader).await; vm.eval("__lateHandlerFrame.remove()") .expect("child frame removal should evaluate"); vm.eval("__lateHandlerChild.__lateHandledPromise.catch(() => {})") @@ -260,9 +319,9 @@ __lateHandlerChild.eval(` ); } -#[test] -fn detached_child_dynamic_import_rejection_is_not_reported_to_parent_window() { - let mut vm = new_storage_test_vm("https://inactive-import-rejection.test/"); +#[tokio::test(flavor = "current_thread")] +async fn detached_child_dynamic_import_rejection_is_not_reported_to_parent_window() { + let mut vm = new_storage_page_task_executor_test_vm("https://inactive-import-rejection.test/"); let promise_shape = vm .eval( @@ -306,6 +365,12 @@ fn detached_child_dynamic_import_rejection_is_not_reported_to_parent_window() { .expect("detached child rejection checkpoint should evaluate"); } + assert!( + !vm.has_ready_dom_manipulation_family_for_test( + PageDomManipulationTestFamily::PromiseRejection, + ), + "no rejection notification may remain queued" + ); assert_eq!( vm.eval( r#"JSON.stringify({ @@ -317,3 +382,83 @@ fn detached_child_dynamic_import_rejection_is_not_reported_to_parent_window() { r#"{"parent":[],"child":[]}"# ); } + +#[tokio::test(flavor = "current_thread")] +async fn promise_rejection_batches_preserve_dom_fifo_across_checkpoints() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = new_page_task_executor_test_vm_with_loader("https://promise-fifo.test/", &loader); + vm.eval( + r#" + globalThis.events = []; + globalThis.receiver = new BroadcastChannel('promise-fifo'); + globalThis.sender = new BroadcastChannel('promise-fifo'); + receiver.onmessage = event => events.push('message:' + event.data); + onunhandledrejection = event => { + events.push('rejection:' + event.reason); + event.preventDefault(); + queueMicrotask(() => events.push('microtask:' + event.reason)); + }; + sender.postMessage('first'); + Promise.reject('first'); + "#, + ) + .unwrap(); + vm.eval("sender.postMessage('second'); Promise.reject('second');") + .unwrap(); + assert_eq!(vm.eval("JSON.stringify(events)").unwrap(), "[]"); + for family in [ + PageDomManipulationTestFamily::BroadcastChannel, + PageDomManipulationTestFamily::PromiseRejection, + PageDomManipulationTestFamily::BroadcastChannel, + PageDomManipulationTestFamily::PromiseRejection, + ] { + assert!( + vm.run_one_dom_manipulation_task_executor_turn(family, &loader) + .await + .unwrap() + ); + } + assert_eq!( + vm.eval("JSON.stringify(events)").unwrap(), + r#"["message:first","rejection:first","microtask:first","message:second","rejection:second","microtask:second"]"# + ); +} + +#[tokio::test(flavor = "current_thread")] +async fn handler_added_by_an_earlier_dom_task_suppresses_rejection_notification() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = + new_page_task_executor_test_vm_with_loader("https://promise-late-task.test/", &loader); + vm.eval( + r#" + globalThis.events = []; + globalThis.receiver = new BroadcastChannel('promise-handler-task'); + globalThis.sender = new BroadcastChannel('promise-handler-task'); + receiver.onmessage = () => { + events.push('task'); + rejected.catch(() => events.push('handler')); + }; + onunhandledrejection = event => { events.push(event.type); event.preventDefault(); }; + onrejectionhandled = event => events.push(event.type); + globalThis.rejected = Promise.reject('handled before notification'); + sender.postMessage('attach'); + "#, + ) + .unwrap(); + assert!( + vm.run_one_dom_manipulation_task_executor_turn( + PageDomManipulationTestFamily::BroadcastChannel, + &loader, + ) + .await + .unwrap() + ); + run_rejection_task(&mut vm, &loader).await; + assert_eq!( + vm.eval("JSON.stringify(events)").unwrap(), + r#"["task","handler"]"# + ); + assert!(!vm.has_ready_dom_manipulation_family_for_test( + PageDomManipulationTestFamily::PromiseRejection + )); +} diff --git a/moli-renderer-v8/src/script_vm/tests/streams.rs b/moli-renderer-v8/src/script_vm/tests/streams.rs index 3f71fa907e..6cbb038cce 100644 --- a/moli-renderer-v8/src/script_vm/tests/streams.rs +++ b/moli-renderer-v8/src/script_vm/tests/streams.rs @@ -4224,9 +4224,9 @@ fn readable_stream_pipe_to_aborts_on_signal_and_honors_prevent_flags() { ); } -#[test] -fn readable_stream_pipe_abort_actions_own_all_rejections() { - let mut vm = stream_test_vm(); +#[tokio::test(flavor = "current_thread")] +async fn readable_stream_pipe_abort_actions_own_all_rejections() { + let mut vm = new_storage_page_task_executor_test_vm("https://stream-runtime.test/"); vm.eval( r#" @@ -4288,6 +4288,12 @@ fn readable_stream_pipe_abort_actions_own_all_rejections() { .expect("pipe abort rejection ownership checkpoint should evaluate"); } + assert!( + !vm.has_ready_dom_manipulation_family_for_test( + PageDomManipulationTestFamily::PromiseRejection, + ), + "no rejection notification may remain queued" + ); let events = vm .eval("JSON.stringify(globalThis.__pipeAbortRejectionEvents.sort())") .expect("pipe abort rejection ownership events should evaluate"); diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs index 1ac51cc621..684dfaf192 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_fetch.rs @@ -2875,9 +2875,9 @@ fn readable_stream_default_reader_release_lock_rejects_closed_and_reads() { ); } -#[test] -fn readable_stream_default_reader_release_lock_suppresses_internal_closed_rejection() { - let mut vm = new_storage_test_vm("https://example.com/"); +#[tokio::test(flavor = "current_thread")] +async fn readable_stream_default_reader_release_lock_suppresses_internal_closed_rejection() { + let mut vm = new_storage_page_task_executor_test_vm("https://example.com/"); let initial = vm .eval( @@ -2912,6 +2912,12 @@ fn readable_stream_default_reader_release_lock_suppresses_internal_closed_reject .expect("ReadableStreamDefaultReader.releaseLock suppress promises should drain"); } + assert!( + !vm.has_ready_dom_manipulation_family_for_test( + PageDomManipulationTestFamily::PromiseRejection, + ), + "no rejection notification may remain queued" + ); let unhandled = vm .eval("JSON.stringify(globalThis.__readerReleaseUnhandled)") .expect("ReadableStreamDefaultReader.releaseLock suppress events should evaluate"); diff --git a/moli-renderer-v8/src/script_vm/tests/window_execution_context.rs b/moli-renderer-v8/src/script_vm/tests/window_execution_context.rs index e9c147f46d..961366ad0f 100644 --- a/moli-renderer-v8/src/script_vm/tests/window_execution_context.rs +++ b/moli-renderer-v8/src/script_vm/tests/window_execution_context.rs @@ -362,9 +362,13 @@ fn chromium_discarded_window_fetch_rejects_in_the_detached_function_realm() { ); } -#[test] -fn borrowed_fetch_uses_receiver_realm_and_keeps_reaction_realm_independent() { - let mut vm = new_storage_test_vm("https://borrowed-fetch-context.test/top/"); +#[tokio::test(flavor = "current_thread")] +async fn borrowed_fetch_uses_receiver_realm_and_keeps_reaction_realm_independent() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_page_task_executor_test_vm_with_loader( + "https://borrowed-fetch-context.test/top/", + &loader, + ); vm.set_fetch_subresource_interception(true, Some(crate::types::SubresourceResourceType::Fetch)); vm.eval( r#" @@ -376,8 +380,21 @@ fn borrowed_fetch_uses_receiver_realm_and_keeps_reaction_realm_independent() { "#, ) .expect("borrowed Fetch child should be exposed"); - let child_context_id = - materialize_single_child_default_realm_for_test(&mut vm, "borrowed Fetch target"); + assert!( + vm.run_one_child_frame_task_executor_turn( + crate::frame_owner_model::ChildFrameSemanticTurnKind::RealmMaterialization, + &loader, + ) + .await + .expect("borrowed Fetch child realm task") + ); + let realms = vm.live_child_default_runtime_realm_inventory(); + assert_eq!( + realms.len(), + 1, + "one materialized borrowed Fetch child realm" + ); + let child_context_id = realms[0].context_id; let child_handle = vm .child_frame_realm_store .get(&child_context_id) @@ -535,6 +552,22 @@ fn borrowed_fetch_uses_receiver_realm_and_keeps_reaction_realm_independent() { "child-response:true", "Response construction and Promise settlement must use the receiver relevant realm" ); + assert_eq!( + vm.eval("JSON.stringify(__borrowedFetchRejections)") + .unwrap(), + "[]", + "Promise reactions queue notifications without dispatching them at the checkpoint" + ); + for _ in 0..2 { + assert!( + vm.run_one_dom_manipulation_task_executor_turn( + PageDomManipulationTestFamily::PromiseRejection, + &loader, + ) + .await + .expect("realm-owned rejection notification") + ); + } assert_eq!( vm.eval("JSON.stringify(__borrowedFetchRejections)") .expect("borrowed Fetch rejection routing should evaluate"),