import { describe, expect, test } from "@odoo/hoot"; import { setupEditor, testEditor } from "../_helpers/editor"; import { unformat } from "../_helpers/format"; import { microTick, press } from "@odoo/hoot-dom"; import { animationFrame, tick } from "@odoo/hoot-mock"; import { deleteBackward, insertText, tripleClick, undo } from "../_helpers/user_actions"; import { getContent } from "../_helpers/selection"; /** * content of the "deleteBackward" sub suite in editor.test.js */ 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

and keeping the // cursor at the right place. await testEditor({ contentBefore: "

[]

", stepFunction: deleteBackward, 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: '

[
]

', // stepFunction: deleteBackward, // // The
is there only to make the

visible. // // It does not exist in VDocument and selecting it // // has no meaning in the DOM. // contentAfter: '

[]

', // }); await testEditor({ contentBefore: "

[]abc

", stepFunction: deleteBackward, contentAfter: "

[]abc

", }); }); test("should delete the first character in a paragraph", async () => { await testEditor({ contentBefore: "

a[]bc

", stepFunction: deleteBackward, contentAfter: "

[]bc

", }); }); test("should delete a character within a paragraph", async () => { await testEditor({ contentBefore: "

ab[]c

", stepFunction: deleteBackward, contentAfter: "

a[]c

", }); }); test("should delete the last character in a paragraph", async () => { await testEditor({ contentBefore: "

abc[]

", stepFunction: deleteBackward, contentAfter: "

ab[]

", }); await testEditor({ contentBefore: "

ab c[]

", stepFunction: deleteBackward, // 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: deleteBackward, contentAfter: "

[]abc

", }); }); test("should merge node correctly", async () => { await testEditor({ contentBefore: '
ab

[]c

d
', stepFunction: deleteBackward, contentAfter: '
ab[]c
d
', }); }); test("should ignore ZWS", async () => { await testEditor({ contentBefore: "

ab\u200B[]c

", stepFunction: deleteBackward, contentAfter: "

a[]c

", }); }); test("should keep inline block", async () => { await testEditor({ contentBefore: "

ab


c[]
", stepFunction: deleteBackward, contentAfterEdit: '

ab


[]\u200B
', contentAfter: "

ab



[]
", }); await testEditor({ contentBefore: '

uv


w[]
', stepFunction: deleteBackward, contentAfterEdit: '

uv


[]\u200B
', contentAfter: '

uv


[]\u200B
', }); await testEditor({ contentBefore: '

cd


e[]
', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfterEdit: '

cd


x[]
', contentAfter: '

cd


x[]
', }); }); test("should keep inline block and then undo (1)", async () => { await testEditor({ contentBefore: "
abc[]de
", stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); undo(editor); }, contentAfterEdit: '
ab[]\u200Bde
', contentAfter: "
ab[]de
", }); }); test("should keep inline block and then undo (2)", async () => { await testEditor({ contentBefore: "
abc[]de
", stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); undo(editor); undo(editor); }, contentAfterEdit: "
abc[]de
", contentAfter: "
abc[]de
", }); }); test("should delete through ZWS and Empty Inline", async () => { await testEditor({ contentBefore: '

abcd[]e

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

a[]e

", }); }); test("ZWS: should delete element content but keep cursor in", async () => { await testEditor({ contentBefore: '

uvw[]xy

', stepFunction: async (editor) => { deleteBackward(editor); }, contentAfterEdit: '

uv[]\u200Bxy

', contentAfter: "

uv[]xy

", }); await testEditor({ contentBefore: '

uvw[]xy

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "i"); }, contentAfterEdit: '

uvi[]xy

', contentAfter: '

uvi[]xy

', }); await testEditor({ contentBefore: '

abcd[]ef

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfterEdit: '

ab[]\u200Bef

', contentAfter: '

ab[]\u200Bef

', }); await testEditor({ contentBefore: '

abcd[]ef

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); await insertText(editor, "x"); }, contentAfterEdit: '

abx[]ef

', contentAfter: '

abx[]ef

', }); }); test("should ignore ZWS and merge (1)", async () => { await testEditor({ contentBefore: '

ab[]\u200B

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfter: "

ax[]

", }); await testEditor({ contentBefore: '

cd[]\u200B

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfter: '

cx[]

', }); await testEditor({ contentBefore: '

ef
[]\u200B

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfter: "

efx[]

", }); }); test("should ignore ZWS and merge (2)", async () => { await testEditor({ contentBefore: '

ab

[]\u200B
', stepFunction: deleteBackward, contentAfter: "

ab[]

", }); await testEditor({ contentBefore: '

cd


[]\u200B
', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfter: "

cd

x[]
", }); }); test("should not remove empty Bootstrap column", async () => { await testEditor({ contentBefore: '

ABC

X[]
', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: '

ABC

[]
', }); await testEditor({ contentBefore: '

ABC

X[]
', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: '

ABC

