replace stale web_editor with html_editor and html_builder for 19.0

web_editor was removed in Odoo 19.0 and replaced by html_editor
and html_builder. The old web_editor was incorrectly included in
the 19.0 vanilla import.

🤖 assisted by claude
This commit is contained in:
Ernad Husremovic 2026-03-09 15:31:13 +01:00
parent 4b94f0abc5
commit f866779561
1513 changed files with 396049 additions and 358525 deletions

View file

@ -0,0 +1,321 @@
import { describe, expect, test } from "@odoo/hoot";
import {
click,
edit,
hover,
queryAll,
queryOne,
select,
waitFor,
waitForNone,
manuallyDispatchProgrammaticEvent,
} from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { contains } from "@web/../tests/web_test_helpers";
import { setupEditor } from "../_helpers/editor";
import { cleanLinkArtifacts, unformat } from "../_helpers/format";
import { getContent, setSelection } from "../_helpers/selection";
import { insertText } from "../_helpers/user_actions";
describe("button style", () => {
test("editable button should have cursor text", async () => {
const { el } = await setupEditor(
'<p><a href="#" class="btn btn-fill-primary">Link styled as button</a></p>'
);
const button = el.querySelector("a");
expect(button).toHaveStyle({ cursor: "text" });
});
test("non-editable .btn-link should have cursor pointer", async () => {
const { el } = await setupEditor(
// A simpliflied version of an embedded component with toolbar
// buttons, as it happens in certain flows in Knowledge.
unformat(`
<div contenteditable="false" data-embedded="clipboard">
<span class="o_embedded_toolbar">
<button class="btn">I am a button</button>
</span>
</div>
`)
);
const button = el.querySelector(".o_embedded_toolbar button");
expect(button).toHaveStyle({ cursor: "pointer" });
});
test("editable button is user-selectable", async () => {
await setupEditor('<p><a href="#" class="btn test-btn">button</a></p>');
expect(queryOne(".test-btn")).toHaveStyle({ userSelect: "auto" });
});
test("non-editable button should not be user-selectable", async () => {
const { el } = await setupEditor('<p><a href="#" class="btn test-btn">button</a></p>');
el.setAttribute("contenteditable", "false");
expect(queryOne(".test-btn")).toHaveStyle({ userSelect: "none" });
});
});
const allowCustomOpt = {
config: {
allowCustomStyle: true,
},
};
const allowTargetBlankOpt = {
config: {
allowTargetBlank: true,
},
};
describe("Custom button style", () => {
test("Editor don't allow custom style by default", async () => {
await setupEditor('<p><a href="https://test.com/">link[]Label</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
const optionsvalues = [...queryOne('select[name="link_type"]').options].map(
(opt) => opt.label
);
expect(optionsvalues).toInclude("Link");
expect(optionsvalues).toInclude("Button Primary");
expect(optionsvalues).toInclude("Button Secondary");
expect(optionsvalues).not.toInclude("Custom");
});
test("Editor don't allow target blank style by default", async () => {
await setupEditor('<p><a href="https://test.com/">link[]Label</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
const count = queryAll(".target-blank-option").length;
expect(count).toBe(0);
});
test("Editor allow custom Style if config is active", async () => {
await setupEditor('<p><a href="https://test.com/">link[]Label</a></p>', allowCustomOpt);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
const optionsvalues = [...queryOne('select[name="link_type"]').options].map(
(opt) => opt.label
);
expect(optionsvalues).toInclude("Link");
expect(optionsvalues).toInclude("Button Primary");
expect(optionsvalues).toInclude("Button Secondary");
expect(optionsvalues).toInclude("Custom");
});
test("The link popover should load the current custom format correctly", async () => {
await setupEditor(
'<p><a href="https://test.com/" class="btn btn-custom" style="color: rgb(0, 255, 0); background-color: rgb(0, 0, 255); border-width: 4px; border-color: rgb(255, 0, 0); border-style: dotted;">link[]Label</a></p>',
allowCustomOpt
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
expect(".o_we_label_link").toHaveValue("linkLabel");
expect(".o_we_href_input_link").toHaveValue("https://test.com/");
expect(queryOne('select[name="link_type"]').selectedOptions[0].value).toBe("custom");
expect(queryOne(".custom-text-picker").style.backgroundColor).toBe("rgb(0, 255, 0)");
expect(queryOne(".custom-fill-picker").style.backgroundColor).toBe("rgb(0, 0, 255)");
expect(queryOne(".custom-border-picker").style.backgroundColor).toBe("rgb(255, 0, 0)");
expect(queryOne(".custom-border-size").value).toBe("4");
expect(queryOne(".custom-border-style").value).toBe("dotted");
});
test.tags("desktop");
test("The color preview should be reset after cursor is out of the colorpicker", async () => {
await setupEditor(
'<p><a href="https://test.com/" class="btn btn-custom" style="color: rgb(0, 255, 0); background-color: rgb(0, 0, 255); border-width: 4px; border-color: rgb(255, 0, 0); border-style: dotted;">link[]Label</a></p>',
allowCustomOpt
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
await click(".custom-fill-picker");
await animationFrame();
await hover(".o_color_button[data-color='#00FF00']");
await animationFrame();
expect(queryOne(".custom-fill-picker").style.backgroundColor).toBe("rgb(0, 255, 0)");
await hover(".custom-fill-picker"); // cursor out of the colorpicker
await animationFrame();
expect(queryOne(".custom-fill-picker").style.backgroundColor).toBe("rgb(0, 0, 255)");
});
test("should convert all selected text to a custom button", async () => {
const { el } = await setupEditor("<p>[Hello]</p>", allowCustomOpt);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("http://test.test/", {
confirm: false,
});
await click('select[name="link_type"]');
await select("custom");
await animationFrame();
await click(".custom-text-picker");
await animationFrame();
await click(".o_color_button[data-color='#FF0000']");
await animationFrame();
await click(".custom-fill-picker");
await animationFrame();
await click(".o_color_button[data-color='#00FF00']");
await animationFrame();
await click("input.custom-border-size");
await edit("1");
await click(waitFor(".custom-border-picker"));
await animationFrame();
await click(".o_color_button[data-color='#0000FF']");
await animationFrame();
await contains(".custom-border input.custom-border-size").edit("6", {
confirm: false,
});
await click('select[name="link_style_border"]');
await select("dotted");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.test/" class="btn btn-custom" style="color: #FF0000; background-color: #00FF00; border-width: 6px; border-color: #0000FF; border-style: dotted; ">Hello</a></p>'
);
await click(".o_we_apply_link");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.test/" class="btn btn-custom" style="color: #FF0000; background-color: #00FF00; border-width: 6px; border-color: #0000FF; border-style: dotted; ">Hello[]</a></p>'
);
});
test("should allow target _blank on custom button", async () => {
const { el } = await setupEditor("<p>[Hello]</p>", allowTargetBlankOpt);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("http://test.test/", {
confirm: false,
});
await click(".o-we-linkpopover .fa-gear");
await contains(".o_advance_option_panel .target-blank-option").click();
await click(".o_advance_option_panel .fa-angle-left");
await waitFor(".o-we-linkpopover");
await animationFrame();
await click(".o_we_apply_link");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.test/" target="_blank">Hello[]</a></p>'
);
});
test("Editor allow custom shape if config is active", async () => {
const { el } = await setupEditor(
'<p><a href="https://test.com/">link[]Label</a></p>',
allowCustomOpt
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await animationFrame();
await click('select[name="link_type"]');
await select("custom");
await animationFrame();
// test outline
await click('select[name="link_style_shape"]');
await select("outline");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="https://test.com/" class="btn btn-outline-custom" style="color: rgb(0, 0, 0); background-color: rgb(166, 227, 226); border-width: 1px; border-color: rgb(0, 143, 140); border-style: dashed; ">linkLabel</a></p>'
);
// test fill + rounded
await click('select[name="link_style_shape"]');
await select("fill rounded-circle");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="https://test.com/" class="rounded-circle btn btn-fill-custom" style="color: rgb(0, 0, 0); background-color: rgb(166, 227, 226); border-width: 1px; border-color: rgb(0, 143, 140); border-style: dashed; ">linkLabel</a></p>'
);
await click(".o_we_apply_link");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="https://test.com/" class="rounded-circle btn btn-fill-custom" style="color: rgb(0, 0, 0); background-color: rgb(166, 227, 226); border-width: 1px; border-color: rgb(0, 143, 140); border-style: dashed; ">link[]Label</a></p>'
);
});
});
describe("button edit", () => {
test("button link should be editable with double click select", async () => {
const { el, editor } = await setupEditor(
'<p>this is a <a href="http://test.test/">link</a></p>'
);
await waitForNone(".o-we-linkpopover");
const button = el.querySelector("a");
// simulate double click selection
setSelection({ anchorNode: button, anchorOffset: 0 });
manuallyDispatchProgrammaticEvent(button, "mousedown", { detail: 2 });
await animationFrame();
expect(getContent(el)).toBe(
'<p>this is a \ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[link]\ufeff</a>\ufeff</p>'
);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/">[link]</a></p>'
);
await insertText(editor, "X");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/">X[]</a></p>'
);
});
test("double click select on a link should stay inside the link (1)", async () => {
const { el } = await setupEditor(
'<p>this is a <a href="http://test.test/">test b[]tn</a><a href="http://test2.test/">test btn2</a></p>'
);
const link = el.querySelector("a[href='http://test.test/']");
// simulate double click selection
manuallyDispatchProgrammaticEvent(link, "mousedown", { detail: 2 });
await animationFrame();
expect(getContent(el)).toBe(
'<p>this is a \ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefftest [btn]\ufeff</a>\ufeff<a href="http://test2.test/">\ufefftest btn2\ufeff</a>\ufeff</p>'
);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/">test [btn]</a><a href="http://test2.test/">test btn2</a></p>'
);
});
test("double click select on a link should stay inside the link (2)", async () => {
const { el } = await setupEditor(
'<p>this is a <a href="http://test.test/">test btn</a><a href="http://test2.test/">t[]est btn2</a></p>'
);
const link = el.querySelector("a[href='http://test2.test/']");
// simulate double click selection
manuallyDispatchProgrammaticEvent(link, "mousedown", { detail: 2 });
await animationFrame();
expect(getContent(el)).toBe(
'<p>this is a \ufeff<a href="http://test.test/">\ufefftest btn\ufeff</a>\ufeff<a href="http://test2.test/" class="o_link_in_selection">\ufeff[test] btn2\ufeff</a>\ufeff</p>'
);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/">test btn</a><a href="http://test2.test/">[test] btn2</a></p>'
);
});
test("triple click select should select the full button text", async () => {
const { el, editor } = await setupEditor(
'<p>this is a <a href="http://test.test/" class="btn btn-fill-primary">test b[]tn</a></p>'
);
const button = el.querySelector("a");
// simulate triple click selection
manuallyDispatchProgrammaticEvent(button, "mousedown", { detail: 3 });
await animationFrame();
expect(getContent(el)).toBe(
'<p>this is a \ufeff<a href="http://test.test/" class="btn btn-fill-primary">[\ufefftest btn\ufeff]</a>\ufeff</p>'
);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/" class="btn btn-fill-primary">[test btn]</a></p>'
);
await insertText(editor, "X");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.test/" class="btn btn-fill-primary">X[]</a></p>'
);
});
});

