Branch: refs/heads/main
Home: https://github.com/WebKit/WebKit
Commit: 04c8cebb8429ca6d158eb0555dfbabe22cab17fb
https://github.com/WebKit/WebKit/commit/04c8cebb8429ca6d158eb0555dfbabe22cab17fb
Author: Basuke Suzuki <[email protected]>
Date: 2026-06-16 (Tue, 16 Jun 2026)
Changed paths:
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_1-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_4-expected.txt
M
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_5-expected.txt
M Source/WebCore/loader/NavigationScheduler.cpp
M Source/WebCore/loader/NavigationScheduler.h
M Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm
Log Message:
-----------
[NavigationScheduler] history.back/forward/go(n) calls don't coalesce per
HTML spec when queued synchronously
https://bugs.webkit.org/show_bug.cgi?id=317077
rdar://179574582
Reviewed by Charlie Wolfe.
Per HTML's "traverse the history by a delta" algorithm, each call to
history.back(), history.forward(), or history.go(n) queues a task on the
top-level traversable's session history traversal queue. When multiple
are queued synchronously before any task has run, intermediate document
loads are skipped and only the net delta determines the destination —
back();forward(); goes nowhere; back();back(); traverses -2 with one
load event; forward();forward(); traverses +2 with one load event. The
queue is keyed by the top-level traversable, not by the calling frame,
so an iframe's history.back() and the main frame's history.forward()
queued in the same task must coalesce as well.
WebKit's NavigationScheduler is per-frame and single-slot: m_redirect
holds one pending ScheduledNavigation, and scheduleHistoryNavigation()
calls schedule(), which in turn calls cancel() before installing the
new ScheduledHistoryNavigation. The second history call therefore
evicted the first instead of being merged with it, and a traversal
queued on a subframe was not visible to a traversal queued on the main
frame. Observable: synchronous back();forward(); from N+1 fired forward,
loaded N+2, and triggered a load event the spec says should not occur.
Have ScheduledHistoryNavigation accumulate the steps of a subsequent
history.go() into its pending m_steps via a new AccumulateResult enum +
accumulateHistorySteps virtual hook on the ScheduledNavigation base. If
the accumulated delta is zero, NavigationScheduler::scheduleHistoryNavigation
cancels the pending navigation entirely (matching the spec's "going
nowhere"); otherwise the existing scheduled navigation is left in place
with the updated step count, and the cached target HistoryItem is
dropped so the subsequent fire() recomputes against the new delta.
history.go(0) is a reload per spec rather than a delta-traversal, so
accumulateHistorySteps returns NotHandled for an additionalSteps of 0
and falls through to the existing schedule() path, preserving reload
semantics. Same-document traversals (hash/pushState) must observably
fire each hashchange/popstate event individually, so they also fall
through to the existing schedule() path. Non-history scheduled
navigations return AccumulateResult::NotHandled and fall through to the
existing schedule() path unchanged.
To bring the coalescing scope to the top-level traversable as the spec
requires, scheduleHistoryNavigation forwards to the top frame's
NavigationScheduler when the top frame is a LocalFrame in this WebProcess.
The forward path threads the originating frame through a new
scheduleHistoryNavigation(Frame& originatingFrame, int) overload so that
the same-document check inside accumulateHistorySteps still runs at the
calling frame's scope — an iframe's cross-document traversal can look
like a same-document traversal from the main frame's perspective and
would otherwise fall out of accumulation. Two side effects that
schedule() normally performs on the calling frame are reproduced on the
originating frame before forwarding: any in-progress load is completed
via FrameLoader::completed(), and any existing m_redirect on the
originating scheduler is cancelled. Without those, an iframe's pending
ScheduledLocationChange (e.g. iframe.location.search="?1") races the
queued traversal at the top frame and produces an extra load event.
adjustPendingHistoryNavigationForNewBackForwardEntry, called by
HistoryController when a fragment scroll or pushState appends a new
back-forward entry, also forwards to the top frame so the queued
traversal's m_steps is adjusted at the same scope where it lives. This
preserves the spec's "calculate at queue time" semantics: a fragment
navigation appended on a subframe after a history.back() was queued
shifts the queued delta so the traversal still reaches its original
target across the newly inserted entry. Once forwarded, this frame's
own m_redirect (if any) belongs to a different code path (e.g. a
ScheduledLocationChange) and isn't the queued traversal we need to
adjust here, so the function returns after forwarding.
history.go(0) (reload) is left on the per-frame path; spec-compliant
top-level reload semantics are out of scope for this change. Cross-
process top frames (Site Isolation, when a cross-site iframe initiates
the traversal) need UIProcess-side queueing for the session history
traversal queue and are tracked in a follow-up bug (TBD).
The fix is in WebProcess scheduling and works regardless of routing
mode; the new tests run with both UseUIProcessForBackForwardItemLoading
off (WebProcess-local goToItem path) and on (UIProcess-routed
dispatchGoToBackForwardItemAtIndex path). Each scenario asserts the
spec's load-event count via a TestNavigationDelegate-tracked
didFinishNavigation counter — 0 for "going nowhere", exactly 1 for
"-2/+2 with one load event" to verify intermediate documents are
skipped — in addition to back-forward list state.
IframeAndMainFrameCoalesceGoesNowhere covers the new in-process
cross-frame coalescing: an iframe's history.back() and the main frame's
history.forward() queued synchronously coalesce to "go nowhere".
IframeBackThenPushStateAdjustsAggregatedTraversal exercises
adjustPendingHistoryNavigationForNewBackForwardEntry's forward to the
top frame: an iframe's queued history.back() followed synchronously by
a pushState that inserts a new entry shifts the aggregated traversal's
delta so it lands on the entry the spec computed at queue time
(loadableURL1 across -2 entries instead of loadableURL3's neighbour at
-1). HistoryGoZeroAfterPendingBackReloads now asserts a finishCount of
0 to pin the observed behavior of history.back();history.go(0); — the
go(0) overwrites the pending back() but the per-frame scheduler folds
the reload into the cancelled back() so no navigation finishes; the
spec-correct outcome (a single reload) requires the same top-level
reload work that history.go(0) is intentionally left out of here, and
is tracked separately.
Updates affected WPT baselines in
imported/w3c/web-platform-tests/html/browsers/{browsing-the-web/overlapping-navigations-and-traversals,history/the-history-interface}/
to match the new actual outcomes. Net WPT change vs. the prior coalesce
attempt:
- cross-document-traversal-cross-document-traversal: opposite-direction
go-nowhere subtest moves from FAIL to PASS, and the same-direction
forward subtest moves from FAIL to PASS once aggregation takes effect.
The same-direction back subtest still FAILs but the failure mode
shifts (got "?2" → got ""): it depends on a separate iframe BFLock
routing fix tracked under bug 317145, not on this coalescing change.
- cross-document-traversal-cross-document-nav: both same-document and
cross-document traversal-vs-nav subtests move from FAIL to PASS once
the queue scope is the top-level traversable.
- traverse_the_history_{1,4,5}: still FAIL with the same assertion but
the got values shift (e.g. [4, 3] → [4, 1]). Spec-correct PASS would
require the queue to land cleanly on the original-position target
rather than the post-adjust position, which the per-frame scheduler
approximation here cannot represent fully.
- same-document-traversal-same-document-traversal-{hashchange,pushstate}:
the "queued up" opposite-direction subtests degrade from FAIL+TIMEOUT
to TIMEOUT+NOTRUN. Per the spec, two SDV traversals queued together
should fire two hashchange/popstate events; the single-slot m_redirect
in the originating scheduler can only retain one, so the harness times
out waiting for the second event. Tracked separately; the cross-
document improvements above are the net win and the SDV TIMEOUT path
was already failing.
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-nav-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-hashchange-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/browsing-the-web/overlapping-navigations-and-traversals/same-document-traversal-same-document-traversal-pushstate-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_1-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_4-expected.txt:
*
LayoutTests/imported/w3c/web-platform-tests/html/browsers/history/the-history-interface/traverse_the_history_5-expected.txt:
* Source/WebCore/loader/NavigationScheduler.cpp:
(WebCore::ScheduledNavigation::accumulateHistorySteps):
(WebCore::ScheduledHistoryNavigation::accumulateHistorySteps):
(WebCore::NavigationScheduler::scheduleHistoryNavigation):
(WebCore::NavigationScheduler::adjustPendingHistoryNavigationForNewBackForwardEntry):
* Source/WebCore/loader/NavigationScheduler.h:
* Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKBackForwardListTests.mm:
(TEST(WKBackForwardList, SynchronousBackForwardGoesNowhere)):
(TEST(WKBackForwardList,
SynchronousBackForwardGoesNowhereWithUIProcessLoading)):
(TEST(WKBackForwardList, SynchronousBackBackTraversesByMinusTwo)):
(TEST(WKBackForwardList,
SynchronousBackBackTraversesByMinusTwoWithUIProcessLoading)):
(TEST(WKBackForwardList, SynchronousForwardForwardTraversesByPlusTwo)):
(TEST(WKBackForwardList,
SynchronousForwardForwardTraversesByPlusTwoWithUIProcessLoading)):
(TEST(WKBackForwardList, HistoryGoZeroAfterPendingBackReloads)):
(TEST(WKBackForwardList,
HistoryGoZeroAfterPendingBackReloadsWithUIProcessLoading)):
(TEST(WKBackForwardList, IframeAndMainFrameCoalesceGoesNowhere)):
(TEST(WKBackForwardList,
IframeAndMainFrameCoalesceGoesNowhereWithUIProcessLoading)):
(TEST(WKBackForwardList, IframeBackThenPushStateAdjustsAggregatedTraversal)):
(TEST(WKBackForwardList,
IframeBackThenPushStateAdjustsAggregatedTraversalWithUIProcessLoading)):
Canonical link: https://commits.webkit.org/315300@main
To unsubscribe from these emails, change your notification settings at
https://github.com/WebKit/WebKit/settings/notifications