[]
', }); await testEditor({ contentBefore: '

ABC

X[]
', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: '

ABC

[]
', }); }); test("should merge the following inline text node", async () => { await testEditor({ contentBefore: "

abc

[]def
", stepFunction: deleteBackward, contentAfter: "

abc[]def

", }); await testEditor({ contentBefore: "

abc

[]def

ghi

", stepFunction: deleteBackward, contentAfter: "

abc[]def

ghi

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

abc

[]def

', stepFunction: deleteBackward, contentAfter: "

abc[]def

", }); await testEditor({ contentBefore: '

abc

[]def

ghi

', stepFunction: deleteBackward, contentAfter: "

abc[]def

ghi

", }); }); test("should delete starting white space and merge paragraphs", async () => { await testEditor({ contentBefore: `

mollis.

\n []Pelentesque

`, stepFunction: deleteBackward, contentAfter: `

mollis.[]Pelentesque

`, }); }); test('should remove contenteditable="false"', async () => { await testEditor({ contentBefore: `
abc[]def
`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `
[]def
`, }); }); test('should remove contenteditable="false" at the beggining of a P', async () => { await testEditor({ contentBefore: `

abc

def

[]ghi

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc

[]ghi

`, }); }); test("should remove a fontawesome", async () => { await testEditor({ contentBefore: `

abc[]def

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc[]def

`, }); }); test("should unwrap a block next to an inline sibling element", async () => { await testEditor({ contentBefore: `

abc

xyz

[]def

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc

xyz[]def
`, }); }); test("should unwrap a block next to an inline unbreakable element", async () => { await testEditor({ contentBefore: `

abc

[]def

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc

[]def
`, }); }); test("should remove an inline unbreakable contenteditable='false' sibling element", async () => { await testEditor({ contentBefore: `

abc

