mirror of
https://github.com/bringout/oca-ocb-web.git
synced 2026-04-19 11:32:04 +02:00
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:
parent
4b94f0abc5
commit
f866779561
1513 changed files with 396049 additions and 358525 deletions
|
|
@ -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>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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>',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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>'
|
||||
);
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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>',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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  
|
||||
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> [] 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> []</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>. []</p>',
|
||||
});
|
||||
await testEditor({
|
||||
contentBefore: "<p>test.com...[]</p>",
|
||||
stepFunction: (editor) => insertSpace(editor),
|
||||
contentAfter: '<p><a href="https://test.com">test.com</a>... []</p>',
|
||||
});
|
||||
await testEditor({
|
||||
contentBefore: "<p>test.com,[]</p>",
|
||||
stepFunction: (editor) => insertSpace(editor),
|
||||
contentAfter: '<p><a href="https://test.com">test.com</a>, []</p>',
|
||||
});
|
||||
await testEditor({
|
||||
contentBefore: "<p>test.com,hello[]</p>",
|
||||
stepFunction: (editor) => insertSpace(editor),
|
||||
contentAfter: '<p><a href="https://test.com">test.com</a>,hello []</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> []</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>[] 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>[] 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 [] c http://test.com d</p>",
|
||||
stepFunction: (editor) => insertSpace(editor),
|
||||
contentAfter:
|
||||
"<p>a http://test.com b http://test.com [] 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> []</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>");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue