LibDevTools: Allow editing Web Storage

Firefox sends the same storage actor mutation requests for Web Storage
that it uses for cookies. Handle addItem, editItem, removeItem, and
removeAll for localStorage and sessionStorage actors.

Apply the changes through the browser-process storage jar and emit the
matching store update packets so the Storage panel stays current after a
DevTools-initiated edit.
This commit is contained in:
Sam Atkins 2026-06-10 16:27:25 +01:00
parent a75e9f23db
commit 0b9e59f61f
11 changed files with 596 additions and 5 deletions

View file

@ -33,6 +33,19 @@ static JsonObject serialize_storage_item(DevToolsDelegate::StorageItem const& it
return object;
}
static Optional<String> string_from_devtools_value(JsonValue const& value)
{
if (value.is_string())
return value.as_string();
if (value.is_bool())
return value.as_bool() ? "true"_string : "false"_string;
if (value.is_number())
return MUST(String::formatted("{}", value.get_number_with_precision_loss<double>().release_value()));
if (value.is_null())
return {};
return value.serialized();
}
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));
@ -122,6 +135,26 @@ void StorageActor::handle_message(Message const& message)
return;
}
if (message.type == "editItem"sv) {
edit_item(message);
return;
}
if (message.type == "addItem"sv) {
add_item(message);
return;
}
if (message.type == "removeItem"sv) {
remove_item(message);
return;
}
if (message.type == "removeAll"sv) {
remove_all(message);
return;
}
send_unrecognized_packet_type_error(message);
}
@ -215,6 +248,163 @@ void StorageActor::send_store_objects(Message const& message, Optional<String> r
send_response(message, move(response));
}
void StorageActor::edit_item(Message const& message)
{
auto data = get_required_parameter<JsonObject>(message, "data"sv);
if (!data.has_value())
return;
auto requested_host = data->get_string("host"sv);
if (!requested_host.has_value()) {
send_missing_parameter_error(message, "host"sv);
return;
}
if (!host().has_value() || *host() != *requested_host) {
send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available")));
return;
}
auto field = data->get_string("field"sv);
if (!field.has_value()) {
send_missing_parameter_error(message, "field"sv);
return;
}
auto items = data->get_object("items"sv);
if (!items.has_value()) {
send_missing_parameter_error(message, "items"sv);
return;
}
auto key = items->get_string("name"sv);
if (!key.has_value()) {
send_missing_parameter_error(message, "items.name"sv);
return;
}
auto value_json = items->get("value"sv);
if (!value_json.has_value()) {
send_missing_parameter_error(message, "items.value"sv);
return;
}
auto value = string_from_devtools_value(*value_json);
if (!value.has_value()) {
send_response(message, to_storage_operation_result(Error::from_string_literal("Missing storage value")));
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
if (*field == "name"sv) {
auto old_value_json = data->get("oldValue"sv);
if (!old_value_json.has_value()) {
send_missing_parameter_error(message, "oldValue"sv);
return;
}
auto old_key = string_from_devtools_value(*old_value_json);
if (!old_key.has_value()) {
send_response(message, to_storage_operation_result(Error::from_string_literal("Missing old storage key")));
return;
}
if (*old_key != *key) {
auto remove_result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *old_key);
if (remove_result.is_error()) {
send_response(message, to_storage_operation_result(remove_result.release_error()));
return;
}
}
}
auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *key, *value);
if (result.is_error())
send_response(message, to_storage_operation_result(result.release_error()));
else
send_response(message, to_storage_operation_result(ErrorOr<void> {}));
}
void StorageActor::add_item(Message const& message)
{
auto guid = get_required_parameter<String>(message, "guid"sv);
if (!guid.has_value())
return;
auto requested_host = message.data.get_string("host"sv)
.value_or_lazy_evaluated_optional([this] { return host(); });
if (!requested_host.has_value()) {
send_response(message, to_storage_operation_result(Error::from_string_literal("No storage host is available for this page")));
return;
}
if (!host().has_value() || *host() != *requested_host) {
send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available")));
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *guid, "value"_string);
if (result.is_error())
send_response(message, to_storage_operation_result(result.release_error()));
else
send_response(message, to_storage_operation_result(ErrorOr<void> {}));
}
void StorageActor::remove_item(Message const& message)
{
auto requested_host = get_required_parameter<String>(message, "host"sv);
if (!requested_host.has_value())
return;
auto name = get_required_parameter<String>(message, "name"sv);
if (!name.has_value())
return;
if (!host().has_value() || *host() != *requested_host) {
send_response(message, {});
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
auto result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *name);
if (result.is_error())
send_response(message, to_storage_operation_result(result.release_error()));
else
send_response(message, {});
}
void StorageActor::remove_all(Message const& message)
{
auto requested_host = get_required_parameter<String>(message, "host"sv);
if (!requested_host.has_value())
return;
if (!host().has_value() || *host() != *requested_host) {
send_response(message, {});
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
auto result = devtools().delegate().clear_storage(tab->description(), m_storage_endpoint, *requested_host);
if (result.is_error())
send_response(message, to_storage_operation_result(result.release_error()));
else
send_response(message, {});
}
void StorageActor::on_storage_changed(DevToolsDelegate::StorageChange change)
{
if (change.storage_endpoint != m_storage_endpoint)

View file

@ -34,6 +34,10 @@ private:
void get_fields(Message const&);
void get_store_objects(Message const&);
void edit_item(Message const&);
void add_item(Message const&);
void remove_item(Message const&);
void remove_all(Message const&);
void send_store_objects(Message const&, Optional<String> requested_host, Optional<JsonArray> requested_names, JsonObject options, ErrorOr<Vector<DevToolsDelegate::StorageItem>>);
void send_store_update(DevToolsDelegate::StorageChange const&);
void send_store_cleared(DevToolsDelegate::StorageChange const&);

View file

@ -53,6 +53,9 @@ public:
using OnStorageItemsReceived = Function<void(ErrorOr<Vector<StorageItem>>)>;
virtual void inspect_storage(TabDescription const&, Web::StorageAPI::StorageEndpointType, OnStorageItemsReceived) const { }
virtual ErrorOr<Optional<String>> set_storage_item(TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&, String const&, String const&) const { return Optional<String> {}; }
virtual ErrorOr<Optional<String>> remove_storage_item(TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&, String const&) const { return Optional<String> {}; }
virtual ErrorOr<void> clear_storage(TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&) const { return {}; }
struct StorageChange {
enum class Type : u8 {

View file

@ -1867,6 +1867,14 @@ void Application::stop_listening_for_host_cookie_changes(DevTools::TabDescriptio
view->stop_listening_for_host_cookie_changes();
}
static ErrorOr<Optional<String>> storage_set_result_to_error_or_old_value(StorageSetResult result)
{
if (result.has<StorageOperationError>())
return Error::from_string_literal("Unable to store more than the storage quota");
return result.get<Optional<String>>();
}
void Application::inspect_storage(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, OnStorageItemsReceived on_complete) const
{
static u64 next_request_id = 0;
@ -1882,6 +1890,67 @@ void Application::inspect_storage(DevTools::TabDescription const& description, W
view->inspect_storage(storage_endpoint, request_id);
}
ErrorOr<Optional<String>> Application::set_storage_item(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key, String const& key, String const& value) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return Error::from_string_literal("Unable to locate tab");
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage) {
auto result = view->set_session_storage_item(key, value);
if (!result.has_value())
return Error::from_string_literal("Unable to locate session storage");
return TRY(storage_set_result_to_error_or_old_value(result.release_value()));
}
auto old_value = TRY(storage_set_result_to_error_or_old_value(Application::storage_jar().set_item(storage_endpoint, storage_key, key, value)));
if (!old_value.has_value()) {
view->notify_storage_changed({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Added, key });
} else if (*old_value != value) {
view->notify_storage_changed({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Changed, key });
}
return old_value;
}
ErrorOr<Optional<String>> Application::remove_storage_item(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key, String const& key) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return Error::from_string_literal("Unable to locate tab");
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage)
return view->remove_session_storage_item(key);
auto old_value = Application::storage_jar().get_item(storage_endpoint, storage_key, key);
if (!old_value.has_value())
return Optional<String> {};
Application::storage_jar().remove_item(storage_endpoint, storage_key, key);
view->notify_storage_changed({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Deleted, key });
return old_value;
}
ErrorOr<void> Application::clear_storage(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return Error::from_string_literal("Unable to locate tab");
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage) {
view->clear_session_storage();
return {};
}
auto keys = Application::storage_jar().get_all_keys(storage_endpoint, storage_key);
if (keys.is_empty())
return {};
Application::storage_jar().clear_storage_key(storage_endpoint, storage_key);
view->notify_storage_changed({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Cleared, {} });
return {};
}
u64 Application::add_storage_change_listener(DevTools::TabDescription const& description, OnStorageChange on_storage_change) const
{
if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value())

