LibWeb: Keep stale child history tasks runnable

Child navigables can lose document-associated apply-history tasks when
a parent page replaces or destroys the child document. Queue child tasks
with no document association so they remain runnable, and share that
choice for both activation and update-only continuations. Keep top-level
work associated with the active document to preserve initial about:blank
Window reuse.

Also abandon a queued child fetch if its parent document is already gone
before reading the parent's relevant settings object. This matches
browser behavior for detached frame navigations and avoids resuming
stale work against a discarded parent.

The Twinings menu to Black Tea to Earl Grey product flow now reaches the
product main content under Ladybird WebDriver. Existing navigation
coverage and the full LibWeb text suite cover the local history cases.
This commit is contained in:
Andreas Kling 2026-06-15 14:27:28 +02:00 committed by Andreas Kling
parent b06955277a
commit 2d9db6c1f8
2 changed files with 39 additions and 7 deletions

View file

@ -1189,7 +1189,18 @@ static void perform_navigation_params_fetch(JS::Realm& realm, GC::Ref<Navigation
// 3. If navigable is not a top-level traversable, then:
if (!state_holder->navigable->is_top_level_traversable()) {
// 1. Let parentEnvironment be navigable's parent's active document's relevant settings object.
auto& parent_environment = state_holder->navigable->parent()->active_document()->relevant_settings_object();
auto parent = state_holder->navigable->parent();
auto parent_document = parent ? parent->active_document() : nullptr;
if (!parent || parent->has_been_destroyed() || !parent_document || parent_document->has_been_destroyed()) {
// AD-HOC: A queued child navigation can resume after its parent document has been destroyed. The
// specification assumes the parent environment is still available here, but browser engines
// abandon this stale detached frame navigation instead of continuing it against a discarded
// parent.
state_holder->response = Fetch::Infrastructure::Response::network_error(realm.vm(), "Parent document is no longer active"_string);
fetch_completion_steps->function()();
return;
}
auto& parent_environment = parent_document->relevant_settings_object();
// 2. Set topLevelCreationURL to parentEnvironment's top-level creation URL.
top_level_creation_url = parent_environment.top_level_creation_url;

View file

@ -740,6 +740,19 @@ struct ChangingNavigableContinuationState : public JS::Cell {
GC_DEFINE_ALLOCATOR(ChangingNavigableContinuationState);
static void queue_apply_history_step_task(GC::Ref<Navigable> navigable, GC::Ptr<DOM::Document> top_level_document, GC::Ref<GC::Function<void()>> steps)
{
// AD-HOC: Queue top-level tasks with the active Document instead of using queue_global_task(active_window).
// During initial about:blank Window reuse, active_window()->associated_document() can already be the
// pending Document, but the apply-history task must run against the current active Document.
//
// Child navigables can destroy or deactivate their active Document before the queued task runs, causing
// document-associated tasks to be dropped. Queue child tasks with a null Document so the task remains
// runnable, and revalidate the child navigable inside the task.
auto task_document = navigable->is_top_level_traversable() ? top_level_document : GC::Ptr<DOM::Document> {};
queue_a_task(Task::Source::NavigationAndTraversal, nullptr, task_document, steps);
}
class ApplyHistoryStepState : public GC::Cell {
GC_CELL(ApplyHistoryStepState, GC::Cell);
GC_DECLARE_ALLOCATOR(ApplyHistoryStepState);
@ -947,12 +960,9 @@ void ApplyHistoryStepState::start()
signal_progress();
continue;
}
// AD-HOC: Queue with navigable's active Document instead of using queue_global_task(active_window).
// During initial about:blank Window reuse, active_window()->associated_document() can already be
// the pending Document. This activation task must stay runnable against the current active Document.
queue_a_task(Task::Source::NavigationAndTraversal, nullptr, navigable->active_document(), GC::create_function(heap(), [this, navigable] {
queue_apply_history_step_task(*navigable, navigable->active_document(), GC::create_function(heap(), [this, navigable] {
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
if (navigable->has_been_destroyed()) {
if (navigable->has_been_destroyed() || !navigable->active_window() || !navigable->active_document()) {
++m_completed_change_jobs;
signal_progress();
return;
@ -1235,6 +1245,17 @@ void ApplyHistoryStepState::process_continuations()
RefPtr<SessionHistoryEntry> const target_entry = continuation->target_entry;
auto const displayed_document_id = continuation->displayed_document_id;
auto after_potential_unload = GC::create_function(heap(), [this, navigable, update_only, target_entry, continuation, population_output, old_origin, displayed_document_id, script_history_length, script_history_index, entries_for_navigation_api = move(entries_for_navigation_api), navigation_type = m_navigation_type] {
if (update_only || continuation->resolved_document.ptr() == continuation->displayed_document.ptr()) {
// AD-HOC: Child navigable same-document/update-only tasks are queued without an associated Document so
// they can survive the old active Document being deactivated. That also lets them run after the
// child frame was destroyed or after a newer navigation claimed the frame. Browser engines let
// the newer frame state win, so skip this stale continuation in that case.
if (!changing_navigable_is_still_current(navigable, displayed_document_id)) {
complete_change_job_without_applying(navigable);
return;
}
}
if (population_output)
population_output->apply_to(*target_entry);
@ -1306,7 +1327,7 @@ void ApplyHistoryStepState::process_continuations()
navigable->set_ongoing_navigation({}, m_navigation_api_abort_behavior);
// 2. Queue a global task on the navigation and traversal task source given navigable's active window to perform afterPotentialUnloads.
queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), after_potential_unload);
queue_apply_history_step_task(*navigable, navigable->active_document(), after_potential_unload);
}
// AD-HOC: During navigable creation, the initial about:blank document can be
// replaced by the container's initial navigation while applying the