/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ function getLoadContext() { var Ci = SpecialPowers.Ci; return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); } var clipboard = SpecialPowers.Services.clipboard; var documentViewer = SpecialPowers.wrap( window ).docShell.docViewer.QueryInterface(SpecialPowers.Ci.nsIDocumentViewerEdit); function getClipboardData(mime) { var transferable = SpecialPowers.Cc[ "@mozilla.org/widget/transferable;1" ].createInstance(SpecialPowers.Ci.nsITransferable); transferable.init(getLoadContext()); transferable.addDataFlavor(mime); clipboard.getData( transferable, 1, SpecialPowers.wrap(window).browsingContext.currentWindowContext ); var data = SpecialPowers.createBlankObject(); transferable.getTransferData(mime, data); return data; } function testClipboardValue(suppressHTMLCheck, mime, expected) { if (suppressHTMLCheck && mime == "text/html") { return null; } var data = SpecialPowers.wrap(getClipboardData(mime)); is( data.value == null ? data.value : data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data, expected, mime + " value in the clipboard" ); return data.value; } function testSelectionToString(expected) { const flags = SpecialPowers.Ci.nsIDocumentEncoder.SkipInvisibleContent | SpecialPowers.Ci.nsIDocumentEncoder.AllowCrossShadowBoundary; is( SpecialPowers.wrap(window) .getSelection() .toStringWithFormat("text/plain", flags, 0) .replace(/\r\n/g, "\n"), expected, "Selection.toString" ); } function testHtmlClipboardValue(suppressHTMLCheck, mime, expected) { // For Windows, navigator.platform returns "Win32". var expectedValue = expected; if (navigator.platform.includes("Win")) { // Windows has extra content. var expectedValue = kTextHtmlPrefixClipboardDataWindows + expected.replace(/\n/g, "\n") + kTextHtmlSuffixClipboardDataWindows; } testClipboardValue(suppressHTMLCheck, mime, expectedValue); } function testPasteText(textarea, expected) { textarea.value = ""; textarea.focus(); textarea.editor.paste(1); is(textarea.value, expected, "value of the textarea after the paste"); } async function copySelectionToClipboard() { await SimpleTest.promiseClipboardChange( () => true, () => { documentViewer.copySelection(); } ); ok(clipboard.hasDataMatchingFlavors(["text/plain"], 1), "check text/plain"); ok(clipboard.hasDataMatchingFlavors(["text/html"], 1), "check text/html"); } async function testCopyPasteShadowDOM() { var textarea = SpecialPowers.wrap(document.getElementById("input")); function clear() { textarea.blur(); var sel = window.getSelection(); sel.removeAllRanges(); } async function copySelectionToClipboardShadow( anchorNode, anchorOffset, focusNode, focusOffset ) { clear(); var sel = window.getSelection(); sel.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); await copySelectionToClipboard(); } info( "Test 1: Both start and end are in light DOM, the range has contents in Shadow DOM." ); await copySelectionToClipboardShadow( document.getElementById("title"), 0, document.getElementById("host1"), 1 ); testSelectionToString("This is a draggable bit of text.\nShadow Content1 "); testClipboardValue( false, "text/plain", "This is a draggable bit of text.\nShadow Content1 " ); testHtmlClipboardValue( false, "text/html", '
This is a draggable bit of text.
\n
\n Shadow Content1\n
' ); testPasteText(textarea, "This is a draggable bit of text.\nShadow Content1 "); info("Test 2: Start is in Shadow DOM and end is in light DOM."); await copySelectionToClipboardShadow( document.getElementById("host1").shadowRoot.getElementById("shadow-content") .firstChild, 3, document.getElementById("light-content").firstChild, 5 ); testSelectionToString("dow Content1\nLight"); testClipboardValue(false, "text/plain", "dow Content1\nLight"); testHtmlClipboardValue( false, "text/html", '
dow Content1\n
\n\n Light' ); info("Test 3: Start is in light DOM and end is in shadow DOM."); await copySelectionToClipboardShadow( document.getElementById("light-content").firstChild, 3, document.getElementById("host2").shadowRoot.getElementById("shadow-content") .firstChild, 5 ); testSelectionToString("ht Content\nShado"); testClipboardValue(false, "text/plain", "ht Content\nShado"); testHtmlClipboardValue( false, "text/html", 'ht Content\n\n
\n Shado
' ); info("Test 4: start is in light DOM and end is a nested shadow DOM.\n"); await copySelectionToClipboardShadow( document.getElementById("light-content").firstChild, 3, document .getElementById("host2") .shadowRoot.getElementById("nested-host") .shadowRoot.getElementById("nested-shadow-content").firstChild, 5 ); testSelectionToString("ht Content\nShadow Content2\nNeste"); testClipboardValue(false, "text/plain", "ht Content\nShadow Content2\nNeste"); testHtmlClipboardValue( false, "text/html", 'ht Content\n\n
\n Shadow Content2\n
\n Neste
' ); info("Test 5: Both start and end are in shadow DOM but in different trees."); await copySelectionToClipboardShadow( document.getElementById("host1").shadowRoot.getElementById("shadow-content") .firstChild, 3, document .getElementById("host2") .shadowRoot.getElementById("nested-host") .shadowRoot.getElementById("nested-shadow-content").firstChild, 5 ); testSelectionToString("dow Content1\nLight Content\nShadow Content2\nNeste"); testClipboardValue( false, "text/plain", "dow Content1\nLight Content\nShadow Content2\nNeste" ); testHtmlClipboardValue( false, "text/html", '
dow Content1\n
\n\n Light Content\n\n
\n Shadow Content2\n
\n Neste
' ); info( "Test 6: Start is in a shadow tree and end is in a nested shadow tree within the same shadow tree." ); await copySelectionToClipboardShadow( document.getElementById("host2").shadowRoot.getElementById("shadow-content") .firstChild, 3, document .getElementById("host2") .shadowRoot.getElementById("nested-host") .shadowRoot.getElementById("nested-shadow-content").firstChild, 5 ); testSelectionToString("dow Content2\nNeste"); testClipboardValue(false, "text/plain", "dow Content2\nNeste"); testHtmlClipboardValue( false, "text/html", 'dow Content2\n
\n Neste
' ); info( "Test 7: End is at a slotted content where the slot element is before the regular shadow dom contents." ); await copySelectionToClipboardShadow( document.getElementById("light-content2").firstChild, 3, document.getElementById("slotted1").firstChild, 8 ); testSelectionToString("ht Content\nShadow Content2 slotted1"); testClipboardValue( false, "text/plain", "ht Content\nShadow Content2 slotted1" ); testHtmlClipboardValue( false, "text/html", 'ht Content\n
\n \n Shadow Content2\n \n slotted1
' ); info( "Test 8: End is at a slotted content where the slot element is after the regular shadow dom contents" ); await copySelectionToClipboardShadow( document.getElementById("light-content2").firstChild, 3, document.getElementById("slotted2").firstChild, 8 ); testSelectionToString("ht Content\nShadow Content2 slotted1slotted2"); testClipboardValue( false, "text/plain", "ht Content\nShadow Content2 slotted1slotted2" ); testHtmlClipboardValue( false, "text/html", 'ht Content\n
\n \n Shadow Content2\n \n slotted1slotted2
' ); info( "Test 9: things still work as expected with a more complex shadow tree." ); await copySelectionToClipboardShadow( document.getElementById("slotted3").firstChild, 3, document.getElementById("slotted4").firstChild, 8 ); testSelectionToString( " Shadow Content2\nShadowNested Nested Slotted\ntted1slotted2" ); testClipboardValue( false, "text/plain", " Shadow Content2\nShadowNested Nested Slotted\ntted1slotted2" ); testHtmlClipboardValue( false, "text/html", '\n \n Shadow Content2\n
\n \n ShadowNested\n \n \n Nested Slotted\n
\n \n tted1slotted2' ); // FIXME: This behaviour is not expected and we'll fix it in bug 1901053 info("Test 10: Slot element is always serialized even if it's not visible"); await copySelectionToClipboardShadow( document.getElementById("light-content3").firstChild, 0, document.getElementById("host5").shadowRoot.querySelector("span") .firstChild, 5 ); testSelectionToString("Light Content\ndefault value Shado Slotted "); testClipboardValue( false, "text/plain", "Light Content\ndefault value Shado Slotted " ); testHtmlClipboardValue( false, "text/html", 'Light Content\n \n
\n default value\n Shado\n \n Slotted\n
' ); }