LibWeb+LibWebView: Add a button to the Inspector to export its contents

When working on the Inspector's HTML, it's often kind of tricky to debug
when an element is styled / positioned incorrectly. We don't have a way
to inspect the Inspector itself.

This adds a button to the Inspector to export its HTML/CSS/JS contents
to the downloads directory. This allows for more easily testing changes,
especially by opening the exported HTML in another browser's dev tools.

We will ultimately likely remove this button (or make it hidden) by the
time we are production-ready. But it's quite useful for now.
This commit is contained in:
Timothy Flynn 2024-08-19 12:11:39 -04:00 committed by Andreas Kling
parent cde7c91c54
commit 3ec5c1941f
Notes: github-actions[bot] 2024-08-20 07:29:17 +00:00
15 changed files with 123 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

View file

@ -92,7 +92,7 @@ body {
display: flex;
align-items: center;
justify-content: center;
justify-content: space-between;
z-index: 100;
}
@ -134,6 +134,30 @@ body {
border-left: 1px solid var(--tab-button-border);
}
.global-controls {
margin: 0 8px 0 8px;
}
.global-controls button {
width: 24px;
height: 24px;
border: none;
outline: none;
cursor: pointer;
}
#export-inspector-button {
background-image: url("resource://icons/16x16/download.png");
background-position: center;
background-repeat: no-repeat;
background-color: var(--tab-controls);
}
#export-inspector-button:hover {
background-color: var(--tab-button-background);
}
.tab-content {
height: calc(100% - 40px);

View file

@ -91,6 +91,11 @@ const scrollToElement = element => {
window.scrollTo(0, position);
};
inspector.exportInspector = () => {
const html = document.documentElement.outerHTML;
inspector.exportInspectorHTML(html);
};
inspector.reset = () => {
let domTree = document.getElementById("dom-tree");
domTree.innerHTML = "";

View file

@ -11,6 +11,7 @@ set(16x16_ICONS
audio-volume-high.png
audio-volume-muted.png
close-tab.png
download.png
edit-copy.png
filetype-css.png
filetype-folder-open.png

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -76,4 +76,9 @@ void Inspector::execute_console_script(String const& script)
global_object().browsing_context()->page().client().inspector_did_execute_console_script(script);
}
void Inspector::export_inspector_html(String const& html)
{
global_object().browsing_context()->page().client().inspector_did_export_inspector_html(html);
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -31,6 +31,8 @@ public:
void execute_console_script(String const& script);
void export_inspector_html(String const& html);
private:
explicit Inspector(JS::Realm&);

View file

@ -14,4 +14,6 @@
undefined executeConsoleScript(DOMString script);
undefined exportInspectorHTML(DOMString html);
};

View file

@ -377,6 +377,7 @@ public:
virtual void inspector_did_replace_dom_node_attribute([[maybe_unused]] i32 node_id, [[maybe_unused]] size_t attribute_index, [[maybe_unused]] JS::NonnullGCPtr<DOM::NamedNodeMap> replacement_attributes) { }
virtual void inspector_did_request_dom_tree_context_menu([[maybe_unused]] i32 node_id, [[maybe_unused]] CSSPixelPoint position, [[maybe_unused]] String const& type, [[maybe_unused]] Optional<String> const& tag, [[maybe_unused]] Optional<size_t> const& attribute_index) { }
virtual void inspector_did_execute_console_script([[maybe_unused]] String const& script) { }
virtual void inspector_did_export_inspector_html([[maybe_unused]] String const& html) { }
virtual void schedule_repaint() = 0;
virtual bool is_ready_to_paint() const = 0;

View file

@ -7,7 +7,12 @@
#include <AK/Base64.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <LibCore/Directory.h>
#include <LibCore/File.h>
#include <LibCore/Resource.h>
#include <LibCore/StandardPaths.h>
#include <LibJS/MarkupGenerator.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWebView/InspectorClient.h>
@ -15,6 +20,9 @@
namespace WebView {
static constexpr auto INSPECTOR_CSS = "resource://ladybird/inspector.css"sv;
static constexpr auto INSPECTOR_JS = "resource://ladybird/inspector.js"sv;
static ErrorOr<JsonValue> parse_json_tree(StringView json)
{
auto parsed_tree = TRY(JsonValue::from_string(json));
@ -179,6 +187,47 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
m_content_web_view.js_console_input(script.to_byte_string());
};
m_inspector_web_view.on_inspector_exported_inspector_html = [this](String const& html) {
auto inspector_path = LexicalPath::join(Core::StandardPaths::downloads_directory(), "inspector"sv);
if (auto result = Core::Directory::create(inspector_path, Core::Directory::CreateDirectories::Yes); result.is_error()) {
append_console_warning(MUST(String::formatted("Unable to create {}: {}", inspector_path, result.error())));
return;
}
auto export_file = [&](auto name, auto const& contents) {
auto path = inspector_path.append(name);
auto file = Core::File::open(path.string(), Core::File::OpenMode::Write);
if (file.is_error()) {
append_console_warning(MUST(String::formatted("Unable to open {}: {}", path, file.error())));
return false;
}
if (auto result = file.value()->write_until_depleted(contents); result.is_error()) {
append_console_warning(MUST(String::formatted("Unable to save {}: {}", path, result.error())));
return false;
}
return true;
};
auto inspector_css = MUST(Core::Resource::load_from_uri(INSPECTOR_CSS));
auto inspector_js = MUST(Core::Resource::load_from_uri(INSPECTOR_JS));
auto inspector_html = MUST(html.replace(INSPECTOR_CSS, "inspector.css"sv, ReplaceMode::All));
inspector_html = MUST(inspector_html.replace(INSPECTOR_JS, "inspector.js"sv, ReplaceMode::All));
if (!export_file("inspector.html"sv, inspector_html))
return;
if (!export_file("inspector.css"sv, inspector_css->data()))
return;
if (!export_file("inspector.js"sv, inspector_js->data()))
return;
append_console_message(MUST(String::formatted("Exported Inspector files to {}", inspector_path)));
};
load_inspector();
}
@ -359,18 +408,22 @@ void InspectorClient::load_inspector()
builder.append(HTML_HIGHLIGHTER_STYLE);
builder.append(R"~~~(
builder.appendff(R"~~~(
</style>
<link href="resource://ladybird/inspector.css" rel="stylesheet" />
<link href="{}" rel="stylesheet" />
</head>
<body>
<div class="split-view">
<div id="inspector-top" class="split-view-container" style="height: 60%">
<div class="tab-controls-container">
<div class="global-controls"></div>
<div class="tab-controls">
<button id="dom-tree-button" onclick="selectTopTab(this, 'dom-tree')">DOM Tree</button>
<button id="accessibility-tree-button" onclick="selectTopTab(this, 'accessibility-tree')">Accessibility Tree</button>
</div>
<div class="global-controls">
<button id="export-inspector-button" title="Export the Inspector to an HTML file" onclick="inspector.exportInspector()"></button>
</div>
</div>
<div id="dom-tree" class="tab-content html"></div>
<div id="accessibility-tree" class="tab-content"></div>
@ -384,6 +437,7 @@ void InspectorClient::load_inspector()
</div>
<div id="inspector-bottom" class="split-view-container" style="height: calc(40% - 5px)">
<div class="tab-controls-container">
<div class="global-controls"></div>
<div class="tab-controls">
<button id="console-button" onclick="selectBottomTab(this, 'console')">Console</button>
<button id="computed-style-button" onclick="selectBottomTab(this, 'computed-style')">Computed Style</button>
@ -391,6 +445,7 @@ void InspectorClient::load_inspector()
<button id="custom-properties-button" onclick="selectBottomTab(this, 'custom-properties')">Custom Properties</button>
<button id="font-button" onclick="selectBottomTab(this, 'fonts')">Fonts</button>
</div>
<div class="global-controls"></div>
</div>
<div id="console" class="tab-content">
<div class="console">
@ -402,7 +457,8 @@ void InspectorClient::load_inspector()
</div>
</div>
</div>
)~~~"sv);
)~~~",
INSPECTOR_CSS);
auto generate_property_table = [&](auto name) {
builder.appendff(R"~~~(
@ -435,14 +491,15 @@ void InspectorClient::load_inspector()
</div>
)~~~"sv);
builder.append(R"~~~(
builder.appendff(R"~~~(
</div>
</div>
<script type="text/javascript" src="resource://ladybird/inspector.js"></script>
<script type="text/javascript" src="{}"></script>
</body>
</html>
)~~~"sv);
)~~~",
INSPECTOR_JS);
m_inspector_web_view.load_html(builder.string_view());
}

