commit 87c14abb1108fe5c19344d2939887bfdf5d9f83b
parent 9e9e587de3c54154f9c3d7ffc6f3f2ee48ed253e
Author: Jan-Niklas Jaeschke <jjaschke@mozilla.com>
Date: Tue, 28 Oct 2025 17:18:26 +0000
Bug 1970123, part 7 - Navigation API: Refactor #commit-a-navigate-event to follow the current version of the spec. r=dom-core,smaug
This patch refactors #inner-navigate-firing to follow the updated spec.
This mostly means moving existing code into the newly added #commit-a-navigate-event helper.
This patch does not yet include the actual precommit handler stuff,
which is implemented in the next patch of the patchset.
Differential Revision: https://phabricator.services.mozilla.com/D270184
Diffstat:
1 file changed, 268 insertions(+), 219 deletions(-)
diff --git a/dom/navigation/Navigation.cpp b/dom/navigation/Navigation.cpp
@@ -1155,19 +1155,36 @@ nsresult Navigation::FireErrorEvent(const nsAString& aName,
return rv.StealNSResult();
}
+// https://html.spec.whatwg.org/#resume-applying-the-traverse-history-step
+static void ResumeApplyTheHistoryStep(
+ SessionHistoryInfo* aTarget, BrowsingContext* aTraversable,
+ UserNavigationInvolvement aUserInvolvement) {
+ MOZ_DIAGNOSTIC_ASSERT(aTraversable->IsTop());
+ auto* childSHistory = aTraversable->GetChildSessionHistory();
+ // Since we've already called #checking-if-unloading-is-canceled, we here pass
+ // checkForCancelation set to false.
+ childSHistory->AsyncGo(aTarget->NavigationKey(), aTraversable,
+ /* aRequireUserInteraction */ false,
+ /* aUserActivation */ false,
+ /* aCheckForCancelation */ false, [](auto) {});
+}
+
struct NavigationWaitForAllScope final : public nsISupports,
public SupportsWeakPtr {
NavigationWaitForAllScope(Navigation* aNavigation,
NavigationAPIMethodTracker* aApiMethodTracker,
- NavigateEvent* aEvent)
+ NavigateEvent* aEvent,
+ NavigationDestination* aDestination)
: mNavigation(aNavigation),
mAPIMethodTracker(aApiMethodTracker),
- mEvent(aEvent) {}
+ mEvent(aEvent),
+ mDestination(aDestination) {}
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(NavigationWaitForAllScope)
RefPtr<Navigation> mNavigation;
RefPtr<NavigationAPIMethodTracker> mAPIMethodTracker;
RefPtr<NavigateEvent> mEvent;
+ RefPtr<NavigationDestination> mDestination;
private:
~NavigationWaitForAllScope() {}
@@ -1210,10 +1227,247 @@ struct NavigationWaitForAllScope final : public nsISupports,
navigation->AbortNavigateEvent(jsapi.cx(), event, aRejectionReason);
}
}
+ // https://html.spec.whatwg.org/#commit-a-navigate-event
+ MOZ_CAN_RUN_SCRIPT void CommitNavigateEvent(NavigationType aNavigationType) {
+ // 1. Let navigation be event's target.
+ // Omitted since Navigation is part of this's state.
+
+ // 3. If event's relevant global object's associated Document is not fully
+ // active, then return.
+ RefPtr document = mEvent->GetDocument();
+ if (!document || !document->IsFullyActive()) {
+ return;
+ }
+ // 2. Let navigable be event's relevant global object's navigable.
+ nsDocShell* docShell = nsDocShell::Cast(document->GetDocShell());
+ Maybe<BrowsingContext&> navigable =
+ ToMaybeRef(mNavigation->GetOwnerWindow()).andThen([](auto& aWindow) {
+ return ToMaybeRef(aWindow.GetBrowsingContext());
+ });
+ // 4. If event's abort controller's signal is aborted, then return.
+ if (AbortSignal* signal = mEvent->Signal(); signal->Aborted()) {
+ return;
+ }
+
+ // 6. Let endResultIsSameDocument be true if event's interception state is
+ // not "none" or event's destination's is same document is true.
+ const bool endResultIsSameDocument =
+ mEvent->InterceptionState() != NavigateEvent::InterceptionState::None ||
+ mDestination->SameDocument();
+
+ // 7. Prepare to run script given navigation's relevant settings object.
+ // This runs step 12 when going out of scope.
+ nsAutoMicroTask mt;
+
+ // 9. If event's interception state is not "none":
+ if (mEvent->InterceptionState() != NavigateEvent::InterceptionState::None) {
+ // 5. Set event's interception state to "committed".
+ // See https://github.com/whatwg/html/issues/11830 for this change.
+ mEvent->SetInterceptionState(NavigateEvent::InterceptionState::Committed);
+ // 9.1 Switch on event's navigationType:
+ switch (aNavigationType) {
+ case NavigationType::Push:
+ case NavigationType::Replace:
+ // Run the URL and history update steps given event's relevant
+ // global object's associated Document and event's destination's
+ // URL, with serializedData set to event's classic history API
+ // state and historyHandling set to event's navigationType.
+ if (docShell) {
+ docShell->UpdateURLAndHistory(
+ document, mDestination->GetURL(),
+ mEvent->ClassicHistoryAPIState(),
+ *NavigationUtils::NavigationHistoryBehavior(aNavigationType),
+ document->GetDocumentURI(),
+ Equals(mDestination->GetURL(), document->GetDocumentURI()));
+ }
+ break;
+ case NavigationType::Reload:
+ // Update the navigation API entries for a same-document navigation
+ // given navigation, navigable's active session history entry, and
+ // "reload".
+ if (docShell) {
+ mNavigation->UpdateEntriesForSameDocumentNavigation(
+ docShell->GetActiveSessionHistoryInfo(), aNavigationType);
+ }
+ break;
+ case NavigationType::Traverse:
+ if (auto* entry = mDestination->GetEntry()) {
+ // 1. Set navigation's suppress normal scroll restoration during
+ // ongoing navigation to true.
+ mNavigation
+ ->mSuppressNormalScrollRestorationDuringOngoingNavigation =
+ true;
+ // 2. Let userInvolvement be "none".
+ // 3. If event's userInitiated is true, then set userInvolvement to
+ // "activation".
+ UserNavigationInvolvement userInvolvement =
+ mEvent->UserInitiated() ? UserNavigationInvolvement::Activation
+ : UserNavigationInvolvement::None;
+ // 4. Append the following session history traversal steps to
+ // navigable's traversable navigable:
+ // 4.1 Resume applying the traverse history step given event's
+ // destination's entry's session history entry's step,
+ // navigable's traversable navigable, and userInvolvement.
+ ResumeApplyTheHistoryStep(entry->SessionHistoryInfo(),
+ navigable->Top(), userInvolvement);
+
+ // This is not in the spec, but both Chrome and Safari does this or
+ // something similar.
+ MOZ_ASSERT(entry->Index() >= 0);
+ mNavigation->SetCurrentEntryIndex(entry->SessionHistoryInfo());
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ // 8. If navigation's transition is not null, then resolve navigation's
+ // transition's committed promise with undefined.
+ // Steps 8 and 9 are swapped to have a consistent promise behavior
+ // (see https://github.com/whatwg/html/issues/11842)
+ if (mNavigation->mTransition) {
+ mNavigation->mTransition->Committed()->MaybeResolveWithUndefined();
+ }
+
+ // 10. If endResultIsSameDocument is true:
+ if (endResultIsSameDocument) {
+ // 10.1 Let promisesList be an empty list.
+ AutoTArray<RefPtr<Promise>, 16> promiseList;
+ // 10.2 For each handler of event's navigation handler list:
+ for (auto& handler : mEvent->NavigationHandlerList().Clone()) {
+ // 10.2.1 Append the result of invoking handler with an empty
+ // arguments list to promisesList.
+ RefPtr promise = MOZ_KnownLive(handler)->Call();
+ if (promise) {
+ promiseList.AppendElement(promise);
+ }
+ }
+ // 10.3 If promisesList's size is 0, then set promisesList to « a promise
+ // resolved with undefined ».
+ nsCOMPtr globalObject = mNavigation->GetOwnerGlobal();
+ if (promiseList.IsEmpty()) {
+ RefPtr promise = Promise::CreateResolvedWithUndefined(
+ globalObject, IgnoredErrorResult());
+ if (promise) {
+ promiseList.AppendElement(promise);
+ }
+ }
+
+ // 10.4 Wait for all of promisesList, with the following success steps:
+
+ // If the committed promise in the api method tracker hasn't resolved yet,
+ // we can't run neither of the success nor failure steps. To handle that
+ // we set up a callback for when that resolves. This differs from how spec
+ // performs these steps, since spec can perform more of
+ // #apply-the-history-steps in a synchronous way.
+ auto cancelSteps =
+ [weakScope = WeakPtr(this)](JS::Handle<JS::Value> aRejectionReason)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
+ // If weakScope is null we've been cycle collected
+ if (weakScope) {
+ RefPtr scope = weakScope.get();
+ scope->ProcessNavigateEventHandlerFailure(aRejectionReason);
+ }
+ };
+ auto successSteps =
+ [weakScope = WeakPtr(this)](const Span<JS::Heap<JS::Value>>&)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
+ // If weakScope is null we've been cycle collected
+ if (weakScope) {
+ RefPtr scope = weakScope.get();
+ scope->CommitNavigateEventSuccessSteps();
+ }
+ };
+ if (mAPIMethodTracker) {
+ LOG_FMTD("Waiting for committed");
+ mAPIMethodTracker->CommittedPromise()
+ ->AddCallbacksWithCycleCollectedArgs(
+ [successSteps, cancelSteps](
+ JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ nsIGlobalObject* aGlobalObject,
+ const Span<RefPtr<Promise>>& aPromiseList,
+ NavigationWaitForAllScope* aScope)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
+ Promise::WaitForAll(aGlobalObject, aPromiseList,
+ successSteps, cancelSteps, aScope);
+ },
+ [](JSContext*, JS::Handle<JS::Value>, ErrorResult&,
+ nsIGlobalObject*, const Span<RefPtr<Promise>>&,
+ NavigationWaitForAllScope*) {},
+ nsCOMPtr(globalObject),
+ nsTArray<RefPtr<Promise>>(std::move(promiseList)),
+ RefPtr<NavigationWaitForAllScope>(this));
+ } else {
+ LOG_FMTD("No API method tracker, not waiting for committed");
+ // If we don't have an apiMethodTracker we can immediately start waiting
+ // for the promise list.
+ Promise::WaitForAll(globalObject, promiseList, successSteps,
+ cancelSteps, this);
+ }
+ } else if (mAPIMethodTracker && mNavigation->mOngoingAPIMethodTracker) {
+ // In contrast to spec we add a check that we're still the ongoing
+ // tracker. If we're not, then we've already been cleaned up.
+ MOZ_DIAGNOSTIC_ASSERT(mAPIMethodTracker ==
+ mNavigation->mOngoingAPIMethodTracker);
+ // Step 11
+ mAPIMethodTracker->CleanUp();
+ } else {
+ // It needs to be ensured that the ongoing navigate event is cleared in
+ // every code path (e.g. for download events), so that we don't keep
+ // intermediate state around.
+ // See also https://github.com/whatwg/html/issues/11802
+ mNavigation->mOngoingNavigateEvent = nullptr;
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT void CommitNavigateEventSuccessSteps() {
+ LogEvent(mEvent, mEvent, "Success"_ns);
+
+ // 1. If event's relevant global object is not fully active, then abort
+ // these steps.
+ RefPtr document = mEvent->GetDocument();
+ if (!document || !document->IsFullyActive()) {
+ return;
+ }
+
+ // 2. If event's abort controller's signal is aborted, then abort these
+ // steps.
+ if (AbortSignal* signal = mEvent->Signal(); signal->Aborted()) {
+ return;
+ }
+
+ // 3. Assert: event equals navigation's ongoing navigate event.
+ MOZ_DIAGNOSTIC_ASSERT(mEvent == mNavigation->mOngoingNavigateEvent);
+
+ // 4. Set navigation's ongoing navigate event to null.
+ mNavigation->mOngoingNavigateEvent = nullptr;
+
+ // 5. Finish event given true.
+ RefPtr event = mEvent;
+ event->Finish(true);
+
+ // 6. If apiMethodTracker is non-null, then resolve the finished promise for
+ // apiMethodTracker.
+ if (mAPIMethodTracker) {
+ mAPIMethodTracker->ResolveFinishedPromise();
+ }
+
+ // 7. Fire an event named navigatesuccess at navigation.
+ RefPtr navigation = mNavigation;
+ navigation->FireEvent(u"navigatesuccess"_ns);
+
+ // 8. If navigation's transition is not null, then resolve navigation's
+ // transition's finished promise with undefined.
+ if (mNavigation->mTransition) {
+ mNavigation->mTransition->Finished()->MaybeResolveWithUndefined();
+ }
+ // 9. Set navigation's transition to null.
+ mNavigation->mTransition = nullptr;
+ }
};
NS_IMPL_CYCLE_COLLECTION_WEAK_PTR(NavigationWaitForAllScope, mNavigation,
- mAPIMethodTracker, mEvent)
+ mAPIMethodTracker, mEvent, mDestination)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NavigationWaitForAllScope)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
@@ -1221,20 +1475,6 @@ NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(NavigationWaitForAllScope)
NS_IMPL_CYCLE_COLLECTING_RELEASE(NavigationWaitForAllScope)
-// https://html.spec.whatwg.org/#resume-applying-the-traverse-history-step
-static void ResumeApplyTheHistoryStep(
- SessionHistoryInfo* aTarget, BrowsingContext* aTraversable,
- UserNavigationInvolvement aUserInvolvement) {
- MOZ_DIAGNOSTIC_ASSERT(aTraversable->IsTop());
- auto* childSHistory = aTraversable->GetChildSessionHistory();
- // Since we've already called #checking-if-unloading-is-canceled, we here pass
- // checkForCancelation set to false.
- childSHistory->AsyncGo(aTarget->NavigationKey(), aTraversable,
- /* aRequireUserInteraction */ false,
- /* aUserActivation */ false,
- /* aCheckForCancelation */ false, [](auto) {});
-}
-
// https://html.spec.whatwg.org/#inner-navigate-event-firing-algorithm
bool Navigation::InnerFireNavigateEvent(
JSContext* aCx, NavigationType aNavigationType,
@@ -1397,224 +1637,33 @@ bool Navigation::InnerFireNavigateEvent(
return false;
}
- // After this point, the step numbers don't match the spec anymore because of
- // the rearrangement due to addition of precommit handlers. The step numbers
- // represent the order before that change.
-
- // Step 31
- bool endResultIsSameDocument =
- event->InterceptionState() != NavigateEvent::InterceptionState::None ||
- aDestination->SameDocument();
-
- // Step 32 (and the destructor of this is step 36)
- nsAutoMicroTask mt;
-
- // Step 33
+ // Step 29
if (event->InterceptionState() != NavigateEvent::InterceptionState::None) {
- // Step 33.1
- event->SetInterceptionState(NavigateEvent::InterceptionState::Committed);
-
- // Step 33.2
+ // Step 29.1
RefPtr<NavigationHistoryEntry> fromNHE = GetCurrentEntry();
- // Step 33.3
+ // Step 29.2
MOZ_DIAGNOSTIC_ASSERT(fromNHE);
- // Step 33.4
+ // Step 29.3
RefPtr<Promise> committedPromise = Promise::CreateInfallible(globalObject);
RefPtr<Promise> finishedPromise = Promise::CreateInfallible(globalObject);
mTransition = MakeAndAddRef<NavigationTransition>(
globalObject, aNavigationType, fromNHE, committedPromise,
finishedPromise);
- // Step 33.5
+ // Step 29.4
MOZ_ALWAYS_TRUE(committedPromise->SetAnyPromiseIsHandled());
+ // Step 29.5
MOZ_ALWAYS_TRUE(finishedPromise->SetAnyPromiseIsHandled());
-
- switch (aNavigationType) {
- case NavigationType::Traverse:
- // Step 33.6
- mSuppressNormalScrollRestorationDuringOngoingNavigation = true;
- // The following steps are from after the precommit handler re-write.
- // Numbering is a bit messed up, but will be fixed when precommit
- // handlers are implemented.
- // Step 32.7.1, case "traverse"
- if (auto* entry = aDestination->GetEntry()) {
- // 32.7.1.2
- UserNavigationInvolvement userInvolvement =
- UserNavigationInvolvement::None;
- // 32.7.1.3
- if (event->UserInitiated()) {
- userInvolvement = UserNavigationInvolvement::Activation;
- }
- // 32.7.1.4
- ResumeApplyTheHistoryStep(entry->SessionHistoryInfo(),
- navigable->Top(), userInvolvement);
-
- // This is not in the spec, but both Chrome and Safari does this or
- // something similar.
- MOZ_ASSERT(entry->Index() >= 0);
- mCurrentEntryIndex = Some(entry->Index());
- }
-
- break;
- case NavigationType::Push:
- case NavigationType::Replace:
- // Step 33.7
- if (nsDocShell* docShell = nsDocShell::Cast(document->GetDocShell())) {
- docShell->UpdateURLAndHistory(
- document, aDestination->GetURL(), event->ClassicHistoryAPIState(),
- *NavigationUtils::NavigationHistoryBehavior(aNavigationType),
- document->GetDocumentURI(),
- Equals(aDestination->GetURL(), document->GetDocumentURI()));
- }
- break;
- case NavigationType::Reload:
- // Step 33.8
- if (nsDocShell* docShell = nsDocShell::Cast(document->GetDocShell())) {
- UpdateEntriesForSameDocumentNavigation(
- docShell->GetActiveSessionHistoryInfo(), aNavigationType);
- }
- break;
- default:
- break;
- }
}
- // Step 34
- if (endResultIsSameDocument) {
- // Step 34.1
- AutoTArray<RefPtr<Promise>, 16> promiseList;
- // Step 34.2
- for (auto& handler : event->NavigationHandlerList().Clone()) {
- // Step 34.2.1
- RefPtr promise = MOZ_KnownLive(handler)->Call();
- if (promise) {
- promiseList.AppendElement(promise);
- }
- }
-
- // Step 34.3
- if (promiseList.IsEmpty()) {
- RefPtr promise = Promise::CreateResolvedWithUndefined(
- globalObject, IgnoredErrorResult());
- if (promise) {
- promiseList.AppendElement(promise);
- }
- }
-
- // Step 34.4
- // We capture the scope which we wish to keep alive in the lambdas passed to
- // Promise::WaitForAll. We pass it as the cycle collected argument to
- // Promise::WaitForAll, which makes it stay alive until all promises
- // resolved, or we've become cycle collected. This means that we can pass
- // the scope as a weak reference.
- RefPtr scope =
- MakeRefPtr<NavigationWaitForAllScope>(this, apiMethodTracker, event);
- auto successSteps =
- [weakScope = WeakPtr(scope)](const Span<JS::Heap<JS::Value>>&)
- MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
- // If weakScope is null we've been cycle collected
- if (!weakScope) {
- return;
- }
-
- RefPtr event = weakScope->mEvent;
- RefPtr self = weakScope->mNavigation;
- RefPtr apiMethodTracker = weakScope->mAPIMethodTracker;
- LogEvent(event, event, "Success"_ns);
-
- // Success steps
- // Step 1
- if (RefPtr document = event->GetDocument();
- !document || !document->IsFullyActive()) {
- return;
- }
-
- // Step 2
- if (AbortSignal* signal = event->Signal(); signal->Aborted()) {
- return;
- }
-
- // Step 3
- MOZ_DIAGNOSTIC_ASSERT(event == self->mOngoingNavigateEvent);
-
- // Step 4
- self->mOngoingNavigateEvent = nullptr;
-
- // Step 5
- event->Finish(true);
-
- // Step 6
- if (apiMethodTracker) {
- apiMethodTracker->ResolveFinishedPromise();
- }
-
- // Step 7
- self->FireEvent(u"navigatesuccess"_ns);
-
- // Step 8
- if (self->mTransition) {
- self->mTransition->Finished()->MaybeResolveWithUndefined();
- }
-
- // Step 9
- self->mTransition = nullptr;
- };
- auto failureSteps =
- [weakScope = WeakPtr(scope)](JS::Handle<JS::Value> aRejectionReason)
- MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
- // If weakScope is null we've been cycle collected
- if (!weakScope) {
- return;
- }
- weakScope->ProcessNavigateEventHandlerFailure(aRejectionReason);
- };
-
- // If the committed promise in the api method tracker hasn't resolved yet,
- // we can't run neither of the success nor failure steps. To handle that we
- // set up a callback for when that resolves. This differs from how spec
- // performs these steps, since spec can perform more of
- // #apply-the-history-steps in a synchronous way.
- if (apiMethodTracker) {
- LOG_FMTD("Waiting for committed");
- apiMethodTracker->CommittedPromise()->AddCallbacksWithCycleCollectedArgs(
- [successSteps, failureSteps](
- JSContext*, JS::Handle<JS::Value>, ErrorResult&,
- nsIGlobalObject* aGlobalObject,
- const Span<RefPtr<Promise>>& aPromiseList,
- const RefPtr<NavigationWaitForAllScope>& aScope)
- MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
- Promise::WaitForAll(aGlobalObject, aPromiseList, successSteps,
- failureSteps, aScope);
- },
- [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, nsIGlobalObject*,
- const Span<RefPtr<Promise>>&,
- const RefPtr<NavigationWaitForAllScope>&) {},
- nsCOMPtr(globalObject),
- nsTArray<RefPtr<Promise>>(std::move(promiseList)), scope);
- } else {
- LOG_FMTD("No API method tracker, not waiting for committed");
- // If we don't have an apiMethodTracker we can immediately start waiting
- // for the promise list.
- Promise::WaitForAll(globalObject, promiseList, successSteps, failureSteps,
- scope);
- }
- } else if (apiMethodTracker && mOngoingAPIMethodTracker) {
- // In contrast to spec we add a check that we're still the ongoing tracker.
- // If we're not, then we've already been cleaned up.
- MOZ_DIAGNOSTIC_ASSERT(apiMethodTracker == mOngoingAPIMethodTracker);
- // Step 35
- apiMethodTracker->CleanUp();
- } else {
- // It needs to be ensured that the ongoing navigate event is cleared in
- // every code path (e.g. for download events), so that we don't keep
- // intermediate state around.
- // See also https://github.com/whatwg/html/issues/11802
- mOngoingNavigateEvent = nullptr;
- }
+ RefPtr scope = MakeRefPtr<NavigationWaitForAllScope>(this, apiMethodTracker,
+ event, aDestination);
+ // Step 30
+ scope->CommitNavigateEvent(aNavigationType);
- // Step 37 and step 38
+ // Step 32 and 33
return event->InterceptionState() == NavigateEvent::InterceptionState::None;
}