LibWeb: Add an Internals hook to interrupt a navigation with a traversal

clobberNextNavigationWithATraversal() arms a one-shot that, on the next
call to Navigable::begin_navigation, re-stamps the navigable’s ongoing
navigation with a synthetic session-history traversal during the unload
check, then clears it on a later turn — draining deferred navigations.

This lets a single-process test deterministically reproduce a race
between a cross-document navigation and a concurrent traversal, which
otherwise only surfaces under scheduling jitter in a multi-process run.
This commit is contained in:
sideshowbarker 2026-06-16 13:28:27 +09:00 committed by Andreas Kling
parent 4ecfd0903f
commit 85a4f2633c
5 changed files with 34 additions and 0 deletions

View file

@ -1990,6 +1990,15 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
// and an optional boolean initialInsertion (default false):
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
// Test-only: armed via Internals.clobberNextNavigationWithATraversal(). Consumed by the next call to begin_navigation,
// which simulates a concurrent session-history traversal interrupting the unload check.
static bool s_clobber_next_navigation_with_a_traversal = false;
void Navigable::clobber_next_navigation_with_a_traversal_for_testing()
{
s_clobber_next_navigation_with_a_traversal = true;
}
void Navigable::begin_navigation(NavigateParams params)
{
// AD-HOC: Not in the spec but we should not navigate a navigable that has been destroyed.
@ -2213,6 +2222,17 @@ void Navigable::begin_navigation(NavigateParams params)
return;
}
// Test-only (Internals.clobberNextNavigationWithATraversal): re-stamp our ongoing navigation with a
// synthetic traversal now, and clear it on a later turn (which drains deferred navigations). This
// deterministically reproduces the race the guard below must survive.
if (s_clobber_next_navigation_with_a_traversal) {
s_clobber_next_navigation_with_a_traversal = false;
set_ongoing_navigation(Traversal::Tag, NavigationAPIAbortBehavior::Preserve);
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this] {
set_ongoing_navigation(Empty {}, NavigationAPIAbortBehavior::Preserve);
}));
}
if (ongoing_navigation() != navigation_id) {
set_delaying_load_events(false);
return;

View file

@ -152,6 +152,10 @@ public:
Variant<Empty, Traversal, String> ongoing_navigation() const { return m_ongoing_navigation; }
void set_ongoing_navigation(Variant<Empty, Traversal, String> ongoing_navigation, NavigationAPIAbortBehavior = NavigationAPIAbortBehavior::Abort);
// Test-only (Internals.clobberNextNavigationWithATraversal): make the next navigation's unload check be interrupted
// by a synthetic session-history traversal that re-stamps the ongoing navigation.
static void clobber_next_navigation_with_a_traversal_for_testing();
void populate_session_history_entry_document(
URL::URL url,
Variant<Empty, String, POSTResource> document_resource,

View file

@ -299,6 +299,11 @@ void Internals::commit_text()
page().handle_keydown(UIEvents::Key_Return, 0, 0x0d, false, true);
}
void Internals::clobber_next_navigation_with_a_traversal()
{
HTML::Navigable::clobber_next_navigation_with_a_traversal_for_testing();
}
UIEvents::MouseButton Internals::button_from_unsigned_short(WebIDL::UnsignedShort button)
{
switch (button) {

View file

@ -113,6 +113,7 @@ public:
String dump_session_history();
String dump_ui_process_session_history();
GC::Ref<WebIDL::Promise> flush_session_history_traversal_queue();
void clobber_next_navigation_with_a_traversal();
GC::Ptr<DOM::ShadowRoot> get_shadow_root(GC::Ref<DOM::Element>);

View file

@ -100,6 +100,10 @@ interface Internals {
DOMString dumpUIProcessSessionHistory();
Promise<undefined> flushSessionHistoryTraversalQueue();
// Test-only: deterministically reproduce the navigation-vs-traversal race in Navigable::begin_navigation by
// re-stamping the next navigation's ongoing navigation with a traversal during its unload check.
undefined clobberNextNavigationWithATraversal();
// Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS.
ShadowRoot? getShadowRoot(Element element);