View file

@ -0,0 +1,100 @@
import { describe, test } from "@odoo/hoot";
import { deleteBackward, deleteImage } from "../_helpers/user_actions";
import { base64Img, testEditor } from "../_helpers/editor";
describe("delete selection involving links", () => {
test("should remove link", async () => {
await testEditor({
contentBefore: '<p><a href="#">[abc</a>d]ef</p>',
contentBeforeEdit: '<p>\ufeff<a href="#">\ufeff[abc\ufeff</a>\ufeffd]ef</p>',
stepFunction: deleteBackward,
contentAfterEdit: "<p>[]ef</p>",
contentAfter: "<p>[]ef</p>",
});
});
test("should remove link (2)", async () => {
await testEditor({
contentBefore: '<p>ab[c<a href="#">def]</a></p>',
contentBeforeEdit: '<p>ab[c\ufeff<a href="#">\ufeffdef]\ufeff</a>\ufeff</p>',
stepFunction: deleteBackward,
contentAfterEdit: "<p>ab[]</p>",
contentAfter: "<p>ab[]</p>",
});
});
test("should not remove link (only after clean)", async () => {
await testEditor({
contentBefore: '<p><a href="#">[abc]</a>def</p>',
contentBeforeEdit:
'<p>\ufeff<a href="#" class="o_link_in_selection">\ufeff[abc]\ufeff</a>\ufeffdef</p>',
stepFunction: deleteBackward,
contentAfterEdit:
'<p>\ufeff<a href="#" class="o_link_in_selection">\ufeff[]\ufeff</a>\ufeffdef</p>',
contentAfter: "<p>[]def</p>",
});
});
});
describe("delete images in a link", () => {
test("should remove link", async () => {
await testEditor({
contentBefore: `<p>x<a href="http://test.test/">[<img src="${base64Img}">]</a></p>`,
stepFunction: deleteImage,
contentAfter: `<p>x[]</p>`,
});
});
test("should not remove unremovable link", async () => {
await testEditor({
contentBefore: `<p>x<a class="oe_unremovable" href="http://test.test/">[<img src="${base64Img}">]</a></p>`,
stepFunction: deleteImage,
contentAfter: `<p>x<a class="oe_unremovable" href="http://test.test/">[]</a></p>`,
});
});
});
describe("empty list items, starting and ending with links", () => {
// Since we introduce \ufeff characters in and around links, we
// can enter situations where the links aren't technically fully
// selected but should be treated as if they were. These tests
// are there to ensure that is the case. They represent four
// variations of the same situation, and have the same expected
// result.
const tests = [
// (1) <a>[...</a>...<a>...]</a>
'<ul><li>ab</li><li><a href="#">[cd</a></li><li>ef</li><li><a href="#a">gh]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">[\ufeffcd</a></li><li>ef</li><li><a href="#a">gh\ufeff]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">\ufeff[cd</a></li><li>ef</li><li><a href="#a">gh\ufeff]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">[\ufeffcd</a></li><li>ef</li><li><a href="#a">gh]\ufeff</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">\ufeff[cd</a></li><li>ef</li><li><a href="#a">gh]\ufeff</a></li><li>ij</li></ul>',
// (2) [<a>...</a>...<a>...]</a>
'<ul><li>ab</li><li>[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li>[\ufeff<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh\ufeff]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li>\ufeff[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh\ufeff]</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li>[\ufeff<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh]\ufeff</a></li><li>ij</li></ul>',
'<ul><li>ab</li><li>\ufeff[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh]\ufeff</a></li><li>ij</li></ul>',
// (3) <a>[...</a>...<a>...</a>]
'<ul><li>ab</li><li><a href="#">[cd</a></li><li>ef</li><li><a href="#a">gh</a>]</li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">[\ufeffcd</a></li><li>ef</li><li><a href="#a">gh</a>\ufeff]</li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">\ufeff[cd</a></li><li>ef</li><li><a href="#a">gh</a>\ufeff]</li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">[\ufeffcd</a></li><li>ef</li><li><a href="#a">gh</a>]\ufeff</li><li>ij</li></ul>',
'<ul><li>ab</li><li><a href="#">\ufeff[cd</a></li><li>ef</li><li><a href="#a">gh</a>]\ufeff</li><li>ij</li></ul>',
// (4) [<a>...</a>...<a>...</a>]
'<ul><li>ab</li><li>[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh</a>]</li><li>ij</li></ul>',
'<ul><li>ab</li><li>[\ufeff<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh</a>\ufeff]</li><li>ij</li></ul>',
'<ul><li>ab</li><li>\ufeff[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh</a>\ufeff]</li><li>ij</li></ul>',
'<ul><li>ab</li><li>[\ufeff<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh</a>]\ufeff</li><li>ij</li></ul>',
'<ul><li>ab</li><li>\ufeff[<a href="#">cd</a></li><li>ef</li><li><a href="#a">gh</a>]\ufeff</li><li>ij</li></ul>',
];
let testIndex = 1;
for (const contentBefore of tests) {
test(`should empty list items, starting and ending with links (${testIndex})`, async () => {
await testEditor({
contentBefore,
stepFunction: deleteBackward,
contentAfterEdit:
'<ul><li>ab</li><li o-we-hint-text="List" class="o-we-hint">[]<br></li><li>ij</li></ul>',
contentAfter: "<ul><li>ab</li><li>[]<br></li><li>ij</li></ul>",
});
});
testIndex += 1;
}
});

