import { expect, describe, test } from "vitest"; import TextEditor from "~/editor/TextEditor"; import { createRoot } from "~/editor/content/dom/Root"; import { createEmptyParagraph, createParagraph } from "~/editor/content/dom/Paragraph"; import { createInline } from "~/editor/content/dom/Inline"; import { createLineBreak } from "~/editor/content/dom/LineBreak"; import { TextEditorMock } from "~/test/TextEditorMock"; import { SelectionController } from "./SelectionController"; import { SelectionDirection } from "./SelectionDirection"; /* @vitest-environment jsdom */ /** * Utility function to make focus and selections work properly in JSDOM. * * @param {Selection} selection * @param {TextEditor} textEditor * @param {Node} focusNode * @param {number} [focusOffset=0] * @param {Node} [anchorNode=null] * @param {number} [anchorOffset=0] */ function focus(selection, textEditor, focusNode, focusOffset = 0, anchorNode = focusNode, anchorOffset = focusOffset) { textEditor.element.focus(); selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); document.dispatchEvent(new Event("selectionchange")); } describe("SelectionController", () => { test("`selection` should return the Selection object kept by the SelectionController", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); const selection = document.getSelection(); const selectionController = new SelectionController(textEditorMock, selection); expect(selectionController.selection).toBe(selection); }); test("`range` should return the Range object kept by the SelectionController", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); // When the editor hasn't been focused // range is null. expect(selectionController.range).toBe(null); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.range).toBeInstanceOf(Range); }); test("`focusAtStart` should return `true` if the offset is 0", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.focusAtStart).toBe(true); }); test("`focusAtEnd` should return `true` if the offset is the length of the `textContent`", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, "Hello, World!".length, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.focusAtEnd).toBe(true); }); test("`anchorAtStart` should return `true` if the offset is 0", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.anchorAtStart).toBe(true); }); test("`anchorAtEnd` should return `true` if the offset is the length of the `textContent`", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, "Hello, World!".length ); expect(selectionController.anchorAtEnd).toBe(true); }); test("`direction` should return the direction of the focus and anchor nodes", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.direction).toBe(SelectionDirection.NONE); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 5, root.firstChild.firstChild.firstChild, 0 ); expect(selectionController.direction).toBe(SelectionDirection.FORWARD); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.firstChild.firstChild, 5 ); expect(selectionController.direction).toBe(SelectionDirection.BACKWARD); }); test("`insertText` should insert some text in a Text node", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, "Hello".length ); selectionController.insertText(", World!"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(HTMLSpanElement); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(Text); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe("Hello, World!"); }); test("`replaceLineBreak` should replace a
with some text", () => { const textEditorMock = TextEditorMock.createTextEditorMockEmpty(); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild ); selectionController.replaceLineBreak("Hello, World!"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf( HTMLDivElement ); expect(textEditorMock.root.firstChild.dataset.itype).toBe( "paragraph" ); expect( textEditorMock.root.firstChild.firstChild ).toBeInstanceOf(HTMLSpanElement); expect( textEditorMock.root.firstChild.firstChild.dataset.itype ).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild .nodeValue ).toBe("Hello, World!"); }); test("`removeBackwardText` should remove text in backward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, "Hello, World!".length ); selectionController.removeBackwardText(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf( HTMLDivElement ); expect(textEditorMock.root.firstChild.dataset.itype).toBe( "paragraph" ); expect( textEditorMock.root.firstChild.firstChild ).toBeInstanceOf(HTMLSpanElement); expect( textEditorMock.root.firstChild.firstChild.dataset.itype ).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, World"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild .nodeValue ).toBe("Hello, World"); }); test("`removeBackwardText` should remove text in backward direction (backspace) and create a new empty paragraph when there's nothing left", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("H"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, "H".length ); selectionController.removeBackwardText(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe(""); }); test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, "))]), createParagraph([createInline(new Text("World!"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.childNodes.item(1).firstChild.firstChild, 0 ); selectionController.mergeBackwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.children.length).toBe(1); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, World!"); }); test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, "))]), createEmptyParagraph(), createParagraph([createInline(new Text("World!"))]) ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController(textEditorMock, selection); focus( selection, textEditorMock, root.childNodes.item(2).firstChild.firstChild, 0 ); selectionController.mergeBackwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.textContent).toBe("Hello, "); expect(textEditorMock.root.lastChild.textContent).toBe("World!"); }); test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, "))]), createParagraph([createInline(new Text("World!"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, root.firstChild.firstChild.firstChild.nodeValue.length ); selectionController.mergeForwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.children.length).toBe(1); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, World!"); }); test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, "))]), createEmptyParagraph(), createParagraph([createInline(new Text("World!"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.childNodes.item(2).firstChild.firstChild, 0 ); selectionController.mergeBackwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.textContent).toBe("Hello, "); expect(textEditorMock.root.lastChild.textContent).toBe("World!"); }); test("`removeForwardText` should remove text in forward direction (delete)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, ); selectionController.removeForwardText(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf( HTMLDivElement ); expect(textEditorMock.root.firstChild.dataset.itype).toBe( "paragraph" ); expect( textEditorMock.root.firstChild.firstChild ).toBeInstanceOf(HTMLSpanElement); expect( textEditorMock.root.firstChild.firstChild.dataset.itype ).toBe("inline"); expect(textEditorMock.root.textContent).toBe("ello, World!"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild .nodeValue ).toBe("ello, World!"); }); test("`replaceText` should replace the selected text", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 7, root.firstChild.firstChild.firstChild, 12 ); selectionController.replaceText("Mundo"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf( HTMLDivElement ); expect(textEditorMock.root.firstChild.dataset.itype).toBe( "paragraph" ); expect( textEditorMock.root.firstChild.firstChild ).toBeInstanceOf(HTMLSpanElement); expect( textEditorMock.root.firstChild.firstChild.dataset.itype ).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, Mundo!"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild .nodeValue ).toBe("Hello, Mundo!"); }); test("`replaceInlines` should replace the selected text in multiple inlines (2 completelly selected)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, ")), createInline(new Text("World!")) ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.lastChild.firstChild, "World!".length ); selectionController.replaceInlines("Mundo"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf( HTMLDivElement ); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe( "paragraph" ); expect( textEditorMock.root.firstChild.firstChild ).toBeInstanceOf(HTMLSpanElement); expect( textEditorMock.root.firstChild.firstChild.dataset.itype ).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Mundo"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild .nodeValue ).toBe("Mundo"); }); test("`replaceInlines` should replace the selected text in multiple inlines (2 partially selected)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, ")), createInline(new Text("World!")), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 2, root.firstChild.lastChild.firstChild, "World!".length - 3 ); selectionController.replaceInlines("Mundo"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(2); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("HeMundold!"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe( "HeMundo" ); expect(textEditorMock.root.firstChild.lastChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe( "ld!" ); }); test("`replaceInlines` should replace the selected text in multiple inlines (1 partially selected, 1 completelly selected)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, ")), createInline(new Text("World!")), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 2, root.firstChild.lastChild.firstChild, "World!".length ); selectionController.replaceInlines("Mundo"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("HeMundo"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe( "HeMundo" ); }); test("`replaceInlines` should replace the selected text in multiple inlines (1 completelly selected, 1 partially selected)", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, ")), createInline(new Text("World!")), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.lastChild.firstChild, "World!".length - 3 ); selectionController.replaceInlines("Mundo"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Mundold!"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe( "Mundold!" ); }); test("`removeSelected` removes a word", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 7, root.firstChild.lastChild.firstChild, "Hello, World!".length - 1 ); selectionController.removeSelected(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, !"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild.nodeValue ).toBe("Hello, !"); }); test("`removeSelected` multiple inlines", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, ")), createInline(new Text("World!")), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 0, root.firstChild.lastChild.firstChild, "World!".length ); selectionController.removeSelected(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe(""); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( HTMLBRElement ); }); test("`removeSelected` multiple paragraphs", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([ createInline(new Text("Hello, ")) ]), createParagraph([ createInline(createLineBreak()) ]), createParagraph([ createInline(new Text("World!")) ]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.lastElementChild, 0, root.children.item(1).firstChild, 0 ); selectionController.removeSelected(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe( "Hello, " ); expect(textEditorMock.root.lastChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe( "World!" ); }); test("`removeSelected` and `removeBackwardParagraph`", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createInline(createLineBreak())]), createParagraph([createInline(new Text("This is a test"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.lastElementChild.firstElementChild.firstChild, // This is a test text 0, root.lastElementChild.firstElementChild.firstChild, "This is a test".length ); selectionController.removeSelected(); selectionController.removeBackwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect( textEditorMock.root.firstChild.firstChild.firstChild ).toBeInstanceOf(Text); expect( textEditorMock.root.firstChild.firstChild.firstChild.nodeValue ).toBe("Hello, World!"); }); test("`removeSelected` and `removeForwardParagraph`", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createInline(createLineBreak())]), createParagraph([createInline(new Text("This is a test"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstElementChild.firstElementChild.firstChild, // This is a test text 0, root.firstElementChild.firstElementChild.firstChild, "Hello, World!".length ); selectionController.removeSelected(); selectionController.removeForwardParagraph(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("This is a test"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( HTMLBRElement ); expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe( "This is a test" ); }); test("performing a `removeSelected` after a `removeSelected` should do nothing", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createInline(createLineBreak())]), createParagraph([createInline(new Text("This is a test"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstElementChild.firstElementChild.firstChild, // This is a test text 0, root.firstElementChild.firstElementChild.firstChild, "Hello, World!".length ); selectionController.removeSelected(); // This should do nothing. selectionController.removeSelected(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(3); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( "inline" ); expect(textEditorMock.root.textContent).toBe("This is a test"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( HTMLBRElement ); expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe( "This is a test" ); }); test("`removeSelected` removes everything", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createInline(createLineBreak())]), createParagraph([createInline(new Text("This is a test"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstElementChild.firstElementChild.firstChild, // This is a test text 0, root.lastElementChild.firstElementChild.firstChild, "This is a test".length ); selectionController.removeSelected(); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(1); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.textContent).toBe(""); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( HTMLBRElement ); }); test("`removeSelected` removes everything and insert text", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createInline(createLineBreak())]), createParagraph([createInline(new Text("This is a test"))]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstElementChild.firstElementChild.firstChild, // This is a test text 0, root.lastElementChild.firstElementChild.firstChild, "This is a test".length ); selectionController.removeSelected(); selectionController.replaceLineBreak("Hello, World!"); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children).toHaveLength(1); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.children).toHaveLength(1); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( Text ); expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe( "Hello, World!" ); }); test('`applyStyles` to text', () => { const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!"); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController(textEditorMock, selection); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, root.firstChild.firstChild.firstChild.nodeValue.length - 1, root.firstChild.firstChild.firstChild, root.firstChild.firstChild.firstChild.nodeValue.length - 6 ); selectionController.applyStyles({ "font-weight": "bold" }); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(1); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.children.length).toBe(3); expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe("Hello, "); expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe("World"); expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe("!"); }); test('`applyStyles` to inlines', () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ createInline(new Text("Hello, "), { "font-style": "italic" }), createInline(new Text("World!"), { "font-style": "oblique" }) ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController(textEditorMock, selection); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 2, root.firstChild.lastChild.firstChild, root.firstChild.lastChild.firstChild.nodeValue.length - 3 ); selectionController.applyStyles({ "font-weight": "bold" }); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(1); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.children.length).toBe(4); expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe("inline"); expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe("He"); expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe("llo, "); expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe("Wor"); expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe("ld!"); }); test('`applyStyles` to paragraphs', () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraph([ createInline(new Text("Hello, "), { "font-style": "italic", }), ]), createParagraph([ createInline(new Text("World!"), { "font-style": "oblique", }), ]), ]); const root = textEditorMock.root; const selection = document.getSelection(); const selectionController = new SelectionController( textEditorMock, selection ); focus( selection, textEditorMock, root.firstChild.firstChild.firstChild, 2, root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild.nodeValue.length - 3 ); selectionController.applyStyles({ "font-weight": "bold", }); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.children.length).toBe(2); expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph"); expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf( HTMLSpanElement ); expect(textEditorMock.root.firstChild.children.length).toBe(2); expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe( "He" ); expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe( "llo, " ); expect(textEditorMock.root.lastChild.children.length).toBe(2); expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe( "Wor" ); expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe( "inline" ); expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe( "ld!" ); }); });