import { describe, expect, test } from "@odoo/hoot";
import { testEditor, setupEditor } from "../_helpers/editor";
import { unformat } from "../_helpers/format";
import { tick } from "@odoo/hoot-mock";
import { deleteForward, insertText, tripleClick } from "../_helpers/user_actions";
import { getContent } from "../_helpers/selection";
import { microTick } from "@odoo/hoot-dom";
/**
* content of the "deleteForward" sub suite in editor.test.js
*/
async function twoDeleteForward(editor) {
deleteForward(editor);
deleteForward(editor);
}
describe("Selection collapsed", () => {
describe("Basic", () => {
test("should do nothing", async () => {
// TODO the addition of
"correction" part was judged
// unnecessary to enforce, the rest of the test still makes
// sense: not removing the unique
[]
', // stepFunction: deleteForward, // contentAfter: '[]
', // }); // TODO this cannot actually be tested currently as a // backspace/delete in that case is not even detected // (no input event to rollback) // await testEditor({ // contentBefore: '[
]
visible. // // It does not exist in VDocument and selecting it // // has no meaning in the DOM. // contentAfter: '
[]
abc[]
", stepFunction: deleteForward, contentAfter: "abc[]
", }); }); test("should delete the first character in a paragraph", async () => { await testEditor({ contentBefore: "[]abc
", stepFunction: deleteForward, contentAfter: "[]bc
", }); }); test("should delete a character within a paragraph", async () => { await testEditor({ contentBefore: "a[]bc
", stepFunction: deleteForward, contentAfter: "a[]c
", }); }); test("should delete the last character in a paragraph", async () => { await testEditor({ contentBefore: "ab[]c
", stepFunction: deleteForward, contentAfter: "ab[]
", }); await testEditor({ contentBefore: "ab []c
", stepFunction: deleteForward, // The space should be converted to an unbreakable space // so it is visible. contentAfter: "ab []
", }); }); test("should merge a paragraph into an empty paragraph", async () => { await testEditor({ contentBefore: "[]
abc
", stepFunction: deleteForward, contentAfter: "[]abc
", }); }); test("should merge P node correctly ", async () => { await testEditor({ contentBefore: "b[]
c
db[]c
dc
dc
dab[]\u200Bc
", stepFunction: deleteForward, contentAfter: "ab[]
", }); await testEditor({ contentBefore: "de[]\u200B
", stepFunction: deleteForward, contentAfter: "de[]
", }); await testEditor({ contentBefore: "[]\u200Bf
", stepFunction: deleteForward, contentAfter: "[]
a[]bcde
', stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "a[]e
", }); }); test("ZWS: should delete element content but keep cursor in", async () => { await testEditor({ contentBefore: 'ab[]cdef
', stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfterEdit: 'ab[]\u200Bef
', contentAfter: 'ab[]\u200Bef
', }); await testEditor({ contentBefore: 'ab[]cdef
', stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); await insertText(editor, "x"); }, contentAfter: 'abx[]ef
', }); }); test("should ignore ZWS and merge", async () => { await testEditor({ contentBefore: '[]\u200Bab
', contentBeforeEdit: '[]\u200Bab
', stepFunction: async (editor) => { deleteForward(editor); await insertText(editor, "x"); }, contentAfter: "x[]b
", }); await testEditor({ contentBefore: '[]\u200Bcd
', stepFunction: async (editor) => { deleteForward(editor); await insertText(editor, "x"); }, contentAfter: 'x[]d
', }); await testEditor({ contentBefore: '[]\u200B
ef
x[]ef
", }); }); test('should remove contenteditable="false"', async () => { await testEditor({ contentBefore: `abc[]
xyzdef
abc[]xyz
def
[]
bca
`, stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: `[]bca
`, }); }); test("should not remove a non editable sibling (inline)", async () => { await testEditor({ contentBefore: unformat(`[]
[]
[]
[]
abc[]
def
abc[]
def
[]\uD83D\uDE0D def
`, stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: `[] def
`, }); }); test("should remove invisible empty space at the start", async () => { await testEditor({ contentBefore: `[] def
`, stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: `[]ef
`, }); }); test("should remove invisible empty space at the start (2)", async () => { await testEditor({ // The first 3 spaces are invisible : considered // formating by the browser. // The is visible (space 1). // The last 3 spaces are consider as 1 visble space and // two formating spaces (space 2). contentBefore: `[] def
`, stepFunction: async (editor) => { deleteForward(editor); }, // Space 1 is deleted and space 2 should be transformed // to a to stay visible. contentAfter: `[] def
`, }); }); test("should merge p elements inside conteneditbale=true inside contenteditable=false", async () => { await testEditor({ contentBefore: `abc[]
def
abc[]def
[]abc
`, }); }); test("should remove a link to uploaded document at the end of the editable", async () => { await testEditor({ contentBefore: ``, stepFunction: deleteForward, contentAfter: `[]
abc[]
def
abc[]def
", }); }); test("should delete a line break and merge the", async () => { await testEditor({ contentBefore: "
abc[]
def
", stepFunction: deleteForward, contentAfter: "abc[]def
", }); }); }); describe("intefering spaces", () => { test("should delete a br line break", async () => { await testEditor({ contentBefore: "abc[]
def
abc[]def
", }); }); test("should merge the two", async () => { await testEditor({ contentBefore: "
abc[]
def
", stepFunction: deleteForward, contentAfter: "abc[]def
", }); await testEditor({ contentBefore: 'abc[]
def
orphan node
', stepFunction: deleteForward, contentAfter: 'abc[]def
orphan node
', }); }); test("should delete the space if the secondis display inline", async () => { await testEditor({ contentBefore: '
def
def
abc[]x
def
abc[]def
", }); }); test("should merge the two", async () => { await testEditor({ contentBefore: "
abc[]x
def
", stepFunction: twoDeleteForward, contentAfter: "abc[]def
", }); }); test("should delete the space if the secondis display inline", async () => { await testEditor({ contentBefore: '
def
def
[]
abc
[]abc
", }); await testEditor({ contentBefore: "[]
abc
[]abc
", }); }); test("should delete a line break within a paragraph", async () => { await testEditor({ contentBefore: "ab[]
cd
ab[]cd
", }); await testEditor({ contentBefore: "ab []
cd
ab []cd
", }); await testEditor({ contentBefore: "ab[]
cd
ab[]cd
", }); }); test("should delete a trailing line break", async () => { await testEditor({ contentBefore: "abc[]
abc[]
", }); await testEditor({ contentBefore: "abc []
abc []
", }); }); test("should delete a character and a line break, emptying a paragraph", async () => { await testEditor({ contentBefore: "[]a
bcd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "[]
bcd
", }); }); test("should delete a character before a trailing line break", async () => { await testEditor({ contentBefore: "ab[]c
ab[]
ab
[]
cd
", stepFunction: deleteForward, contentAfter: "ab
[]cd
ab
[]
cd
", stepFunction: deleteForward, // This should be identical to 1 contentAfter: "ab
[]cd
ab
[]
cd
", stepFunction: deleteForward, contentAfter: "ab
[]
cd
", }); }); test("should delete a trailing line break, then merge a paragraph into a paragraph with 3ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]cd
ab
[]
cd
", stepFunction: deleteForward, contentAfter: "ab
[]
cd
", }); }); test("should delete two line breaks (1)", async () => { // 4-2 await testEditor({ contentBefore: "ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]
cd
", }); }); test("should delete two line breaks, then merge a paragraph into a paragraph with 2ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]cd
ab
[]
cd
", stepFunction: deleteForward, contentAfter: "ab
[]
cd
", }); }); test("should delete two line breaks (2)", async () => { // 5-2 await testEditor({ contentBefore: "ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]
cd
", }); }); test("should delete three line breaks (emptying a paragraph)", async () => { // 5-3 await testEditor({ contentBefore: "ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]
cd
", }); }); test("should delete three line breaks, then merge a paragraph into an empty parargaph", async () => { // 5-4 await testEditor({ contentBefore: "ab
[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab
[]cd
", }); }); test("should merge a paragraph with 4ab[]
cd
", stepFunction: deleteForward, contentAfter: "ab[]
cd
", }); }); test("should merge a paragraph with 4ab[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "ab[]
cd
", }); }); test("should merge a paragraph with 4ab[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab[]
cd
", }); }); test("should merge a paragraph with 4ab[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab[]
cd
", }); }); test("should merge a paragraph with 4ab[]
cd
", stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); deleteForward(editor); deleteForward(editor); deleteForward(editor); }, contentAfter: "ab[]cd
", }); }); }); }); describe("Pre", () => { test("should delete a character in a pre", async () => { await testEditor({ contentBefore: "ab[]cd", stepFunction: deleteForward, contentAfter: "
ab[]d", }); }); test("should delete a character in a pre (space before)", async () => { await testEditor({ contentBefore: "
ab[]cd", stepFunction: deleteForward, contentAfter: "
ab[]d", }); }); test("should delete a character in a pre (space after)", async () => { await testEditor({ contentBefore: "
ab[]cd", stepFunction: deleteForward, contentAfter: "
ab[]d", }); }); test("should delete a character in a pre (space before and after)", async () => { await testEditor({ contentBefore: "
ab[]cd", stepFunction: deleteForward, contentAfter: "
ab[]d", }); }); test("should delete a space in a pre", async () => { await testEditor({ contentBefore: "
[] ab", stepFunction: deleteForward, contentAfter: "
[] ab", }); }); test("should delete a newline in a pre", async () => { await testEditor({ contentBefore: "
ab[]\ncd", stepFunction: deleteForward, contentAfter: "
ab[]cd", }); }); test("should delete all leading space in a pre", async () => { await testEditor({ contentBefore: "
[] ab", stepFunction: async (BasicEditor) => { deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); }, contentAfter: "
[]ab", }); }); test("should delete all trailing space in a pre", async () => { await testEditor({ contentBefore: "
ab[]", stepFunction: async (BasicEditor) => { deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); deleteForward(BasicEditor); }, contentAfter: "
ab[]", }); }); }); describe("Formats", () => { test("should delete a character after a format node", async () => { await testEditor({ contentBefore: "
abc[]def
", stepFunction: deleteForward, contentAfter: "abc[]ef
", }); await testEditor({ contentBefore: "abc[]def
", stepFunction: deleteForward, // The selection is normalized so we only have one way // to represent a position. contentAfter: "abc[]ef
", }); }); }); describe("Merging different types of elements", () => { test("should merge a paragraph with text into a heading1 with text", async () => { await testEditor({ contentBefore: "cd
", stepFunction: deleteForward, contentAfter: "
[]
ab[]
cd
', stepFunction: deleteForward, contentAfter: "ab[]cd
", }); await testEditor({ contentBefore: 'ab[]
cd
ef
', stepFunction: deleteForward, contentAfter: "ab[]cd
ef
", }); }); }); describe("With attributes", () => { test("should remove empty paragraph with class", async () => { await testEditor({ contentBefore: '
[]
abc
', stepFunction: deleteForward, contentAfter: "[]abc
", }); }); test("should merge two paragraphs with spans of same classes", async () => { await testEditor({ contentBefore: 'dom to[]
edit
', stepFunction: deleteForward, contentAfter: 'dom to[]edit
', }); }); test("should merge two paragraphs with spans of different classes without merging the spans", async () => { await testEditor({ contentBefore: 'dom to[]
edit
', stepFunction: deleteForward, contentAfter: 'dom to[]edit
', }); }); test("should merge two paragraphs of different classes, each containing spans of the same class", async () => { await testEditor({ contentBefore: 'ab[]
cd
', stepFunction: deleteForward, contentAfter: 'ab[]cd
', }); }); test("should merge two paragraphs of different classes, each containing spans of different classes without merging the spans", async () => { await testEditor({ contentBefore: 'ab[]
cd
', stepFunction: deleteForward, contentAfter: 'ab[]cd
', }); }); test("should delete a line break between two spans with bold and merge these formats", async () => { await testEditor({ contentBefore: 'ab[]
cd
ab[]cd
', }); }); test("should delete a character in a span with bold, then a line break between two spans with bold and merge these formats", async () => { await testEditor({ contentBefore: 'a[]b
cde
a[]
cde
[]
[]
content
[]
content
[]
content[]
content[]
content[]
content
content[]
`), }); }); }); describe("POC extra tests", () => { test("should not remove a table without selecting it", async () => { await testEditor({ contentBefore: unformat( `ab[]
cd | ef |
gh | ij |
kl
` ), stepFunction: deleteForward, contentAfter: unformat( `ab[]
cd | ef |
gh | ij |
kl
` ), }); }); test("should not merge a table into its next sibling", async () => { await testEditor({ contentBefore: unformat( `ab
cd | ef |
gh | ij[] |
kl
` ), stepFunction: deleteForward, contentAfter: unformat( `ab
cd | ef |
gh | ij[] |
kl
` ), }); }); test("should delete the list item", async () => { await testEditor({ contentBefore: unformat( `
|
|
|
|
ab[cd]ef
", stepFunction: deleteForward, contentAfter: "ab[]ef
", }); }); test("should delete part of the text within a paragraph (forward, backward selection)", async () => { // Backward selection await testEditor({ contentBefore: "ab]cd[ef
", stepFunction: deleteForward, contentAfter: "ab[]ef
", }); }); test("should merge node correctly", async () => { await testEditor({ contentBefore: 'd]e
fb[c
d]e
fb[]e
fb]c
d[e
fb[]e
fa[bcde]f
', stepFunction: deleteForward, contentAfter: 'a[]\u200Bf
', }); }); test("should delete styling nodes when delete if empty (forward, with space around inline)", async () => { await testEditor({ contentBefore: 'ab [cd] ef
', stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "ab []ef
", }); }); test("should delete styling nodes when delete if empty (forward)", async () => { await testEditor({ contentBefore: 'uv[wx]yz
', stepFunction: async (editor) => { deleteForward(editor); deleteForward(editor); }, contentAfter: "uv[]z
", }); }); test("should delete across two paragraphs", async () => { // Forward selection await testEditor({ contentBefore: "ab[cd
ef]gh
", stepFunction: deleteForward, contentAfter: "ab[]gh
", }); // Backward selection await testEditor({ contentBefore: "ab]cd
ef[gh
", stepFunction: deleteForward, contentAfter: "ab[]gh
", }); }); test("should delete all the text in a paragraph", async () => { // Forward selection await testEditor({ contentBefore: "[abc]
", stepFunction: deleteForward, contentAfter: "[]
]abc[
", stepFunction: deleteForward, contentAfter: "[]
ab[cd
ef
ghijkl]mn
ab[]mn
", }); await testEditor({ contentBefore: "ab[cd
ef
ghijk]lmn
ab[]lmn
", }); // Backward selection await testEditor({ contentBefore: "ab]cd
ef
ghijkl[mn
ab[]mn
", }); await testEditor({ contentBefore: "ab]cd
ef
ghijk[lmn
ab[]lmn
", }); }); test("should delete all contents of a complex DOM with format nodes and multiple paragraphs (forward, forward selection)", async () => { await testEditor({ contentBefore: "[abcd
ef
ghijklmn]
[]
]abcd
ef
ghijklmn[
[]
ef]gh
", stepFunction: deleteForward, contentAfter: "ef[gh
", stepFunction: deleteForward, contentAfter: "ef]gh1
", stepFunction: deleteForward, contentAfter: "[]gh1
", }); await testEditor({ contentBefore: "ef]gh2
", stepFunction: deleteForward, contentAfter: "[]gh2
", }); // Backward selection await testEditor({ contentBefore: "ef[gh3
", stepFunction: deleteForward, contentAfter: "[]gh3
", }); await testEditor({ contentBefore: "ef[gh4
", stepFunction: deleteForward, contentAfter: "[]gh4
", }); }); test("should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph + content", async () => { await testEditor({ contentBefore: "content
ef]gh1
", stepFunction: deleteForward, contentAfter: "content
[]gh1
", }); await testEditor({ contentBefore: "content
ef]gh2
", stepFunction: deleteForward, contentAfter: "content
[]gh2
", }); }); test("should delete a selection from the beginning of a heading1 to the end of a paragraph", async () => { //Forward selection await testEditor({ contentBefore: "ef]
ef]
ef[
ef[
ef]
ef]
ef[
ef[
def
", {}); tripleClick(el.querySelector("h1")); await microTick(); // Chrome puts the cursor at the start of next sibling expect(getContent(el)).toBe("]def
"); await tick(); // The Editor corrects it on selection change expect(getContent(el)).toBe("def
"); deleteForward(editor); expect(getContent(el)).toBe( 'def
' ); }); test("should delete a heading (triple click delete) (2)", async () => { const { editor, el } = await setupEditor("def
", {}); tripleClick(el.querySelector("h1")); await microTick(); // Chrome puts the cursor at the start of next sibling expect(getContent(el)).toBe("]
def
"); await tick(); // The Editor corrects it on selection change expect(getContent(el)).toBe("def
"); deleteForward(editor); expect(getContent(el)).toBe( 'def
' ); }); test("should delete last character of paragraph, as well as selected paragraph break", async () => { await testEditor({ contentBefore: "ab[c
]def
", stepFunction: deleteForward, contentAfter: "ab[]def
", }); }); test("should delete first character of paragraph, as well as selected paragraph break", async () => { await testEditor({ contentBefore: "abc[
d]ef
", stepFunction: deleteForward, contentAfter: "abc[]ef
", }); }); test("should remove a fully selected table", async () => { await testEditor({ contentBefore: unformat( `a[b
cd | ef |
gh | ij |
k]l
` ), stepFunction: deleteForward, contentAfter: "a[]l
", }); }); test("should only remove the text content of cells in a partly selected table", async () => { await testEditor({ contentBefore: unformat( `cd | e[f | gh |
ij | k]l | mn |
op | qr | st |
cd | [] | gh |
ij | mn | |
op | qr | st |
a[b
cd | ef |
g]h | ij |
kl
` ), stepFunction: deleteForward, contentAfter: unformat( `a[]
kl
` ), }); }); test("should remove a table and some text (even if the table is partly selected)", async () => { await testEditor({ contentBefore: unformat( `ab
cd | ef |
gh | i[j |
k]l
` ), stepFunction: deleteForward, contentAfter: unformat( `ab
[]l
` ), }); }); test("should remove some text, a table and some more text", async () => { await testEditor({ contentBefore: unformat( `a[b
cd | ef |
gh | ij |
k]l
` ), stepFunction: deleteForward, contentAfter: `a[]l
`, }); }); test("should remove a selection of several tables", async () => { await testEditor({ contentBefore: unformat( `cd | e[f |
gh | ij |
cd | ef |
gh | ij |
cd | e]f |
gh | ij |
[]
0[1
cd | ef |
gh | ij |
23
cd | ef |
gh | ij |
45
cd | ef |
gh | ij |
67]
` ), stepFunction: deleteForward, contentAfter: `0[]
`, }); }); test("should remove everything, including several tables", async () => { await testEditor({ contentBefore: unformat( `[01
cd | ef |
gh | ij |
23
cd | ef |
gh | ij |
45
cd | ef |
gh | ij |
67]
` ), stepFunction: deleteForward, contentAfter: `[]
ab[cd]ef
', stepFunction: deleteForward, contentAfter: 'ab[]\u200Bef
', }); }); test("should remove element which is contenteditable=true even if their parent is contenteditable=false", async () => { await testEditor({ contentBefore: unformat(`before[o
intruder
o]after
`), stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: unformat(`before[]after
`), }); }); test("should extend the range to fully include contenteditable=false that are partially selected at the end of the range", async () => { await testEditor({ contentBefore: unformat(`before[o
intruder]
after
`), stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: unformat(`before[]
after
`), }); }); // @todo @phoenix: review this spec. It should not merge, like the test above. test("should extend the range to fully include contenteditable=false that are partially selected at the start of the range", async () => { await testEditor({ contentBefore: unformat(`before
[intruder
o]after
`), stepFunction: async (editor) => { deleteForward(editor); }, contentAfter: unformat(`before[]after
`), }); }); test("should remove empty paragraph and content from the second one", async () => { await testEditor({ contentBefore: "ab
[
d]ef
", stepFunction: deleteForward, contentAfter: "ab
[]ef
", }); }); });