View file

@ -0,0 +1,170 @@
import { describe, test } from "@odoo/hoot";
import { deleteBackward, insertLineBreak, insertText, undo } from "../_helpers/user_actions";
import { testEditor } from "../_helpers/editor";
import { animationFrame } from "@odoo/hoot-mock";
describe("range collapsed", () => {
test("should not change the url when a link is not edited", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.co">google.com</a>b</p>',
contentAfter: '<p>a<a href="https://google.co">google.com</a>b</p>',
});
await testEditor({
contentBefore:
'<p>a<a href="https://google.xx">google.com</a>b<a href="https://google.co">cd[]</a></p>',
stepFunction: async (editor) => {
await insertText(editor, "e");
},
contentAfter:
'<p>a<a href="https://google.xx">google.com</a>b<a href="https://google.co">cde[]</a></p>',
});
});
test("should change the url when the label change", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.co">google.co[]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "m");
},
contentAfter: '<p>a<a href="https://google.com">google.com[]</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://gogle.com">go[]gle.com</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "o");
},
contentAfter: '<p>a<a href="https://google.com">goo[]gle.com</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://else.com">go[]gle.com</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "o");
},
contentAfter: '<p>a<a href="https://else.com">goo[]gle.com</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://else.com">http://go[]gle.com</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "o");
},
contentAfter: '<p>a<a href="https://else.com">http://goo[]gle.com</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="mailto:hello@moto.com">hello@moto[].com</a></p>',
stepFunction: async (editor) => {
await insertText(editor, "r");
},
contentAfter: '<p>a<a href="mailto:hello@motor.com">hello@motor[].com</a></p>',
});
});
test("should change the url when the label change, without changing the protocol", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://google.co">google.co[]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "m");
},
contentAfter: '<p>a<a href="http://google.com">google.com[]</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://google.co">google.co[]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "m");
},
contentAfter: '<p>a<a href="https://google.com">google.com[]</a>b</p>',
});
});
test("should change the url when the label change, changing to the suitable protocol", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://hellomoto.com">hello[]moto.com</a></p>',
stepFunction: async (editor) => {
await insertText(editor, "@");
},
contentAfter: '<p>a<a href="mailto:hello@moto.com">hello@[]moto.com</a></p>',
});
await testEditor({
contentBefore: '<p>a<a href="mailto:hello@moto.com">hello@[]moto.com</a></p>',
stepFunction: async (editor) => {
deleteBackward(editor);
},
contentAfter: '<p>a<a href="https://hellomoto.com">hello[]moto.com</a></p>',
});
});
test("should change the url in one step", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.co">google.co[]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "m");
await undo(editor);
},
contentAfter: '<p>a<a href="https://google.co">google.co[]</a>b</p>',
});
});
test("should not change the url when the label change (1)", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.com">google.com[]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "u");
},
contentAfter: '<p>a<a href="https://google.com">google.comu[]</a>b</p>',
});
});
test("should not change the url when the label change (2)", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.com">google.com[]</a>b</p>',
stepFunction: async (editor) => {
await animationFrame();
await insertLineBreak(editor);
await insertText(editor, "odoo.com");
},
contentAfter: '<p>a<a href="https://google.com">google.com</a><br>odoo.com[]b</p>',
});
});
});
describe("range not collapsed", () => {
test("should change the url when the label change", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.com">google.[com]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "be");
},
contentAfter: '<p>a<a href="https://google.be">google.be[]</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://gogle.com">[yahoo].com</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "google");
},
contentAfter: '<p>a<a href="https://gogle.com">google[].com</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://else.com">go[gle.c]om</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, ".c");
},
contentAfter: '<p>a<a href="https://else.com">go.c[]om</a>b</p>',
});
});
test("should not change the url when the label change", async () => {
await testEditor({
contentBefore: '<p>a<a href="https://google.com">googl[e.com]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "e");
},
contentAfter: '<p>a<a href="https://google.com">google[]</a>b</p>',
});
await testEditor({
contentBefore: '<p>a<a href="https://google.com">google.[com]</a>b</p>',
stepFunction: async (editor) => {
await insertText(editor, "vvv");
},
contentAfter: '<p>a<a href="https://google.com">google.vvv[]</a>b</p>',
});
});
});

View file

