| <!DOCTYPE html> |
| <meta charset="utf-8" /> |
| <title> |
| 'clipboardchange' event should be fired upon setting clipboard using JS |
| </title> |
| <link rel="help" href="https://www.w3.org/TR/clipboard-apis/#clipboard-event-clipboardchange" /> |
| |
| <body> |
| Body needed for test_driver.click() |
| <p><button id="button">Put payload in the clipboard</button></p> |
| <div id="output"></div> |
| <iframe id="iframe" srcdoc="<p>Some text</p>"></iframe> |
| <link rel="help" href="https://issues.chromium.org/issues/41442253" /> |
| |
| <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> |
| function waitForRender() { |
| return new Promise(resolve => { |
| requestAnimationFrame(() => requestAnimationFrame(resolve)); |
| }); |
| } |
| |
| let typesToSet_ = ["text/html", "web txt/csv"]; |
| button.onclick = () => document.execCommand("copy"); |
| document.oncopy = (ev) => { |
| ev.preventDefault(); |
| for (let i = 0; i < typesToSet_.length; i++) { |
| const type = typesToSet_[i]; |
| const data = new Blob([`Test data for ${type}`], {type: type}); |
| ev.clipboardData.setData(type, data); |
| } |
| }; |
| |
| function triggerCopyToClipboard(typesToSet) { |
| if (typesToSet) { |
| typesToSet_ = typesToSet; |
| } |
| return test_driver.click(button); |
| } |
| |
| promise_test(async (test) => { |
| let clipboardChangeEventCount = 0; |
| let eventType = ""; |
| let capturedEventTypes = null; |
| navigator.clipboard.addEventListener("clipboardchange", (ev) => { |
| clipboardChangeEventCount++; |
| eventType = ev.type; |
| capturedEventTypes = ev.types; |
| }); |
| await triggerCopyToClipboard(); |
| assert_equals(clipboardChangeEventCount, 1, "clipboardchange event should be called exactly once"); |
| assert_equals(eventType, "clipboardchange", "Event type should be 'clipboardchange'"); |
| assert_true(capturedEventTypes.includes("text/html"), "types should contain 'text/html'"); |
| assert_false(capturedEventTypes.includes("web txt/csv"), "types should not contain custom MIME type"); |
| }, "clipboardchange event is invoked"); |
| |
| promise_test(async (test) => { |
| await tryGrantWritePermission(); |
| let clipboardChangeEventCount = 0; |
| let capturedEventTypes = null; |
| navigator.clipboard.addEventListener("clipboardchange", (ev) => { |
| clipboardChangeEventCount++; |
| capturedEventTypes = ev.types; |
| }); |
| await navigator.clipboard.writeText("Test text"); |
| await waitForRender(); |
| assert_equals(clipboardChangeEventCount, 1, "clipboardchange event should be called exactly once"); |
| assert_true(capturedEventTypes.includes("text/plain"), "types should contain 'text/plain'"); |
| }, "clipboardchange event is invoked with async clipboard API"); |
| |
| promise_test(async (test) => { |
| let onClipboardChangeAttributeCount = 0; |
| let capturedEventTypes = null; |
| navigator.clipboard.onclipboardchange = (ev) => { |
| onClipboardChangeAttributeCount++; |
| capturedEventTypes = ev.types; |
| }; |
| await triggerCopyToClipboard(); |
| assert_equals(onClipboardChangeAttributeCount, 1, "onclipboardchange attribute should be called exactly once"); |
| assert_true(capturedEventTypes.includes("text/html"), "types should contain 'text/html'"); |
| assert_false(capturedEventTypes.includes("web txt/csv"), "types should not contain custom MIME type"); |
| }, "clipboardchange event is invoked using onclipboardchange attribute"); |
| |
| promise_test(async (test) => { |
| let onClipboardChangeAttributeCount = 0; |
| let capturedEventTypes = null; |
| navigator.clipboard.onclipboardchange = (ev) => { |
| onClipboardChangeAttributeCount++; |
| capturedEventTypes = ev.types; |
| }; |
| await triggerCopyToClipboard(["web txt/csv"]); |
| assert_equals(onClipboardChangeAttributeCount, 1, "onclipboardchange attribute should be called exactly once"); |
| assert_equals(capturedEventTypes.length, 0, "clipboardchange event should have no types"); |
| }, "clipboardchange event is invoked even when only custom MIME types are set"); |
| |
| promise_test(async (test) => { |
| let listenerCallCount = 0; |
| function clipboardChangeListener() { |
| listenerCallCount++; |
| } |
| |
| // 1. Add listener and verify it's called |
| navigator.clipboard.addEventListener("clipboardchange", clipboardChangeListener); |
| await triggerCopyToClipboard(); |
| assert_equals(listenerCallCount, 1, "Event listener should be called exactly once after adding"); |
| |
| // 2. Remove listener and verify it's not called |
| navigator.clipboard.removeEventListener("clipboardchange", clipboardChangeListener); |
| await triggerCopyToClipboard(); |
| assert_equals(listenerCallCount, 1, "Event listener should not be called after removing"); |
| |
| // 3. Re-add listener and verify it's called again |
| navigator.clipboard.addEventListener("clipboardchange", clipboardChangeListener); |
| await triggerCopyToClipboard(); |
| assert_equals(listenerCallCount, 2, "Event listener should be called exactly once after re-adding"); |
| }, "clipboardchange event listener behavior when adding, removing, and re-adding"); |
| |
| promise_test(async (test) => { |
| // https://w3c.github.io/clipboard-apis/#mandatory-data-types-x |
| const standardTypes = [ |
| "text/plain", |
| "text/html", |
| "image/png", |
| ]; |
| const unsupportedTypes = [ |
| "web application/custom", |
| "web web/proprietary", |
| "web x-custom/type", |
| "txt/json", |
| "text/rtf", |
| "image/svg+xml", |
| "text/uri-list", |
| ]; |
| const allTypesToSet = [...standardTypes, ...unsupportedTypes]; |
| |
| let clipboardChangeEventCount = 0; |
| let capturedEventTypes = null; |
| |
| navigator.clipboard.addEventListener("clipboardchange", (ev) => { |
| clipboardChangeEventCount++; |
| capturedEventTypes = ev.types; |
| }); |
| |
| await triggerCopyToClipboard(allTypesToSet); |
| |
| assert_true(clipboardChangeEventCount == 1, "clipboardchange event should be invoked once"); |
| |
| // Check that types is a frozen array |
| assert_true(Array.isArray(capturedEventTypes), "types should be an array"); |
| assert_true(Object.isFrozen(capturedEventTypes), "types should be frozen"); |
| |
| // Verify all standard types are included |
| for (const type of standardTypes) { |
| assert_true(capturedEventTypes.includes(type), `types should contain standard MIME type '${type}'`); |
| } |
| |
| // Verify custom types are filtered out |
| for (const type of unsupportedTypes) { |
| assert_false(capturedEventTypes.includes(type), `types should not contain custom MIME type '${type}'`); |
| } |
| |
| // Verify we have exactly the standard types and nothing else |
| assert_equals(capturedEventTypes.length, standardTypes.length, |
| "clipboardchange event types should contain exactly the standard MIME types"); |
| }, "clipboardchange event exposes all standard MIME types and filters non-standard ones"); |
| |
| promise_test(async (test) => { |
| // Focus the document and acquire permission to write to the clipboard |
| await test_driver.click(document.body); |
| await tryGrantWritePermission(); |
| |
| const iframe = document.getElementById('iframe'); |
| |
| let frameEventCount = 0; |
| let capturedEventTypes = null; |
| let focusEventFired = false; |
| iframe.contentWindow.addEventListener("focus", () => { |
| focusEventFired = true; |
| }); |
| |
| // Add listener to iframe |
| iframe.contentWindow.navigator.clipboard.addEventListener("clipboardchange", () => { |
| assert_true(focusEventFired, "focus event should fire before clipboardchange event"); |
| frameEventCount++; |
| capturedEventTypes = event.types; |
| }); |
| |
| // Ensure iFrame doesn't have the focus |
| assert_false(iframe.contentWindow.document.hasFocus(), "iFrame should not have focus"); |
| assert_false(focusEventFired, "focus event should not have fired yet"); |
| |
| // Trigger multiple clipboard changes |
| await navigator.clipboard.writeText("Test text"); |
| |
| // Write HTML to clipboard to ensure the event captured only html and not txt |
| await navigator.clipboard.write([ |
| new ClipboardItem({ |
| "text/html": new Blob(["<p>Test HTML</p>"], {type: "text/html"}) |
| }) |
| ]); |
| await waitForRender(); |
| |
| assert_equals(frameEventCount, 0, "iframe should not recieve any clipboardchange event yet"); |
| |
| iframe.focus(); |
| assert_true(iframe.contentWindow.document.hasFocus(), "iFrame should have focus"); |
| assert_equals(frameEventCount, 1, "iframe should receive event only 1 event after focus"); |
| assert_equals(capturedEventTypes.length, 1, "clipboardchange event should only have one type"); |
| assert_true(capturedEventTypes.includes("text/html"), "clipboardchange event should only have text/html type"); |
| }, "clipboardchange event should only fire in the focused context"); |
| |
| </script> |
| </body> |