Odoo18-Base/addons/html_editor/static/tests/paste.test.js
2025-01-06 10:57:38 +07:00

3721 lines
164 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()}>b</${node.toLowerCase()}>c`
: `a</p><${node.toLowerCase()}>b</${node.toLowerCase()}><p>c`;
await testEditor({
contentBefore: "<p>123[]4</p>",
stepFunction: async (editor) => {
pasteHtml(editor, `a<${node.toLowerCase()}>b</${node.toLowerCase()}>c`);
},
contentAfter: "<p>123" + html + "[]4</p>",
});
}
}
});
test("should keep whitelisted Tags tag (2)", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, 'a<img src="http://www.imgurl.com/img.jpg">d');
},
contentAfter: '<p>123a<img src="http://www.imgurl.com/img.jpg">d[]</p>',
});
});
test("should keep tables Tags tag and add classes", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
"a<table><thead><tr><th>h</th></tr></thead><tbody><tr><td>b</td></tr></tbody></table>d"
);
},
contentAfter:
'<p>123a</p><table class="table table-bordered"><thead><tr><th>h</th></tr></thead><tbody><tr><td>b</td></tr></tbody></table><p>d[]</p>',
});
});
test("should not keep span", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "a<span>bc</span>d");
},
contentAfter: "<p>123abcd[]</p>",
});
});
test("should not keep orphan LI", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "a<li>bc</li>d");
},
contentAfter: "<p>123a</p><p>bc</p><p>d[]</p>",
});
});
test("should keep LI in UL", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "a<ul><li>bc</li></ul>d");
},
contentAfter: "<p>123a</p><ul><li>bc</li></ul><p>d[]</p>",
});
});
test("should keep P and B and not span", async () => {
await testEditor({
contentBefore: "<p>123[]xx</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "a<p>bc</p>d<span>e</span>f<b>g</b>h");
},
contentAfter: "<p>123a</p><p>bc</p><p>def<b>g</b>h[]xx</p>",
});
});
test("should keep styled span", async () => {
await testEditor({
contentBefore: "<p>123[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, 'a<span style="text-decoration: underline">bc</span>d');
},
contentAfter: "<p>123abcd[]</p>",
});
});
test("should remove unwanted styles and b tag when pasting from paragraph from gdocs", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
`<meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-ddad60c5-7fff-0a8f-fdd5-c1107201fe26"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">test1</span></p><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">test2</span></p></b>`
);
},
contentAfter: "<p>test1</p><p>test2[]</p>",
});
});
test("should remove b, keep p, and remove unwanted styles when pasting list from gdocs", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
'<meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-5d8bcf85-7fff-ebec-8604-eedd96f2d601"><ul style="margin-top:0;margin-bottom:0;padding-inline-start:48px;"><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;" role="presentation"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Google</span></p></li><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;" role="presentation"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Test</span></p></li><li dir="ltr" style="list-style-type:disc;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;" role="presentation"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">test2</span></p></li></ul></b>'
);
},
contentAfter:
"<ul><li><p>Google</p></li><li><p>Test</p></li><li><p>test2[]</p></li></ul>",
});
});
test("should remove unwanted styles and keep tags when pasting list from gdoc", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
'<meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-477946a8-7fff-f959-18a4-05014997e161"><ul style="margin-top:0;margin-bottom:0;padding-inline-start:48px;"><li dir="ltr" style="list-style-type:disc;font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><h1 dir="ltr" style="line-height:1.38;margin-top:20pt;margin-bottom:0pt;" role="presentation"><span style="font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Google</span></h1></li><li dir="ltr" style="list-style-type:disc;font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><h1 dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:6pt;" role="presentation"><span style="font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Test</span></h1></li><li dir="ltr" style="list-style-type:disc;font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1"><h1 dir="ltr" style="line-height:1.38;margin-top:20pt;margin-bottom:0pt;" role="presentation"><span style="font-size:20pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">test2</span></h1></li></ul></b>'
);
},
contentAfter:
"<ul><li><h1>Google</h1></li><li><h1>Test</h1></li><li><h1>test2[]</h1></li></ul>",
});
});
});
describe("Simple text", () => {
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "x");
},
contentAfter: "<p>x[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: "<p>abx[]cd</p>",
});
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteText(editor, "xyz 123");
},
contentAfter: "<p>abxyz 123[]cd</p>",
});
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteText(editor, "x y");
},
contentAfter: "<p>abx&nbsp; &nbsp; y[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<p>a<span class="a">bx[]c</span>d</p>',
});
});
// 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: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteText(editor, "a\nb\nc\nd");
},
contentAfter:
'<p style="margin-bottom: 0px;">a</p>' +
'<p style="margin-bottom: 0px;">b</p>' +
'<p style="margin-bottom: 0px;">c</p>' +
"<p>d[]</p>",
});
});
test("should paste text and understand \\r\\n newlines", async () => {
await testEditor({
// @phoenix content adapted to make it valid html
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteText(editor, "a\r\nb\r\nc\r\nd");
},
contentAfter:
'<p style="margin-bottom: 0px;">a</p>' +
'<p style="margin-bottom: 0px;">b</p>' +
'<p style="margin-bottom: 0px;">c</p>' +
"<p>d[]</p>",
});
});
test("should paste text and understand \\n newlines within UNBREAKABLE node", async () => {
await testEditor({
contentBefore: "<div>[]<br></div>",
stepFunction: async (editor) => {
pasteText(editor, "a\nb\nc\nd");
},
contentAfter: "<div>a<br>b<br>c<br>d[]</div>",
});
});
test("should paste text and understand \\n newlines within UNBREAKABLE node(2)", async () => {
await testEditor({
contentBefore: '<div><span style="font-size: 9px;">a[]</span></div>',
stepFunction: async (editor) => {
pasteText(editor, "b\nc\nd");
},
contentAfter: '<div><span style="font-size: 9px;">ab<br>c<br>d[]</span></div>',
});
});
test("should paste text and understand \\n newlines within PRE element", async () => {
await testEditor({
contentBefore: "<pre>[]<br></pre>",
stepFunction: async (editor) => {
pasteText(editor, "a\nb\nc");
},
contentAfter: "<pre>a<br>b<br>c[]</pre>",
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: "<p>ax[]d</p>",
});
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteText(editor, "xyz 123");
},
contentAfter: "<p>axyz 123[]d</p>",
});
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteText(editor, "x y");
},
contentAfter: "<p>ax&nbsp; &nbsp; y[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteText(editor, "y");
},
contentAfter: '<p>a<span class="a">by[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (1)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: "<div>a<p>bx[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteText(editor, "y");
},
contentAfter: "<div>a<p>by[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (1)", async () => {
await testEditor({
contentBefore: '<div>ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<div>ab<span class="a">cx[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteText(editor, "y");
},
contentAfter: '<div>ay[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection across two element (1)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<div>1a<p>bx[]<span class="a">e</span>f</p></div>',
});
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteText(editor, "x");
},
contentAfter: '<div>2a<span class="a">bx[]</span>e<br>f</div>',
});
});
});
});
describe("Simple html span", () => {
const simpleHtmlCharX =
'<span style="font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">x</span>';
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>x[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>abx[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>ax[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (2)", async () => {
await testEditor({
contentBefore: "<div>1a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>1a<p>bx[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>2a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>2a<p>bx[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (2)", async () => {
await testEditor({
contentBefore: '<div>ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>ab<span class="a">cx[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>ax[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection across two element (2)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>1a<p>bx[]<span class="a">e</span>f</p></div>',
});
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>2a<span class="a">bx[]</span>e<br>f</div>',
});
await testEditor({
contentBefore: "<div>3a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>3a<p>bx[]e</p>f</div>",
});
});
});
});
describe("Simple html p", () => {
const simpleHtmlCharX = "<p>x</p>";
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>x[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>abx[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<p>ax[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<p>a<span class="a">bx[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (3)", async () => {
await testEditor({
contentBefore: "<div>1a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>1a<p>bx[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>2a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>2a<p>bx[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (3)", async () => {
await testEditor({
contentBefore: '<div>ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>ab<span class="a">cx[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>ax[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection across two element (3)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>1a<p>bx[]<span class="a">e</span>f</p></div>',
});
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: '<div>2a<span class="a">bx[]</span>e<br>f</div>',
});
await testEditor({
contentBefore: "<div>3a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, simpleHtmlCharX);
},
contentAfter: "<div>3a<p>bx[]e</p>f</div>",
});
});
});
});
describe("Simple html elements containing <br>", () => {
describe("breaking <br> elements", () => {
test("should split h1 with <br> into seperate h1 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc<br>def<br>ghi<br>jkl</h1>");
},
contentAfter: "<h1>abc</h1><h1>def</h1><h1>ghi</h1><h1>jkl[]</h1>",
});
});
test("should split h2 with <br> into seperate h2 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h2>abc<br>def<br>ghi<br>jkl</h2>");
},
contentAfter: "<h2>abc</h2><h2>def</h2><h2>ghi</h2><h2>jkl[]</h2>",
});
});
test("should split h3 with <br> into seperate h3 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h3>abc<br>def<br>ghi<br>jkl</h3>");
},
contentAfter: "<h3>abc</h3><h3>def</h3><h3>ghi</h3><h3>jkl[]</h3>",
});
});
test("should split h4 with <br> into seperate h4 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h4>abc<br>def<br>ghi<br>jkl</h4>");
},
contentAfter: "<h4>abc</h4><h4>def</h4><h4>ghi</h4><h4>jkl[]</h4>",
});
});
test("should split h5 with <br> into seperate h5 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h5>abc<br>def<br>ghi<br>jkl</h5>");
},
contentAfter: "<h5>abc</h5><h5>def</h5><h5>ghi</h5><h5>jkl[]</h5>",
});
});
test("should split h6 with <br> into seperate h6 elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h6>abc<br>def<br>ghi<br>jkl</h6>");
},
contentAfter: "<h6>abc</h6><h6>def</h6><h6>ghi</h6><h6>jkl[]</h6>",
});
});
test("should split p with <br> into seperate p elements", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc<br>def<br>ghi<br>jkl</p>");
},
contentAfter: "<p>abc</p><p>def</p><p>ghi</p><p>jkl[]</p>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc<br>def<br>ghi<br>jkl</p><p>mno</p>");
},
contentAfter: "<p>abc</p><p>def</p><p>ghi</p><p>jkl</p><p>mno[]</p>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc<br>def<br>ghi<br>jkl</p><p><br></p><p>mno</p>");
},
contentAfter: "<p>abc</p><p>def</p><p>ghi</p><p>jkl</p><p><br></p><p>mno[]</p>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc<br>def<br><br><br>ghi</p>");
},
contentAfter: "<p>abc</p><p>def</p><p><br></p><p><br></p><p>ghi[]</p>",
});
});
test("should split multiple elements with <br>", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
"<p>abc<br>def</p><h1>ghi<br>jkl</h1><h2><br></h2><h3>mno<br>pqr</h3>"
);
},
contentAfter:
"<p>abc</p><p>def</p><h1>ghi</h1><h1>jkl</h1><h2><br></h2><h3>mno</h3><h3>pqr[]</h3>",
});
});
test("should split div with <br>", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<div>abc<br>def</div>");
},
contentAfter: "<p>abc</p><p>def[]</p>",
});
});
});
describe("not breaking <br> elements", () => {
test("should not split li with <br>", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>abc<br>def</li></ul>");
},
contentAfter: "<ul><li>abc<br>def[]</li></ul>",
});
});
test("should not split blockquote with <br>", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<blockquote>abc<br>def</blockquote>");
},
contentAfter: "<blockquote>abc<br>def[]</blockquote>",
});
});
test("should not split pre with <br>", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<pre>abc<br>def</pre>");
},
contentAfter: "<pre>abc<br>def[]</pre>",
});
});
});
});
describe("Unwrapping html element", () => {
test("should not unwrap a node when pasting on empty node", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<h1>abc[]</h1>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h2>abc</h2>");
},
contentAfter: "<h2>abc[]</h2>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h3>abc</h3>");
},
contentAfter: "<h3>abc[]</h3>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<h1>abc</h1><h2>def[]</h2>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<h1>abc</h1><h2>def</h2><h3>ghi[]</h3>",
});
await testEditor({
contentBefore: "<p>[]<br></p><p><br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h3>abc</h3>");
},
contentAfter: "<h3>abc[]</h3><p><br></p>",
});
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc</p><p><br></p><p><br></p>");
},
contentAfter: "<p>abc</p><p><br></p><p>[]<br></p>",
});
});
test("should not unwrap a node when pasting in between different node", async () => {
await testEditor({
contentBefore: "<p>mn[]op</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<p>mn</p><h1>abc[]</h1><p>op</p>",
});
await testEditor({
contentBefore: "<p>mn[]op</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<p>mn</p><h1>abc</h1><h2>def[]</h2><p>op</p>",
});
await testEditor({
contentBefore: "<p>mn[]op</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<p>mn</p><h1>abc</h1><h2>def</h2><h3>ghi[]</h3><p>op</p>",
});
});
test("should unwrap a node when pasting in between same node", async () => {
await testEditor({
contentBefore: "<h1>mn[]op</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<h1>mnabc[]op</h1>",
});
await testEditor({
contentBefore: "<h1>mn[]op</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<h1>mnabc</h1><h2>def[]</h2><h1>op</h1>",
});
await testEditor({
contentBefore: "<h2>mn[]op</h2>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<h2>mn</h2><h1>abc</h1><h2>def[]op</h2>",
});
await testEditor({
contentBefore: "<h1>mn[]op</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h1>def</h1><h1>ghi</h1>");
},
contentAfter: "<h1>mnabc</h1><h1>def</h1><h1>ghi[]op</h1>",
});
});
test("should not unwrap a node when pasting at start of different node", async () => {
await testEditor({
contentBefore: "<p>[]mn</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<h1>abc[]</h1><p>mn</p>",
});
await testEditor({
contentBefore: "<p>[]mn</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<h1>abc</h1><h2>def[]</h2><p>mn</p>",
});
await testEditor({
contentBefore: "<p>[]mn</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<h1>abc</h1><h2>def</h2><h3>ghi[]</h3><p>mn</p>",
});
});
test("should unwrap a node when pasting at start of same node", async () => {
await testEditor({
contentBefore: "<h1>[]mn</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<h1>abc[]mn</h1>",
});
await testEditor({
contentBefore: "<h1>[]mn</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h2>abc</h2><h1>def</h1>");
},
contentAfter: "<h2>abc</h2><h1>def[]mn</h1>",
});
await testEditor({
contentBefore: '<h1><font style="background-color: rgb(255, 0, 0);">[]mn</font></h1>',
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h1>def</h1><h1>ghi</h1>");
},
contentAfter:
'<h1>abc</h1><h1>def</h1><h1><font style="background-color: rgb(255, 0, 0);">ghi[]mn</font></h1>',
});
});
test("should not unwrap a node when pasting at end of different node", async () => {
await testEditor({
contentBefore: "<p>mn[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<p>mn</p><h1>abc[]</h1>",
});
await testEditor({
contentBefore: "<p>mn[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<p>mn</p><h1>abc</h1><h2>def[]</h2>",
});
await testEditor({
contentBefore: "<p>mn[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<p>mn</p><h1>abc</h1><h2>def</h2><h3>ghi[]</h3>",
});
});
test("should unwrap a node when pasting at end of same node", async () => {
await testEditor({
contentBefore: "<h1>mn[]</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1>");
},
contentAfter: "<h1>mnabc[]</h1>",
});
await testEditor({
contentBefore: "<h1>mn[]</h1>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2>");
},
contentAfter: "<h1>mnabc</h1><h2>def[]</h2>",
});
await testEditor({
contentBefore: '<h1><font style="background-color: rgb(255, 0, 0);">mn[]</font></h1>',
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h1>def</h1><h1>ghi</h1>");
},
contentAfter:
'<h1><font style="background-color: rgb(255, 0, 0);">mnabc</font></h1><h1>def</h1><h1>ghi[]</h1>',
});
});
test("should not unwrap empty block nodes even when pasting on same node", async () => {
await testEditor({
contentBefore: "<p>a[]</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p><br></p><p><br></p><p><br></p>");
},
contentAfter: "<p>a</p><p><br></p><p><br></p><p>[]<br></p>",
});
});
});
describe("Complex html span", () => {
const complexHtmlData =
'<span style="font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">1</span><b style="box-sizing: border-box; font-weight: bolder; font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">23</b><span style="font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span>4</span>';
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>1<b>23</b>&nbsp;4[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>ab1<b>23</b>&nbsp;4[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span class="a">b1<b>23</b>&nbsp;4[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>a1<b>23</b>&nbsp;4[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span class="a">b1<b>23</b>&nbsp;4[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span class="a">b1<b>23</b>&nbsp;4[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span class="a">b1<b>23</b>&nbsp;4[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (4)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (4)", async () => {
await testEditor({
contentBefore: '<div>ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>ab<span class="a">c1<b>23</b>&nbsp;4[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>a1<b>23</b>&nbsp;4[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection across two element (4)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>1a<p>b1<b>23</b>&nbsp;4[]<span class="a">e</span>f</p></div>',
});
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a<span class="a">b1<b>23</b>&nbsp;4[]</span>e<br>f</div>',
});
await testEditor({
contentBefore: "<div>3a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>3a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>",
});
});
});
});
describe("Complex html p", () => {
const complexHtmlData =
'<p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 1rem; color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">12</p><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 1rem; color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">34</p>';
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>12</p><p>34[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>ab12</p><p>34[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a">34[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>a12</p><p>34[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a">34[]e</span>f</p>',
});
});
test("should paste a text when selection across two span (1)", async () => {
await testEditor({
contentBefore: '<p>1a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>1a<span class="a">b12</span></p><p><span class="a">34[]e</span>f</p>',
});
});
test("should paste a text when selection across two span (2)", async () => {
await testEditor({
contentBefore: '<p>2a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>2a<span class="a">b12</span></p><p><span class="a">34[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (5)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b12</p><p>34[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b12</p><p>34[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (5)", async () => {
await testEditor({
contentBefore: '<div>1ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>1ab<span class="a">c12<br>34[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>2a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a12<br>34[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection leave a span (6)", async () => {
await testEditor({
contentBefore: '<p>1ab<span class="a">c[d</span>e]f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>1ab<span class="a">c12</span></p><p><span class="a">34[]</span>f</p>',
});
await testEditor({
contentBefore: '<p>2a[b<span class="a">c]d</span>ef</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>2a12</p><p>34[]<span class="a">d</span>ef</p>',
});
});
test("should paste a text when selection across two element (5)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
// FIXME: Bringing `e` and `f` into the `<p>` is a tradeOff
// Should we change it ? How ? Might warrant a discussion.
// possible alt contentAfter : <div>1a<p>b12</p>34[]<span>e</span>f</div>
contentAfter: '<div>1a<p>b12</p><p>34[]<span class="a">e</span>f</p></div>',
});
});
test("should paste a text when selection across two element (6)", async () => {
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a<span class="a">b12<br>34[]</span>e<br>f</div>',
});
});
});
});
describe("Complex html 3 p", () => {
const complexHtmlData = "<p>1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6</p>";
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>ab1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<i>X</i>2</span></p><p>3<i>X</i>4</p><p><span class="a">5<i>X</i>6[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>a1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<i>X</i>2</span></p><p>3<i>X</i>4</p><p><span class="a">5<i>X</i>6[]e</span>f</p>',
});
});
test("should paste a text when selection across two span (1)", async () => {
await testEditor({
contentBefore: '<p>1a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>1a<span class="a">b1<i>X</i>2</span></p><p>3<i>X</i>4</p><p><span class="a">5<i>X</i>6[]e</span>f</p>',
});
});
test("should paste a text when selection across two span (2)", async () => {
await testEditor({
contentBefore: '<p>2a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>2a<span class="a">b1<i>X</i>2</span></p><p>3<i>X</i>4</p><p><span class="a">5<i>X</i>6[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (6)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
"<div>a<p>b1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
"<div>a<p>b1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (7)", async () => {
await testEditor({
contentBefore: '<div>1ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>1ab<span class="a">c1<i>X</i>2</span><p>3<i>X</i>4</p><span class="a">5<i>X</i>6[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>2a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>2a1<i>X</i>2<p>3<i>X</i>4</p>5<i>X</i>6[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection leave a span (8)", async () => {
await testEditor({
contentBefore: '<p>1ab<span class="a">c[d</span>e]f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>1ab<span class="a">c1<i>X</i>2</span></p><p>3<i>X</i>4</p><p><span class="a">5<i>X</i>6[]</span>f</p>',
});
await testEditor({
contentBefore: '<p>2a[b<span class="a">c]d</span>ef</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>2a1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]<span class="a">d</span>ef</p>',
});
});
test("should paste a text when selection across two element (7)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>1a<p>b1<i>X</i>2</p><p>3<i>X</i>4</p><p>5<i>X</i>6[]<span class="a">e</span>f</p></div>',
});
});
test("should paste a text when selection across two element (8)", async () => {
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>2a<span class="a">b1<i>X</i>2</span><p>3<i>X</i>4</p><span class="a">5<i>X</i>6[]</span>e<br>f</div>',
});
});
});
});
describe("Complex html p+i", () => {
const complexHtmlData =
'<p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 1rem; color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">12</p><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 1rem; color: rgb(0, 0, 0); font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><i style="box-sizing: border-box;">ii</i></p>';
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>12</p><p><i>ii</i>[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>ab12</p><p><i>ii</i>[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a"><i>ii</i>[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>a12</p><p><i>ii</i>[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a"><i>ii</i>[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a"><i>ii</i>[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>x<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b12</span></p><p><span class="a"><i>ii</i>[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (7)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b12</p><p><i>ii</i>[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b12</p><p><i>ii</i>[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (9)", async () => {
await testEditor({
contentBefore: '<div>1ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>1ab<span class="a">c12<i><br>ii</i>[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>2a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a12<i><br>ii</i>[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection leave a span (10)", async () => {
await testEditor({
contentBefore: '<p>1ab<span class="a">c[d</span>e]f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>1ab<span class="a">c12</span></p><p><span class="a"><i>ii</i>[]</span>f</p>',
});
await testEditor({
contentBefore: '<p>2a[b<span class="a">c]d</span>ef</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>2a12</p><p><i>ii</i>[]<span class="a">d</span>ef</p>',
});
});
test("should paste a text when selection across two element (9)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>1a<p>b12</p><p><i>ii</i>[]<span class="a">e</span>f</p></div>',
});
});
test("should paste a text when selection across two element (10)", async () => {
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a<span class="a">b12<i><br>ii</i>[]</span>e<br>f</div>',
});
});
});
});
describe("Complex html 3p+b", () => {
const complexHtmlData = "<p>1<b>23</b></p><p>zzz</p><p>45<b>6</b>7</p>";
describe("range collapsed", () => {
test("should paste a text at the beginning of a p", async () => {
await testEditor({
contentBefore: "<p>[]abcd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]abcd</p>",
});
});
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>ab1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]cd</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<b>23</b></span></p><p>zzz</p><p><span class="a">45<b>6</b>7[]c</span>d</p>',
});
});
});
describe("range not collapsed", () => {
test("should paste a text in a p", async () => {
await testEditor({
contentBefore: "<p>a[bc]d</p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<p>a1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]d</p>",
});
});
test("should paste a text in a span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[cd]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<b>23</b></span></p><p>zzz</p><p><span class="a">45<b>6</b>7[]e</span>f</p>',
});
});
test("should paste a text when selection across two span", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span><span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<b>23</b></span></p><p>zzz</p><p><span class="a">45<b>6</b>7[]e</span>f</p>',
});
await testEditor({
contentBefore: '<p>a<span class="a">b[c</span>- -<span class="a">d]e</span>f</p>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<p>a<span class="a">b1<b>23</b></span></p><p>zzz</p><p><span class="a">45<b>6</b>7[]e</span>f</p>',
});
});
test("should paste a text when selection across two p (8)", async () => {
await testEditor({
contentBefore: "<div>a<p>b[c</p><p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]e</p>f</div>",
});
await testEditor({
contentBefore: "<div>a<p>b[c</p>- -<p>d]e</p>f</div>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: "<div>a<p>b1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]e</p>f</div>",
});
});
test("should paste a text when selection leave a span (11)", async () => {
await testEditor({
contentBefore: '<div>1ab<span class="a">c[d</span>e]f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>1ab<span class="a">c1<b>23</b></span><p>zzz</p><span class="a">45<b>6</b>7[]</span>f</div>',
});
await testEditor({
contentBefore: '<div>2a[b<span class="a">c]d</span>ef</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>2a1<b>23</b><p>zzz</p>45<b>6</b>7[]<span class="a">d</span>ef</div>',
});
});
test("should paste a text when selection across two element (11)", async () => {
await testEditor({
contentBefore: '<div>1a<p>b[c</p><span class="a">d]e</span>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>1a<p>b1<b>23</b></p><p>zzz</p><p>45<b>6</b>7[]<span class="a">e</span>f</p></div>',
});
});
test("should paste a text when selection across two element (12)", async () => {
await testEditor({
contentBefore: '<div>2a<span class="a">b[c</span><p>d]e</p>f</div>',
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter:
'<div>2a<span class="a">b1<b>23</b></span><p>zzz</p><span class="a">45<b>6</b>7[]</span>e<br>f</div>',
});
});
});
});
describe("Complex html div", () => {
const complexHtmlData = `<div><div><span style="color: #fb4934;">abc</span><span style="color: #ebdbb2;">def</span></div><div dir="rtl"><span style="color: #fb4934;">ghi</span><span style="color: #fe8019;">jkl</span></div><div><span style="color: #fb4934;">jkl</span><span style="color: #ebdbb2;">mno</span></div></div>`;
test("should convert div to p", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>abcdef</p><p dir="rtl">ghijkl</p><p>jklmno[]</p>',
});
});
});
describe("Special cases", () => {
describe("lists", () => {
test("should paste a list in a p", async () => {
await testEditor({
contentBefore: "<p>12[]34</p>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>abc</li><li>def</li><li>ghi</li></ul>");
},
contentAfter: "<p>12</p><ul><li>abc</li><li>def</li><li>ghi[]</li></ul><p>34</p>",
});
});
test("should paste the text of an li into another li", async () => {
await testEditor({
contentBefore: "<ul><li>abc</li><li>de[]f</li><li>ghi</li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>123</li></ul>");
},
contentAfter: "<ul><li>abc</li><li>de123[]f</li><li>ghi</li></ul>",
});
});
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: "<ul><li>abc</li><li>de[]f</li><li>ghi</li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>123</li><li>456</li></ul>");
},
contentAfter: "<ul><li>abc</li><li>de123</li><li>456[]f</li><li>ghi</li></ul>",
});
});
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: "<ul><li>abc</li><li>de[]f</li><li>ghi</li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>123</li><li>456</li><li>789</li></ul>");
},
contentAfter:
"<ul><li>abc</li><li>de123</li><li>456</li><li>789[]f</li><li>ghi</li></ul>",
});
});
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: "<ul><li>abc</li><li>def</li><li>ghi[]</li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>123</li><li>456</li></ul>");
},
contentAfter: "<ul><li>abc</li><li>def</li><li>ghi123</li><li>456[]</li></ul>",
});
});
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: "<ul><li>[]abc</li><li>def</li><li>ghi</li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>123</li><li>456</li></ul>");
},
contentAfter: "<ul><li>123</li><li>456[]abc</li><li>def</li><li>ghi</li></ul>",
});
});
test("should insert a list and a p tag inside a new list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><li>abc</li><li>def</li></ul><p>ghi</p>");
},
contentAfter: "<ul><li>abc</li><li>def</li><li>ghi[]</li></ul>",
});
});
test("should insert content ending with a list inside a new list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc</p><ul><li>def</li><li>ghi</li></ul>");
},
contentAfter: "<ul><li>abc</li><li>def</li><li>ghi[]</li></ul>",
});
});
test("should convert a mixed list containing a paragraph into a checklist", async () => {
await testEditor({
contentBefore: `<ul class="o_checklist"><li>[]<br></li></ul>`,
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ul>
<li>abc</li>
<li>def</li>
<li>ghi</li>
</ul>
<p>jkl</p>
<ol>
<li>mno</li>
<li>pqr</li>
<li>stu</li>
</ol>
`)
);
},
contentAfter: unformat(`
<ul class="o_checklist">
<li>abc</li>
<li>def</li>
<li>ghi</li>
<li>jkl</li>
<li>mno</li>
<li>pqr</li>
<li>stu[]</li>
</ul>
`),
});
});
test("should not unwrap a list twice when pasting on new list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<ul><ul><li>abc</li><li>def</li></ul></ul>");
},
contentAfter: `<ul><li class="oe-nested"><ul><li>abc</li><li>def[]</li></ul></li></ul>`,
});
});
test("should paste a nested list into another list", async () => {
await testEditor({
contentBefore: "<ol><li>Alpha</li><li>[]<br></li></ol>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ul>
<li>abc</li>
<li>def</li>
<li class="oe-nested">
<ul>
<li>123</li>
<li>456</li>
</ul>
</li>
</ul>
`)
);
},
contentAfter: unformat(`
<ol>
<li>Alpha</li>
<li>abc</li>
<li>def</li>
<li class="oe-nested">
<ol>
<li>123</li>
<li>456[]</li>
</ol>
</li>
</ol>
`),
});
});
test("should paste a nested list into another list (2)", async () => {
await testEditor({
contentBefore: "<ul><li>Alpha</li><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ol>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ol>
<li class="oe-nested">
<ul class="o_checklist">
<li>abc</li>
</ul>
</li>
<li>def</li>
</ol>
</li>
<li>ghi</li>
</ul>
</li>
<li>jkl</li>
</ol>
`)
);
},
contentAfter: unformat(`
<ul>
<li>Alpha</li>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li>abc</li>
</ul>
</li>
<li>def</li>
</ul>
</li>
<li>ghi</li>
</ul>
</li>
<li>jkl[]</li>
</ul>
`),
});
});
test("should convert a mixed list into a ordered list", async () => {
await testEditor({
contentBefore: "<ol><li>[]<br></li></ol>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ul>
<li>ab</li>
<li>cd</li>
<li class="oe-nested">
<ol>
<li>ef</li>
<li>gh</li>
<li class="oe-nested">
<ul class="o_checklist">
<li>ij</li>
<li>kl</li>
</ul>
</li>
</ol>
</li>
</ul>
`)
);
},
contentAfter: unformat(`
<ol>
<li>ab</li>
<li>cd</li>
<li class="oe-nested">
<ol>
<li>ef</li>
<li>gh</li>
<li class="oe-nested">
<ol>
<li>ij</li>
<li>kl[]</li>
</ol>
</li>
</ol>
</li>
</ol>
`),
});
});
test("should convert a mixed list starting with bullet list into a bullet list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ul>
<li>ab</li>
<li>cd</li>
<li class="oe-nested">
<ol>
<li>ef</li>
<li>gh</li>
<li class="oe-nested">
<ul class="o_checklist">
<li>ij</li>
<li>kl</li>
</ul>
</li>
</ol>
</li>
</ul>
`)
);
},
contentAfter: unformat(`
<ul>
<li>ab</li>
<li>cd</li>
<li class="oe-nested">
<ul>
<li>ef</li>
<li>gh</li>
<li class="oe-nested">
<ul>
<li>ij</li>
<li>kl[]</li>
</ul>
</li>
</ul>
</li>
</ul>
`),
});
});
test("should paste a mixed list starting with deeply nested bullet list into a bullet list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ul>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li>ab</li>
<li>cd</li>
</ul>
</li>
<li>ef</li>
<li>gh</li>
</ul>
</li>
<li>ij</li>
<li>kl</li>
</ul>
</li>
<li>mn</li>
<li>op</li>
</ul>
`)
);
},
contentAfter: unformat(`
<ul>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li class="oe-nested">
<ul>
<li>ab</li>
<li>cd</li>
</ul>
</li>
<li>ef</li>
<li>gh</li>
</ul>
</li>
<li>ij</li>
<li>kl</li>
</ul>
</li>
<li>mn</li>
<li>op[]</li>
</ul>
`),
});
});
test("should paste a deeply nested list copied outside from odoo", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<ol>
<li>ab</li>
<ol>
<li>cd</li>
<li>ef</li>
<ul>
<li>gh</li>
<li>ij</li>
</ul>
<ol>
<li>kl</li>
<li>mn</li>
</ol>
</ol>
<ul>
<li>op</li>
<li>qr</li>
<ol>
<li>st</li>
<li>uv</li>
</ol>
</ul>
</ol>
`)
);
},
contentAfter: unformat(`
<ul>
<li>ab</li>
<li class="oe-nested">
<ul>
<li>cd</li>
<li>ef</li>
<li class="oe-nested">
<ul>
<li>gh</li>
<li>ij</li>
<li>kl</li>
<li>mn</li>
</ul>
</li>
<li>op</li>
<li>qr</li>
<li class="oe-nested">
<ul>
<li>st</li>
<li>uv[]</li>
</ul>
</li>
</ul>
</li>
</ul>
`),
});
});
test("should paste checklist from gdoc", async () => {
await testEditor({
contentBefore: "<p>[]<br></p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
unformat(`
<b style="font-weight:normal;" id="docs-internal-guid-5c9e50d3-7fff-c129-6dcc-e76588942722">
<ul style="margin-top:0;margin-bottom:0;padding-inline-start:28px;">
<li dir="ltr" role="checkbox" aria-checked="false" style="list-style-type:none;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" width="17.599999999999998px" height="17.599999999999998px" alt="unchecked" aria-roledescription="checkbox" style="margin-right:3px;" />
<p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Abc</span></p>
</li>
<li dir="ltr" role="checkbox" aria-checked="false" style="list-style-type:none;font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;" aria-level="1">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" width="17.599999999999998px" height="17.599999999999998px" alt="checked" aria-roledescription="checkbox" style="margin-right:3px;" />
<p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;display:inline-block;vertical-align:top;margin-top:0;" role="presentation"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">def</span></p>
</li>
</ul>
</b>
`)
);
},
contentAfter: `<ul class="o_checklist"><li><p>Abc</p></li><li class="o_checked"><p>def[]</p></li></ul>`,
});
});
});
describe("paragraphs", () => {
test("should paste multiple paragraphs into a list", async () => {
await testEditor({
contentBefore: "<ul><li>[]<br></li></ul>",
stepFunction: async (editor) => {
pasteHtml(editor, "<p>abc</p><p>def</p><p>ghi</p><p>jkl</p><p>mno</p>");
},
contentAfter:
"<ul><li>abc</li><li>def</li><li>ghi</li><li>jkl</li><li>mno[]</li></ul>",
});
});
});
});
describe("pasting within blockquote", () => {
test("should paste paragraph related elements within blockquote", async () => {
await testEditor({
contentBefore: "<blockquote>[]<br></blockquote>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<blockquote><h1>abc</h1><h2>def</h2><h3>ghi[]</h3></blockquote>",
});
await testEditor({
contentBefore: "<blockquote>x[]</blockquote>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<blockquote>x<h1>abc</h1><h2>def</h2><h3>ghi[]</h3></blockquote>",
});
await testEditor({
contentBefore: "<blockquote>[]x</blockquote>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<blockquote><h1>abc</h1><h2>def</h2><h3>ghi[]</h3>x</blockquote>",
});
await testEditor({
contentBefore: "<blockquote>x[]y</blockquote>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<blockquote>x<h1>abc</h1><h2>def</h2><h3>ghi[]</h3>y</blockquote>",
});
});
});
describe("pasting within pre", () => {
test("should paste paragraph releted elements within pre", async () => {
await testEditor({
contentBefore: "<pre>[]<br></pre>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<pre><h1>abc</h1><h2>def</h2><h3>ghi[]</h3></pre>",
});
await testEditor({
contentBefore: "<pre>x[]</pre>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<pre>x<h1>abc</h1><h2>def</h2><h3>ghi[]</h3></pre>",
});
await testEditor({
contentBefore: "<pre>[]x</pre>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<pre><h1>abc</h1><h2>def</h2><h3>ghi[]</h3>x</pre>",
});
await testEditor({
contentBefore: "<pre>x[]y</pre>",
stepFunction: async (editor) => {
pasteHtml(editor, "<h1>abc</h1><h2>def</h2><h3>ghi</h3>");
},
contentAfter: "<pre>x<h1>abc</h1><h2>def</h2><h3>ghi[]</h3>y</pre>",
});
});
});
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: "<p>ab[]cd</p>",
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter: '<p>ab<a href="http://www.xyz.com">http://www.xyz.com</a>[]cd</p>',
});
});
test("should paste and transform an URL in a span (collapsed)", async () => {
await testEditor({
contentBefore: '<p>a<span class="a">b[]c</span>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter:
'<p>a<span class="a">b<a href="http://www.xyz.com">http://www.xyz.com</a>[]c</span>d</p>',
});
});
test("should paste and not transform an URL in a existing link (collapsed)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://existing.com">b[]c</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter: '<p>a<a href="http://existing.com">bhttp://www.xyz.com[]c</a>d</p>',
});
await testEditor({
contentBefore: '<p>a<a href="http://existing.com">b[]c</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "random");
},
contentAfter: '<p>a<a href="http://existing.com">brandom[]c</a>d</p>',
});
});
test("should paste and transform an URL in a existing link if pasting valid url (collapsed)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://existing.com">[]c</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "https://www.xyz.xdc");
},
contentAfter: '<p>a<a href="https://www.xyz.xdcc">https://www.xyz.xdc[]c</a>d</p>',
});
await testEditor({
contentBefore: '<p>a<a href="http://existing.com">b[].com</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "oom");
},
contentAfter: '<p>a<a href="http://boom.com">boom[].com</a>d</p>',
});
});
test("should replace link for new content when pasting in an empty link (collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#" oe-zws-empty-inline="">[]\u200B</a></p>',
stepFunction: async (editor) => {
pasteText(editor, "abc");
},
contentAfter: "<p>abc[]</p>",
});
});
test("should replace link for new content when pasting in an empty link (collapsed)(2)", async () => {
await testEditor({
contentBefore: '<p>xy<a href="#" oe-zws-empty-inline="">\u200B[]</a>z</p>',
stepFunction: async (editor) => {
pasteText(editor, "abc");
},
contentAfter: "<p>xyabc[]z</p>",
});
});
test("should replace link for new content (url) when pasting in an empty link (collapsed)", async () => {
const { el, editor } = await setupEditor(
`<p>xy<a href="#" oe-zws-empty-inline="">\u200B[]</a>z</p>`
);
pasteText(editor, "http://odoo.com");
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>xy<a href="http://odoo.com">http://odoo.com</a>[]z</p>`
);
});
test("should replace link for new content (imgUrl) when pasting in an empty link (collapsed) (1)", async () => {
const { el, editor } = await setupEditor(`<p>xy<a href="#">[]</a>z</p>`);
expect(getContent(el)).toBe(
`<p>xy\ufeff<a href="#" class="o_link_in_selection">\ufeff[]</a>\ufeffz</p>`
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
expect(getContent(el)).toBe(`<p>xy${imgUrl}[]z</p>`);
await press("Enter");
expect(getContent(el)).toBe(`<p>xy<img src="${imgUrl}">[]z</p>`);
});
test("should replace link for new content (url) when pasting in an empty link (collapsed) (2)", async () => {
const { el, editor } = await setupEditor(
`<p>xy<a href="#" oe-zws-empty-inline="">\u200B[]</a>z</p>`
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
expect(getContent(el)).toBe(`<p>xy${imgUrl}[]z</p>`);
await press("ArrowDown");
await press("Enter");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>xy<a href="${imgUrl}">${imgUrl}</a>[]z</p>`
);
});
test("should paste and transform plain text content over an empty link (collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#">[]\u200B</a></p>',
stepFunction: async (editor) => {
pasteText(editor, "abc www.odoo.com xyz");
},
contentAfter: '<p>abc <a href="http://www.odoo.com">www.odoo.com</a> xyz[]</p>',
});
await testEditor({
contentBefore: '<p><a href="#">[]\u200B</a></p>',
stepFunction: async (editor) => {
pasteText(editor, "odoo.com\ngoogle.com");
},
contentAfter:
'<p style="margin-bottom: 0px;"><a href="http://odoo.com">odoo.com</a></p>' +
'<p><a href="http://google.com">google.com</a>[]</p>',
});
});
test("should paste html content over an empty link (collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#">[]\u200B</a></p>',
stepFunction: async (editor) => {
pasteHtml(
editor,
'<a href="www.odoo.com">odoo.com</a><br><a href="www.google.com">google.com</a>'
);
},
contentAfter:
'<p><a href="www.odoo.com">odoo.com</a></p><p><a href="www.google.com">google.com</a>[]</p>',
});
});
test("should paste and transform URL among text (collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
pasteText(editor, `abc ${url} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${url}">${url}</a> def[]</p>`
);
});
test("should paste and transform image URL among text (collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
pasteText(editor, `abc ${imgUrl} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${imgUrl}">${imgUrl}</a> def[]</p>`
);
});
test("should paste and transform video URL among text (collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
pasteText(editor, `abc ${videoUrl} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${videoUrl}">${videoUrl}</a> def[]</p>`
);
});
test("should paste and transform multiple URLs (collapsed) (1)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
pasteText(editor, `${url} ${videoUrl} ${imgUrl}`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p><a href="${url}">${url}</a> <a href="${videoUrl}">${videoUrl}</a> <a href="${imgUrl}">${imgUrl}</a>[]</p>`
);
});
test("should paste and transform multiple URLs (collapsed) (2)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
pasteText(editor, `${url} abc ${videoUrl} def ${imgUrl}`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p><a href="${url}">${url}</a> abc <a href="${videoUrl}">${videoUrl}</a> def <a href="${imgUrl}">${imgUrl}</a>[]</p>`
);
});
test("should paste plain text inside non empty link (collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#">a[]b</a></p>',
stepFunction: async (editor) => {
pasteHtml(editor, "<span>123</span>");
},
contentAfter: '<p><a href="#">a123[]b</a></p>',
});
});
test("should paste and not transform an URL in a pre tag", async () => {
await testEditor({
contentBefore: "<pre>[]<br></pre>",
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter: "<pre>http://www.xyz.com[]</pre>",
});
});
});
describe("range not collapsed", () => {
test("should paste and transform an URL in a p (not collapsed)", async () => {
await testEditor({
contentBefore: "<p>ab[xxx]cd</p>",
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter: '<p>ab<a href="http://www.xyz.com">http://www.xyz.com</a>[]cd</p>',
});
});
test("should paste and transform an URL in a span (not collapsed)", async () => {
await testEditor({
contentBefore:
'<p>a<span class="a">b[x<a href="http://existing.com">546</a>x]c</span>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter:
'<p>a<span class="a">b<a href="http://www.xyz.com">http://www.xyz.com</a>[]c</span>d</p>',
});
});
test("should paste and not transform an URL in a existing link (not collapsed)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://existing.com">b[qsdqsd]c</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "http://www.xyz.com");
},
contentAfter: '<p>a<a href="http://existing.com">bhttp://www.xyz.com[]c</a>d</p>',
});
});
test("should restore selection when pasting plain text followed by UNDO (1) (not collapsed)", async () => {
await testEditor({
contentBefore: "<p>[abc]</p>",
stepFunction: async (editor) => {
pasteText(editor, "def");
undo(editor);
},
contentAfter: "<p>[abc]</p>",
});
});
test("should restore selection when pasting plain text followed by UNDO (2) (not collapsed)", async () => {
await testEditor({
contentBefore: "<p>[abc]</p>",
stepFunction: async (editor) => {
pasteText(editor, "www.odoo.com");
undo(editor);
},
contentAfter: "<p>[abc]</p>",
});
});
test("should restore selection when pasting plain text followed by UNDO (3) (not collapsed)", async () => {
await testEditor({
contentBefore: "<p>[abc]</p>",
stepFunction: async (editor) => {
pasteText(editor, "def www.odoo.com xyz");
undo(editor);
},
contentAfter: "<p>[abc]</p>",
});
});
test("should restore selection after pasting HTML followed by UNDO (not collapsed)", async () => {
await testEditor({
contentBefore: "<p>[abc]</p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
'<a href="www.odoo.com">odoo.com</a><br><a href="www.google.com">google.com</a>'
);
undo(editor);
},
contentAfter: "<p>[abc]</p>",
});
});
test("should paste and transform URL among text (not collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[xyz]</p>");
pasteText(editor, `abc ${url} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${url}">${url}</a> def[]</p>`
);
});
test("should paste and transform image URL among text (not collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[xyz]</p>");
pasteText(editor, `abc ${imgUrl} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${imgUrl}">${imgUrl}</a> def[]</p>`
);
});
test("should paste and transform video URL among text (not collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[xyz]</p>");
pasteText(editor, `abc ${videoUrl} def`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>abc <a href="${videoUrl}">${videoUrl}</a> def[]</p>`
);
});
test("should paste and transform multiple URLs among text (not collapsed)", async () => {
const { el, editor } = await setupEditor("<p>[xyz]</p>");
pasteText(editor, `${url} ${videoUrl} ${imgUrl}`);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p><a href="${url}">${url}</a> <a href="${videoUrl}">${videoUrl}</a> <a href="${imgUrl}">${imgUrl}</a>[]</p>`
);
});
test("should paste and transform URL over the existing url", async () => {
await testEditor({
contentBefore: '<p>ab[<a href="http://www.xyz.com">http://www.xyz.com</a>]cd</p>',
stepFunction: async (editor) => {
pasteText(editor, "https://www.xyz.xdc ");
},
contentAfter: '<p>ab<a href="https://www.xyz.xdc">https://www.xyz.xdc</a> []cd</p>',
});
});
test("should paste and transform URL over the existing url (not collapsed)", async () => {
await testEditor({
contentBefore: '<p>ab[<a href="http://www.xyz.com">http://www.xyz.com</a>]cd</p>',
stepFunction: async (editor) => {
pasteText(editor, "https://www.xyz.xdc ");
},
contentAfter: '<p>ab<a href="https://www.xyz.xdc">https://www.xyz.xdc</a> []cd</p>',
});
});
test("should paste plain text content over a link if all of its contents is selected (not collapsed)", async () => {
await testEditor({
contentBefore: '<p>a<a href="#">[xyz]</a>d</p>',
stepFunction: async (editor) => {
pasteText(editor, "bc");
},
contentAfter: "<p>abc[]d</p>",
});
});
test("should paste and transform plain text content over a link if all of its contents is selected (not collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#">[xyz]</a></p>',
stepFunction: async (editor) => {
pasteText(editor, "www.odoo.com");
},
contentAfter: '<p><a href="http://www.odoo.com">www.odoo.com</a>[]</p>',
});
await testEditor({
contentBefore: '<p><a href="#">[xyz]</a></p>',
stepFunction: async (editor) => {
pasteText(editor, "abc www.odoo.com xyz");
},
contentAfter: '<p>abc <a href="http://www.odoo.com">www.odoo.com</a> xyz[]</p>',
});
});
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(
`<p>ab<a href="http://www.xyz.com">[http://www.xyz.com]</a>cd</p>`
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
expect(getContent(el)).toBe(
`<p>abhttps://download.odoocdn.com/icons/website/static/description/icon.png[]cd</p>`
);
await press("Enter");
expect(getContent(el)).toBe(`<p>ab<img src="${imgUrl}">[]cd</p>`);
});
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(
`<p>ab<a href="http://www.xyz.com">[http://www.xyz.com]</a>cd</p>`
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
expect(getContent(el)).toBe(
`<p>abhttps://download.odoocdn.com/icons/website/static/description/icon.png[]cd</p>`
);
await press("ArrowDown");
await press("Enter");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="${imgUrl}">${imgUrl}</a>[]cd</p>`
);
});
test("should paste html content over a link if all of its contents is selected (not collapsed)", async () => {
await testEditor({
contentBefore: '<p><a href="#">[xyz]</a></p>',
stepFunction: async (editor) => {
pasteHtml(
editor,
'<a href="www.odoo.com">odoo.com</a><br><a href="www.google.com">google.com</a>'
);
},
contentAfter:
'<p><a href="www.odoo.com">odoo.com</a></p><p><a href="www.google.com">google.com</a>[]</p>',
});
});
});
});
describe("images", () => {
describe("range collapsed", () => {
test("should paste and transform an image URL in a p (1)", async () => {
const { el, editor } = await setupEditor("<p>ab[]cd</p>");
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
await press("Enter");
expect(getContent(el)).toBe(`<p>ab<img src="${imgUrl}">[]cd</p>`);
});
test("should paste and transform an image URL in a span", async () => {
const { el, editor } = await setupEditor('<p>a<span class="a">b[]c</span>d</p>');
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
await press("Enter");
expect(getContent(el)).toBe(
`<p>a<span class="a">b<img src="${imgUrl}">[]c</span>d</p>`
);
});
test("should paste and transform an image URL in an existing link", async () => {
const { el, editor } = await setupEditor(
'<p>a<a href="http://existing.com">b[]c</a>d</p>'
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>a<a href="http://existing.com">b<img src="${imgUrl}">[]c</a>d</p>`
);
});
test("should paste an image URL as a link in a p (1)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
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(
`<p><a href="${imgUrl}">${imgUrl}</a>[]</p>`
);
});
test("should not revert a history step when pasting an image URL as a link (1)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
// 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(
`<p>*should not disappear*<a href="${imgUrl}">${imgUrl}</a>[]</p>`
);
});
});
describe("range not collapsed", () => {
test("should paste and transform an image URL in a p (2)", async () => {
const { el, editor } = await setupEditor("<p>ab[xxx]cd</p>");
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
await press("Enter");
expect(cleanLinkArtifacts(getContent(el))).toBe(`<p>ab<img src="${imgUrl}">[]cd</p>`);
});
test("should paste and transform an image URL in a span", async () => {
const { el, editor } = await setupEditor(
'<p>a<span class="a">b[x<a href="http://existing.com">546</a>x]c</span>d</p>'
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(1);
await press("Enter");
expect(getContent(el)).toBe(
`<p>a<span class="a">b<img src="${imgUrl}">[]c</span>d</p>`
);
});
test("should paste and transform an image URL inside an existing link", async () => {
const { el, editor } = await setupEditor(
'<p>a<a href="http://existing.com">b[qsdqsd]c</a>d</p>'
);
pasteText(editor, imgUrl);
await animationFrame();
expect(".o-we-powerbox").toHaveCount(0);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>a<a href="http://existing.com">b<img src="${imgUrl}">[]c</a>d</p>`
);
});
test("should paste an image URL as a link in a p (2)", async () => {
const { el, editor } = await setupEditor("<p>ab[xxx]cd</p>");
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(
`<p>ab<a href="${imgUrl}">${imgUrl}</a>[]cd</p>`
);
});
test("should not revert a history step when pasting an image URL as a link (2)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
// paste text (to have a history step recorded)
pasteText(editor, "abxxxcd");
// select xxx in "<p>ab[xxx]cd</p>""
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(
`<p>ab<a href="${imgUrl}">${imgUrl}</a>[]cd</p>`
);
});
test("should restore selection after pasting image URL followed by UNDO (1)", async () => {
const { el, editor } = await setupEditor("<p>[abc]</p>");
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("<p>[abc]</p>");
});
test("should restore selection after pasting image URL followed by UNDO (2)", async () => {
const { el, editor } = await setupEditor("<p>[abc]</p>");
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("<p>[abc]</p>");
});
});
});
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("<p>ab[]cd</p>");
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(
`<p>ab</p><div data-oe-expression="${videoUrl}" class="media_iframe_video" contenteditable="false"><div class="css_editable_mode_display"></div><div class="media_iframe_video_size" contenteditable="false"></div><iframe frameborder="0" contenteditable="false" allowfullscreen="allowfullscreen" src="${videoUrl}"></iframe></div><p>[]cd</p>`
);
});
test("should paste and transform a youtube URL in a span (1)", async () => {
const { el, editor } = await setupEditor('<p>a<span class="a">b[]c</span>d</p>');
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(
'<p>a<span class="a">b</span></p><div data-oe-expression="https://youtu.be/dQw4w9WgXcQ" class="media_iframe_video" contenteditable="false"><div class="css_editable_mode_display"></div><div class="media_iframe_video_size" contenteditable="false"></div><iframe frameborder="0" contenteditable="false" allowfullscreen="allowfullscreen" src="https://youtu.be/dQw4w9WgXcQ"></iframe></div><p><span class="a">[]c</span>d</p>'
);
});
test("should paste and not transform a youtube URL in a existing link", async () => {
const { el, editor, plugins } = await setupEditor(
'<p>a<a href="http://existing.com">b[]c</a>d</p>'
);
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(
'<p>a<a href="http://existing.com">bhttps://youtu.be/dQw4w9WgXcQ[]c</a>d</p>'
);
});
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("<p>[]</p>");
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(`<p><a href="${url}">${url}</a>[]</p>`);
});
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("<p>[]</p>");
// 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(
`<p>*should not disappear*<a href="${url}">${url}</a>[]</p>`
);
});
});
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("<p>ab[xxx]cd</p>");
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(
'<p>ab</p><div data-oe-expression="https://youtu.be/dQw4w9WgXcQ" class="media_iframe_video" contenteditable="false"><div class="css_editable_mode_display"></div><div class="media_iframe_video_size" contenteditable="false"></div><iframe frameborder="0" contenteditable="false" allowfullscreen="allowfullscreen" src="https://youtu.be/dQw4w9WgXcQ"></iframe></div><p>[]cd</p>'
);
});
test("should paste and transform a youtube URL in a span (2)", async () => {
const { el, editor } = await setupEditor(
'<p>a<span class="a">b[x<a href="http://existing.com">546</a>x]c</span>d</p>'
);
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(
`<p>a<span class="a">b</span></p><div data-oe-expression="${videoUrl}" class="media_iframe_video" contenteditable="false"><div class="css_editable_mode_display"></div><div class="media_iframe_video_size" contenteditable="false"></div><iframe frameborder="0" contenteditable="false" allowfullscreen="allowfullscreen" src="${videoUrl}"></iframe></div><p><span class="a">[]c</span>d</p>`
);
});
test("should paste and not transform a youtube URL in a existing link", async () => {
const { el, editor, plugins } = await setupEditor(
'<p>a<a href="http://existing.com">b[qsdqsd]c</a>d</p>'
);
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(
`<p>a<a href="http://existing.com">b${videoUrl}[]c</a>d</p>`
);
});
test("should paste a youtube URL as a link in a p (2)", async () => {
const { el, editor } = await setupEditor("<p>ab[xxx]cd</p>");
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(
`<p>ab<a href="${videoUrl}">${videoUrl}</a>[]cd</p>`
);
});
test("should not revert a history step when pasting a youtube URL as a link (2)", async () => {
const { el, editor } = await setupEditor("<p>[]</p>");
// paste text (to have a history step recorded)
pasteText(editor, "abxxxcd");
// select xxx in "<p>ab[xxx]cd</p>"
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(
`<p>ab<a href="${videoUrl}">${videoUrl}</a>[]cd</p>`
);
});
test("should restore selection after pasting video URL followed by UNDO (1)", async () => {
const { el, editor } = await setupEditor("<p>[abc]</p>");
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("<p>[abc]</p>");
});
test("should restore selection after pasting video URL followed by UNDO (2)", async () => {
const { el, editor } = await setupEditor("<p>[abc]</p>");
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("<p>[abc]</p>");
});
});
});
describe("Odoo editor own html", () => {
test("should paste html as is", async () => {
await testEditor({
contentBefore: "<p>a[]b</p>",
stepFunction: async (editor) => {
pasteOdooEditorHtml(editor, '<div class="custom-paste">b</div>');
},
contentAfter: '<p>a</p><div class="custom-paste">b</div><p>[]b</p>',
});
});
test("should not paste unsafe content", async () => {
await testEditor({
contentBefore: "<p>a[]b</p>",
stepFunction: async (editor) => {
pasteOdooEditorHtml(editor, `<script>console.log('xss attack')</script>`);
},
contentAfter: "<p>a[]b</p>",
});
});
});
describe("editable in iframe", () => {
test("should paste odoo-editor html", async () => {
const { el, editor } = await setupEditor("<p>[]</p>", { props: { iframe: true } });
pasteOdooEditorHtml(editor, `<p>text<b>bold text</b>more text</p>`);
expect(getContent(el)).toBe("<p>text<b>bold text</b>more text[]</p>");
});
});
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: "<p>[]</p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
`<div ccp_infra_version='3' ccp_infra_timestamp='1684505961078' ccp_infra_user_hash='540904553' ccp_infra_copy_id=''
data-ccp-timestamp='1684505961078'>
<html>
<head>
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
<meta name=ProgId content=Excel.Sheet>
<meta name=Generator content="Microsoft Excel 15">
<style>
table {
mso-displayed-decimal-separator: "\\,";
mso-displayed-thousand-separator: "\\.";
}
tr {
mso-height-source: auto;
}
col {
mso-width-source: auto;
}
td {
padding-top: 1px;
padding-right: 1px;
padding-left: 1px;
mso-ignore: padding;
color: black;
font-size: 11.0pt;
font-weight: 400;
font-style: normal;
text-decoration: none;
font-family: Calibri, sans-serif;
mso-font-charset: 0;
text-align: general;
vertical-align: bottom;
border: none;
white-space: nowrap;
mso-rotate: 0;
}
.font12 {
color: #495057;
font-size: 10.0pt;
font-weight: 400;
font-style: italic;
text-decoration: none;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 0;
}
.font13 {
color: #495057;
font-size: 10.0pt;
font-weight: 700;
font-style: italic;
text-decoration: none;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 0;
}
.font33 {
color: #495057;
font-size: 10.0pt;
font-weight: 700;
font-style: normal;
text-decoration: none;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 0;
}
.xl87 {
font-size: 14.0pt;
font-family: "Roboto Mono";
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
}
.xl88 {
color: #495057;
font-size: 10.0pt;
font-style: italic;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 0;
text-align: center;
}
.xl89 {
color: #495057;
font-size: 10.0pt;
font-style: italic;
font-family: Arial;
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
}
.xl90 {
color: #495057;
font-size: 10.0pt;
font-weight: 700;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 0;
text-align: center;
}
.xl91 {
color: #495057;
font-size: 10.0pt;
font-weight: 700;
text-decoration: underline;
text-underline-style: single;
font-family: Arial;
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
}
.xl92 {
color: red;
font-size: 10.0pt;
font-family: Arial;
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
}
.xl93 {
color: red;
font-size: 10.0pt;
text-decoration: underline;
text-underline-style: single;
font-family: Arial;
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
}
.xl94 {
color: #495057;
font-size: 10.0pt;
font-family: "Odoo Unicode Support Noto";
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
background: yellow;
mso-pattern: black none;
}
.xl95 {
color: red;
font-size: 10.0pt;
font-family: Arial;
mso-generic-font-family: auto;
mso-font-charset: 1;
text-align: center;
background: yellow;
mso-pattern: black none;
white-space: normal;
}
</style>
</head>
<body link="#0563C1" vlink="#954F72">
<table width=398 style='border-collapse:collapse;width:299pt'><!--StartFragment-->
<col width=187 style='width:140pt'>
<col width=211 style='width:158pt'>
<tr height=20 style='height:15.0pt'>
<td width=187 height=20 class=xl88 dir=LTR style='width:140pt;height:15.0pt'><span class=font12>Italic
then also </span><span class=font13>BOLD</span></td>
<td width=211 class=xl89 dir=LTR style='width:158pt'><s>Italic strike</s></td>
</tr>
<tr height=20 style='height:15.0pt'>
<td height=20 class=xl90 dir=LTR style='height:15.0pt'><span class=font33>Just bold </span><span
class=font12>Just Italic</span></td>
<td class=xl91 dir=LTR>Bold underline</td>
</tr>
<tr height=20 style='height:15.0pt'>
<td height=20 class=xl92 dir=LTR style='height:15.0pt'>Color text</td>
<td class=xl93 dir=LTR><s>Color strike and underline</s></td>
</tr>
<tr height=20 style='height:15.0pt'>
<td height=20 class=xl94 dir=LTR style='height:15.0pt'>Color background</td>
<td width=211 class=xl95 dir=LTR style='width:158pt'>Color text on color background</td>
</tr>
<tr height=27 style='height:20.25pt'>
<td colspan=2 width=398 height=27 class=xl87 dir=LTR style='width:299pt;height:20.25pt'>14pt MONO TEXT
</td>
</tr><!--EndFragment-->
</table>
</body>
</html>
</div>`
);
},
contentAfter: `<table class="table table-bordered">
${" "}
${" "}
<tbody><tr>
<td>Italic
then also BOLD</td>
<td><s>Italic strike</s></td>
</tr>
<tr>
<td>Just bold Just Italic</td>
<td>Bold underline</td>
</tr>
<tr>
<td>Color text</td>
<td><s>Color strike and underline</s></td>
</tr>
<tr>
<td>Color background</td>
<td>Color text on color background</td>
</tr>
<tr>
<td>14pt MONO TEXT
</td>
</tr>
</tbody></table><p>
${" "}
${" "}
[]</p>`,
});
});
test("should keep all allowed style (Google Sheets)", async () => {
await testEditor({
contentBefore: "<p>[]</p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
`<google-sheets-html-origin>
<style type="text/css">
td {
border: 1px solid #cccccc;
}
br {
mso-data-placement: same-cell;
}
</style>
<table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1"
style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none">
<colgroup>
<col width="170" />
<col width="187" />
</colgroup>
<tbody>
<tr style="height:21px;">
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-family:Odoo Unicode Support Noto;font-weight:normal;font-style:italic;color:#495057;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Italic then also BOLD&quot;}"
data-sheets-textstyleruns="{&quot;1&quot;:0,&quot;2&quot;:{&quot;3&quot;:&quot;Arial&quot;}}{&quot;1&quot;:17,&quot;2&quot;:{&quot;3&quot;:&quot;Arial&quot;,&quot;5&quot;:1}}">
<span style="font-size:10pt;font-family:Arial;font-style:italic;color:#495057;">Italic then also
</span><span
style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:italic;color:#495057;">BOLD</span>
</td>
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;text-decoration:line-through;color:#495057;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Italic strike&quot;}">Italic strike</td>
</tr>
<tr style="height:21px;">
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-family:Odoo Unicode Support Noto;font-weight:bold;color:#495057;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Just bold Just italic&quot;}"
data-sheets-textstyleruns="{&quot;1&quot;:0,&quot;2&quot;:{&quot;3&quot;:&quot;Arial&quot;}}{&quot;1&quot;:10,&quot;2&quot;:{&quot;3&quot;:&quot;Arial&quot;,&quot;5&quot;:0,&quot;6&quot;:1}}">
<span
style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;color:#495057;">Just
Bold </span><span style="font-size:10pt;font-family:Arial;font-style:italic;color:#495057;">Just
Italic</span>
</td>
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;text-decoration:underline;color:#495057;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Bold underline&quot;}">Bold underline</td>
</tr>
<tr style="height:21px;">
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Color text&quot;}"><span style="color:#ff0000;">Color text</span></td>
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline line-through;color:#ff0000;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Color strike and underline&quot;}">Color
strike and underline</td>
</tr>
<tr style="height:21px;">
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;background-color:#ffff00;font-family:Odoo Unicode Support Noto;font-weight:normal;color:#495057;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Color background&quot;}">Color background
</td>
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;background-color:#ffff00;color:#ff0000;"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Color text on color background&quot;}">Color
text on color background</td>
</tr>
<tr style="height:21px;">
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-family:Roboto Mono;font-size:14pt;font-weight:normal;text-align:center;"
rowspan="1" colspan="2"
data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;14pt MONO TEXT&quot;}">14pt MONO TEXT</td>
</tr>
</tbody>
</table>
</google-sheets-html-origin>`
);
},
contentAfter: `<table class="table table-bordered">
${" "}
${" "}
${" "}
${" "}
<tbody>
<tr>
<td>
Italic then also
BOLD
</td>
<td>Italic strike</td>
</tr>
<tr>
<td>
Just
Bold Just
Italic
</td>
<td>Bold underline</td>
</tr>
<tr>
<td>Color text</td>
<td>Color
strike and underline</td>
</tr>
<tr>
<td>Color background
</td>
<td>Color
text on color background</td>
</tr>
<tr>
<td>14pt MONO TEXT</td>
</tr>
</tbody>
</table><p>
[]</p>`,
});
});
test("should keep all allowed style (Libre Office)", async () => {
await testEditor({
contentBefore: "<p>[]</p>",
stepFunction: async (editor) => {
pasteHtml(
editor,
`<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title></title>
<meta name="generator" content="LibreOffice 6.4.7.2 (Linux)" />
<style type="text/css">
body,
div,
table,
thead,
tbody,
tfoot,
tr,
th,
td,
p {
font-family: "Arial";
font-size: x-small
}
a.comment-indicator:hover+comment {
background: #ffd;
position: absolute;
display: block;
border: 1px solid black;
padding: 0.5em;
}
a.comment-indicator {
background: red;
display: inline-block;
border: 1px solid black;
width: 0.5em;
height: 0.5em;
}
comment {
display: none;
}
</style>
</head>
<body>
<table cellspacing="0" border="0">
<colgroup width="212"></colgroup>
<colgroup width="209"></colgroup>
<tr>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
height="20" align="left"><i>Italic then also BOLD</i></td>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
align="left"><i><s>Italic strike</s></i></td>
</tr>
<tr>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
height="20" align="left"><b>Just bold Just italic</b></td>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
align="left"><b><u>Bold underline</u></b></td>
</tr>
<tr>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
height="20" align="left">
<font color="#FF0000">Color text</font>
</td>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
align="left"><u><s>
<font color="#FF0000">Color strike and underline</font>
</s></u></td>
</tr>
<tr>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
height="20" align="left" bgcolor="#FFFF00">Color background</td>
<td style="border-top: 1px solid #000000; border-bottom: 1px solid #000000; border-left: 1px solid #000000; border-right: 1px solid #000000"
align="left" bgcolor="#FFFF00">
<font color="#FF0000">Color text on color background</font>
</td>
</tr>
<tr>
<td colspan=2 height="26" align="center" valign=middle>
<font face="Andale Mono" size=4>14pt MONO TEXT</font>
</td>
</tr>
</table>
</body>
</html>`
);
},
contentAfter: `<table class="table table-bordered">
${" "}
${" "}
<tbody><tr>
<td><i>Italic then also BOLD</i></td>
<td><i><s>Italic strike</s></i></td>
</tr>
<tr>
<td><b>Just bold Just italic</b></td>
<td><b><u>Bold underline</u></b></td>
</tr>
<tr>
<td>
Color text
</td>
<td><u><s>
Color strike and underline
</s></u></td>
</tr>
<tr>
<td>Color background</td>
<td>
Color text on color background
</td>
</tr>
<tr>
<td>
14pt MONO TEXT
</td>
</tr>
</tbody></table><p>
[]</p>`,
});
});
});
describe("onDrop", () => {
test("should add text from htmlTransferItem", async () => {
const { el } = await setupEditor("<p>a[b]cd</p>");
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("<p>abcb[]d</p>");
});
test("should not be able to paste inside some branded node", async () => {
const { el } = await setupEditor(`<p data-oe-model="foo" data-oe-type="text">a[b]cd</p>`);
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(`<p data-oe-model="foo" data-oe-type="text">a[b]cd</p>`);
});
test("should add new images form fileTransferItems", async () => {
const { el } = await setupEditor(`<p>a[b]cd</p>`);
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(
`<p>abc<img class="img-fluid" data-file-name="image.png" src="${base64Image}">[]d</p>`
);
});
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(
`<p>a[<img class="img-fluid" data-file-name="image.png" src="${base64Image}">]bc</p>`
);
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(
`<img class="img-fluid" data-file-name="image.png" src="${base64Image}">`
);
const dropData = new DataTransfer();
dropData.setData(
"text/html",
`<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"><img src="${base64Image}">`
);
// 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(
`<p>ab<img class="img-fluid" data-file-name="image.png" src="${base64Image}">[]c</p>`
);
});
});
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 });
}