@ -0,0 +1,255 @@
import { expect, test } from "@odoo/hoot";
import { click, waitFor } from "@odoo/hoot-dom";
import { contains } from "@web/../tests/web_test_helpers";
import { setupEditor, testEditor } from "../_helpers/editor";
import { cleanLinkArtifacts } from "../_helpers/format";
import { getContent } from "../_helpers/selection";
import { deleteBackward, insertText } from "../_helpers/user_actions";
test("should parse correctly a span inside a Link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b[]</span></a>c</p>',
contentAfter: '<p>a<a href="http://test.test/"><span class="a">b[]</span></a>c</p>',
});
});
test("should parse correctly an empty span inside a Link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]<span class="a"></span></a>c</p>',
contentAfter: '<p>a<a href="http://test.test/">b[]<span class="a"></span></a>c</p>',
});
});
test("should parse correctly a span inside a Link 2", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b[]</span>c</a>d</p>',
contentAfter: '<p>a<a href="http://test.test/"><span class="a">b[]</span>c</a>d</p>',
});
});
test("should parse correctly an empty span inside a Link then add a char", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]<span class="a"></span></a>c</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="http://test.test/">bc[]<span class="a"></span></a>c</p>',
});
});
test("should parse correctly a span inside a Link then add a char", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b[]</span></a>d</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
// JW cAfter: '<p>a<span><a href="http://test.test/">b</a>c[]</span>d</p>',
contentAfter: '<p>a<a href="http://test.test/"><span class="a">bc[]</span></a>d</p>',
});
});
test("should parse correctly a span inside a Link then add a char 2", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b[]</span>d</a>e</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="http://test.test/"><span class="a">bc[]</span>d</a>e</p>',
});
});
test("should parse correctly a span inside a Link then add a char 3", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b</span>c[]</a>e</p>',
stepFunction: async (editor) => {
await insertText(editor, "d");
},
// JW cAfter: '<p>a<a href="http://test.test/"><span class="a">b</span>c</a>d[]e</p>',
contentAfter: '<p>a<a href="http://test.test/"><span class="a">b</span>cd[]</a>e</p>',
});
});
test("should add a character after the link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>d</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
// JW cAfter: '<p>a<a href="http://test.test/">b</a>c[]d</p>',
contentAfter: '<p>a<a href="http://test.test/">bc[]</a>d</p>',
});
});
test("should add two character after the link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>e</p>',
stepFunction: async (editor) => {
await insertText(editor, "cd");
},
contentAfter: '<p>a<a href="http://test.test/">bcd[]</a>e</p>',
});
});
test("should add a character after the link if range just after link", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist">b</a>[]d</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="exist">b</a>c[]d</p>',
});
});
test("should add a character in the link after a br tag", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b<br>[]</a>d</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="http://test.test/">b<br>c[]</a>d</p>',
});
});
test("should remove an empty link on save", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>c</p>',
contentBeforeEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffb[]\ufeff</a>\ufeffc</p>',
stepFunction: deleteBackward,
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]\ufeff</a>\ufeffc</p>',
contentAfter: "<p>a[]c</p>",
});
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"></a>b</p>',
contentBeforeEdit: '<p>a\ufeff<a href="http://test.test/">\ufeff\ufeff</a>\ufeffb</p>',
contentAfterEdit: '<p>a\ufeff<a href="http://test.test/">\ufeff\ufeff</a>\ufeffb</p>',
contentAfter: "<p>ab</p>",
});
});
test("should not remove a link containing an image on save", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist"><img></a>b</p>',
contentBeforeEdit: '<p>a<a href="exist"><img></a>b</p>',
contentAfterEdit: '<p>a<a href="exist"><img></a>b</p>',
contentAfter: '<p>a<a href="exist"><img></a>b</p>',
});
});
test("should not remove a document link on save", async () => {
await testEditor({
contentBefore:
'<p>a<a href="exist" class="o_image" title="file.js.map" data-mimetype="text/plain"></a>b</p>',
contentBeforeEdit:
'<p>a<a href="exist" class="o_image" title="file.js.map" data-mimetype="text/plain" contenteditable="false"></a>b</p>',
contentAfterEdit:
'<p>a<a href="exist" class="o_image" title="file.js.map" data-mimetype="text/plain" contenteditable="false"></a>b</p>',
contentAfter:
'<p>a<a href="exist" class="o_image" title="file.js.map" data-mimetype="text/plain"></a>b</p>',
});
});
test("should not remove a link containing a pictogram on save", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist"><span class="fa fa-star"></span></a>b</p>',
contentBeforeEdit:
'<p>a\ufeff<a href="exist">\ufeff<span class="fa fa-star" contenteditable="false">\u200b</span>\ufeff</a>\ufeffb</p>',
contentAfterEdit:
'<p>a\ufeff<a href="exist">\ufeff<span class="fa fa-star" contenteditable="false">\u200b</span>\ufeff</a>\ufeffb</p>',
contentAfter: '<p>a<a href="exist"><span class="fa fa-star"></span></a>b</p>',
});
});
test("should not add a character in the link if start of paragraph", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist">b</a></p><p>[]d</p>',
stepFunction: async (editor) => {
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="exist">b</a></p><p>c[]d</p>',
});
});
// test.todo('should select and replace all text and add the next char in bold', async () => {
// await testEditor({
// contentBefore: '<div><p>[]123</p><p><a href="#">abc</a></p></div>',
// stepFunction: async (editor) => {
// const p = editor.selection.anchor.parent.nextSibling();
// await editor.execCommand('setSelection', {
// vSelection: {
// anchorNode: p.firstLeaf(),
// anchorPosition: RelativePosition.BEFORE,
// focusNode: p.lastLeaf(),
// focusPosition: RelativePosition.AFTER,
// direction: Direction.FORWARD,
// },
// });
// await editor.execCommand('insert', 'd');
// },
// contentAfter: '<div><p>123</p><p><a href="#">d[]</a></p></div>',
// });
// });
test("should not allow to extend a link if selection spans multiple links", async () => {
const { el } = await setupEditor(
'<p>xxx <a href="exist">lin[k1</a> yyy <a href="exist">li]nk2</a> zzz</p>'
);
await waitFor(".o-we-toolbar");
// link button should be disabled
expect('.o-we-toolbar button[name="link"]').toHaveClass("disabled");
expect('.o-we-toolbar button[name="link"]').toHaveAttribute("disabled");
await click('.o-we-toolbar button[name="link"]');
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>xxx <a href="exist">lin[k1</a> yyy <a href="exist">li]nk2</a> zzz</p>'
);
});
test("should not allow to extend a link if selection spans multiple links (2)", async () => {
const { el } = await setupEditor(
'<p>xxx <a href="exist">[link1</a> yyy <a href="exist">li]nk2</a> zzz</p>'
);
await waitFor(".o-we-toolbar");
// link button should be disabled
expect('.o-we-toolbar button[name="link"]').toHaveClass("disabled");
expect('.o-we-toolbar button[name="link"]').toHaveAttribute("disabled");
await click('.o-we-toolbar button[name="link"]');
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>xxx <a href="exist">[link1</a> yyy <a href="exist">li]nk2</a> zzz</p>'
);
});
test("should not allow to extend a link if selection spans multiple links (3)", async () => {
const { el } = await setupEditor(
'<p>xxx <a href="exist">[link1</a> yyy <a href="exist">link2]</a> zzz</p>'
);
await waitFor(".o-we-toolbar");
// link button should be disabled
expect('.o-we-toolbar button[name="link"]').toHaveClass("disabled");
expect('.o-we-toolbar button[name="link"]').toHaveAttribute("disabled");
await click('.o-we-toolbar button[name="link"]');
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>xxx <a href="exist">[link1</a> yyy <a href="exist">link2]</a> zzz</p>'
);
});
test("when label === url popover label input should be empty", async () => {
await setupEditor('<p>abc <a href="http://odoo.com">http://odo[]o.com</a> def</p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_label_link");
expect(".o_we_label_link").toHaveValue("");
});
test("when label === url changing url should change label", async () => {
const { el } = await setupEditor(
'<p>abc <a href="http://odoo.com">http://odo[]o.com</a> def</p>'
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_label_link");
expect(".o_we_label_link").toHaveValue("");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("http://test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>abc <a href="http://test.com">http://test.com[]</a> def</p>'
);
});

View file

@ -0,0 +1,397 @@
import { describe, expect, test } from "@odoo/hoot";
import { deleteBackward, insertText } from "../_helpers/user_actions";
import { setupEditor, testEditor } from "../_helpers/editor";
import { descendants } from "@html_editor/utils/dom_traversal";
import { tick } from "@odoo/hoot-mock";
import { getContent, setSelection } from "../_helpers/selection";
import { cleanLinkArtifacts } from "../_helpers/format";
import { animationFrame, pointerDown, pointerUp, queryOne } from "@odoo/hoot-dom";
import { dispatchNormalize } from "../_helpers/dispatch";
import { nodeSize } from "@html_editor/utils/position";
import { expectElementCount } from "../_helpers/ui_expectations";
test("should pad a link with ZWNBSPs and add visual indication", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b</a>c</p>',
contentBeforeEdit: '<p>a\ufeff<a href="http://test.test/">\ufeffb\ufeff</a>\ufeffc</p>',
stepFunction: async (editor) => {
setSelection({ anchorNode: editor.editable.querySelector("a"), anchorOffset: 1 });
await tick();
},
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]b\ufeff</a>\ufeffc</p>',
contentAfter: '<p>a<a href="http://test.test/">[]b</a>c</p>',
});
});
test("should pad a link with ZWNBSPs and add visual indication (2)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/"><span class="a">b</span></a></p>',
contentBeforeEdit:
'<p>a\ufeff<a href="http://test.test/">\ufeff<span class="a">b</span>\ufeff</a>\ufeff</p>',
stepFunction: async (editor) => {
setSelection({ anchorNode: editor.editable.querySelector("a span"), anchorOffset: 0 });
await tick();
},
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff<span class="a">[]b</span>\ufeff</a>\ufeff</p>',
contentAfter: '<p>a<a href="http://test.test/"><span class="a">[]b</span></a></p>',
});
});
test("should keep link padded with ZWNBSPs after a delete", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>c</p>',
stepFunction: deleteBackward,
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]\ufeff</a>\ufeffc</p>',
contentAfter: "<p>a[]c</p>",
});
});
test("should keep isolated link after a delete and typing", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>c</p>',
stepFunction: async (editor) => {
deleteBackward(editor);
await insertText(editor, "a");
await insertText(editor, "b");
await insertText(editor, "c");
},
contentAfter: '<p>a<a href="http://test.test/">abc[]</a>c</p>',
});
});
test("should delete the content from the link when popover is active", async () => {
const { editor, el } = await setupEditor('<p><a href="http://test.test/">abc[]abc</a></p>');
await expectElementCount(".o-we-linkpopover", 1);
deleteBackward(editor);
deleteBackward(editor);
deleteBackward(editor);
const content = getContent(el);
expect(content).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]abc\ufeff</a>\ufeff</p>'
);
expect(cleanLinkArtifacts(content)).toBe('<p><a href="http://test.test/">[]abc</a></p>');
});
describe.tags("desktop");
describe("should position the cursor outside the link", () => {
test("clicking at the start of the link", async () => {
const { el } = await setupEditor('<p><a href="http://test.test/">te[]st</a></p>');
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffte[]st\ufeff</a>\ufeff</p>'
);
const aElement = queryOne("p a");
await pointerDown(el);
// Simulate the selection with mousedown
setSelection({ anchorNode: aElement.childNodes[0], anchorOffset: 0 });
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">[]\ufefftest\ufeff</a>\ufeff</p>'
);
await animationFrame(); // selection change
await pointerUp(el);
expect(getContent(el)).toBe(
'<p>[]\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufeff</p>'
);
});
test("clicking at the start of the link when format is applied on link", async () => {
const { el } = await setupEditor('<p><strong><a href="#/">test</a></strong></p>');
expect(getContent(el)).toBe(
// The editable selection is in the link (first leaf of the editable
// upon initialization).
'<p><strong>\ufeff<a href="#/" class="o_link_in_selection">\ufefftest\ufeff</a>\ufeff</strong></p>'
);
const aElement = queryOne("p a");
await pointerDown(el);
// Simulate the selection with mousedown
setSelection({ anchorNode: aElement.childNodes[0], anchorOffset: 0 });
expect(getContent(el)).toBe(
'<p><strong>\ufeff<a href="#/" class="o_link_in_selection">[]\ufefftest\ufeff</a>\ufeff</strong></p>'
);
await animationFrame(); // selection change
await pointerUp(el);
expect(getContent(el)).toBe(
'<p><strong>[]\ufeff<a href="#/">\ufefftest\ufeff</a>\ufeff</strong></p>'
);
});
test("clicking at the end of the link", async () => {
const { el } = await setupEditor('<p><a href="http://test.test/">te[]st</a></p>');
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffte[]st\ufeff</a>\ufeff</p>'
);
const aElement = queryOne("p a");
await pointerDown(el);
// Simulate the selection with mousedown
setSelection({
anchorNode: aElement.childNodes[2],
anchorOffset: nodeSize(aElement.childNodes[2]),
});
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefftest\ufeff[]</a>\ufeff</p>'
);
await animationFrame(); // selectionChange
await pointerUp(el);
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufeff[]</p>'
);
});
test("clicking before the link's text content", async () => {
const { el, editor } = await setupEditor('<p><a href="http://test.test/">te[]st</a></p>');
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffte[]st\ufeff</a>\ufeff</p>'
);
const aElement = queryOne("p a");
await pointerDown(el);
// Simulate the selection with mousedown
setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: 0 });
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]test\ufeff</a>\ufeff</p>'
);
await animationFrame(); // selection change
await pointerUp(el);
expect(getContent(el)).toBe(
'<p>[]\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufeff</p>'
);
await insertText(editor, "link");
expect(getContent(el)).toBe(
'<p>link[]\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufeff</p>'
);
setSelection({ anchorNode: aElement.childNodes[1], anchorOffset: 0 });
await animationFrame(); // selectionChange
expect(getContent(el)).toBe(
'<p>link\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]test\ufeff</a>\ufeff</p>'
);
await insertText(editor, "content");
expect(getContent(el)).toBe(
'<p>link\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffcontent[]test\ufeff</a>\ufeff</p>'
);
});
test(" clicking after the link's text content", async () => {
const { el, editor } = await setupEditor('<p><a href="http://test.test/">t[]est</a></p>');
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefft[]est\ufeff</a>\ufeff</p>'
);
const aElement = queryOne("p a");
await pointerDown(el);
// Simulate the selection with mousedown
setSelection({
anchorNode: aElement.childNodes[1],
anchorOffset: nodeSize(aElement.childNodes[1]),
});
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefftest[]\ufeff</a>\ufeff</p>'
);
await animationFrame(); // selection change
await pointerUp(el);
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufeff[]</p>'
);
await insertText(editor, "link");
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/">\ufefftest\ufeff</a>\ufefflink[]</p>'
);
setSelection({
anchorNode: aElement.childNodes[1],
anchorOffset: nodeSize(aElement.childNodes[1]),
});
await animationFrame(); // selectionChange
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefftest[]\ufeff</a>\ufefflink</p>'
);
await insertText(editor, "content");
expect(getContent(el)).toBe(
'<p>\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufefftestcontent[]\ufeff</a>\ufefflink</p>'
);
});
});
describe("should zwnbsp-pad simple text link", () => {
const removeZwnbsp = (editor) => {
for (const descendant of descendants(editor.editable)) {
if (descendant.nodeType === Node.TEXT_NODE && descendant.textContent === "\ufeff") {
descendant.remove();
}
}
};
test("should zwnbsp-pad simple text link (1)", async () => {
await testEditor({
contentBefore: '<p>a[]<a href="#/">bc</a>d</p>',
contentBeforeEdit: '<p>a[]\ufeff<a href="#/">\ufeffbc\ufeff</a>\ufeffd</p>',
stepFunction: async (editor) => {
removeZwnbsp(editor);
const p = editor.editable.querySelector("p");
// set the selection via the parent
setSelection({ anchorNode: p, anchorOffset: 1 });
// insert the zwnbsp again
dispatchNormalize(editor);
},
contentAfterEdit: '<p>a\ufeff[]<a href="#/">\ufeffbc\ufeff</a>\ufeffd</p>',
});
});
test("should zwnbsp-pad simple text link (2)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">[]bc</a>d</p>',
contentBeforeEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]bc\ufeff</a>\ufeffd</p>',
stepFunction: async (editor) => {
removeZwnbsp(editor);
const a = editor.editable.querySelector("a");
// set the selection via the parent
setSelection({ anchorNode: a, anchorOffset: 0 });
await tick();
// insert the zwnbsp again
dispatchNormalize(editor);
},
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeff[]bc\ufeff</a>\ufeffd</p>',
});
});
test("should zwnbsp-pad simple text link (3)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]</a>d</p>',
contentBeforeEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffb[]\ufeff</a>\ufeffd</p>',
stepFunction: async (editor) => {
const a = editor.editable.querySelector("a");
// Insert an extra character as a text node so we can set
// the selection between the characters while still
// targetting their parent.
a.appendChild(editor.document.createTextNode("c"));
removeZwnbsp(editor);
// set the selection via the parent
setSelection({ anchorNode: a, anchorOffset: 1 });
await tick();
// insert the zwnbsp again
dispatchNormalize(editor);
},
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffb[]c\ufeff</a>\ufeffd</p>',
});
});
test("should zwnbsp-pad simple text link (4)", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">bc[]</a>d</p>',
contentBeforeEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffbc[]\ufeff</a>\ufeffd</p>',
stepFunction: async (editor) => {
removeZwnbsp(editor);
const a = editor.editable.querySelector("a");
// set the selection via the parent
setSelection({ anchorNode: a, anchorOffset: 1 });
await tick();
// insert the zwnbsp again
dispatchNormalize(editor);
},
contentAfterEdit:
'<p>a\ufeff<a href="http://test.test/" class="o_link_in_selection">\ufeffbc[]\ufeff</a>\ufeffd</p>',
});
});
test("should zwnbsp-pad simple text link (5)", async () => {
await testEditor({
contentBefore: '<p>a<a href="#/">bc</a>[]d</p>',
contentBeforeEdit: '<p>a\ufeff<a href="#/">\ufeffbc\ufeff</a>\ufeff[]d</p>',
stepFunction: async (editor) => {
removeZwnbsp(editor);
const p = editor.editable.querySelector("p");
// set the selection via the parent
setSelection({ anchorNode: p, anchorOffset: 2 });
await tick();
// insert the zwnbsp again
dispatchNormalize(editor);
},
contentAfterEdit: '<p>a\ufeff<a href="#/">\ufeffbc\ufeff</a>\ufeff[]d</p>',
});
});
});
test("should not zwnbsp-pad nav-link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/" class="nav-link">[]b</a>c</p>',
contentBeforeEdit: '<p>a<a href="http://test.test/" class="nav-link">[]b</a>c</p>',
});
});
test("should not zwnbsp-pad link with block fontawesome", async () => {
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">[]<i style="display: flex;" class="fa fa-star"></i></a>b</p>',
contentBeforeEdit:
'<p>a<a href="http://test.test/">\ufeff[]<i style="display: flex;" class="fa fa-star" contenteditable="false">\u200b</i>\ufeff</a>b</p>',
});
});
test("should not zwnbsp-pad link with image", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">[]<img style="display: inline;"></a>b</p>',
contentBeforeEdit:
'<p>a<a href="http://test.test/">[]<img style="display: inline;"></a>b</p>',
});
});
test("should remove zwnbsp from middle of the link", async () => {
await testEditor({
contentBefore: '<p><a href="#/">content</a></p>',
contentBeforeEdit:
// The editable selection is in the link (first leaf of the editable
// upon initialization).
'<p>\ufeff<a href="#/" class="o_link_in_selection">\ufeffcontent\ufeff</a>\ufeff</p>',
stepFunction: async (editor) => {
// Cursor before the FEFF text node
setSelection({ anchorNode: editor.editable.querySelector("a"), anchorOffset: 0 });
await insertText(editor, "more ");
},
contentAfterEdit:
'<p>\ufeff<a href="#/" class="o_link_in_selection">\ufeffmore []content\ufeff</a>\ufeff</p>',
contentAfter: '<p><a href="#/">more []content</a></p>',
});
});
test("should remove zwnbsp from middle of the link (2)", async () => {
await testEditor({
contentBefore: '<p><a href="#/">content</a></p>',
contentBeforeEdit:
// The editable selection is in the link (first leaf of the editable
// upon initialization).
'<p>\ufeff<a href="#/" class="o_link_in_selection">\ufeffcontent\ufeff</a>\ufeff</p>',
stepFunction: async (editor) => {
// Cursor inside the FEFF text node
setSelection({
anchorNode: editor.editable.querySelector("a").firstChild,
anchorOffset: 0,
});
await insertText(editor, "more ");
},
contentAfterEdit:
'<p>\ufeff<a href="#/" class="o_link_in_selection">\ufeffmore []content\ufeff</a>\ufeff</p>',
contentAfter: '<p><a href="#/">more []content</a></p>',
});
});
test("should zwnbps-pad links with .btn class", async () => {
await testEditor({
contentBefore: '<p><a href="#" class="btn">content</a></p>',
contentBeforeEdit: '<p>\ufeff<a href="#" class="btn">\ufeffcontent\ufeff</a>\ufeff</p>',
});
});
test("should not add visual indication to a button", async () => {
await testEditor({
contentBefore: '<p><a href="http://test.test/" class="btn">[]content</a></p>',
contentBeforeEdit:
'<p>\ufeff<a href="http://test.test/" class="btn">\ufeff[]content\ufeff</a>\ufeff</p>',
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,392 @@
import { describe, expect, test } from "@odoo/hoot";
import { testEditor, setupEditor } from "../_helpers/editor";
import { unlinkFromPopover, unlinkByCommand, unlinkFromToolbar } from "../_helpers/user_actions";
import { getContent, setSelection } from "../_helpers/selection";
describe("range collapsed, remove by popover unlink button", () => {
test("should remove the link if collapsed range at the end of a link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">bcd[]</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: "<p>abcd[]e</p>",
});
// With fontawesome at the start of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/"><span class="fa fa-music" contenteditable="false">\u200B</span>bcd[]</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>a<span class="fa fa-music"></span>bcd[]e</p>',
});
// With fontawesome at the middle of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">bc<span class="fa fa-music" contenteditable="false">\u200B</span>d[]</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>abc<span class="fa fa-music"></span>d[]e</p>',
});
// With fontawesome at the end of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">bcd[]<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>abcd[]<span class="fa fa-music"></span>e</p>',
});
});
test("should remove the link if collapsed range in the middle a link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">b[]cd</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: "<p>ab[]cde</p>",
});
// With fontawesome at the start of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/"><span class="fa fa-music" contenteditable="false">\u200B</span>b[]cd</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>a<span class="fa fa-music"></span>b[]cde</p>',
});
// With fontawesome at the middle of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">b[]c<span class="fa fa-music" contenteditable="false">\u200B</span>d</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>ab[]c<span class="fa fa-music"></span>de</p>',
});
// With fontawesome at the end of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">b[]cd<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>ab[]cd<span class="fa fa-music"></span>e</p>',
});
});
test("should remove the link if collapsed range at the start of a link", async () => {
await testEditor({
contentBefore: '<p>a<a href="http://test.test/">[]bcd</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: "<p>a[]bcde</p>",
});
// With fontawesome at the start of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/"><span class="fa fa-music" contenteditable="false">\u200B</span>[]bcd</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>a<span class="fa fa-music"></span>[]bcde</p>',
});
// With fontawesome at the middle of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">[]bc<span class="fa fa-music" contenteditable="false">\u200B</span>d</a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>a[]bc<span class="fa fa-music"></span>de</p>',
});
// With fontawesome at the end of the link.
await testEditor({
contentBefore:
'<p>a<a href="http://test.test/">[]bcd<span class="fa fa-music" contenteditable="false">\u200B</span></a>e</p>',
stepFunction: unlinkFromPopover,
contentAfter: '<p>a[]bcd<span class="fa fa-music"></span>e</p>',
});
});
test("should remove only the current link if collapsed range in the middle of a link", async () => {
await testEditor({
contentBefore:
'<p><a href="http://test.test/">a</a>b<a href="http://test.test/">c[]d</a>e<a href="http://test.test/">f</a></p>',
stepFunction: unlinkFromPopover,
contentAfter:
'<p><a href="http://test.test/">a</a>bc[]de<a href="http://test.test/">f</a></p>',
});
// With fontawesome at the start of the link.
await testEditor({
contentBefore:
'<p><a href="http://test.test/">a</a>b<a href="http://test.test/"><span class="fa fa-music" contenteditable="false">\u200B</span>c[]d</a>e<a href="http://test.test/">f</a></p>',
stepFunction: unlinkFromPopover,
contentAfter:
'<p><a href="http://test.test/">a</a>b<span class="fa fa-music"></span>c[]de<a href="http://test.test/">f</a></p>',
});
// With fontawesome at the middle of the link.
await testEditor({
contentBefore:
'<p><a href="http://test.test/">a</a>b<a href="http://test.test/">c<span class="fa fa-music" contenteditable="false">\u200B</span>d[]e</a>f<a href="http://test.test/">g</a></p>',
stepFunction: unlinkFromPopover,
contentAfter:
'<p><a href="http://test.test/">a</a>bc<span class="fa fa-music"></span>d[]ef<a href="http://test.test/">g</a></p>',
});
// With fontawesome at the end of the link.
await testEditor({
contentBefore:
'<p><a href="http://test.test/">a</a>b<a href="http://test.test/">c[]d<span class="fa fa-music" contenteditable="false">\u200B</span></a>e<a href="http://test.test/">f</a></p>',
stepFunction: unlinkFromPopover,
contentAfter:
'<p><a href="http://test.test/">a</a>bc[]d<span class="fa fa-music"></span>e<a href="http://test.test/">f</a></p>',
});
});
});
describe("range not collapsed", () => {
describe("remove by toolbar unlink button", () => {
test("should remove the link in the selected range at the end of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a<a href="exist">bc[d]</a>e</p>',
stepFunction: unlinkFromToolbar,
contentAfter: '<p>a<a href="exist">bc</a>[d]e</p>',
});
});
test("should remove fully selected link by toolbar unlink button", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist">[bcd]</a>e</p>',
stepFunction: unlinkFromToolbar,
contentAfterEdit: "<p>a[bcd]e</p>",
contentAfter: "<p>a[bcd]e</p>",
});
});
test("should remove fully selected link along with text by toolbar unlink button", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist" class="btn btn-primary">[bcd</a>ef]g</p>',
stepFunction: unlinkFromToolbar,
contentAfterEdit: "<p>a[bcdef]g</p>",
contentAfter: "<p>a[bcdef]g</p>",
});
});
test("should remove fully selected link along with text by toolbar unlink button (2)", async () => {
await testEditor({
contentBefore: '<p>a[bc<a href="exist">def]</a>g</p>',
stepFunction: unlinkFromToolbar,
contentAfterEdit: "<p>a[bcdef]g</p>",
contentAfter: "<p>a[bcdef]g</p>",
});
});
test("should remove fully selected formatted link by toolbar unlink button", async () => {
await testEditor({
contentBefore: '<p>a<a href="exist"><i>[bcd]</i></a>e</p>',
stepFunction: unlinkFromToolbar,
contentAfterEdit: "<p>a<i>[bcd]</i>e</p>",
contentAfter: "<p>a<i>[bcd]</i>e</p>",
});
});
});
describe("remove by command", () => {
test("should remove the link in the selected range at the end of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a<a href="exist">bc[d]</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfterEdit: '<p>a\ufeff<a href="exist">\ufeffbc\ufeff</a>\ufeff[d]e</p>',
contentAfter: '<p>a<a href="exist">bc</a>[d]e</p>',
});
// BACKWARD
await testEditor({
contentBefore: '<p>a<a href="exist">bc]d[</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfterEdit: '<p>a\ufeff<a href="exist">\ufeffbc\ufeff</a>\ufeff]d[e</p>',
contentAfter: '<p>a<a href="exist">bc</a>]d[e</p>',
});
});
test("should remove the link in the selected range in the middle of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a<a href="exist">b[c]d</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a<a href="exist">b</a>[c]<a href="exist">d</a>e</p>',
});
// BACKWARD
await testEditor({
contentBefore: '<p>a<a href="exist">b]c[d</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a<a href="exist">b</a>]c[<a href="exist">d</a>e</p>',
});
});
test("should remove the link in the selected range at the start of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a<a href="exist">[b]cd</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfterEdit: '<p>a[b]\ufeff<a href="exist">\ufeffcd\ufeff</a>\ufeffe</p>',
contentAfter: '<p>a[b]<a href="exist">cd</a>e</p>',
});
// BACKWARD
await testEditor({
contentBefore: '<p>a<a href="exist">]b[cd</a>e</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfterEdit: '<p>a]b[\ufeff<a href="exist">\ufeffcd\ufeff</a>\ufeffe</p>',
contentAfter: '<p>a]b[<a href="exist">cd</a>e</p>',
});
});
test("should remove the link in the selected range overlapping the end of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a<a href="exist">bc[d</a>e]f</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a<a href="exist">bc</a>[de]f</p>',
});
// BACKWARD
await testEditor({
contentBefore: '<p>a<a href="exist">bc]d</a>e[f</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a<a href="exist">bc</a>]de[f</p>',
});
});
test("should remove the link in the selected range overlapping the start of a link", async () => {
// FORWARD
await testEditor({
contentBefore: '<p>a[b<a href="exist">c]de</a>f</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a[bc]<a href="exist">de</a>f</p>',
});
// BACKWARD
await testEditor({
contentBefore: '<p>a]b<a href="exist">c[de</a>f</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>a]bc[<a href="exist">de</a>f</p>',
});
});
test("should not unlink selected non-editable links", async () => {
await testEditor({
contentBefore:
'<p><a href="exist">[ab</a><a contenteditable="false" href="exist">cd</a>ef]</p>',
stepFunction: async (editor) => {
await unlinkByCommand(editor);
},
contentAfter: '<p>[ab<a contenteditable="false" href="exist">cd</a>ef]</p>',
});
});
test("should not unlink editable links with selection in non-editable", async () => {
await testEditor({
contentBefore:
'<p contenteditable="false">ab<a contenteditable="true" href="http://test.test">[cd]</a>ef</p>',
stepFunction: unlinkByCommand,
contentAfter:
'<p contenteditable="false">ab<a contenteditable="true" href="http://test.test">[cd]</a>ef</p>',
});
});
test("should not remove unremovable links when inside selection with other links", async () => {
await testEditor({
contentBefore:
'<p>[a<a href="http://test.test">b</a>c<a class="oe_unremovable" href="http://test.test">d</a>e<a href="http://test.test">f]</a></p>',
stepFunction: unlinkByCommand,
contentAfter:
'<p>[abc<a class="oe_unremovable" href="http://test.test">d</a>ef]</p>',
});
});
test("should not remove selected part of unremovable links when partially selected with other links", async () => {
await testEditor({
contentBefore:
'<p><a class="oe_unremovable" href="http://test.test">a[b</a>c<a href="http://test.test">d</a>e<a class="oe_unremovable" href="http://test.test">f]g</a></p>',
stepFunction: unlinkByCommand,
contentAfter:
'<p><a class="oe_unremovable" href="http://test.test">a[b</a>cde<a class="oe_unremovable" href="http://test.test">f]g</a></p>',
});
});
test("should not remove unremovable links when fully selected with other links", async () => {
await testEditor({
contentBefore:
'<p>a<a class="oe_unremovable" href="http://test.test">[b</a>c<a href="http://test.test">d</a>e<a class="oe_unremovable" href="http://test.test">f]</a></p>',
stepFunction: unlinkByCommand,
contentAfter:
'<p>a<a class="oe_unremovable" href="http://test.test">[b</a>cde<a class="oe_unremovable" href="http://test.test">f]</a></p>',
});
});
test("should not remove unremovable links when fully selected (including feff) with other links", async () => {
await testEditor({
contentBefore:
'<p>a<a class="oe_unremovable" href="http://test.test">[b</a>c<a href="http://test.test">d</a>e<a class="oe_unremovable" href="http://test.test">f]</a></p>',
/** @param {import("@html_editor/plugin").Editor} editor */
stepFunction: (editor) => {
const selection = editor.shared.selection.getEditableSelection();
// extends selection to contain the feffs
editor.shared.selection.setSelection({
anchorNode: selection.anchorNode.previousSibling,
anchorOffset: 0,
focusNode: selection.focusNode.nextSibling,
focusOffset: 1,
});
unlinkByCommand(editor);
},
contentAfter:
'<p>a<a class="oe_unremovable" href="http://test.test">[b</a>cde<a class="oe_unremovable" href="http://test.test">f]</a></p>',
});
});
});
test("should be able to remove link if selection has FEFF character", async () => {
const { el } = await setupEditor(
'<p><a href="google.com" class="btn btn-primary">[test]</a></p>'
);
const link = el.querySelector("a");
const firstFeffChar = link.firstChild;
const secondFeffChar = link.lastChild;
setSelection({
anchorNode: firstFeffChar,
anchorOffset: 0,
focusNode: secondFeffChar,
focusOffset: 1,
});
await unlinkFromToolbar();
expect(getContent(el)).toBe("<p>[test]</p>");
});
test("should be able to remove link if selection has FEFF character (2)", async () => {
const { el } = await setupEditor(
'<p><a href="http://test.test/" class="btn btn-primary">[]test</a></p>'
);
const link = el.querySelector("a");
const firstFeffChar = link.firstChild;
const textNode = firstFeffChar.nextSibling;
const secondFeffChar = link.lastChild;
setSelection({
anchorNode: secondFeffChar,
anchorOffset: 1,
focusNode: textNode,
focusOffset: 0,
});
await unlinkFromToolbar();
expect(getContent(el)).toBe("<p>]test[</p>");
});
});
describe("empty link", () => {
test("should not remove empty link in uneditable zone", async () => {
await testEditor({
contentBefore: '<p contenteditable="false"><a href="exist"></a></p>',
contentAfter: '<p contenteditable="false"><a href="exist"></a></p>',
});
});
test("should not remove empty link in uneditable zone (2)", async () => {
await testEditor({
contentBefore:
'<p>[]<span contenteditable="false"><a contenteditable="true" href="exist"></a></span></p>',
contentAfter:
'<p>[]<span contenteditable="false"><a contenteditable="true" href="exist"></a></span></p>',
});
});
});

