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"),