LibDevTools: Populate DevTools Web Storage

Firefox exposes localStorage and sessionStorage through the same
storage actor protocol used by cookies. Add matching resources for the
current tab so the Storage panel can list key/value pairs.

Read the values through WebContent rather than directly from the
browser process. Session storage lives in LibWeb, and using the same
path for both stores keeps the actor independent of the backing store.
This commit is contained in:
Sam Atkins 2026-06-10 16:15:21 +01:00
parent 5b5f848689
commit ddb4bdadd7
18 changed files with 579 additions and 3 deletions

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/QuickSort.h>
#include <LibDevTools/Actors/StorageActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/DevToolsServer.h>
#include <LibDevTools/StorageHelpers.h>
namespace DevTools {
static constexpr auto max_store_object_count = 50uz;
static JsonObject storage_field(StringView name)
{
JsonObject field;
field.set("name"sv, name);
field.set("editable"sv, true);
return field;
}
static JsonObject serialize_storage_item(DevToolsDelegate::StorageItem const& item)
{
JsonObject object;
object.set("name"sv, item.name);
object.set("value"sv, item.value);
return object;
}
NonnullRefPtr<StorageActor> StorageActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
{
return adopt_ref(*new StorageActor(devtools, move(name), move(tab), storage_endpoint));
}
StorageActor::StorageActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
: Actor(devtools, move(name))
, m_tab(move(tab))
, m_storage_endpoint(storage_endpoint)
{
}
StorageActor::~StorageActor() = default;
StringView StorageActor::resource_type() const
{
if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
return "local-storage"sv;
VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
return "session-storage"sv;
}
StringView StorageActor::resource_key() const
{
if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
return "localStorage"sv;
VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
return "sessionStorage"sv;
}
Optional<String> StorageActor::host() const
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
return storage_host_for_url(tab->description().url);
}
JsonObject StorageActor::serialize_storage() const
{
JsonObject hosts;
if (auto storage_host = host(); storage_host.has_value())
hosts.set(*storage_host, JsonArray {});
JsonObject traits;
traits.set("supportsAddItem"sv, true);
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("{}-{}", resource_key(), tab->inner_window_id())));
}
storage.set("hosts"sv, move(hosts));
storage.set("resourceKey"sv, resource_key());
storage.set("traits"sv, move(traits));
return storage;
}
void StorageActor::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 StorageActor::get_fields(Message const& message)
{
JsonArray fields;
fields.must_append(storage_field("name"sv));
fields.must_append(storage_field("value"sv));
JsonObject response;
response.set("value"sv, move(fields));
send_response(message, move(response));
}
void StorageActor::get_store_objects(Message const& message)
{
auto requested_host_ref = get_required_parameter<String>(message, "host"sv);
if (!requested_host_ref.has_value())
return;
String requested_host = *requested_host_ref;
auto names_value = message.data.get("names"sv);
Optional<JsonArray> requested_names;
if (names_value.has_value() && names_value->is_array())
requested_names = names_value->as_array();
JsonObject options;
auto options_value = message.data.get("options"sv);
if (options_value.has_value() && options_value->is_object())
options = options_value->as_object();
auto tab = m_tab.strong_ref();
if (!tab) {
send_store_objects(message, requested_host, move(requested_names), move(options), Vector<DevToolsDelegate::StorageItem> {});
return;
}
devtools().delegate().inspect_storage(
tab->description(),
m_storage_endpoint,
weak_callback(*this, [message, requested_host, requested_names, options](auto& self, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items) {
self.send_store_objects(message, requested_host, requested_names, options, move(storage_items));
}));
}
void StorageActor::send_store_objects(Message const& message, Optional<String> requested_host, Optional<JsonArray> requested_names, JsonObject options, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items_or_error)
{
if (storage_items_or_error.is_error()) {
JsonObject response;
response.set("error"sv, "storageError"sv);
response.set("message"sv, storage_items_or_error.error().string_literal());
send_response(message, move(response));
return;
}
Vector<DevToolsDelegate::StorageItem> storage_items = storage_items_or_error.release_value();
if (!host().has_value() || !requested_host.has_value() || *host() != *requested_host)
storage_items.clear();
if (requested_names.has_value()) {
storage_items.remove_all_matching([&](auto const& item) {
return !requested_names->values().contains([&](auto const& value) {
return value.is_string() && value.as_string() == item.name;
});
});
}
quick_sort(storage_items, [](auto const& a, auto const& b) {
return a.name < b.name;
});
auto total = storage_items.size();
auto offset = options.get_integer<size_t>("offset"sv).value_or(0);
auto size = options.get_integer<size_t>("size"sv).value_or(max_store_object_count);
if (size > max_store_object_count)
size = max_store_object_count;
JsonArray data;
if (offset > total) {
offset = total;
} else {
auto end = min(total, offset + size);
for (auto i = offset; i < end; ++i)
data.must_append(serialize_storage_item(storage_items[i]));
}
JsonObject response;
response.set("offset"sv, offset);
response.set("total"sv, total);
response.set("data"sv, move(data));
send_response(message, move(response));
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2026, Ladybird contributors
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/JsonArray.h>
#include <LibDevTools/Actor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/Forward.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
namespace DevTools {
class DEVTOOLS_API StorageActor final : public Actor {
public:
static constexpr auto base_name = "storage"sv;
static NonnullRefPtr<StorageActor> create(DevToolsServer&, String name, WeakPtr<TabActor>, Web::StorageAPI::StorageEndpointType);
virtual ~StorageActor() override;
StringView resource_type() const;
StringView resource_key() const;
Optional<String> host() const;
JsonObject serialize_storage() const;
private:
StorageActor(DevToolsServer&, String name, WeakPtr<TabActor>, Web::StorageAPI::StorageEndpointType);
virtual void handle_message(Message const&) override;
void get_fields(Message const&);
void get_store_objects(Message const&);
void send_store_objects(Message const&, Optional<String> requested_host, Optional<JsonArray> requested_names, JsonObject options, ErrorOr<Vector<DevToolsDelegate::StorageItem>>);
WeakPtr<TabActor> m_tab;
Web::StorageAPI::StorageEndpointType m_storage_endpoint;
};
}

View file

@ -14,6 +14,7 @@
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/NetworkParentActor.h>
#include <LibDevTools/Actors/StorageActor.h>
#include <LibDevTools/Actors/StyleSheetsActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/TargetConfigurationActor.h>
@ -91,11 +92,13 @@ void WatcherActor::handle_message(Message const& message)
return;
bool should_send_cookie_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 (resource_type.as_string() != "console-message"sv && resource_type.as_string() != "cookies"sv)
if (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "local-storage"sv, "session-storage"sv))
dbgln("Unrecognized `watchResources` resource type: '{}'", resource_type.as_string());
}
}
@ -105,12 +108,22 @@ 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() == "local-storage"sv) {
m_is_watching_local_storage_resources = true;
should_send_local_storage_resources = true;
} else if (resource_type.as_string() == "session-storage"sv) {
m_is_watching_session_storage_resources = true;
should_send_session_storage_resources = true;
}
}
send_response(message, move(response));
if (should_send_cookie_resources)
send_cookies_resource_available_message();
if (should_send_local_storage_resources)
send_storage_resource_available_message(local_storage_actor());
if (should_send_session_storage_resources)
send_storage_resource_available_message(session_storage_actor());
return;
}
@ -155,13 +168,13 @@ JsonObject WatcherActor::serialize_description() const
resources.set("jstracer-state"sv, false);
resources.set("jstracer-trace"sv, false);
resources.set("last-private-context-exit"sv, false);
resources.set("local-storage"sv, false);
resources.set("local-storage"sv, true);
resources.set("network-event"sv, true);
resources.set("network-event-stacktrace"sv, false);
resources.set("platform-message"sv, false);
resources.set("reflow"sv, false);
resources.set("server-sent-event"sv, false);
resources.set("session-storage"sv, false);
resources.set("session-storage"sv, true);
resources.set("source"sv, false);
resources.set("stylesheet"sv, false);
resources.set("thread-state"sv, false);
@ -214,6 +227,10 @@ 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_local_storage_resources)
send_storage_resource_available_message(local_storage_actor());
if (m_is_watching_session_storage_resources)
send_storage_resource_available_message(session_storage_actor());
}
void WatcherActor::send_frame_target_available_message(FrameActor& target)
@ -264,4 +281,40 @@ void WatcherActor::send_cookies_resource_available_message()
send_message(move(message));
}
StorageActor& WatcherActor::local_storage_actor()
{
if (auto storage = m_local_storage.strong_ref())
return *storage;
m_local_storage = devtools().register_actor<StorageActor>(m_tab, Web::StorageAPI::StorageEndpointType::LocalStorage);
return *m_local_storage.strong_ref();
}
StorageActor& WatcherActor::session_storage_actor()
{
if (auto storage = m_session_storage.strong_ref())
return *storage;
m_session_storage = devtools().register_actor<StorageActor>(m_tab, Web::StorageAPI::StorageEndpointType::SessionStorage);
return *m_session_storage.strong_ref();
}
void WatcherActor::send_storage_resource_available_message(StorageActor& storage)
{
JsonArray resources;
resources.must_append(storage.serialize_storage());
JsonArray typed_resources;
typed_resources.must_append(storage.resource_type());
typed_resources.must_append(move(resources));
JsonArray array;
array.must_append(move(typed_resources));
JsonObject message;
message.set("type"sv, "resources-available-array"sv);
message.set("array"sv, move(array));
send_message(move(message));
}
}

