import { CLIPBOARD_WHITELISTS } from "@html_editor/core/clipboard_plugin"; import { beforeEach, describe, expect, test } from "@odoo/hoot"; import { manuallyDispatchProgrammaticEvent as dispatch, press, waitFor } from "@odoo/hoot-dom"; import { animationFrame, tick } from "@odoo/hoot-mock"; import { onRpc, patchWithCleanup } from "@web/../tests/web_test_helpers"; import { setupEditor, testEditor } from "./_helpers/editor"; import { cleanLinkArtifacts, unformat } from "./_helpers/format"; import { getContent, setSelection } from "./_helpers/selection"; import { pasteHtml, pasteOdooEditorHtml, pasteText, undo } from "./_helpers/user_actions"; function isInline(node) { return ["I", "B", "U", "S", "EM", "STRONG", "IMG", "BR", "A", "FONT"].includes(node); } function toIgnore(node) { return ["TABLE", "THEAD", "TH", "TBODY", "TR", "TD", "IMG", "BR", "LI", ".fa"].includes(node); } describe("Html Paste cleaning - whitelist", () => { test("should keep whitelisted Tags tag", async () => { for (const node of CLIPBOARD_WHITELISTS.nodes) { if (!toIgnore(node)) { const html = isInline(node) ? `a<${node.toLowerCase()}>bc` : `a

<${node.toLowerCase()}>b

c`; await testEditor({ contentBefore: "

123[]4

", stepFunction: async (editor) => { pasteHtml(editor, `a<${node.toLowerCase()}>bc`); }, contentAfter: "

123" + html + "[]4

", }); } } }); test("should keep whitelisted Tags tag (2)", async () => { await testEditor({ contentBefore: "

123[]

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

123ad[]

', }); }); test("should keep tables Tags tag and add classes", async () => { await testEditor({ contentBefore: "

123[]

", stepFunction: async (editor) => { pasteHtml( editor, "a
h
b
d" ); }, contentAfter: '

123a

h
b

d[]

', }); }); test("should not keep span", async () => { await testEditor({ contentBefore: "

123[]

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

123abcd[]

", }); }); test("should not keep orphan LI", async () => { await testEditor({ contentBefore: "

123[]

", stepFunction: async (editor) => { pasteHtml(editor, "a
  • bc
  • d"); }, contentAfter: "

    123a

    bc

    d[]

    ", }); }); test("should keep LI in UL", async () => { await testEditor({ contentBefore: "

    123[]

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

    123a

    d[]

    ", }); }); test("should keep P and B and not span", async () => { await testEditor({ contentBefore: "

    123[]xx

    ", stepFunction: async (editor) => { pasteHtml(editor, "a

    bc

    defgh"); }, contentAfter: "

    123a

    bc

    defgh[]xx

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

    123[]

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

    123abcd[]

    ", }); }); test("should remove unwanted styles and b tag when pasting from paragraph from gdocs", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, `

    test1

    test2

    ` ); }, contentAfter: "

    test1

    test2[]

    ", }); }); test("should remove b, keep p, and remove unwanted styles when pasting list from gdocs", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, '' ); }, contentAfter: "", }); }); test("should remove unwanted styles and keep tags when pasting list from gdoc", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, '' ); }, contentAfter: "", }); }); }); describe("Simple text", () => { describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    x[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    abx[]cd

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

    ab[]cd

    ", stepFunction: async (editor) => { pasteText(editor, "xyz 123"); }, contentAfter: "

    abxyz 123[]cd

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

    ab[]cd

    ", stepFunction: async (editor) => { pasteText(editor, "x y"); }, contentAfter: "

    abx    y[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    abx[]cd

    ', }); }); // TODO: We might want to have it consider \n as paragraph breaks // instead of linebreaks but that would be an opinionated choice. test("should paste text and understand \\n newlines", async () => { await testEditor({ // @phoenix content adapted to make it valid html contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteText(editor, "a\nb\nc\nd"); }, contentAfter: '

    a

    ' + '

    b

    ' + '

    c

    ' + "

    d[]

    ", }); }); test("should paste text and understand \\r\\n newlines", async () => { await testEditor({ // @phoenix content adapted to make it valid html contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteText(editor, "a\r\nb\r\nc\r\nd"); }, contentAfter: '

    a

    ' + '

    b

    ' + '

    c

    ' + "

    d[]

    ", }); }); test("should paste text and understand \\n newlines within UNBREAKABLE node", async () => { await testEditor({ contentBefore: "
    []
    ", stepFunction: async (editor) => { pasteText(editor, "a\nb\nc\nd"); }, contentAfter: "
    a
    b
    c
    d[]
    ", }); }); test("should paste text and understand \\n newlines within UNBREAKABLE node(2)", async () => { await testEditor({ contentBefore: '
    a[]
    ', stepFunction: async (editor) => { pasteText(editor, "b\nc\nd"); }, contentAfter: '
    ab
    c
    d[]
    ', }); }); test("should paste text and understand \\n newlines within PRE element", async () => { await testEditor({ contentBefore: "
    []
    ", stepFunction: async (editor) => { pasteText(editor, "a\nb\nc"); }, contentAfter: "
    a
    b
    c[]
    ", }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    ax[]d

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

    a[bc]d

    ", stepFunction: async (editor) => { pasteText(editor, "xyz 123"); }, contentAfter: "

    axyz 123[]d

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

    a[bc]d

    ", stepFunction: async (editor) => { pasteText(editor, "x y"); }, contentAfter: "

    ax    y[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

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

    ab[c- -d]ef

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

    aby[]ef

    ', }); }); test("should paste a text when selection across two p (1)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteText(editor, "x"); }, contentAfter: "
    a

    bx[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteText(editor, "y"); }, contentAfter: "
    a

    by[]e

    f
    ", }); }); test("should paste a text when selection leave a span (1)", async () => { await testEditor({ contentBefore: '
    abc[de]f
    ', stepFunction: async (editor) => { pasteText(editor, "x"); }, contentAfter: '
    abcx[]f
    ', }); await testEditor({ contentBefore: '
    a[bc]def
    ', stepFunction: async (editor) => { pasteText(editor, "y"); }, contentAfter: '
    ay[]def
    ', }); }); test("should paste a text when selection across two element (1)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteText(editor, "x"); }, contentAfter: '
    1a

    bx[]ef

    ', }); await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteText(editor, "x"); }, contentAfter: '
    2abx[]e
    f
    ', }); }); }); }); describe("Simple html span", () => { const simpleHtmlCharX = 'x'; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    x[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    abx[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    abx[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    ax[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

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

    ab[c- -d]ef

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

    abx[]ef

    ', }); }); test("should paste a text when selection across two p (2)", async () => { await testEditor({ contentBefore: "
    1a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    1a

    bx[]e

    f
    ", }); await testEditor({ contentBefore: "
    2a

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    2a

    bx[]e

    f
    ", }); }); test("should paste a text when selection leave a span (2)", async () => { await testEditor({ contentBefore: '
    abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    abcx[]f
    ', }); await testEditor({ contentBefore: '
    a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    ax[]def
    ', }); }); test("should paste a text when selection across two element (2)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    1a

    bx[]ef

    ', }); await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    2abx[]e
    f
    ', }); await testEditor({ contentBefore: "
    3a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    3a

    bx[]e

    f
    ", }); }); }); }); describe("Simple html p", () => { const simpleHtmlCharX = "

    x

    "; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    x[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    abx[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    abx[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    ax[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    abx[]ef

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

    ab[c- -d]ef

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

    abx[]ef

    ', }); }); test("should paste a text when selection across two p (3)", async () => { await testEditor({ contentBefore: "
    1a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    1a

    bx[]e

    f
    ", }); await testEditor({ contentBefore: "
    2a

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    2a

    bx[]e

    f
    ", }); }); test("should paste a text when selection leave a span (3)", async () => { await testEditor({ contentBefore: '
    abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    abcx[]f
    ', }); await testEditor({ contentBefore: '
    a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    ax[]def
    ', }); }); test("should paste a text when selection across two element (3)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    1a

    bx[]ef

    ', }); await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: '
    2abx[]e
    f
    ', }); await testEditor({ contentBefore: "
    3a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, simpleHtmlCharX); }, contentAfter: "
    3a

    bx[]e

    f
    ", }); }); }); }); describe("Simple html elements containing
    ", () => { describe("breaking
    elements", () => { test("should split h1 with
    into seperate h1 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl[]

    ", }); }); test("should split h2 with
    into seperate h2 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl[]

    ", }); }); test("should split h3 with
    into seperate h3 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl[]

    ", }); }); test("should split h4 with
    into seperate h4 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl[]

    ", }); }); test("should split h5 with
    into seperate h5 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "
    abc
    def
    ghi
    jkl
    "); }, contentAfter: "
    abc
    def
    ghi
    jkl[]
    ", }); }); test("should split h6 with
    into seperate h6 elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "
    abc
    def
    ghi
    jkl
    "); }, contentAfter: "
    abc
    def
    ghi
    jkl[]
    ", }); }); test("should split p with
    into seperate p elements", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl

    mno

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl

    mno[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def
    ghi
    jkl


    mno

    "); }, contentAfter: "

    abc

    def

    ghi

    jkl


    mno[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc
    def


    ghi

    "); }, contentAfter: "

    abc

    def



    ghi[]

    ", }); }); test("should split multiple elements with
    ", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, "

    abc
    def

    ghi
    jkl


    mno
    pqr

    " ); }, contentAfter: "

    abc

    def

    ghi

    jkl


    mno

    pqr[]

    ", }); }); test("should split div with
    ", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "
    abc
    def
    "); }, contentAfter: "

    abc

    def[]

    ", }); }); }); describe("not breaking
    elements", () => { test("should not split li with
    ", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should not split blockquote with
    ", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "
    abc
    def
    "); }, contentAfter: "
    abc
    def[]
    ", }); }); test("should not split pre with
    ", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "
    abc
    def
    "); }, contentAfter: "
    abc
    def[]
    ", }); }); }); }); describe("Unwrapping html element", () => { test("should not unwrap a node when pasting on empty node", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    abc

    def[]

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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

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

    []


    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]


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

    []

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc



    "); }, contentAfter: "

    abc


    []

    ", }); }); test("should not unwrap a node when pasting in between different node", async () => { await testEditor({ contentBefore: "

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    mn

    abc[]

    op

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

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    mn

    abc

    def[]

    op

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

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    mn

    abc

    def

    ghi[]

    op

    ", }); }); test("should unwrap a node when pasting in between same node", async () => { await testEditor({ contentBefore: "

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    mnabc[]op

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

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    mnabc

    def[]

    op

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

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    mn

    abc

    def[]op

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

    mn[]op

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    mnabc

    def

    ghi[]op

    ", }); }); test("should not unwrap a node when pasting at start of different node", async () => { await testEditor({ contentBefore: "

    []mn

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]

    mn

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

    []mn

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    abc

    def[]

    mn

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

    []mn

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

    mn

    ", }); }); test("should unwrap a node when pasting at start of same node", async () => { await testEditor({ contentBefore: "

    []mn

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    abc[]mn

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

    []mn

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    abc

    def[]mn

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

    []mn

    ', stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: '

    abc

    def

    ghi[]mn

    ', }); }); test("should not unwrap a node when pasting at end of different node", async () => { await testEditor({ contentBefore: "

    mn[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    mn

    abc[]

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

    mn[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    mn

    abc

    def[]

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

    mn[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    mn

    abc

    def

    ghi[]

    ", }); }); test("should unwrap a node when pasting at end of same node", async () => { await testEditor({ contentBefore: "

    mn[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "

    mnabc[]

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

    mn[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    "); }, contentAfter: "

    mnabc

    def[]

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

    mn[]

    ', stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: '

    mnabc

    def

    ghi[]

    ', }); }); test("should not unwrap empty block nodes even when pasting on same node", async () => { await testEditor({ contentBefore: "

    a[]

    ", stepFunction: async (editor) => { pasteHtml(editor, "




    "); }, contentAfter: "

    a



    []

    ", }); }); }); describe("Complex html span", () => { const complexHtmlData = '123 4'; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    123 4[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    ab123 4[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    ab123 4[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    a123 4[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab123 4[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab123 4[]ef

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

    ab[c- -d]ef

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

    ab123 4[]ef

    ', }); }); test("should paste a text when selection across two p (4)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b123 4[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b123 4[]e

    f
    ", }); }); test("should paste a text when selection leave a span (4)", async () => { await testEditor({ contentBefore: '
    abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    abc123 4[]f
    ', }); await testEditor({ contentBefore: '
    a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    a123 4[]def
    ', }); }); test("should paste a text when selection across two element (4)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1a

    b123 4[]ef

    ', }); await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2ab123 4[]e
    f
    ', }); await testEditor({ contentBefore: "
    3a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    3a

    b123 4[]e

    f
    ", }); }); }); }); describe("Complex html p", () => { const complexHtmlData = '

    12

    34

    '; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    12

    34[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    ab12

    34[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    ab12

    34[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    a12

    34[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab12

    34[]ef

    ', }); }); test("should paste a text when selection across two span (1)", async () => { await testEditor({ contentBefore: '

    1ab[cd]ef

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

    1ab12

    34[]ef

    ', }); }); test("should paste a text when selection across two span (2)", async () => { await testEditor({ contentBefore: '

    2ab[c- -d]ef

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

    2ab12

    34[]ef

    ', }); }); test("should paste a text when selection across two p (5)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b12

    34[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b12

    34[]e

    f
    ", }); }); test("should paste a text when selection leave a span (5)", async () => { await testEditor({ contentBefore: '
    1abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1abc12
    34[]
    f
    ', }); await testEditor({ contentBefore: '
    2a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2a12
    34[]def
    ', }); }); test("should paste a text when selection leave a span (6)", async () => { await testEditor({ contentBefore: '

    1abc[de]f

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

    1abc12

    34[]f

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

    2a[bc]def

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

    2a12

    34[]def

    ', }); }); test("should paste a text when selection across two element (5)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, // FIXME: Bringing `e` and `f` into the `

    ` is a tradeOff // Should we change it ? How ? Might warrant a discussion. // possible alt contentAfter :

    1a

    b12

    34[]ef
    contentAfter: '
    1a

    b12

    34[]ef

    ', }); }); test("should paste a text when selection across two element (6)", async () => { await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2ab12
    34[]
    e
    f
    ', }); }); }); }); describe("Complex html 3 p", () => { const complexHtmlData = "

    1X2

    3X4

    5X6

    "; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    1X2

    3X4

    5X6[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    ab1X2

    3X4

    5X6[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    ab1X2

    3X4

    5X6[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    a1X2

    3X4

    5X6[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab1X2

    3X4

    5X6[]ef

    ', }); }); test("should paste a text when selection across two span (1)", async () => { await testEditor({ contentBefore: '

    1ab[cd]ef

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

    1ab1X2

    3X4

    5X6[]ef

    ', }); }); test("should paste a text when selection across two span (2)", async () => { await testEditor({ contentBefore: '

    2ab[c- -d]ef

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

    2ab1X2

    3X4

    5X6[]ef

    ', }); }); test("should paste a text when selection across two p (6)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b1X2

    3X4

    5X6[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b1X2

    3X4

    5X6[]e

    f
    ", }); }); test("should paste a text when selection leave a span (7)", async () => { await testEditor({ contentBefore: '
    1abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1abc1X2

    3X4

    5X6[]f
    ', }); await testEditor({ contentBefore: '
    2a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2a1X2

    3X4

    5X6[]def
    ', }); }); test("should paste a text when selection leave a span (8)", async () => { await testEditor({ contentBefore: '

    1abc[de]f

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

    1abc1X2

    3X4

    5X6[]f

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

    2a[bc]def

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

    2a1X2

    3X4

    5X6[]def

    ', }); }); test("should paste a text when selection across two element (7)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1a

    b1X2

    3X4

    5X6[]ef

    ', }); }); test("should paste a text when selection across two element (8)", async () => { await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2ab1X2

    3X4

    5X6[]e
    f
    ', }); }); }); }); describe("Complex html p+i", () => { const complexHtmlData = '

    12

    ii

    '; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    12

    ii[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    ab12

    ii[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    ab12

    ii[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    a12

    ii[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab12

    ii[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab12

    ii[]ef

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

    ab[cxd]ef

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

    ab12

    ii[]ef

    ', }); }); test("should paste a text when selection across two p (7)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b12

    ii[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b12

    ii[]e

    f
    ", }); }); test("should paste a text when selection leave a span (9)", async () => { await testEditor({ contentBefore: '
    1abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1abc12
    ii
    []
    f
    ', }); await testEditor({ contentBefore: '
    2a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2a12
    ii
    []def
    ', }); }); test("should paste a text when selection leave a span (10)", async () => { await testEditor({ contentBefore: '

    1abc[de]f

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

    1abc12

    ii[]f

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

    2a[bc]def

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

    2a12

    ii[]def

    ', }); }); test("should paste a text when selection across two element (9)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1a

    b12

    ii[]ef

    ', }); }); test("should paste a text when selection across two element (10)", async () => { await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2ab12
    ii
    []
    e
    f
    ', }); }); }); }); describe("Complex html 3p+b", () => { const complexHtmlData = "

    123

    zzz

    4567

    "; describe("range collapsed", () => { test("should paste a text at the beginning of a p", async () => { await testEditor({ contentBefore: "

    []abcd

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

    123

    zzz

    4567[]abcd

    ", }); }); test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    ab[]cd

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

    ab123

    zzz

    4567[]cd

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[]cd

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

    ab123

    zzz

    4567[]cd

    ', }); }); }); describe("range not collapsed", () => { test("should paste a text in a p", async () => { await testEditor({ contentBefore: "

    a[bc]d

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

    a123

    zzz

    4567[]d

    ", }); }); test("should paste a text in a span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab123

    zzz

    4567[]ef

    ', }); }); test("should paste a text when selection across two span", async () => { await testEditor({ contentBefore: '

    ab[cd]ef

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

    ab123

    zzz

    4567[]ef

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

    ab[c- -d]ef

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

    ab123

    zzz

    4567[]ef

    ', }); }); test("should paste a text when selection across two p (8)", async () => { await testEditor({ contentBefore: "
    a

    b[c

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b123

    zzz

    4567[]e

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

    b[c

    - -

    d]e

    f
    ", stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: "
    a

    b123

    zzz

    4567[]e

    f
    ", }); }); test("should paste a text when selection leave a span (11)", async () => { await testEditor({ contentBefore: '
    1abc[de]f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1abc123

    zzz

    4567[]f
    ', }); await testEditor({ contentBefore: '
    2a[bc]def
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2a123

    zzz

    4567[]def
    ', }); }); test("should paste a text when selection across two element (11)", async () => { await testEditor({ contentBefore: '
    1a

    b[c

    d]ef
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    1a

    b123

    zzz

    4567[]ef

    ', }); }); test("should paste a text when selection across two element (12)", async () => { await testEditor({ contentBefore: '
    2ab[c

    d]e

    f
    ', stepFunction: async (editor) => { pasteHtml(editor, complexHtmlData); }, contentAfter: '
    2ab123

    zzz

    4567[]e
    f
    ', }); }); }); }); describe("Complex html div", () => { const complexHtmlData = `
    abcdef
    ghijkl
    jklmno
    `; test("should convert div to p", async () => { await testEditor({ contentBefore: "

    []

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

    abcdef

    ghijkl

    jklmno[]

    ', }); }); }); describe("Special cases", () => { describe("lists", () => { test("should paste a list in a p", async () => { await testEditor({ contentBefore: "

    12[]34

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

    12

    34

    ", }); }); test("should paste the text of an li into another li", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should paste the text of an li into another li, and the text of another li into the next li", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should paste the text of an li into another li, insert a new li, and paste the text of a third li into the next li", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should paste the text of an li into another li and insert a new li at the end of a list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should insert a new li at the beginning of a list and paste the text of another li into the next li", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: "", }); }); test("should insert a list and a p tag inside a new list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, "

    ghi

    "); }, contentAfter: "", }); }); test("should insert content ending with a list inside a new list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    "); }, contentAfter: "", }); }); test("should convert a mixed list containing a paragraph into a checklist", async () => { await testEditor({ contentBefore: ``, stepFunction: async (editor) => { pasteHtml( editor, unformat(`

    jkl

    1. mno
    2. pqr
    3. stu
    `) ); }, contentAfter: unformat(` `), }); }); test("should not unwrap a list twice when pasting on new list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, ""); }, contentAfter: ``, }); }); test("should paste a nested list into another list", async () => { await testEditor({ contentBefore: "
    1. Alpha
    2. []
    ", stepFunction: async (editor) => { pasteHtml( editor, unformat(` `) ); }, contentAfter: unformat(`
    1. Alpha
    2. abc
    3. def
      1. 123
      2. 456[]
    `), }); }); test("should paste a nested list into another list (2)", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml( editor, unformat(`
    1. jkl
    `) ); }, contentAfter: unformat(` `), }); }); test("should convert a mixed list into a ordered list", async () => { await testEditor({ contentBefore: "
    1. []
    ", stepFunction: async (editor) => { pasteHtml( editor, unformat(` `) ); }, contentAfter: unformat(`
    1. ab
    2. cd
      1. ef
      2. gh
        1. ij
        2. kl[]
    `), }); }); test("should convert a mixed list starting with bullet list into a bullet list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml( editor, unformat(` `) ); }, contentAfter: unformat(` `), }); }); test("should paste a mixed list starting with deeply nested bullet list into a bullet list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml( editor, unformat(` `) ); }, contentAfter: unformat(` `), }); }); test("should paste a deeply nested list copied outside from odoo", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml( editor, unformat(`
    1. ab
      1. cd
      2. ef
        1. kl
        2. mn
    `) ); }, contentAfter: unformat(` `), }); }); test("should paste checklist from gdoc", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, unformat(` `) ); }, contentAfter: ``, }); }); }); describe("paragraphs", () => { test("should paste multiple paragraphs into a list", async () => { await testEditor({ contentBefore: "", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    jkl

    mno

    "); }, contentAfter: "", }); }); }); }); describe("pasting within blockquote", () => { test("should paste paragraph related elements within blockquote", async () => { await testEditor({ contentBefore: "
    []
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

    ", }); await testEditor({ contentBefore: "
    x[]
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "
    x

    abc

    def

    ghi[]

    ", }); await testEditor({ contentBefore: "
    []x
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

    x
    ", }); await testEditor({ contentBefore: "
    x[]y
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "
    x

    abc

    def

    ghi[]

    y
    ", }); }); }); describe("pasting within pre", () => { test("should paste paragraph releted elements within pre", async () => { await testEditor({ contentBefore: "
    []
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

    ", }); await testEditor({ contentBefore: "
    x[]
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "
    x

    abc

    def

    ghi[]

    ", }); await testEditor({ contentBefore: "
    []x
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "

    abc

    def

    ghi[]

    x
    ", }); await testEditor({ contentBefore: "
    x[]y
    ", stepFunction: async (editor) => { pasteHtml(editor, "

    abc

    def

    ghi

    "); }, contentAfter: "
    x

    abc

    def

    ghi[]

    y
    ", }); }); }); const url = "https://www.odoo.com"; const imgUrl = "https://download.odoocdn.com/icons/website/static/description/icon.png"; const videoUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; describe("link", () => { describe("range collapsed", () => { test("should paste and transform an URL in a p (collapsed)", async () => { await testEditor({ contentBefore: "

    ab[]cd

    ", stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

    ', }); }); test("should paste and transform an URL in a span (collapsed)", async () => { await testEditor({ contentBefore: '

    ab[]cd

    ', stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

    ', }); }); test("should paste and not transform an URL in a existing link (collapsed)", async () => { await testEditor({ contentBefore: '

    ab[]cd

    ', stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

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

    ab[]cd

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

    abrandom[]cd

    ', }); }); test("should paste and transform an URL in a existing link if pasting valid url (collapsed)", async () => { await testEditor({ contentBefore: '

    a[]cd

    ', stepFunction: async (editor) => { pasteText(editor, "https://www.xyz.xdc"); }, contentAfter: '

    ahttps://www.xyz.xdc[]cd

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

    ab[].comd

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

    aboom[].comd

    ', }); }); test("should replace link for new content when pasting in an empty link (collapsed)", async () => { await testEditor({ contentBefore: '

    []\u200B

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

    abc[]

    ", }); }); test("should replace link for new content when pasting in an empty link (collapsed)(2)", async () => { await testEditor({ contentBefore: '

    xy\u200B[]z

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

    xyabc[]z

    ", }); }); test("should replace link for new content (url) when pasting in an empty link (collapsed)", async () => { const { el, editor } = await setupEditor( `

    xy\u200B[]z

    ` ); pasteText(editor, "http://odoo.com"); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    xyhttp://odoo.com[]z

    ` ); }); test("should replace link for new content (imgUrl) when pasting in an empty link (collapsed) (1)", async () => { const { el, editor } = await setupEditor(`

    xy[]z

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

    xy\ufeff\ufeff[]\ufeffz

    ` ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); expect(getContent(el)).toBe(`

    xy${imgUrl}[]z

    `); await press("Enter"); expect(getContent(el)).toBe(`

    xy[]z

    `); }); test("should replace link for new content (url) when pasting in an empty link (collapsed) (2)", async () => { const { el, editor } = await setupEditor( `

    xy\u200B[]z

    ` ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); expect(getContent(el)).toBe(`

    xy${imgUrl}[]z

    `); await press("ArrowDown"); await press("Enter"); await animationFrame(); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    xy${imgUrl}[]z

    ` ); }); test("should paste and transform plain text content over an empty link (collapsed)", async () => { await testEditor({ contentBefore: '

    []\u200B

    ', stepFunction: async (editor) => { pasteText(editor, "abc www.odoo.com xyz"); }, contentAfter: '

    abc www.odoo.com xyz[]

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

    []\u200B

    ', stepFunction: async (editor) => { pasteText(editor, "odoo.com\ngoogle.com"); }, contentAfter: '

    odoo.com

    ' + '

    google.com[]

    ', }); }); test("should paste html content over an empty link (collapsed)", async () => { await testEditor({ contentBefore: '

    []\u200B

    ', stepFunction: async (editor) => { pasteHtml( editor, 'odoo.com
    google.com' ); }, contentAfter: '

    odoo.com

    google.com[]

    ', }); }); test("should paste and transform URL among text (collapsed)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, `abc ${url} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${url} def[]

    ` ); }); test("should paste and transform image URL among text (collapsed)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, `abc ${imgUrl} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${imgUrl} def[]

    ` ); }); test("should paste and transform video URL among text (collapsed)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, `abc ${videoUrl} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${videoUrl} def[]

    ` ); }); test("should paste and transform multiple URLs (collapsed) (1)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, `${url} ${videoUrl} ${imgUrl}`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ${url} ${videoUrl} ${imgUrl}[]

    ` ); }); test("should paste and transform multiple URLs (collapsed) (2)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, `${url} abc ${videoUrl} def ${imgUrl}`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ${url} abc ${videoUrl} def ${imgUrl}[]

    ` ); }); test("should paste plain text inside non empty link (collapsed)", async () => { await testEditor({ contentBefore: '

    a[]b

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

    a123[]b

    ', }); }); test("should paste and not transform an URL in a pre tag", async () => { await testEditor({ contentBefore: "
    []
    ", stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: "
    http://www.xyz.com[]
    ", }); }); }); describe("range not collapsed", () => { test("should paste and transform an URL in a p (not collapsed)", async () => { await testEditor({ contentBefore: "

    ab[xxx]cd

    ", stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

    ', }); }); test("should paste and transform an URL in a span (not collapsed)", async () => { await testEditor({ contentBefore: '

    ab[x546x]cd

    ', stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

    ', }); }); test("should paste and not transform an URL in a existing link (not collapsed)", async () => { await testEditor({ contentBefore: '

    ab[qsdqsd]cd

    ', stepFunction: async (editor) => { pasteText(editor, "http://www.xyz.com"); }, contentAfter: '

    abhttp://www.xyz.com[]cd

    ', }); }); test("should restore selection when pasting plain text followed by UNDO (1) (not collapsed)", async () => { await testEditor({ contentBefore: "

    [abc]

    ", stepFunction: async (editor) => { pasteText(editor, "def"); undo(editor); }, contentAfter: "

    [abc]

    ", }); }); test("should restore selection when pasting plain text followed by UNDO (2) (not collapsed)", async () => { await testEditor({ contentBefore: "

    [abc]

    ", stepFunction: async (editor) => { pasteText(editor, "www.odoo.com"); undo(editor); }, contentAfter: "

    [abc]

    ", }); }); test("should restore selection when pasting plain text followed by UNDO (3) (not collapsed)", async () => { await testEditor({ contentBefore: "

    [abc]

    ", stepFunction: async (editor) => { pasteText(editor, "def www.odoo.com xyz"); undo(editor); }, contentAfter: "

    [abc]

    ", }); }); test("should restore selection after pasting HTML followed by UNDO (not collapsed)", async () => { await testEditor({ contentBefore: "

    [abc]

    ", stepFunction: async (editor) => { pasteHtml( editor, 'odoo.com
    google.com' ); undo(editor); }, contentAfter: "

    [abc]

    ", }); }); test("should paste and transform URL among text (not collapsed)", async () => { const { el, editor } = await setupEditor("

    [xyz]

    "); pasteText(editor, `abc ${url} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${url} def[]

    ` ); }); test("should paste and transform image URL among text (not collapsed)", async () => { const { el, editor } = await setupEditor("

    [xyz]

    "); pasteText(editor, `abc ${imgUrl} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${imgUrl} def[]

    ` ); }); test("should paste and transform video URL among text (not collapsed)", async () => { const { el, editor } = await setupEditor("

    [xyz]

    "); pasteText(editor, `abc ${videoUrl} def`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    abc ${videoUrl} def[]

    ` ); }); test("should paste and transform multiple URLs among text (not collapsed)", async () => { const { el, editor } = await setupEditor("

    [xyz]

    "); pasteText(editor, `${url} ${videoUrl} ${imgUrl}`); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ${url} ${videoUrl} ${imgUrl}[]

    ` ); }); test("should paste and transform URL over the existing url", async () => { await testEditor({ contentBefore: '

    ab[http://www.xyz.com]cd

    ', stepFunction: async (editor) => { pasteText(editor, "https://www.xyz.xdc "); }, contentAfter: '

    abhttps://www.xyz.xdc []cd

    ', }); }); test("should paste and transform URL over the existing url (not collapsed)", async () => { await testEditor({ contentBefore: '

    ab[http://www.xyz.com]cd

    ', stepFunction: async (editor) => { pasteText(editor, "https://www.xyz.xdc "); }, contentAfter: '

    abhttps://www.xyz.xdc []cd

    ', }); }); test("should paste plain text content over a link if all of its contents is selected (not collapsed)", async () => { await testEditor({ contentBefore: '

    a[xyz]d

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

    abc[]d

    ", }); }); test("should paste and transform plain text content over a link if all of its contents is selected (not collapsed)", async () => { await testEditor({ contentBefore: '

    [xyz]

    ', stepFunction: async (editor) => { pasteText(editor, "www.odoo.com"); }, contentAfter: '

    www.odoo.com[]

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

    [xyz]

    ', stepFunction: async (editor) => { pasteText(editor, "abc www.odoo.com xyz"); }, contentAfter: '

    abc www.odoo.com xyz[]

    ', }); }); test("should paste and transform plain text content over an image link if all of its contents is selected (not collapsed) (1)", async () => { const { el, editor } = await setupEditor( `

    ab[http://www.xyz.com]cd

    ` ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); expect(getContent(el)).toBe( `

    abhttps://download.odoocdn.com/icons/website/static/description/icon.png[]cd

    ` ); await press("Enter"); expect(getContent(el)).toBe(`

    ab[]cd

    `); }); test("should paste and transform plain text content over an image link if all of its contents is selected (not collapsed) (2)", async () => { const { el, editor } = await setupEditor( `

    ab[http://www.xyz.com]cd

    ` ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); expect(getContent(el)).toBe( `

    abhttps://download.odoocdn.com/icons/website/static/description/icon.png[]cd

    ` ); await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${imgUrl}[]cd

    ` ); }); test("should paste html content over a link if all of its contents is selected (not collapsed)", async () => { await testEditor({ contentBefore: '

    [xyz]

    ', stepFunction: async (editor) => { pasteHtml( editor, 'odoo.com
    google.com' ); }, contentAfter: '

    odoo.com

    google.com[]

    ', }); }); }); }); describe("images", () => { describe("range collapsed", () => { test("should paste and transform an image URL in a p (1)", async () => { const { el, editor } = await setupEditor("

    ab[]cd

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); await press("Enter"); expect(getContent(el)).toBe(`

    ab[]cd

    `); }); test("should paste and transform an image URL in a span", async () => { const { el, editor } = await setupEditor('

    ab[]cd

    '); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); await press("Enter"); expect(getContent(el)).toBe( `

    ab[]cd

    ` ); }); test("should paste and transform an image URL in an existing link", async () => { const { el, editor } = await setupEditor( '

    ab[]cd

    ' ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab[]cd

    ` ); }); test("should paste an image URL as a link in a p (1)", async () => { const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ${imgUrl}[]

    ` ); }); test("should not revert a history step when pasting an image URL as a link (1)", async () => { const { el, editor } = await setupEditor("

    []

    "); // paste text to have a history step recorded pasteText(editor, "*should not disappear*"); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    *should not disappear*${imgUrl}[]

    ` ); }); }); describe("range not collapsed", () => { test("should paste and transform an image URL in a p (2)", async () => { const { el, editor } = await setupEditor("

    ab[xxx]cd

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe(`

    ab[]cd

    `); }); test("should paste and transform an image URL in a span", async () => { const { el, editor } = await setupEditor( '

    ab[x546x]cd

    ' ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); await press("Enter"); expect(getContent(el)).toBe( `

    ab[]cd

    ` ); }); test("should paste and transform an image URL inside an existing link", async () => { const { el, editor } = await setupEditor( '

    ab[qsdqsd]cd

    ' ); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(0); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab[]cd

    ` ); }); test("should paste an image URL as a link in a p (2)", async () => { const { el, editor } = await setupEditor("

    ab[xxx]cd

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${imgUrl}[]cd

    ` ); }); test("should not revert a history step when pasting an image URL as a link (2)", async () => { const { el, editor } = await setupEditor("

    []

    "); // paste text (to have a history step recorded) pasteText(editor, "abxxxcd"); // select xxx in "

    ab[xxx]cd

    "" const p = editor.editable.querySelector("p"); const selection = { anchorNode: p.childNodes[1], anchorOffset: 2, focusNode: p.childNodes[1], focusOffset: 5, }; setSelection(selection); // paste url pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${imgUrl}[]cd

    ` ); }); test("should restore selection after pasting image URL followed by UNDO (1)", async () => { const { el, editor } = await setupEditor("

    [abc]

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick first command (Embed image) await press("Enter"); // Undo undo(editor); expect(getContent(el)).toBe("

    [abc]

    "); }); test("should restore selection after pasting image URL followed by UNDO (2)", async () => { const { el, editor } = await setupEditor("

    [abc]

    "); pasteText(editor, imgUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick second command (Paste as URL) await press("ArrowDown"); await press("Enter"); // Undo undo(editor); expect(getContent(el)).toBe("

    [abc]

    "); }); }); }); describe("youtube video", () => { describe("range collapsed", () => { beforeEach(() => { onRpc("/html_editor/video_url/data", async (request) => { const { params } = await request.json(); return { embed_url: params.video_url }; }); }); test("should paste and transform a youtube URL in a p (1)", async () => { const { el, editor } = await setupEditor("

    ab[]cd

    "); pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Force powerbox validation on the default first choice await press("Enter"); // Wait for the getYoutubeVideoElement promise to resolve. await tick(); expect(getContent(el)).toBe( `

    ab

    []cd

    ` ); }); test("should paste and transform a youtube URL in a span (1)", async () => { const { el, editor } = await setupEditor('

    ab[]cd

    '); pasteText(editor, "https://youtu.be/dQw4w9WgXcQ"); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Force powerbox validation on the default first choice await press("Enter"); // Wait for the getYoutubeVideoElement promise to resolve. await tick(); expect(getContent(el)).toBe( '

    ab

    []cd

    ' ); }); test("should paste and not transform a youtube URL in a existing link", async () => { const { el, editor, plugins } = await setupEditor( '

    ab[]cd

    ' ); pasteText(editor, "https://youtu.be/dQw4w9WgXcQ"); // Ensure the powerbox is active const powerbox = plugins.get("powerbox"); expect(powerbox.overlay.isOpen).not.toBe(true); expect(cleanLinkArtifacts(getContent(el))).toBe( '

    abhttps://youtu.be/dQw4w9WgXcQ[]cd

    ' ); }); test("should paste a youtube URL as a link in a p (1)", async () => { const url = "https://youtu.be/dQw4w9WgXcQ"; const { el, editor } = await setupEditor("

    []

    "); pasteText(editor, url); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe(`

    ${url}[]

    `); }); test("should not revert a history step when pasting a youtube URL as a link (1)", async () => { const url = "https://youtu.be/dQw4w9WgXcQ"; const { el, editor } = await setupEditor("

    []

    "); // paste text to have a history step recorded pasteText(editor, "*should not disappear*"); pasteText(editor, url); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    *should not disappear*${url}[]

    ` ); }); }); describe("range not collapsed", () => { beforeEach(() => { onRpc("/html_editor/video_url/data", async (request) => { const { params } = await request.json(); return { embed_url: params.video_url }; }); }); test("should paste and transform a youtube URL in a p (2)", async () => { const { el, editor } = await setupEditor("

    ab[xxx]cd

    "); pasteText(editor, "https://youtu.be/dQw4w9WgXcQ"); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Force powerbox validation on the default first choice await press("Enter"); // Wait for the getYoutubeVideoElement promise to resolve. await tick(); expect(getContent(el)).toBe( '

    ab

    []cd

    ' ); }); test("should paste and transform a youtube URL in a span (2)", async () => { const { el, editor } = await setupEditor( '

    ab[x546x]cd

    ' ); pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Force powerbox validation on the default first choice await press("Enter"); // Wait for the getYoutubeVideoElement promise to resolve. await tick(); expect(getContent(el)).toBe( `

    ab

    []cd

    ` ); }); test("should paste and not transform a youtube URL in a existing link", async () => { const { el, editor, plugins } = await setupEditor( '

    ab[qsdqsd]cd

    ' ); pasteText(editor, videoUrl); // Ensure the powerbox is active const powerbox = plugins.get("powerbox"); expect(powerbox.overlay.isOpen).not.toBe(true); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${videoUrl}[]cd

    ` ); }); test("should paste a youtube URL as a link in a p (2)", async () => { const { el, editor } = await setupEditor("

    ab[xxx]cd

    "); pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${videoUrl}[]cd

    ` ); }); test("should not revert a history step when pasting a youtube URL as a link (2)", async () => { const { el, editor } = await setupEditor("

    []

    "); // paste text (to have a history step recorded) pasteText(editor, "abxxxcd"); // select xxx in "

    ab[xxx]cd

    " const p = editor.editable.querySelector("p"); const selection = { anchorNode: p.childNodes[1], anchorOffset: 2, focusNode: p.childNodes[1], focusOffset: 5, }; setSelection(selection); setSelection(selection); // paste url pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); expect(cleanLinkArtifacts(getContent(el))).toBe( `

    ab${videoUrl}[]cd

    ` ); }); test("should restore selection after pasting video URL followed by UNDO (1)", async () => { const { el, editor } = await setupEditor("

    [abc]

    "); pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Force powerbox validation on the default first choice await press("Enter"); // Undo undo(editor); expect(getContent(el)).toBe("

    [abc]

    "); }); test("should restore selection after pasting video URL followed by UNDO (2)", async () => { const { el, editor } = await setupEditor("

    [abc]

    "); pasteText(editor, videoUrl); await animationFrame(); expect(".o-we-powerbox").toHaveCount(1); // Pick the second command (Paste as URL) await press("ArrowDown"); await press("Enter"); // Undo undo(editor); expect(getContent(el)).toBe("

    [abc]

    "); }); }); }); describe("Odoo editor own html", () => { test("should paste html as is", async () => { await testEditor({ contentBefore: "

    a[]b

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

    a

    b

    []b

    ', }); }); test("should not paste unsafe content", async () => { await testEditor({ contentBefore: "

    a[]b

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

    a[]b

    ", }); }); }); describe("editable in iframe", () => { test("should paste odoo-editor html", async () => { const { el, editor } = await setupEditor("

    []

    ", { props: { iframe: true } }); pasteOdooEditorHtml(editor, `

    textbold textmore text

    `); expect(getContent(el)).toBe("

    textbold textmore text[]

    "); }); }); describe("Paste HTML tables", () => { // The tests below are very sensitive to whitespaces as they do represent actual // whitespace text nodes in the DOM. The tests will fail if those are removed. test("should keep all allowed style (Excel Online)", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, `
    Italic then also BOLD Italic strike
    Just bold Just Italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT
    ` ); }, contentAfter: ` ${" "} ${" "}
    Italic then also BOLD Italic strike
    Just bold Just Italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT

    ${" "} ${" "} []

    `, }); }); test("should keep all allowed style (Google Sheets)", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, `
    Italic then also BOLD Italic strike
    Just Bold Just Italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT
    ` ); }, contentAfter: ` ${" "} ${" "} ${" "} ${" "}
    Italic then also BOLD Italic strike
    Just Bold Just Italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT

    []

    `, }); }); test("should keep all allowed style (Libre Office)", async () => { await testEditor({ contentBefore: "

    []

    ", stepFunction: async (editor) => { pasteHtml( editor, `
    Italic then also BOLD Italic strike
    Just bold Just italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT
    ` ); }, contentAfter: ` ${" "} ${" "}
    Italic then also BOLD Italic strike
    Just bold Just italic Bold underline
    Color text Color strike and underline
    Color background Color text on color background
    14pt MONO TEXT

    []

    `, }); }); }); describe("onDrop", () => { test("should add text from htmlTransferItem", async () => { const { el } = await setupEditor("

    a[b]cd

    "); const pElement = el.firstChild; const textNode = pElement.firstChild; patchWithCleanup(document, { caretPositionFromPoint: () => ({ offsetNode: textNode, offset: 3 }), }); const dropData = new DataTransfer(); dropData.setData("text/html", "b"); await dispatch(pElement, "drop", { dataTransfer: dropData }); await tick(); expect(getContent(el)).toBe("

    abcb[]d

    "); }); test("should not be able to paste inside some branded node", async () => { const { el } = await setupEditor(`

    a[b]cd

    `); const pElement = el.firstChild; const textNode = pElement.firstChild; patchWithCleanup(document, { caretPositionFromPoint: () => ({ offsetNode: textNode, offset: 3 }), }); const dropData = new DataTransfer(); dropData.setData("text/html", "x"); await dispatch(pElement, "drop", { dataTransfer: dropData }); await tick(); expect(getContent(el)).toBe(`

    a[b]cd

    `); }); test("should add new images form fileTransferItems", async () => { const { el } = await setupEditor(`

    a[b]cd

    `); const pElement = el.firstChild; const textNode = pElement.firstChild; patchWithCleanup(document, { caretPositionFromPoint: () => ({ offsetNode: textNode, offset: 3 }), }); const base64Image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="; const blob = dataURItoBlob(base64Image); const dropData = new DataTransfer(); const f = new File([blob], "image.png", { type: blob.type }); dropData.items.add(f); await dispatch(pElement, "drop", { dataTransfer: dropData }); await waitFor("img"); expect(getContent(el)).toBe( `

    abc[]d

    ` ); }); test("should move an image if it originated from the editor", async () => { const base64Image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="; const { el } = await setupEditor( `

    a[]bc

    ` ); const pElement = el.firstChild; const imgElement = pElement.childNodes[1]; const bcTextNode = pElement.childNodes[2]; patchWithCleanup(document, { caretPositionFromPoint: () => ({ offsetNode: bcTextNode, offset: 1 }), }); const dragdata = new DataTransfer(); await dispatch(imgElement, "dragstart", { dataTransfer: dragdata }); await animationFrame(); const imageHTML = dragdata.getData("application/vnd.odoo.odoo-editor-node"); expect(imageHTML).toBe( `` ); const dropData = new DataTransfer(); dropData.setData( "text/html", `` ); // Simulate the application/vnd.odoo.odoo-editor-node data that the browser would do. dropData.setData("application/vnd.odoo.odoo-editor-node", imageHTML); await dispatch(pElement, "drop", { dataTransfer: dropData }); await animationFrame(); expect(getContent(el)).toBe( `

    ab[]c

    ` ); }); }); function dataURItoBlob(dataURI) { const binary = atob(dataURI.split(",")[1]); const array = []; const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; for (let i = 0; i < binary.length; i++) { array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { type: mimeString }); }