LibDevTools+LibWeb: Show IndexedDB in DevTools

Firefox asks the storage watcher for an indexed-db resource before it
shows IndexedDB entries in the Storage panel. Add an IndexedDB actor and
serialize the live LibWeb database registry on demand, so WebContent can
return the host tree and table rows without duplicating database state.

Use the LibWeb inspection helpers to read IndexedDB internals, and keep
the Firefox protocol shape in LibDevTools. WebContent only forwards the
serialized response over the existing DevTools IPC path.
This commit is contained in:
Sam Atkins 2026-06-17 12:39:37 +01:00 committed by Jelle Raaijmakers
parent d48bb33fc7
commit cb47dbfc7a
23 changed files with 949 additions and 7 deletions

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/IndexedDBActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/DevToolsServer.h>
#include <LibDevTools/StorageHelpers.h>
namespace DevTools {
NonnullRefPtr<IndexedDBActor> IndexedDBActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab)
{
return adopt_ref(*new IndexedDBActor(devtools, move(name), move(tab)));
}
IndexedDBActor::IndexedDBActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab)
: Actor(devtools, move(name))
, m_tab(move(tab))
{
}
IndexedDBActor::~IndexedDBActor() = default;
JsonObject IndexedDBActor::serialize_storage(JsonObject hosts) const
{
if (hosts.is_empty()) {
if (auto tab = m_tab.strong_ref()) {
if (auto host = storage_host_for_url(tab->description().url); host.has_value())
hosts.set(*host, JsonArray {});
}
}
JsonObject traits;
traits.set("supportsAddItem"sv, false);
traits.set("supportsRemoveAll"sv, true);
traits.set("supportsRemoveAllSessionCookies"sv, false);
traits.set("supportsRemoveItem"sv, true);
JsonObject storage;
storage.set("actor"sv, name());
if (auto tab = m_tab.strong_ref()) {
storage.set("browsingContextID"sv, tab->description().id);
storage.set("innerWindowId"sv, tab->inner_window_id());
storage.set("resourceId"sv, MUST(String::formatted("indexed-db-{}", tab->inner_window_id())));
}
storage.set("hosts"sv, move(hosts));
storage.set("resourceKey"sv, "indexedDB"sv);
storage.set("traits"sv, move(traits));
return storage;
}
void IndexedDBActor::get_storage_resource(Function<void(JsonObject)> callback)
{
auto tab = m_tab.strong_ref();
if (!tab) {
callback(serialize_storage({}));
return;
}
devtools().delegate().inspect_indexed_database_storage(tab->description(),
[weak_self = make_weak_ptr<IndexedDBActor>(), callback = move(callback)](ErrorOr<JsonObject> hosts_or_error) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
callback(self->serialize_storage(hosts_or_error.is_error() ? JsonObject {} : hosts_or_error.release_value()));
});
}
void IndexedDBActor::handle_message(Message const& message)
{
if (message.type == "getFields"sv) {
get_fields(message);
return;
}
if (message.type == "getStoreObjects"sv) {
get_store_objects(message);
return;
}
send_unrecognized_packet_type_error(message);
}
void IndexedDBActor::get_fields(Message const& message)
{
auto sub_type = message.data.get_string("subType"sv);
JsonArray fields;
if (sub_type == "database"sv) {
fields.must_append(define_storage_field("objectStore"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("keyPath"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("autoIncrement"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("indexes"sv, StorageFieldType::Immutable));
} else if (sub_type == "object store"sv) {
fields.must_append(define_storage_field("name"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("value"sv, StorageFieldType::Immutable));
} else {
fields.must_append(define_storage_field("uniqueKey"sv, StorageFieldType::Private));
fields.must_append(define_storage_field("db"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("storage"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("origin"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("version"sv, StorageFieldType::Immutable));
fields.must_append(define_storage_field("objectStores"sv, StorageFieldType::Immutable));
}
JsonObject response;
response.set("value"sv, move(fields));
send_response(message, move(response));
}
void IndexedDBActor::send_inspection_error(Message const& message, Error const& error)
{
JsonObject response;
response.set("error"sv, "indexedDBInspectionFailed"sv);
response.set("message"sv, error.string_literal());
send_response(message, move(response));
}
void IndexedDBActor::get_store_objects(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
if (!host.has_value())
return;
auto tab = m_tab.strong_ref();
if (!tab) {
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
return;
}
Optional<JsonArray> names;
if (auto names_array = message.data.get_array("names"sv); names_array.has_value())
names = *names_array;
auto options = message.data.get_object("options"sv).value_or({});
devtools().delegate().inspect_indexed_database_objects(tab->description(), *host, move(names), move(options),
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id](ErrorOr<JsonObject> result) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
if (result.is_error()) {
self->send_inspection_error({ .id = message_id }, result.error());
return;
}
self->send_response({ .id = message_id }, result.release_value());
});
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <LibDevTools/Actor.h>
namespace DevTools {
class DEVTOOLS_API IndexedDBActor final : public Actor {
public:
static constexpr auto base_name = "indexed-db"sv;
static NonnullRefPtr<IndexedDBActor> create(DevToolsServer&, String name, WeakPtr<TabActor>);
virtual ~IndexedDBActor() override;
void get_storage_resource(Function<void(JsonObject)>);
private:
IndexedDBActor(DevToolsServer&, String name, WeakPtr<TabActor>);
virtual void handle_message(Message const&) override;
void get_fields(Message const&);
void get_store_objects(Message const&);
JsonObject serialize_storage(JsonObject hosts) const;
void send_inspection_error(Message const&, Error const&);
WeakPtr<TabActor> m_tab;
};
}

View file

@ -12,6 +12,7 @@
#include <LibDevTools/Actors/ConsoleActor.h>
#include <LibDevTools/Actors/CookiesActor.h>
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/IndexedDBActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/NetworkParentActor.h>
#include <LibDevTools/Actors/StorageActor.h>
@ -92,13 +93,14 @@ void WatcherActor::handle_message(Message const& message)
return;
bool should_send_cookie_resources = false;
bool should_send_indexed_db_resources = false;
bool should_send_local_storage_resources = false;
bool should_send_session_storage_resources = false;
if constexpr (DEVTOOLS_DEBUG) {
for (auto const& resource_type : resource_types->values()) {
if (!resource_type.is_string())
continue;
if (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "local-storage"sv, "session-storage"sv))
if (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "indexed-db"sv, "local-storage"sv, "session-storage"sv))
dbgln("Unrecognized `watchResources` resource type: '{}'", resource_type.as_string());
}
}
@ -108,6 +110,9 @@ void WatcherActor::handle_message(Message const& message)
if (resource_type.as_string() == "cookies"sv) {
m_is_watching_cookie_resources = true;
should_send_cookie_resources = true;
} else if (resource_type.as_string() == "indexed-db"sv) {
m_is_watching_indexed_db_resources = true;
should_send_indexed_db_resources = true;
} else if (resource_type.as_string() == "local-storage"sv) {
m_is_watching_local_storage_resources = true;
should_send_local_storage_resources = true;
@ -120,6 +125,8 @@ void WatcherActor::handle_message(Message const& message)
send_response(message, move(response));
if (should_send_cookie_resources)
send_cookies_resource_available_message();
if (should_send_indexed_db_resources)
send_indexed_db_resource_available_message();
if (should_send_local_storage_resources)
send_storage_resource_available_message(local_storage_actor());
if (should_send_session_storage_resources)
@ -164,7 +171,7 @@ JsonObject WatcherActor::serialize_description() const
resources.set("document-event"sv, true);
resources.set("error-message"sv, false);
resources.set("extension-storage"sv, false);
resources.set("indexed-db"sv, false);
resources.set("indexed-db"sv, true);
resources.set("jstracer-state"sv, false);
resources.set("jstracer-trace"sv, false);
resources.set("last-private-context-exit"sv, false);
@ -227,6 +234,8 @@ void WatcherActor::switch_frame_target(FrameActor& previous_target, String const
target.set_pending_navigation_document_events_after_target_switch(url, title);
if (m_is_watching_cookie_resources)
send_cookies_resource_available_message();
if (m_is_watching_indexed_db_resources)
send_indexed_db_resource_available_message();
if (m_is_watching_local_storage_resources)
send_storage_resource_available_message(local_storage_actor());
if (m_is_watching_session_storage_resources)
@ -263,6 +272,15 @@ CookiesActor& WatcherActor::cookies_actor()
return *m_cookies.strong_ref();
}
IndexedDBActor& WatcherActor::indexed_db_actor()
{
if (auto indexed_db = m_indexed_db.strong_ref())
return *indexed_db;
m_indexed_db = devtools().register_actor<IndexedDBActor>(m_tab);
return *m_indexed_db.strong_ref();
}
void WatcherActor::send_cookies_resource_available_message()
{
JsonArray cookies;
@ -317,4 +335,28 @@ void WatcherActor::send_storage_resource_available_message(StorageActor& storage
send_message(move(message));
}
void WatcherActor::send_indexed_db_resource_available_message()
{
indexed_db_actor().get_storage_resource([weak_self = make_weak_ptr<WatcherActor>()](JsonObject indexed_db) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
JsonArray indexed_databases;
indexed_databases.must_append(move(indexed_db));
JsonArray indexed_database_resources;
indexed_database_resources.must_append("indexed-db"sv);
indexed_database_resources.must_append(move(indexed_databases));
JsonArray array;
array.must_append(move(indexed_database_resources));
JsonObject message;
message.set("type"sv, "resources-available-array"sv);
message.set("array"sv, move(array));
self->send_message(move(message));
});
}
}

View file

@ -36,10 +36,13 @@ private:
StorageActor& local_storage_actor();
StorageActor& session_storage_actor();
void send_storage_resource_available_message(StorageActor&);
IndexedDBActor& indexed_db_actor();
void send_indexed_db_resource_available_message();
WeakPtr<TabActor> m_tab;
WeakPtr<FrameActor> m_target;
WeakPtr<CookiesActor> m_cookies;
WeakPtr<IndexedDBActor> m_indexed_db;
WeakPtr<StorageActor> m_local_storage;
WeakPtr<StorageActor> m_session_storage;
WeakPtr<TargetConfigurationActor> m_target_configuration;
@ -47,6 +50,7 @@ private:
WeakPtr<NetworkParentActor> m_network_parent;
bool m_is_watching_frame_targets { false };
bool m_is_watching_cookie_resources { false };
bool m_is_watching_indexed_db_resources { false };
bool m_is_watching_local_storage_resources { false };
bool m_is_watching_session_storage_resources { false };
};

View file

@ -9,6 +9,7 @@ set(SOURCES
Actors/DeviceActor.cpp
Actors/FrameActor.cpp
Actors/HighlighterActor.cpp
Actors/IndexedDBActor.cpp
Actors/InspectorActor.cpp
Actors/LayoutInspectorActor.cpp
Actors/NetworkEventActor.cpp
@ -30,6 +31,7 @@ set(SOURCES
Actors/WatcherActor.cpp
Connection.cpp
DevToolsServer.cpp
IndexedDBSerialization.cpp
StorageHelpers.cpp
)

View file

@ -75,6 +75,10 @@ public:
virtual u64 add_storage_change_listener(TabDescription const&, OnStorageChange) const { return 0; }
virtual void remove_storage_change_listener(TabDescription const&, u64) const { }
using OnIndexedDBInspectionComplete = Function<void(ErrorOr<JsonObject>)>;
virtual void inspect_indexed_database_storage(TabDescription const&, OnIndexedDBInspectionComplete) const { }
virtual void inspect_indexed_database_objects(TabDescription const&, String const&, Optional<JsonArray>, JsonObject, OnIndexedDBInspectionComplete) const { }
using OnTabInspectionComplete = Function<void(ErrorOr<JsonValue>)>;
virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { }

View file

@ -23,6 +23,7 @@ class DevToolsDelegate;
class DevToolsServer;
class FrameActor;
class HighlighterActor;
class IndexedDBActor;
class InspectorActor;
class LayoutInspectorActor;
class NetworkEventActor;

View file

@ -0,0 +1,228 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <LibDevTools/IndexedDBSerialization.h>
#include <LibDevTools/StorageHelpers.h>
#include <LibURL/URL.h>
#include <LibWeb/IndexedDB/Inspection.h>
namespace DevTools::IndexedDB {
static constexpr auto indexed_database_default_storage_name = "default"sv;
static constexpr auto indexed_database_default_storage_suffix = " (default)"sv;
static String database_name_for_devtools(String const& database_name)
{
return MUST(String::formatted("{}{}", database_name, indexed_database_default_storage_suffix));
}
static String database_name_from_devtools(String const& name)
{
auto view = name.bytes_as_string_view();
if (view.ends_with(indexed_database_default_storage_suffix))
return MUST(String::from_utf8(view.substring_view(0, view.length() - indexed_database_default_storage_suffix.length())));
return name;
}
static String indexed_database_path(String const& database_name, Optional<String const&> object_store_name = {}, Optional<JsonValue const&> key = {})
{
JsonArray path;
path.must_append(database_name_for_devtools(database_name));
if (object_store_name.has_value())
path.must_append(*object_store_name);
if (key.has_value())
path.must_append(*key);
return path.serialized();
}
JsonObject serialize_storage(Web::DOM::Document& document)
{
JsonObject hosts;
for (auto const& storage_host : Web::IndexedDB::inspect_indexed_database_storage(document)) {
auto host = storage_host_for_url(storage_host.url);
if (!host.has_value())
continue;
JsonArray names;
for (auto const& database : storage_host.databases) {
if (database.object_store_names.is_empty()) {
names.must_append(indexed_database_path(database.name));
continue;
}
for (auto const& object_store_name : database.object_store_names)
names.must_append(indexed_database_path(database.name, object_store_name));
}
hosts.set(*host, move(names));
}
return hosts;
}
static Optional<Web::IndexedDB::InspectionPath> parse_indexed_database_path(JsonValue const& name)
{
if (!name.is_string())
return {};
auto parsed = JsonValue::from_string(name.as_string());
if (parsed.is_error() || !parsed.value().is_array())
return {};
auto array = parsed.release_value().as_array();
if (array.is_empty() || !array.at(0).is_string())
return {};
Web::IndexedDB::InspectionPath path;
path.database_name = database_name_from_devtools(array.at(0).as_string());
if (array.size() >= 2) {
if (!array.at(1).is_string())
return {};
path.object_store_name = array.at(1).as_string();
}
if (array.size() >= 3)
path.key = array.at(2);
return path;
}
static Optional<Vector<Web::IndexedDB::InspectionPath>> parse_indexed_database_paths(Optional<JsonArray> const& names)
{
if (!names.has_value())
return {};
Vector<Web::IndexedDB::InspectionPath> paths;
for (auto const& name : names->values()) {
auto path = parse_indexed_database_path(name);
if (path.has_value())
paths.append(path.release_value());
}
if (paths.is_empty())
return {};
return paths;
}
static Function<bool(URL::URL const&)> host_filter(String const& host)
{
return [host](URL::URL const& url) {
auto storage_host = storage_host_for_url(url);
return storage_host.has_value() && *storage_host == host;
};
}
static JsonObject serialize_object_row(Web::IndexedDB::InspectionObject const& inspection_object, String const& host)
{
return inspection_object.visit(
[&](Web::IndexedDB::InspectionDatabaseRow const& row) {
JsonObject object;
object.set("uniqueKey"sv, database_name_for_devtools(row.name));
object.set("db"sv, row.name);
object.set("storage"sv, indexed_database_default_storage_name);
object.set("origin"sv, host);
object.set("version"sv, row.version);
object.set("objectStores"sv, row.object_store_count);
return object;
},
[](Web::IndexedDB::InspectionObjectStore const& store) {
JsonArray indexes;
for (auto const& index : store.indexes) {
JsonObject object;
object.set("name"sv, index.name);
object.set("keyPath"sv, index.key_path);
object.set("unique"sv, index.unique);
object.set("multiEntry"sv, index.multi_entry);
indexes.must_append(move(object));
}
JsonObject object;
object.set("objectStore"sv, store.name);
if (store.key_path.has_value())
object.set("keyPath"sv, *store.key_path);
else
object.set("keyPath"sv, JsonValue {});
object.set("autoIncrement"sv, store.auto_increment);
object.set("indexes"sv, indexes.serialized());
return object;
},
[](Web::IndexedDB::InspectionRecord const& record) {
JsonObject object;
object.set("name"sv, record.key);
object.set("value"sv, record.value);
return object;
});
}
static JsonObject paginated_indexed_database_response(Vector<Web::IndexedDB::InspectionObject> rows, JsonObject const& options, String const& host)
{
static constexpr auto max_objects_per_page = 50uz;
size_t offset = options.get_integer<size_t>("offset"sv).value_or(0);
auto size = min(options.get_integer<size_t>("size"sv).value_or(max_objects_per_page), max_objects_per_page);
auto total = rows.size();
JsonArray data;
if (offset < total) {
auto end = min(total, offset + size);
for (size_t i = offset; i < end; ++i)
data.must_append(serialize_object_row(rows.at(i), host));
} else {
offset = total;
}
JsonObject response;
response.set("offset"sv, offset);
response.set("total"sv, total);
response.set("data"sv, move(data));
return response;
}
JsonObject serialize_objects(Web::DOM::Document& document, String const& host, JsonValue const& names, JsonValue const& options)
{
auto parsed_options = options.is_object() ? options.as_object() : JsonObject {};
auto parsed_named = names.is_array() ? names.as_array() : JsonArray {};
return paginated_indexed_database_response(
Web::IndexedDB::inspect_indexed_database_objects(document, host_filter(host), parse_indexed_database_paths(parsed_named)),
parsed_options,
host);
}
static ErrorOr<Web::IndexedDB::InspectionPath> parse_required_indexed_database_path(String const& name)
{
auto path = parse_indexed_database_path(JsonValue { name });
if (!path.has_value())
return Error::from_string_literal("Invalid IndexedDB path");
return path.release_value();
}
ErrorOr<JsonObject> delete_database(Web::DOM::Document& document, String const& host, String const& name)
{
auto blocked = TRY(Web::IndexedDB::delete_indexed_database_for_inspection(document, host_filter(host), database_name_from_devtools(name)));
JsonObject response;
if (blocked)
response.set("blocked"sv, true);
return response;
}
ErrorOr<JsonObject> clear_object_store(Web::DOM::Document& document, String const& host, String const& name)
{
auto path = TRY(parse_required_indexed_database_path(name));
if (!path.object_store_name.has_value() || path.key.has_value())
return Error::from_string_literal("Invalid IndexedDB object store path");
TRY(Web::IndexedDB::clear_indexed_database_object_store_for_inspection(document, host_filter(host), path.database_name, *path.object_store_name));
return JsonObject {};
}
ErrorOr<JsonObject> delete_record(Web::DOM::Document& document, String const& host, String const& name)
{
auto path = TRY(parse_required_indexed_database_path(name));
if (!path.object_store_name.has_value() || !path.key.has_value())
return Error::from_string_literal("Invalid IndexedDB record path");
TRY(Web::IndexedDB::delete_indexed_database_record_for_inspection(document, host_filter(host), path.database_name, *path.object_store_name, *path.key));
return JsonObject {};
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/String.h>
#include <LibDevTools/Forward.h>
#include <LibWeb/Forward.h>
namespace DevTools::IndexedDB {
DEVTOOLS_API JsonObject serialize_storage(Web::DOM::Document&);
DEVTOOLS_API JsonObject serialize_objects(Web::DOM::Document&, String const& host, JsonValue const& names, JsonValue const& options);
DEVTOOLS_API ErrorOr<JsonObject> delete_database(Web::DOM::Document&, String const& host, String const& name);
DEVTOOLS_API ErrorOr<JsonObject> clear_object_store(Web::DOM::Document&, String const& host, String const& name);
DEVTOOLS_API ErrorOr<JsonObject> delete_record(Web::DOM::Document&, String const& host, String const& name);
}

View file

@ -31,19 +31,24 @@ Optional<String> storage_host_for_url(String const& url_string)
if (!url.has_value())
return {};
auto const& scheme = url->scheme();
return storage_host_for_url(url.value());
}
Optional<String> storage_host_for_url(URL::URL const& url)
{
auto const& scheme = url.scheme();
if (scheme == "http"sv || scheme == "https"sv) {
StringBuilder builder;
builder.append(scheme);
builder.append("://"sv);
builder.append(url->serialized_host());
if (auto port = url->port(); port.has_value())
builder.append(url.serialized_host());
if (auto port = url.port(); port.has_value())
builder.appendff(":{}", *port);
return builder.to_string_without_validation();
}
if (scheme == "about"sv || scheme == "file"sv || scheme == "javascript"sv || scheme == "resource"sv)
return url->serialize();
return url.serialize();
return {};
}

View file

@ -11,10 +11,12 @@
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibDevTools/Forward.h>
#include <LibURL/Forward.h>
namespace DevTools {
DEVTOOLS_API Optional<String> storage_host_for_url(String const&);
DEVTOOLS_API Optional<String> storage_host_for_url(URL::URL const& url);
DEVTOOLS_API Optional<String> storage_host_name(String const&);
enum class StorageFieldType : u8 {

View file

@ -1976,6 +1976,28 @@ void Application::remove_storage_change_listener(DevTools::TabDescription const&
view->remove_storage_change_listener(listener_id);
}
void Application::inspect_indexed_database_storage(DevTools::TabDescription const& description, OnIndexedDBInspectionComplete on_complete) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value()) {
on_complete(Error::from_string_literal("Unable to locate tab"));
return;
}
view->inspect_indexed_database_storage(move(on_complete));
}
void Application::inspect_indexed_database_objects(DevTools::TabDescription const& description, String const& host, Optional<JsonArray> names, JsonObject options, OnIndexedDBInspectionComplete on_complete) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value()) {
on_complete(Error::from_string_literal("Unable to locate tab"));
return;
}
view->inspect_indexed_database_objects(host, move(names), move(options), move(on_complete));
}
void Application::inspect_tab(DevTools::TabDescription const& description, OnTabInspectionComplete on_complete) const
{
auto view = ViewImplementation::find_view_by_id(description.id);

View file

@ -283,6 +283,8 @@ private:
virtual ErrorOr<void> clear_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&) const override;
virtual u64 add_storage_change_listener(DevTools::TabDescription const&, OnStorageChange) const override;
virtual void remove_storage_change_listener(DevTools::TabDescription const&, u64) const override;
virtual void inspect_indexed_database_storage(DevTools::TabDescription const&, OnIndexedDBInspectionComplete) const override;
virtual void inspect_indexed_database_objects(DevTools::TabDescription const&, String const&, Optional<JsonArray>, JsonObject, OnIndexedDBInspectionComplete) const override;
virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete) const override;
virtual void inspect_accessibility_tree(DevTools::TabDescription const&, OnAccessibilityTreeInspectionComplete) const override;
virtual void listen_for_dom_properties(DevTools::TabDescription const&, OnDOMNodePropertiesReceived) const override;

View file

@ -947,6 +947,37 @@ void ViewImplementation::inspect_current_flexbox(Web::UniqueNodeID node_id, bool
client().async_inspect_current_flexbox(page_id(), node_id, only_look_at_parents);
}
void ViewImplementation::inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
{
auto request_id = m_next_indexed_database_inspection_request_id++;
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
client().async_inspect_indexed_database_storage(page_id(), request_id);
}
void ViewImplementation::inspect_indexed_database_objects(String const& host, Optional<JsonArray> names, JsonObject options, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
{
auto request_id = m_next_indexed_database_inspection_request_id++;
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
if (names.has_value())
client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue { names.release_value() }, JsonValue { move(options) });
else
client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue {}, JsonValue { move(options) });
}
void ViewImplementation::did_receive_indexed_database_inspection(u64 request_id, JsonObject result)
{
auto callback = m_pending_indexed_database_inspection_requests.take(request_id);
if (!callback.has_value())
return;
if (result.has_string("error"sv)) {
(*callback)(Error::from_string_literal("IndexedDB operation failed"));
return;
}
(*callback)(move(result));
}
void ViewImplementation::clear_inspected_dom_node()
{
client().async_clear_inspected_dom_node(page_id());

View file

@ -147,6 +147,9 @@ public:
u64 add_storage_change_listener(DevTools::DevToolsDelegate::OnStorageChange);
void remove_storage_change_listener(u64 listener_id);
void inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
void inspect_indexed_database_objects(String const& host, Optional<JsonArray> names, JsonObject options, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
ByteString selected_text();
ByteString cut_selected_text();
Optional<String> selected_text_with_whitespace_collapsed();
@ -605,6 +608,9 @@ protected:
HashMap<u64, DevTools::DevToolsDelegate::OnStorageChange> m_storage_change_listeners;
u64 m_next_storage_change_listener_id { 1 };
HashMap<u64, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete> m_pending_indexed_database_inspection_requests;
u64 m_next_indexed_database_inspection_request_id { 1 };
// FIXME: Reconcile this ID with `page_id`. The latter is only unique per WebContent connection, whereas the view ID
// is required to be globally unique for Firefox DevTools.
u64 m_view_id { 0 };
@ -619,6 +625,7 @@ protected:
};
void request_node_picker_hit_test(NodePickerRequestType, Web::DevicePixelPoint);
void did_receive_node_picker_hit_test(u64 request_id, Web::UniqueNodeID);
void did_receive_indexed_database_inspection(u64 request_id, JsonObject);
bool m_node_picker_active { false };
Optional<Web::UniqueNodeID> m_node_picker_hovered_node_id;

View file

@ -777,6 +777,12 @@ void WebContentClient::did_inspect_current_flexbox(u64 page_id, String flexbox_l
}
}
void WebContentClient::did_inspect_indexed_database(u64 page_id, u64 request_id, String result)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_receive_indexed_database_inspection(request_id, parse_json(result, "IndexedDB inspection result"sv));
}
void WebContentClient::did_inspect_accessibility_tree(u64 page_id, String accessibility_tree)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -127,6 +127,7 @@ private:
virtual void did_inspect_grid_layouts(u64 page_id, String) override;
virtual void did_inspect_current_grid(u64 page_id, String) override;
virtual void did_inspect_current_flexbox(u64 page_id, String) override;
virtual void did_inspect_indexed_database(u64 page_id, u64 request_id, String) override;
virtual void did_inspect_accessibility_tree(u64 page_id, String) override;
virtual void did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID node_id) override;
virtual void did_get_node_id_at_position(u64 page_id, u64 request_id, Web::UniqueNodeID node_id) override;