View file

@ -33,15 +33,22 @@ private:
void send_frame_target_destroyed_message(FrameActor&);
CookiesActor& cookies_actor();
void send_cookies_resource_available_message();
StorageActor& local_storage_actor();
StorageActor& session_storage_actor();
void send_storage_resource_available_message(StorageActor&);
WeakPtr<TabActor> m_tab;
WeakPtr<FrameActor> m_target;
WeakPtr<CookiesActor> m_cookies;
WeakPtr<StorageActor> m_local_storage;
WeakPtr<StorageActor> m_session_storage;
WeakPtr<TargetConfigurationActor> m_target_configuration;
WeakPtr<ThreadConfigurationActor> m_thread_configuration;
WeakPtr<NetworkParentActor> m_network_parent;
bool m_is_watching_frame_targets { false };
bool m_is_watching_cookie_resources { false };
bool m_is_watching_local_storage_resources { false };
bool m_is_watching_session_storage_resources { false };
};
}

View file

@ -21,6 +21,7 @@ set(SOURCES
Actors/RootActor.cpp
Actors/StyleRuleActor.cpp
Actors/StyleSheetsActor.cpp
Actors/StorageActor.cpp
Actors/TabActor.cpp
Actors/TargetConfigurationActor.cpp
Actors/ThreadActor.cpp

View file

@ -24,6 +24,7 @@
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/Forward.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWebView/DOMNodeProperties.h>
#include <LibWebView/Forward.h>
@ -45,6 +46,14 @@ public:
virtual void listen_for_host_cookie_changes(TabDescription const&, OnHostCookieChange) const { }
virtual void stop_listening_for_host_cookie_changes(TabDescription const&) const { }
struct StorageItem {
String name;
String value;
};
using OnStorageItemsReceived = Function<void(ErrorOr<Vector<StorageItem>>)>;
virtual void inspect_storage(TabDescription const&, Web::StorageAPI::StorageEndpointType, OnStorageItemsReceived) const { }
using OnTabInspectionComplete = Function<void(ErrorOr<JsonValue>)>;
virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { }

View file

@ -35,6 +35,7 @@ class ProcessActor;
class RootActor;
class StyleRuleActor;
class StyleSheetsActor;
class StorageActor;
class TabActor;
class TargetConfigurationActor;
class ThreadActor;

View file

@ -1867,6 +1867,21 @@ void Application::stop_listening_for_host_cookie_changes(DevTools::TabDescriptio
view->stop_listening_for_host_cookie_changes();
}
void Application::inspect_storage(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, OnStorageItemsReceived on_complete) const
{
static u64 next_request_id = 0;
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value()) {
on_complete(Error::from_string_literal("Unable to locate tab"));
return;
}
auto request_id = next_request_id++;
view->on_received_storage_items.set(request_id, move(on_complete));
view->inspect_storage(storage_endpoint, request_id);
}
void Application::inspect_tab(DevTools::TabDescription const& description, OnTabInspectionComplete on_complete) const
{
auto view = ViewImplementation::find_view_by_id(description.id);

View file

@ -274,6 +274,7 @@ private:
virtual void delete_cookies(DevTools::TabDescription const&, Vector<HTTP::Cookie::Cookie>) const override;
virtual void listen_for_host_cookie_changes(DevTools::TabDescription const&, OnHostCookieChange) const override;
virtual void stop_listening_for_host_cookie_changes(DevTools::TabDescription const&) const override;
virtual void inspect_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, OnStorageItemsReceived) 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

