copypaste_shadow_dom.js (10616B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 function getLoadContext() { 5 var Ci = SpecialPowers.Ci; 6 return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); 7 } 8 9 var clipboard = SpecialPowers.Services.clipboard; 10 var documentViewer = SpecialPowers.wrap( 11 window 12 ).docShell.docViewer.QueryInterface(SpecialPowers.Ci.nsIDocumentViewerEdit); 13 14 function getClipboardData(mime) { 15 var transferable = SpecialPowers.Cc[ 16 "@mozilla.org/widget/transferable;1" 17 ].createInstance(SpecialPowers.Ci.nsITransferable); 18 transferable.init(getLoadContext()); 19 transferable.addDataFlavor(mime); 20 clipboard.getData( 21 transferable, 22 1, 23 SpecialPowers.wrap(window).browsingContext.currentWindowContext 24 ); 25 var data = SpecialPowers.createBlankObject(); 26 transferable.getTransferData(mime, data); 27 return data; 28 } 29 30 function testClipboardValue(suppressHTMLCheck, mime, expected) { 31 if (suppressHTMLCheck && mime == "text/html") { 32 return null; 33 } 34 var data = SpecialPowers.wrap(getClipboardData(mime)); 35 is( 36 data.value == null 37 ? data.value 38 : data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data, 39 expected, 40 mime + " value in the clipboard" 41 ); 42 return data.value; 43 } 44 45 function testSelectionToString(expected) { 46 const flags = 47 SpecialPowers.Ci.nsIDocumentEncoder.SkipInvisibleContent | 48 SpecialPowers.Ci.nsIDocumentEncoder.AllowCrossShadowBoundary; 49 is( 50 SpecialPowers.wrap(window) 51 .getSelection() 52 .toStringWithFormat("text/plain", flags, 0) 53 .replace(/\r\n/g, "\n"), 54 expected, 55 "Selection.toString" 56 ); 57 } 58 59 function testHtmlClipboardValue(suppressHTMLCheck, mime, expected) { 60 // For Windows, navigator.platform returns "Win32". 61 var expectedValue = expected; 62 if (navigator.platform.includes("Win")) { 63 // Windows has extra content. 64 expectedValue = 65 kTextHtmlPrefixClipboardDataWindows + 66 expected.replace(/\n/g, "\n") + 67 kTextHtmlSuffixClipboardDataWindows; 68 } 69 testClipboardValue(suppressHTMLCheck, mime, expectedValue); 70 } 71 72 function testPasteText(textarea, expected) { 73 textarea.value = ""; 74 textarea.focus(); 75 textarea.editor.paste(1); 76 is(textarea.value, expected, "value of the textarea after the paste"); 77 } 78 79 async function copySelectionToClipboard() { 80 await SimpleTest.promiseClipboardChange( 81 () => true, 82 () => { 83 documentViewer.copySelection(); 84 } 85 ); 86 ok(clipboard.hasDataMatchingFlavors(["text/plain"], 1), "check text/plain"); 87 ok(clipboard.hasDataMatchingFlavors(["text/html"], 1), "check text/html"); 88 } 89 90 async function testCopyPasteShadowDOM() { 91 var textarea = SpecialPowers.wrap(document.getElementById("input")); 92 93 function clear() { 94 textarea.blur(); 95 var sel = window.getSelection(); 96 sel.removeAllRanges(); 97 } 98 99 async function copySelectionToClipboardShadow( 100 anchorNode, 101 anchorOffset, 102 focusNode, 103 focusOffset 104 ) { 105 clear(); 106 var sel = window.getSelection(); 107 sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); 108 await copySelectionToClipboard(); 109 } 110 111 info( 112 "Test 1: Both start and end are in light DOM, the range has contents in Shadow DOM." 113 ); 114 await copySelectionToClipboardShadow( 115 document.getElementById("title"), 116 0, 117 document.getElementById("host1"), 118 1 119 ); 120 testSelectionToString("This is a draggable bit of text.\nShadow Content1 "); 121 testClipboardValue( 122 false, 123 "text/plain", 124 "This is a draggable bit of text.\nShadow Content1 " 125 ); 126 testHtmlClipboardValue( 127 false, 128 "text/html", 129 '<div id="title" title="title to have a long HTML line">This is a <em>draggable</em> bit of text.</div>\n <div id="host1">\n <span id="shadow-content">Shadow Content1</span>\n </div>' 130 ); 131 testPasteText(textarea, "This is a draggable bit of text.\nShadow Content1 "); 132 133 info("Test 2: Start is in Shadow DOM and end is in light DOM."); 134 await copySelectionToClipboardShadow( 135 document.getElementById("host1").shadowRoot.getElementById("shadow-content") 136 .firstChild, 137 3, 138 document.getElementById("light-content").firstChild, 139 5 140 ); 141 testSelectionToString("dow Content1\nLight"); 142 testClipboardValue(false, "text/plain", "dow Content1\nLight"); 143 testHtmlClipboardValue( 144 false, 145 "text/html", 146 '<div id="host1"><span id="shadow-content">dow Content1</span>\n </div>\n\n <span id="light-content">Light</span>' 147 ); 148 149 info("Test 3: Start is in light DOM and end is in shadow DOM."); 150 await copySelectionToClipboardShadow( 151 document.getElementById("light-content").firstChild, 152 3, 153 document.getElementById("host2").shadowRoot.getElementById("shadow-content") 154 .firstChild, 155 5 156 ); 157 testSelectionToString("ht Content\nShado"); 158 testClipboardValue(false, "text/plain", "ht Content\nShado"); 159 testHtmlClipboardValue( 160 false, 161 "text/html", 162 '<span id="light-content">ht Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shado</span></div>' 163 ); 164 165 info("Test 4: start is in light DOM and end is a nested shadow DOM.\n"); 166 await copySelectionToClipboardShadow( 167 document.getElementById("light-content").firstChild, 168 3, 169 document 170 .getElementById("host2") 171 .shadowRoot.getElementById("nested-host") 172 .shadowRoot.getElementById("nested-shadow-content").firstChild, 173 5 174 ); 175 testSelectionToString("ht Content\nShadow Content2\nNeste"); 176 testClipboardValue(false, "text/plain", "ht Content\nShadow Content2\nNeste"); 177 testHtmlClipboardValue( 178 false, 179 "text/html", 180 '<span id="light-content">ht Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shadow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div></div>' 181 ); 182 183 info("Test 5: Both start and end are in shadow DOM but in different trees."); 184 await copySelectionToClipboardShadow( 185 document.getElementById("host1").shadowRoot.getElementById("shadow-content") 186 .firstChild, 187 3, 188 document 189 .getElementById("host2") 190 .shadowRoot.getElementById("nested-host") 191 .shadowRoot.getElementById("nested-shadow-content").firstChild, 192 5 193 ); 194 testSelectionToString("dow Content1\nLight Content\nShadow Content2\nNeste"); 195 testClipboardValue( 196 false, 197 "text/plain", 198 "dow Content1\nLight Content\nShadow Content2\nNeste" 199 ); 200 testHtmlClipboardValue( 201 false, 202 "text/html", 203 '<div id="host1"><span id="shadow-content">dow Content1</span>\n </div>\n\n <span id="light-content">Light Content</span>\n\n <div id="host2">\n <span id="shadow-content">Shadow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div></div>' 204 ); 205 206 info( 207 "Test 6: Start is in a shadow tree and end is in a nested shadow tree within the same shadow tree." 208 ); 209 await copySelectionToClipboardShadow( 210 document.getElementById("host2").shadowRoot.getElementById("shadow-content") 211 .firstChild, 212 3, 213 document 214 .getElementById("host2") 215 .shadowRoot.getElementById("nested-host") 216 .shadowRoot.getElementById("nested-shadow-content").firstChild, 217 5 218 ); 219 testSelectionToString("dow Content2\nNeste"); 220 testClipboardValue(false, "text/plain", "dow Content2\nNeste"); 221 testHtmlClipboardValue( 222 false, 223 "text/html", 224 '<span id="shadow-content">dow Content2</span>\n <div id="nested-host">\n <span id="nested-shadow-content">Neste</span></div>' 225 ); 226 227 info( 228 "Test 7: End is at a slotted content where the slot element is before the regular shadow dom contents." 229 ); 230 await copySelectionToClipboardShadow( 231 document.getElementById("light-content2").firstChild, 232 3, 233 document.getElementById("slotted1").firstChild, 234 8 235 ); 236 testSelectionToString("ht Content\nslotted1"); 237 testClipboardValue(false, "text/plain", "ht Content\nslotted1"); 238 testHtmlClipboardValue( 239 false, 240 "text/html", 241 '<span id=\"light-content2\">ht Content</span>\n <div id=\"host3\">\n <slot name=\"slot1\"><span slot=\"slot1\" id=\"slotted1\">slotted1</span></slot></div>' 242 ); 243 244 info( 245 "Test 8: End is at a slotted content where the slot element is after the regular shadow dom contents" 246 ); 247 await copySelectionToClipboardShadow( 248 document.getElementById("light-content2").firstChild, 249 3, 250 document.getElementById("slotted2").firstChild, 251 8 252 ); 253 testSelectionToString("ht Content\nslotted1 Shadow Content2 slotted2"); 254 testClipboardValue( 255 false, 256 "text/plain", 257 "ht Content\nslotted1 Shadow Content2 slotted2" 258 ); 259 testHtmlClipboardValue( 260 false, 261 "text/html", 262 '<span id=\"light-content2\">ht Content</span>\n <div id=\"host3\">\n <slot name=\"slot1\"><span slot=\"slot1\" id=\"slotted1\">slotted1</span></slot>\n <span id=\"shadow-content\">Shadow Content2</span>\n <slot name=\"slot2\"><span slot=\"slot2\" id=\"slotted2\">slotted2</span></slot></div>' 263 ); 264 265 info( 266 "Test 9: things still work as expected with a more complex shadow tree." 267 ); 268 await copySelectionToClipboardShadow( 269 document.getElementById("slotted3").firstChild, 270 3, 271 document.getElementById("slotted4").firstChild, 272 8 273 ); 274 testSelectionToString( 275 "tted1 Shadow Content2\nNested Slotted ShadowNested\nslotted2" 276 ); 277 testClipboardValue( 278 false, 279 "text/plain", 280 "tted1 Shadow Content2\nNested Slotted ShadowNested\nslotted2" 281 ); 282 testHtmlClipboardValue( 283 false, 284 "text/html", 285 '<slot name=\"slot1\"><span slot=\"slot1\" id=\"slotted3\">tted1</span></slot>\n <span id=\"shadow-content\">Shadow Content2</span>\n <div id=\"nestedHost\">\n <slot>\n \n <span>Nested Slotted</span>\n </slot>\n <span>ShadowNested</span>\n </div>\n <slot name=\"slot2\"><span slot=\"slot2\" id=\"slotted4\">slotted2</span></slot>' 286 ); 287 288 info("Test 10: Slot element is always serialized even if it's not visible"); 289 await copySelectionToClipboardShadow( 290 document.getElementById("light-content3").firstChild, 291 0, 292 document.getElementById("host5").shadowRoot.querySelector("span") 293 .firstChild, 294 5 295 ); 296 testSelectionToString("Light Content\nSlotted Shado"); 297 testClipboardValue(false, "text/plain", "Light Content\nSlotted Shado"); 298 testHtmlClipboardValue( 299 false, 300 "text/html", 301 '<span id=\"light-content3\">Light Content</span>\n \n <div id=\"host5\">\n <slot>\n \n <span>Slotted</span>\n </slot>\n <span>Shado</span></div>' 302 ); 303 }