mirror of
https://github.com/lexmount/moli.git
synced 2026-09-25 16:01:33 +00:00
fix(navigation): track intercepted traversal transitions
Carry transition resolvers through pending traversal commits and settlement, reusing precommit transitions and the shared navigation lifecycle. Keep committed reactions after currententrychange and intercept handlers, and finish empty handler lists asynchronously. Preserve rejection reasons and transitions started by completion callbacks. Add default protocol regressions and four unmodified upstream WPT variants. Serve their .mjs helper through the compat fixture server.
This commit is contained in:
@@ -102,6 +102,10 @@ wpt_compat_cases! {
|
||||
wpt_compat_case_upstream_navigation_api_precommit_handler_precommithandler_reload => "upstream-navigation-api-precommit-handler-precommithandler-reload",
|
||||
wpt_compat_case_upstream_navigation_api_precommit_handler_precommithandler_replace => "upstream-navigation-api-precommit-handler-precommithandler-replace",
|
||||
wpt_compat_case_upstream_navigation_api_precommit_handler_precommithandler_traverse => "upstream-navigation-api-precommit-handler-precommithandler-traverse",
|
||||
wpt_compat_case_upstream_navigation_api_ordering_and_transition_back_same_document_intercept_reject_no_currententrychange => "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-reject-no-currententrychange",
|
||||
wpt_compat_case_upstream_navigation_api_ordering_and_transition_back_same_document_intercept_reject_currententrychange => "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-reject-currententrychange",
|
||||
wpt_compat_case_upstream_navigation_api_ordering_and_transition_back_same_document_intercept_no_currententrychange => "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-no-currententrychange",
|
||||
wpt_compat_case_upstream_navigation_api_ordering_and_transition_back_same_document_intercept_currententrychange => "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-currententrychange",
|
||||
wpt_compat_case_navigation_currententrychange_ignores_page_tampered_dispatch_basic => "navigation-currententrychange-ignores-page-tampered-dispatch-basic",
|
||||
wpt_compat_case_navigation_update_current_entry_event_surface_basic => "navigation-update-current-entry-event-surface-basic",
|
||||
wpt_compat_case_navigation_cross_document_currententrychange_quietness_basic => "navigation-cross-document-currententrychange-quietness-basic",
|
||||
|
||||
@@ -182,6 +182,322 @@ impl SameDocumentPage {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn intercepted_traversal_transitions_track_commit_and_completion() {
|
||||
for precommit in [false, true] {
|
||||
for mode in ["empty", "sync", "async", "reject", "undefined"] {
|
||||
let mut page = SameDocumentPage::new().await;
|
||||
page.run(
|
||||
"history.pushState(null, '', '#back'); history.pushState(null, '', '#current'); void 0",
|
||||
"#current",
|
||||
).await;
|
||||
page.ctx.sent.clear();
|
||||
let script = r#"
|
||||
(async () => {
|
||||
const mode = MODE;
|
||||
const precommit = PRECOMMIT;
|
||||
const order = [];
|
||||
const checks = [];
|
||||
const from = navigation.currentEntry;
|
||||
const error = mode === 'undefined' ? undefined : new Error('traversal failure');
|
||||
const fails = mode === 'reject' || mode === 'undefined';
|
||||
let transition;
|
||||
let event;
|
||||
let transitionCommitted;
|
||||
let transitionFinished;
|
||||
const capture = phase => {
|
||||
order.push(phase);
|
||||
const current = navigation.transition;
|
||||
checks.push(current !== null && current.from === from &&
|
||||
current.navigationType === 'traverse' && current.to === event.destination);
|
||||
if (!transition) {
|
||||
transition = current;
|
||||
transitionCommitted = transition?.committed.then(value => value === undefined);
|
||||
transitionFinished = transition?.finished.then(
|
||||
value => { order.push('transition finished'); return !fails && value === undefined; },
|
||||
reason => { order.push('transition finished'); return fails && reason === error; }
|
||||
);
|
||||
}
|
||||
checks.push(current === transition);
|
||||
};
|
||||
navigation.addEventListener('navigate', e => {
|
||||
event = e;
|
||||
order.push('navigate');
|
||||
checks.push(navigation.transition === null);
|
||||
const options = {};
|
||||
if (precommit) options.precommitHandler = () => {
|
||||
capture('precommit');
|
||||
checks.push(location.hash === '#current');
|
||||
return new Promise(resolve => setTimeout(resolve, 0));
|
||||
};
|
||||
if (mode !== 'empty') options.handler = () => {
|
||||
capture('handler');
|
||||
checks.push(location.hash === '#back');
|
||||
if (fails) return Promise.reject(error);
|
||||
if (mode === 'async') return new Promise(resolve => setTimeout(resolve, 0));
|
||||
};
|
||||
e.intercept(options);
|
||||
e.signal.addEventListener('abort', () => checks.push(fails && e.signal.reason === error));
|
||||
}, {once: true});
|
||||
navigation.addEventListener('currententrychange', () => capture('currententrychange'), {once: true});
|
||||
navigation.addEventListener('navigatesuccess', () => capture('success'), {once: true});
|
||||
navigation.addEventListener('navigateerror', e => {
|
||||
capture('error');
|
||||
checks.push(fails && e.error === error);
|
||||
}, {once: true});
|
||||
const result = navigation.back();
|
||||
const committed = result.committed.then(entry => {
|
||||
capture('committed');
|
||||
return entry === navigation.currentEntry;
|
||||
});
|
||||
const finished = result.finished.then(
|
||||
entry => { order.push('finished'); return !fails && entry === navigation.currentEntry && navigation.transition === null; },
|
||||
reason => { order.push('finished'); return fails && reason === error && navigation.transition === null; }
|
||||
);
|
||||
checks.push(await committed, await finished);
|
||||
checks.push(await transitionCommitted, await transitionFinished);
|
||||
return {order, checks};
|
||||
})()
|
||||
"#.replace("MODE", &json!(mode).to_string())
|
||||
.replace("PRECOMMIT", if precommit { "true" } else { "false" });
|
||||
let result = page.evaluate(&script).await;
|
||||
let mut expected = vec!["navigate"];
|
||||
if precommit {
|
||||
expected.push("precommit");
|
||||
}
|
||||
expected.push("currententrychange");
|
||||
if mode != "empty" {
|
||||
expected.push("handler");
|
||||
}
|
||||
expected.extend([
|
||||
"committed",
|
||||
if matches!(mode, "reject" | "undefined") {
|
||||
"error"
|
||||
} else {
|
||||
"success"
|
||||
},
|
||||
"finished",
|
||||
"transition finished",
|
||||
]);
|
||||
assert_eq!(
|
||||
result["order"],
|
||||
json!(expected),
|
||||
"{mode}, precommit={precommit}"
|
||||
);
|
||||
assert!(
|
||||
result["checks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|value| value == true),
|
||||
"{mode}, precommit={precommit}: {result}"
|
||||
);
|
||||
page.assert_history(&["", "#back", "#current"], 1).await;
|
||||
page.assert_commits(&[("#back", "other")]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn intercepted_traversal_transitions_settle_when_canceled() {
|
||||
for phase in ["precommit", "handler"] {
|
||||
for action in ["stop", "navigate"] {
|
||||
let mut page = SameDocumentPage::new().await;
|
||||
let script = r#"(async () => {
|
||||
const phase = PHASE;
|
||||
const action = ACTION;
|
||||
history.pushState(null, '', '#back');
|
||||
history.pushState(null, '', '#current');
|
||||
await new Promise(resolve=>setTimeout(resolve,0));
|
||||
const from = navigation.currentEntry;
|
||||
let ready;
|
||||
const started = new Promise(resolve => ready = resolve);
|
||||
let transition, signal, release;
|
||||
const observed = {committed: 'pending', finished: 'pending', transitionCommitted: 'pending', transitionFinished: 'pending'};
|
||||
navigation.addEventListener('navigate', event => {
|
||||
signal = event.signal;
|
||||
const handler = () => {
|
||||
transition = navigation.transition;
|
||||
observed.transitionPresent = transition !== null;
|
||||
observed.from = transition?.from === from;
|
||||
transition?.committed?.then(() => observed.transitionCommitted = 'resolved', reason => observed.transitionCommitted = reason.name);
|
||||
transition?.finished.then(() => observed.transitionFinished = 'resolved', reason => observed.transitionFinished = reason.name);
|
||||
ready();
|
||||
return new Promise(resolve => release = resolve);
|
||||
};
|
||||
event.intercept(phase === 'precommit' ? {precommitHandler: handler} : {handler});
|
||||
}, {once: true});
|
||||
const result = navigation.back();
|
||||
result.committed.then(() => observed.committed = 'resolved', reason => observed.committed = reason.name);
|
||||
result.finished.then(() => observed.finished = 'resolved', reason => observed.finished = reason.name);
|
||||
await started;
|
||||
if (action === 'stop') window.stop();
|
||||
else await navigation.navigate('#nested').finished;
|
||||
await result.finished.catch(() => {});
|
||||
release();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
observed.currentTransition = navigation.transition;
|
||||
observed.hash = location.hash;
|
||||
observed.aborted = signal.aborted;
|
||||
return observed;
|
||||
})()
|
||||
"#
|
||||
.replace("PHASE", &json!(phase).to_string())
|
||||
.replace("ACTION", &json!(action).to_string());
|
||||
let result = page.evaluate(&script).await;
|
||||
let committed = if phase == "precommit" {
|
||||
"AbortError"
|
||||
} else {
|
||||
"resolved"
|
||||
};
|
||||
let hash = if action == "navigate" {
|
||||
"#nested"
|
||||
} else if phase == "precommit" {
|
||||
"#current"
|
||||
} else {
|
||||
"#back"
|
||||
};
|
||||
assert_eq!(
|
||||
result,
|
||||
json!({
|
||||
"committed": committed,
|
||||
"finished": "AbortError",
|
||||
"transitionCommitted": committed,
|
||||
"transitionFinished": "AbortError",
|
||||
"transitionPresent": true,
|
||||
"from": true,
|
||||
"currentTransition": null,
|
||||
"hash": hash,
|
||||
"aborted": true,
|
||||
}),
|
||||
"phase={phase}, action={action}"
|
||||
);
|
||||
if action == "stop" {
|
||||
page.assert_history(
|
||||
&["", "#back", "#current"],
|
||||
if phase == "precommit" { 2 } else { 1 },
|
||||
)
|
||||
.await;
|
||||
} else if phase == "precommit" {
|
||||
page.assert_history(&["", "#back", "#current", "#nested"], 3)
|
||||
.await;
|
||||
} else {
|
||||
page.assert_history(&["", "#back", "#nested"], 2).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn intercepted_traversal_transitions_reject_before_commit() {
|
||||
for undefined in [false, true] {
|
||||
let mut page = SameDocumentPage::new().await;
|
||||
page.run(
|
||||
"history.pushState(null, '', '#current'); void 0",
|
||||
"#current",
|
||||
)
|
||||
.await;
|
||||
page.ctx.sent.clear();
|
||||
let script = r#"(async () => {
|
||||
const expected = UNDEFINED ? undefined : new Error('precommit failure');
|
||||
const from = navigation.currentEntry;
|
||||
const checks = [];
|
||||
let transition;
|
||||
let handlerRan = false;
|
||||
navigation.addEventListener('navigate', event => {
|
||||
event.signal.addEventListener('abort', () => checks.push(event.signal.reason === expected));
|
||||
event.intercept({
|
||||
precommitHandler() {
|
||||
transition = navigation.transition;
|
||||
return new Promise((_, reject) => setTimeout(() => reject(expected), 0));
|
||||
},
|
||||
handler() { handlerRan = true; }
|
||||
});
|
||||
}, {once: true});
|
||||
const result = navigation.back();
|
||||
const rejectedWithExpected = promise => promise.then(() => false, reason => reason === expected);
|
||||
checks.push(...await Promise.all([result.committed, result.finished].map(rejectedWithExpected)));
|
||||
checks.push(...await Promise.all([transition.committed, transition.finished].map(rejectedWithExpected)));
|
||||
checks.push(!handlerRan, navigation.currentEntry === from, navigation.transition === null);
|
||||
return checks;
|
||||
})()
|
||||
"#.replace("UNDEFINED", if undefined { "true" } else { "false" });
|
||||
let result = page.evaluate(&script).await;
|
||||
assert_eq!(
|
||||
result,
|
||||
json!([true, true, true, true, true, true, true, true]),
|
||||
"undefined={undefined}"
|
||||
);
|
||||
page.assert_history(&["", "#current"], 1).await;
|
||||
page.assert_commits(&[]);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn intercepted_traversal_transitions_preserve_navigation_started_during_completion() {
|
||||
for trigger in ["success", "error", "abort"] {
|
||||
let mut page = SameDocumentPage::new().await;
|
||||
page.run(
|
||||
"history.pushState(null, '', '#back'); history.pushState(null, '', '#current'); void 0",
|
||||
"#current",
|
||||
)
|
||||
.await;
|
||||
page.ctx.sent.clear();
|
||||
let script = r#"(async () => {
|
||||
const trigger = TRIGGER;
|
||||
const error = new Error('expected');
|
||||
const observed = {};
|
||||
let transition, nestedTransition, nested, release;
|
||||
navigation.addEventListener('currententrychange', () => {
|
||||
transition = navigation.transition;
|
||||
transition?.finished.then(() => observed.oldFinished = 'resolved', reason => observed.oldFinished = reason === error ? 'expected' : reason.name);
|
||||
}, {once: true});
|
||||
const startNested = () => {
|
||||
navigation.addEventListener('navigate', e => e.intercept({handler() {
|
||||
nestedTransition = navigation.transition;
|
||||
return new Promise(resolve => release = resolve);
|
||||
}}), {once: true});
|
||||
nested = navigation.navigate('#nested');
|
||||
nested.finished.then(() => observed.nestedFinished = true, reason => observed.nestedError = reason.name);
|
||||
};
|
||||
navigation.addEventListener('navigate', e => {
|
||||
if (trigger === 'abort') e.signal.addEventListener('abort', startNested, {once: true});
|
||||
e.intercept({handler() {
|
||||
if (trigger !== 'success') return Promise.reject(error);
|
||||
}});
|
||||
}, {once: true});
|
||||
if (trigger !== 'abort') navigation.addEventListener(trigger === 'success' ? 'navigatesuccess' : 'navigateerror', startNested, {once: true});
|
||||
const result = navigation.back();
|
||||
await result.finished.catch(() => {});
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
observed.preservedNewTransition = navigation.transition === nestedTransition && nestedTransition !== null;
|
||||
observed.distinct = nestedTransition !== transition;
|
||||
observed.newPending = !observed.nestedFinished;
|
||||
release();
|
||||
await nested.finished.catch(() => {});
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
observed.cleared = navigation.transition === null;
|
||||
return observed;
|
||||
})()
|
||||
"#.replace("TRIGGER", &json!(trigger).to_string());
|
||||
let result = page.evaluate(&script).await;
|
||||
assert_eq!(
|
||||
result,
|
||||
json!({
|
||||
"oldFinished": if trigger == "success" { "resolved" } else { "expected" },
|
||||
"preservedNewTransition": true,
|
||||
"distinct": true,
|
||||
"newPending": true,
|
||||
"nestedFinished": true,
|
||||
"cleared": true,
|
||||
}),
|
||||
"trigger={trigger}"
|
||||
);
|
||||
page.assert_history(&["", "#back", "#nested"], 2).await;
|
||||
page.assert_commits(&[("#back", "other"), ("#nested", "other")]);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn same_document_commits_keep_navigation_api_and_browser_history_in_sync() {
|
||||
let mut page = SameDocumentPage::new().await;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use crate::context_bootstrap::file_api::is_branded_data_transfer_object;
|
||||
use crate::context_bootstrap::navigation_activation::install_navigation_transition;
|
||||
use crate::context_bootstrap::navigation_activation::{
|
||||
NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT, install_navigation_transition,
|
||||
};
|
||||
use crate::context_bootstrap::navigation_events::navigation_scroll_event_is_active;
|
||||
use crate::context_bootstrap::navigation_handler_callbacks::{
|
||||
NAVIGATE_EVENT_ADDED_HANDLERS_SLOT, NAVIGATE_EVENT_DEFERRED_HANDLERS_SLOT,
|
||||
@@ -36,8 +38,6 @@ const NAVIGATE_EVENT_PRECOMMIT_TRANSITION_DESTINATION_SLOT: &str =
|
||||
"__lmNavigateEventPrecommitTransitionDestination";
|
||||
const NAVIGATE_EVENT_PRECOMMIT_TRANSITION_TYPE_SLOT: &str =
|
||||
"__lmNavigateEventPrecommitTransitionType";
|
||||
const NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT: &str =
|
||||
"__lmNavigateEventPrecommitTransitionResolver";
|
||||
const PRECOMMIT_CONTROLLER_EVENT_SLOT: &str = "__lmPrecommitControllerEvent";
|
||||
const PRECOMMIT_CONTROLLER_ACTIVE_SLOT: &str = "__lmPrecommitControllerActive";
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
use super::super::navigation_activation::{
|
||||
install_navigation_transition, navigation_transition_matches_resolver,
|
||||
precommit_transition_resolver_from_event, reject_navigation_transition_committed,
|
||||
resolve_navigation_transition_committed,
|
||||
};
|
||||
use super::super::navigation_entry::{
|
||||
history_entries, history_index, navigation_current_entry, navigation_entry_key_value,
|
||||
};
|
||||
@@ -6,7 +11,9 @@ use super::super::navigation_events::{
|
||||
dispatch_navigation_traverse_event_with_outcome, mark_navigation_outcome_default_prevented,
|
||||
run_navigation_precommit_deferred_handlers,
|
||||
};
|
||||
use super::super::navigation_lifecycle::finish_navigation_error_events;
|
||||
use super::super::navigation_lifecycle::{
|
||||
finish_navigation_error_events, settle_navigation_transition_finished_local,
|
||||
};
|
||||
use super::super::navigation_result::{
|
||||
navigation_dom_exception, perform_navigation_scroll_if_needed, suppress_unhandled_rejection,
|
||||
};
|
||||
@@ -22,6 +29,7 @@ use super::apply::{
|
||||
};
|
||||
use super::results::{reject_pending_navigation_results, resolve_pending_navigation_results};
|
||||
use crate::native_bridge::PendingHistoryTraversal;
|
||||
use crate::script_cleanup::ScriptExecutionScope;
|
||||
use crate::util::{get_private_value, set_private_value};
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
@@ -46,6 +54,7 @@ const TRAVERSAL_INTERCEPT_VALUE_SLOT: &str = "__lmTraversalInterceptValue";
|
||||
const TRAVERSAL_INTERCEPT_URL_SLOT: &str = "__lmTraversalInterceptUrl";
|
||||
const TRAVERSAL_INTERCEPT_PROMISE_SLOT: &str = "__lmTraversalInterceptPromise";
|
||||
const NAVIGATION_ACTIVE_TRAVERSAL_INTERCEPT_SLOT: &str = "__lmNavigationActiveTraversalIntercept";
|
||||
const TRAVERSAL_TRANSITION_RESOLVER_SLOT: &str = "__lmTraversalTransitionResolver";
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain)]
|
||||
@@ -82,6 +91,9 @@ struct TraversalPrecommitDataDeclaration<'scope> {
|
||||
|
||||
#[webapi(slot = TRAVERSAL_PRECOMMIT_FINISHED_RESOLVERS_SLOT)]
|
||||
finished_resolvers: v8::Local<'scope, v8::Array>,
|
||||
|
||||
#[webapi(slot = TRAVERSAL_TRANSITION_RESOLVER_SLOT)]
|
||||
transition_resolver: Option<v8::Local<'scope, v8::PromiseResolver>>,
|
||||
}
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
@@ -104,6 +116,33 @@ struct TraversalInterceptSettlementDataDeclaration<'scope> {
|
||||
|
||||
#[webapi(slot = TRAVERSAL_INTERCEPT_URL_SLOT)]
|
||||
url: v8::Local<'scope, v8::String>,
|
||||
|
||||
#[webapi(slot = TRAVERSAL_TRANSITION_RESOLVER_SLOT)]
|
||||
transition_resolver: Option<v8::Local<'scope, v8::PromiseResolver>>,
|
||||
}
|
||||
|
||||
struct InterceptedHistoryTraversal<'s> {
|
||||
joint_step: Option<moli_session_history::SessionHistoryStepId>,
|
||||
admission: Option<String>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
target_index: u32,
|
||||
event: Option<v8::Local<'s, v8::Object>>,
|
||||
signal: Option<v8::Local<'s, v8::Object>>,
|
||||
intercept_result: Option<v8::Local<'s, v8::Value>>,
|
||||
committed_resolvers: v8::Local<'s, v8::Array>,
|
||||
finished_resolvers: v8::Local<'s, v8::Array>,
|
||||
transition_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
|
||||
}
|
||||
|
||||
fn traversal_transition_resolver<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Value>,
|
||||
) -> Option<v8::Local<'s, v8::PromiseResolver>> {
|
||||
let data = v8::Local::<v8::Object>::try_from(data).ok()?;
|
||||
get_private_value(scope, data, TRAVERSAL_TRANSITION_RESOLVER_SLOT)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
|
||||
}
|
||||
|
||||
fn navigation_pending_traversal_precommit<'s>(
|
||||
@@ -321,6 +360,9 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(NavigationDispatchOutcome::proceed);
|
||||
let transition_resolver = outcome
|
||||
.precommit_event
|
||||
.and_then(|event| precommit_transition_resolver_from_event(scope, event));
|
||||
let owner_still_active = navigation_document_is_active(scope, owner);
|
||||
let target_still_available = target_entry_is_still_available(
|
||||
scope,
|
||||
@@ -330,18 +372,61 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
traversal.target_key.as_deref(),
|
||||
);
|
||||
if let Some(error) = outcome.abort_error {
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
if let Some(navigation) = navigation {
|
||||
reject_traversal_transition_committed(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
error,
|
||||
);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
reject_pending_navigation_results(scope, &results, error);
|
||||
return;
|
||||
}
|
||||
if let Some(error) = outcome.precommit_error {
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
if let Some(navigation) = navigation {
|
||||
reject_traversal_transition_committed(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
error,
|
||||
);
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
reject_pending_navigation_results(scope, &results, error);
|
||||
return;
|
||||
}
|
||||
if !outcome.proceed || !owner_still_active || !target_still_available {
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
if let Some(navigation) = navigation {
|
||||
reject_traversal_transition_committed(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
error,
|
||||
);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
mark_navigation_outcome_default_prevented(scope, &outcome);
|
||||
if let Some(signal) = outcome.signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
@@ -367,6 +452,17 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
if outcome.intercepted
|
||||
&& let Some(navigation) = navigation
|
||||
{
|
||||
let transition_resolver = transition_resolver.or_else(|| {
|
||||
navigation_current_entry(scope, owner).and_then(|from| {
|
||||
install_navigation_transition(
|
||||
scope,
|
||||
navigation,
|
||||
from,
|
||||
outcome.destination,
|
||||
"traverse",
|
||||
)
|
||||
})
|
||||
});
|
||||
if outcome.precommit_result.is_some() {
|
||||
let queued = queue_pending_precommit_history_traversal(
|
||||
scope,
|
||||
@@ -376,85 +472,47 @@ pub(in crate::context_bootstrap) fn apply_pending_history_traversal(
|
||||
joint_plan.as_ref(),
|
||||
outcome,
|
||||
&results,
|
||||
transition_resolver,
|
||||
);
|
||||
if !queued {
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
let error =
|
||||
navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
reject_traversal_transition_committed(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
error,
|
||||
);
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
reject_pending_navigation_results(scope, &results, error);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(applied) = apply_history_entry_commit(
|
||||
scope,
|
||||
history,
|
||||
traversal.target_index,
|
||||
joint_plan.as_ref(),
|
||||
Some("other"),
|
||||
) else {
|
||||
let error =
|
||||
navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
reject_pending_navigation_results(scope, &results, error);
|
||||
return;
|
||||
};
|
||||
let (committed_resolvers, finished_resolvers) =
|
||||
pending_result_resolver_arrays(scope, &results);
|
||||
resolve_resolver_array(scope, committed_resolvers, applied.resolved_entry);
|
||||
let active_intercept = set_active_traversal_intercept_settlement(
|
||||
commit_intercepted_history_traversal(
|
||||
scope,
|
||||
navigation,
|
||||
outcome.signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
InterceptedHistoryTraversal {
|
||||
navigation,
|
||||
history,
|
||||
target_index: traversal.target_index,
|
||||
joint_step: traversal.joint_step,
|
||||
admission: joint_plan.as_ref().map(super::super::session_history::traversal_admission_signature),
|
||||
event: outcome.precommit_event,
|
||||
signal: outcome.signal,
|
||||
intercept_result: outcome.intercept_result,
|
||||
committed_resolvers,
|
||||
finished_resolvers,
|
||||
transition_resolver,
|
||||
},
|
||||
);
|
||||
dispatch_history_entry_currententrychange(scope, &applied);
|
||||
let (intercept_error, intercept_result) = if let Some(event) = outcome.precommit_event {
|
||||
run_navigation_precommit_deferred_handlers(scope, event)
|
||||
} else {
|
||||
(None, outcome.intercept_result)
|
||||
};
|
||||
suppress_intercept_result_unhandled_rejection(scope, intercept_result);
|
||||
dispatch_history_entry_post_commit_events(scope, &applied, true);
|
||||
if !traversal_intercept_is_active(scope, active_intercept.into()) {
|
||||
return;
|
||||
}
|
||||
if let Some(error) = intercept_error {
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
finish_navigation_error_events(scope, navigation, error, &applied.url);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
if let Some(signal) = outcome.signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(result) = intercept_result else {
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &applied.url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
return;
|
||||
};
|
||||
if !traversal_intercept_is_active(scope, active_intercept.into()) {
|
||||
return;
|
||||
}
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
if !queue_pending_traversal_intercept_settlement(
|
||||
scope,
|
||||
navigation,
|
||||
outcome.signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
result,
|
||||
) {
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &applied.url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let pending_results = (!results.is_empty()).then_some(results.as_slice());
|
||||
@@ -629,6 +687,7 @@ fn queue_pending_precommit_history_traversal<'s>(
|
||||
plan: Option<&moli_session_history::SessionHistoryTraversalPlan>,
|
||||
outcome: NavigationDispatchOutcome<'s>,
|
||||
results: &[crate::native_bridge::PendingNavigationResult],
|
||||
transition_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
|
||||
) -> bool {
|
||||
let Some(precommit_result) = outcome.precommit_result else {
|
||||
return false;
|
||||
@@ -665,6 +724,7 @@ fn queue_pending_precommit_history_traversal<'s>(
|
||||
promise,
|
||||
committed_resolvers,
|
||||
finished_resolvers,
|
||||
transition_resolver,
|
||||
}
|
||||
.bind(scope)
|
||||
.expect("traversal precommit data should bind");
|
||||
@@ -804,6 +864,9 @@ pub(in crate::context_bootstrap) fn cancel_pending_precommit_history_traversal<'
|
||||
set_traversal_precommit_inactive(scope, data);
|
||||
let error =
|
||||
navigation_dom_exception(scope, "Navigation was canceled before commit", "AbortError");
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
let transition_resolver = traversal_transition_resolver(scope, data.into());
|
||||
reject_traversal_transition_committed(scope, navigation, transition_resolver, error);
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
@@ -812,6 +875,12 @@ pub(in crate::context_bootstrap) fn cancel_pending_precommit_history_traversal<'
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
reject_resolver_array(scope, committed_resolvers, error, false);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -849,76 +918,23 @@ fn traversal_precommit_fulfilled_callback<'s>(
|
||||
.and_then(|data| get_private_value(scope, data, TRAVERSAL_PRECOMMIT_ADMISSION_SLOT))
|
||||
.and_then(|value| value.to_string(scope))
|
||||
.map(|value| value.to_rust_string_lossy(scope));
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let plan = joint_step
|
||||
.and_then(|step| super::super::session_history::plan_traversal(scope, owner, step))
|
||||
.filter(|plan| {
|
||||
Some(super::super::session_history::traversal_admission_signature(plan)) == admission
|
||||
});
|
||||
let applied = if joint_step.is_some() && plan.is_none() {
|
||||
None
|
||||
} else {
|
||||
apply_history_entry_commit(scope, history, target_index, plan.as_ref(), Some("other"))
|
||||
};
|
||||
let Some(applied) = applied else {
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
reject_resolver_array(scope, committed_resolvers, error, false);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
return;
|
||||
};
|
||||
resolve_resolver_array(scope, committed_resolvers, applied.resolved_entry);
|
||||
let active_intercept = set_active_traversal_intercept_settlement(
|
||||
let transition_resolver = traversal_transition_resolver(scope, args.data());
|
||||
commit_intercepted_history_traversal(
|
||||
scope,
|
||||
navigation,
|
||||
signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
InterceptedHistoryTraversal {
|
||||
navigation,
|
||||
history,
|
||||
target_index,
|
||||
joint_step,
|
||||
admission,
|
||||
event: Some(event),
|
||||
signal,
|
||||
intercept_result: None,
|
||||
committed_resolvers,
|
||||
finished_resolvers,
|
||||
transition_resolver,
|
||||
},
|
||||
);
|
||||
dispatch_history_entry_currententrychange(scope, &applied);
|
||||
let (intercept_error, intercept_result) =
|
||||
run_navigation_precommit_deferred_handlers(scope, event);
|
||||
suppress_intercept_result_unhandled_rejection(scope, intercept_result);
|
||||
dispatch_history_entry_post_commit_events(scope, &applied, true);
|
||||
if !traversal_intercept_is_active(scope, active_intercept.into()) {
|
||||
return;
|
||||
}
|
||||
if let Some(error) = intercept_error {
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
finish_navigation_error_events(scope, navigation, error, &applied.url);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(result) = intercept_result else {
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &applied.url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
return;
|
||||
};
|
||||
if !traversal_intercept_is_active(scope, active_intercept.into()) {
|
||||
return;
|
||||
}
|
||||
set_traversal_intercept_inactive(scope, navigation, active_intercept.into());
|
||||
if !queue_pending_traversal_intercept_settlement(
|
||||
scope,
|
||||
navigation,
|
||||
signal,
|
||||
finished_resolvers,
|
||||
applied.resolved_entry,
|
||||
&applied.url,
|
||||
result,
|
||||
) {
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &applied.url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, applied.resolved_entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn traversal_precommit_rejected_callback<'s>(
|
||||
@@ -941,6 +957,9 @@ fn traversal_precommit_rejected_callback<'s>(
|
||||
.filter(|promise| promise.state() == v8::PromiseState::Rejected)
|
||||
.map(|promise| promise.result(scope))
|
||||
.unwrap_or_else(|| args.get(0));
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
let transition_resolver = traversal_transition_resolver(scope, args.data());
|
||||
reject_traversal_transition_committed(scope, navigation, transition_resolver, error);
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
@@ -949,6 +968,105 @@ fn traversal_precommit_rejected_callback<'s>(
|
||||
finish_navigation_error_events(scope, navigation, error, "");
|
||||
reject_resolver_array(scope, committed_resolvers, error, false);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
navigation,
|
||||
transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
}
|
||||
|
||||
fn commit_intercepted_history_traversal<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: InterceptedHistoryTraversal<'s>,
|
||||
) {
|
||||
// HTML's "prepare to run script" keeps commit-time Promise reactions after
|
||||
// currententrychange listeners and intercept handlers, even on a native task.
|
||||
let execution = ScriptExecutionScope::enter(scope);
|
||||
let owner = runtime_window_owner(scope, data.history);
|
||||
let plan = data.joint_step
|
||||
.and_then(|step| super::super::session_history::plan_traversal(scope, owner, step))
|
||||
.filter(|plan| Some(super::super::session_history::traversal_admission_signature(plan)) == data.admission);
|
||||
let applied = if data.joint_step.is_some() && plan.is_none() {
|
||||
None
|
||||
} else {
|
||||
apply_history_entry_commit(scope, data.history, data.target_index, plan.as_ref(), Some("other"))
|
||||
};
|
||||
let Some(applied) = applied else {
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
reject_traversal_transition_committed(
|
||||
scope,
|
||||
data.navigation,
|
||||
data.transition_resolver,
|
||||
error,
|
||||
);
|
||||
finish_navigation_error_events(scope, data.navigation, error, "");
|
||||
reject_resolver_array(scope, data.committed_resolvers, error, false);
|
||||
reject_resolver_array(scope, data.finished_resolvers, error, true);
|
||||
settle_navigation_transition_finished_local(
|
||||
scope,
|
||||
data.navigation,
|
||||
data.transition_resolver,
|
||||
Some(error),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let url = v8_string(scope, &applied.url).unwrap_or_else(|| v8::String::empty(scope));
|
||||
let active_intercept = TraversalInterceptSettlementDataDeclaration {
|
||||
active: true,
|
||||
navigation: data.navigation,
|
||||
signal: data.signal,
|
||||
finished_resolvers: data.finished_resolvers,
|
||||
value: applied.resolved_entry,
|
||||
url,
|
||||
transition_resolver: data.transition_resolver,
|
||||
}
|
||||
.bind(scope)
|
||||
.expect("traversal intercept settlement data should bind");
|
||||
set_navigation_active_traversal_intercept(scope, data.navigation, active_intercept);
|
||||
resolve_resolver_array(scope, data.committed_resolvers, applied.resolved_entry);
|
||||
resolve_navigation_transition_committed(scope, data.navigation, v8::undefined(scope).into());
|
||||
dispatch_history_entry_currententrychange(scope, &applied);
|
||||
let (intercept_error, intercept_result) = if let Some(event) = data.event {
|
||||
run_navigation_precommit_deferred_handlers(scope, event)
|
||||
} else {
|
||||
(None, data.intercept_result)
|
||||
};
|
||||
suppress_intercept_result_unhandled_rejection(scope, intercept_result);
|
||||
drop(execution);
|
||||
dispatch_history_entry_post_commit_events(scope, &applied, true);
|
||||
if !traversal_intercept_is_active(scope, active_intercept.into()) {
|
||||
return;
|
||||
}
|
||||
if let Some(error) = intercept_error {
|
||||
finish_traversal_intercept(scope, active_intercept.into(), Some(error));
|
||||
} else {
|
||||
// Waiting for an empty handler list is asynchronous too. In particular,
|
||||
// a precommit Promise callback must not finish before committed reactions.
|
||||
let result = intercept_result.or_else(|| {
|
||||
let resolver = v8::PromiseResolver::new(scope)?;
|
||||
resolver.resolve(scope, v8::undefined(scope).into())?;
|
||||
Some(resolver.get_promise(scope).into())
|
||||
});
|
||||
if result.is_none_or(|result| {
|
||||
!queue_pending_traversal_intercept_settlement(scope, active_intercept, result)
|
||||
}) {
|
||||
finish_traversal_intercept(scope, active_intercept.into(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_traversal_transition_committed<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
transition_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
|
||||
error: v8::Local<'s, v8::Value>,
|
||||
) {
|
||||
if transition_resolver
|
||||
.is_some_and(|resolver| navigation_transition_matches_resolver(scope, navigation, resolver))
|
||||
{
|
||||
reject_navigation_transition_committed(scope, navigation, error);
|
||||
}
|
||||
}
|
||||
|
||||
fn suppress_intercept_result_unhandled_rejection<'s>(
|
||||
@@ -964,11 +1082,7 @@ fn suppress_intercept_result_unhandled_rejection<'s>(
|
||||
|
||||
fn queue_pending_traversal_intercept_settlement<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
signal: Option<v8::Local<'s, v8::Object>>,
|
||||
finished_resolvers: v8::Local<'s, v8::Array>,
|
||||
resolved_value: v8::Local<'s, v8::Value>,
|
||||
url: &str,
|
||||
data: v8::Local<'s, v8::Object>,
|
||||
result: v8::Local<'s, v8::Value>,
|
||||
) -> bool {
|
||||
let Some(result_object) = v8::Local::<v8::Object>::try_from(result).ok() else {
|
||||
@@ -980,14 +1094,6 @@ fn queue_pending_traversal_intercept_settlement<'s>(
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let data = set_active_traversal_intercept_settlement(
|
||||
scope,
|
||||
navigation,
|
||||
signal,
|
||||
finished_resolvers,
|
||||
resolved_value,
|
||||
url,
|
||||
);
|
||||
if let Ok(promise) = v8::Local::<v8::Promise>::try_from(result) {
|
||||
suppress_unhandled_rejection(scope, promise);
|
||||
set_private_value(scope, data, TRAVERSAL_INTERCEPT_PROMISE_SLOT, result);
|
||||
@@ -1008,29 +1114,6 @@ fn queue_pending_traversal_intercept_settlement<'s>(
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn set_active_traversal_intercept_settlement<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
signal: Option<v8::Local<'s, v8::Object>>,
|
||||
finished_resolvers: v8::Local<'s, v8::Array>,
|
||||
resolved_value: v8::Local<'s, v8::Value>,
|
||||
url: &str,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let url = v8_string(scope, url).unwrap_or_else(|| v8::String::empty(scope));
|
||||
let data = TraversalInterceptSettlementDataDeclaration {
|
||||
active: true,
|
||||
navigation,
|
||||
signal,
|
||||
finished_resolvers,
|
||||
value: resolved_value,
|
||||
url,
|
||||
}
|
||||
.bind(scope)
|
||||
.expect("traversal intercept settlement data should bind");
|
||||
set_navigation_active_traversal_intercept(scope, navigation, data);
|
||||
data
|
||||
}
|
||||
|
||||
fn traversal_intercept_data<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Value>,
|
||||
@@ -1113,20 +1196,8 @@ pub(in crate::context_bootstrap) fn cancel_active_history_traversal_intercept_se
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some((navigation, signal, finished_resolvers, _, url, _)) =
|
||||
traversal_intercept_data(scope, data.into())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
set_traversal_intercept_inactive(scope, navigation, data.into());
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
}
|
||||
finish_navigation_error_events(scope, navigation, error, &url);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
finish_traversal_intercept(scope, data.into(), Some(error));
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1135,33 +1206,7 @@ fn traversal_intercept_fulfilled_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some((navigation, signal, finished_resolvers, resolved_value, url, _)) =
|
||||
traversal_intercept_data(scope, args.data())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
set_traversal_intercept_inactive(scope, navigation, args.data());
|
||||
let owner = runtime_window_owner(scope, navigation);
|
||||
if !navigation_document_is_active(scope, owner) {
|
||||
let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError");
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
}
|
||||
let top_owner = runtime_top_window_owner(scope, owner);
|
||||
let filename = window_location_for_holder(scope, top_owner)
|
||||
.and_then(|location| {
|
||||
super::super::location_runtime::location_href_slot(scope, location)
|
||||
})
|
||||
.unwrap_or(url);
|
||||
finish_navigation_error_events(scope, navigation, error, &filename);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
return;
|
||||
}
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, resolved_value);
|
||||
finish_traversal_intercept(scope, args.data(), None);
|
||||
}
|
||||
|
||||
fn traversal_intercept_rejected_callback<'s>(
|
||||
@@ -1169,23 +1214,58 @@ fn traversal_intercept_rejected_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some((navigation, signal, finished_resolvers, _, url, promise)) =
|
||||
traversal_intercept_data(scope, args.data())
|
||||
else {
|
||||
let Some((_, _, _, _, _, promise)) = traversal_intercept_data(scope, args.data()) else {
|
||||
return;
|
||||
};
|
||||
set_traversal_intercept_inactive(scope, navigation, args.data());
|
||||
let error = promise
|
||||
.filter(|promise| promise.state() == v8::PromiseState::Rejected)
|
||||
.map(|promise| promise.result(scope))
|
||||
.unwrap_or_else(|| args.get(0));
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
finish_traversal_intercept(scope, args.data(), Some(error));
|
||||
}
|
||||
|
||||
fn finish_traversal_intercept<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
data: v8::Local<'s, v8::Value>,
|
||||
error: Option<v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
let Some((navigation, signal, finished_resolvers, resolved_value, url, _)) =
|
||||
traversal_intercept_data(scope, data)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _execution = ScriptExecutionScope::enter(scope);
|
||||
let transition_resolver = traversal_transition_resolver(scope, data);
|
||||
set_traversal_intercept_inactive(scope, navigation, data);
|
||||
let owner = runtime_window_owner(scope, navigation);
|
||||
let active = navigation_document_is_active(scope, owner);
|
||||
let error = error.or_else(|| {
|
||||
(!active).then(|| navigation_dom_exception(scope, "Navigation was canceled", "AbortError"))
|
||||
});
|
||||
if let Some(error) = error {
|
||||
if let Some(signal) = signal
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.abort_signal(scope, signal, error);
|
||||
}
|
||||
let filename = if active {
|
||||
url
|
||||
} else {
|
||||
let top_owner = runtime_top_window_owner(scope, owner);
|
||||
window_location_for_holder(scope, top_owner)
|
||||
.and_then(|location| {
|
||||
super::super::location_runtime::location_href_slot(scope, location)
|
||||
})
|
||||
.unwrap_or(url)
|
||||
};
|
||||
finish_navigation_error_events(scope, navigation, error, &filename);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
} else {
|
||||
perform_navigation_scroll_if_needed(scope, navigation, &url, true);
|
||||
dispatch_navigation_success(scope, navigation);
|
||||
resolve_resolver_array(scope, finished_resolvers, resolved_value);
|
||||
}
|
||||
finish_navigation_error_events(scope, navigation, error, &url);
|
||||
reject_resolver_array(scope, finished_resolvers, error, true);
|
||||
settle_navigation_transition_finished_local(scope, navigation, transition_resolver, error);
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn resolve_resolver_array<'s>(
|
||||
|
||||
@@ -16,6 +16,9 @@ use crate::util::{get_private_value, set_private_value};
|
||||
use crate::web_api_interfaces;
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
pub(super) const NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT: &str =
|
||||
"__lmNavigateEventPrecommitTransitionResolver";
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(
|
||||
interface = web_api_interfaces::NavigationActivation,
|
||||
@@ -70,6 +73,9 @@ struct NavigationTransitionSettleDataDeclaration<'scope> {
|
||||
|
||||
#[webapi(slot = NAVIGATION_TRANSITION_SETTLE_ERROR_SLOT)]
|
||||
error: Option<v8::Local<'scope, v8::Value>>,
|
||||
|
||||
#[webapi(slot = NAVIGATION_TRANSITION_SETTLE_REJECTED_SLOT)]
|
||||
rejected: bool,
|
||||
}
|
||||
|
||||
pub(super) fn install_navigation_activation_template_bindings<'s>(
|
||||
@@ -249,6 +255,19 @@ pub(super) fn resolve_navigation_transition_committed<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn precommit_transition_resolver_from_event<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::PromiseResolver>> {
|
||||
get_private_value(
|
||||
scope,
|
||||
event,
|
||||
NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT,
|
||||
)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
|
||||
}
|
||||
|
||||
pub(super) fn reject_navigation_transition_committed<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
@@ -270,6 +289,7 @@ const NAVIGATION_TRANSITION_SETTLE_NAVIGATION_SLOT: &str =
|
||||
"__lmNavigationTransitionSettleNavigation";
|
||||
const NAVIGATION_TRANSITION_SETTLE_RESOLVER_SLOT: &str = "__lmNavigationTransitionSettleResolver";
|
||||
const NAVIGATION_TRANSITION_SETTLE_ERROR_SLOT: &str = "__lmNavigationTransitionSettleError";
|
||||
const NAVIGATION_TRANSITION_SETTLE_REJECTED_SLOT: &str = "__lmNavigationTransitionSettleRejected";
|
||||
const NAVIGATION_TRANSITION_COMMITTED_RESOLVER_SLOT: &str =
|
||||
"__lmNavigationTransitionCommittedResolver";
|
||||
|
||||
@@ -304,6 +324,7 @@ pub(super) fn schedule_settle_navigation_transition<'s>(
|
||||
navigation,
|
||||
resolver,
|
||||
error,
|
||||
rejected: error.is_some(),
|
||||
}
|
||||
.bind(scope)
|
||||
.expect("navigation transition settle data should bind");
|
||||
@@ -311,7 +332,9 @@ pub(super) fn schedule_settle_navigation_transition<'s>(
|
||||
.data(data.into())
|
||||
.build(scope)
|
||||
else {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
if navigation_transition_matches_resolver(scope, navigation, resolver) {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
}
|
||||
return;
|
||||
};
|
||||
enqueue_navigation_lifecycle_microtask(scope, callback);
|
||||
@@ -340,9 +363,11 @@ fn settle_navigation_transition_callback<'s>(
|
||||
if navigation_transition_matches_resolver(scope, navigation, resolver) {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
}
|
||||
if let Some(error) = get_private_value(scope, data, NAVIGATION_TRANSITION_SETTLE_ERROR_SLOT)
|
||||
.filter(|value| !value.is_undefined())
|
||||
if get_private_value(scope, data, NAVIGATION_TRANSITION_SETTLE_REJECTED_SLOT)
|
||||
.is_some_and(|value| value.is_true())
|
||||
{
|
||||
let error = get_private_value(scope, data, NAVIGATION_TRANSITION_SETTLE_ERROR_SLOT)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
let _ = resolver.reject(scope, error);
|
||||
} else {
|
||||
let _ = resolver.resolve(scope, v8::undefined(scope).into());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::super::navigation_activation::{
|
||||
clear_navigation_transition, install_navigation_transition,
|
||||
navigation_transition_matches_resolver, reject_navigation_transition_committed,
|
||||
navigation_transition_matches_resolver, precommit_transition_resolver_from_event, reject_navigation_transition_committed,
|
||||
resolve_navigation_transition_committed,
|
||||
};
|
||||
use super::super::navigation_events::dispatch_popstate_event;
|
||||
@@ -22,9 +22,6 @@ use crate::webidl;
|
||||
use moli_url_policy::{LocalFileNavigationAccess, route_navigation_url};
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
const NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT: &str =
|
||||
"__lmNavigateEventPrecommitTransitionResolver";
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "NavigationNavigateOptions")]
|
||||
struct NavigationNavigateOptionsMembers {
|
||||
@@ -1088,19 +1085,6 @@ fn cancel_precommit_commit_attempt<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn precommit_transition_resolver_from_event<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::PromiseResolver>> {
|
||||
get_private_value(
|
||||
scope,
|
||||
event,
|
||||
NAVIGATE_EVENT_PRECOMMIT_TRANSITION_RESOLVER_SLOT,
|
||||
)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
|
||||
}
|
||||
|
||||
fn set_pending_precommit_commit_active<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
navigation: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::navigation_activation::{
|
||||
clear_navigation_transition, resolve_navigation_transition_committed,
|
||||
schedule_settle_navigation_transition,
|
||||
clear_navigation_transition, navigation_transition_matches_resolver,
|
||||
resolve_navigation_transition_committed, schedule_settle_navigation_transition,
|
||||
};
|
||||
use super::navigation_events::{
|
||||
NAVIGATE_EVENT_SCROLL_AFTER_TRANSITION_SLOT, NAVIGATE_EVENT_SCROLL_CALLED_SLOT,
|
||||
@@ -174,9 +174,6 @@ pub(super) fn settle_navigation_transition_finished<'s>(
|
||||
transition_resolver: Option<v8::Global<v8::PromiseResolver>>,
|
||||
error: Option<v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
if transition_resolver.is_some() {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
}
|
||||
if let Some(transition_resolver) = transition_resolver {
|
||||
let transition_resolver = v8::Local::new(scope, transition_resolver);
|
||||
settle_navigation_transition_finished_local(
|
||||
@@ -194,10 +191,10 @@ pub(super) fn settle_navigation_transition_finished_local<'s>(
|
||||
transition_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
|
||||
error: Option<v8::Local<'s, v8::Value>>,
|
||||
) {
|
||||
if transition_resolver.is_some() {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
}
|
||||
if let Some(transition_resolver) = transition_resolver {
|
||||
if navigation_transition_matches_resolver(scope, navigation, transition_resolver) {
|
||||
clear_navigation_transition(scope, navigation);
|
||||
}
|
||||
schedule_settle_navigation_transition(scope, navigation, transition_resolver, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20701,3 +20701,75 @@ source_path = "navigation-api/precommit-handler/precommitHandler-traverse.html"
|
||||
source_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
tags = ["navigation-api", "precommit-handler", "navigation", "history"]
|
||||
notes = "Imported upstream WPT test. Keep this in the broad suite until it is proven stable under the Moli harness."
|
||||
|
||||
[[test]]
|
||||
id = "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-reject-no-currententrychange"
|
||||
upstream = "web-platform-tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html?no-currententrychange"
|
||||
upstream_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
local_path = "upstream/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html"
|
||||
type = "testharness"
|
||||
global = "window"
|
||||
status = "pass"
|
||||
wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "broad"
|
||||
source = "upstream-wpt"
|
||||
source_path = "navigation-api/ordering-and-transition/back-same-document-intercept-reject.html"
|
||||
source_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
tags = ["navigation-api", "ordering-and-transition", "navigation", "history"]
|
||||
notes = "Unmodified upstream test for intercepted traversal transition lifetime and event/Promise ordering, including this explicit query variant."
|
||||
query = "no-currententrychange"
|
||||
|
||||
[[test]]
|
||||
id = "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-reject-currententrychange"
|
||||
upstream = "web-platform-tests/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html?currententrychange"
|
||||
upstream_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
local_path = "upstream/navigation-api/ordering-and-transition/back-same-document-intercept-reject.html"
|
||||
type = "testharness"
|
||||
global = "window"
|
||||
status = "pass"
|
||||
wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "broad"
|
||||
source = "upstream-wpt"
|
||||
source_path = "navigation-api/ordering-and-transition/back-same-document-intercept-reject.html"
|
||||
source_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
tags = ["navigation-api", "ordering-and-transition", "navigation", "history"]
|
||||
notes = "Unmodified upstream test for intercepted traversal transition lifetime and event/Promise ordering, including this explicit query variant."
|
||||
query = "currententrychange"
|
||||
|
||||
[[test]]
|
||||
id = "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-no-currententrychange"
|
||||
upstream = "web-platform-tests/navigation-api/ordering-and-transition/back-same-document-intercept.html?no-currententrychange"
|
||||
upstream_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
local_path = "upstream/navigation-api/ordering-and-transition/back-same-document-intercept.html"
|
||||
type = "testharness"
|
||||
global = "window"
|
||||
status = "pass"
|
||||
wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "broad"
|
||||
source = "upstream-wpt"
|
||||
source_path = "navigation-api/ordering-and-transition/back-same-document-intercept.html"
|
||||
source_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
tags = ["navigation-api", "ordering-and-transition", "navigation", "history"]
|
||||
notes = "Unmodified upstream test for intercepted traversal transition lifetime and event/Promise ordering, including this explicit query variant."
|
||||
query = "no-currententrychange"
|
||||
|
||||
[[test]]
|
||||
id = "upstream-navigation-api-ordering-and-transition-back-same-document-intercept-currententrychange"
|
||||
upstream = "web-platform-tests/navigation-api/ordering-and-transition/back-same-document-intercept.html?currententrychange"
|
||||
upstream_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
local_path = "upstream/navigation-api/ordering-and-transition/back-same-document-intercept.html"
|
||||
type = "testharness"
|
||||
global = "window"
|
||||
status = "pass"
|
||||
wait_until = "load"
|
||||
timeout_ms = 5000
|
||||
suite = "broad"
|
||||
source = "upstream-wpt"
|
||||
source_path = "navigation-api/ordering-and-transition/back-same-document-intercept.html"
|
||||
source_commit = "db95fafd1fcef8428805e41eb5705d444e8c67ce"
|
||||
tags = ["navigation-api", "ordering-and-transition", "navigation", "history"]
|
||||
notes = "Unmodified upstream test for intercepted traversal transition lifetime and event/Promise ordering, including this explicit query variant."
|
||||
query = "currententrychange"
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<meta name="variant" content="?no-currententrychange">
|
||||
<meta name="variant" content="?currententrychange">
|
||||
|
||||
<script type="module">
|
||||
import { Recorder, hasVariant } from "./resources/helpers.mjs";
|
||||
|
||||
promise_test(async t => {
|
||||
// Wait for after the load event so that the navigation doesn't get converted
|
||||
// into a replace navigation.
|
||||
await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
|
||||
await navigation.navigate("#1").finished;
|
||||
|
||||
const from = navigation.currentEntry;
|
||||
const expectedError = new Error("boo");
|
||||
|
||||
const recorder = new Recorder({
|
||||
skipCurrentChange: !hasVariant("currententrychange"),
|
||||
finalExpectedEvent: "transition.finished rejected"
|
||||
});
|
||||
|
||||
recorder.setUpNavigationAPIListeners();
|
||||
|
||||
navigation.addEventListener("navigate", e => {
|
||||
e.intercept({ handler() {
|
||||
recorder.record("handler run");
|
||||
return Promise.reject(expectedError);
|
||||
}});
|
||||
});
|
||||
|
||||
const result = navigation.back();
|
||||
recorder.setUpResultListeners(result);
|
||||
|
||||
Promise.resolve().then(() => recorder.record("promise microtask"));
|
||||
|
||||
await recorder.readyToAssert;
|
||||
|
||||
recorder.assert([
|
||||
/* event name, location.hash value, navigation.transition properties */
|
||||
["promise microtask", "#1", null],
|
||||
["navigate", "#1", null],
|
||||
["currententrychange", "", { from, navigationType: "traverse" }],
|
||||
["handler run", "", { from, navigationType: "traverse" }],
|
||||
["committed fulfilled", "", { from, navigationType: "traverse" }],
|
||||
["AbortSignal abort", "", { from, navigationType: "traverse" }],
|
||||
["navigateerror", "", { from, navigationType: "traverse" }],
|
||||
["finished rejected", "", null],
|
||||
["transition.finished rejected", "", null]
|
||||
]);
|
||||
|
||||
recorder.assertErrorsAre(expectedError);
|
||||
}, "event and promise ordering for same-document navigation.back() intercepted by passing a rejected promise to intercept()");
|
||||
</script>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<meta name="variant" content="?no-currententrychange">
|
||||
<meta name="variant" content="?currententrychange">
|
||||
|
||||
<script type="module">
|
||||
import { Recorder, hasVariant } from "./resources/helpers.mjs";
|
||||
|
||||
promise_test(async t => {
|
||||
// Wait for after the load event so that the navigation doesn't get converted
|
||||
// into a replace navigation.
|
||||
await new Promise(resolve => window.onload = () => t.step_timeout(resolve, 0));
|
||||
await navigation.navigate("#1").finished;
|
||||
|
||||
const from = navigation.currentEntry;
|
||||
|
||||
const recorder = new Recorder({
|
||||
skipCurrentChange: !hasVariant("currententrychange"),
|
||||
finalExpectedEvent: "transition.finished fulfilled"
|
||||
});
|
||||
|
||||
recorder.setUpNavigationAPIListeners();
|
||||
|
||||
navigation.addEventListener("navigate", e => {
|
||||
e.intercept({ handler() { recorder.record("handler run"); } });
|
||||
});
|
||||
|
||||
const result = navigation.back();
|
||||
recorder.setUpResultListeners(result);
|
||||
|
||||
Promise.resolve().then(() => recorder.record("promise microtask"));
|
||||
|
||||
await recorder.readyToAssert;
|
||||
|
||||
recorder.assert([
|
||||
/* event name, location.hash value, navigation.transition properties */
|
||||
["promise microtask", "#1", null],
|
||||
["navigate", "#1", null],
|
||||
["currententrychange", "", { from, navigationType: "traverse" }],
|
||||
["handler run", "", { from, navigationType: "traverse" }],
|
||||
["committed fulfilled", "", { from, navigationType: "traverse" }],
|
||||
["navigatesuccess", "", { from, navigationType: "traverse" }],
|
||||
["finished fulfilled", "", null],
|
||||
["transition.finished fulfilled", "", null]
|
||||
]);
|
||||
}, "event and promise ordering for same-document navigation.back() intercepted by intercept()");
|
||||
</script>
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
const variants = new Set((new URLSearchParams(location.search)).keys());
|
||||
|
||||
export function hasVariant(name) {
|
||||
return variants.has(name);
|
||||
}
|
||||
|
||||
export class Recorder {
|
||||
#events = [];
|
||||
#errors = [];
|
||||
#navigationAPI;
|
||||
#domExceptionConstructor;
|
||||
#location;
|
||||
#skipCurrentChange;
|
||||
#finalExpectedEvent;
|
||||
#finalExpectedEventCount;
|
||||
#currentFinalEventCount = 0;
|
||||
|
||||
#readyToAssertResolve;
|
||||
#readyToAssertPromise = new Promise(resolve => { this.#readyToAssertResolve = resolve; });
|
||||
|
||||
constructor({ window = self, skipCurrentChange = false, finalExpectedEvent, finalExpectedEventCount = 1 }) {
|
||||
assert_equals(typeof finalExpectedEvent, "string", "Must pass a string for finalExpectedEvent");
|
||||
|
||||
this.#navigationAPI = window.navigation;
|
||||
this.#domExceptionConstructor = window.DOMException;
|
||||
this.#location = window.location;
|
||||
|
||||
this.#skipCurrentChange = skipCurrentChange;
|
||||
this.#finalExpectedEvent = finalExpectedEvent;
|
||||
this.#finalExpectedEventCount = finalExpectedEventCount;
|
||||
}
|
||||
|
||||
setUpNavigationAPIListeners() {
|
||||
this.#navigationAPI.addEventListener("navigate", e => {
|
||||
this.record("navigate");
|
||||
|
||||
e.signal.addEventListener("abort", () => {
|
||||
this.recordWithError("AbortSignal abort", e.signal.reason);
|
||||
});
|
||||
});
|
||||
|
||||
this.#navigationAPI.addEventListener("navigateerror", e => {
|
||||
this.recordWithError("navigateerror", e.error);
|
||||
|
||||
this.#navigationAPI.transition?.finished.then(
|
||||
() => this.record("transition.finished fulfilled"),
|
||||
err => this.recordWithError("transition.finished rejected", err)
|
||||
);
|
||||
});
|
||||
|
||||
this.#navigationAPI.addEventListener("navigatesuccess", () => {
|
||||
this.record("navigatesuccess");
|
||||
|
||||
this.#navigationAPI.transition?.finished.then(
|
||||
() => this.record("transition.finished fulfilled"),
|
||||
err => this.recordWithError("transition.finished rejected", err)
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.#skipCurrentChange) {
|
||||
this.#navigationAPI.addEventListener("currententrychange", () => this.record("currententrychange"));
|
||||
}
|
||||
}
|
||||
|
||||
setUpResultListeners(result, suffix = "") {
|
||||
|
||||
result.committed.then(
|
||||
() => this.record(`committed fulfilled${suffix}`),
|
||||
err => this.recordWithError(`committed rejected${suffix}`, err)
|
||||
);
|
||||
|
||||
result.finished.then(
|
||||
() => this.record(`finished fulfilled${suffix}`),
|
||||
err => this.recordWithError(`finished rejected${suffix}`, err)
|
||||
);
|
||||
|
||||
this.#navigationAPI.transition?.committed?.then(
|
||||
() => this.record(`transition.committed fulfilled${suffix}`),
|
||||
err => this.recordWithError(`transition.committed rejected${suffix}`, err)
|
||||
);
|
||||
}
|
||||
|
||||
record(name) {
|
||||
const transitionProps = this.#navigationAPI.transition === null ? null : {
|
||||
from: this.#navigationAPI.transition.from,
|
||||
navigationType: this.#navigationAPI.transition.navigationType
|
||||
};
|
||||
|
||||
this.#events.push({ name, location: this.#location.hash, transitionProps });
|
||||
|
||||
if (name === this.#finalExpectedEvent && ++this.#currentFinalEventCount === this.#finalExpectedEventCount) {
|
||||
this.#readyToAssertResolve();
|
||||
}
|
||||
}
|
||||
|
||||
recordWithError(name, errorObject) {
|
||||
this.record(name);
|
||||
this.#errors.push({ name, errorObject });
|
||||
}
|
||||
|
||||
get readyToAssert() {
|
||||
return this.#readyToAssertPromise;
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// recorder.assert([
|
||||
// /* event name, location.hash value, navigation.transition properties */
|
||||
// ["currententrychange", "", null],
|
||||
// ["committed fulfilled", "#1", { from, navigationType }],
|
||||
// ...
|
||||
// ]);
|
||||
//
|
||||
// The array format is to avoid repitition at the call site, but I recommend
|
||||
// you document it like above.
|
||||
//
|
||||
// This will automatically also assert that any error objects recorded are
|
||||
// equal to each other. Use the other assert functions to check the actual
|
||||
// contents of the error objects.
|
||||
assert(expectedAsArray) {
|
||||
if (this.#skipCurrentChange) {
|
||||
expectedAsArray = expectedAsArray.filter(expected => expected[0] !== "currententrychange");
|
||||
}
|
||||
// TODO: Remove once https://github.com/whatwg/html/pull/10919 is merged.
|
||||
if (!('committed' in NavigationTransition.prototype)) {
|
||||
expectedAsArray = expectedAsArray.filter(expected => {
|
||||
return !expected[0].includes("transition.committed fulfilled") &&
|
||||
!expected[0].includes("transition.committed rejected")
|
||||
});
|
||||
}
|
||||
|
||||
// Doing this up front gives nicer error messages because
|
||||
// assert_array_equals is nice.
|
||||
const recordedNames = this.#events.map(e => e.name);
|
||||
const expectedNames = expectedAsArray.map(e => e[0]);
|
||||
assert_array_equals(recordedNames, expectedNames);
|
||||
|
||||
for (let i = 0; i < expectedAsArray.length; ++i) {
|
||||
const recorded = this.#events[i];
|
||||
const expected = expectedAsArray[i];
|
||||
|
||||
assert_equals(
|
||||
recorded.location,
|
||||
expected[1],
|
||||
`event ${i} (${recorded.name}): location.hash value`
|
||||
);
|
||||
|
||||
if (expected[2] === null) {
|
||||
assert_equals(
|
||||
recorded.transitionProps,
|
||||
null,
|
||||
`event ${i} (${recorded.name}): navigation.transition expected to be null`
|
||||
);
|
||||
} else {
|
||||
assert_not_equals(
|
||||
recorded.transitionProps,
|
||||
null,
|
||||
`event ${i} (${recorded.name}): navigation.transition expected not to be null`
|
||||
);
|
||||
assert_equals(
|
||||
recorded.transitionProps.from,
|
||||
expected[2].from,
|
||||
`event ${i} (${recorded.name}): navigation.transition.from`
|
||||
);
|
||||
assert_equals(
|
||||
recorded.transitionProps.navigationType,
|
||||
expected[2].navigationType,
|
||||
`event ${i} (${recorded.name}): navigation.transition.navigationType`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#errors.length > 1) {
|
||||
for (let i = 1; i < this.#errors.length; ++i) {
|
||||
assert_equals(
|
||||
this.#errors[i].errorObject,
|
||||
this.#errors[0].errorObject,
|
||||
`error objects must match: error object for ${this.#errors[i].name} did not match the one for ${this.#errors[0].name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertErrorsAreAbortErrors() {
|
||||
assert_greater_than(
|
||||
this.#errors.length,
|
||||
0,
|
||||
"No errors were recorded but assertErrorsAreAbortErrors() was called"
|
||||
);
|
||||
|
||||
// Assume assert() has been called so all error objects are the same.
|
||||
const { errorObject } = this.#errors[0];
|
||||
assert_throws_dom("AbortError", this.#domExceptionConstructor, () => { throw errorObject; });
|
||||
}
|
||||
|
||||
assertErrorsAre(expectedErrorObject) {
|
||||
assert_greater_than(
|
||||
this.#errors.length,
|
||||
0,
|
||||
"No errors were recorded but assertErrorsAre() was called"
|
||||
);
|
||||
|
||||
// Assume assert() has been called so all error objects are the same.
|
||||
const { errorObject } = this.#errors[0];
|
||||
assert_equals(errorObject, expectedErrorObject);
|
||||
}
|
||||
}
|
||||
@@ -1362,7 +1362,7 @@ fn wpt_fixture_content_type(path: &StdPath, fixture_path: &str) -> Option<&'stat
|
||||
Some("css") => Some("text/css; charset=utf-8"),
|
||||
Some("htm") => Some("text/html; charset=utf-8"),
|
||||
Some("html") => Some("text/html; charset=utf-8"),
|
||||
Some("js") => Some("application/javascript"),
|
||||
Some("js" | "mjs") => Some("application/javascript"),
|
||||
Some("json") => Some("application/json"),
|
||||
Some("txt") => Some("text/plain; charset=utf-8"),
|
||||
Some("xml") => Some("application/xml"),
|
||||
|
||||
Reference in New Issue
Block a user