@ -477,6 +477,11 @@ void ViewImplementation::inspect_dom_tree()
client().async_inspect_dom_tree(page_id());
}
void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id)
{
client().async_inspect_storage(page_id(), storage_endpoint, request_id);
}
void ViewImplementation::inspect_accessibility_tree()
{
client().async_inspect_accessibility_tree(page_id());

View file

@ -122,6 +122,7 @@ public:
void get_source();
void inspect_dom_tree();
void inspect_storage(Web::StorageAPI::StorageEndpointType, u64 request_id);
void inspect_accessibility_tree();
void get_hovered_node_id();
void start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent);
@ -258,6 +259,7 @@ public:
Function<void()> on_request_dismiss_dialog;
Function<void(JsonObject)> on_received_dom_tree;
Function<void(DOMNodeProperties)> on_received_dom_node_properties;
HashMap<u64, Function<void(ErrorOr<Vector<DevTools::DevToolsDelegate::StorageItem>>)>> on_received_storage_items;
Function<void(JsonArray)> on_received_grid_layouts;
Function<void(Optional<JsonObject>)> on_received_current_grid;
Function<void(Optional<JsonObject>)> on_received_current_flexbox;

View file

@ -680,6 +680,39 @@ void WebContentClient::did_inspect_dom_tree(u64 page_id, String dom_tree)
}
}
static ErrorOr<Vector<DevTools::DevToolsDelegate::StorageItem>> parse_storage_items(String const& storage_items)
{
auto parsed_items = JsonValue::from_string(storage_items);
if (parsed_items.is_error())
return Error::from_string_literal("Unable to parse storage items");
if (!parsed_items.value().is_array())
return Error::from_string_literal("Expected storage items to be an array");
Vector<DevTools::DevToolsDelegate::StorageItem> items;
parsed_items.value().as_array().for_each([&](auto const& item) {
if (!item.is_object())
return;
auto name = item.as_object().get_string("name"sv);
auto value = item.as_object().get_string("value"sv);
if (!name.has_value() || !value.has_value())
return;
items.append({ name.release_value(), value.release_value() });
});
return items;
}
void WebContentClient::did_inspect_storage(u64 page_id, u64 request_id, String storage_items)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
auto handler = view->on_received_storage_items.take(request_id);
if (handler.has_value())
(*handler)(parse_storage_items(storage_items));
}
}
void WebContentClient::did_inspect_dom_node(u64 page_id, DOMNodeProperties properties)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -117,6 +117,7 @@ private:
virtual void did_request_media_context_menu(u64 page_id, Gfx::IntPoint, ByteString, unsigned, Web::Page::MediaContextMenu) override;
virtual void did_get_source(u64 page_id, URL::URL, URL::URL, String) override;
virtual void did_inspect_dom_tree(u64 page_id, String) override;
virtual void did_inspect_storage(u64 page_id, u64 request_id, String) override;
virtual void did_inspect_dom_node(u64 page_id, DOMNodeProperties) override;
virtual void did_inspect_grid_layouts(u64 page_id, String) override;
virtual void did_inspect_current_grid(u64 page_id, String) override;

