LibWebView: Move headless clipboard management to LibWebView

We only supported headless clipboard management in test-web. So when WPT
tests the clipboard APIs, we would blindly try to access the Qt app,
which does not exist.

Note that the AppKit UI has no such restriction, as the NSPasteboard is
accessible even without a GUI.
This commit is contained in:
Timothy Flynn 2025-10-10 10:11:56 -04:00 committed by Tim Flynn
parent a4a15b9a1e
commit e57176b484
Notes: github-actions[bot] 2025-10-10 19:11:12 +00:00
9 changed files with 247 additions and 22 deletions

View file

@ -625,6 +625,27 @@ void Application::display_error_dialog(StringView error_message) const
warnln("{}", error_message);
}
Utf16String Application::clipboard_text() const
{
if (!m_clipboard.has_value())
return {};
if (m_clipboard->mime_type != "text/plain"sv)
return {};
return Utf16String::from_utf8(m_clipboard->data);
}
Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_entries() const
{
if (!m_clipboard.has_value())
return {};
return { *m_clipboard };
}
void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation entry)
{
m_clipboard = move(entry);
}
void Application::initialize_actions()
{
auto debug_request = [this](auto request) {

View file

@ -76,9 +76,10 @@ public:
virtual void display_download_confirmation_dialog(StringView download_name, LexicalPath const& path) const;
virtual void display_error_dialog(StringView error_message) const;
virtual Utf16String clipboard_text() const { return {}; }
virtual Vector<Web::Clipboard::SystemClipboardRepresentation> clipboard_entries() const { return {}; }
virtual void insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation) { }
// FIXME: We should implement UI-agnostic platform APIs to interact with the system clipboard.
virtual Utf16String clipboard_text() const;
virtual Vector<Web::Clipboard::SystemClipboardRepresentation> clipboard_entries() const;
virtual void insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation);
Action& reload_action() { return *m_reload_action; }
Action& copy_selection_action() { return *m_copy_selection_action; }
@ -221,6 +222,8 @@ private:
StringView m_user_agent_string;
StringView m_navigator_compatibility_mode;
Optional<Web::Clipboard::SystemClipboardRepresentation> m_clipboard;
#if defined(AK_OS_MACOS)
OwnPtr<MachPortServer> m_mach_port_server;
#endif

View file

