From 496c88d0c9cdbd6bb5d4dfd0ca780f15170957aa Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 28 Apr 2026 14:21:17 -0400 Subject: [PATCH] LibWebView: Add about:history to view, query and mangage browser history This adds a WebUI to view the local browsing history, with controls to search and delete entries. The APIs used to search history are paginated to prevent excessive query sizes. --- Base/res/ladybird/about-pages/history.html | 569 +++++++++++++++++++++ Libraries/LibURL/InternalURLs.h | 1 + Libraries/LibWebView/CMakeLists.txt | 1 + Libraries/LibWebView/HistoryStore.cpp | 223 ++++++++ Libraries/LibWebView/HistoryStore.h | 15 + Libraries/LibWebView/WebUI.cpp | 3 + Libraries/LibWebView/WebUI/HistoryUI.cpp | 130 +++++ Libraries/LibWebView/WebUI/HistoryUI.h | 24 + Tests/LibWebView/TestHistoryStore.cpp | 125 +++++ UI/cmake/ResourceFiles.cmake | 1 + 10 files changed, 1092 insertions(+) create mode 100644 Base/res/ladybird/about-pages/history.html create mode 100644 Libraries/LibWebView/WebUI/HistoryUI.cpp create mode 100644 Libraries/LibWebView/WebUI/HistoryUI.h diff --git a/Base/res/ladybird/about-pages/history.html b/Base/res/ladybird/about-pages/history.html new file mode 100644 index 0000000000..24738b0433 --- /dev/null +++ b/Base/res/ladybird/about-pages/history.html @@ -0,0 +1,569 @@ + + + + History + + + + + + +
+ + + + +

History

