import { describe, expect, test } from "@odoo/hoot"; import { deleteBackward, insertText } from "../_helpers/user_actions"; import { setupEditor, testEditor } from "../_helpers/editor"; import { descendants } from "@html_editor/utils/dom_traversal"; import { tick } from "@odoo/hoot-mock"; import { getContent, setSelection } from "../_helpers/selection"; import { cleanLinkArtifacts } from "../_helpers/format"; import { animationFrame, pointerDown, pointerUp, queryOne } from "@odoo/hoot-dom"; import { dispatchNormalize } from "../_helpers/dispatch"; import { nodeSize } from "@html_editor/utils/position"; import { expectElementCount } from "../_helpers/ui_expectations"; test("should pad a link with ZWNBSPs and add visual indication", async () => { await testEditor({ contentBefore: '

abc

', contentBeforeEdit: '

a\ufeff\ufeffb\ufeff\ufeffc

', stepFunction: async (editor) => { setSelection({ anchorNode: editor.editable.querySelector("a"), anchorOffset: 1 }); await tick(); }, contentAfterEdit: '

a\ufeff\ufeff[]b\ufeff\ufeffc

', contentAfter: '

a[]bc

', }); }); test("should pad a link with ZWNBSPs and add visual indication (2)", async () => { await testEditor({ contentBefore: '

ab

', contentBeforeEdit: '

a\ufeff\ufeffb\ufeff\ufeff

', stepFunction: async (editor) => { setSelection({ anchorNode: editor.editable.querySelector("a span"), anchorOffset: 0 }); await tick(); }, contentAfterEdit: '

a\ufeff\ufeff[]b\ufeff\ufeff

', contentAfter: '

a[]b

', }); }); test("should keep link padded with ZWNBSPs after a delete", async () => { await testEditor({ contentBefore: '

ab[]c

', stepFunction: deleteBackward, contentAfterEdit: '

a\ufeff\ufeff[]\ufeff\ufeffc

', contentAfter: "

a[]c

", }); }); test("should keep isolated link after a delete and typing", async () => { await testEditor({ contentBefore: '

ab[]c

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

aabc[]c

', }); }); test("should delete the content from the link when popover is active", async () => { const { editor, el } = await setupEditor('

abc[]abc

'); await expectElementCount(".o-we-linkpopover", 1); deleteBackward(editor); deleteBackward(editor); deleteBackward(editor); const content = getContent(el); expect(content).toBe( '

\ufeff\ufeff[]abc\ufeff\ufeff

' ); expect(cleanLinkArtifacts(content)).toBe('

[]abc

'); }); describe.tags("desktop"); describe("should position the cursor outside the link", () => { test("clicking at the start of the link", async () => { const { el } = await setupEditor('

test

'); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufeff

'); const aElement = queryOne("p a"); await pointerDown(el); // Simulate the selection with mousedown setSelection({ anchorNode: aElement.childNodes[0], anchorOffset: 0 }); expect(getContent(el)).toBe('

\ufeff[]\ufefftest\ufeff\ufeff

'); await animationFrame(); // selection change await pointerUp(el); expect(getContent(el)).toBe('

[]\ufeff\ufefftest\ufeff\ufeff

'); }); test("clicking at the start of the link when format is applied on link", async () => { const { el } = await setupEditor('

test

'); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufeff

'); const aElement = queryOne("p a"); await pointerDown(el); // Simulate the selection with mousedown setSelection({ anchorNode: aElement.childNodes[0], anchorOffset: 0 }); expect(getContent(el)).toBe('

\ufeff[]\ufefftest\ufeff\ufeff

'); await animationFrame(); // selection change await pointerUp(el); expect(getContent(el)).toBe('

[]\ufeff\ufefftest\ufeff\ufeff

'); }); test("clicking at the end of the link", async () => { const { el } = await setupEditor('

test

'); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufeff

'); const aElement = queryOne("p a"); await pointerDown(el); // Simulate the selection with mousedown setSelection({ anchorNode: aElement.childNodes[2], anchorOffset: nodeSize(aElement.childNodes[2]), }); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff[]\ufeff

'); await animationFrame(); // selectionChange await pointerUp(el); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufeff[]

'); }); test("clicking before the link's text content", async () => { const { el, editor } = await setupEditor('

te[]st

'); expect(getContent(el)).toBe( '

\ufeff\ufeffte[]st\ufeff\ufeff

' ); const aElement = queryOne("p a"); await pointerDown(el); // Simulate the selection with mousedown setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: 0 }); expect(getContent(el)).toBe( '

\ufeff\ufeff[]test\ufeff\ufeff

' ); await animationFrame(); // selection change await pointerUp(el); expect(getContent(el)).toBe('

[]\ufeff\ufefftest\ufeff\ufeff

'); await insertText(editor, "link"); expect(getContent(el)).toBe('

link[]\ufeff\ufefftest\ufeff\ufeff

'); setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: 0 }); await animationFrame(); // selectionChange expect(getContent(el)).toBe( '

link\ufeff\ufeff[]test\ufeff\ufeff

' ); await insertText(editor, "content"); expect(getContent(el)).toBe( '

link\ufeff\ufeffcontent[]test\ufeff\ufeff

' ); }); test(" clicking after the link's text content", async () => { const { el, editor } = await setupEditor('

t[]est

'); expect(getContent(el)).toBe( '

\ufeff\ufefft[]est\ufeff\ufeff

' ); const aElement = queryOne("p a"); await pointerDown(el); // Simulate the selection with mousedown setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: nodeSize(aElement.childNodes[1]), }); expect(getContent(el)).toBe( '

\ufeff\ufefftest[]\ufeff\ufeff

' ); await animationFrame(); // selection change await pointerUp(el); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufeff[]

'); await insertText(editor, "link"); expect(getContent(el)).toBe('

\ufeff\ufefftest\ufeff\ufefflink[]

'); setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: nodeSize(aElement.childNodes[1]), }); await animationFrame(); // selectionChange expect(getContent(el)).toBe( '

\ufeff\ufefftest[]\ufeff\ufefflink

' ); await insertText(editor, "content"); expect(getContent(el)).toBe( '

\ufeff\ufefftestcontent[]\ufeff\ufefflink

' ); }); }); describe("should zwnbsp-pad simple text link", () => { const removeZwnbsp = (editor) => { for (const descendant of descendants(editor.editable)) { if (descendant.nodeType === Node.TEXT_NODE && descendant.textContent === "\ufeff") { descendant.remove(); } } }; test("should zwnbsp-pad simple text link (1)", async () => { await testEditor({ contentBefore: '

a[]bcd

', contentBeforeEdit: '

a[]\ufeff\ufeffbc\ufeff\ufeffd

', stepFunction: async (editor) => { removeZwnbsp(editor); const p = editor.editable.querySelector("p"); // set the selection via the parent setSelection({ anchorNode: p, anchorOffset: 1 }); // insert the zwnbsp again dispatchNormalize(editor); }, contentAfterEdit: '

a\ufeff[]\ufeffbc\ufeff\ufeffd

', }); }); test("should zwnbsp-pad simple text link (2)", async () => { await testEditor({ contentBefore: '

a[]bcd

', contentBeforeEdit: '

a\ufeff\ufeff[]bc\ufeff\ufeffd

', stepFunction: async (editor) => { removeZwnbsp(editor); const a = editor.editable.querySelector("a"); // set the selection via the parent setSelection({ anchorNode: a, anchorOffset: 0 }); await tick(); // insert the zwnbsp again dispatchNormalize(editor); }, contentAfterEdit: '

a\ufeff\ufeff[]bc\ufeff\ufeffd

', }); }); test("should zwnbsp-pad simple text link (3)", async () => { await testEditor({ contentBefore: '

ab[]d

', contentBeforeEdit: '

a\ufeff\ufeffb[]\ufeff\ufeffd

', stepFunction: async (editor) => { const a = editor.editable.querySelector("a"); // Insert an extra character as a text node so we can set // the selection between the characters while still // targetting their parent. a.appendChild(editor.document.createTextNode("c")); removeZwnbsp(editor); // set the selection via the parent setSelection({ anchorNode: a, anchorOffset: 1 }); await tick(); // insert the zwnbsp again dispatchNormalize(editor); }, contentAfterEdit: '

a\ufeff\ufeffb[]c\ufeff\ufeffd

', }); }); test("should zwnbsp-pad simple text link (4)", async () => { await testEditor({ contentBefore: '

abc[]d

', contentBeforeEdit: '

a\ufeff\ufeffbc[]\ufeff\ufeffd

', stepFunction: async (editor) => { removeZwnbsp(editor); const a = editor.editable.querySelector("a"); // set the selection via the parent setSelection({ anchorNode: a, anchorOffset: 1 }); await tick(); // insert the zwnbsp again dispatchNormalize(editor); }, contentAfterEdit: '

a\ufeff\ufeffbc[]\ufeff\ufeffd

', }); }); test("should zwnbsp-pad simple text link (5)", async () => { await testEditor({ contentBefore: '

abc[]d

', contentBeforeEdit: '

a\ufeff\ufeffbc\ufeff\ufeff[]d

', stepFunction: async (editor) => { removeZwnbsp(editor); const p = editor.editable.querySelector("p"); // set the selection via the parent setSelection({ anchorNode: p, anchorOffset: 2 }); await tick(); // insert the zwnbsp again dispatchNormalize(editor); }, contentAfterEdit: '

a\ufeff\ufeffbc\ufeff\ufeff[]d

', }); }); }); test("should not zwnbsp-pad nav-link", async () => { await testEditor({ contentBefore: '

a[]bc

', contentBeforeEdit: '

a[]bc

', }); }); test("should not zwnbsp-pad in nav", async () => { await testEditor({ contentBefore: '', contentBeforeEdit: '', }); }); test("should not zwnbsp-pad link with block fontawesome", async () => { await testEditor({ contentBefore: '

a[]b

', contentBeforeEdit: '

a\ufeff[]\u200b\ufeffb

', }); }); test("should not zwnbsp-pad link with image", async () => { await testEditor({ contentBefore: '

a[]b

', contentBeforeEdit: '

a[]b

', }); }); test("should remove zwnbsp from middle of the link", async () => { await testEditor({ contentBefore: '

content

', contentBeforeEdit: '

\ufeff\ufeffcontent\ufeff\ufeff

', stepFunction: async (editor) => { // Cursor before the FEFF text node setSelection({ anchorNode: editor.editable.querySelector("a"), anchorOffset: 0 }); await insertText(editor, "more "); }, contentAfterEdit: '

\ufeff\ufeffmore []content\ufeff\ufeff

', contentAfter: '

more []content

', }); }); test("should remove zwnbsp from middle of the link (2)", async () => { await testEditor({ contentBefore: '

content

', contentBeforeEdit: '

\ufeff\ufeffcontent\ufeff\ufeff

', stepFunction: async (editor) => { // Cursor inside the FEFF text node setSelection({ anchorNode: editor.editable.querySelector("a").firstChild, anchorOffset: 0, }); await insertText(editor, "more "); }, contentAfterEdit: '

\ufeff\ufeffmore []content\ufeff\ufeff

', contentAfter: '

more []content

', }); }); test("should zwnbps-pad links with .btn class", async () => { await testEditor({ contentBefore: '

content

', contentBeforeEdit: '

\ufeff\ufeffcontent\ufeff\ufeff

', }); }); test("should not add visual indication to a button", async () => { await testEditor({ contentBefore: '

[]content

', contentBeforeEdit: '

\ufeff\ufeffcontent\ufeff\ufeff

', }); });