diff --git a/Libraries/LibWeb/HTML/DocumentState.h b/Libraries/LibWeb/HTML/DocumentState.h
index a7452973cd..10f530860c 100644
--- a/Libraries/LibWeb/HTML/DocumentState.h
+++ b/Libraries/LibWeb/HTML/DocumentState.h
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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 {
+class WEB_API DocumentState final : public RefCounted {
public:
static NonnullRefPtr create() { return adopt_ref(*new DocumentState()); }
~DocumentState();
diff --git a/Libraries/LibWeb/HTML/Navigable.cpp b/Libraries/LibWeb/HTML/Navigable.cpp
index 7cf0ff2ca6..57de3c1c57 100644
--- a/Libraries/LibWeb/HTML/Navigable.cpp
+++ b/Libraries/LibWeb/HTML/Navigable.cpp
@@ -488,9 +488,11 @@ RefPtr 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 result = nullptr;
for (auto& entry : entries) {
- auto entry_step = entry->step().get();
- if (entry_step <= target_step) {
- if (!result || result->step().get() < 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 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 window_features)
{
@@ -1961,7 +1970,7 @@ WebIDL::ExceptionOr 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> 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, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, NonnullRefPtr history_entry, GC::Ptr pending_document, GC::Ref on_complete)
+void finalize_a_cross_document_navigation(GC::Ref navigable, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, NonnullRefPtr history_entry, GC::Ptr pending_document, Optional expected_ongoing_navigation_id, GC::Ref 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, 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())
diff --git a/Libraries/LibWeb/HTML/Navigable.h b/Libraries/LibWeb/HTML/Navigable.h
index 9ee5392083..e36b46a020 100644
--- a/Libraries/LibWeb/HTML/Navigable.h
+++ b/Libraries/LibWeb/HTML/Navigable.h
@@ -300,7 +300,13 @@ protected:
Variant 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 source_element, Optional navigation_api_state, String navigation_id);
void navigate_to_a_javascript_url(URL::URL const&, HistoryHandlingBehavior, GC::Ref, 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>& all_navigables();
bool navigation_must_be_a_replace(URL::URL const& url, DOM::Document const& document);
-void finalize_a_cross_document_navigation(GC::Ref, HistoryHandlingBehavior, UserNavigationInvolvement, NonnullRefPtr, GC::Ptr pending_document, GC::Ref on_complete);
+void finalize_a_cross_document_navigation(GC::Ref, HistoryHandlingBehavior, UserNavigationInvolvement, NonnullRefPtr, GC::Ptr pending_document, Optional expected_ongoing_navigation_id, GC::Ref on_complete);
void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_url, Optional = {}, HistoryHandlingBehavior history_handling = HistoryHandlingBehavior::Replace);
}
diff --git a/Libraries/LibWeb/HTML/Navigation.cpp b/Libraries/LibWeb/HTML/Navigation.cpp
index 84232a9c5e..e977e1d242 100644
--- a/Libraries/LibWeb/HTML/Navigation.cpp
+++ b/Libraries/LibWeb/HTML/Navigation.cpp
@@ -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);
}
diff --git a/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp b/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp
index b8f9be4fea..7f70ab0ab7 100644
--- a/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp
+++ b/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp
@@ -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 nested_history_descriptors;
+ nested_history_descriptors.ensure_capacity(document_state.nested_histories().size());
+ for (auto const& nested_history : document_state.nested_histories()) {
+ Vector 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(*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 const& a, Vector const& b);
static bool serialized_directives_match(Vector const& a, Vector 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() || entry.step().get() != 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;
diff --git a/Libraries/LibWeb/HTML/SessionHistoryEntry.h b/Libraries/LibWeb/HTML/SessionHistoryEntry.h
index 5edb8df01a..fc2842cbd0 100644
--- a/Libraries/LibWeb/HTML/SessionHistoryEntry.h
+++ b/Libraries/LibWeb/HTML/SessionHistoryEntry.h
@@ -6,6 +6,8 @@
#pragma once
+#include
+#include
#include
#include
#include
@@ -85,7 +87,7 @@ struct SessionHistoryNestedHistoryDescriptor {
};
// https://html.spec.whatwg.org/multipage/history.html#session-history-entry
-class SessionHistoryEntry final : public RefCounted {
+class WEB_API SessionHistoryEntry final : public RefCounted {
public:
static NonnullRefPtr create();
@@ -98,6 +100,12 @@ public:
[[nodiscard]] Variant step() const { return m_step; }
void set_step(Variant step) { m_step = step; }
+ [[nodiscard]] Optional step_value() const
+ {
+ if (auto const* step = m_step.get_pointer())
+ 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 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,
diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Libraries/LibWeb/HTML/TraversableNavigable.cpp
index 831f8cb724..24859bdc4f 100644
--- a/Libraries/LibWeb/HTML/TraversableNavigable.cpp
+++ b/Libraries/LibWeb/HTML/TraversableNavigable.cpp
@@ -330,6 +330,15 @@ static bool synchronous_same_document_navigation_must_preserve_ongoing_navigatio
return navigable.ongoing_navigation().has();
}
+static bool expected_ongoing_navigation_was_superseded(GC::Ptr navigable, Optional 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 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 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());
+ // 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> 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> 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> 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 pending_document,
+ GC::Ptr expected_ongoing_navigation_navigable,
+ Optional expected_ongoing_navigation_id,
GC::Ref 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);
+ bool changing_navigable_is_still_current(GC::Ptr, Optional expected_active_document_id) const;
+ void clear_ongoing_traversal_for_changing_navigable(GC::Ptr);
+ void clear_ongoing_traversals_for_changing_navigables();
Phase m_phase { Phase::WaitingForDocumentPopulation };
GC::Ref m_traversable;
@@ -885,6 +916,8 @@ private:
TraversableNavigable::SynchronousNavigation m_synchronous_navigation;
Navigable::NavigationAPIAbortBehavior m_navigation_api_abort_behavior;
GC::Ptr m_pending_document;
+ GC::Ptr m_expected_ongoing_navigation_navigable;
+ Optional m_expected_ongoing_navigation_id;
GC::Ptr m_on_complete;
GC::Ref 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 {};
+ auto target_step = target_entry ? target_entry->step_value() : Optional {};
+ 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();
@@ -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() > displayed_entry->step().get());
break;
}
}
@@ -1393,76 +1461,6 @@ void ApplyHistoryStepState::enter_waiting_for_non_changing_jobs()
try_advance();
}
-struct SessionHistoryEntryDescriptorCreationState {
- HashMap 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 nested_history_descriptors;
- nested_history_descriptors.ensure_capacity(document_state.nested_histories().size());
- for (auto const& nested_history : document_state.nested_histories()) {
- Vector 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(entry.step().get()),
- .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)
+{
+ 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, Optional 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();
+}
+
+void ApplyHistoryStepState::clear_ongoing_traversal_for_changing_navigable(GC::Ptr navigable)
+{
+ if (!navigable || navigable->has_been_destroyed())
+ return;
+
+ if (!navigable->ongoing_navigation().has())
+ 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 pending_document,
+ GC::Ptr expected_ongoing_navigation_navigable,
+ Optional expected_ongoing_navigation_id,
GC::Ref 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 pending_document,
+ GC::Ptr expected_ongoing_navigation_navigable,
+ Optional expected_ongoing_navigation_id,
GC::Ref> 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(*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> 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 max_step;
+ Optional maybe_starting_index;
for (auto i = 0u; i < raw_entries.size(); ++i) {
auto const& entry = raw_entries[i];
- if (entry->step().has()) {
- auto step = entry->step().get();
- 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(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> TraversableNavigable::get_session_his
// 7. While i > 0:
while (i > 0) {
auto& entry = raw_entries[static_cast(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> TraversableNavigable::get_session_his
// 9. While i < rawEntries's size:
while (i < static_cast(raw_entries.size())) {
auto& entry = raw_entries[static_cast(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() > 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 target_top_level_entry;
for (auto const& entry : session_history_entries()) {
- if (entry->step().template get() > 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 pending_document, GC::Ref on_complete)
+void TraversableNavigable::apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, SynchronousNavigation synchronous_navigation, GC::Ptr pending_document, GC::Ptr expected_ongoing_navigation_navigable, Optional expected_ongoing_navigation_id, GC::Ref 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 update_session_history_entries_for_same_document_navigation(TraversableNavigable& traversable, GC::Ref target_navigable, NonnullRefPtr target_entry, RefPtr 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 source_snapshot_params, GC::Ptr initiator_to_check, UserNavigationInvolvement user_involvement, GC::Ref> 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 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
diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.h b/Libraries/LibWeb/HTML/TraversableNavigable.h
index 71e2410bb1..994ee6a048 100644
--- a/Libraries/LibWeb/HTML/TraversableNavigable.h
+++ b/Libraries/LibWeb/HTML/TraversableNavigable.h
@@ -77,7 +77,7 @@ public:
No,
};
[[nodiscard]] bool try_to_synchronously_commit_same_document_navigation(GC::Ref, NonnullRefPtr, RefPtr entry_to_replace);
- void apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement, SynchronousNavigation, GC::Ptr pending_document, GC::Ref on_complete);
+ void apply_the_push_or_replace_history_step(int step, HistoryHandlingBehavior history_handling, UserNavigationInvolvement, SynchronousNavigation, GC::Ptr pending_document, GC::Ptr expected_ongoing_navigation_navigable, Optional expected_ongoing_navigation_id, GC::Ref on_complete);
void update_for_navigable_creation_or_destruction(GC::Ref on_complete);
int get_the_used_step(int step) const;
@@ -154,6 +154,8 @@ private:
SynchronousNavigation,
Navigable::NavigationAPIAbortBehavior,
GC::Ptr pending_document,
+ GC::Ptr expected_ongoing_navigation_navigable,
+ Optional expected_ongoing_navigation_id,
GC::Ref on_complete);
void apply_the_history_step_after_unload_check(
@@ -165,6 +167,8 @@ private:
SynchronousNavigation,
Navigable::NavigationAPIAbortBehavior,
GC::Ptr pending_document,
+ GC::Ptr expected_ongoing_navigation_navigable,
+ Optional expected_ongoing_navigation_id,
GC::Ref on_complete);
using OnHistoryStepPrechecksComplete = GC::Function;
diff --git a/Tests/LibWeb/CMakeLists.txt b/Tests/LibWeb/CMakeLists.txt
index d9bcd083c2..d906bbabc5 100644
--- a/Tests/LibWeb/CMakeLists.txt
+++ b/Tests/LibWeb/CMakeLists.txt
@@ -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)
diff --git a/Tests/LibWeb/TestSessionHistoryEntry.cpp b/Tests/LibWeb/TestSessionHistoryEntry.cpp
index 86f8a68d3b..0d702ffeb0 100644
--- a/Tests/LibWeb/TestSessionHistoryEntry.cpp
+++ b/Tests/LibWeb/TestSessionHistoryEntry.cpp
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include
#include
#include
#include
@@ -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());
+}
diff --git a/Tests/LibWeb/Text/expected/navigation/iframe-pushstate-before-nested-history-ready.txt b/Tests/LibWeb/Text/expected/navigation/iframe-pushstate-before-nested-history-ready.txt
index 98343dae4c..372122596c 100644
--- a/Tests/LibWeb/Text/expected/navigation/iframe-pushstate-before-nested-history-ready.txt
+++ b/Tests/LibWeb/Text/expected/navigation/iframe-pushstate-before-nested-history-ready.txt
@@ -1,2 +1,3 @@
#child
queue drained
+navigation drained
diff --git a/Tests/LibWeb/Text/expected/navigation/iframe-remove-before-history-update.txt b/Tests/LibWeb/Text/expected/navigation/iframe-remove-before-history-update.txt
new file mode 100644
index 0000000000..7ef22e9a43
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/navigation/iframe-remove-before-history-update.txt
@@ -0,0 +1 @@
+PASS
diff --git a/Tests/LibWeb/Text/expected/navigation/iframe-renavigate-during-history-commit.txt b/Tests/LibWeb/Text/expected/navigation/iframe-renavigate-during-history-commit.txt
new file mode 100644
index 0000000000..7ef22e9a43
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/navigation/iframe-renavigate-during-history-commit.txt
@@ -0,0 +1 @@
+PASS
diff --git a/Tests/LibWeb/Text/input/navigation/iframe-pushstate-before-nested-history-ready.html b/Tests/LibWeb/Text/input/navigation/iframe-pushstate-before-nested-history-ready.html
index d93e67ef7f..ccda672d52 100644
--- a/Tests/LibWeb/Text/input/navigation/iframe-pushstate-before-nested-history-ready.html
+++ b/Tests/LibWeb/Text/input/navigation/iframe-pushstate-before-nested-history-ready.html
@@ -3,6 +3,13 @@
diff --git a/Tests/LibWeb/Text/input/navigation/iframe-remove-before-history-update.html b/Tests/LibWeb/Text/input/navigation/iframe-remove-before-history-update.html
new file mode 100644
index 0000000000..eb5981338f
--- /dev/null
+++ b/Tests/LibWeb/Text/input/navigation/iframe-remove-before-history-update.html
@@ -0,0 +1,18 @@
+
+
+
diff --git a/Tests/LibWeb/Text/input/navigation/iframe-renavigate-during-history-commit.html b/Tests/LibWeb/Text/input/navigation/iframe-renavigate-during-history-commit.html
new file mode 100644
index 0000000000..8e1f89ea4e
--- /dev/null
+++ b/Tests/LibWeb/Text/input/navigation/iframe-renavigate-during-history-commit.html
@@ -0,0 +1,45 @@
+
+
+