View file

@ -275,6 +275,9 @@ private:
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 ErrorOr<Optional<String>> set_storage_item(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&, String const&, String const&) const override;
virtual ErrorOr<Optional<String>> remove_storage_item(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&, String const&) const override;
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_tab(DevTools::TabDescription const&, OnTabInspectionComplete) const override;

View file

@ -500,6 +500,21 @@ void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType st
client().async_inspect_storage(page_id(), storage_endpoint, request_id);
}
Optional<StorageSetResult> ViewImplementation::set_session_storage_item(String const& key, String const& value)
{
return client().set_session_storage_item(page_id(), key, value);
}
Optional<String> ViewImplementation::remove_session_storage_item(String const& key)
{
return client().remove_session_storage_item(page_id(), key);
}
bool ViewImplementation::clear_session_storage()
{
return client().clear_session_storage(page_id());
}
void ViewImplementation::inspect_accessibility_tree()
{
client().async_inspect_accessibility_tree(page_id());

View file

@ -44,6 +44,7 @@
#include <LibWebView/Forward.h>
#include <LibWebView/PageInfo.h>
#include <LibWebView/Settings.h>
#include <LibWebView/StorageSetResult.h>
#include <LibWebView/WebContentClient.h>
namespace WebView {
@ -127,6 +128,9 @@ public:
void inspect_dom_tree();
void inspect_storage(Web::StorageAPI::StorageEndpointType, u64 request_id);
Optional<StorageSetResult> set_session_storage_item(String const& key, String const& value);
Optional<String> remove_session_storage_item(String const& key);
bool clear_session_storage();
void inspect_accessibility_tree();
void get_hovered_node_id();
void start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent);

View file

@ -604,6 +604,67 @@ void ConnectionFromClient::inspect_storage(u64 page_id, Web::StorageAPI::Storage
async_did_inspect_storage(page_id, request_id, storage_items.serialized());
}
static Optional<GC::Ref<Web::HTML::Storage>> active_session_storage_for_page(PageClient& page)
{
auto* document = page.page().top_level_browsing_context().active_document();
if (!document || !document->window())
return {};
auto storage_or_error = document->window()->session_storage();
if (storage_or_error.is_exception())
return {};
return storage_or_error.release_value();
}
Messages::WebContentServer::SetSessionStorageItemResponse ConnectionFromClient::set_session_storage_item(u64 page_id, String key, String value)
{
auto page = this->page(page_id);
if (!page.has_value())
return Optional<WebView::StorageSetResult> {};
auto storage = active_session_storage_for_page(*page);
if (!storage.has_value())
return Optional<WebView::StorageSetResult> {};
auto old_value = (*storage)->get_item(key);
auto result = (*storage)->set_item(key, value);
if (result.is_exception())
return WebView::StorageSetResult { WebView::StorageOperationError::QuotaExceededError };
return WebView::StorageSetResult { move(old_value) };
}
Messages::WebContentServer::RemoveSessionStorageItemResponse ConnectionFromClient::remove_session_storage_item(u64 page_id, String key)
{
auto page = this->page(page_id);
if (!page.has_value())
return Optional<String> {};
auto storage = active_session_storage_for_page(*page);
if (!storage.has_value())
return Optional<String> {};
auto old_value = (*storage)->get_item(key);
if (old_value.has_value())
(*storage)->remove_item(key);
return old_value;
}
Messages::WebContentServer::ClearSessionStorageResponse ConnectionFromClient::clear_session_storage(u64 page_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return false;
auto storage = active_session_storage_for_page(*page);
if (!storage.has_value() || (*storage)->length() == 0)
return false;
(*storage)->clear();
return true;
}
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

@ -93,6 +93,9 @@ private:
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 Messages::WebContentServer::SetSessionStorageItemResponse set_session_storage_item(u64 page_id, String key, String value) override;
virtual Messages::WebContentServer::RemoveSessionStorageItemResponse remove_session_storage_item(u64 page_id, String key) override;
virtual Messages::WebContentServer::ClearSessionStorageResponse clear_session_storage(u64 page_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

@ -23,6 +23,7 @@
#include <LibWebView/DOMNodeProperties.h>
#include <LibWebView/PageInfo.h>
#include <LibWebView/Settings.h>
#include <LibWebView/StorageSetResult.h>
endpoint WebContentServer
{
@ -60,6 +61,9 @@ endpoint WebContentServer
get_source(u64 page_id) =|
inspect_dom_tree(u64 page_id) =|
inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) =|
set_session_storage_item(u64 page_id, String key, String value) => (Optional<WebView::StorageSetResult> result)
remove_session_storage_item(u64 page_id, String key) => (Optional<String> old_value)
clear_session_storage(u64 page_id) => (bool did_clear)
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

@ -656,13 +656,65 @@ public:
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;
callback(storage_items_for_endpoint(storage_endpoint));
}
virtual ErrorOr<Optional<String>> set_storage_item(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key, String const& key, String const& value) const override
{
++set_storage_item_call_count;
if (fail_set_storage_item)
return Error::from_string_literal("Unable to set storage item");
auto& storage_items = storage_items_for_endpoint(storage_endpoint);
for (auto& item : storage_items) {
if (item.name != key)
continue;
auto old_value = item.value;
item.value = value;
if (old_value != value)
emit_storage_change({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Changed, key });
return old_value;
}
VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
callback(fixture_session_storage_items);
storage_items.append({ key, value });
emit_storage_change({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Added, key });
return Optional<String> {};
}
virtual ErrorOr<Optional<String>> remove_storage_item(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key, String const& key) const override
{
++remove_storage_item_call_count;
if (fail_remove_storage_item)
return Error::from_string_literal("Unable to remove storage item");
auto& storage_items = storage_items_for_endpoint(storage_endpoint);
for (auto i = 0uz; i < storage_items.size(); ++i) {
if (storage_items[i].name != key)
continue;
auto old_value = storage_items[i].value;
storage_items.remove(i);
emit_storage_change({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Deleted, key });
return old_value;
}
return Optional<String> {};
}
virtual ErrorOr<void> clear_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key) const override
{
++clear_storage_call_count;
if (fail_clear_storage)
return Error::from_string_literal("Unable to clear storage");
auto& storage_items = storage_items_for_endpoint(storage_endpoint);
if (storage_items.is_empty())
return {};
storage_items.clear();
emit_storage_change({ storage_endpoint, storage_key, DevTools::DevToolsDelegate::StorageChange::Type::Cleared, {} });
return {};
}
virtual u64 add_storage_change_listener(DevTools::TabDescription const&, OnStorageChange callback) const override
@ -1084,6 +1136,15 @@ public:
listener.value(change);
}
Vector<DevTools::DevToolsDelegate::StorageItem>& storage_items_for_endpoint(Web::StorageAPI::StorageEndpointType storage_endpoint) const
{
if (storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
return fixture_local_storage_items;
VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
return fixture_session_storage_items;
}
mutable Function<void(WebView::DOMNodeProperties)> on_dom_node_properties;
mutable Function<void(WebView::Mutation)> on_dom_mutation;
mutable Function<void(Web::CSS::StyleSheetIdentifier const&, String)> on_style_sheet_source;
@ -1115,9 +1176,15 @@ public:
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 set_storage_item_call_count { 0 };
mutable size_t remove_storage_item_call_count { 0 };
mutable size_t clear_storage_call_count { 0 };
mutable size_t add_storage_change_listener_call_count { 0 };
mutable size_t remove_storage_change_listener_call_count { 0 };
mutable u64 next_storage_change_listener_id { 1 };
mutable bool fail_set_storage_item { false };
mutable bool fail_remove_storage_item { false };
mutable bool fail_clear_storage { false };
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 };
@ -1260,6 +1327,7 @@ private:
|| *type == "pickerNodePicked"sv
|| *type == "pickerNodePreviewed"sv
|| *type == "tabListChanged"sv
|| *type == "storesCleared"sv
|| *type == "storesUpdate"sv
|| *type == "target-available-form"sv
|| *type == "target-destroyed-form"sv;
@ -1501,6 +1569,58 @@ static JsonArray get_storage_update_keys(JsonObject const& stores_update, String
.release_value();
}
static JsonObject make_storage_edit_items(StringView name, StringView value)
{
JsonObject items;
items.set("name"sv, name);
items.set("value"sv, value);
return items;
}
static JsonObject edit_storage_item(ProtocolClient& client, StringView storage_actor, StringView field, JsonValue old_value, StringView name, StringView value)
{
JsonObject data;
data.set("host"sv, "https://example.test"sv);
data.set("field"sv, field);
data.set("oldValue"sv, move(old_value));
data.set("items"sv, make_storage_edit_items(name, value));
JsonObject request;
request.set("to"sv, storage_actor);
request.set("type"sv, "editItem"sv);
request.set("data"sv, move(data));
return client.request(move(request));
}
static JsonObject add_storage_item(ProtocolClient& client, StringView storage_actor, StringView name)
{
JsonObject request;
request.set("to"sv, storage_actor);
request.set("type"sv, "addItem"sv);
request.set("guid"sv, name);
request.set("host"sv, "https://example.test"sv);
return client.request(move(request));
}
static JsonObject remove_storage_item(ProtocolClient& client, StringView storage_actor, StringView name)
{
JsonObject request;
request.set("to"sv, storage_actor);
request.set("type"sv, "removeItem"sv);
request.set("host"sv, "https://example.test"sv);
request.set("name"sv, name);
return client.request(move(request));
}
static JsonObject remove_all_storage_items(ProtocolClient& client, StringView storage_actor)
{
JsonObject request;
request.set("to"sv, storage_actor);
request.set("type"sv, "removeAll"sv);
request.set("host"sv, "https://example.test"sv);
return client.request(move(request));
}
static JsonObject make_cookie_edit_items(HTTP::Cookie::Cookie const& cookie)
{
JsonObject items;
@ -1987,6 +2107,121 @@ TEST_CASE(storage_web_storage_change_events_for_file_urls)
EXPECT_EQ(added_keys.at(0).as_string(), "alpha"sv);
}
TEST_CASE(storage_web_storage_add_edit_and_remove_items)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto local_storage_actor = get_storage_actor(client, "local-storage"sv);
auto response = add_storage_item(client, local_storage_actor, "devtools-key"sv);
EXPECT(response.get("errorString"sv).value().is_null());
EXPECT_EQ(session->delegate.set_storage_item_call_count, 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].name, "devtools-key"sv);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].value, "value"sv);
auto stores_update = read_packet_with_type(client, "storesUpdate"sv);
auto added_keys = get_storage_update_keys(stores_update, "added"sv, "localStorage"sv);
EXPECT_EQ(added_keys.size(), 1u);
EXPECT_EQ(added_keys.at(0).as_string(), "devtools-key"sv);
response = edit_storage_item(client, local_storage_actor, "value"sv, "value"_string, "devtools-key"sv, "updated-value"sv);
EXPECT(response.get("errorString"sv).value().is_null());
EXPECT_EQ(session->delegate.set_storage_item_call_count, 2u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].name, "devtools-key"sv);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].value, "updated-value"sv);
stores_update = read_packet_with_type(client, "storesUpdate"sv);
auto changed_keys = get_storage_update_keys(stores_update, "changed"sv, "localStorage"sv);
EXPECT_EQ(changed_keys.size(), 1u);
EXPECT_EQ(changed_keys.at(0).as_string(), "devtools-key"sv);
response = edit_storage_item(client, local_storage_actor, "name"sv, "devtools-key"_string, "renamed-key"sv, "updated-value"sv);
EXPECT(response.get("errorString"sv).value().is_null());
EXPECT_EQ(session->delegate.remove_storage_item_call_count, 1u);
EXPECT_EQ(session->delegate.set_storage_item_call_count, 3u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].name, "renamed-key"sv);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].value, "updated-value"sv);
stores_update = read_packet_with_type(client, "storesUpdate"sv);
auto deleted_keys = get_storage_update_keys(stores_update, "deleted"sv, "localStorage"sv);
EXPECT_EQ(deleted_keys.size(), 1u);
EXPECT_EQ(deleted_keys.at(0).as_string(), "devtools-key"sv);
stores_update = read_packet_with_type(client, "storesUpdate"sv);
added_keys = get_storage_update_keys(stores_update, "added"sv, "localStorage"sv);
EXPECT_EQ(added_keys.size(), 1u);
EXPECT_EQ(added_keys.at(0).as_string(), "renamed-key"sv);
response = remove_storage_item(client, local_storage_actor, "renamed-key"sv);
EXPECT_EQ(response.get_string("from"sv).value(), local_storage_actor);
EXPECT_EQ(session->delegate.remove_storage_item_call_count, 2u);
EXPECT(session->delegate.fixture_local_storage_items.is_empty());
stores_update = read_packet_with_type(client, "storesUpdate"sv);
deleted_keys = get_storage_update_keys(stores_update, "deleted"sv, "localStorage"sv);
EXPECT_EQ(deleted_keys.size(), 1u);
EXPECT_EQ(deleted_keys.at(0).as_string(), "renamed-key"sv);
session->delegate.fixture_local_storage_items.append({ "first"_string, "one"_string });
session->delegate.fixture_local_storage_items.append({ "second"_string, "two"_string });
response = remove_all_storage_items(client, local_storage_actor);
EXPECT_EQ(response.get_string("from"sv).value(), local_storage_actor);
EXPECT_EQ(session->delegate.clear_storage_call_count, 1u);
EXPECT(session->delegate.fixture_local_storage_items.is_empty());
auto stores_cleared = read_packet_with_type(client, "storesCleared"sv);
EXPECT_EQ(stores_cleared.get_string("from"sv).value(), local_storage_actor);
auto cleared_hosts = stores_cleared.get_object("data"sv)->get_array("clearedHostsOrPaths"sv).release_value();
EXPECT_EQ(cleared_hosts.size(), 1u);
EXPECT_EQ(cleared_hosts.at(0).as_string(), "https://example.test"sv);
auto session_storage_actor = get_storage_actor(client, "session-storage"sv);
response = add_storage_item(client, session_storage_actor, "session-key"sv);
EXPECT(response.get("errorString"sv).value().is_null());
EXPECT_EQ(session->delegate.fixture_session_storage_items.size(), 1u);
EXPECT_EQ(session->delegate.fixture_session_storage_items[0].name, "session-key"sv);
EXPECT_EQ(session->delegate.fixture_session_storage_items[0].value, "value"sv);
stores_update = read_packet_with_type(client, "storesUpdate"sv);
added_keys = get_storage_update_keys(stores_update, "added"sv, "sessionStorage"sv);
EXPECT_EQ(added_keys.size(), 1u);
EXPECT_EQ(added_keys.at(0).as_string(), "session-key"sv);
}
TEST_CASE(storage_web_storage_mutation_errors)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto local_storage_actor = get_storage_actor(client, "local-storage"sv);
session->delegate.fixture_local_storage_items.append({ "devtools-key"_string, "value"_string });
session->delegate.fail_remove_storage_item = true;
auto response = edit_storage_item(client, local_storage_actor, "name"sv, "devtools-key"_string, "renamed-key"sv, "value"sv);
EXPECT_EQ(response.get_string("errorString"sv).value(), "Unable to remove storage item"sv);
EXPECT_EQ(session->delegate.remove_storage_item_call_count, 1u);
EXPECT_EQ(session->delegate.set_storage_item_call_count, 0u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items[0].name, "devtools-key"sv);
response = remove_storage_item(client, local_storage_actor, "devtools-key"sv);
EXPECT_EQ(response.get_string("errorString"sv).value(), "Unable to remove storage item"sv);
EXPECT_EQ(session->delegate.remove_storage_item_call_count, 2u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
session->delegate.fail_remove_storage_item = false;
session->delegate.fail_clear_storage = true;
response = remove_all_storage_items(client, local_storage_actor);
EXPECT_EQ(response.get_string("errorString"sv).value(), "Unable to clear storage"sv);
EXPECT_EQ(session->delegate.clear_storage_call_count, 1u);
EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u);
}
TEST_CASE(storage_cookie_store_objects)
{
auto session = create_session();