[]def
`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc

[]def
`, }); }); test("should not remove an inline contenteditable='false' in a previous sibling", async () => { await testEditor({ contentBefore: `

abc

[]

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc[]

`, }); }); test("should not remove a non editable sibling (inline)", async () => { await testEditor({ contentBefore: unformat(`
a

[]

`), stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: unformat(`
a

[]

`), }); }); test("should not remove a non editable sibling (block)", async () => { await testEditor({ contentBefore: unformat(`
aa

[]

`), stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: unformat(`
aa

[]

`), }); }); test("should remove a hr", async () => { await testEditor({ contentBefore: `

abc


[]def

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

abc

[]def

`, }); }); test("should merge paragraph with previous one containing a media element", async () => { await testEditor({ contentBefore: `

abc

[]def

`, stepFunction: deleteBackward, contentAfterEdit: `

abc

[]def

`, contentAfter: `

abc

[]def

`, }); }); test("should remove a media element inside a p", async () => { await testEditor({ contentBefore: `

abc

[]def

`, stepFunction: deleteBackward, contentAfter: `

abc

[]def

`, }); }); test("should remove a link to uploaded document", async () => { await testEditor({ contentBefore: `

abc[]

`, stepFunction: deleteBackward, contentAfter: `

abc[]

`, }); }); test("should remove a link to uploaded document at the beginning of the editable", async () => { await testEditor({ contentBefore: `

[]

`, stepFunction: deleteBackward, contentAfter: `

[]

`, }); }); test.todo("should not delete in contenteditable=false", async () => { await testEditor({ contentBefore: `

ab[]cdef

`, stepFunction: deleteBackward, contentAfter: `

ab[]cdef

`, }); }); test("should merge p elements inside conteneditbale=true inside contenteditable=false", async () => { await testEditor({ contentBefore: `

abc

[]def

`, stepFunction: deleteBackward, contentAfter: `

abc[]def

`, }); }); test("should not remove preceding character with U+0020 whitespace", async () => { await testEditor({ contentBefore: `

abcd\u0020[]

`, stepFunction: deleteBackward, contentAfter: `

abcd[]

`, }); }); }); describe("Line breaks", () => { describe("Single", () => { test("should delete a leading line break", async () => { await testEditor({ contentBefore: "


[]abc

", stepFunction: deleteBackward, contentAfter: "

[]abc

", }); await testEditor({ contentBefore: "


[] abc

", stepFunction: deleteBackward, // The space after the
is expected to be parsed // away, like it is in the DOM. contentAfter: "

[]abc

", }); }); test("should delete a line break within a paragraph", async () => { await testEditor({ contentBefore: "

ab
[]cd

", stepFunction: deleteBackward, contentAfter: "

ab[]cd

", }); await testEditor({ contentBefore: "

ab
[]cd

", stepFunction: deleteBackward, contentAfter: "

ab []cd

", }); await testEditor({ contentBefore: "

ab
[] cd

", stepFunction: deleteBackward, // The space after the
is expected to be parsed // away, like it is in the DOM. contentAfter: "

ab[]cd

", }); }); test("should delete a trailing line break", async () => { await testEditor({ contentBefore: "

abc

[]

", stepFunction: deleteBackward, contentAfter: "

abc[]

", }); await testEditor({ contentBefore: "

abc
[]

", stepFunction: deleteBackward, contentAfter: "

abc[]

", }); await testEditor({ contentBefore: "

abc

[]

", stepFunction: deleteBackward, contentAfter: "

abc []

", }); }); test("should delete a character and a line break, emptying a paragraph", async () => { await testEditor({ contentBefore: "

aaa


a[]

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

aaa

[]

", }); }); test("should delete a character after a trailing line break", async () => { await testEditor({ contentBefore: "

ab
c[]

", stepFunction: deleteBackward, // A new
should be insterted, to make the first one // visible. contentAfter: "

ab
[]

", }); }); }); describe("Consecutive", () => { test("should merge a paragraph with 4
into a paragraph with text", async () => { // 1 await testEditor({ contentBefore: "

ab

[]



cd

", stepFunction: deleteBackward, contentAfter: "

ab[]



cd

", }); }); test("should delete a line break (1)", async () => { // 2-1 await testEditor({ contentBefore: "

ab


[]


cd

", stepFunction: deleteBackward, contentAfter: "

ab

[]


cd

", }); }); test("should delete a line break, then merge a paragraph with 3
into a paragraph with text", async () => { // 2-2 await testEditor({ contentBefore: "

ab


[]


cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab[]


cd

", }); }); test("should delete a line break (2)", async () => { // 3-1 await testEditor({ contentBefore: "

ab



[]

cd

", stepFunction: deleteBackward, contentAfter: "

ab


[]

cd

", }); }); test("should delete two line breaks (3)", async () => { // 3-2 await testEditor({ contentBefore: "

ab



[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab

[]

cd

", }); }); test("should delete two line breaks, then merge a paragraph with 3
into a paragraph with text", async () => { // 3-3 await testEditor({ contentBefore: "

ab



[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab[]

cd

", }); }); test("should delete a line break when several", async () => { // 4-1 await testEditor({ contentBefore: "

ab




[]

cd

", stepFunction: deleteBackward, // A trailing line break is rendered as two
. contentAfter: "

ab



[]

cd

", }); // 5-1 await testEditor({ contentBefore: "

ab





[]

cd

", stepFunction: deleteBackward, // This should be identical to 4-1 contentAfter: "

ab



[]

cd

", }); }); test("should delete two line breaks", async () => { // 4-2 await testEditor({ contentBefore: "

ab




[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, // A trailing line break is rendered as two
. contentAfter: "

ab


[]

cd

", }); // 5-2 await testEditor({ contentBefore: "

ab





[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, // This should be identical to 4-2 contentAfter: "

ab


[]

cd

", }); }); test("should delete three line breaks (emptying a paragraph)", async () => { // 4-3 await testEditor({ contentBefore: "

ab




[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab

[]

cd

", }); // 5-3 await testEditor({ contentBefore: "

ab





[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, // This should be identical to 4-3 contentAfter: "

ab

[]

cd

", }); }); test("should delete three line breaks, then merge an empty parargaph into a paragraph with text", async () => { // 4-4 await testEditor({ contentBefore: "

ab




[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, // This should be identical to 4-4 contentAfter: "

ab[]

cd

", }); // 5-4 await testEditor({ contentBefore: "

ab





[]

cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab[]

cd

", }); }); test("should merge a paragraph into a paragraph with 4
", async () => { // 6-1 await testEditor({ contentBefore: "

ab





[]cd

", stepFunction: deleteBackward, contentAfter: "

ab




[]cd

", }); }); test("should merge a paragraph into a paragraph with 4
, then delete a trailing line break", async () => { // 6-2 await testEditor({ contentBefore: "

ab





[]cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab



[]cd

", }); }); test("should merge a paragraph into a paragraph with 4
, then delete two line breaks", async () => { // 6-3 await testEditor({ contentBefore: "

ab





[]cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab


[]cd

", }); }); test("should merge a paragraph into a paragraph with 4
, then delete three line breaks", async () => { // 6-4 await testEditor({ contentBefore: "

ab





[]cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab

[]cd

", }); }); test("should merge a paragraph into a paragraph with 4
, then delete three line breaks, then merge two paragraphs with text", async () => { // 6-5 await testEditor({ contentBefore: "

ab





[]cd

", stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab[]cd

", }); }); }); }); describe("Pre", () => { test("should delete a character in a pre", async () => { await testEditor({ contentBefore: "
ab[]cd
", stepFunction: deleteBackward, contentAfter: "
a[]cd
", }); }); test("should delete a character in a pre (space before)", async () => { await testEditor({ contentBefore: "
     ab[]cd
", stepFunction: deleteBackward, contentAfter: "
     a[]cd
", }); }); test("should delete a character in a pre (space after)", async () => { await testEditor({ contentBefore: "
ab[]cd     
", stepFunction: deleteBackward, contentAfter: "
a[]cd     
", }); }); test("should delete a character in a pre (space before and after)", async () => { await testEditor({ contentBefore: "
     ab[]cd     
", stepFunction: deleteBackward, contentAfter: "
     a[]cd     
", }); }); test("should delete a space in a pre", async () => { await testEditor({ contentBefore: "
   []  ab
", stepFunction: deleteBackward, contentAfter: "
  []  ab
", }); }); test("should delete a newline in a pre", async () => { await testEditor({ contentBefore: "
ab\n[]cd
", stepFunction: deleteBackward, contentAfter: "
ab[]cd
", }); }); test("should delete all leading space in a pre", async () => { await testEditor({ contentBefore: "
     []ab
", stepFunction: async (BasicEditor) => { deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); }, contentAfter: "
[]ab
", }); }); test("should delete all trailing space in a pre", async () => { await testEditor({ contentBefore: "
ab     []
", stepFunction: async (BasicEditor) => { deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); deleteBackward(BasicEditor); }, contentAfter: "
ab[]
", }); }); }); describe("Formats", () => { test("should delete a character before a format node", async () => { await testEditor({ contentBefore: "

abc[]def

", stepFunction: deleteBackward, // The selection is normalized so we only have one way // to represent a position. contentAfter: "

ab[]def

", }); await testEditor({ contentBefore: "

abc[]def

", stepFunction: deleteBackward, contentAfter: "

ab[]def

", }); }); }); describe("Nested Elements", () => { test("should delete a h1 inside a td immediately after insertion", async () => { await testEditor({ contentBefore: "
[]








", stepFunction: async (editor) => { await insertText(editor, "/"); await insertText(editor, "Heading"); await animationFrame(); await press("Enter"); deleteBackward(editor); }, contentAfter: "

[]









", }); }); test("should delete a h1 inside a nested list immediately after insertion", async () => { await testEditor({ contentBefore: '', stepFunction: async (editor) => { await insertText(editor, "/"); await insertText(editor, "Heading"); await animationFrame(); await press("Enter"); deleteBackward(editor); deleteBackward(editor); }, contentAfter: "", }); }); }); describe("Merging different types of elements", () => { test("should merge a paragraph with text into a paragraph with text", async () => { await testEditor({ contentBefore: "

ab

[]cd

", stepFunction: deleteBackward, contentAfter: "

ab[]cd

", }); }); test("should merge a paragraph with formated text into a paragraph with text", async () => { await testEditor({ contentBefore: "

aa

[]abbb

", stepFunction: deleteBackward, contentAfter: "

aa[]abbb

", }); }); test("should merge a paragraph with text into a heading1 with text", async () => { await testEditor({ contentBefore: "

ab

[]cd

", stepFunction: deleteBackward, contentAfter: "

ab[]cd

", }); }); test("should merge an empty paragraph into a heading1 with text", async () => { await testEditor({ contentBefore: "

ab

[]

", stepFunction: deleteBackward, contentAfter: "

ab[]

", }); await testEditor({ contentBefore: "

ab


[]

", stepFunction: deleteBackward, contentAfter: "

ab[]

", }); }); test("should remove empty paragraph (keeping the heading)", async () => { await testEditor({ contentBefore: "


[]ab

", stepFunction: deleteBackward, contentAfter: "

[]ab

", }); }); test("should merge a text preceding a paragraph (removing the paragraph)", async () => { await testEditor({ contentBefore: "
ab

[]cd

", stepFunction: deleteBackward, contentAfter: "
ab[]cd
", }); await testEditor({ contentBefore: "
ab

[]cd

ef
", stepFunction: deleteBackward, contentAfter: "
ab[]cd
ef
", }); }); }); describe("With attributes", () => { test("should remove paragraph with class", async () => { await testEditor({ contentBefore: '


[]abc

', stepFunction: deleteBackward, contentAfter: "

[]abc

", }); }); test("should merge two paragraphs with spans of same classes", async () => { await testEditor({ contentBefore: '

ab

[]cd

', stepFunction: deleteBackward, contentAfter: '

ab[]cd

', }); }); test("should merge two paragraphs with spans of different classes without merging the spans", async () => { await testEditor({ contentBefore: '

ab

[]cd

', stepFunction: deleteBackward, contentAfter: '

ab[]cd

', }); }); test("should merge two paragraphs of different classes, each containing spans of the same class", async () => { await testEditor({ contentBefore: '

ab

[]cd

', stepFunction: deleteBackward, 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: deleteBackward, contentAfter: '

ab[]cd

', }); }); test("should delete a line break between two spans with bold and merge these formats", async () => { await testEditor({ contentBefore: '

ab
[]cd

', stepFunction: deleteBackward, contentAfter: '

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: '

ab

c[]de

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: '

ab
[]de

', }); }); }); describe("Nested editable zone (inside contenteditable=false element)", () => { test("should not remove the uneditable nesting zone nor the editable nested zone if the last element of the nested zone is empty", async () => { await testEditor({ contentBefore: unformat(`

[]

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

[]

`), }); }); test("should not remove the uneditable nesting zone nor the editable nested zone even if there is a paragraph after", async () => { await testEditor({ contentBefore: unformat(`

[]

content

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

[]

content

`), }); }); test("should not remove the uneditable nesting zone nor the editable nested zone if the last element of the nested zone is not empty", async () => { await testEditor({ contentBefore: unformat(`

[]content

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

[]content

`), }); }); test("should remove the uneditable nesting zone from the outside", async () => { await testEditor({ contentBefore: unformat(`

content

[]content

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

[]content

`), }); }); }); describe("POC extra tests", () => { test("should delete an unique space between letters", async () => { await testEditor({ contentBefore: "

ab []cd

", stepFunction: deleteBackward, contentAfter: "

ab[]cd

", }); }); test("should delete the first character in a paragraph (2)", async () => { await testEditor({ contentBefore: "

a[] bc

", stepFunction: deleteBackward, contentAfter: "

[] bc

", }); }); test("should delete a space", async () => { await testEditor({ contentBefore: "

ab [] de

", stepFunction: deleteBackward, contentAfter: "

ab[]de

", }); }); test("should delete a one letter word followed by visible space (start of block)", async () => { await testEditor({ contentBefore: "

a[] b

", stepFunction: deleteBackward, contentAfter: "

[] b

", }); await testEditor({ contentBefore: "

[a] b

", stepFunction: deleteBackward, contentAfter: "

[] b

", }); }); test("should delete a one letter word surrounded by visible space", async () => { await testEditor({ contentBefore: "

ab c[] de

", stepFunction: deleteBackward, contentAfter: "

ab [] de

", }); await testEditor({ contentBefore: "

ab [c] de

", stepFunction: deleteBackward, contentAfter: "

ab [] de

", }); }); test("should delete a one letter word preceded by visible space (end of block)", async () => { await testEditor({ contentBefore: "

a b[]

", stepFunction: deleteBackward, contentAfter: "

a []

", }); await testEditor({ contentBefore: "

a [b]

", stepFunction: deleteBackward, contentAfter: "

a []

", }); }); test("should delete an empty paragraph in a table cell", async () => await testEditor({ contentBefore: "

a

[]

", stepFunction: deleteBackward, contentAfter: "

a[]

", })); test("should fill empty block with a
", async () => { await testEditor({ contentBefore: "

a[]

", stepFunction: deleteBackward, contentAfter: "

[]

", }); await testEditor({ contentBefore: "

[]

", stepFunction: deleteBackward, contentAfter: "

[]

", }); }); test("should merge a paragraph with text into a paragraph with text removing spaces", async () => { await testEditor({ contentBefore: "

ab

[]cd

", stepFunction: deleteBackward, // This is a tricky case: the spaces after ab are // visible on Firefox but not on Chrome... to be // consistent we enforce the space removal here but // maybe not a good idea... see next case -> contentAfter: "

ab[]cd

", }); await testEditor({ contentBefore: "

ab

[]cd

", stepFunction: deleteBackward, // This is the same visible case as the one above. The // difference is that here the space after ab is visible // on both Firefox and Chrome, so it should stay // visible. contentAfter: "

ab []cd

", }); }); test("should remove a br and remove following spaces", async () => { await testEditor({ contentBefore: "

ab
[] cd

", stepFunction: deleteBackward, contentAfter: "

ab[]cd

", }); await testEditor({ contentBefore: "

ab
[] xcd

", stepFunction: deleteBackward, contentAfter: "

ab[]xcd

", }); }); test("should ignore empty inline node between blocks being merged", async () => { await testEditor({ contentBefore: "

abc

[]def

", stepFunction: deleteBackward, contentAfter: "

abc[]def

", }); }); test("should merge in nested paragraphs and remove invisible inline content", async () => { await testEditor({ contentBefore: '

ab

[]c

', stepFunction: deleteBackward, contentAfter: '

ab[]c

', }); await testEditor({ contentBefore: '

ab

[]c

', stepFunction: deleteBackward, contentAfter: '

ab[]c

', }); }); test("should not merge in nested blocks if inline content afterwards", async () => { await testEditor({ contentBefore: '

ab

de

[]fg

', stepFunction: deleteBackward, contentAfter: '

ab

de[]fg
', }); await testEditor({ contentBefore: '

ab

[]fg

', stepFunction: deleteBackward, contentAfter: '

ab

[]fg
', }); }); test("should move paragraph content to empty block", async () => { await testEditor({ contentBefore: "

abc


[]def

", stepFunction: deleteBackward, contentAfter: "

abc

[]def

", }); }); test("should remove only one br between contents", async () => { await testEditor({ contentBefore: "

abc
[]
def

", stepFunction: deleteBackward, contentAfter: "

abc[]
def

", }); }); test("should remove an empty block instead of merging it", async () => { await testEditor({ contentBefore: "


[]

", stepFunction: deleteBackward, contentAfter: "

[]

", }); }); test("should not remove a table without selecting it", async () => { await testEditor({ contentBefore: unformat( `

ab

cdef
ghij

[]kl

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

ab

cdef
ghij

[]kl

` ), }); }); test("should not merge a table into its previous sibling", async () => { await testEditor({ contentBefore: unformat( `

ab

[]cdef
ghij

kl

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

ab

[]cdef
ghij

kl

` ), }); }); test("should delete an image that is displayed as a block", async () => { await testEditor({ // @phoenix content adapted to make it valid html contentBefore: unformat(`
a[bc]d
`), stepFunction: deleteBackward, contentAfter: unformat(`
a[]d
`), }); }); }); }); describe("Selection not collapsed", () => { test("ZWS : should keep inline block", async () => { await testEditor({ contentBefore: '

ab [c] d

', stepFunction: async (editor) => { deleteBackward(editor); }, contentAfterEdit: '

ab []\u200B d

', contentAfter: '

ab []\u200B d

', }); await testEditor({ contentBefore: '

ab [c] d

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfterEdit: '

ab x[] d

', contentAfter: '

ab x[] d

', }); await testEditor({ contentBefore: "

ab [c] d

", stepFunction: async (editor) => { deleteBackward(editor); }, contentAfterEdit: '

ab []\u200B d

', contentAfter: "

ab [] d

", }); await testEditor({ contentBefore: '

ab[c]d

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfterEdit: '

abx[]d

', contentAfter: '

abx[]d

', }); await testEditor({ contentBefore: '

ab [cde] f

', stepFunction: async (editor) => { deleteBackward(editor); await insertText(editor, "x"); }, contentAfterEdit: '

ab x[] f

', contentAfter: '

ab x[] f

', }); }); test("should merge node correctly (1)", async () => { await testEditor({ contentBefore: '
ab[c

d]e

f
g
', stepFunction: deleteBackward, // FIXME ?? : Maybe this should bing the content inside the

// Instead of removing the

, // ex :

ab[]e

f
g
contentAfter: '
ab[]e
f
g
', }); }); test("should merge node correctly (2)", async () => { await testEditor({ contentBefore: '
a

b[c

d]ef

xxx

', stepFunction: deleteBackward, contentAfter: '
a

b[]ef

xxx

', }); }); test("should delete part of the text within a paragraph (backward, forward selection)", async () => { // Forward selection await testEditor({ contentBefore: "

ab[cd]ef

", stepFunction: deleteBackward, contentAfter: "

ab[]ef

", }); }); test("should delete part of the text within a paragraph (backward, backward selection)", async () => { // Backward selection await testEditor({ contentBefore: "

ab]cd[ef

", stepFunction: deleteBackward, contentAfter: "

ab[]ef

", }); }); test("should delete across two paragraphs", async () => { // Forward selection await testEditor({ contentBefore: "

ab[cd

ef]gh

", stepFunction: deleteBackward, contentAfter: "

ab[]gh

", }); // Backward selection await testEditor({ contentBefore: "

ab]cd

ef[gh

", stepFunction: deleteBackward, contentAfter: "

ab[]gh

", }); }); test("should delete part of the text across two paragraphs (backward, forward selection)", async () => { await testEditor({ contentBefore: "
a

b[c

d]e

f
", stepFunction: deleteBackward, contentAfter: "
a

b[]e

f
", }); }); test("should delete part of the text across two paragraphs (backward, backward selection)", async () => { await testEditor({ contentBefore: "
a

b]c

d[e

f
", stepFunction: deleteBackward, contentAfter: "
a

b[]e

f
", }); }); test("should delete all the text in a paragraph", async () => { // Forward selection await testEditor({ contentBefore: "

[abc]

", stepFunction: deleteBackward, contentAfter: "

[]

", }); // Backward selection await testEditor({ contentBefore: "

]abc[

", stepFunction: deleteBackward, contentAfter: "

[]

", }); }); test("should delete a complex selection accross format nodes and multiple paragraphs", async () => { // Forward selection await testEditor({ contentBefore: "

ab[cd

ef
gh
ijkl]mn

", stepFunction: deleteBackward, contentAfter: "

ab[]mn

", }); await testEditor({ contentBefore: "

ab[cd

ef
gh
ijk]lmn

", stepFunction: deleteBackward, contentAfter: "

ab[]lmn

", }); // Backward selection await testEditor({ contentBefore: "

ab]cd

ef
gh
ijkl[mn

", stepFunction: deleteBackward, contentAfter: "

ab[]mn

", }); await testEditor({ contentBefore: "

ab]cd

ef
gh
ijk[lmn

", stepFunction: deleteBackward, contentAfter: "

ab[]lmn

", }); }); // test("should delete all contents of a complex DOM with format nodes and multiple paragraphs (backward, forward selection)", async () => { await testEditor({ contentBefore: "

[abcd

ef
gh
ijklmn]

", stepFunction: deleteBackward, contentAfter: "

[]

", }); }); test("should delete all contents of a complex DOM with format nodes and multiple paragraphs (backward, backward selection)", async () => { await testEditor({ contentBefore: "

]abcd

ef
gh
ijklmn[

", stepFunction: deleteBackward, contentAfter: "

[]

", }); }); test("should delete a selection accross a heading1 and a paragraph", async () => { // Forward selection await testEditor({ contentBefore: "

ab [cd

ef]gh

", stepFunction: deleteBackward, contentAfter: "

ab []gh

", }); // Backward selection await testEditor({ contentBefore: "

ab ]cd

ef[gh

", stepFunction: deleteBackward, contentAfter: "

ab []gh

", }); }); test("should delete a selection from the beginning of a heading1 with a format to the middle of a paragraph", async () => { // Forward selection await testEditor({ contentBefore: "

[abcd

ef]gh1

", stepFunction: deleteBackward, contentAfter: "

[]gh1

", }); await testEditor({ contentBefore: "

[abcd

ef]gh2

", stepFunction: deleteBackward, contentAfter: "

[]gh2

", }); // Backward selection await testEditor({ contentBefore: "

]abcd

ef[gh3

", stepFunction: deleteBackward, contentAfter: "

[]gh3

", }); await testEditor({ contentBefore: "

]abcd

ef[gh4

", stepFunction: deleteBackward, contentAfter: "

[]gh4

", }); }); test("should delete a heading (triple click backspace) (1)", async () => { const { editor, el } = await setupEditor("

abc

def

", {}); tripleClick(el.querySelector("h1")); await microTick(); // Chrome puts the cursor at the start of next sibling expect(getContent(el)).toBe("

[abc

]def

"); await tick(); // The Editor corrects it on selection change expect(getContent(el)).toBe("

[abc]

def

"); tripleClick(el.querySelector("h1")); await microTick(); // Chrome puts the cursor at the start of next sibling expect(getContent(el)).toBe("

[abc

]def

"); await tick(); // The Editor corrects it repeatedly on selection change expect(getContent(el)).toBe("

[abc]

def

"); deleteBackward(editor); expect(getContent(el)).toBe( '

[]

def

' ); }); test("should delete a heading (triple click backspace) (2)", async () => { const { editor, el } = await setupEditor("

abc


def

", {}); tripleClick(el.querySelector("h1")); await microTick(); // Chrome puts the cursor at the start of next sibling expect(getContent(el)).toBe("

[abc

]

def

"); await tick(); // The Editor corrects it on selection change expect(getContent(el)).toBe("

[abc]


def

"); deleteBackward(editor); expect(getContent(el)).toBe( '

[]


def

' ); }); test("should delete last character of paragraph and merge the two p elements", async () => { await testEditor({ contentBefore: "

ab[c

]def

", stepFunction: deleteBackward, contentAfter: "

ab[]def

", }); await testEditor({ contentBefore: "

ab[c

]

def

", stepFunction: deleteBackward, contentAfter: "

ab[]

def

", }); }); test("should delete first character of paragraph, as well as selected paragraph break", async () => { await testEditor({ contentBefore: "

abc[

d]ef

", stepFunction: deleteBackward, contentAfter: "

abc[]ef

", }); }); test("should remove a fully selected table", async () => { await testEditor({ contentBefore: unformat( `

a[b

cdef
ghij

k]l

` ), stepFunction: deleteBackward, contentAfter: "

a[]l

", }); }); test("should remove a fully selected nested table", async () => { await testEditor({ contentBefore: unformat( `

a[b





ef
ghij

k]l

` ), stepFunction: deleteBackward, contentAfter: "

a[]l

", }); }); test("should delete nothing when in an empty table cell", async () => { await testEditor({ contentBefore: "
abc[]
abc
", stepFunction: deleteBackward, contentAfter: "
abc[]
abc
", }); }); test("should delete nothing when in an empty paragraph in a table cell", async () => { await testEditor({ contentBefore: "
abc

[]

", stepFunction: deleteBackward, contentAfter: "
abc

[]

", }); }); test("should only remove the text content of cells in a partly selected table", async () => { await testEditor({ contentBefore: unformat( `
cde[fgh
ijk]lmn
opqrst
` ), stepFunction: deleteBackward, contentAfter: unformat( `
cd[]
gh
ij
mn
opqrst
` ), }); }); test("should remove a row in a partly selected table", async () => { await testEditor({ contentBefore: unformat( `
[abcd]
efgh
` ), stepFunction: deleteBackward, contentAfter: unformat( `
[]efgh
` ), }); }); test("should remove a column in a partly selected table", async () => { await testEditor({ contentBefore: unformat( `
[ab cd
ef] gh
` ), stepFunction: deleteBackward, contentAfter: unformat( `
[]cd
gh
` ), }); }); test("should remove some text and a table (even if the table is partly selected)", async () => { await testEditor({ contentBefore: unformat( `

a[b

cdef
g]hij

kl

` ), stepFunction: deleteBackward, 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

cdef
ghi[j

k]l

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

ab

[]l

` ), }); }); test("should remove some text, a table and some more text", async () => { await testEditor({ contentBefore: unformat( `

a[b

cdef
ghij

k]l

` ), stepFunction: deleteBackward, contentAfter: `

a[]l

`, }); }); test("should remove a selection of several tables", async () => { await testEditor({ contentBefore: unformat( `
cde[f
ghij
cdef
ghij
cde]f
ghij
` ), stepFunction: deleteBackward, contentAfter: `

[]

`, }); }); test("should remove a selection including several tables", async () => { await testEditor({ contentBefore: unformat( `

0[1

cdef
ghij

23

cdef
ghij

45

cdef
ghij

67]

` ), stepFunction: deleteBackward, contentAfter: `

0[]

`, }); }); test("should remove everything, including several tables", async () => { await testEditor({ contentBefore: unformat( `

[01

cdef
ghij

23

cdef
ghij

45

cdef
ghij

67]

` ), stepFunction: deleteBackward, contentAfter: `

[]

`, }); }); test("should do nothing with selection before table and start of middle cell", async () => { await testEditor({ contentBefore: unformat( `[



]
` ), stepFunction: deleteBackward, contentAfter: unformat( `
[]



` ), }); }); test("should empty an inline unremovable but remain in it", async () => { await testEditor({ contentBefore: '

ab[cd]ef

', stepFunction: deleteBackward, contentAfter: '

ab[]\u200Bef

', }); }); test("should delete if first element and append in paragraph", async () => { await testEditor({ contentBefore: `

[]
`, stepFunction: deleteBackward, contentAfter: `

[]

`, }); await testEditor({ contentBefore: `


[]

`, stepFunction: deleteBackward, contentAfter: `

[]

`, }); await testEditor({ contentBefore: `


[]

`, stepFunction: deleteBackward, contentAfter: `

[]

`, }); }); test("should not delete the block and appends a paragraph if the element has textContent ", async () => { await testEditor({ contentBefore: `

[]abc

`, stepFunction: deleteBackward, contentAfter: `

[]abc

`, }); await testEditor({ contentBefore: `

[]abc

`, stepFunction: deleteBackward, contentAfter: `

[]abc

`, }); await testEditor({ contentBefore: `

[]ab

cdef



`, stepFunction: deleteBackward, contentAfter: `

[]ab

cdef



`, }); }); test("should not delete styling nodes if not selected", async () => { // deleteBackward selection await testEditor({ contentBefore: '

a[bcde]f

', stepFunction: deleteBackward, contentAfter: '

a[]\u200Bf

', }); }); test("should delete styling nodes when delete if empty with space around inline (backward)", async () => { // deleteBackward selection await testEditor({ contentBefore: '

ab [cd] ef

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

ab[] ef

", }); }); test("should delete styling nodes when delete if empty (backward)", async () => { await testEditor({ contentBefore: '

uv[wx]yz

', stepFunction: async (editor) => { deleteBackward(editor); deleteBackward(editor); }, contentAfter: "

u[]yz

", }); }); test("should transform the last space of a container to an   after removing the last word through deleteRange", async () => { await testEditor({ contentBefore: `

a [b]

`, stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: `

a []

`, }); }); describe("Nested editable zone (inside contenteditable=false element)", () => { 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) => { deleteBackward(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) => { deleteBackward(editor); }, contentAfter: unformat(`

before[]after

`), }); }); 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) => { deleteBackward(editor); }, contentAfter: unformat(`

before[]after

`), }); }); test("should remove empty paragraph and content from the second one", async () => { await testEditor({ contentBefore: "

ab

[

d]ef

", stepFunction: deleteBackward, contentAfter: "

ab

[]ef

", }); }); test.todo("should not delete in contenteditable=false 1", async () => { await testEditor({ contentBefore: `

ab[cd]ef

`, stepFunction: deleteBackward, contentAfter: `

ab[cd]ef

`, }); }); test.todo("should not delete in contenteditable=false 2", async () => { await testEditor({ contentBefore: `

a[b

cd

e]f

`, stepFunction: deleteBackward, contentAfter: `

a[b

cd

e]f

`, }); }); test("should fill the inner editable with a P when all of its contents are removed", async () => { await testEditor({ contentBefore: unformat(`
[

abc

]
`), stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: unformat(`

[]

`), }); }); test("should fill the inner editable with a P when all of its contents are removed (2)", async () => { await testEditor({ contentBefore: unformat(`
[

abc

def

]
`), stepFunction: async (editor) => { deleteBackward(editor); }, contentAfter: unformat(`

[]

`), }); }); }); });