From 44ae6d34380feb7044b114740dc0668bef9c6749 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 13 Jun 2026 16:06:27 +0200 Subject: [PATCH] Tests/LibWebView: Cover the UI history model Add fast coverage for merging WebContent snapshots into the UI-owned session history mirror. The tests exercise seeding, nested histories, reload state, replacement entries, and traversal target selection. --- Tests/LibWebView/CMakeLists.txt | 1 + Tests/LibWebView/TestSessionHistory.cpp | 1864 +++++++++++++++++++++++ 2 files changed, 1865 insertions(+) create mode 100644 Tests/LibWebView/TestSessionHistory.cpp diff --git a/Tests/LibWebView/CMakeLists.txt b/Tests/LibWebView/CMakeLists.txt index 6fa2f05a7d..e4372955e0 100644 --- a/Tests/LibWebView/CMakeLists.txt +++ b/Tests/LibWebView/CMakeLists.txt @@ -1,6 +1,7 @@ set(TEST_SOURCES TestHistoryStore.cpp TestHSTSStore.cpp + TestSessionHistory.cpp TestWebViewURL.cpp ) diff --git a/Tests/LibWebView/TestSessionHistory.cpp b/Tests/LibWebView/TestSessionHistory.cpp new file mode 100644 index 0000000000..75992fe8cb --- /dev/null +++ b/Tests/LibWebView/TestSessionHistory.cpp @@ -0,0 +1,1864 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +static Web::HTML::SessionHistoryNestedHistoryDescriptor nested_history(StringView id, Vector entries) +{ + return { + .id = MUST(String::from_utf8(id)), + .entries = move(entries), + }; +} + +static URL::URL parse_url(StringView url) +{ + auto parsed_url = URL::Parser::basic_parse(url); + VERIFY(parsed_url.has_value()); + return parsed_url.release_value(); +} + +static Web::HTML::SerializationRecord state_record(u8 byte) +{ + Web::HTML::SerializationRecord record; + record.append(byte); + return record; +} + +static Web::HTML::SessionHistoryEntryDescriptor create_test_entry(i32 step, URL::URL url) +{ + return { + .step = step, + .url = move(url), + .document_state = { + .id = 0, + .history_policy_container = Web::HTML::DocumentState::Client::Tag, + .request_referrer = Web::Fetch::Infrastructure::Request::Referrer::Client, + .request_referrer_policy = Web::ReferrerPolicy::DEFAULT_REFERRER_POLICY, + .initiator_origin = {}, + .origin = {}, + .about_base_url = {}, + .resource = {}, + .reload_pending = false, + .ever_populated = false, + .navigable_target_name = {}, + .nested_histories = {}, + }, + .classic_history_api_state = {}, + .navigation_api_state = {}, + .navigation_api_key = {}, + .navigation_api_id = {}, + .scroll_restoration_mode = Web::HTML::ScrollRestorationMode::Auto, + .scroll_position_data = {}, + }; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, StringView url) +{ + return create_test_entry(step, parse_url(url)); +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, StringView url, u8 classic_history_api_state, u8 navigation_api_state, StringView navigation_api_key, StringView navigation_api_id, Web::HTML::ScrollRestorationMode scroll_restoration_mode) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.classic_history_api_state = state_record(classic_history_api_state); + entry.navigation_api_state = state_record(navigation_api_state); + entry.navigation_api_key = MUST(String::from_utf8(navigation_api_key)); + entry.navigation_api_id = MUST(String::from_utf8(navigation_api_id)); + entry.scroll_restoration_mode = scroll_restoration_mode; + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry_with_scroll_position(i32 step, StringView url, Web::CSSPixelPoint viewport_scroll_position) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.scroll_position_data.viewport_scroll_position = viewport_scroll_position; + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, StringView url, u64 document_state_id, StringView navigable_target_name) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.document_state.id = document_state_id; + entry.document_state.ever_populated = true; + entry.document_state.navigable_target_name = MUST(String::from_utf8(navigable_target_name)); + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry_with_reload_pending(i32 step, StringView url, u64 document_state_id, StringView navigable_target_name, Vector nested_histories) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.document_state.id = document_state_id; + entry.document_state.reload_pending = true; + entry.document_state.ever_populated = true; + entry.document_state.navigable_target_name = MUST(String::from_utf8(navigable_target_name)); + entry.document_state.nested_histories = move(nested_histories); + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry_with_post_resource(i32 step, StringView url) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.document_state.resource = Web::HTML::POSTResource { + .request_body = MUST(ByteBuffer::copy("field=value"sv.bytes())), + .request_content_type = Web::HTML::POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded, + }; + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, StringView url, Vector nested_histories) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.document_state.nested_histories = move(nested_histories); + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, StringView url, u64 document_state_id, StringView navigable_target_name, Vector nested_histories) +{ + auto entry = create_test_entry(step, parse_url(url)); + entry.document_state.id = document_state_id; + entry.document_state.ever_populated = true; + entry.document_state.navigable_target_name = MUST(String::from_utf8(navigable_target_name)); + entry.document_state.nested_histories = move(nested_histories); + return entry; +} + +static Web::HTML::SessionHistoryEntryDescriptor entry(i32 step, URL::URL url) +{ + return create_test_entry(step, move(url)); +} + +static void expect_entry(WebView::TraversableSessionHistory const& history, size_t index, i32 expected_step, StringView expected_url) +{ + auto* entry = history.entry_at(index); + VERIFY(entry); + EXPECT_EQ(entry->step, expected_step); + EXPECT_EQ(entry->url, parse_url(expected_url)); +} + +static void expect_current_entry(WebView::TraversableSessionHistory const& history, i32 expected_step, StringView expected_url) +{ + auto* entry = history.current_entry(); + VERIFY(entry); + EXPECT_EQ(entry->step, expected_step); + EXPECT_EQ(entry->url, parse_url(expected_url)); +} + +static void expect_entry_state(Web::HTML::SessionHistoryEntryDescriptor const& entry, u8 expected_classic_history_api_state, u8 expected_navigation_api_state, StringView expected_navigation_api_key, StringView expected_navigation_api_id, Web::HTML::ScrollRestorationMode expected_scroll_restoration_mode) +{ + EXPECT(entry.classic_history_api_state == state_record(expected_classic_history_api_state)); + EXPECT(entry.navigation_api_state == state_record(expected_navigation_api_state)); + EXPECT_EQ(entry.navigation_api_key, MUST(String::from_utf8(expected_navigation_api_key))); + EXPECT_EQ(entry.navigation_api_id, MUST(String::from_utf8(expected_navigation_api_id))); + EXPECT_EQ(entry.scroll_restoration_mode, expected_scroll_restoration_mode); +} + +static void expect_entry_document_state(Web::HTML::SessionHistoryEntryDescriptor const& entry, u64 expected_document_state_id, StringView expected_navigable_target_name) +{ + EXPECT_EQ(entry.document_state.id, expected_document_state_id); + EXPECT_EQ(entry.document_state.navigable_target_name, MUST(String::from_utf8(expected_navigable_target_name))); +} + +static void expect_entry_viewport_scroll_position(Web::HTML::SessionHistoryEntryDescriptor const& entry, Web::CSSPixelPoint expected_viewport_scroll_position) +{ + VERIFY(entry.scroll_position_data.viewport_scroll_position.has_value()); + EXPECT_EQ(*entry.scroll_position_data.viewport_scroll_position, expected_viewport_scroll_position); +} + +static void expect_entry_resource(Web::HTML::SessionHistoryEntryDescriptor const& entry, StringView expected_resource) +{ + if (expected_resource == "post"sv) { + EXPECT(entry.document_state.resource.has()); + return; + } + + if (expected_resource == "string"sv) { + EXPECT(entry.document_state.resource.has()); + return; + } + + VERIFY(expected_resource == "none"sv); + EXPECT(entry.document_state.resource.has()); +} + +static void expect_used_step(WebView::TraversableSessionHistory const& history, size_t index, i32 expected_step) +{ + auto step = history.step_at(index); + VERIFY(step.has_value()); + EXPECT_EQ(*step, expected_step); +} + +static void expect_step_to_restore(Optional step, i32 expected_step) +{ + VERIFY(step.has_value()); + EXPECT_EQ(*step, expected_step); +} + +static void expect_nested_history(Web::HTML::SessionHistoryEntryDescriptor const& entry, size_t index, StringView expected_id, size_t expected_size) +{ + VERIFY(index < entry.document_state.nested_histories.size()); + EXPECT_EQ(entry.document_state.nested_histories[index].id, MUST(String::from_utf8(expected_id))); + EXPECT_EQ(entry.document_state.nested_histories[index].entries.size(), expected_size); +} + +static void expect_nested_entry(Web::HTML::SessionHistoryNestedHistoryDescriptor const& nested_history, size_t index, i32 expected_step, StringView expected_url) +{ + VERIFY(index < nested_history.entries.size()); + EXPECT_EQ(nested_history.entries[index].step, expected_step); + EXPECT_EQ(nested_history.entries[index].url, parse_url(expected_url)); +} + +TEST_CASE(complete_web_content_update_replaces_mirror) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); +} + +TEST_CASE(fresh_process_snapshot_does_not_drop_previous_entries) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); +} + +TEST_CASE(replace_navigation_snapshot_drops_speculative_entry) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.navigate(parse_url("https://c.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://c.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://c.example/"sv); +} + +TEST_CASE(replace_navigation_snapshot_drops_speculative_first_entry) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + }, + { 0 }, 0); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.replace_current_entry_url(parse_url("https://b.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, "https://b.example/"sv), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 1uz); + EXPECT_EQ(history.used_step_count(), 1uz); + EXPECT(!history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_current_entry(history, 0, "https://b.example/"sv); +} + +TEST_CASE(fresh_single_entry_snapshot_does_not_drop_previous_entries) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, "https://b.example/"sv), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 2uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 1uz); + EXPECT_EQ(web_content_known_used_steps[0], 1); + auto target_a = history.traversal_target_for_delta(-1); + VERIFY(target_a.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_a)); +} + +TEST_CASE(complete_matching_snapshot_updates_current_index) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_current_entry(history, 1, "https://b.example/"sv); +} + +TEST_CASE(matching_snapshot_updates_session_history_entry_state) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, 2, "key-a"sv, "id-a"sv, Web::HTML::ScrollRestorationMode::Auto), + }, + { 0 }, 0); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 3, 4, "key-b"sv, "id-b"sv, Web::HTML::ScrollRestorationMode::Manual), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 1uz); + EXPECT_EQ(history.used_step_count(), 1uz); + expect_current_entry(history, 0, "https://a.example/"sv); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_entry_state(*current_entry, 3, 4, "key-b"sv, "id-b"sv, Web::HTML::ScrollRestorationMode::Manual); +} + +TEST_CASE(matching_snapshot_updates_session_history_entry_scroll_position) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry_with_scroll_position(0, "https://a.example/"sv, { 0, 100 }), + }, + { 0 }, 0); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry_with_scroll_position(0, "https://a.example/"sv, { 0, 300 }), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 1uz); + EXPECT_EQ(history.used_step_count(), 1uz); + expect_current_entry(history, 0, "https://a.example/"sv); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_entry_viewport_scroll_position(*current_entry, { 0, 300 }); +} + +TEST_CASE(matching_snapshot_updates_document_state) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, "target-a"sv), + }, + { 0 }, 0); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, "target-b"sv), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 1uz); + EXPECT_EQ(history.used_step_count(), 1uz); + expect_current_entry(history, 0, "https://a.example/"sv); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_entry_document_state(*current_entry, 1, "target-b"sv); +} + +TEST_CASE(partial_snapshot_preserves_forward_history_after_fallback_traversal) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); +} + +TEST_CASE(partial_snapshot_preserves_entries_outside_web_content_known_history) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 1uz); + EXPECT_EQ(web_content_known_used_steps[0], 1); + + auto target_a = history.traversal_target_for_delta(-1); + VERIFY(target_a.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_a)); + + auto target_c = history.traversal_target_for_delta(1); + VERIFY(target_c.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_c)); +} + +TEST_CASE(complete_snapshot_accepts_reassigned_document_state_ids) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, "top-a"sv), + entry(1, "https://nested.example/"sv, 2, "top-nested"sv, { + nested_history("frame-1"sv, { + entry(1, "https://frame.example/a"sv, 3, "frame"sv), + entry(2, "https://frame.example/b"sv, 4, "frame"sv), + }), + }), + entry(3, "https://c.example/"sv, 5, "top-c"sv), + }, + { 0, 1, 2, 3 }, 2); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 10, "top-a"sv), + entry(1, "https://nested.example/"sv, 20, "top-nested"sv, { + nested_history("frame-1"sv, { + entry(1, "https://frame.example/a"sv, 30, "frame"sv), + entry(2, "https://frame.example/b"sv, 40, "frame"sv), + }), + }), + entry(3, "https://c.example/"sv, 50, "top-c"sv), + }, + { 0, 1, 2, 3 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 4uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_current_entry(history, 1, "https://nested.example/"sv); + expect_used_step(history, 2, 2); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_entry_document_state(*current_entry, 2, "top-nested"sv); + expect_nested_history(*current_entry, 0, "frame-1"sv, 2); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 1, "https://frame.example/a"sv); + expect_nested_entry(current_entry->document_state.nested_histories[0], 1, 2, "https://frame.example/b"sv); + expect_entry_document_state(current_entry->document_state.nested_histories[0].entries[0], 3, "frame"sv); + expect_entry_document_state(current_entry->document_state.nested_histories[0].entries[1], 4, "frame"sv); +} + +TEST_CASE(partial_snapshot_updates_nested_history_and_preserves_forward_history) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv, { + nested_history("frame-1"sv, { + entry(1, "https://frame.example/a"sv), + }), + }), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(!history.has_only_top_level_used_steps()); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_nested_history(*current_entry, 0, "frame-1"sv, 1); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 1, "https://frame.example/a"sv); +} + +TEST_CASE(fresh_process_snapshot_preserves_forward_history_at_first_entry) +{ + WebView::TraversableSessionHistory history; + history.navigate(URL::about_blank()); + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.traverse_to(0); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(!history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_current_entry(history, 0, "about:blank"sv); + expect_entry(history, 1, 1, "https://a.example/"sv); + expect_entry(history, 2, 2, "https://b.example/"sv); +} + +TEST_CASE(partial_snapshot_extends_history_without_dropping_prefix) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_current_entry(history, 2, "https://c.example/"sv); +} + +TEST_CASE(partial_snapshot_accepts_same_document_update_at_current_step) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, "main"sv), + entry(1, "https://b.example/"sv, 2, "main"sv), + }, + { 0, 1 }, 1); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(1, "https://b.example/?replaced"sv, 20, "main"sv), + entry(2, "https://b.example/?pushed"sv, 20, "main"sv), + }, + { 1, 2 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/?replaced"sv); + expect_current_entry(history, 2, "https://b.example/?pushed"sv); + EXPECT(history.web_content_uses_ui_step_coordinates()); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 2uz); + EXPECT_EQ(web_content_known_used_steps[0], 1); + EXPECT_EQ(web_content_known_used_steps[1], 2); + + auto web_content_known_entries = history.web_content_known_entries(); + EXPECT_EQ(web_content_known_entries.size(), 2uz); + EXPECT_EQ(web_content_known_entries[0].url, parse_url("https://b.example/?replaced"sv)); + EXPECT_EQ(web_content_known_entries[1].url, parse_url("https://b.example/?pushed"sv)); +} + +TEST_CASE(partial_snapshot_allows_web_content_traversal_within_known_suffix) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_current_entry(history, 2, "https://c.example/"sv); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 2uz); + EXPECT_EQ(web_content_known_used_steps[0], 1); + EXPECT_EQ(web_content_known_used_steps[1], 2); + EXPECT(history.web_content_uses_ui_step_coordinates()); + + auto target_b = history.traversal_target_for_delta(-1); + VERIFY(target_b.has_value()); + EXPECT(history.web_content_can_traverse_to(*target_b)); + + auto target_a = history.traversal_target_for_delta(-2); + VERIFY(target_a.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_a)); +} + +TEST_CASE(reseeded_partial_snapshot_preserves_ui_only_history) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://d.example/"sv), + }, + { 0, 1, 2, 3 }, 3); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(1); + history.forget_web_content_state(); + auto current_top_level_entry_index = history.current_top_level_entry_index(); + VERIFY(current_top_level_entry_index.has_value()); + history.did_seed_web_content_from_ui_process(*current_top_level_entry_index); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 4uz); + EXPECT_EQ(history.used_step_count(), 4uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + expect_entry(history, 3, 3, "https://d.example/"sv); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 1uz); + EXPECT_EQ(web_content_known_used_steps[0], 1); + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); + + auto target_a = history.traversal_target_for_delta(-1); + VERIFY(target_a.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_a)); + + auto target_c = history.traversal_target_for_delta(1); + VERIFY(target_c.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_c)); +} + +TEST_CASE(partial_snapshot_replaces_forward_history_after_new_navigation) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + entry(2, "https://d.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_current_entry(history, 2, "https://d.example/"sv); +} + +TEST_CASE(partial_snapshot_anchors_to_current_duplicate_url_entry) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(2); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://a.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 4uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_current_entry(history, 2, "https://a.example/"sv); + expect_entry(history, 3, 3, "https://c.example/"sv); +} + +TEST_CASE(partial_snapshot_prefers_duplicate_url_entry_with_matching_document_state) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, "main"sv), + entry(1, "https://b.example/"sv, 2, "main"sv), + entry(2, "https://b.example/"sv, 3, "main"sv), + entry(3, "https://c.example/"sv, 4, "main"sv), + }, + { 0, 1, 2, 3 }, 3); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(2); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv, 2, "main"sv), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 4uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://b.example/"sv); + expect_entry(history, 3, 3, "https://c.example/"sv); +} + +TEST_CASE(partial_snapshot_translates_fresh_process_steps_to_ui_steps) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(4, "https://a.example/"sv), + entry(5, "https://b.example/"sv), + entry(6, "https://c.example/"sv), + }, + { 4, 5, 6 }, 2); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + entry(2, "https://d.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 4, "https://a.example/"sv); + expect_entry(history, 1, 5, "https://b.example/"sv); + expect_current_entry(history, 6, "https://d.example/"sv); + + auto target_b = history.traversal_target_for_delta(-1); + VERIFY(target_b.has_value()); + EXPECT(!history.web_content_uses_ui_step_coordinates()); + EXPECT(!history.web_content_can_traverse_to(*target_b)); +} + +TEST_CASE(used_steps_include_nested_history_steps) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(1, "https://frame.example/a"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(!history.has_only_top_level_used_steps()); + EXPECT(history.current_step_is_top_level_entry()); + EXPECT(!history.current_step_to_restore_after_loading_top_level_entry().has_value()); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 2, "https://b.example/"sv); + + auto* first_entry = history.entry_at(0); + VERIFY(first_entry); + expect_nested_history(*first_entry, 0, "frame-1"sv, 1); + expect_nested_entry(first_entry->document_state.nested_histories[0], 0, 1, "https://frame.example/a"sv); + + auto target_step_index = history.target_step_index_for_delta(-1); + VERIFY(target_step_index.has_value()); + expect_used_step(history, *target_step_index, 1); + EXPECT_EQ(history.entry_for_step(1), nullptr); + auto* target_top_level_entry = history.top_level_entry_for_step(1); + VERIFY(target_top_level_entry); + EXPECT_EQ(target_top_level_entry->url, parse_url("https://a.example/"sv)); + + auto traversal_target = history.traversal_target_for_delta(-1); + VERIFY(traversal_target.has_value()); + EXPECT_EQ(traversal_target->target_step_index, *target_step_index); + EXPECT_EQ(traversal_target->target_step, 1); + EXPECT_EQ(traversal_target->target_top_level_entry, target_top_level_entry); + EXPECT(!traversal_target->target_step_is_top_level_entry); + EXPECT(traversal_target->changes_top_level_entry); + + history.traverse_to(*target_step_index); + expect_current_entry(history, 0, "https://a.example/"sv); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); +} + +TEST_CASE(traversal_target_for_top_level_step) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + EXPECT(history.current_step_is_top_level_entry()); + EXPECT(!history.current_step_to_restore_after_loading_top_level_entry().has_value()); + + auto traversal_target = history.traversal_target_for_delta(1); + VERIFY(traversal_target.has_value()); + EXPECT_EQ(traversal_target->target_step_index, 2uz); + EXPECT_EQ(traversal_target->target_step, 2); + VERIFY(traversal_target->target_top_level_entry); + EXPECT_EQ(traversal_target->target_top_level_entry->url, parse_url("https://c.example/"sv)); + EXPECT(traversal_target->target_step_is_top_level_entry); + EXPECT(traversal_target->changes_top_level_entry); +} + +TEST_CASE(traversal_target_for_nested_step_in_current_top_level_entry) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto traversal_target = history.traversal_target_for_delta(1); + VERIFY(traversal_target.has_value()); + EXPECT_EQ(traversal_target->target_step_index, 1uz); + EXPECT_EQ(traversal_target->target_step, 1); + VERIFY(traversal_target->target_top_level_entry); + EXPECT_EQ(traversal_target->target_top_level_entry->url, parse_url("https://a.example/"sv)); + EXPECT(!traversal_target->target_step_is_top_level_entry); + EXPECT(!traversal_target->changes_top_level_entry); + + history.traverse_to(traversal_target->target_step_index); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); +} + +TEST_CASE(current_top_level_step_can_need_nested_history_restore) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 7, "main"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://a.example/same-document"sv, 7, "main"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT(history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 2); + + auto traversal_target = history.traversal_target_for_delta(-1); + VERIFY(traversal_target.has_value()); + EXPECT_EQ(traversal_target->target_step_index, 1uz); + EXPECT_EQ(traversal_target->target_step, 1); + VERIFY(traversal_target->target_top_level_entry); + EXPECT_EQ(traversal_target->target_top_level_entry->url, parse_url("https://a.example/"sv)); + EXPECT(!traversal_target->target_step_is_top_level_entry); + EXPECT(traversal_target->changes_top_level_entry); + + traversal_target = history.traversal_target_for_step(1); + VERIFY(traversal_target.has_value()); + EXPECT_EQ(traversal_target->target_step_index, 1uz); + EXPECT_EQ(traversal_target->target_step, 1); + VERIFY(traversal_target->target_top_level_entry); + EXPECT_EQ(traversal_target->target_top_level_entry->url, parse_url("https://a.example/"sv)); + EXPECT(!traversal_target->target_step_is_top_level_entry); + EXPECT(traversal_target->changes_top_level_entry); + + EXPECT(!history.traversal_target_for_step(42).has_value()); +} + +TEST_CASE(seeded_web_content_must_restore_nested_current_step_before_traversing) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); + + history.forget_web_content_state(); + auto current_top_level_entry_index = history.current_top_level_entry_index(); + VERIFY(current_top_level_entry_index.has_value()); + history.did_seed_web_content_from_ui_process(*current_top_level_entry_index); + + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 0); + + auto traversal_target = history.traversal_target_for_delta(1); + VERIFY(traversal_target.has_value()); + EXPECT(!history.web_content_can_traverse_to(*traversal_target)); + + update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); + EXPECT(history.web_content_can_traverse_to(*traversal_target)); +} + +TEST_CASE(seed_ack_snapshot_preserves_nested_ui_current_step) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); + + history.forget_web_content_state(); + auto accepted = history.did_seed_web_content_from_ui_process({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 0); + + EXPECT(accepted); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); + + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 0); +} + +TEST_CASE(seed_ack_snapshot_rejects_mismatched_reconstructed_history) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.forget_web_content_state(); + auto accepted = history.did_seed_web_content_from_ui_process({ + entry(0, "https://a.example/"sv), + entry(1, "https://c.example/"sv), + }, + { 0, 1 }, 1); + + EXPECT(!accepted); + EXPECT(!history.web_content_current_step().has_value()); + EXPECT(!history.web_content_uses_ui_step_coordinates()); + expect_current_entry(history, 1, "https://b.example/"sv); +} + +TEST_CASE(seeded_web_content_restore_updates_current_step) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://b.example/"sv), + }, + { 0, 1, 2 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.forget_web_content_state(); + auto current_top_level_entry_index = history.current_top_level_entry_index(); + VERIFY(current_top_level_entry_index.has_value()); + history.did_seed_web_content_from_ui_process(*current_top_level_entry_index); + + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 0); + + EXPECT(history.did_restore_web_content_to_current_step(1)); + web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); + + EXPECT(!history.did_restore_web_content_to_current_step(3)); + web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); +} + +TEST_CASE(restored_web_content_step_must_match_current_ui_step) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(1); + history.forget_web_content_state(); + auto current_top_level_entry_index = history.current_top_level_entry_index(); + VERIFY(current_top_level_entry_index.has_value()); + history.did_seed_web_content_from_ui_process(*current_top_level_entry_index); + + EXPECT(!history.did_restore_web_content_to_current_step(2)); + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); +} + +TEST_CASE(applied_web_content_traversal_updates_current_step) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + expect_current_entry(history, 2, "https://c.example/"sv); + + EXPECT(history.did_apply_web_content_traversal_to_step(1)); + expect_current_entry(history, 1, "https://b.example/"sv); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + + auto web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); + + auto target_a = history.traversal_target_for_delta(-1); + VERIFY(target_a.has_value()); + EXPECT(history.web_content_can_traverse_to(*target_a)); + + EXPECT(!history.did_apply_web_content_traversal_to_step(42)); + expect_current_entry(history, 1, "https://b.example/"sv); + web_content_current_step = history.web_content_current_step(); + VERIFY(web_content_current_step.has_value()); + EXPECT_EQ(*web_content_current_step, 1); +} + +TEST_CASE(applied_web_content_traversal_rejects_unknown_web_content_state) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(1); + EXPECT(!history.did_apply_web_content_traversal_to_step(0)); + expect_current_entry(history, 1, "https://b.example/"sv); + EXPECT(!history.web_content_current_step().has_value()); +} + +TEST_CASE(same_document_reseed_snapshot_accepts_reassigned_document_state_ids) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://nested.example/"sv, 6, "main"sv, { + nested_history("frame-1"sv, { + entry(3, "https://frame.example/a"sv, 7, ""sv), + entry(4, "https://frame.example/b"sv, 8, ""sv), + }), + }), + entry(5, "https://nested.example/same-document"sv, 6, "main"sv, { + nested_history("frame-1"sv, { + entry(3, "https://frame.example/a"sv, 7, ""sv), + entry(4, "https://frame.example/b"sv, 8, ""sv), + }), + }), + }, + { 0, 1, 2, 3, 4, 5 }, 5); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.forget_web_content_state(); + auto current_top_level_entry_index = history.current_top_level_entry_index(); + VERIFY(current_top_level_entry_index.has_value()); + history.did_seed_web_content_from_ui_process(*current_top_level_entry_index); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://nested.example/"sv, 8, "main"sv, { + nested_history("1"sv, { + entry(3, "https://frame.example/a"sv, 6, ""sv), + entry(4, "https://frame.example/b"sv, 7, ""sv), + }), + }), + entry(5, "https://nested.example/same-document"sv, 8, "main"sv, { + nested_history("1"sv, { + entry(3, "https://frame.example/a"sv, 6, ""sv), + entry(4, "https://frame.example/b"sv, 7, ""sv), + }), + }), + }, + { 0, 1, 2, 3, 4, 5 }, 5); + + EXPECT_NE(update_result, WebView::TraversableSessionHistory::UpdateResult::InvalidSnapshot); + EXPECT_EQ(history.current_used_step_index(), 5uz); + expect_current_entry(history, 5, "https://nested.example/same-document"sv); + EXPECT(history.web_content_can_traverse_to(*history.traversal_target_for_delta(-1))); +} + +TEST_CASE(seed_ack_accepts_reconstructed_document_state_for_unknown_current_entry) +{ + WebView::TraversableSessionHistory history; + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://a.example/replaced"sv, 1, "main"sv), + entry(2, "https://a.example/pushed"sv, 1, "main"sv), + }, + { 0, 1, 2 }, 0); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.forget_web_content_state(); + + EXPECT(history.did_seed_web_content_from_ui_process({ + entry(0, "https://a.example/"sv, 1, "main"sv), + entry(1, "https://a.example/replaced"sv, 2, "main"sv), + entry(2, "https://a.example/pushed"sv, 2, "main"sv), + }, + { 0, 1, 2 }, 0)); + EXPECT(history.web_content_history_matches_mirror()); + EXPECT_EQ(history.web_content_known_entries().size(), 3uz); + EXPECT_EQ(history.web_content_current_step().value(), 0); +} + +TEST_CASE(seed_ack_rejects_reconstructed_history_with_mismatched_state) +{ + WebView::TraversableSessionHistory history; + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 1, 2, "key-a"sv, "id-a"sv, Web::HTML::ScrollRestorationMode::Manual), + entry(1, "https://a.example/replaced"sv, 3, 4, "key-b"sv, "id-b"sv, Web::HTML::ScrollRestorationMode::Auto), + }, + { 0, 1 }, 0); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.forget_web_content_state(); + + EXPECT(!history.did_seed_web_content_from_ui_process({ + entry(0, "https://a.example/"sv, 9, 2, "key-a"sv, "id-a"sv, Web::HTML::ScrollRestorationMode::Manual), + entry(1, "https://a.example/replaced"sv, 3, 4, "key-b"sv, "id-b"sv, Web::HTML::ScrollRestorationMode::Auto), + }, + { 0, 1 }, 0)); + EXPECT(!history.web_content_history_matches_mirror()); + EXPECT(history.web_content_known_entries().is_empty()); +} + +TEST_CASE(traversal_target_for_delta_outside_used_steps) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + + EXPECT(!history.traversal_target_for_delta(-1).has_value()); + EXPECT(!history.traversal_target_for_delta(0).has_value()); + EXPECT(!history.traversal_target_for_delta(1).has_value()); +} + +TEST_CASE(complete_web_content_update_stores_nested_history_descriptors) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1 }, 1); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + EXPECT_EQ(history.size(), 1uz); + EXPECT_EQ(history.used_step_count(), 2uz); + EXPECT(!history.has_only_top_level_used_steps()); + EXPECT(!history.current_step_is_top_level_entry()); + expect_step_to_restore(history.current_step_to_restore_after_loading_top_level_entry(), 1); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT_EQ(current_entry->step, 0); + EXPECT_EQ(current_entry->url, parse_url("https://a.example/"sv)); + expect_nested_history(*current_entry, 0, "frame-1"sv, 2); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 0, "https://frame.example/a"sv); + expect_nested_entry(current_entry->document_state.nested_histories[0], 1, 1, "https://frame.example/b"sv); +} + +TEST_CASE(navigate_clears_forward_nested_history_entries) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + entry(2, "https://frame.example/c"sv), + }), + }), + entry(3, "https://c.example/"sv), + }, + { 0, 1, 2, 3 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.navigate(parse_url("https://b.example/"sv)); + + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_current_entry(history, 2, "https://b.example/"sv); + expect_used_step(history, 0, 0); + expect_used_step(history, 1, 1); + expect_used_step(history, 2, 2); + + auto* first_entry = history.entry_at(0); + VERIFY(first_entry); + expect_nested_history(*first_entry, 0, "frame-1"sv, 2); + expect_nested_entry(first_entry->document_state.nested_histories[0], 0, 0, "https://frame.example/a"sv); + expect_nested_entry(first_entry->document_state.nested_histories[0], 1, 1, "https://frame.example/b"sv); +} + +TEST_CASE(partial_snapshot_missing_nested_history_descriptor_is_invalid) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::InvalidSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_used_step(history, 2, 2); +} + +TEST_CASE(invalid_snapshot_forgets_known_web_content_state) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::InvalidSnapshot); + + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + expect_current_entry(history, 2, "https://c.example/"sv); + + auto web_content_known_used_steps = history.web_content_known_used_steps(); + EXPECT_EQ(web_content_known_used_steps.size(), 0uz); + EXPECT(!history.web_content_current_step().has_value()); + + auto target_b = history.traversal_target_for_delta(-1); + VERIFY(target_b.has_value()); + EXPECT(!history.web_content_can_traverse_to(*target_b)); +} + +TEST_CASE(partial_snapshot_translates_nested_history_descriptors) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(4, "https://a.example/"sv), + entry(5, "https://b.example/"sv), + entry(6, "https://c.example/"sv), + }, + { 4, 5, 6 }, 2); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, URL::about_blank()), + entry(1, "https://b.example/"sv, { + nested_history("frame-1"sv, { + entry(1, "https://frame.example/a"sv), + entry(2, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 4, "https://a.example/"sv); + expect_current_entry(history, 5, "https://b.example/"sv); + expect_used_step(history, 2, 6); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_nested_history(*current_entry, 0, "frame-1"sv, 2); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 5, "https://frame.example/a"sv); + expect_nested_entry(current_entry->document_state.nested_histories[0], 1, 6, "https://frame.example/b"sv); + EXPECT_EQ(history.entry_for_step(6), nullptr); +} + +TEST_CASE(partial_snapshot_translates_seeded_nested_history_initial_step) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://nested.example/"sv, { + nested_history("frame-1"sv, { + entry(3, "https://frame.example/a"sv), + }), + }), + }, + { 0, 1, 2, 3 }, 3); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://nested.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(4, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1, 2, 3, 4 }, 4); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + EXPECT_EQ(history.size(), 4uz); + EXPECT_EQ(history.used_step_count(), 5uz); + EXPECT(!history.has_only_top_level_used_steps()); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + expect_current_entry(history, 3, "https://nested.example/"sv); + expect_used_step(history, 4, 4); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_nested_history(*current_entry, 0, "frame-1"sv, 2); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 3, "https://frame.example/a"sv); + expect_nested_entry(current_entry->document_state.nested_histories[0], 1, 4, "https://frame.example/b"sv); + EXPECT_EQ(history.entry_for_step(4), nullptr); +} + +TEST_CASE(partial_snapshot_missing_preserved_nested_history_descriptor_is_invalid) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + entry(2, "https://c.example/"sv), + entry(3, "https://nested.example/"sv, { + nested_history("frame-1"sv, { + entry(3, "https://frame.example/a"sv), + entry(4, "https://frame.example/b"sv), + }), + }), + }, + { 0, 1, 2, 3, 4 }, 4); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto update_result = history.update_from_web_content({ + entry(0, "https://nested.example/"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + }), + }), + }, + { 0 }, 0); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::InvalidSnapshot); + EXPECT_EQ(history.size(), 4uz); + EXPECT_EQ(history.used_step_count(), 5uz); + EXPECT(history.can_go_back()); + EXPECT(!history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + expect_current_entry(history, 3, "https://nested.example/"sv); + expect_used_step(history, 4, 4); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + expect_nested_history(*current_entry, 0, "frame-1"sv, 2); + expect_nested_entry(current_entry->document_state.nested_histories[0], 0, 3, "https://frame.example/a"sv); + expect_nested_entry(current_entry->document_state.nested_histories[0], 1, 4, "https://frame.example/b"sv); + EXPECT_EQ(history.entry_for_step(4), nullptr); +} + +TEST_CASE(matching_snapshot_missing_nested_history_descriptor_is_invalid) +{ + WebView::TraversableSessionHistory history; + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv)); + history.navigate(parse_url("https://c.example/"sv)); + history.traverse_to(1); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1, 2 }, 2); + + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::InvalidSnapshot); + EXPECT_EQ(history.size(), 3uz); + EXPECT_EQ(history.used_step_count(), 3uz); + EXPECT(history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_entry(history, 0, 0, "https://a.example/"sv); + expect_entry(history, 1, 1, "https://b.example/"sv); + expect_entry(history, 2, 2, "https://c.example/"sv); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_used_step(history, 2, 2); +} + +TEST_CASE(history_log_entries_marks_current_entries_steps_and_reload_pending) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry_with_post_resource(1, "https://b.example/"sv), + entry_with_reload_pending(2, "https://c.example/"sv, 7, "main"sv, { + nested_history("frame"sv, { + entry(3, "https://frame.example/"sv), + }), + }), + }, + { 0, 1, 2, 3 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + EXPECT_EQ(history_log_entries(history), ByteString { "entries=[0:0:https://a.example/, *1:1:https://b.example/ " + "document_state={id=0, resource=post}, 2:2:https://c.example/ " + "document_state={id=1, reload_pending=true, target_name=main} " + "nested={frame=[3:https://frame.example/]}] " + "used_steps=[0:0, *1:1, 2:2, 3:3]"sv }); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + auto serialized_entry = WebView::history_json_entry(*current_entry, true); + EXPECT_EQ(serialized_entry.get_string("resource"sv), "post"sv); + + auto* reload_pending_entry = history.entry_at(2); + VERIFY(reload_pending_entry); + auto serialized_reload_pending_entry = WebView::history_json_entry(*reload_pending_entry, false); + EXPECT_EQ(serialized_reload_pending_entry.get_bool("reloadPending"sv), true); + EXPECT_EQ(serialized_reload_pending_entry.get_string("resource"sv), "none"sv); +} + +TEST_CASE(history_log_entries_marks_nested_histories) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv, 7, "main"sv, { + nested_history("frame"sv, { + entry(2, "https://frame.example/"sv), + }), + }), + }, + { 0, 1, 2 }, 2); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + EXPECT_EQ(history_log_entries(history), ByteString { "entries=[0:0:https://a.example/, *1:1:https://b.example/ " + "document_state={id=1, target_name=main} " + "nested={frame=[2:https://frame.example/]}] " + "used_steps=[0:0, 1:1, *2:2]"sv }); +} + +TEST_CASE(web_content_known_entries_describe_current_web_content_view) +{ + WebView::TraversableSessionHistory history; + + auto initial_update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + EXPECT_EQ(initial_update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.traverse_to(0); + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + }, + { 0 }, 0); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::MergedPartialSnapshot); + + auto web_content_known_entries = history.web_content_known_entries(); + EXPECT_EQ(history_log_entries(history), ByteString { "entries=[*0:0:https://a.example/, 1:1:https://b.example/] used_steps=[*0:0, 1:1]"sv }); + EXPECT_EQ(WebView::history_log_entries(web_content_known_entries, 0), ByteString { "[*0:0:https://a.example/]"sv }); +} + +TEST_CASE(clear_current_entry_reload_pending) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry_with_reload_pending(1, "https://b.example/"sv, 7, "main"sv, {}), + }, + { 0, 1 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT(current_entry->document_state.reload_pending); + + history.clear_current_entry_reload_pending(); + + current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT(!current_entry->document_state.reload_pending); +} + +TEST_CASE(mark_current_entry_reload_pending) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv), + entry(1, "https://b.example/"sv), + }, + { 0, 1 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT(!current_entry->document_state.reload_pending); + + history.mark_current_entry_reload_pending(); + + current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT(current_entry->document_state.reload_pending); +} + +TEST_CASE(navigate_preserves_document_resource) +{ + WebView::TraversableSessionHistory history; + + history.navigate(parse_url("https://a.example/"sv)); + history.navigate(parse_url("https://b.example/"sv), Web::HTML::POSTResource { + .request_body = MUST(ByteBuffer::copy("name=ladybird"sv.bytes())), + .request_content_type = Web::HTML::POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded, + }); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + expect_entry_resource(*current_entry, "post"sv); +} + +TEST_CASE(replace_current_entry_preserves_document_resource) +{ + WebView::TraversableSessionHistory history; + + history.navigate(parse_url("https://a.example/"sv)); + history.replace_current_entry(parse_url("https://b.example/"sv), Web::HTML::POSTResource { + .request_body = MUST(ByteBuffer::copy("name=ladybird"sv.bytes())), + .request_content_type = Web::HTML::POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded, + }); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT_EQ(current_entry->url, parse_url("https://b.example/"sv)); + expect_entry_resource(*current_entry, "post"sv); +} + +TEST_CASE(replace_current_entry_discards_replaced_document_state) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 7, "main"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 0); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.replace_current_entry(parse_url("https://b.example/"sv), Web::HTML::POSTResource { + .request_body = MUST(ByteBuffer::copy("name=ladybird"sv.bytes())), + .request_content_type = Web::HTML::POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded, + }); + + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 2uz); + EXPECT(!history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_current_entry(history, 0, "https://b.example/"sv); + expect_entry(history, 1, 2, "https://c.example/"sv); + expect_used_step(history, 0, 0); + expect_used_step(history, 1, 2); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT_EQ(current_entry->document_state.id, 0u); + EXPECT(current_entry->document_state.navigable_target_name.is_empty()); + EXPECT(current_entry->document_state.nested_histories.is_empty()); + expect_entry_resource(*current_entry, "post"sv); +} + +TEST_CASE(replace_current_entry_at_nested_step_keeps_current_step_valid) +{ + WebView::TraversableSessionHistory history; + + auto update_result = history.update_from_web_content({ + entry(0, "https://a.example/"sv, 7, "main"sv, { + nested_history("frame-1"sv, { + entry(0, "https://frame.example/a"sv), + entry(1, "https://frame.example/b"sv), + }), + }), + entry(2, "https://c.example/"sv), + }, + { 0, 1, 2 }, 1); + EXPECT_EQ(update_result, WebView::TraversableSessionHistory::UpdateResult::CompleteSnapshot); + + history.replace_current_entry(parse_url("https://b.example/"sv), Empty {}); + + EXPECT_EQ(history.size(), 2uz); + EXPECT_EQ(history.used_step_count(), 2uz); + EXPECT(!history.can_go_back()); + EXPECT(history.can_go_forward()); + expect_current_entry(history, 1, "https://b.example/"sv); + expect_entry(history, 1, 2, "https://c.example/"sv); + expect_used_step(history, 0, 1); + expect_used_step(history, 1, 2); + EXPECT(history.current_step_is_top_level_entry()); + EXPECT(!history.current_step_to_restore_after_loading_top_level_entry().has_value()); + + auto* current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT(current_entry->document_state.nested_histories.is_empty()); +} + +TEST_CASE(replace_current_entry_url_keeps_document_resource) +{ + WebView::TraversableSessionHistory history; + + history.navigate(parse_url("https://a.example/"sv), Web::HTML::POSTResource { + .request_body = MUST(ByteBuffer::copy("name=ladybird"sv.bytes())), + .request_content_type = Web::HTML::POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded, + }); + history.replace_current_entry_url(parse_url("https://b.example/"sv)); + + auto current_entry = history.current_entry(); + VERIFY(current_entry); + EXPECT_EQ(current_entry->url, parse_url("https://b.example/"sv)); + expect_entry_resource(*current_entry, "post"sv); +}