View file

@ -558,6 +558,52 @@ void ConnectionFromClient::inspect_dom_tree(u64 page_id)
}
}
void ConnectionFromClient::inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id)
{
auto page = this->page(page_id);
if (!page.has_value()) {
async_did_inspect_storage(page_id, request_id, "[]"_string);
return;
}
auto* document = page->page().top_level_browsing_context().active_document();
if (!document || !document->window()) {
async_did_inspect_storage(page_id, request_id, "[]"_string);
return;
}
Web::WebIDL::ExceptionOr<GC::Ref<Web::HTML::Storage>> storage_or_error = [&]() -> Web::WebIDL::ExceptionOr<GC::Ref<Web::HTML::Storage>> {
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
return document->window()->local_storage();
VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
return document->window()->session_storage();
}();
if (storage_or_error.is_error()) {
async_did_inspect_storage(page_id, request_id, "[]"_string);
return;
}
auto storage = storage_or_error.release_value();
JsonArray storage_items;
for (auto i = 0uz; i < storage->length(); ++i) {
auto name = storage->key(i);
if (!name.has_value())
continue;
auto value = storage->get_item(*name);
if (!value.has_value())
continue;
JsonObject item;
item.set("name"sv, name.release_value());
item.set("value"sv, value.release_value());
storage_items.must_append(move(item));
}
async_did_inspect_storage(page_id, request_id, storage_items.serialized());
}
void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type property_type, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options_value)
{
auto page = this->page(page_id);

View file

@ -92,6 +92,7 @@ private:
virtual void debug_request(u64 page_id, ByteString, ByteString) override;
virtual void get_source(u64 page_id) override;
virtual void inspect_dom_tree(u64 page_id) override;
virtual void inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) override;
virtual void inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options) override;
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;

