import { describe, expect, test } from "@odoo/hoot"; import { setupEditor, testEditor } from "../_helpers/editor"; import { unformat } from "../_helpers/format"; import { CORE_PLUGINS } from "@html_editor/plugin_sets"; import { getContent, setSelection } from "../_helpers/selection"; async function testCoreEditor(testConfig) { return testEditor({ ...testConfig, config: { Plugins: CORE_PLUGINS } }); } // Tests the deleteRange shared method. async function deleteRange(editor) { // Avoid SelectionPlugin methods to avoid normalization. The goal is to // simulate the range passed as argument to the deleteRange method. const selection = editor.document.getSelection(); let range = selection.getRangeAt(0); range = editor.shared.delete.deleteRange(range); const { startContainer, startOffset, endContainer, endOffset } = range; selection.setBaseAndExtent(startContainer, startOffset, endContainer, endOffset); } // Tests the DELETE_SELECTION command. async function deleteSelection(editor) { editor.shared.delete.deleteSelection(); } describe("deleteRange method", () => { describe("Basic", () => { test("should delete a range inside a text node in a paragraph", async () => { await testCoreEditor({ contentBefore: "

a[bc]d

", stepFunction: deleteRange, contentAfterEdit: "

a[]d

", }); }); test("should delete a range across different nodes in a paragraph", async () => { await testCoreEditor({ contentBefore: "

a[bcdefghi]j

", stepFunction: deleteRange, contentAfterEdit: "

a[]j

", }); }); }); describe("Inside inline", () => { test("should delete a range inside an inline element", async () => { await testCoreEditor({ contentBefore: "

a[bc]d

", stepFunction: deleteRange, contentAfterEdit: "

a[]d

", }); }); test("should delete a range inside an inline element and fill empty inline", async () => { await testCoreEditor({ contentBefore: "

[abcd]

", stepFunction: deleteRange, contentAfterEdit: '

[]\u200b

', }); }); }); describe("Across inlines", () => { test("delete across two inlines (no merge)", async () => { await testCoreEditor({ contentBefore: "

a[bcdefg]h

", stepFunction: deleteRange, contentAfterEdit: "

a[]h

", }); }); test("delete across two inlines, start one left empty (should fill empty inline) ", async () => { await testCoreEditor({ contentBefore: "

[abcdefg]h

", stepFunction: deleteRange, contentAfterEdit: '

[]\u200bh

', }); }); test("delete across two inlines, end one left empty (should fill empty inline) ", async () => { await testCoreEditor({ contentBefore: "

a[bcdefgh]

", stepFunction: deleteRange, contentAfterEdit: '

a[]\u200b

', }); }); test("delete across two inlines, both left empty (should fill both)", async () => { await testCoreEditor({ contentBefore: "

[abcdefgh]jkl

", stepFunction: deleteRange, contentAfterEdit: '

[]\u200b\u200bjkl

', }); }); test("delete across two inlines, both left empty, block left shrunk (should fill inlines and block", async () => { await testCoreEditor({ contentBefore: "

[abcdefgh]

", stepFunction: deleteRange, contentAfterEdit: '

[]\u200b\u200b

', }); }); }); describe("Inside block", () => { test("should delete a range inside a text node in a paragraph and fill shrunk block", async () => { await testCoreEditor({ contentBefore: "

[abcd]

", stepFunction: deleteRange, contentAfterEdit: "

[]

", }); }); }); describe("Across blocks", () => { test("should merge paragraphs", async () => { await testEditor({ contentBefore: "

ab[c

d]ef

", stepFunction: deleteRange, contentAfter: "

ab[]ef

", }); }); test("should merge right block's content into left block", async () => { await testEditor({ contentBefore: "

ab[c

d]ef

", stepFunction: deleteRange, contentAfter: "

ab[]ef

", }); }); test("should merge right block's content into fully selected left block", async () => { // As opposed to the DELETE_SELECTION command, in which fully selected block on the left is removed. // See "should remove fully selected left block and keep second block" await testEditor({ contentBefore: "

[abc

d]ef

", stepFunction: deleteRange, contentAfter: "

[]ef

", }); }); test("should merge right block's content into left block and fill shrunk block", async () => { await testEditor({ contentBefore: "

[abc

def]

", stepFunction: deleteRange, contentAfter: "

[]

", }); }); test("should not merge paragraph with paragraph before it", async () => { await testEditor({ contentBefore: "

abc

[

]def

", stepFunction: deleteRange, contentAfter: "

abc

[]

def

", }); }); test("should merge paragraph with paragraph before it", async () => { await testEditor({ contentBefore: "

abc[

]def

", stepFunction: deleteRange, contentAfter: "

abc[]def

", }); }); }); describe("Block + inline", () => { test("should merge paragraph with inline content after it", async () => { await testEditor({ contentBefore: "

ab[c

d]ef
", stepFunction: deleteRange, contentAfter: "

ab[]ef

", }); }); test("should merge paragraph with inline content after it (2)", async () => { // This is the kind of range passed to deleteRange on `...

