// XxxEditor.attach(): the toolbar, the dialogs, source mode, undo, paste and the keys "use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { editorWindow, select, selectText, button, key } = require("./helpers"); // Type into the editor: replace its HTML and send the input event function type(env, html, inputType = "insertText") { env.box.dispatchEvent(new env.window.InputEvent("beforeinput", { inputType, bubbles: true, cancelable: true })); env.box.innerHTML = html; env.box.dispatchEvent(new env.window.InputEvent("input", { inputType, bubbles: true })); } function paste(env, data) { const event = new env.window.Event("paste", { bubbles: true, cancelable: true }); event.clipboardData = { getData: (type) => data[type] || "" }; env.box.dispatchEvent(event); return event; } test("the editor takes the place of the textarea in the form", () => { const env = editorWindow("
Hello
"); const wrapper = env.document.querySelector("#textparent > .xed"); assert.ok(wrapper); assert.equal(env.textarea.parentNode, wrapper); assert.equal(env.textarea.hidden, true); assert.equal(env.textarea.style.display, "none"); assert.equal(env.textarea.form.id, "form"); assert.equal(env.box.contentEditable, "true"); assert.equal(env.box.getAttribute("aria-label"), "Story:"); assert.equal(env.box.innerHTML, "Hello
"); }); test("the toolbar has the TinyMCE buttons, in order", () => { const env = editorWindow(); const labels = Array.from(env.document.querySelectorAll(".xed-btn"), (b) => b.getAttribute("aria-label")); assert.deepEqual(labels, ["Bold", "Italic", "Align left", "Align center", "Align right", "Justify", "Blockquote", "Decrease indent", "Increase indent", "Insert/edit link", "Remove link", "Special character", "Source code"]); assert.equal(button(env.document, "Bold").title, "Bold (Ctrl+B)"); assert.equal(button(env.document, "Insert/edit link").title, "Insert/edit link (Ctrl+K)"); assert.equal(env.document.querySelectorAll(".xed-group").length, 6); for (const b of env.document.querySelectorAll(".xed-btn")) { assert.equal(b.type, "button"); assert.ok(b.querySelector("svg[aria-hidden=true] path")); } }); test("the story in the textarea is cleaned when the editor starts", () => { const env = editorWindow('a
loose text'); assert.equal(env.textarea.value, "a
\nloose text
"); assert.equal(env.editor.getContent(), env.textarea.value); }); test("an empty story starts with an empty paragraph", () => { const env = editorWindow(""); assert.equal(env.box.innerHTML, "Some words
"); assert.equal(env.textarea.value, "Some words
"); assert.equal(inputs, 1); type(env, "Some words
"); assert.equal(inputs, 1, "no event when nothing changed"); }); test("text typed outside a paragraph is put in one", () => { const env = editorWindow(); type(env, "loose textpara
more"); assert.equal(env.textarea.value, "loose text
\npara
\nmore
"); }); test("deleting everything starts again with a plain paragraph", () => { const env = editorWindow("bold
"); type(env, "new story
"); assert.equal(env.editor.getContent(), "new story
"); assert.equal(env.textarea.value, "new story
"); }); test("the form sends the editor's content", () => { const env = editorWindow("a
"); env.box.innerHTML = "changed without an input event
"; env.textarea.form.dispatchEvent(new env.window.Event("submit", { cancelable: true })); assert.equal(env.textarea.value, "changed without an input event
"); }); test("bold and italic use the browser's commands", () => { const env = editorWindow("word
"); selectText(env.window, env.box.querySelector("p")); button(env.document, "Bold").click(); button(env.document, "Italic").click(); assert.deepEqual(env.window.commands.filter((c) => c[0] !== "styleWithCSS" && c[0] !== "defaultParagraphSeparator"), [["bold", null], ["italic", null]]); }); test("the alignment buttons set and clear text-align", () => { const env = editorWindow("one
two
"); const [one, two] = env.box.querySelectorAll("p"); select(env.window, one.firstChild, 0, two.firstChild, 3); button(env.document, "Align center").click(); assert.equal(env.textarea.value, 'one
\ntwo
'); button(env.document, "Align right").click(); assert.equal(env.textarea.value, 'one
\ntwo
'); button(env.document, "Align right").click(); assert.equal(env.textarea.value, "one
\ntwo
"); }); test("an old align attribute is replaced", () => { const env = editorWindow('x
'); select(env.window, env.box.querySelector("p").firstChild, 0); button(env.document, "Justify").click(); assert.equal(env.textarea.value, 'x
'); }); test("indent steps by 30 pixels and never goes below zero", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 0); button(env.document, "Increase indent").click(); button(env.document, "Increase indent").click(); assert.equal(env.textarea.value, 'x
'); button(env.document, "Decrease indent").click(); assert.equal(env.textarea.value, 'x
'); button(env.document, "Decrease indent").click(); button(env.document, "Decrease indent").click(); assert.equal(env.textarea.value, "x
"); }); test("indent in a list uses the browser's command", () => { const env = editorWindow("one
two
three
"); const ps = env.box.querySelectorAll("p"); select(env.window, ps[0].firstChild, 1, ps[1].firstChild, 1); button(env.document, "Blockquote").click(); assert.equal(env.textarea.value, "\n\none
\ntwo
\n
three
"); select(env.window, ps[0].firstChild, 1); button(env.document, "Blockquote").click(); assert.equal(env.textarea.value, "one
\ntwo
\nthree
"); }); test("the link dialog adds a link", () => { const env = editorWindow("go here
"); const text = env.box.querySelector("p").firstChild; select(env.window, text, 3, text, 7); button(env.document, "Insert/edit link").click(); const dialog = env.document.querySelector(".xed-window[role=dialog]"); assert.ok(dialog); assert.equal(dialog.querySelector(".xed-title").textContent, "Insert link"); const inputs = dialog.querySelectorAll("input"); assert.equal(inputs.length, 3); assert.equal(inputs[1].value, "here"); assert.equal(env.document.activeElement, inputs[0]); inputs[0].value = "www.example.org"; inputs[1].value = "there"; inputs[2].value = "A title"; dialog.querySelector(".xed-primary").click(); assert.equal(env.document.querySelector(".xed-window"), null); assert.equal(env.textarea.value, 'go there
'); }); test("the link dialog makes email addresses into mailto links", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 1); button(env.document, "Insert/edit link").click(); const inputs = env.document.querySelectorAll(".xed-window input"); inputs[0].value = "ed@example.org"; key(env.window, inputs[0], "Enter"); assert.equal(env.textarea.value, ''); }); test("the link dialog refuses unsafe links", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 1); button(env.document, "Insert/edit link").click(); env.document.querySelector(".xed-window input").value = "javascript:alert(1)"; env.document.querySelector(".xed-primary").click(); assert.equal(env.textarea.value, "x
"); }); test("the link dialog edits and removes a link", () => { const env = editorWindow(''); select(env.window, env.box.querySelector("a").firstChild, 1); button(env.document, "Insert/edit link").click(); let inputs = env.document.querySelectorAll(".xed-window input"); assert.equal(inputs[0].value, "https://old.example/"); inputs[0].value = "https://new.example/"; env.document.querySelector(".xed-primary").click(); assert.equal(env.textarea.value, ''); select(env.window, env.box.querySelector("a").firstChild, 1); button(env.document, "Insert/edit link").click(); env.document.querySelector(".xed-window input").value = ""; env.document.querySelector(".xed-primary").click(); assert.equal(env.textarea.value, "old
"); }); test("Escape and Cancel close a dialog without a change", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 1); button(env.document, "Insert/edit link").click(); key(env.window, env.document.querySelector(".xed-window input"), "Escape"); assert.equal(env.document.querySelector(".xed-window"), null); assert.equal(env.document.querySelector(".xed-modal-block"), null); button(env.document, "Insert/edit link").click(); env.document.querySelector(".xed-window input").value = "https://example.org/"; Array.from(env.document.querySelectorAll(".xed-button")).find((b) => b.textContent === "Cancel").click(); assert.equal(env.document.querySelector(".xed-window"), null); assert.equal(env.textarea.value, "x
"); }); test("a click beside the link dialog does not close it", () => { const env = editorWindow("x
"); button(env.document, "Insert/edit link").click(); env.document.querySelector(".xed-modal-block").click(); assert.ok(env.document.querySelector(".xed-window")); }); test("remove link unwraps the link at the caret", () => { const env = editorWindow('a link b
'); select(env.window, env.box.querySelector("a").firstChild, 2); button(env.document, "Remove link").click(); assert.equal(env.textarea.value, "a link b
"); }); test("the special character window inserts a character", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 1); button(env.document, "Special character").click(); const cells = env.document.querySelectorAll(".xed-charmap td"); assert.equal(cells.length, 250); assert.equal(env.document.querySelectorAll(".xed-charmap tr").length, 10); const euro = Array.from(cells).find((c) => c.title === "euro sign"); euro.dispatchEvent(new env.window.MouseEvent("mouseover")); assert.equal(env.document.querySelector(".xed-charmap-char").textContent, "€"); assert.equal(env.document.querySelector(".xed-charmap-name").textContent, "euro sign"); euro.click(); assert.equal(env.document.querySelector(".xed-window"), null); assert.deepEqual(env.window.commands.at(-1), ["insertText", "€"]); }); test("the special character window answers the keyboard", () => { const env = editorWindow("x
"); button(env.document, "Special character").click(); const cell = env.document.querySelector(".xed-charmap td"); assert.equal(cell.tabIndex, 0); key(env.window, cell, "Enter"); assert.deepEqual(env.window.commands.at(-1), ["insertText", " "]); }); test("a click outside the special character window closes it", () => { const env = editorWindow("x
"); button(env.document, "Special character").click(); const backdrop = env.document.querySelector(".xed-modal-block"); assert.ok(backdrop.classList.contains("xed-modal-light")); backdrop.click(); assert.equal(env.document.querySelector(".xed-window"), null); assert.equal(env.document.querySelector(".xed-modal-block"), null); assert.equal(env.window.commands.filter((c) => c[0] === "insertText").length, 0); }); test("the source button swaps the editor for the textarea and back", () => { const env = editorWindow("a
"); const code = button(env.document, "Source code"); code.click(); assert.equal(env.textarea.hidden, false); assert.equal(env.textarea.style.display, ""); assert.equal(env.box.hidden, true); assert.equal(code.getAttribute("aria-pressed"), "true"); assert.ok(code.classList.contains("xed-active")); for (const b of env.document.querySelectorAll(".xed-btn")) assert.equal(b.disabled, b !== code); assert.equal(env.document.querySelector(".xed-window"), null, "no source window"); env.textarea.value = 'edited
'; assert.equal(env.editor.getContent(), env.textarea.value, "the textarea is the story in source mode"); code.click(); assert.equal(env.box.hidden, false); assert.equal(env.textarea.hidden, true); assert.equal(code.getAttribute("aria-pressed"), "false"); assert.ok(Array.from(env.document.querySelectorAll(".xed-btn")).every((b) => !b.disabled)); assert.equal(env.textarea.value, "edited
"); }); test("setContent leaves source mode", () => { const env = editorWindow("a
"); button(env.document, "Source code").click(); env.editor.setContent("b
"); assert.equal(env.box.hidden, false); assert.equal(env.editor.getContent(), "b
"); }); test("undo and redo, with the keys", () => { const env = editorWindow("x
"); select(env.window, env.box.querySelector("p").firstChild, 0); button(env.document, "Align center").click(); assert.equal(env.textarea.value, 'x
'); assert.ok(key(env.window, env.box, "z", { ctrlKey: true }).defaultPrevented); assert.equal(env.textarea.value, "x
"); key(env.window, env.box, "y", { ctrlKey: true }); assert.equal(env.textarea.value, 'x
'); key(env.window, env.box, "z", { metaKey: true }); key(env.window, env.box, "Z", { ctrlKey: true, shiftKey: true }); assert.equal(env.textarea.value, 'x
'); }); test("undo keeps a run of typing as one step", () => { const env = editorWindow("a
"); type(env, "ab
"); type(env, "abc
"); key(env.window, env.box, "z", { ctrlKey: true }); assert.equal(env.textarea.value, "a
"); }); test("undo from the browser's history input", () => { const env = editorWindow("a
"); type(env, "b
"); const event = new env.window.InputEvent("beforeinput", { inputType: "historyUndo", bubbles: true, cancelable: true }); env.box.dispatchEvent(event); assert.ok(event.defaultPrevented); assert.equal(env.textarea.value, "a
"); }); test("the shortcut keys run the toolbar commands", () => { const env = editorWindow("x
"); selectText(env.window, env.box.querySelector("p")); assert.ok(key(env.window, env.box, "b", { ctrlKey: true }).defaultPrevented); assert.deepEqual(env.window.commands.at(-1), ["bold", null]); key(env.window, env.box, "i", { metaKey: true }); assert.deepEqual(env.window.commands.at(-1), ["italic", null]); key(env.window, env.box, "k", { ctrlKey: true }); assert.ok(env.document.querySelector(".xed-window")); }); test("other keys are left to the browser", () => { const env = editorWindow("x
"); assert.equal(key(env.window, env.box, "b").defaultPrevented, false); assert.equal(key(env.window, env.box, "b", { ctrlKey: true, altKey: true }).defaultPrevented, false); assert.equal(key(env.window, env.box, "c", { ctrlKey: true }).defaultPrevented, false); }); test("pasted HTML is cleaned first", () => { const env = editorWindow("x
"); const event = paste(env, { "text/html": 'Bold
' }); assert.ok(event.defaultPrevented); assert.deepEqual(env.window.commands.at(-1), ["insertHTML", "Bold
"]); }); test("pasted text keeps its paragraphs and line breaks", () => { const env = editorWindow("x
"); env.window.commands.length = 0; paste(env, { "text/plain": "one\r\ntwo\n\nthree" }); assert.deepEqual(env.window.commands, [["insertText", "one"], ["insertLineBreak", null], ["insertText", "two"], ["insertParagraph", null], ["insertText", "three"]]); });