View file

@ -67,6 +67,7 @@ endpoint WebContentClient
did_get_source(u64 page_id, URL::URL url, URL::URL base_url, String source) =|
did_inspect_dom_tree(u64 page_id, String dom_tree) =|
did_inspect_storage(u64 page_id, u64 request_id, String storage_items) =|
did_inspect_dom_node(u64 page_id, WebView::DOMNodeProperties properties) =|
did_inspect_grid_layouts(u64 page_id, String grid_layouts) =|
did_inspect_current_grid(u64 page_id, String grid_layout) =|

View file

@ -16,6 +16,7 @@
#include <LibWeb/HTML/VisibilityState.h>
#include <LibWeb/HTML/WorkerAgentTypes.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWeb/Page/ViewportIsFullscreen.h>
#include <LibWeb/WebDriver/ExecuteScript.h>
#include <LibWebView/Attribute.h>
@ -58,6 +59,7 @@ endpoint WebContentServer
debug_request(u64 page_id, ByteString request, ByteString argument) =|
get_source(u64 page_id) =|
inspect_dom_tree(u64 page_id) =|
inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) =|
inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type property_type, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element, JsonValue options) =|
inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) =|
inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) =|

View file

@ -653,6 +653,18 @@ public:
on_host_cookie_change = nullptr;
}
virtual void inspect_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType storage_endpoint, OnStorageItemsReceived callback) const override
{
++inspect_storage_call_count;
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage) {
callback(fixture_local_storage_items);
return;
}
VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
callback(fixture_session_storage_items);
}
virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override
{
++inspect_tab_call_count;
@ -1071,6 +1083,8 @@ public:
mutable bool use_navigation_dom_tree { false };
mutable Vector<HTTP::Cookie::Cookie> fixture_cookies;
mutable Vector<DevTools::DevToolsDelegate::StorageItem> fixture_local_storage_items;
mutable Vector<DevTools::DevToolsDelegate::StorageItem> fixture_session_storage_items;
mutable size_t inspect_tab_call_count { 0 };
mutable size_t cookies_call_count { 0 };
@ -1078,6 +1092,7 @@ public:
mutable size_t delete_cookies_call_count { 0 };
mutable size_t listen_for_cookie_changes_call_count { 0 };
mutable size_t stop_listening_for_cookie_changes_call_count { 0 };
mutable size_t inspect_storage_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 };
@ -1412,6 +1427,36 @@ static JsonObject get_cookie_store_objects(ProtocolClient& client, StringView co
return client.request(move(get_store_objects));
}
static String get_storage_actor(ProtocolClient& client, StringView resource_type)
{
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(resource_type);
watch_resources.set("resourceTypes"sv, move(resource_types));
EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor);
return actor_from(read_resource(client, resource_type), "actor"sv);
}
static JsonObject get_storage_store_objects(ProtocolClient& client, StringView storage_actor, StringView host = "https://example.test"sv, Optional<JsonArray> names = {})
{
JsonObject get_store_objects;
get_store_objects.set("to"sv, storage_actor);
get_store_objects.set("type"sv, "getStoreObjects"sv);
get_store_objects.set("host"sv, host);
if (names.has_value())
get_store_objects.set("names"sv, names.release_value());
else
get_store_objects.set("names"sv, JsonValue {});
get_store_objects.set("options"sv, JsonObject {});
return client.request(move(get_store_objects));
}
static JsonArray get_cookie_update_keys(JsonObject const& stores_update, StringView update_type, StringView host = "https://example.test"sv)
{
return stores_update.get_object("data"sv)
@ -1702,6 +1747,113 @@ TEST_CASE(storage_cookie_resource)
EXPECT(objects.get_array("data"sv)->is_empty());
}
TEST_CASE(storage_web_storage_resources)
{
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("local-storage"sv).value());
EXPECT(resources.get_bool("session-storage"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("local-storage"sv);
resource_types.must_append("session-storage"sv);
watch_resources.set("resourceTypes"sv, move(resource_types));
EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor);
auto local_storage_resource = read_resource(client, "local-storage"sv);
EXPECT_EQ(local_storage_resource.get_string("resourceKey"sv).value(), "localStorage"sv);
EXPECT_EQ(local_storage_resource.get_integer<u64>("browsingContextID"sv).value(), 1u);
EXPECT_EQ(local_storage_resource.get_integer<u64>("innerWindowId"sv).value(), 1u);
EXPECT_EQ(local_storage_resource.get_string("resourceId"sv).value(), "localStorage-1"sv);
EXPECT(local_storage_resource.get_object("hosts"sv)->has_array("https://example.test"sv));
auto local_traits = local_storage_resource.get_object("traits"sv).release_value();
EXPECT(local_traits.get_bool("supportsAddItem"sv).value());
EXPECT(local_traits.get_bool("supportsRemoveAll"sv).value());
EXPECT(!local_traits.get_bool("supportsRemoveAllSessionCookies"sv).value());
EXPECT(local_traits.get_bool("supportsRemoveItem"sv).value());
auto local_storage_actor = actor_from(local_storage_resource, "actor"sv);
auto fields = client.request(local_storage_actor, "getFields"sv).get_array("value"sv).release_value();
EXPECT_EQ(fields.size(), 2u);
EXPECT_EQ(fields.at(0).as_object().get_string("name"sv).value(), "name"sv);
EXPECT(fields.at(0).as_object().get_bool("editable"sv).value());
EXPECT_EQ(fields.at(1).as_object().get_string("name"sv).value(), "value"sv);
EXPECT(fields.at(1).as_object().get_bool("editable"sv).value());
auto session_storage_resource = read_resource(client, "session-storage"sv);
EXPECT_EQ(session_storage_resource.get_string("resourceKey"sv).value(), "sessionStorage"sv);
EXPECT_EQ(session_storage_resource.get_string("resourceId"sv).value(), "sessionStorage-1"sv);
EXPECT(session_storage_resource.get_object("hosts"sv)->has_array("https://example.test"sv));
}
TEST_CASE(storage_web_storage_store_objects)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
session->delegate.fixture_local_storage_items.append({ "beta"_string, "two"_string });
session->delegate.fixture_local_storage_items.append({ "alpha"_string, "one"_string });
session->delegate.fixture_session_storage_items.append({ "session-key"_string, "session-value"_string });
auto local_storage_actor = get_storage_actor(client, "local-storage"sv);
auto objects = get_storage_store_objects(client, local_storage_actor);
EXPECT_EQ(session->delegate.inspect_storage_call_count, 1u);
EXPECT_EQ(objects.get_integer<size_t>("offset"sv).value(), 0u);
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 2u);
auto data = objects.get_array("data"sv).release_value();
EXPECT_EQ(data.size(), 2u);
EXPECT_EQ(data.at(0).as_object().get_string("name"sv).value(), "alpha"sv);
EXPECT_EQ(data.at(0).as_object().get_string("value"sv).value(), "one"sv);
EXPECT_EQ(data.at(1).as_object().get_string("name"sv).value(), "beta"sv);
EXPECT_EQ(data.at(1).as_object().get_string("value"sv).value(), "two"sv);
JsonArray names;
names.must_append("beta"sv);
auto filtered_objects = get_storage_store_objects(client, local_storage_actor, "https://example.test"sv, move(names));
EXPECT_EQ(filtered_objects.get_integer<size_t>("total"sv).value(), 1u);
auto filtered_data = filtered_objects.get_array("data"sv).release_value();
EXPECT_EQ(filtered_data.size(), 1u);
EXPECT_EQ(filtered_data.at(0).as_object().get_string("name"sv).value(), "beta"sv);
JsonObject paginated_request;
paginated_request.set("to"sv, local_storage_actor);
paginated_request.set("type"sv, "getStoreObjects"sv);
paginated_request.set("host"sv, "https://example.test"sv);
paginated_request.set("names"sv, JsonValue {});
JsonObject options;
options.set("offset"sv, 1);
options.set("size"sv, 1);
paginated_request.set("options"sv, move(options));
auto paginated_objects = client.request(move(paginated_request));
EXPECT_EQ(paginated_objects.get_integer<size_t>("offset"sv).value(), 1u);
EXPECT_EQ(paginated_objects.get_integer<size_t>("total"sv).value(), 2u);
auto paginated_data = paginated_objects.get_array("data"sv).release_value();
EXPECT_EQ(paginated_data.size(), 1u);
EXPECT_EQ(paginated_data.at(0).as_object().get_string("name"sv).value(), "beta"sv);
auto session_storage_actor = get_storage_actor(client, "session-storage"sv);
objects = get_storage_store_objects(client, session_storage_actor);
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 1u);
data = objects.get_array("data"sv).release_value();
EXPECT_EQ(data.at(0).as_object().get_string("name"sv).value(), "session-key"sv);
EXPECT_EQ(data.at(0).as_object().get_string("value"sv).value(), "session-value"sv);
objects = get_storage_store_objects(client, session_storage_actor, "https://other.test"sv);
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 0u);
EXPECT(objects.get_array("data"sv)->is_empty());
}
TEST_CASE(storage_cookie_store_objects)
{
auto session = create_session();