View file

@ -218,6 +218,7 @@ public:
Function<void(i32, size_t, Vector<Attribute> const&)> on_inspector_replaced_dom_node_attribute;
Function<void(i32, Gfx::IntPoint, String const&, Optional<String> const&, Optional<size_t> const&)> on_inspector_requested_dom_tree_context_menu;
Function<void(String const&)> on_inspector_executed_console_script;
Function<void(String const&)> on_inspector_exported_inspector_html;
Function<IPC::File()> on_request_worker_agent;
virtual Web::DevicePixelSize viewport_size() const = 0;

View file

@ -693,6 +693,14 @@ void WebContentClient::inspector_did_execute_console_script(u64 page_id, String
}
}
void WebContentClient::inspector_did_export_inspector_html(u64 page_id, String const& html)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_inspector_exported_inspector_html)
view->on_inspector_exported_inspector_html(html);
}
}
Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request_worker_agent(u64 page_id)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -120,6 +120,7 @@ private:
virtual void inspector_did_replace_dom_node_attribute(u64 page_id, i32 node_id, size_t attribute_index, Vector<Attribute> const& replacement_attributes) override;
virtual void inspector_did_request_dom_tree_context_menu(u64 page_id, i32 node_id, Gfx::IntPoint position, String const& type, Optional<String> const& tag, Optional<size_t> const& attribute_index) override;
virtual void inspector_did_execute_console_script(u64 page_id, String const& script) override;
virtual void inspector_did_export_inspector_html(u64 page_id, String const& html) override;
virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id) override;
Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());

