Treat pending session history entries as absent from the used step graph, and share that through a small step_value() helper so snapshotting, Navigation API entry construction, target-entry lookup, and forward clearing do not drift apart. Keep cross-document history application tied to the navigation id that created it. Queued changing-navigable work now finishes without applying when a later navigation has already replaced its target, and any traversal sentinel is cleared through the shared setter so queued navigations can drain. When navigation arrives while traversal is still ongoing, keep only the newest pending navigation. This matches Chromium, WebKit, and Gecko on sites that click through product or category links while prior loads settle. Revalidate queued same-document child continuations before running them from null-document tasks, so removed frames or frames claimed by newer navigations do not receive stale history state. Preserve nested-history descriptors even when all child entries are pending, keeping live child navigable identity available for later UI process history seeds. Add regression coverage for iframe renavigation during history commit, for pending child history followed by a real navigation, and for removed iframes with queued history updates.
68 lines
2.7 KiB
C++
68 lines
2.7 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibJS/Runtime/VM.h>
|
|
#include <LibTest/TestCase.h>
|
|
#include <LibURL/Parser.h>
|
|
#include <LibWeb/HTML/SessionHistoryEntry.h>
|
|
|
|
static URL::URL parse_url(StringView url)
|
|
{
|
|
auto parsed_url = URL::Parser::basic_parse(url);
|
|
VERIFY(parsed_url.has_value());
|
|
return parsed_url.release_value();
|
|
}
|
|
|
|
TEST_CASE(post_load_seed_match_allows_ui_owned_nested_histories)
|
|
{
|
|
Web::HTML::SessionHistoryEntryDescriptor live_descriptor;
|
|
live_descriptor.step = 0;
|
|
live_descriptor.url = parse_url("https://a.example/"sv);
|
|
live_descriptor.document_state.id = 1;
|
|
live_descriptor.document_state.ever_populated = true;
|
|
live_descriptor.document_state.navigable_target_name = MUST(String::from_utf8("main"sv));
|
|
|
|
auto seed_descriptor = live_descriptor;
|
|
|
|
Web::HTML::SessionHistoryEntryDescriptor nested_entry;
|
|
nested_entry.step = 1;
|
|
nested_entry.url = parse_url("https://frame.example/"sv);
|
|
seed_descriptor.document_state.nested_histories.append({
|
|
.id = MUST(String::from_utf8("frame-1"sv)),
|
|
.entries = { move(nested_entry) },
|
|
});
|
|
|
|
EXPECT(!Web::HTML::session_history_entry_descriptors_match_ignoring_document_state_id(live_descriptor, seed_descriptor));
|
|
EXPECT(Web::HTML::session_history_entry_descriptors_match_ignoring_document_state_id(live_descriptor, seed_descriptor, Web::HTML::MatchNestedHistories::No));
|
|
}
|
|
|
|
TEST_CASE(descriptor_creation_preserves_nested_history_with_only_pending_entries)
|
|
{
|
|
auto vm = JS::VM::create();
|
|
|
|
auto top_level_document_state = Web::HTML::DocumentState::create();
|
|
|
|
auto pending_child_entry = Web::HTML::SessionHistoryEntry::create();
|
|
pending_child_entry->set_url(parse_url("https://frame.example/pending"sv));
|
|
pending_child_entry->set_document_state(Web::HTML::DocumentState::create());
|
|
|
|
top_level_document_state->nested_histories().append({
|
|
.id = MUST(String::from_utf8("frame-1"sv)),
|
|
.entries = { pending_child_entry },
|
|
});
|
|
|
|
auto top_level_entry = Web::HTML::SessionHistoryEntry::create();
|
|
top_level_entry->set_step(0);
|
|
top_level_entry->set_url(parse_url("https://a.example/"sv));
|
|
top_level_entry->set_document_state(top_level_document_state);
|
|
|
|
Web::HTML::SessionHistoryEntryDescriptorCreationState creation_state;
|
|
auto descriptor = Web::HTML::create_session_history_entry_descriptor(top_level_entry, creation_state);
|
|
|
|
EXPECT_EQ(descriptor.document_state.nested_histories.size(), 1u);
|
|
EXPECT_EQ(descriptor.document_state.nested_histories[0].id, "frame-1"sv);
|
|
EXPECT(descriptor.document_state.nested_histories[0].entries.is_empty());
|
|
}
|