View file

@ -0,0 +1,193 @@
import { expect, test } from "@odoo/hoot";
import { manuallyDispatchProgrammaticEvent } from "@odoo/hoot-dom";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
import { setupEditor, testEditor } from "../_helpers/editor";
import { cleanLinkArtifacts } from "../_helpers/format";
import { getContent, setSelection } from "../_helpers/selection";
import { insertText, undo } from "../_helpers/user_actions";
async function insertSpace(editor) {
const keydownEvent = await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", {
key: " ",
});
if (keydownEvent.defaultPrevented) {
return;
}
// InputEvent is required to simulate the insert text.
const [beforeinputEvent] = await manuallyDispatchProgrammaticEvent(
editor.editable,
"beforeinput",
{
inputType: "insertText",
data: " ",
}
);
if (beforeinputEvent.defaultPrevented) {
return;
}
const range = editor.document.getSelection().getRangeAt(0);
if (!range.collapsed) {
throw new Error("need to implement something... maybe");
}
let offset = range.startOffset;
const node = range.startContainer;
// mimic the behavior of the browser when inserting a &nbsp
const twoSpace = " \u00A0";
node.textContent = (
node.textContent.slice(0, offset) +
" " +
node.textContent.slice(offset)
).replaceAll(" ", twoSpace);
if (
node.nextSibling &&
node.nextSibling.textContent.startsWith(" ") &&
node.textContent.endsWith(" ")
) {
node.nextSibling.textContent = "\u00A0" + node.nextSibling.textContent.slice(1);
}
offset++;
setSelection({
anchorNode: node,
anchorOffset: offset,
});
const [inputEvent] = await manuallyDispatchProgrammaticEvent(editor.editable, "input", {
inputType: "insertText",
data: " ",
});
if (inputEvent.defaultPrevented) {
return;
}
// KeyUpEvent is not required but is triggered like the browser would.
await manuallyDispatchProgrammaticEvent(editor.editable, "keyup", { key: " " });
}
/**
* Automatic link creation when pressing Space, Enter or Shift+Enter after an url
*/
test("should transform url after space", async () => {
await testEditor({
contentBefore: "<p>a http://test.com b http://test.com[] c http://test.com d</p>",
stepFunction: async (editor) => {
await insertSpace(editor);
},
contentAfter:
'<p>a http://test.com b <a href="http://test.com">http://test.com</a>&nbsp;[] c http://test.com d</p>',
});
await testEditor({
contentBefore: "<p>http://test.com[]</p>",
stepFunction: async (editor) => {
// Setup: simulate multiple text nodes in a p: <p>"http://test" ".com"</p>
editor.editable.firstChild.firstChild.splitText(11);
/** @todo fix warnings */
patchWithCleanup(console, { warn: () => {} });
// Action: insert space
await insertSpace(editor);
},
contentAfter: '<p><a href="http://test.com">http://test.com</a>&nbsp;[]</p>',
});
});
test("should transform url followed by punctuation characters after space", async () => {
await testEditor({
contentBefore: "<p>http://test.com.[]</p>",
stepFunction: async (editor) => {
await insertSpace(editor);
},
contentAfter: '<p><a href="http://test.com">http://test.com</a>.&nbsp;[]</p>',
});
await testEditor({
contentBefore: "<p>test.com...[]</p>",
stepFunction: (editor) => insertSpace(editor),
contentAfter: '<p><a href="https://test.com">test.com</a>...&nbsp;[]</p>',
});
await testEditor({
contentBefore: "<p>test.com,[]</p>",
stepFunction: (editor) => insertSpace(editor),
contentAfter: '<p><a href="https://test.com">test.com</a>,&nbsp;[]</p>',
});
await testEditor({
contentBefore: "<p>test.com,hello[]</p>",
stepFunction: (editor) => insertSpace(editor),
contentAfter: '<p><a href="https://test.com">test.com</a>,hello&nbsp;[]</p>',
});
await testEditor({
contentBefore: "<p>http://test.com[]</p>",
stepFunction: async (editor) => {
// Setup: simulate multiple text nodes in a p: <p>"http://test" ".com"</p>
editor.editable.firstChild.firstChild.splitText(11);
/** @todo fix warnings */
patchWithCleanup(console, { warn: () => {} });
// Action: insert space
await insertSpace(editor);
},
contentAfter: '<p><a href="http://test.com">http://test.com</a>&nbsp;[]</p>',
});
});
test("should transform url after enter", async () => {
await testEditor({
contentBefore: "<p>a http://test.com b http://test.com[] c http://test.com d</p>",
stepFunction: async (editor) => {
// Simulate "Enter"
await manuallyDispatchProgrammaticEvent(editor.editable, "beforeinput", {
inputType: "insertParagraph",
});
},
contentAfter:
'<p>a http://test.com b <a href="http://test.com">http://test.com</a></p><p>[]&nbsp;c http://test.com d</p>',
});
});
test("should transform url after shift+enter", async () => {
await testEditor({
contentBefore: "<p>a http://test.com b http://test.com[] c http://test.com d</p>",
stepFunction: async (editor) => {
// Simulate "Shift + Enter"
await manuallyDispatchProgrammaticEvent(editor.editable, "beforeinput", {
inputType: "insertLineBreak",
});
},
contentAfter:
'<p>a http://test.com b <a href="http://test.com">http://test.com</a><br>[]&nbsp;c http://test.com d</p>',
});
});
test("should not transform an email url after space", async () => {
await testEditor({
contentBefore: "<p>user@domain.com[]</p>",
stepFunction: (editor) => insertSpace(editor),
contentAfter: "<p>user@domain.com []</p>",
});
});
test("should not transform url after two space", async () => {
await testEditor({
contentBefore: "<p>a http://test.com b http://test.com&nbsp;[] c http://test.com d</p>",
stepFunction: (editor) => insertSpace(editor),
contentAfter:
"<p>a http://test.com b http://test.com&nbsp; []&nbsp;c http://test.com d</p>",
});
});
test("transform text url into link and undo it", async () => {
const { el, editor } = await setupEditor(`<p>[]</p>`);
await insertText(editor, "www.abc.jpg ");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="https://www.abc.jpg">www.abc.jpg</a>&nbsp;[]</p>'
);
undo(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="https://www.abc.jpg">www.abc.jpg</a>[]</p>'
);
undo(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>www.abc.jpg[]</p>");
});