+
+ +
+ + + +
+ +
+
+
Loading history...
+ +
+ +
+
+ + + + diff --git a/Libraries/LibURL/InternalURLs.h b/Libraries/LibURL/InternalURLs.h index 34d24d79fd..45c33af793 100644 --- a/Libraries/LibURL/InternalURLs.h +++ b/Libraries/LibURL/InternalURLs.h @@ -14,6 +14,7 @@ namespace URL { #define ENUMERATE_INTERNAL_URLS \ __URL_ENUMERATE(bookmarks) \ + __URL_ENUMERATE(history) \ __URL_ENUMERATE(newtab) \ __URL_ENUMERATE(processes) \ __URL_ENUMERATE(settings) \ diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index d228f24dac..e29b8d861b 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -33,6 +33,7 @@ set(SOURCES WorkerProcessManager.cpp WebUI.cpp WebUI/BookmarksUI.cpp + WebUI/HistoryUI.cpp WebUI/ProcessesUI.cpp WebUI/SettingsUI.cpp WebUI/VersionUI.cpp diff --git a/Libraries/LibWebView/HistoryStore.cpp b/Libraries/LibWebView/HistoryStore.cpp index efbe42304d..ff2314a113 100644 --- a/Libraries/LibWebView/HistoryStore.cpp +++ b/Libraries/LibWebView/HistoryStore.cpp @@ -214,8 +214,41 @@ ErrorOr> HistoryStore::create(Database::Database& da url ASC LIMIT ?4; )#"sv)); + statements.list_entries = TRY(database.prepare_statement(R"#( + SELECT url, title, visit_count, last_visited_time, favicon + FROM ( + SELECT + url, + title, + visit_count, + last_visited_time, + COALESCE(favicon, '') AS favicon, + CASE + WHEN LOWER(CASE + WHEN INSTR(url, '://') > 0 THEN SUBSTR(url, INSTR(url, '://') + 3) + ELSE url + END) LIKE 'www.%' + THEN SUBSTR(CASE + WHEN INSTR(url, '://') > 0 THEN SUBSTR(url, INSTR(url, '://') + 3) + ELSE url + END, 5) + ELSE CASE + WHEN INSTR(url, '://') > 0 THEN SUBSTR(url, INSTR(url, '://') + 3) + ELSE url + END + END AS searchable_url + FROM History + ) + WHERE ((?1 = '' AND ?2 = '') + OR (?1 != '' AND INSTR(LOWER(title), LOWER(?1)) > 0) + OR (?2 != '' AND INSTR(LOWER(searchable_url), LOWER(?2)) > 0)) + ORDER BY last_visited_time DESC, url ASC + LIMIT ?3 OFFSET ?4; + )#"sv)); statements.clear_entries = TRY(database.prepare_statement("DELETE FROM History;"sv)); + statements.delete_entry = TRY(database.prepare_statement("DELETE FROM History WHERE url = ?;"sv)); statements.delete_entries_accessed_since = TRY(database.prepare_statement("DELETE FROM History WHERE last_visited_time >= ?;"sv)); + statements.all_urls = TRY(database.prepare_statement("SELECT url FROM History;"sv)); return adopt_own(*new HistoryStore { adopt_own(*new PersistedStorage { database, move(statements) }) }); } @@ -421,6 +454,28 @@ Vector HistoryStore::autocomplete_entries(StringView query, size_t return entries; } +Vector HistoryStore::list_entries(StringView query, size_t offset, size_t limit) +{ + if (m_is_disabled || limit == 0) + return {}; + + auto title_query = query.trim_whitespace(); + auto url_query = autocomplete_url_query(title_query); + + auto entries = m_storage->list_entries(title_query, url_query, offset, limit); + + dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] {} history page entries for '{}' (title_query='{}', url_query='{}', offset={}, limit={}): {}", + m_storage->name(), + title_query, + title_query, + url_query, + offset, + limit, + log_history_entries(entries)); + + return entries; +} + void HistoryStore::clear() { if (m_is_disabled) @@ -431,6 +486,65 @@ void HistoryStore::clear() m_recently_closed_entries.clear(); } +void HistoryStore::remove_entry_for_url(URL::URL const& url) +{ + if (m_is_disabled) + return; + + auto normalized_url = normalize_url(url); + if (!normalized_url.has_value()) + return; + + dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] Removing history entry for '{}'", *normalized_url); + m_storage->remove_entry_for_url(*normalized_url); +} + +static Optional site_key_for_history_entry(URL::URL const& url) +{ + if (!url.host().has_value() || url.host()->is_empty_host()) + return {}; + + if (auto registrable_domain = url.host()->registrable_domain(); registrable_domain.has_value()) + return registrable_domain.release_value(); + + return url.serialized_host(); +} + +static bool history_entry_matches_site_key(StringView entry_url, StringView site_key) +{ + auto parsed_url = URL::Parser::basic_parse(entry_url); + if (!parsed_url.has_value()) + return false; + + auto const& host = parsed_url->host(); + if (!host.has_value() || host->is_empty_host()) + return false; + + auto serialized_host = parsed_url->serialized_host(); + auto serialized_host_view = serialized_host.bytes_as_string_view(); + if (serialized_host_view.equals_ignoring_ascii_case(site_key)) + return true; + + return serialized_host_view.length() > site_key.length() + && serialized_host_view.ends_with(site_key, CaseSensitivity::CaseInsensitive) + && serialized_host_view[serialized_host_view.length() - site_key.length() - 1] == '.'; +} + +void HistoryStore::remove_entries_for_same_site(URL::URL const& url) +{ + if (m_is_disabled) + return; + + auto site_key = site_key_for_history_entry(url); + if (!site_key.has_value()) { + remove_entry_for_url(url); + return; + } + + dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] Removing history entries for site '{}'", *site_key); + m_storage->remove_entries_for_same_site(*site_key); +} + void HistoryStore::remove_entries_accessed_since(UnixDateTime since) { if (m_is_disabled) @@ -515,11 +629,70 @@ Vector HistoryStore::TransientStorage::autocomplete_entries(String return entries; } +static bool matches_history_page_query(HistoryEntry const& entry, StringView title_query, StringView url_query) +{ + if (title_query.is_empty() && url_query.is_empty()) + return true; + + auto searchable_url = autocomplete_searchable_url(entry.url.bytes_as_string_view()); + if (!url_query.is_empty() && searchable_url.contains(url_query, CaseSensitivity::CaseInsensitive)) + return true; + + return !title_query.is_empty() + && entry.title.has_value() + && entry.title->contains(title_query, CaseSensitivity::CaseInsensitive); +} + +static void sort_entries_for_history_page(Vector& matches) +{ + quick_sort(matches, [](auto const* left, auto const* right) { + if (left->last_visited_time != right->last_visited_time) + return left->last_visited_time > right->last_visited_time; + return left->url < right->url; + }); +} + +Vector HistoryStore::TransientStorage::list_entries(StringView title_query, StringView url_query, size_t offset, size_t limit) +{ + Vector matches; + + for (auto const& entry : m_entries) { + if (matches_history_page_query(entry.value, title_query, url_query)) + matches.append(&entry.value); + } + + sort_entries_for_history_page(matches); + + Vector entries; + if (offset >= matches.size()) + return entries; + + auto end = min(matches.size(), offset + limit); + entries.ensure_capacity(end - offset); + + for (size_t i = offset; i < end; ++i) + entries.unchecked_append(*matches[i]); + + return entries; +} + void HistoryStore::TransientStorage::clear() { m_entries.clear(); } +void HistoryStore::TransientStorage::remove_entry_for_url(String const& url) +{ + m_entries.remove(url); +} + +void HistoryStore::TransientStorage::remove_entries_for_same_site(StringView site_key) +{ + m_entries.remove_all_matching([&](auto const&, auto const& entry) { + return history_entry_matches_site_key(entry.url, site_key); + }); +} + void HistoryStore::TransientStorage::remove_entries_accessed_since(UnixDateTime since) { m_entries.remove_all_matching([&](auto const&, auto const& entry) { @@ -616,11 +789,61 @@ Vector HistoryStore::PersistedStorage::autocomplete_entries(String return entries; } +Vector HistoryStore::PersistedStorage::list_entries(StringView title_query, StringView url_query, size_t offset, size_t limit) +{ + Vector entries; + entries.ensure_capacity(limit); + auto title_query_string = MUST(String::from_utf8(title_query)); + auto url_query_string = MUST(String::from_utf8(url_query)); + + m_database.execute_statement( + m_statements.list_entries, + [&](auto statement_id) { + auto title = m_database.result_column(statement_id, 1); + auto favicon = m_database.result_column(statement_id, 4); + + entries.append(HistoryEntry { + .url = m_database.result_column(statement_id, 0), + .title = title.is_empty() ? Optional {} : Optional { move(title) }, + .favicon_base64_png = favicon.is_empty() ? Optional {} : Optional { move(favicon) }, + .visit_count = m_database.result_column(statement_id, 2), + .last_visited_time = m_database.result_column(statement_id, 3), + }); + }, + title_query_string, + url_query_string, + static_cast(limit), + static_cast(offset)); + + return entries; +} + void HistoryStore::PersistedStorage::clear() { m_database.execute_statement(m_statements.clear_entries, {}); } +void HistoryStore::PersistedStorage::remove_entry_for_url(String const& url) +{ + m_database.execute_statement(m_statements.delete_entry, {}, url); +} + +void HistoryStore::PersistedStorage::remove_entries_for_same_site(StringView site_key) +{ + Vector urls_to_remove; + + m_database.execute_statement( + m_statements.all_urls, + [&](auto statement_id) { + auto url = m_database.result_column(statement_id, 0); + if (history_entry_matches_site_key(url.bytes_as_string_view(), site_key)) + urls_to_remove.append(move(url)); + }); + + for (auto const& url : urls_to_remove) + remove_entry_for_url(url); +} + void HistoryStore::PersistedStorage::remove_entries_accessed_since(UnixDateTime since) { m_database.execute_statement(m_statements.delete_entries_accessed_since, {}, since); diff --git a/Libraries/LibWebView/HistoryStore.h b/Libraries/LibWebView/HistoryStore.h index 48fec5aa4c..f1c3e3c90f 100644 --- a/Libraries/LibWebView/HistoryStore.h +++ b/Libraries/LibWebView/HistoryStore.h @@ -57,8 +57,11 @@ public: Optional entry_for_url(URL::URL const&); Vector autocomplete_entries(StringView query, size_t limit = 8); + Vector list_entries(StringView query = {}, size_t offset = 0, size_t limit = 50); void clear(); + void remove_entry_for_url(URL::URL const&); + void remove_entries_for_same_site(URL::URL const&); void remove_entries_accessed_since(UnixDateTime since); private: @@ -68,8 +71,11 @@ private: Database::StatementID update_favicon { 0 }; Database::StatementID get_entry { 0 }; Database::StatementID search_entries { 0 }; + Database::StatementID list_entries { 0 }; Database::StatementID clear_entries { 0 }; + Database::StatementID delete_entry { 0 }; Database::StatementID delete_entries_accessed_since { 0 }; + Database::StatementID all_urls { 0 }; }; class StorageImpl { @@ -84,8 +90,11 @@ private: virtual Optional entry_for_url(String const& url) = 0; virtual Vector autocomplete_entries(StringView title_query, StringView url_query, size_t limit) = 0; + virtual Vector list_entries(StringView title_query, StringView url_query, size_t offset, size_t limit) = 0; virtual void clear() = 0; + virtual void remove_entry_for_url(String const& url) = 0; + virtual void remove_entries_for_same_site(StringView site_key) = 0; virtual void remove_entries_accessed_since(UnixDateTime since) = 0; }; @@ -101,8 +110,11 @@ private: virtual Optional entry_for_url(String const& url) override; virtual Vector autocomplete_entries(StringView title_query, StringView url_query, size_t limit) override; + virtual Vector list_entries(StringView title_query, StringView url_query, size_t offset, size_t limit) override; virtual void clear() override; + virtual void remove_entry_for_url(String const& url) override; + virtual void remove_entries_for_same_site(StringView site_key) override; virtual void remove_entries_accessed_since(UnixDateTime since) override; private: @@ -122,8 +134,11 @@ private: virtual Optional entry_for_url(String const& url) override; virtual Vector autocomplete_entries(StringView title_query, StringView url_query, size_t limit) override; + virtual Vector list_entries(StringView title_query, StringView url_query, size_t offset, size_t limit) override; virtual void clear() override; + virtual void remove_entry_for_url(String const& url) override; + virtual void remove_entries_for_same_site(StringView site_key) override; virtual void remove_entries_accessed_since(UnixDateTime since) override; private: diff --git a/Libraries/LibWebView/WebUI.cpp b/Libraries/LibWebView/WebUI.cpp index 19c7e44c45..24a512115e 100644 --- a/Libraries/LibWebView/WebUI.cpp +++ b/Libraries/LibWebView/WebUI.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,8 @@ ErrorOr> WebUI::create(WebContentClient& client, u64 page_id, Stri if (host == "bookmarks"sv) web_ui = TRY(create_web_ui(client, page_id, move(host))); + else if (host == "history"sv) + web_ui = TRY(create_web_ui(client, page_id, move(host))); else if (host == "processes"sv) web_ui = TRY(create_web_ui(client, page_id, move(host))); else if (host == "settings"sv) diff --git a/Libraries/LibWebView/WebUI/HistoryUI.cpp b/Libraries/LibWebView/WebUI/HistoryUI.cpp new file mode 100644 index 0000000000..14c1b5773d --- /dev/null +++ b/Libraries/LibWebView/WebUI/HistoryUI.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +#include + +namespace WebView { + +static constexpr size_t DEFAULT_HISTORY_PAGE_SIZE = 50; +static constexpr size_t MAX_HISTORY_PAGE_SIZE = 100; + +static Optional site_key_for_entry(HistoryEntry const& entry) +{ + auto parsed_url = URL::Parser::basic_parse(entry.url); + if (!parsed_url.has_value()) + return {}; + + if (!parsed_url->host().has_value() || parsed_url->host()->is_empty_host()) + return {}; + + if (auto registrable_domain = parsed_url->host()->registrable_domain(); registrable_domain.has_value()) + return registrable_domain.release_value(); + + return parsed_url->serialized_host(); +} + +static JsonObject serialize_history_entry(HistoryEntry const& entry) +{ + JsonObject serialized; + serialized.set("url"sv, entry.url); + serialized.set("title"sv, entry.title.value_or(String {})); + serialized.set("faviconBase64Png"sv, entry.favicon_base64_png.value_or(String {})); + serialized.set("visitCount"sv, entry.visit_count); + serialized.set("lastVisitedTime"sv, entry.last_visited_time.milliseconds_since_epoch()); + serialized.set("siteKey"sv, site_key_for_entry(entry).value_or(String {})); + return serialized; +} + +void HistoryUI::register_interfaces() +{ + register_interface("loadHistoryEntries"sv, [this](auto const& data) { + load_history_entries(data); + }); + register_interface("removeHistoryEntry"sv, [this](auto const& data) { + remove_history_entry(data); + }); + register_interface("forgetHistorySite"sv, [this](auto const& data) { + forget_history_site(data); + }); +} + +void HistoryUI::load_history_entries(JsonValue const& data) +{ + if (!data.is_object()) + return; + + auto const& object = data.as_object(); + + auto offset = object.get_integer("offset"sv).value_or(0); + auto limit = object.get_integer("limit"sv); + auto request_id = object.get_integer("requestId"sv).value_or(0); + auto query = object.get_string("query"sv).value_or(String {}); + + if (limit.has_value()) + limit = min(*limit, MAX_HISTORY_PAGE_SIZE); + else + limit = DEFAULT_HISTORY_PAGE_SIZE; + + auto entries = Application::history_store().list_entries(query, offset, *limit + 1); + auto has_more = entries.size() > *limit; + if (has_more) + entries.resize(*limit); + + JsonArray serialized_entries; + serialized_entries.ensure_capacity(entries.size()); + for (auto const& entry : entries) + serialized_entries.must_append(serialize_history_entry(entry)); + + JsonObject result; + result.set("requestId"sv, request_id); + result.set("query"sv, query); + result.set("offset"sv, offset); + result.set("hasMore"sv, has_more); + result.set("entries"sv, move(serialized_entries)); + + async_send_message("loadHistoryEntries"sv, move(result)); +} + +void HistoryUI::remove_history_entry(JsonValue const& data) +{ + if (!data.is_object()) + return; + + auto url = data.as_object().get_string("url"sv); + if (!url.has_value()) + return; + + auto parsed_url = URL::Parser::basic_parse(*url); + if (!parsed_url.has_value()) + return; + + Application::history_store().remove_entry_for_url(*parsed_url); +} + +void HistoryUI::forget_history_site(JsonValue const& data) +{ + if (!data.is_object()) + return; + + auto url = data.as_object().get_string("url"sv); + if (!url.has_value()) + return; + + auto parsed_url = URL::Parser::basic_parse(*url); + if (!parsed_url.has_value()) + return; + + Application::history_store().remove_entries_for_same_site(*parsed_url); +} + +} diff --git a/Libraries/LibWebView/WebUI/HistoryUI.h b/Libraries/LibWebView/WebUI/HistoryUI.h new file mode 100644 index 0000000000..787f04e944 --- /dev/null +++ b/Libraries/LibWebView/WebUI/HistoryUI.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace WebView { + +class HistoryUI final : public WebUI { + WEB_UI(HistoryUI); + +private: + virtual void register_interfaces() override; + + void load_history_entries(JsonValue const&); + void remove_history_entry(JsonValue const&); + void forget_history_site(JsonValue const&); +}; + +} diff --git a/Tests/LibWebView/TestHistoryStore.cpp b/Tests/LibWebView/TestHistoryStore.cpp index 03acb0ca04..d3247ac709 100644 --- a/Tests/LibWebView/TestHistoryStore.cpp +++ b/Tests/LibWebView/TestHistoryStore.cpp @@ -87,6 +87,59 @@ static void expect_history_autocomplete_entries_include_metadata(WebView::Histor EXPECT_EQ(entries[0].last_visited_time, UnixDateTime::from_seconds_since_epoch(20)); } +static void expect_history_page_entries_are_paginated_and_searchable(WebView::HistoryStore& store) +{ + store.record_visit(parse_url("https://www.alpha.example.com/path"sv), "Alpha docs"_string, UnixDateTime::from_seconds_since_epoch(10)); + store.record_visit(parse_url("https://beta.example.com/"sv), "Beta page"_string, UnixDateTime::from_seconds_since_epoch(30)); + store.record_visit(parse_url("https://gamma.example.com/search"sv), "Gamma search"_string, UnixDateTime::from_seconds_since_epoch(20)); + store.record_visit(parse_url("https://docs.ladybird.dev/"sv), "Ladybird docs"_string, UnixDateTime::from_seconds_since_epoch(40)); + + auto paginated_entries = store.list_entries({}, 1, 2); + VERIFY(paginated_entries.size() == 2); + EXPECT_EQ(paginated_entries[0].url, "https://beta.example.com/"_string); + EXPECT_EQ(paginated_entries[1].url, "https://gamma.example.com/search"_string); + + auto url_search_entries = store.list_entries("https://www.alpha"sv, 0, 10); + VERIFY(url_search_entries.size() == 1); + EXPECT_EQ(url_search_entries[0].url, "https://www.alpha.example.com/path"_string); + + auto title_search_entries = store.list_entries("docs"sv, 0, 10); + VERIFY(title_search_entries.size() == 2); + EXPECT_EQ(title_search_entries[0].url, "https://docs.ladybird.dev/"_string); + EXPECT_EQ(title_search_entries[1].url, "https://www.alpha.example.com/path"_string); +} + +static void expect_history_entries_can_be_removed(WebView::HistoryStore& store) +{ + auto example_url = parse_url("https://example.com/"sv); + auto other_url = parse_url("https://other.example.com/"sv); + + store.record_visit(example_url, "Example"_string, UnixDateTime::from_seconds_since_epoch(10)); + store.record_visit(other_url, "Other"_string, UnixDateTime::from_seconds_since_epoch(20)); + + store.remove_entry_for_url(example_url); + + EXPECT(!store.entry_for_url(example_url).has_value()); + EXPECT(store.entry_for_url(other_url).has_value()); +} + +static void expect_history_entries_for_same_site_can_be_removed(WebView::HistoryStore& store) +{ + auto example_url = parse_url("https://www.example.com/"sv); + auto subdomain_url = parse_url("https://docs.example.com/guide"sv); + auto other_url = parse_url("https://ladybird.dev/"sv); + + store.record_visit(example_url, "Example"_string, UnixDateTime::from_seconds_since_epoch(10)); + store.record_visit(subdomain_url, "Docs"_string, UnixDateTime::from_seconds_since_epoch(20)); + store.record_visit(other_url, "Ladybird"_string, UnixDateTime::from_seconds_since_epoch(30)); + + store.remove_entries_for_same_site(example_url); + + EXPECT(!store.entry_for_url(example_url).has_value()); + EXPECT(!store.entry_for_url(subdomain_url).has_value()); + EXPECT(store.entry_for_url(other_url).has_value()); +} + TEST_CASE(record_and_lookup_history_entries) { auto store = WebView::HistoryStore::create(); @@ -271,6 +324,12 @@ TEST_CASE(history_autocomplete_entries_include_metadata) expect_history_autocomplete_entries_include_metadata(*store); } +TEST_CASE(history_page_entries_are_paginated_and_searchable) +{ + auto store = WebView::HistoryStore::create(); + expect_history_page_entries_are_paginated_and_searchable(*store); +} + TEST_CASE(non_browsable_urls_are_not_recorded) { auto store = WebView::HistoryStore::create(); @@ -321,6 +380,18 @@ TEST_CASE(history_entries_accessed_since_can_be_removed) EXPECT(!store->entry_for_url(newer_url).has_value()); } +TEST_CASE(history_entries_can_be_removed) +{ + auto store = WebView::HistoryStore::create(); + expect_history_entries_can_be_removed(*store); +} + +TEST_CASE(history_entries_for_same_site_can_be_removed) +{ + auto store = WebView::HistoryStore::create(); + expect_history_entries_for_same_site_can_be_removed(*store); +} + TEST_CASE(persisted_history_survives_reopen) { auto database_directory = ByteString::formatted( @@ -457,3 +528,57 @@ TEST_CASE(persisted_history_autocomplete_entries_include_metadata) expect_history_autocomplete_entries_include_metadata(*store); } + +TEST_CASE(persisted_history_page_entries_are_paginated_and_searchable) +{ + auto database_directory = ByteString::formatted( + "{}/ladybird-history-store-page-list-test-{}", + Core::StandardPaths::tempfile_directory(), + generate_random_uuid()); + TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes)); + + auto cleanup = ScopeGuard([&] { + MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed)); + }); + + auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv)); + auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database)); + + expect_history_page_entries_are_paginated_and_searchable(*store); +} + +TEST_CASE(persisted_history_entries_can_be_removed) +{ + auto database_directory = ByteString::formatted( + "{}/ladybird-history-store-remove-entry-test-{}", + Core::StandardPaths::tempfile_directory(), + generate_random_uuid()); + TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes)); + + auto cleanup = ScopeGuard([&] { + MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed)); + }); + + auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv)); + auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database)); + + expect_history_entries_can_be_removed(*store); +} + +TEST_CASE(persisted_history_entries_for_same_site_can_be_removed) +{ + auto database_directory = ByteString::formatted( + "{}/ladybird-history-store-remove-site-test-{}", + Core::StandardPaths::tempfile_directory(), + generate_random_uuid()); + TRY_OR_FAIL(Core::Directory::create(database_directory, Core::Directory::CreateDirectories::Yes)); + + auto cleanup = ScopeGuard([&] { + MUST(FileSystem::remove(database_directory, FileSystem::RecursionMode::Allowed)); + }); + + auto database = TRY_OR_FAIL(Database::Database::create(database_directory, "HistoryStore"sv)); + auto store = TRY_OR_FAIL(WebView::HistoryStore::create(*database)); + + expect_history_entries_for_same_site_can_be_removed(*store); +} diff --git a/UI/cmake/ResourceFiles.cmake b/UI/cmake/ResourceFiles.cmake index 871cace7f3..a23bf2311f 100644 --- a/UI/cmake/ResourceFiles.cmake +++ b/UI/cmake/ResourceFiles.cmake @@ -32,6 +32,7 @@ list(TRANSFORM INTERNAL_RESOURCES PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/ladyb set(ABOUT_PAGES about.html bookmarks.html + history.html newtab.html processes.html settings.html