[]def...` + deleteBackward await testEditor({ contentBefore: "

abc[

]def
", stepFunction: deleteRange, contentAfter: "

abc[]def

", }); }); }); describe("Inline + block", () => { test("should merge paragraph with inline content before it (remove paragraph)", async () => { await testEditor({ contentBefore: "
ab[c

d]ef

", stepFunction: deleteRange, contentAfter: "
ab[]ef
", }); }); test("should merge paragraph with inline content before it", async () => { await testEditor({ contentBefore: "
ab[c

d]ef

ghi

", stepFunction: deleteRange, contentAfter: "
ab[]ef

ghi

", }); }); test("should merge paragraph with inline content before it (remove paragraph) (2)", async () => { await testEditor({ contentBefore: "
abc[

]def

", stepFunction: deleteRange, contentAfter: "
abc[]def
", }); }); test("should merge paragraph with inline content before it and insert a line-break after it", async () => { await testEditor({ contentBefore: "
ab[c

d]ef

ghi
", stepFunction: deleteRange, contentAfter: "
ab[]ef
ghi
", }); }); test("should merge nested paragraph with inline content before it and insert a line-break after it", async () => { await testEditor({ contentBefore: `
ab[c

d]ef

ghi
`, stepFunction: deleteRange, contentAfter: "
ab[]ef
ghi
", }); }); }); describe("Fake line breaks", () => { test("should not crash if cursor is inside a fake BR", async () => { // The goal of this tests is to make sure deleteRange does not rely // on selection normaliztion. It should not assume that the cursor // is never inside a BR. const contentBefore = unformat( `




` ); const { editor, el } = await setupEditor(contentBefore); // Place the cursor inside the BR. setSelection({ anchorNode: el, anchorOffset: 0, focusNode: el.querySelector("tr:nth-child(2) td br"), focusOffset: 0, }); /* [


<]br>
*/ deleteRange(editor); const contentAfter = unformat( `[


]

` ); expect(getContent(el)).toBe(contentAfter); }); }); describe("Fill shrunk blocks", () => { test("should not fill a HR with BR", async () => { const { editor, el } = await setupEditor("

abc[

]def

"); deleteRange(editor); const hr = el.firstElementChild; expect(hr.childNodes.length).toBe(0); }); }); }); describe("deleteSelection", () => { describe("Merge blocks", () => { test("should remove fully selected left block and keep second block", async () => { // As opposed to the deleteRange method. // This is done by expanding the range to fully include the left // block before calling deleteRange. See `includeEndOrStartBlock` method. //

[abc

d]ef

-> [

abc

d]ef

-> deleteRange await testEditor({ contentBefore: "

[abc

d]ef

", stepFunction: deleteSelection, contentAfter: "

[]ef

", }); }); test("should keep left block if both have been emptied", async () => { await testEditor({ contentBefore: "

[abc

def]

", stepFunction: deleteSelection, contentAfter: "

[]

", }); }); }); describe("Unmergeables", () => { test("should not merge paragraph with unmeargeble block", async () => { await testEditor({ contentBefore: "

ab[c

d]ef
", stepFunction: deleteSelection, contentAfter: "

ab[]

ef
", }); }); test("should remove unmergeable block that has been emptied", async () => { // `includeEndOrStartBlock` fully includes the right block. //

ab[c

def]
->

ab[c

def
] -> deleteRange await testEditor({ contentBefore: "

ab[c

def]
", stepFunction: deleteSelection, contentAfter: "

ab[]

", }); }); }); describe("Unremovables", () => { test("should not remove unremovable node, but clear its content", async () => { await testEditor({ contentBefore: `

a[bc

def

gh]i

`, stepFunction: deleteSelection, contentAfter: `

a[]


i

`, }); }); test("should move the unremovable up the tree", async () => { await testEditor({ contentBefore: `

a[bc

def

gh]i

`, stepFunction: deleteSelection, contentAfter: `

a[]


i

`, }); }); test("should preserve parent-child relations between unremovables", async () => { await testEditor({ contentBefore: unformat( `

a[bc

jkl

mno

gh]i

` ), stepFunction: deleteSelection, contentAfter: unformat( `

a[]


i

` ), }); }); test("should preserve parent-child relations between unremovables (2)", async () => { await testEditor({ contentBefore: unformat( `

a[bc

xyz
jkl

mno

mno

gh]i

` ), stepFunction: deleteSelection, contentAfter: unformat( `

a[]




i

` ), }); }); }); describe("Conditional unremovables", () => { describe("Bootstrap columns", () => { test("should not remove bootstrap columns, but clear its content", async () => { await testEditor({ contentBefore: unformat( `
a[bc
def

gh]i

` ), stepFunction: deleteSelection, contentAfterEdit: unformat( `
a[]

i

` ), contentAfter: unformat( `
a[]

i

` ), }); }); test("should remove bootstrap columns", async () => { await testEditor({ contentBefore: unformat( `

x[yz

abc
def

gh]i

` ), stepFunction: deleteSelection, contentAfter: "

x[]i

", }); }); }); describe("Table cells", () => { test("should not remove table cell, but clear its content", async () => { // Actually this is handled by the table plugin, and does not // involve the unremovable mechanism. await testEditor({ contentBefore: unformat( `
[a b] c
d e f
` ), stepFunction: deleteSelection, contentAfter: unformat( `
[]

c
d e f
` ), }); }); test("should remove table", async () => { await testEditor({ contentBefore: unformat( `

a[bc

abc

def

gh]i

` ), stepFunction: deleteSelection, contentAfter: "

a[]i

", }); }); }); }); describe("Allowed content mismatch on blocks merge", () => { test("should not add H1 (flow content) to P (allows phrasing content only)", async () => { await testEditor({ contentBefore: unformat( `

a[bc

` ), stepFunction: deleteSelection, contentAfter: unformat( `

a[]

` ), }); }); test("should add P (flow content) to LI (allows flow content) ", async () => { await testEditor({ contentBefore: unformat( `` ), stepFunction: deleteSelection, contentAfter: unformat( `` ), }); }); }); });