LibWeb: Let newer navigations win history races

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.
This commit is contained in:
Andreas Kling 2026-06-15 18:26:07 +02:00 committed by Andreas Kling
parent 2d9db6c1f8
commit 394312ab5a
16 changed files with 435 additions and 118 deletions

View file

@ -10,6 +10,7 @@
#include <AK/RefCounted.h>
#include <LibURL/Origin.h>
#include <LibURL/URL.h>
#include <LibWeb/Export.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/POSTResource.h>
@ -19,7 +20,7 @@
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#document-state-2
class DocumentState final : public RefCounted<DocumentState> {
class WEB_API DocumentState final : public RefCounted<DocumentState> {
public:
static NonnullRefPtr<DocumentState> create() { return adopt_ref(*new DocumentState()); }
~DocumentState();

View file

@ -488,9 +488,11 @@ RefPtr<SessionHistoryEntry> Navigable::get_the_target_history_entry(int target_s
// 2. Return the item in entries that has the greatest step less than or equal to step.
RefPtr<SessionHistoryEntry> result = nullptr;
for (auto& entry : entries) {
auto entry_step = entry->step().get<int>();
if (entry_step <= target_step) {
if (!result || result->step().get<int>() < entry_step) {
// NB: "pending" is not a used history step.
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-step
auto entry_step = entry->step_value();
if (entry_step.has_value() && *entry_step <= target_step) {
if (!result || *result->step_value() < *entry_step) {
result = entry;
}
}
@ -744,6 +746,13 @@ void Navigable::set_ongoing_navigation(Variant<Empty, Traversal, String> ongoing
}
}
void Navigable::queue_pending_navigation(NavigateParams params, PendingNavigationBehavior behavior)
{
if (behavior == PendingNavigationBehavior::Replace)
m_pending_navigations.clear();
m_pending_navigations.append(move(params));
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#the-rules-for-choosing-a-navigable
Navigable::ChosenNavigable Navigable::choose_a_navigable(StringView name, TokenizedFeature::NoOpener no_opener, ActivateTab activate_tab, Optional<TokenizedFeature::Map const&> window_features)
{
@ -1961,7 +1970,7 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
}
if (!m_has_session_history_entry_and_ready_for_navigation) {
m_pending_navigations.append(move(params));
queue_pending_navigation(move(params), PendingNavigationBehavior::Append);
return {};
}
@ -2112,11 +2121,10 @@ void Navigable::begin_navigation(NavigateParams params)
// FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id
// is navigationId, status is "canceled", and url is url.
// AD-HOC: Instead of canceling the navigation (per spec step 18.2), defer it until the traversal completes.
// This prevents a race condition where page_did_finish_loading is sent to the client before the session
// history traversal from finalize_a_cross_document_navigation completes. If the client sends a new
// navigation before the traversal finishes, it would be dropped, causing the page to appear stuck.
m_pending_navigations.append(move(params));
// AD-HOC: The HTML Standard cancels a navigation that starts while a traversal is ongoing. We defer it
// instead so UI-initiated navigations that race the tail end of a previous load are not dropped.
// Match Chromium, WebKit, and Gecko's observable behavior by letting the newest navigation win.
queue_pending_navigation(move(params), PendingNavigationBehavior::Replace);
// 2. Return.
return;
@ -2379,7 +2387,7 @@ void Navigable::begin_navigation(NavigateParams params)
signal->resolve({});
return;
}
finalize_a_cross_document_navigation(*this, to_history_handling_behavior(history_handling), user_involvement, history_entry, pending_document, GC::create_function(heap(), [signal](HistoryStepResult) {
finalize_a_cross_document_navigation(*this, to_history_handling_behavior(history_handling), user_involvement, history_entry, pending_document, navigation_id, GC::create_function(heap(), [signal](HistoryStepResult) {
signal->resolve({});
}));
}));
@ -2688,7 +2696,7 @@ void Navigable::navigate_to_a_javascript_url(URL::URL const& url, HistoryHandlin
// 14. Append session history traversal steps to targetNavigable's traversable to finalize a cross-document navigation with targetNavigable, historyHandling, userInvolvement, and historyEntry.
traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, new_document, history_entry, history_handling, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) {
finalize_a_cross_document_navigation(*this, history_handling, user_involvement, history_entry, new_document, GC::create_function(heap(), [signal](HistoryStepResult) {
finalize_a_cross_document_navigation(*this, history_handling, user_involvement, history_entry, new_document, {}, GC::create_function(heap(), [signal](HistoryStepResult) {
signal->resolve({});
}));
}));
@ -2832,7 +2840,7 @@ TargetSnapshotParams Navigable::snapshot_target_snapshot_params()
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#finalize-a-cross-document-navigation
void finalize_a_cross_document_navigation(GC::Ref<Navigable> navigable, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, NonnullRefPtr<SessionHistoryEntry> history_entry, GC::Ptr<DOM::Document> pending_document, GC::Ref<OnApplyHistoryStepComplete> on_complete)
void finalize_a_cross_document_navigation(GC::Ref<Navigable> navigable, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, NonnullRefPtr<SessionHistoryEntry> history_entry, GC::Ptr<DOM::Document> pending_document, Optional<String> expected_ongoing_navigation_id, GC::Ref<OnApplyHistoryStepComplete> on_complete)
{
// NOTE: This is not in the spec but we should not navigate destroyed navigable.
if (navigable->has_been_destroyed()) {
@ -2965,7 +2973,7 @@ void finalize_a_cross_document_navigation(GC::Ref<Navigable> navigable, HistoryH
}
// 10. Apply the push/replace history step targetStep to traversable given historyHandling and userInvolvement.
traversable->apply_the_push_or_replace_history_step(target_step, history_handling, user_involvement, TraversableNavigable::SynchronousNavigation::No, pending_document,
traversable->apply_the_push_or_replace_history_step(target_step, history_handling, user_involvement, TraversableNavigable::SynchronousNavigation::No, pending_document, navigable, move(expected_ongoing_navigation_id),
GC::create_function(navigable->heap(), [on_complete, navigable](HistoryStepResult result) {
// AD-HOC: Trigger a relayout in the container document for size negotiation with SVG documents.
if (auto container = navigable->container())

View file

@ -300,7 +300,13 @@ protected:
Variant<Empty, Traversal, String> m_ongoing_navigation;
private:
enum class PendingNavigationBehavior {
Append,
Replace
};
void begin_navigation(NavigateParams);
void queue_pending_navigation(NavigateParams, PendingNavigationBehavior);
void navigate_to_a_fragment(URL::URL const&, HistoryHandlingBehavior, UserNavigationInvolvement, GC::Ptr<DOM::Element> source_element, Optional<SerializationRecord> navigation_api_state, String navigation_id);
void navigate_to_a_javascript_url(URL::URL const&, HistoryHandlingBehavior, GC::Ref<SourceSnapshotParams>, URL::Origin const& initiator_origin, UserNavigationInvolvement, ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type, InitialInsertion, String navigation_id);
@ -411,7 +417,7 @@ private:
WEB_API HashTable<GC::RawRef<Navigable>>& all_navigables();
bool navigation_must_be_a_replace(URL::URL const& url, DOM::Document const& document);
void finalize_a_cross_document_navigation(GC::Ref<Navigable>, HistoryHandlingBehavior, UserNavigationInvolvement, NonnullRefPtr<SessionHistoryEntry>, GC::Ptr<DOM::Document> pending_document, GC::Ref<OnApplyHistoryStepComplete> on_complete);
void finalize_a_cross_document_navigation(GC::Ref<Navigable>, HistoryHandlingBehavior, UserNavigationInvolvement, NonnullRefPtr<SessionHistoryEntry>, GC::Ptr<DOM::Document> pending_document, Optional<String> expected_ongoing_navigation_id, GC::Ref<OnApplyHistoryStepComplete> on_complete);
void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_url, Optional<SerializationRecord> = {}, HistoryHandlingBehavior history_handling = HistoryHandlingBehavior::Replace);
}

View file

@ -1324,16 +1324,21 @@ bool Navigation::inner_navigate_event_firing_algorithm(
}
}
auto const defer_commit_handler_steps_until_same_document_entry_update = end_result_is_same_document
auto const event_was_intercepted = event->interception_state() != NavigateEvent::InterceptionState::None;
auto const same_document_entry_update_will_run_commit_handler_steps = end_result_is_same_document
&& (navigation_type == Bindings::NavigationType::Traverse
|| navigation_type == Bindings::NavigationType::Push
|| navigation_type == Bindings::NavigationType::Replace);
|| (!event_was_intercepted
&& (navigation_type == Bindings::NavigationType::Push
|| navigation_type == Bindings::NavigationType::Replace)));
if (end_result_is_same_document) {
// NB: Same-document Navigation API entry updates run these steps after currententrychange.
// If those steps have not started here, run them as a fallback for same-document paths
// that did not update entries.
if (!defer_commit_handler_steps_until_same_document_entry_update && !event->has_started_navigate_event_intercept_commit_handler_steps())
// If those steps have not started here, run them as a fallback for same-document paths that did not update
// entries. Intercepted push/replace navigations have already attempted their same-document entry update
// above, so keep this fallback enabled in case the navigate event handler detached its document and caused
// that update to return early due to entries and events being disabled.
if (!same_document_entry_update_will_run_commit_handler_steps
&& !event->has_started_navigate_event_intercept_commit_handler_steps())
run_the_navigate_event_intercept_commit_handler_steps(event, api_method_tracker);
}

View file

@ -32,6 +32,78 @@ SessionHistoryEntry::SessionHistoryEntry()
{
}
static u64 document_state_id_for_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
if (auto id = creation_state.document_state_ids.get(&document_state); id.has_value())
return *id;
auto id = creation_state.next_document_state_id++;
VERIFY(id != 0);
creation_state.document_state_ids.set(&document_state, id);
return id;
}
static SessionHistoryDocumentStateDescriptor create_session_history_document_state_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
Vector<SessionHistoryNestedHistoryDescriptor> nested_history_descriptors;
nested_history_descriptors.ensure_capacity(document_state.nested_histories().size());
for (auto const& nested_history : document_state.nested_histories()) {
Vector<SessionHistoryEntryDescriptor> nested_entry_descriptors;
nested_entry_descriptors.ensure_capacity(nested_history.entries.size());
for (auto const& nested_entry : nested_history.entries) {
// NB: UI-process session history mirrors only concrete used history steps. A child entry whose step is
// still "pending" has not been attached to the traversable's step graph yet.
if (!nested_entry->step_value().has_value())
continue;
nested_entry_descriptors.unchecked_append(create_session_history_entry_descriptor(nested_entry, creation_state));
}
// NB: Keep the nested-history descriptor even when every entry in it is still pending. The entries are not
// used history steps yet, but the descriptor id preserves the live child navigable identity when the UI
// process later reseeds an already-loaded document.
nested_history_descriptors.unchecked_append({
.id = nested_history.id,
.entries = move(nested_entry_descriptors),
});
}
return {
.id = document_state_id_for_descriptor(document_state, creation_state),
.history_policy_container = document_state.history_policy_container(),
.request_referrer = document_state.request_referrer(),
.request_referrer_policy = document_state.request_referrer_policy(),
.initiator_origin = document_state.initiator_origin(),
.origin = document_state.origin(),
.about_base_url = document_state.about_base_url(),
.resource = document_state.resource(),
.reload_pending = document_state.reload_pending(),
.ever_populated = document_state.ever_populated(),
.navigable_target_name = document_state.navigable_target_name(),
.nested_histories = move(nested_history_descriptors),
};
}
SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptorCreationState& creation_state)
{
auto entry_step = entry.step_value();
VERIFY(entry_step.has_value());
SessionHistoryDocumentStateDescriptor document_state_descriptor;
if (auto document_state = entry.document_state())
document_state_descriptor = create_session_history_document_state_descriptor(*document_state, creation_state);
return {
.step = static_cast<i32>(*entry_step),
.url = entry.url(),
.document_state = move(document_state_descriptor),
.classic_history_api_state = entry.classic_history_api_state(),
.navigation_api_state = entry.navigation_api_state(),
.navigation_api_key = entry.navigation_api_key(),
.navigation_api_id = entry.navigation_api_id(),
.scroll_restoration_mode = entry.scroll_restoration_mode(),
.scroll_position_data = entry.scroll_position_data(),
};
}
static bool session_history_nested_history_descriptors_match(Vector<SessionHistoryNestedHistoryDescriptor> const& a, Vector<SessionHistoryNestedHistoryDescriptor> const& b);
static bool serialized_directives_match(Vector<ContentSecurityPolicy::Directives::SerializedDirective> const& a, Vector<ContentSecurityPolicy::Directives::SerializedDirective> const& b)
@ -250,7 +322,8 @@ static bool session_history_document_state_descriptor_matches_document_state_ign
bool session_history_entry_matches_descriptor_ignoring_document_state_id(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptor const& descriptor, MatchNestedHistories match_nested_histories)
{
if (!entry.step().has<int>() || entry.step().get<int>() != descriptor.step)
auto entry_step = entry.step_value();
if (!entry_step.has_value() || *entry_step != descriptor.step)
return false;
if (entry.url() != descriptor.url)
return false;

View file

@ -6,6 +6,8 @@
#pragma once
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
@ -85,7 +87,7 @@ struct SessionHistoryNestedHistoryDescriptor {
};
// https://html.spec.whatwg.org/multipage/history.html#session-history-entry
class SessionHistoryEntry final : public RefCounted<SessionHistoryEntry> {
class WEB_API SessionHistoryEntry final : public RefCounted<SessionHistoryEntry> {
public:
static NonnullRefPtr<SessionHistoryEntry> create();
@ -98,6 +100,12 @@ public:
[[nodiscard]] Variant<int, Pending> step() const { return m_step; }
void set_step(Variant<int, Pending> step) { m_step = step; }
[[nodiscard]] Optional<int> step_value() const
{
if (auto const* step = m_step.get_pointer<int>())
return *step;
return {};
}
[[nodiscard]] URL::URL const& url() const { return m_url; }
void set_url(URL::URL url) { m_url = move(url); }
@ -164,6 +172,12 @@ private:
// NOTE: This is where we could remember the state of form controls, for example.
};
struct SessionHistoryEntryDescriptorCreationState {
HashMap<DocumentState const*, u64> document_state_ids;
u64 next_document_state_id { 1 };
};
WEB_API SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const&, SessionHistoryEntryDescriptorCreationState&);
WEB_API bool session_history_entry_descriptors_match(SessionHistoryEntryDescriptor const&, SessionHistoryEntryDescriptor const&);
enum class MatchNestedHistories {
Yes,

View file

@ -330,6 +330,15 @@ static bool synchronous_same_document_navigation_must_preserve_ongoing_navigatio
return navigable.ongoing_navigation().has<String>();
}
static bool expected_ongoing_navigation_was_superseded(GC::Ptr<Navigable> navigable, Optional<String> const& expected_navigation_id)
{
if (!navigable || !expected_navigation_id.has_value())
return false;
if (navigable->has_been_destroyed())
return true;
return navigable->ongoing_navigation() != *expected_navigation_id;
}
bool TraversableNavigable::replace_top_level_session_history_entries_from_ui_process(Vector<SessionHistoryEntryDescriptor> entries_from_ui_process, size_t current_top_level_entry_index, bool allow_reconstructing_current_entry)
{
if (entries_from_ui_process.is_empty() || current_top_level_entry_index >= entries_from_ui_process.size())
@ -504,7 +513,13 @@ Vector<int> TraversableNavigable::get_all_used_history_steps() const
// 1. For each entry of entryList:
for (auto& entry : entry_list) {
// 1. Append entry's step to steps.
steps.set(entry->step().get<int>());
// NB: "pending" is not a used history step.
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-step
if (auto entry_step = entry->step_value(); entry_step.has_value()) {
steps.set(*entry_step);
} else {
continue;
}
// 2. For each nestedHistory of entry's document state's nested histories, append nestedHistory's entries list to entryLists.
for (auto& nested_history : entry->document_state()->nested_histories())
@ -575,6 +590,8 @@ Vector<GC::Root<Navigable>> TraversableNavigable::get_all_navigables_whose_curre
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
if (!target_entry)
continue;
// 2. If targetEntry is not navigable's current session history entry or targetEntry's document state's reload
// pending is true, then append navigable to results.
@ -613,6 +630,8 @@ Vector<GC::Root<Navigable>> TraversableNavigable::get_all_navigables_that_only_n
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
if (!target_entry)
continue;
// 2. If targetEntry is navigable's current session history entry and targetEntry's document state's reload pending is false, then:
if (target_entry == navigable->current_session_history_entry() && !target_entry->document_state()->reload_pending()) {
@ -650,6 +669,8 @@ Vector<GC::Root<Navigable>> TraversableNavigable::get_all_navigables_that_might_
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
if (!target_entry)
continue;
// 2. If targetEntry's document is not navigable's document or targetEntry's document state's reload pending is true, then append navigable to results.
// NOTE: Although navigable's active history entry can change synchronously, the new entry will always have the same Document,
@ -768,6 +789,8 @@ public:
TraversableNavigable::SynchronousNavigation synchronous_navigation,
Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior,
GC::Ptr<DOM::Document> pending_document,
GC::Ptr<Navigable> expected_ongoing_navigation_navigable,
Optional<String> expected_ongoing_navigation_id,
GC::Ref<OnApplyHistoryStepComplete> on_complete)
: m_traversable(traversable)
, m_step(step)
@ -778,6 +801,8 @@ public:
, m_synchronous_navigation(synchronous_navigation)
, m_navigation_api_abort_behavior(navigation_api_abort_behavior)
, m_pending_document(pending_document)
, m_expected_ongoing_navigation_navigable(expected_ongoing_navigation_navigable)
, m_expected_ongoing_navigation_id(move(expected_ongoing_navigation_id))
, m_on_complete(on_complete)
, m_timeout(Platform::Timer::create_single_shot(heap(), TIMEOUT_MS, GC::create_function(heap(), [this] {
if (m_phase != Phase::Completed) {
@ -837,6 +862,7 @@ private:
visitor.visit(m_traversable);
visitor.visit(m_source_snapshot_params);
visitor.visit(m_pending_document);
visitor.visit(m_expected_ongoing_navigation_navigable);
visitor.visit(m_on_complete);
visitor.visit(m_timeout);
visitor.visit(m_changing_navigables);
@ -874,6 +900,11 @@ private:
void process_continuations();
void enter_waiting_for_non_changing_jobs();
void complete();
void finish_without_applying();
void complete_change_job_without_applying(GC::Ptr<Navigable>);
bool changing_navigable_is_still_current(GC::Ptr<Navigable>, Optional<UniqueNodeID> expected_active_document_id) const;
void clear_ongoing_traversal_for_changing_navigable(GC::Ptr<Navigable>);
void clear_ongoing_traversals_for_changing_navigables();
Phase m_phase { Phase::WaitingForDocumentPopulation };
GC::Ref<TraversableNavigable> m_traversable;
@ -885,6 +916,8 @@ private:
TraversableNavigable::SynchronousNavigation m_synchronous_navigation;
Navigable::NavigationAPIAbortBehavior m_navigation_api_abort_behavior;
GC::Ptr<DOM::Document> m_pending_document;
GC::Ptr<Navigable> m_expected_ongoing_navigation_navigable;
Optional<String> m_expected_ongoing_navigation_id;
GC::Ptr<OnApplyHistoryStepComplete> m_on_complete;
GC::Ref<Platform::Timer> m_timeout;
@ -905,6 +938,15 @@ GC_DEFINE_ALLOCATOR(ApplyHistoryStepState);
void ApplyHistoryStepState::start()
{
if (expected_ongoing_navigation_was_superseded(m_expected_ongoing_navigation_navigable, m_expected_ongoing_navigation_id)) {
// NB: A cross-document navigation can be superseded after its document has populated but before its queued
// history-step application runs. The navigate algorithm's earlier navigation ID check caught the same
// condition before appending these steps; this re-check keeps a stale finalization from claiming
// "traversal" and canceling the newer navigation.
finish_without_applying();
return;
}
// 7. Let nonchangingNavigablesThatStillNeedUpdates be the result of getting all navigables that only need history object length/index update given traversable and targetStep.
auto non_changing_navigables = m_traversable->get_all_navigables_that_only_need_history_object_length_index_update(m_target_step);
for (auto& nav : non_changing_navigables)
@ -927,6 +969,8 @@ void ApplyHistoryStepState::start()
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(m_target_step);
if (!target_entry)
continue;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-traverse-navigate-event
// NB: Same-document traversals are synchronous in browser engines, but the specification routes them through
@ -956,15 +1000,14 @@ void ApplyHistoryStepState::start()
// task, because Document::destroy() removes tasks associated with a document from
// the task queue, which can cause those tasks to never run.
if (navigable->has_been_destroyed() || !navigable->active_window()) {
++m_completed_change_jobs;
signal_progress();
complete_change_job_without_applying(navigable);
continue;
}
queue_apply_history_step_task(*navigable, navigable->active_document(), GC::create_function(heap(), [this, navigable] {
auto expected_target_entry = navigable->current_session_history_entry();
queue_apply_history_step_task(*navigable, navigable->active_document(), GC::create_function(heap(), [this, navigable, expected_target_entry] {
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
if (navigable->has_been_destroyed() || !navigable->active_window() || !navigable->active_document()) {
++m_completed_change_jobs;
signal_progress();
complete_change_job_without_applying(navigable);
return;
}
@ -973,6 +1016,25 @@ void ApplyHistoryStepState::start()
// 2. Let targetEntry be navigable's current session history entry.
auto target_entry = navigable->current_session_history_entry();
if (!target_entry || target_entry != expected_target_entry) {
// AD-HOC: The HTML Standard expects the session history traversal queue to serialize this task with
// later navigations. Our web-compatible deferral of navigations that arrive during traversal
// can let a newer navigation replace the current entry before this task runs. Treat this state
// as stale instead of applying its old target step after the newer navigation.
finish_without_applying();
return;
}
auto displayed_step = displayed_entry ? displayed_entry->step_value() : Optional<int> {};
auto target_step = target_entry ? target_entry->step_value() : Optional<int> {};
if (!displayed_step.has_value() || !target_step.has_value()) {
// NB: Child navigables created during a busy top-level navigation can still have a pending initial
// session history entry. The spec's step-based history algorithms operate on used history steps,
// so a pending child entry must not block or crash the top-level apply-history step. The queued
// child creation/destruction history step will reconcile the child once it has a concrete step.
complete_change_job_without_applying(navigable);
return;
}
// 3. Let changingNavigableContinuation be a changing navigable continuation state with:
auto changing_navigable_continuation = heap().allocate<ChangingNavigableContinuationState>();
@ -1021,8 +1083,14 @@ void ApplyHistoryStepState::start()
case Bindings::NavigationType::Push:
// FIXME: Add ever populated check, and fix the bug where top level traversable's step is not updated when a child navigable navigates
// - "push": Assert: targetEntry's step is displayedEntry's step + 1 and targetEntry's document state's ever populated is false.
if (*target_step <= *displayed_step) {
// AD-HOC: A queued push can become stale if a later navigation commits before this task runs.
// Browser engines let the later navigation win; do the same and avoid moving the
// traversable's current step back to this push target during completion.
finish_without_applying();
return;
}
VERIFY(target_entry != displayed_entry);
VERIFY(target_entry->step().get<int>() > displayed_entry->step().get<int>());
break;
}
}
@ -1393,76 +1461,6 @@ void ApplyHistoryStepState::enter_waiting_for_non_changing_jobs()
try_advance();
}
struct SessionHistoryEntryDescriptorCreationState {
HashMap<DocumentState const*, u64> document_state_ids;
u64 next_document_state_id { 1 };
};
static u64 document_state_id_for_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
if (auto id = creation_state.document_state_ids.get(&document_state); id.has_value())
return *id;
auto id = creation_state.next_document_state_id++;
VERIFY(id != 0);
creation_state.document_state_ids.set(&document_state, id);
return id;
}
static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const&, SessionHistoryEntryDescriptorCreationState&);
static SessionHistoryDocumentStateDescriptor create_session_history_document_state_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state)
{
Vector<SessionHistoryNestedHistoryDescriptor> nested_history_descriptors;
nested_history_descriptors.ensure_capacity(document_state.nested_histories().size());
for (auto const& nested_history : document_state.nested_histories()) {
Vector<SessionHistoryEntryDescriptor> nested_entry_descriptors;
nested_entry_descriptors.ensure_capacity(nested_history.entries.size());
for (auto const& nested_entry : nested_history.entries)
nested_entry_descriptors.unchecked_append(create_session_history_entry_descriptor(nested_entry, creation_state));
nested_history_descriptors.unchecked_append({
.id = nested_history.id,
.entries = move(nested_entry_descriptors),
});
}
return {
.id = document_state_id_for_descriptor(document_state, creation_state),
.history_policy_container = document_state.history_policy_container(),
.request_referrer = document_state.request_referrer(),
.request_referrer_policy = document_state.request_referrer_policy(),
.initiator_origin = document_state.initiator_origin(),
.origin = document_state.origin(),
.about_base_url = document_state.about_base_url(),
.resource = document_state.resource(),
.reload_pending = document_state.reload_pending(),
.ever_populated = document_state.ever_populated(),
.navigable_target_name = document_state.navigable_target_name(),
.nested_histories = move(nested_history_descriptors),
};
}
static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptorCreationState& creation_state)
{
SessionHistoryDocumentStateDescriptor document_state_descriptor;
if (auto document_state = entry.document_state()) {
document_state_descriptor = create_session_history_document_state_descriptor(*document_state, creation_state);
}
return {
.step = static_cast<i32>(entry.step().get<int>()),
.url = entry.url(),
.document_state = move(document_state_descriptor),
.classic_history_api_state = entry.classic_history_api_state(),
.navigation_api_state = entry.navigation_api_state(),
.navigation_api_key = entry.navigation_api_key(),
.navigation_api_id = entry.navigation_api_id(),
.scroll_restoration_mode = entry.scroll_restoration_mode(),
.scroll_position_data = entry.scroll_position_data(),
};
}
TraversableNavigable::SessionHistorySnapshot TraversableNavigable::create_session_history_snapshot(SaveActiveEntryPersistedState save_active_entry_persisted_state)
{
if (save_active_entry_persisted_state == SaveActiveEntryPersistedState::Yes)
@ -1552,6 +1550,69 @@ void ApplyHistoryStepState::complete()
m_on_complete->function()(HistoryStepResult::Applied);
}
void ApplyHistoryStepState::finish_without_applying()
{
if (m_phase == Phase::Completed)
return;
m_phase = Phase::Completed;
m_timeout->stop();
clear_ongoing_traversals_for_changing_navigables();
m_traversable->m_apply_history_step_state = nullptr;
if (m_on_complete)
m_on_complete->function()(HistoryStepResult::Applied);
}
void ApplyHistoryStepState::complete_change_job_without_applying(GC::Ptr<Navigable> navigable)
{
if (m_phase == Phase::Completed)
return;
clear_ongoing_traversal_for_changing_navigable(navigable);
// NB: During document population, signal_progress() only advances the state
// machine. Later phases let it own the per-change-job accounting.
if (m_phase == Phase::WaitingForDocumentPopulation)
++m_completed_change_jobs;
signal_progress();
}
bool ApplyHistoryStepState::changing_navigable_is_still_current(GC::Ptr<Navigable> navigable, Optional<UniqueNodeID> expected_active_document_id) const
{
if (!navigable || navigable->has_been_destroyed() || !navigable->active_window())
return false;
auto active_document = navigable->active_document();
if (!active_document || active_document->has_been_destroyed())
return false;
if (navigable->active_document_id() != expected_active_document_id)
return false;
return navigable->ongoing_navigation().has<Empty>();
}
void ApplyHistoryStepState::clear_ongoing_traversal_for_changing_navigable(GC::Ptr<Navigable> navigable)
{
if (!navigable || navigable->has_been_destroyed())
return;
if (!navigable->ongoing_navigation().has<Navigable::Traversal>())
return;
// AD-HOC: The HTML Standard's traversal queue normally reaches one of the per-navigable "Set the ongoing
// navigation for navigable to null" steps before this state completes. Our stale-task exits deliberately
// skip the rest of the history step so newer navigations win like they do in Chromium, WebKit, and Gecko,
// but we still have to remove the traversal sentinel. Use the shared setter so pending navigations queued
// behind this traversal are drained in one place.
navigable->set_ongoing_navigation({}, m_navigation_api_abort_behavior);
}
void ApplyHistoryStepState::clear_ongoing_traversals_for_changing_navigables()
{
for (auto& navigable : m_changing_navigables)
clear_ongoing_traversal_for_changing_navigable(navigable);
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step
void TraversableNavigable::apply_the_history_step(
int step,
@ -1563,6 +1624,8 @@ void TraversableNavigable::apply_the_history_step(
SynchronousNavigation synchronous_navigation,
Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior,
GC::Ptr<DOM::Document> pending_document,
GC::Ptr<Navigable> expected_ongoing_navigation_navigable,
Optional<String> expected_ongoing_navigation_id,
GC::Ref<OnApplyHistoryStepComplete> on_complete)
{
// FIXME: 1. Assert: This is running within traversable's session history traversal queue.
@ -1570,7 +1633,7 @@ void TraversableNavigable::apply_the_history_step(
VERIFY(!m_apply_history_step_state || m_paused_apply_history_step_state);
run_the_history_step_prechecks(step, check_for_cancelation, source_snapshot_params, initiator_to_check, user_involvement, navigation_type, navigation_api_abort_behavior,
GC::create_function(heap(), [this, step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, pending_document, on_complete](HistoryStepResult result, int target_step, Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior) {
GC::create_function(heap(), [this, step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, pending_document, expected_ongoing_navigation_navigable, expected_ongoing_navigation_id = move(expected_ongoing_navigation_id), on_complete](HistoryStepResult result, int target_step, Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior) mutable {
if (result != HistoryStepResult::Applied) {
on_complete->function()(result);
return;
@ -1578,7 +1641,7 @@ void TraversableNavigable::apply_the_history_step(
// 6. Let changingNavigables be the result of get all navigables whose current session history entry will
// change or reload given traversable and targetStep.
apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete);
apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, expected_ongoing_navigation_navigable, move(expected_ongoing_navigation_id), on_complete);
}));
}
@ -1655,10 +1718,18 @@ void TraversableNavigable::apply_the_history_step_after_unload_check(
SynchronousNavigation synchronous_navigation,
Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior,
GC::Ptr<DOM::Document> pending_document,
GC::Ptr<Navigable> expected_ongoing_navigation_navigable,
Optional<String> expected_ongoing_navigation_id,
GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete)
{
if (expected_ongoing_navigation_was_superseded(expected_ongoing_navigation_navigable, expected_ongoing_navigation_id)) {
on_complete->function()(HistoryStepResult::Applied);
return;
}
auto state = heap().allocate<ApplyHistoryStepState>(*this, step, target_step, source_snapshot_params,
user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete);
user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document,
expected_ongoing_navigation_navigable, move(expected_ongoing_navigation_id), on_complete);
VERIFY(!m_apply_history_step_state || m_paused_apply_history_step_state);
m_apply_history_step_state = state;
@ -1913,16 +1984,20 @@ Vector<NonnullRefPtr<SessionHistoryEntry>> TraversableNavigable::get_session_his
// 3. Let startingIndex be the index of the session history entry in rawEntries who has the greatest step less than or equal to targetStep.
// FIXME: Use min/max_element algorithm or some such here
int starting_index = 0;
auto max_step = 0;
Optional<int> max_step;
Optional<int> maybe_starting_index;
for (auto i = 0u; i < raw_entries.size(); ++i) {
auto const& entry = raw_entries[i];
if (entry->step().has<int>()) {
auto step = entry->step().get<int>();
if (step <= target_step && step > max_step) {
if (auto step = entry->step_value(); step.has_value()) {
if (*step <= target_step && (!max_step.has_value() || *step > *max_step)) {
starting_index = static_cast<int>(i);
maybe_starting_index = starting_index;
max_step = *step;
}
}
}
if (!maybe_starting_index.has_value())
return {};
// 4. Append rawEntries[startingIndex] to entriesForNavigationAPI.
entries_for_navigation_api.append(raw_entries[starting_index]);
@ -1936,6 +2011,11 @@ Vector<NonnullRefPtr<SessionHistoryEntry>> TraversableNavigable::get_session_his
// 7. While i > 0:
while (i > 0) {
auto& entry = raw_entries[static_cast<unsigned>(i)];
if (!entry->step_value().has_value()) {
--i;
continue;
}
// 1. If rawEntries[i]'s document state's origin is not same origin with startingOrigin, then break.
auto entry_origin = entry->document_state()->origin();
if (starting_origin.has_value() && entry_origin.has_value() && !entry_origin->is_same_origin(*starting_origin))
@ -1954,6 +2034,11 @@ Vector<NonnullRefPtr<SessionHistoryEntry>> TraversableNavigable::get_session_his
// 9. While i < rawEntries's size:
while (i < static_cast<int>(raw_entries.size())) {
auto& entry = raw_entries[static_cast<unsigned>(i)];
if (!entry->step_value().has_value()) {
++i;
continue;
}
// 1. If rawEntries[i]'s document state's origin is not same origin with startingOrigin, then break.
auto entry_origin = entry->document_state()->origin();
if (starting_origin.has_value() && entry_origin.has_value() && !entry_origin->is_same_origin(*starting_origin))
@ -1988,11 +2073,17 @@ void TraversableNavigable::clear_the_forward_session_history()
// 1. Remove every session history entry from entryList that has a step greater than step.
entry_list.remove_all_matching([step](auto& entry) {
return entry->step().template get<int>() > step;
auto entry_step = entry->step_value();
return entry_step.has_value() && *entry_step > step;
});
// 2. For each entry of entryList:
for (auto& entry : entry_list) {
// NB: "pending" is not a used history step, so its nested histories
// are not part of the traversable's used step graph yet.
if (!entry->step_value().has_value())
continue;
// 1. For each nestedHistory of entry's document state's nested histories, append nestedHistory's entries list to entryLists.
for (auto& nested_history : entry->document_state()->nested_histories()) {
entry_lists.append(nested_history.entries);
@ -2088,7 +2179,10 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr<DOM:
if (source_snapshot_params) {
RefPtr<SessionHistoryEntry> target_top_level_entry;
for (auto const& entry : session_history_entries()) {
if (entry->step().template get<int>() > target_step)
auto entry_step = entry->step_value();
if (!entry_step.has_value())
continue;
if (*entry_step > target_step)
break;
target_top_level_entry = entry;
}
@ -2169,7 +2263,7 @@ void TraversableNavigable::update_for_navigable_creation_or_destruction(GC::Ref<
auto step = current_session_history_step();
// 2. Return the result of applying the history step to traversable given false, null, null, null, and null.
apply_the_history_step(step, false, {}, {}, UserNavigationInvolvement::None, {}, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, on_complete);
apply_the_history_step(step, false, {}, {}, UserNavigationInvolvement::None, {}, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, nullptr, {}, on_complete);
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-reload-history-step
@ -2179,7 +2273,7 @@ void TraversableNavigable::apply_the_reload_history_step(UserNavigationInvolveme
auto step = current_session_history_step();
// 2. Return the result of applying the history step step to traversable given true, null, null, null, and "reload".
apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr,
apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, nullptr, {},
GC::create_function(heap(), [this, on_complete](HistoryStepResult result) {
if (result != HistoryStepResult::Applied) {
// NB: A canceled reload must not keep treating the active
@ -2198,11 +2292,11 @@ void TraversableNavigable::apply_the_reload_history_step(UserNavigationInvolveme
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-push/replace-history-step
void TraversableNavigable::apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, SynchronousNavigation synchronous_navigation, GC::Ptr<DOM::Document> pending_document, GC::Ref<OnApplyHistoryStepComplete> on_complete)
void TraversableNavigable::apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, SynchronousNavigation synchronous_navigation, GC::Ptr<DOM::Document> pending_document, GC::Ptr<Navigable> expected_ongoing_navigation_navigable, Optional<String> expected_ongoing_navigation_id, GC::Ref<OnApplyHistoryStepComplete> on_complete)
{
// 1. Return the result of applying the history step step to traversable given false, null, null, userInvolvement, and historyHandling.
auto navigation_type = history_handling == HistoryHandlingBehavior::Replace ? Bindings::NavigationType::Replace : Bindings::NavigationType::Push;
apply_the_history_step(step, false, {}, {}, user_involvement, navigation_type, synchronous_navigation, Navigable::NavigationAPIAbortBehavior::Abort, pending_document, on_complete);
apply_the_history_step(step, false, {}, {}, user_involvement, navigation_type, synchronous_navigation, Navigable::NavigationAPIAbortBehavior::Abort, pending_document, expected_ongoing_navigation_navigable, move(expected_ongoing_navigation_id), on_complete);
}
static Optional<int> update_session_history_entries_for_same_document_navigation(TraversableNavigable& traversable, GC::Ref<Navigable> target_navigable, NonnullRefPtr<SessionHistoryEntry> target_entry, RefPtr<SessionHistoryEntry> entry_to_replace)
@ -2310,7 +2404,7 @@ bool TraversableNavigable::try_to_synchronously_commit_same_document_navigation(
void TraversableNavigable::apply_the_traverse_history_step(int step, GC::Ptr<SourceSnapshotParams> source_snapshot_params, GC::Ptr<Navigable> initiator_to_check, UserNavigationInvolvement user_involvement, GC::Ref<GC::Function<void(HistoryStepResult)>> on_complete)
{
// 1. Return the result of applying the history step step to traversable given true, sourceSnapshotParams, initiatorToCheck, userInvolvement, and "traverse".
apply_the_history_step(step, true, source_snapshot_params, initiator_to_check, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, on_complete);
apply_the_history_step(step, true, source_snapshot_params, initiator_to_check, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, nullptr, {}, on_complete);
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#resume-applying-the-traverse-history-step
@ -2324,7 +2418,7 @@ void TraversableNavigable::resume_applying_the_traverse_history_step(int step, U
// same-document traversal. Hence, we can pass false and null for those arguments.
// NB: The committed navigate event remains ongoing until the same-document entry update runs
// the navigate event intercept commit handler steps.
apply_the_history_step(step, false, {}, {}, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Preserve, nullptr, on_complete);
apply_the_history_step(step, false, {}, {}, user_involvement, Bindings::NavigationType::Traverse, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Preserve, nullptr, nullptr, {}, on_complete);
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#close-a-top-level-traversable
@ -2420,7 +2514,7 @@ void finalize_a_same_document_navigation(GC::Ref<TraversableNavigable> traversab
}
// 6. Apply the push/replace history step targetStep to traversable given historyHandling and userInvolvement.
traversable->apply_the_push_or_replace_history_step(*target_step, history_handling, user_involvement, TraversableNavigable::SynchronousNavigation::Yes, nullptr, on_complete);
traversable->apply_the_push_or_replace_history_step(*target_step, history_handling, user_involvement, TraversableNavigable::SynchronousNavigation::Yes, nullptr, nullptr, {}, on_complete);
}
// https://html.spec.whatwg.org/multipage/interaction.html#system-visibility-state

View file

@ -77,7 +77,7 @@ public:
No,
};
[[nodiscard]] bool try_to_synchronously_commit_same_document_navigation(GC::Ref<Navigable>, NonnullRefPtr<SessionHistoryEntry>, RefPtr<SessionHistoryEntry> entry_to_replace);
void apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement, SynchronousNavigation, GC::Ptr<DOM::Document> pending_document, GC::Ref<OnApplyHistoryStepComplete> on_complete);
void apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement, SynchronousNavigation, GC::Ptr<DOM::Document> pending_document, GC::Ptr<Navigable> expected_ongoing_navigation_navigable, Optional<String> expected_ongoing_navigation_id, GC::Ref<OnApplyHistoryStepComplete> on_complete);
void update_for_navigable_creation_or_destruction(GC::Ref<OnApplyHistoryStepComplete> on_complete);
int get_the_used_step(int step) const;
@ -154,6 +154,8 @@ private:
SynchronousNavigation,
Navigable::NavigationAPIAbortBehavior,
GC::Ptr<DOM::Document> pending_document,
GC::Ptr<Navigable> expected_ongoing_navigation_navigable,
Optional<String> expected_ongoing_navigation_id,
GC::Ref<OnApplyHistoryStepComplete> on_complete);
void apply_the_history_step_after_unload_check(
@ -165,6 +167,8 @@ private:
SynchronousNavigation,
Navigable::NavigationAPIAbortBehavior,
GC::Ptr<DOM::Document> pending_document,
GC::Ptr<Navigable> expected_ongoing_navigation_navigable,
Optional<String> expected_ongoing_navigation_id,
GC::Ref<OnApplyHistoryStepComplete> on_complete);
using OnHistoryStepPrechecksComplete = GC::Function<void(HistoryStepResult, int target_step, Navigable::NavigationAPIAbortBehavior)>;

View file

@ -34,7 +34,7 @@ target_link_libraries(TestFetchResponse PRIVATE LibGC LibHTTP LibJS)
target_link_libraries(TestFetchURL PRIVATE LibURL)
target_link_libraries(TestPage PRIVATE LibGC LibJS)
target_link_libraries(TestSecureContexts PRIVATE LibURL)
target_link_libraries(TestSessionHistoryEntry PRIVATE LibURL)
target_link_libraries(TestSessionHistoryEntry PRIVATE LibJS LibURL)
target_link_libraries(TestSourceHighlighter PRIVATE LibURL LibWebView)
if (NOT WIN32)

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/VM.h>
#include <LibTest/TestCase.h>
#include <LibURL/Parser.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
@ -37,3 +38,31 @@ TEST_CASE(post_load_seed_match_allows_ui_owned_nested_histories)
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());
}

View file

@ -1,2 +1,3 @@
#child
queue drained
navigation drained

View file

@ -3,6 +3,13 @@
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const server = httpTestServer();
const finalPageUrl = await server.createEcho("GET", "/iframe-pushstate-before-nested-history-ready-final", {
status: 200,
headers: { "Content-Type": "text/html" },
body: `<!DOCTYPE html><script>parent.postMessage("final page loaded", "*");<\/script>`,
});
const iframe = document.createElement("iframe");
document.body.append(iframe);
@ -11,6 +18,16 @@
await internals.flushSessionHistoryTraversalQueue();
println(iframe.contentWindow.location.hash);
println("queue drained");
const loaded = new Promise(resolve => {
window.addEventListener("message", event => {
if (event.source === iframe.contentWindow && event.data === "final page loaded")
resolve();
});
});
iframe.src = finalPageUrl;
await loaded;
println("navigation drained");
done();
});
</script>

View file

@ -0,0 +1,18 @@
<!doctype html>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const iframe = document.createElement("iframe");
document.body.append(iframe);
iframe.contentWindow.history.pushState({}, "", "about:blank#child");
await internals.flushSessionHistoryTraversalQueue();
iframe.contentWindow.history.back();
iframe.remove();
await internals.flushSessionHistoryTraversalQueue();
println("PASS");
done();
});
</script>

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const server = httpTestServer();
const finalPageUrl = await server.createEcho("GET", "/iframe-renavigate-during-history-commit-final", {
status: 200,
headers: { "Content-Type": "text/html" },
body: `<!DOCTYPE html><script>parent.postMessage({ page: "final", href: location.href }, "*");<\/script>`,
});
const intermediatePageUrl = await server.createEcho("GET", "/iframe-renavigate-during-history-commit-intermediate", {
status: 200,
headers: { "Content-Type": "text/html" },
body: `<!DOCTYPE html><script>parent.postMessage({ page: "intermediate" }, "*");<\/script>`,
});
const iframe = document.createElement("iframe");
document.body.append(iframe);
const result = await new Promise(resolve => {
window.addEventListener("message", async event => {
if (event.source !== iframe.contentWindow)
return;
if (event.data.page === "intermediate") {
iframe.src = finalPageUrl;
return;
}
if (event.data.page === "final") {
await internals.flushSessionHistoryTraversalQueue();
resolve(event.data.href.endsWith("/iframe-renavigate-during-history-commit-final") ? "PASS" : `FAIL: ${event.data.href}`);
}
});
iframe.src = intermediatePageUrl;
});
iframe.remove();
println(result);
done();
});
</script>