async-navigator-clipboard-change-event.tentative.https.html (9218B)
1 <!DOCTYPE html> 2 <meta charset="utf-8" /> 3 <title> 4 'clipboardchange' event should be fired upon setting clipboard using JS 5 </title> 6 <link rel="help" href="https://www.w3.org/TR/clipboard-apis/#clipboard-event-clipboardchange" /> 7 8 <body> 9 Body needed for test_driver.click() 10 <p><button id="button">Put payload in the clipboard</button></p> 11 <div id="output"></div> 12 <iframe id="iframe" srcdoc="<p>Some text</p>"></iframe> 13 <link rel="help" href="https://issues.chromium.org/issues/41442253" /> 14 15 <script src="/resources/testharness.js"></script> 16 <script src="/resources/testharnessreport.js"></script> 17 <script src="/resources/testdriver.js"></script> 18 <script src="/resources/testdriver-vendor.js"></script> 19 <script src="resources/user-activation.js"></script> 20 21 <script> 22 function waitForRender() { 23 return new Promise(resolve => { 24 requestAnimationFrame(() => requestAnimationFrame(resolve)); 25 }); 26 } 27 28 let typesToSet_ = ["text/html", "web txt/csv"]; 29 button.onclick = () => document.execCommand("copy"); 30 document.oncopy = (ev) => { 31 ev.preventDefault(); 32 for (let i = 0; i < typesToSet_.length; i++) { 33 const type = typesToSet_[i]; 34 const data = new Blob([`Test data for ${type}`], {type: type}); 35 ev.clipboardData.setData(type, data); 36 } 37 }; 38 39 function triggerCopyToClipboard(typesToSet) { 40 if (typesToSet) { 41 typesToSet_ = typesToSet; 42 } 43 return test_driver.click(button); 44 } 45 46 promise_test(async (test) => { 47 let clipboardChangeEventCount = 0; 48 let eventType = ""; 49 let capturedEventTypes = null; 50 navigator.clipboard.addEventListener("clipboardchange", (ev) => { 51 clipboardChangeEventCount++; 52 eventType = ev.type; 53 capturedEventTypes = ev.types; 54 }); 55 await triggerCopyToClipboard(); 56 assert_equals(clipboardChangeEventCount, 1, "clipboardchange event should be called exactly once"); 57 assert_equals(eventType, "clipboardchange", "Event type should be 'clipboardchange'"); 58 assert_true(capturedEventTypes.includes("text/html"), "types should contain 'text/html'"); 59 assert_false(capturedEventTypes.includes("web txt/csv"), "types should not contain custom MIME type"); 60 }, "clipboardchange event is invoked"); 61 62 promise_test(async (test) => { 63 await tryGrantWritePermission(); 64 let clipboardChangeEventCount = 0; 65 let capturedEventTypes = null; 66 navigator.clipboard.addEventListener("clipboardchange", (ev) => { 67 clipboardChangeEventCount++; 68 capturedEventTypes = ev.types; 69 }); 70 await navigator.clipboard.writeText("Test text"); 71 await waitForRender(); 72 assert_equals(clipboardChangeEventCount, 1, "clipboardchange event should be called exactly once"); 73 assert_true(capturedEventTypes.includes("text/plain"), "types should contain 'text/plain'"); 74 }, "clipboardchange event is invoked with async clipboard API"); 75 76 promise_test(async (test) => { 77 let onClipboardChangeAttributeCount = 0; 78 let capturedEventTypes = null; 79 navigator.clipboard.onclipboardchange = (ev) => { 80 onClipboardChangeAttributeCount++; 81 capturedEventTypes = ev.types; 82 }; 83 await triggerCopyToClipboard(); 84 assert_equals(onClipboardChangeAttributeCount, 1, "onclipboardchange attribute should be called exactly once"); 85 assert_true(capturedEventTypes.includes("text/html"), "types should contain 'text/html'"); 86 assert_false(capturedEventTypes.includes("web txt/csv"), "types should not contain custom MIME type"); 87 }, "clipboardchange event is invoked using onclipboardchange attribute"); 88 89 promise_test(async (test) => { 90 let onClipboardChangeAttributeCount = 0; 91 let capturedEventTypes = null; 92 navigator.clipboard.onclipboardchange = (ev) => { 93 onClipboardChangeAttributeCount++; 94 capturedEventTypes = ev.types; 95 }; 96 await triggerCopyToClipboard(["web txt/csv"]); 97 assert_equals(onClipboardChangeAttributeCount, 1, "onclipboardchange attribute should be called exactly once"); 98 assert_equals(capturedEventTypes.length, 0, "clipboardchange event should have no types"); 99 }, "clipboardchange event is invoked even when only custom MIME types are set"); 100 101 promise_test(async (test) => { 102 let listenerCallCount = 0; 103 function clipboardChangeListener() { 104 listenerCallCount++; 105 } 106 107 // 1. Add listener and verify it's called 108 navigator.clipboard.addEventListener("clipboardchange", clipboardChangeListener); 109 await triggerCopyToClipboard(); 110 assert_equals(listenerCallCount, 1, "Event listener should be called exactly once after adding"); 111 112 // 2. Remove listener and verify it's not called 113 navigator.clipboard.removeEventListener("clipboardchange", clipboardChangeListener); 114 await triggerCopyToClipboard(); 115 assert_equals(listenerCallCount, 1, "Event listener should not be called after removing"); 116 117 // 3. Re-add listener and verify it's called again 118 navigator.clipboard.addEventListener("clipboardchange", clipboardChangeListener); 119 await triggerCopyToClipboard(); 120 assert_equals(listenerCallCount, 2, "Event listener should be called exactly once after re-adding"); 121 }, "clipboardchange event listener behavior when adding, removing, and re-adding"); 122 123 promise_test(async (test) => { 124 // https://w3c.github.io/clipboard-apis/#mandatory-data-types-x 125 const standardTypes = [ 126 "text/plain", 127 "text/html", 128 "image/png", 129 ]; 130 const unsupportedTypes = [ 131 "web application/custom", 132 "web web/proprietary", 133 "web x-custom/type", 134 "txt/json", 135 "text/rtf", 136 "image/svg+xml", 137 "text/uri-list", 138 ]; 139 const allTypesToSet = [...standardTypes, ...unsupportedTypes]; 140 141 let clipboardChangeEventCount = 0; 142 let capturedEventTypes = null; 143 144 navigator.clipboard.addEventListener("clipboardchange", (ev) => { 145 clipboardChangeEventCount++; 146 capturedEventTypes = ev.types; 147 }); 148 149 await triggerCopyToClipboard(allTypesToSet); 150 151 assert_true(clipboardChangeEventCount == 1, "clipboardchange event should be invoked once"); 152 153 // Check that types is a frozen array 154 assert_true(Array.isArray(capturedEventTypes), "types should be an array"); 155 assert_true(Object.isFrozen(capturedEventTypes), "types should be frozen"); 156 157 // Verify all standard types are included 158 for (const type of standardTypes) { 159 assert_true(capturedEventTypes.includes(type), `types should contain standard MIME type '${type}'`); 160 } 161 162 // Verify custom types are filtered out 163 for (const type of unsupportedTypes) { 164 assert_false(capturedEventTypes.includes(type), `types should not contain custom MIME type '${type}'`); 165 } 166 167 // Verify we have exactly the standard types and nothing else 168 assert_equals(capturedEventTypes.length, standardTypes.length, 169 "clipboardchange event types should contain exactly the standard MIME types"); 170 }, "clipboardchange event exposes all standard MIME types and filters non-standard ones"); 171 172 promise_test(async (test) => { 173 // Focus the document and acquire permission to write to the clipboard 174 await test_driver.click(document.body); 175 await tryGrantWritePermission(); 176 177 const iframe = document.getElementById('iframe'); 178 179 let frameEventCount = 0; 180 let capturedEventTypes = null; 181 let focusEventFired = false; 182 iframe.contentWindow.addEventListener("focus", () => { 183 focusEventFired = true; 184 }); 185 186 // Add listener to iframe 187 iframe.contentWindow.navigator.clipboard.addEventListener("clipboardchange", () => { 188 assert_true(focusEventFired, "focus event should fire before clipboardchange event"); 189 frameEventCount++; 190 capturedEventTypes = event.types; 191 }); 192 193 // Ensure iFrame doesn't have the focus 194 assert_false(iframe.contentWindow.document.hasFocus(), "iFrame should not have focus"); 195 assert_false(focusEventFired, "focus event should not have fired yet"); 196 197 // Trigger multiple clipboard changes 198 await navigator.clipboard.writeText("Test text"); 199 200 // Write HTML to clipboard to ensure the event captured only html and not txt 201 await navigator.clipboard.write([ 202 new ClipboardItem({ 203 "text/html": new Blob(["<p>Test HTML</p>"], {type: "text/html"}) 204 }) 205 ]); 206 await waitForRender(); 207 208 assert_equals(frameEventCount, 0, "iframe should not recieve any clipboardchange event yet"); 209 210 iframe.focus(); 211 assert_true(iframe.contentWindow.document.hasFocus(), "iFrame should have focus"); 212 assert_equals(frameEventCount, 1, "iframe should receive event only 1 event after focus"); 213 assert_equals(capturedEventTypes.length, 1, "clipboardchange event should only have one type"); 214 assert_true(capturedEventTypes.includes("text/html"), "clipboardchange event should only have text/html type"); 215 }, "clipboardchange event should only fire in the focused context"); 216 217 </script> 218 </body>