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
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
[abcd]
", stepFunction: deleteRange, contentAfterEdit: "[]
ab[c
d]ef
", stepFunction: deleteRange, contentAfter: "ab[]ef
", }); }); test("should merge right block's content into left block", async () => { await testEditor({ contentBefore: "d]ef
", stepFunction: deleteRange, contentAfter: "d]ef
", stepFunction: deleteRange, contentAfter: "def]
", stepFunction: deleteRange, contentAfter: "abc
[]def
abc
[]def
abc[
]def
abc[]def
ab[c
d]efab[]ef
abc[
]defabc[]def
d]ef
d]ef
ghi
ghi
]def
d]ef
ghid]ef
<]br> |
] |
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. //d]ef
-> [d]ef
-> deleteRange await testEditor({ contentBefore: "d]ef
", stepFunction: deleteSelection, contentAfter: "[]ef
", }); }); test("should keep left block if both have been emptied", async () => { await testEditor({ contentBefore: "def]
", stepFunction: deleteSelection, contentAfter: "ab[c
ab[]
ab[c
ab[c
ab[c
ab[]
", }); }); }); describe("Unremovables", () => { test("should not remove unremovable node, but clear its content", async () => { await testEditor({ contentBefore: `a[bc
gh]i
`, stepFunction: deleteSelection, contentAfter: `a[]
i
`, }); }); test("should move the unremovable up the tree", async () => { await testEditor({ contentBefore: `a[bc
gh]i
`, stepFunction: deleteSelection, contentAfter: `a[]
i
`, }); }); test("should preserve parent-child relations between unremovables", async () => { await testEditor({ contentBefore: unformat( `a[bc
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
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( `gh]i
` ), stepFunction: deleteSelection, contentAfterEdit: unformat( `i
` ), contentAfter: unformat( `i
` ), }); }); test("should remove bootstrap columns", async () => { await testEditor({ contentBefore: unformat( `x[yz
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 |
[] | c | |
d | e | f |
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
a[]
ghi
]jkl
[]jkl