penpot/frontend/text-editor/editor/controllers/SelectionController.test.js
2024-11-19 17:05:30 +01:00

1166 lines
43 KiB
JavaScript

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 <br> 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!"
);
});
});