@ -0,0 +1,23 @@
Harness status: OK
Found 17 tests
14 Pass
3 Fail
Pass navigator.clipboard exists
Pass navigator.clipboard.write([text/plain ClipboardItem]) succeeds
Fail navigator.clipboard.write([>1 ClipboardItems]) fails (not implemented)
Pass navigator.clipboard.write() fails (expect [ClipboardItem])
Pass navigator.clipboard.write(null) fails (expect [ClipboardItem])
Pass navigator.clipboard.write(DOMString) fails (expect [ClipboardItem])
Pass navigator.clipboard.write(Blob) fails (expect [ClipboardItem])
Pass navigator.clipboard.writeText(DOMString) succeeds
Pass navigator.clipboard.writeText() fails (expect DOMString)
Pass navigator.clipboard.write({string : DOMString}) succeeds
Pass navigator.clipboard.write({string : image/png Blob}) succeeds
Pass navigator.clipboard.write([text + png] succeeds
Fail navigator.clipboard.write(image/png DOMString) fails
Pass navigator.clipboard.read() succeeds
Fail navigator.clipboard.readText() succeeds
Pass navigator.clipboard.write(Promise<Blob>) succeeds
Pass navigator.clipboard.write(Promise<Blob>s) succeeds

View file

@ -0,0 +1,142 @@
<!doctype html>
<meta charset="utf-8">
<title>Async Clipboard input type validation tests</title>
<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api">
<body>Body needed for test_driver.click()</body>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../resources/testdriver.js"></script>
<script src="../resources/testdriver-vendor.js"></script>
<script src="resources/user-activation.js"></script>
<script>
// Permissions are required in order to invoke navigator.clipboard functions in
// an automated test.
async function getPermissions() {
await tryGrantReadPermission();
await tryGrantWritePermission();
await waitForUserActivation();
}
test(() => {
assert_not_equals(navigator.clipboard, undefined);
assert_true(navigator.clipboard instanceof Clipboard);
assert_equals(navigator.clipboard, navigator.clipboard);
}, 'navigator.clipboard exists');
promise_test(async () => {
await getPermissions();
const blob = new Blob(['hello'], {type: 'text/plain'});
const item = new ClipboardItem({'text/plain': blob});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write([text/plain ClipboardItem]) succeeds');
promise_test(async t => {
await getPermissions();
const blob1 = new Blob(['hello'], {type: 'text/plain'});
const blob2 = new Blob(['world'], {type: 'text/plain'});
const item1 = new ClipboardItem({'text/plain': blob1});
const item2 = new ClipboardItem({'text/plain': blob2});
await promise_rejects_dom(t, "NotAllowedError",
navigator.clipboard.write([item1, item2]));
}, 'navigator.clipboard.write([>1 ClipboardItems]) fails (not implemented)');
promise_test(async t => {
await getPermissions();
await promise_rejects_js(t, TypeError, navigator.clipboard.write());
}, 'navigator.clipboard.write() fails (expect [ClipboardItem])');
promise_test(async t => {
await getPermissions();
await promise_rejects_js(t, TypeError, navigator.clipboard.write(null));
}, 'navigator.clipboard.write(null) fails (expect [ClipboardItem])');
promise_test(async t => {
await getPermissions();
await promise_rejects_js(t, TypeError,
navigator.clipboard.write('Bad string'));
}, 'navigator.clipboard.write(DOMString) fails (expect [ClipboardItem])');
promise_test(async t => {
await getPermissions();
const blob = new Blob(['hello'], {type: 'text/plain'});
await promise_rejects_js(t, TypeError, navigator.clipboard.write(blob));
}, 'navigator.clipboard.write(Blob) fails (expect [ClipboardItem])');
promise_test(async () => {
await getPermissions();
await navigator.clipboard.writeText('New clipboard text');
}, 'navigator.clipboard.writeText(DOMString) succeeds');
promise_test(async t => {
await getPermissions();
await promise_rejects_js(t, TypeError,
navigator.clipboard.writeText());
}, 'navigator.clipboard.writeText() fails (expect DOMString)');
promise_test(async () => {
await getPermissions();
const item = new ClipboardItem({'text/plain': 'test'});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write({string : DOMString}) succeeds');
promise_test(async () => {
await getPermissions();
const fetched = await fetch('../clipboard-apis/resources/greenbox.png');
const image = await fetched.blob();
const item = new ClipboardItem({'image/png': image});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write({string : image/png Blob}) succeeds');
promise_test(async() => {
await getPermissions();
const fetched = await fetch('../clipboard-apis/resources/greenbox.png');
const image = await fetched.blob();
const item = new ClipboardItem({
'text/plain': new Blob(['first'], {type: 'text/plain'}),
'image/png': image});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write([text + png] succeeds');
promise_test(async t => {
await getPermissions();
const item = new ClipboardItem({'image/png': 'not an image'});
await promise_rejects_js(t, TypeError, navigator.clipboard.write([item]));
}, 'navigator.clipboard.write(image/png DOMString) fails');
promise_test(async () => {
await getPermissions();
const result = await navigator.clipboard.read();
assert_true(result instanceof Object);
assert_true(result[0] instanceof ClipboardItem);
}, 'navigator.clipboard.read() succeeds');
promise_test(async () => {
await getPermissions();
const result = await navigator.clipboard.readText();
assert_equals(typeof result, 'string');
}, 'navigator.clipboard.readText() succeeds');
promise_test(async () => {
await getPermissions();
const promise_blob = Promise.resolve(new Blob(['hello'], {type: 'text/plain'}));
const item = new ClipboardItem({'text/plain': promise_blob});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write(Promise<Blob>) succeeds');
promise_test(async () => {
await getPermissions();
const promise_text_blob = Promise.resolve(new Blob(['hello'], {type: 'text/plain'}));
const promise_html_blob = Promise.resolve(new Blob(["<p style='color: red; font-style: oblique;'>Test</p>"], {type: 'text/html'}));
const item = new ClipboardItem({'text/plain': promise_text_blob, 'text/html': promise_html_blob});
await navigator.clipboard.write([item]);
}, 'navigator.clipboard.write(Promise<Blob>s) succeeds');
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View file

@ -0,0 +1,44 @@
'use strict';
// In order to use this function, please import testdriver.js and
// testdriver-vendor.js, and include a <body> element.
async function waitForUserActivation() {
if (window.opener) {
throw new Error(
"waitForUserActivation() only works in the top-level frame");
}
const loadedPromise = new Promise(resolve => {
if(document.readyState == 'complete') {
resolve();
return;
}
window.addEventListener('load', resolve, {once: true});
});
await loadedPromise;
const clickedPromise = new Promise(resolve => {
document.body.addEventListener('click', resolve, {once: true});
});
test_driver.click(document.body);
await clickedPromise;
}
async function trySetPermission(perm, state) {
try {
await test_driver.set_permission({ name: perm }, state)
} catch {
// This is expected, as clipboard permissions are not supported by every engine
// and also the set_permission. The permission is not required by such engines as
// they require user activation instead.
}
}
async function tryGrantReadPermission() {
await trySetPermission("clipboard-read", "granted");
}
async function tryGrantWritePermission() {
await trySetPermission("clipboard-write", "granted");
}

View file

@ -93,16 +93,4 @@ ErrorOr<void> Application::launch_test_fixtures()
return {};
}
Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_entries() const
{
if (m_clipboard.has_value())
return { *m_clipboard };
return {};
}
void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation entry)
{
m_clipboard = move(entry);
}
}

View file

@ -45,13 +45,6 @@ public:
int per_test_timeout_in_seconds { 30 };
u8 verbosity { 0 };
private:
virtual Vector<Web::Clipboard::SystemClipboardRepresentation> clipboard_entries() const override;
virtual void insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation) override;
// FIXME: We should implement UI-agnostic platform APIs to interact with the system clipboard.
Optional<Web::Clipboard::SystemClipboardRepresentation> m_clipboard;
};
}

View file

@ -177,12 +177,18 @@ void Application::display_error_dialog(StringView error_message) const
Utf16String Application::clipboard_text() const
{
if (browser_options().headless_mode.has_value())
return WebView::Application::clipboard_text();
auto const* clipboard = QGuiApplication::clipboard();
return utf16_string_from_qstring(clipboard->text());
}
Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_entries() const
{
if (browser_options().headless_mode.has_value())
return WebView::Application::clipboard_entries();
Vector<Web::Clipboard::SystemClipboardRepresentation> representations;
auto const* clipboard = QGuiApplication::clipboard();
@ -202,6 +208,11 @@ Vector<Web::Clipboard::SystemClipboardRepresentation> Application::clipboard_ent
void Application::insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation entry)
{
if (browser_options().headless_mode.has_value()) {
WebView::Application::insert_clipboard_entry(move(entry));
return;
}
auto* mime_data = new QMimeData();
mime_data->setData(qstring_from_ak_string(entry.mime_type), qbytearray_from_ak_string(entry.data));