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 @@ + + +