View file

@ -31,7 +31,7 @@ target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${CMAKE_CU
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}>)
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}/Services/>)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWasm LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibDevTools LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWasm LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PRIVATE OpenSSL::Crypto OpenSSL::SSL)
target_link_libraries(webcontentservice PRIVATE SDL3::SDL3)
target_compile_options(webcontentservice PRIVATE $<$<COMPILE_LANG_AND_ID:CXX,Clang,AppleClang>:-Wexit-time-destructors>)

View file

@ -16,6 +16,7 @@
#include <AK/QuickSort.h>
#include <LibCore/Process.h>
#include <LibCore/System.h>
#include <LibDevTools/IndexedDBSerialization.h>
#include <LibGC/Heap.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Color.h>
@ -1236,6 +1237,93 @@ void ConnectionFromClient::inspect_current_flexbox(u64 page_id, Web::UniqueNodeI
async_did_inspect_current_flexbox(page_id, "null"_string);
}
void ConnectionFromClient::inspect_indexed_database_storage(u64 page_id, u64 request_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
auto* document = page->page().top_level_browsing_context().active_document();
if (!document) {
async_did_inspect_indexed_database(page_id, request_id, "{}"_string);
return;
}
async_did_inspect_indexed_database(page_id, request_id, DevTools::IndexedDB::serialize_storage(*document).serialized());
}
void ConnectionFromClient::inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
auto* document = page->page().top_level_browsing_context().active_document();
if (!document) {
async_did_inspect_indexed_database(page_id, request_id, "{}"_string);
return;
}
async_did_inspect_indexed_database(page_id, request_id, DevTools::IndexedDB::serialize_objects(*document, host, names, options).serialized());
}
static void send_indexed_database_operation_result(ConnectionFromClient& connection, u64 page_id, u64 request_id, ErrorOr<JsonObject> result)
{
if (result.is_error()) {
JsonObject error;
error.set("error"sv, result.error().string_literal());
connection.async_did_inspect_indexed_database(page_id, request_id, error.serialized());
return;
}
connection.async_did_inspect_indexed_database(page_id, request_id, result.release_value().serialized());
}
void ConnectionFromClient::delete_indexed_database(u64 page_id, u64 request_id, String host, String name)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
auto* document = page->page().top_level_browsing_context().active_document();
if (!document) {
async_did_inspect_indexed_database(page_id, request_id, "{}"_string);
return;
}
send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::delete_database(*document, host, name));
}
void ConnectionFromClient::clear_indexed_database_object_store(u64 page_id, u64 request_id, String host, String name)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
auto* document = page->page().top_level_browsing_context().active_document();
if (!document) {
async_did_inspect_indexed_database(page_id, request_id, "{}"_string);
return;
}
send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::clear_object_store(*document, host, name));
}
void ConnectionFromClient::delete_indexed_database_record(u64 page_id, u64 request_id, String host, String name)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
auto* document = page->page().top_level_browsing_context().active_document();
if (!document) {
async_did_inspect_indexed_database(page_id, request_id, "{}"_string);
return;
}
send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::delete_record(*document, host, name));
}
void ConnectionFromClient::clear_inspected_dom_node(u64 page_id)
{
auto page = this->page(page_id);

View file

@ -111,6 +111,8 @@ private:
virtual void inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) override;
virtual void inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) override;
virtual void inspect_current_flexbox(u64 page_id, Web::UniqueNodeID node_id, bool only_look_at_parents) override;
virtual void inspect_indexed_database_storage(u64 page_id, u64 request_id) override;
virtual void inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options) override;
virtual void clear_inspected_dom_node(u64 page_id) override;
virtual void highlight_dom_node(u64 page_id, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element) override;
virtual void highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) override;

