<!doctype html> <html> <head> <meta chareset="utf-8"> <meta name="timeout" content="long"> <meta name="variant" content="?designMode=off&method=backspace"> <meta name="variant" content="?designMode=off&method=forwarddelete"> <meta name="variant" content="?designMode=on&method=backspace"> <meta name="variant" content="?designMode=on&method=forwarddelete"> <title>Join paragraphs outside the body</title> <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/testdriver-actions.js"></script> <script src="../include/editor-test-utils.js"></script> </head> <body> <iframe srcdoc=""></iframe> <script> "use strict"; const searchParams = new URLSearchParams(document.location.search); const testingBackspace = searchParams.get("method") == "backspace"; const commandName = testingBackspace ? "delete" : "forwarddelete"; const testingDesignMode = searchParams.get("designMode") == "on"; const iframe = document.querySelector("iframe"); const minimumSrcDoc = "<html>" + "<head>" + "<title>iframe</title>" + "<script src='/resources/testdriver.js'></" + "script>" + "<script src='/resources/testdriver-vendor.js'></" + "script>" + "<script src='/resources/testdriver-actions.js'></" + "script>" + "</head>" + "<body><br></body>" + "</html>"; async function initializeAndWaitForLoad(iframeElement, srcDocValue) { const waitForLoad = new Promise( resolve => iframeElement.addEventListener("load", resolve, {once: true}) ); iframeElement.srcdoc = srcDocValue; await waitForLoad; if (testingDesignMode) { iframeElement.contentDocument.designMode = "on"; } else { iframeElement.contentDocument.documentElement.setAttribute("contenteditable", ""); } iframeElement.contentWindow.focus(); iframeElement.contentDocument.execCommand("defaultParagraphSeparator", false, "div"); } function removeResourceScriptElements(node) { node.querySelectorAll("script").forEach( element => { if (element.getAttribute("src")?.startsWith("/resources")) { element.remove() } } ); } // DO NOT USE multi-line comment in this file, then, you can comment out // unnecessary tests when you need to attach the browser with a debugger. // For backward compatibility, normal block elements outside <body> should be // joined by deletion. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div1 = childDoc.createElement("div"); div1.innerHTML = "abc"; const div2 = childDoc.createElement("div"); div2.innerHTML = "def"; childDoc.documentElement.appendChild(div1); childDoc.documentElement.appendChild(div2); // Now: </head><body><br></body><div>abc</div><div>def</div> childDoc.getSelection().collapse( testingBackspace ? div2.firstChild : div1.firstChild, testingBackspace ? 0 : div1.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body><br></body><div>abcdef</div>', '<head><title>iframe</title></head><body><br></body><div>abcdef<br></div>', ], "The <div> elements should be merged" ); assert_equals( div1.isConnected ^ div2.isConnected, 1, "One <div> element should be removed, and the other should stay" ); }, `${commandName} in <div> elements after <body> should join them`); // Deleting around end of the <body> should merge the element after the // <body> into the <body>. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); childDoc.body.innerHTML = "abc"; const div = childDoc.createElement("div"); div.innerHTML = "def"; childDoc.documentElement.appendChild(div); // Now: </head><body>abc</body><div>def</div> childDoc.getSelection().collapse( testingBackspace ? div.firstChild : childDoc.body.firstChild, testingBackspace ? 0 : childDoc.body.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body>abcdef</body>', '<head><title>iframe</title></head><body>abcdef<br></body>', ], "The text should be merged" ); assert_false( div.isConnected, "The <div> following <body> should be removed" ); }, `${commandName} should merge <div> after <body> into the <body>`); promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div1 = childDoc.createElement("div"); div1.innerHTML = "abc"; const div2 = childDoc.createElement("div"); div2.innerHTML = "def"; childDoc.body.innerHTML = ""; childDoc.body.appendChild(div1); childDoc.documentElement.appendChild(div2); // Now: </head><body><div>abc</div></body><div>def</div> childDoc.getSelection().collapse( testingBackspace ? div2.firstChild : div1.firstChild, testingBackspace ? 0 : div1.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body><div>abcdef</div></body>', '<head><title>iframe</title></head><body><div>abcdef<br></div></body>', ], "The <div> elements should be merged" ); assert_true( !div2.isConnected || (div2.isConnected && div2.parentNode == childDoc.body), "The <div> following <body> should be removed or moved into the <body>" ); }, `${commandName} should merge <div> after <body> into the <div> in the <body>`); promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div = childDoc.createElement("div"); div.innerHTML = "abc"; childDoc.documentElement.appendChild(div); // Now: </head><body><br></body><div>abc</div> childDoc.getSelection().collapse( testingBackspace ? div.firstChild : childDoc.body, 0 ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body>abc</body>', '<head><title>iframe</title></head><body>abc<br></body>', ], "The <div> element should be merged into the <body>" ); assert_false( div.isConnected, "The <div> element should be removed" ); }, `${commandName} should merge <div> after <body> into the empty <body>`); // Deleting around start of the <body> should merge the element before the // <body> into the <body>. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div = childDoc.createElement("div"); div.innerHTML = "abc"; childDoc.body.innerHTML = "def"; childDoc.documentElement.insertBefore(div, childDoc.body); // Now: </head><div>abc</div><body>def</body> childDoc.getSelection().collapse( testingBackspace ? childDoc.body.firstChild : div.firstChild, testingBackspace ? 0 : div.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body>abcdef</body>', '<head><title>iframe</title></head><body>abcdef<br></body>', ], "The text should be merged" ); assert_false( div.isConnected, "The <div> following <body> should be removed" ); }, `${commandName} should merge <div> before <body> into the <body>`); promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div1 = childDoc.createElement("div"); div1.innerHTML = "abc"; const div2 = childDoc.createElement("div"); div2.innerHTML = "def"; childDoc.documentElement.insertBefore(div1, childDoc.body); childDoc.body.innerHTML = ""; childDoc.body.appendChild(div2); // Now: </head><div>abc</div><body><div>def</div></body> childDoc.getSelection().collapse( testingBackspace ? div2.firstChild : div1.firstChild, testingBackspace ? 0 : div1.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body><div>abcdef</div></body>', '<head><title>iframe</title></head><body><div>abcdef<br></div></body>', ], "The <div> elements should be merged" ); assert_true( !div2.isConnected || (div2.isConnected && div2.parentNode == childDoc.body), "The <div> following <body> should be removed or moved into the <body>" ); }, `${commandName} should merge <div> before <body> into the <div> in the <body>`); promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div = childDoc.createElement("div"); div.innerHTML = "abc"; childDoc.documentElement.insertBefore(div, childDoc.body); // Now: </head><div>abc</div><body><br></body> childDoc.getSelection().collapse( testingBackspace ? childDoc.body : div.firstChild, testingBackspace ? 0: div.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title></head><body>abc</body>', '<head><title>iframe</title></head><body>abc<br></body>', ], "The <div> element should be merged into the <body>" ); assert_false( div.isConnected, "The <div> element should be removed" ); }, `${commandName} should merge <div> before <body> into the empty <body>`); // Deleting around end of the <head> should not delete the <head> element. if (testingBackspace) { promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div = childDoc.createElement("div"); div.innerHTML = "abc"; childDoc.body.innerHTML = "def"; childDoc.documentElement.insertBefore(div, childDoc.body); // Now: </head><div>abc</div><body>def</body> childDoc.getSelection().collapse(div.firstChild, 0); await utils.sendBackspaceKey(); removeResourceScriptElements(childDoc); assert_equals( childDoc.documentElement.innerHTML, '<head><title>iframe</title></head><div>abc</div><body>def</body>', "The <div> element should be merged into the <body>" ); assert_true( div.isConnected, "The <div> element should not be removed" ); }, `delete from <div> following invisible <head> element shouldn't delete the <head> element`); } // Joining elements around <head> element should not delete the <head> element. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; const utils = new EditorTestUtils(childDoc.documentElement); const div1 = childDoc.createElement("div"); div1.innerHTML = "abc"; const div2 = childDoc.createElement("div"); div2.innerHTML = "def"; childDoc.documentElement.insertBefore(div1, childDoc.head); childDoc.documentElement.insertBefore(div2, childDoc.body); // Now: <div>abc</div><head>...</head><div>def</div><body><br></body> childDoc.getSelection().collapse( testingBackspace ? div2.firstChild : div1.firstChild, testingBackspace ? 0 : div1.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); assert_in_array( childDoc.documentElement.innerHTML, [ '<div>abcdef</div><head><title>iframe</title></head><body><br></body>', '<div>abcdef<br></div><head><title>iframe</title></head><body><br></body>', '<head><title>iframe</title></head><div>abcdef</div><body><br></body>', '<head><title>iframe</title></head><div>abcdef<br></div><body><br></body>', ], "The <div> element should be merged into the left <div> without deleting the <head>" ); assert_true( div1.isConnected ^ div2.isConnected, "One <div> element should be removed, but the other should stay" ); }, `${commandName} from <div> around invisible <head> element should not delete the <head>`); // Same as <body> element boundary, allow joining across <head> elements if // and only if both elements are normal elements. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; childDoc.head.setAttribute("style", "display:block"); const utils = new EditorTestUtils(childDoc.documentElement); const div1 = childDoc.createElement("div"); div1.innerHTML = "abc"; const div2 = childDoc.createElement("div"); div2.innerHTML = "def"; childDoc.head.appendChild(div1); childDoc.documentElement.insertBefore(div2, childDoc.body); // Now: <div>abc</div></head><div>def</div><body><br></body> childDoc.getSelection().collapse( testingBackspace ? div2.firstChild : div1.firstChild, testingBackspace ? 0 : div1.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); childDoc.head.removeAttribute("style"); assert_in_array( childDoc.documentElement.innerHTML, [ '<head><title>iframe</title><div>abcdef</div></head><body><br></body>', '<head><title>iframe</title><div>abcdef<br></div></head><body><br></body>', ], "The <div> element should be merged into the <div> in the <head>" ); assert_false( div2.isConnected, "The <div> element should be removed" ); }, `${commandName} from <div> following visible <head> element should be merged with the <div> in the <head>`); // However, don't allow to join with <script> and <style> elements because // changing them may not be safe. promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; childDoc.head.setAttribute("style", "display:block"); const utils = new EditorTestUtils(childDoc.documentElement); const style = childDoc.createElement("style"); style.setAttribute("style", "display:block;white-space:pre"); style.innerHTML = "abc"; const div = childDoc.createElement("div"); div.innerHTML = "def"; childDoc.head.appendChild(style); childDoc.documentElement.insertBefore(div, childDoc.body); // Now: <style>abc</style></head><div>def</div><body><br></body> childDoc.getSelection().collapse( testingBackspace ? div.firstChild : style.firstChild, testingBackspace ? 0 : style.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); childDoc.head.removeAttribute("style"); style.removeAttribute("style"); assert_equals( childDoc.documentElement.innerHTML, '<head><title>iframe</title><style>abc</style></head><div>def</div><body><br></body>', "The <div> element should not be merged with the <style> in the <head>" ); assert_true( div.isConnected, "The <div> element should not be removed" ); }, `${commandName} from <div> following visible <head> element should be merged with the visible <style> in the <head>`); promise_test(async () => { await initializeAndWaitForLoad(iframe, minimumSrcDoc); const childDoc = iframe.contentDocument; childDoc.head.setAttribute("style", "display:block"); const utils = new EditorTestUtils(childDoc.documentElement); const script = childDoc.createElement("script"); script.setAttribute("style", "display:block;white-space:pre"); script.innerHTML = "// abc"; const div = childDoc.createElement("div"); div.innerHTML = "def"; childDoc.head.appendChild(script); childDoc.documentElement.insertBefore(div, childDoc.body); // Now: <script>// abc</ script></head><div>def</div><body><br></body> childDoc.getSelection().collapse( testingBackspace ? div.firstChild : script.firstChild, testingBackspace ? 0 : script.firstChild.length ); await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); removeResourceScriptElements(childDoc); childDoc.head.removeAttribute("style"); script.removeAttribute("style"); assert_equals( childDoc.documentElement.innerHTML, '<head><title>iframe</title><script>// abc</' + 'script></head><div>def</div><body><br></body>', "The <div> element should not be merged with the <script> in the <head>" ); assert_true( div.isConnected, "The <div> element should not be removed" ); }, `${commandName} from <div> following visible <script> element should be merged with the visible <script> in the <head>`); </script> </body> </html>