View file

@ -659,6 +659,11 @@ void PageClient::inspector_did_execute_console_script(String const& script)
client().async_inspector_did_execute_console_script(m_id, script);
}
void PageClient::inspector_did_export_inspector_html(String const& html)
{
client().async_inspector_did_export_inspector_html(m_id, html);
}
ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path)
{
VERIFY(!m_webdriver);

View file

@ -167,6 +167,7 @@ private:
virtual void inspector_did_replace_dom_node_attribute(i32 node_id, size_t attribute_index, JS::NonnullGCPtr<Web::DOM::NamedNodeMap> replacement_attributes) override;
virtual void inspector_did_request_dom_tree_context_menu(i32 node_id, Web::CSSPixelPoint position, String const& type, Optional<String> const& tag, Optional<size_t> const& attribute_index) override;
virtual void inspector_did_execute_console_script(String const& script) override;
virtual void inspector_did_export_inspector_html(String const& script) override;
Web::Layout::Viewport* layout_root();
void setup_palette();

View file

@ -101,5 +101,6 @@ endpoint WebContentClient
inspector_did_replace_dom_node_attribute(u64 page_id, i32 node_id, size_t attribute_index, Vector<WebView::Attribute> replacement_attributes) =|
inspector_did_request_dom_tree_context_menu(u64 page_id, i32 node_id, Gfx::IntPoint position, String type, Optional<String> tag, Optional<size_t> attribute_index) =|
inspector_did_execute_console_script(u64 page_id, String script) =|
inspector_did_export_inspector_html(u64 page_id, String html) =|
}