View file

@ -78,6 +78,7 @@ endpoint WebContentClient
did_inspect_grid_layouts(u64 page_id, String grid_layouts) =|
did_inspect_current_grid(u64 page_id, String grid_layout) =|
did_inspect_current_flexbox(u64 page_id, String flexbox_layout) =|
did_inspect_indexed_database(u64 page_id, u64 request_id, String result) =|
did_inspect_accessibility_tree(u64 page_id, String accessibility_tree) =|
did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID node_id) =|
did_get_node_id_at_position(u64 page_id, u64 request_id, Web::UniqueNodeID node_id) =|

View file

@ -82,6 +82,8 @@ endpoint WebContentServer
inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) =|
inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) =|
inspect_current_flexbox(u64 page_id, Web::UniqueNodeID node_id, bool only_look_at_parents) =|
inspect_indexed_database_storage(u64 page_id, u64 request_id) =|
inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options) =|
clear_inspected_dom_node(u64 page_id) =|
highlight_dom_node(u64 page_id, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element) =|
highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) =|

View file

@ -545,6 +545,91 @@ static bool cookie_matches(HTTP::Cookie::Cookie const& cookie, StringView name,
&& cookie.path == path;
}
static String indexed_database_path(StringView database, Optional<StringView> object_store = {})
{
JsonArray path;
path.must_append(database);
if (object_store.has_value())
path.must_append(*object_store);
return path.serialized();
}
static JsonObject make_indexed_database_storage_hosts()
{
JsonArray names;
names.must_append(indexed_database_path("fixtures (default)"sv, "people"sv));
names.must_append(indexed_database_path("empty (default)"sv));
JsonObject hosts;
hosts.set("https://example.test"sv, move(names));
return hosts;
}
static JsonObject make_indexed_database_store_objects(Optional<JsonArray> const& names)
{
JsonArray data;
bool should_return_databases = !names.has_value() || names->is_empty();
Vector<JsonArray> paths;
if (names.has_value()) {
for (auto const& name : names->values()) {
if (!name.is_string())
continue;
auto parsed = JsonValue::from_string(name.as_string());
if (parsed.is_error() || !parsed.value().is_array())
continue;
auto path = parsed.release_value().as_array();
if (path.is_empty())
continue;
paths.append(move(path));
}
if (paths.is_empty())
should_return_databases = true;
}
if (should_return_databases) {
JsonObject fixtures;
fixtures.set("uniqueKey"sv, "fixtures (default)"sv);
fixtures.set("db"sv, "fixtures"sv);
fixtures.set("storage"sv, "default"sv);
fixtures.set("origin"sv, "https://example.test"sv);
fixtures.set("version"sv, 1);
fixtures.set("objectStores"sv, 1);
data.must_append(move(fixtures));
JsonObject empty;
empty.set("uniqueKey"sv, "empty (default)"sv);
empty.set("db"sv, "empty"sv);
empty.set("storage"sv, "default"sv);
empty.set("origin"sv, "https://example.test"sv);
empty.set("version"sv, 2);
empty.set("objectStores"sv, 0);
data.must_append(move(empty));
} else {
for (auto const& path : paths) {
if (path.size() == 1) {
JsonObject store;
store.set("objectStore"sv, "people"sv);
store.set("keyPath"sv, "id"sv);
store.set("autoIncrement"sv, true);
store.set("indexes"sv, "[]"_string);
data.must_append(move(store));
} else if (path.size() >= 2) {
JsonObject record;
record.set("name"sv, 1);
record.set("value"sv, "{\"name\":\"Ada\"}"sv);
data.must_append(move(record));
}
}
}
JsonObject response;
response.set("offset"sv, 0);
response.set("total"sv, data.size());
response.set("data"sv, move(data));
return response;
}
class TestDevToolsDelegate final : public DevTools::DevToolsDelegate {
public:
virtual Vector<DevTools::TabDescription> tab_list() const override
@ -733,6 +818,19 @@ public:
++remove_storage_change_listener_call_count;
}
virtual void inspect_indexed_database_storage(DevTools::TabDescription const&, OnIndexedDBInspectionComplete callback) const override
{
++inspect_indexed_database_storage_call_count;
callback(make_indexed_database_storage_hosts());
}
virtual void inspect_indexed_database_objects(DevTools::TabDescription const&, String const& host, Optional<JsonArray> names, JsonObject, OnIndexedDBInspectionComplete callback) const override
{
++inspect_indexed_database_objects_call_count;
last_indexed_database_host = host;
callback(make_indexed_database_store_objects(names));
}
virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override
{
++inspect_tab_call_count;
@ -1187,6 +1285,8 @@ public:
mutable bool fail_set_storage_item { false };
mutable bool fail_remove_storage_item { false };
mutable bool fail_clear_storage { false };
mutable size_t inspect_indexed_database_storage_call_count { 0 };
mutable size_t inspect_indexed_database_objects_call_count { 0 };
mutable size_t inspect_accessibility_tree_call_count { 0 };
mutable size_t listen_for_dom_properties_call_count { 0 };
mutable size_t stop_listening_for_dom_properties_call_count { 0 };
@ -1259,6 +1359,7 @@ public:
mutable Optional<String> last_navigated_url;
mutable Optional<bool> last_reload_bypass_cache;
mutable Optional<int> last_history_delta;
mutable Optional<String> last_indexed_database_host;
};
class ProtocolClient {
@ -1646,6 +1747,68 @@ static JsonObject remove_all_storage_items(ProtocolClient& client, StringView st
return client.request(move(request));
}
static String get_indexed_database_actor(ProtocolClient& client)
{
auto tab_actor = actor_from(get_tab(client), "actor"sv);
auto watcher_actor = actor_from(client.request(tab_actor, "getWatcher"sv), "actor"sv);
JsonObject watch_resources;
watch_resources.set("to"sv, watcher_actor);
watch_resources.set("type"sv, "watchResources"sv);
JsonArray resource_types;
resource_types.must_append("indexed-db"sv);
watch_resources.set("resourceTypes"sv, move(resource_types));
EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor);
auto indexed_database_resource = read_resource(client, "indexed-db"sv);
return actor_from(indexed_database_resource, "actor"sv);
}
static JsonObject get_indexed_database_store_objects(ProtocolClient& client, StringView indexed_database_actor, Optional<String> name = {})
{
JsonObject get_store_objects;
get_store_objects.set("to"sv, indexed_database_actor);
get_store_objects.set("type"sv, "getStoreObjects"sv);
get_store_objects.set("host"sv, "https://example.test"sv);
if (name.has_value()) {
JsonArray names;
names.must_append(*name);
get_store_objects.set("names"sv, move(names));
} else {
get_store_objects.set("names"sv, JsonValue {});
}
get_store_objects.set("options"sv, JsonObject {});
return client.request(move(get_store_objects));
}
static JsonObject get_indexed_database_store_objects(ProtocolClient& client, StringView indexed_database_actor, JsonArray names)
{
JsonObject get_store_objects;
get_store_objects.set("to"sv, indexed_database_actor);
get_store_objects.set("type"sv, "getStoreObjects"sv);
get_store_objects.set("host"sv, "https://example.test"sv);
get_store_objects.set("names"sv, move(names));
get_store_objects.set("options"sv, JsonObject {});
return client.request(move(get_store_objects));
}
static JsonArray get_indexed_database_update_paths(JsonObject const& stores_update, StringView update_type, StringView host = "https://example.test"sv)
{
return stores_update.get_object("data"sv)
->get_object(update_type)
->get_object("indexedDB"sv)
->get_array(host)
.release_value();
}
static JsonArray get_indexed_database_cleared_paths(JsonObject const& stores_cleared, StringView host = "https://example.test"sv)
{
return stores_cleared.get_object("data"sv)
->get_object("clearedHostsOrPaths"sv)
->get_array(host)
.release_value();
}
static JsonObject make_cookie_edit_items(HTTP::Cookie::Cookie const& cookie)
{
JsonObject items;
@ -2275,6 +2438,117 @@ TEST_CASE(storage_web_storage_mutation_errors)
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
}
TEST_CASE(storage_indexed_database_resource)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto tab_actor = actor_from(get_tab(client), "actor"sv);
auto watcher_response = client.request(tab_actor, "getWatcher"sv);
auto watcher_actor = actor_from(watcher_response, "actor"sv);
auto resources = watcher_response.get_object("traits"sv)->get_object("resources"sv).release_value();
EXPECT(resources.get_bool("indexed-db"sv).value());
JsonObject watch_resources;
watch_resources.set("to"sv, watcher_actor);
watch_resources.set("type"sv, "watchResources"sv);
JsonArray resource_types;
resource_types.must_append("indexed-db"sv);
watch_resources.set("resourceTypes"sv, move(resource_types));
EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor);
auto indexed_database_resource = read_resource(client, "indexed-db"sv);
EXPECT_EQ(indexed_database_resource.get_string("resourceKey"sv).value(), "indexedDB"sv);
EXPECT_EQ(indexed_database_resource.get_integer<u64>("browsingContextID"sv).value(), 1u);
EXPECT_EQ(indexed_database_resource.get_integer<u64>("innerWindowId"sv).value(), 1u);
EXPECT_EQ(indexed_database_resource.get_string("resourceId"sv).value(), "indexed-db-1"sv);
auto hosts = indexed_database_resource.get_object("hosts"sv).release_value();
auto names = hosts.get_array("https://example.test"sv).release_value();
EXPECT_EQ(names.size(), 2u);
EXPECT_EQ(names.at(0).as_string(), "[\"fixtures (default)\",\"people\"]"sv);
EXPECT_EQ(names.at(1).as_string(), "[\"empty (default)\"]"sv);
auto traits = indexed_database_resource.get_object("traits"sv).release_value();
EXPECT(!traits.get_bool("supportsAddItem"sv).value());
EXPECT(traits.get_bool("supportsRemoveAll"sv).value());
EXPECT(!traits.get_bool("supportsRemoveAllSessionCookies"sv).value());
EXPECT(traits.get_bool("supportsRemoveItem"sv).value());
auto indexed_database_actor = actor_from(indexed_database_resource, "actor"sv);
auto fields = client.request(indexed_database_actor, "getFields"sv).get_array("value"sv).release_value();
EXPECT_EQ(fields.at(0).as_object().get_string("name"sv).value(), "uniqueKey"sv);
EXPECT(fields.at(0).as_object().get_bool("private"sv).value());
EXPECT_EQ(fields.at(1).as_object().get_string("name"sv).value(), "db"sv);
JsonObject database_fields_request;
database_fields_request.set("to"sv, indexed_database_actor);
database_fields_request.set("type"sv, "getFields"sv);
database_fields_request.set("subType"sv, "database"sv);
auto database_fields = client.request(move(database_fields_request)).get_array("value"sv).release_value();
EXPECT_EQ(database_fields.at(0).as_object().get_string("name"sv).value(), "objectStore"sv);
JsonObject object_store_fields_request;
object_store_fields_request.set("to"sv, indexed_database_actor);
object_store_fields_request.set("type"sv, "getFields"sv);
object_store_fields_request.set("subType"sv, "object store"sv);
auto object_store_fields = client.request(move(object_store_fields_request)).get_array("value"sv).release_value();
EXPECT_EQ(object_store_fields.at(0).as_object().get_string("name"sv).value(), "name"sv);
EXPECT_EQ(object_store_fields.at(1).as_object().get_string("name"sv).value(), "value"sv);
EXPECT_EQ(session->delegate.inspect_indexed_database_storage_call_count, 1u);
}
TEST_CASE(storage_indexed_database_store_objects)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto indexed_database_actor = get_indexed_database_actor(client);
auto databases = get_indexed_database_store_objects(client, indexed_database_actor);
EXPECT_EQ(session->delegate.inspect_indexed_database_objects_call_count, 1u);
EXPECT_EQ(session->delegate.last_indexed_database_host.value(), "https://example.test"sv);
EXPECT_EQ(databases.get_integer<size_t>("offset"sv).value(), 0u);
EXPECT_EQ(databases.get_integer<size_t>("total"sv).value(), 2u);
auto database_rows = databases.get_array("data"sv).release_value();
EXPECT_EQ(database_rows.size(), 2u);
auto fixtures_database = database_rows.at(0).as_object();
EXPECT_EQ(fixtures_database.get_string("uniqueKey"sv).value(), "fixtures (default)"sv);
EXPECT_EQ(fixtures_database.get_string("db"sv).value(), "fixtures"sv);
EXPECT_EQ(fixtures_database.get_string("storage"sv).value(), "default"sv);
EXPECT_EQ(fixtures_database.get_string("origin"sv).value(), "https://example.test"sv);
EXPECT_EQ(fixtures_database.get_integer<int>("version"sv).value(), 1);
EXPECT_EQ(fixtures_database.get_integer<int>("objectStores"sv).value(), 1);
auto databases_from_empty_names = get_indexed_database_store_objects(client, indexed_database_actor, JsonArray {});
EXPECT_EQ(databases_from_empty_names.get_integer<size_t>("total"sv).value(), 2u);
JsonArray empty_path_names;
empty_path_names.must_append("[]"sv);
auto databases_from_empty_path = get_indexed_database_store_objects(client, indexed_database_actor, move(empty_path_names));
EXPECT_EQ(databases_from_empty_path.get_integer<size_t>("total"sv).value(), 2u);
auto object_stores = get_indexed_database_store_objects(client, indexed_database_actor, indexed_database_path("fixtures (default)"sv));
EXPECT_EQ(object_stores.get_integer<size_t>("total"sv).value(), 1u);
auto object_store_rows = object_stores.get_array("data"sv).release_value();
auto people_store = object_store_rows.at(0).as_object();
EXPECT_EQ(people_store.get_string("objectStore"sv).value(), "people"sv);
EXPECT_EQ(people_store.get_string("keyPath"sv).value(), "id"sv);
EXPECT(people_store.get_bool("autoIncrement"sv).value());
EXPECT_EQ(people_store.get_string("indexes"sv).value(), "[]"sv);
auto records = get_indexed_database_store_objects(client, indexed_database_actor, indexed_database_path("fixtures (default)"sv, "people"sv));
EXPECT_EQ(records.get_integer<size_t>("total"sv).value(), 1u);
auto record_rows = records.get_array("data"sv).release_value();
auto record = record_rows.at(0).as_object();
EXPECT_EQ(record.get_integer<int>("name"sv).value(), 1);
EXPECT_EQ(record.get_string("value"sv).value(), "{\"name\":\"Ada\"}"sv);
}
TEST_CASE(storage_cookie_store_objects)
{
auto session = create_session();