Odoo18-Base/addons/html_editor/static/tests/link/popover.test.js

887 lines
41 KiB
JavaScript
Raw Permalink Normal View History

2025-01-06 10:57:38 +07:00
import { describe, expect, test } from "@odoo/hoot";
import {
click,
fill,
getActiveElement,
press,
queryAllTexts,
queryFirst,
queryOne,
queryText,
select,
waitFor,
waitUntil,
} from "@odoo/hoot-dom";
import { animationFrame, tick } from "@odoo/hoot-mock";
import { markup } from "@odoo/owl";
import { contains, onRpc } from "@web/../tests/web_test_helpers";
import { setupEditor } from "../_helpers/editor";
import { cleanLinkArtifacts } from "../_helpers/format";
import { getContent, setContent, setSelection } from "../_helpers/selection";
import { insertLineBreak, insertText, splitBlock, undo } from "../_helpers/user_actions";
const base64Img =
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA\n AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n 9TXL0Y4OHwAAAABJRU5ErkJggg==";
describe("should open a popover", () => {
test("should open a popover when the selection is inside a link and close outside of a link", async () => {
const { el } = await setupEditor("<p>this is a <a>link</a></p>");
expect(".o-we-linkpopover").toHaveCount(0);
// selection inside a link
setContent(el, "<p>this is a <a>li[]nk</a></p>");
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
// selection outside a link
setContent(el, "<p>this []is a <a>link</a></p>");
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
expect(".o-we-linkpopover").toHaveCount(0);
});
test("link popover should have input field for href when the link doesn't have href", async () => {
await setupEditor("<p>this is a <a>li[]nk</a></p>");
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
expect(".o_we_label_link").toHaveValue("link");
expect(".o_we_href_input_link").toHaveValue("");
});
test("link popover should have buttons for link operation when the link has href", async () => {
await setupEditor('<p>this is a <a href="test.com">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
expect(".o_we_copy_link").toHaveCount(1);
expect(".o_we_edit_link").toHaveCount(1);
expect(".o_we_remove_link").toHaveCount(1);
});
test("link popover should not repositioned when clicking in the input field", async () => {
await setupEditor("<p>this is a <a>li[]nk</a></p>");
await waitFor(".o_we_href_input_link");
const style = queryOne(".o-we-linkpopover").parentElement.style.cssText;
queryOne(".o_we_href_input_link").focus();
await animationFrame();
expect(queryOne(".o-we-linkpopover").parentElement).toHaveAttribute("style", style);
});
});
describe("popover should switch UI depending on editing state", () => {
test("after clicking on edit button, the popover should switch to editing mode", async () => {
await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_href_input_link");
expect(".o_we_label_link").toHaveValue("link");
expect(".o_we_href_input_link").toHaveValue("http://test.com/");
});
test("after clicking on apply button, the selection should be restored", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_href_input_link");
await click(".o_we_href_input_link");
await click(".o_we_apply_link");
await waitFor(".o_we_edit_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.com/">li[]nk</a></p>'
);
});
test("after editing the URL and clicking on apply button, the selection should be restored", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_href_input_link");
// This changes the selection to outside the editor.
await click(".o_we_href_input_link");
await tick();
await press("a");
await click(".o_we_apply_link");
await waitFor(".o_we_edit_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.com/a">li[]nk</a></p>'
);
});
test("after clicking on apply button, the popover should be with the non editing mode, e.g. with three buttons", async () => {
await setupEditor("<p>this is a <a>li[]nk</a></p>");
await waitFor(".o-we-linkpopover");
await click(".o_we_href_input_link");
await click(".o_we_apply_link");
await waitFor(".o_we_edit_link");
expect(".o-we-linkpopover").toHaveCount(1);
expect(".o_we_copy_link").toHaveCount(1);
expect(".o_we_edit_link").toHaveCount(1);
expect(".o_we_remove_link").toHaveCount(1);
});
});
describe("popover should edit,copy,remove the link", () => {
test("after apply url on a link without href, the link element should be updated", async () => {
const { el } = await setupEditor("<p>this is a <a>li[]nk</a></p>");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("http://test.com/");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.com/">li[]nk</a></p>'
);
});
test("after clicking on remove button, the link element should be unwrapped", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_remove_link");
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
expect(getContent(el)).toBe("<p>this is a li[]nk</p>");
});
test("after edit the label, the text of the link should be updated", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await contains(".o-we-linkpopover input.o_we_label_link").fill("new");
await click(".o_we_apply_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.com/">linknew[]</a></p>'
);
});
test("when the label is empty, it should be set as the URL", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor(".o_we_apply_link");
await contains(".o-we-linkpopover input.o_we_label_link").clear();
await click(".o_we_apply_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://test.com/">http://test.com/[]</a></p>'
);
});
test("after clicking on copy button, the url should be copied to clipboard", async () => {
await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_copy_link");
await waitFor(".o_notification_bar.bg-success");
const notifications = queryAllTexts(".o_notification_body");
expect(notifications).toInclude("Link copied to clipboard.");
await animationFrame();
expect(".o-we-linkpopover").toHaveCount(0);
await expect(navigator.clipboard.readText()).resolves.toBe("http://test.com/");
});
test("when edit a link's label and URL to '', the link should be removed", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await contains(".o-we-linkpopover input.o_we_label_link").clear();
await contains(".o-we-linkpopover input.o_we_href_input_link").clear();
expect(getContent(el)).toBe("<p>this is a&nbsp;[]</p>");
});
});
describe("Incorrect URL should be corrected", () => {
test("when edit a link's URL to 'test.com', the link's URL should be corrected", async () => {
const { el } = await setupEditor('<p>this is a <a href="http://test.com/">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("newtest.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="http://newtest.com">li[]nk</a></p>'
);
});
test("when a link's URL is an email, the link's URL should start with mailto:", async () => {
const { el } = await setupEditor("<p>this is a <a>li[]nk</a></p>");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("test@test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="mailto:test@test.com">li[]nk</a></p>'
);
});
test("when a link's URL is an phonenumber, the link's URL should start with tel://:", async () => {
const { el } = await setupEditor("<p>this is a <a>li[]nk</a></p>");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("+1234567890");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>this is a <a href="tel://+1234567890">li[]nk</a></p>'
);
});
});
describe("Link creation", () => {
describe("Creation by space", () => {
test("typing valid URL + space should convert to link", async () => {
const { editor, el } = await setupEditor("<p>[]</p>");
await insertText(editor, "http://google.co.in");
await insertText(editor, " ");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://google.co.in">http://google.co.in</a> []</p>'
);
});
test("typing invalid URL + space should not convert to link", async () => {
const { editor, el } = await setupEditor("<p>[]</p>");
await insertText(editor, "www.odoo");
await insertText(editor, " ");
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>www.odoo []</p>");
});
});
describe("Creation by powerbox", () => {
test("click on link command in powerbox should create a link element and open the linkpopover", async () => {
const { editor, el } = await setupEditor("<p>ab[]</p>");
await insertText(editor, "/link");
await animationFrame();
expect(".active .o-we-command-name").toHaveText("Link");
await click(".o-we-command-name:first");
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>ab<a>[]</a></p>");
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
expect(getActiveElement()).toBe(queryOne(".o-we-linkpopover input.o_we_label_link"), {
message: "should focus label input by default, when we don't have a label",
});
});
test("when create a new link by powerbox and not input anything, the link should be removed", async () => {
const { editor, el } = await setupEditor("<p>ab[]</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>ab<a></a></p>");
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 0,
focusNode: pNode,
focusOffset: 0,
});
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>[]ab</p>");
});
test("when create a new link by powerbox and only input the URL, the link should be created with corrected URL", async () => {
const { editor, el } = await setupEditor("<p>ab[]</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>ab<a href="http://test.com">test.com[]</a></p>'
);
});
test("Should be able to insert link on empty p", async () => {
const { editor, el } = await setupEditor("<p>[]</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
expect(cleanLinkArtifacts(getContent(el))).toBe('<p><a href="#">#[]</a></p>');
});
test("should insert a link and preserve spacing", async () => {
const { editor, el } = await setupEditor("<p>a []&nbsp;&nbsp;b</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_label_link").fill("link");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>a <a href="#">link[]</a>&nbsp;&nbsp;b</p>'
);
});
test("should insert a link then create a new <p>", async () => {
const { editor, el } = await setupEditor("<p>ab[]</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_label_link").fill("link");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
// press("Enter");
splitBlock(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">link</a></p><p placeholder='Type "/" for commands' class="o-we-hint">[]<br></p>`
);
});
test("should insert a link then create a new <p>, and another character", async () => {
const { editor, el } = await setupEditor("<p>a[]b</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_label_link").fill("link");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
// press("Enter");
splitBlock(editor);
await insertText(editor, "D");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>a<a href="#">link</a></p><p>D[]b</p>'
);
});
test("should insert a link then insert a <br>", async () => {
const { editor, el } = await setupEditor("<p>a[]b</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_label_link").fill("link");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
// press("Enter");
insertLineBreak(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe('<p>a<a href="#">link</a><br>[]b</p>');
});
test("should insert a link then insert a <br> and another character", async () => {
const { editor, el } = await setupEditor("<p>a[]b</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_label_link").fill("link");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("#");
// press("Enter");
insertLineBreak(editor);
await insertText(editor, "D");
expect(cleanLinkArtifacts(getContent(el))).toBe('<p>a<a href="#">link</a><br>D[]b</p>');
});
});
describe("Creation by toolbar", () => {
test("should convert all selected text to link", async () => {
const { el } = await setupEditor("<p>[Hello]</p>");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit(
"#"
);
expect(cleanLinkArtifacts(getContent(el))).toBe('<p><a href="#">Hello[]</a></p>');
});
test("should set the link on two existing characters", async () => {
const { el } = await setupEditor("<p>H[el]lo</p>");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit(
"#"
);
expect(cleanLinkArtifacts(getContent(el))).toBe('<p>H<a href="#">el[]</a>lo</p>');
});
test("when you open link popover with a label, url input should be focus by default ", async () => {
const { el } = await setupEditor("<p>[Hello]</p>");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(getActiveElement()).toBe(
queryOne(".o-we-linkpopover input.o_we_href_input_link")
);
await fill("test.com");
await click(".o_we_apply_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.com">Hello[]</a></p>'
);
});
test("should be correctly unlink/link", async () => {
const { el } = await setupEditor('<p>aaaa[b<a href="http://test.com/">cd</a>e]f</p>');
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
await tick();
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>aaaa[bcde]f</p>");
const pNode = queryOne("p");
setSelection({
anchorNode: pNode.childNodes[1],
anchorOffset: 1,
focusNode: pNode.childNodes[0],
focusOffset: 3,
});
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit(
"#"
);
expect(cleanLinkArtifacts(getContent(el))).toBe('<p>aaa<a href="#">ab[]</a>cdef</p>');
});
test("should remove link when click away without inputting url", async () => {
const { el } = await setupEditor("<p>H[el]lo</p>");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 0,
focusNode: pNode,
focusOffset: 0,
});
await tick();
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>[]Hello</p>");
});
test("when selection includes partially a link and click the link icon in toolbar, the link should be extended", async () => {
const { el } = await setupEditor('<p>a[b<a href="http://test.com/">c]d</a>ef</p>');
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(".o-we-linkpopover").toHaveCount(1);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>a<a href="http://test.com/">bcd[]</a>ef</p>'
);
});
test("when selection includes a link and click the link icon in toolbar, the link should be extended", async () => {
const { el } = await setupEditor('<p>a[b<a href="http://test.com/">cd</a>e]f</p>');
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(".o-we-linkpopover").toHaveCount(1);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>a<a href="http://test.com/">bcde[]</a>f</p>'
);
});
test("when selection includes another block and the link extending stays inside of the block", async () => {
const { el } = await setupEditor(
'<p>a[b<a href="http://test.com/">cd</a>ef</p><p>gh]</p>'
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(".o-we-linkpopover").toHaveCount(1);
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>a<a href="http://test.com/">bcdef[]</a></p><p>gh</p>'
);
});
test("when create a link on selection which doesn't include a link, it should create a new one", async () => {
await setupEditor(
'<p><strong>abc<a href="http://test.com/">de</a>te[st</strong> m]e</p>'
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(".o_we_label_link").toHaveValue("st m");
expect(".o_we_href_input_link").toHaveValue("");
});
test("create a link and undo it", async () => {
const { el, editor } = await setupEditor("<p>[Hello]</p>");
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit(
"#"
);
expect(cleanLinkArtifacts(getContent(el))).toBe('<p><a href="#">Hello[]</a></p>');
undo(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe("<p>[Hello]</p>");
});
test("extend a link on selection and undo it", async () => {
const { el, editor } = await setupEditor(
`<p>[<a href="https://www.test.com">Hello</a> my friend]</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await waitFor(".o-we-linkpopover", { timeout: 1500 });
expect(queryFirst(".o-we-linkpopover a").href).toBe("https://www.test.com/");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p><a href="https://www.test.com">Hello my friend[]</a></p>`
);
undo(editor);
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>[<a href="https://www.test.com">Hello</a> my friend]</p>`
);
});
});
});
describe.tags("desktop");
describe("Link formatting in the popover", () => {
test("click on link, the link popover should load the current format correctly", async () => {
await setupEditor(
'<p><a href="http://test.com/" class="btn btn-outline-primary rounded-circle btn-lg">link2[]</a></p>'
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
const linkPreviewEl = await waitFor("#link-preview");
expect(linkPreviewEl).toHaveClass([
"btn",
"btn-outline-primary",
"rounded-circle",
"btn-lg",
]);
expect(queryOne('select[name="link_type"]').selectedIndex).toBe(1);
expect(queryOne('select[name="link_style_size"]').selectedIndex).toBe(2);
expect(queryOne('select[name="link_style_shape"]').selectedIndex).toBe(3);
});
test("after changing the link format, the link preview should be updated", async () => {
await setupEditor(
'<p><a href="http://test.com/" class="btn btn-fill-secondary rounded-circle btn-sm">link2[]</a></p>'
);
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
const linkPreviewEl = await waitFor("#link-preview");
expect(linkPreviewEl).toHaveClass([
"btn",
"rounded-circle",
"btn-fill-secondary",
"btn-sm",
]);
expect(queryOne('select[name="link_type"]').selectedIndex).toBe(2);
expect(queryOne('select[name="link_style_size"]').selectedIndex).toBe(0);
expect(queryOne('select[name="link_style_shape"]').selectedIndex).toBe(1);
await click('select[name="link_type"');
await select("primary");
await click('select[name="link_style_size"');
await select("lg");
await click('select[name="link_style_shape"');
await select("fill,rounded-circle");
await animationFrame();
expect(linkPreviewEl).toHaveClass(["btn", "btn-fill-primary", "rounded-circle", "btn-lg"]);
});
test("after applying the link format, the link's format should be updated", async () => {
const { el } = await setupEditor('<p><a href="http://test.com/">link2[]</a></p>');
await waitFor(".o-we-linkpopover");
await click(".o_we_edit_link");
await waitFor("#link-preview");
expect(queryOne('select[name="link_type"]').selectedIndex).toBe(0);
await click('select[name="link_type"');
await select("secondary");
await animationFrame();
await click('select[name="link_style_shape"');
await select("outline,rounded-circle");
await animationFrame();
const linkPreviewEl = queryOne("#link-preview");
expect(linkPreviewEl).toHaveClass(["btn", "btn-outline-secondary", "rounded-circle"]);
await click(".o_we_apply_link");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.com/" class="btn btn-outline-secondary rounded-circle">link2[]</a></p>'
);
});
test("no preview of the link when the url is empty", async () => {
await setupEditor("<p><a>link2[]</a></p>");
await waitFor(".o-we-linkpopover");
expect("#link-preview").toHaveCount(0);
});
test("when no label input, the link preview should have the content of the url", async () => {
const { editor } = await setupEditor("<p>ab[]</p>");
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await waitFor(".o-we-linkpopover");
queryOne(".o_we_href_input_link").focus();
for (const char of "newtest.com") {
await press(char);
}
await animationFrame();
const linkPreviewEl = queryOne("#link-preview");
expect(linkPreviewEl).toHaveText("newtest.com");
});
});
describe("shortcut", () => {
test("create link shortcut should be at the first", async () => {
const { editor } = await setupEditor(`<p>[]</p>`);
editor.services.command.add("A test command", () => {}, {
hotkey: "alt+k",
category: "app",
});
await press(["ctrl", "k"]);
await animationFrame();
expect(queryText(".o_command_name:first")).toBe("Create link");
});
test("create a link with shortcut", async () => {
const { el } = await setupEditor(`<p>[]</p>`);
await press(["ctrl", "k"]);
await animationFrame();
await click(".o_command_name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.com">test.com[]</a></p>'
);
});
test("should be able to create link with ctrl+k and ctrl+k", async () => {
const { el } = await setupEditor(`<p>[]</p>`);
await press(["ctrl", "k"]);
await animationFrame();
await press(["ctrl", "k"]);
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.com">test.com[]</a></p>'
);
});
test("should be able to create link with ctrl+k , and should make link on two existing characters", async () => {
const { el } = await setupEditor(`<p>H[el]lo</p>`);
await press(["ctrl", "k"]);
await animationFrame();
await press(["ctrl", "k"]);
await contains(".o-we-linkpopover input.o_we_href_input_link").edit("test.com");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>H<a href="http://test.com">el[]</a>lo</p>'
);
});
test("Press enter to apply when create a link", async () => {
const { el } = await setupEditor(`<p><a>li[]nk</a></p>`);
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("test.com");
await press("Enter");
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p><a href="http://test.com">li[]nk</a></p>'
);
});
});
describe("link preview", () => {
test("test internal link preview", async () => {
onRpc("/html_editor/link_preview_internal", () => {
return {
description: markup("Test description"),
link_preview_name: "Task name | Project name",
};
});
onRpc("/odoo/project/1/tasks/8", () => new Response("", { status: 200 }));
const { editor, el } = await setupEditor(`<p>[]</p>`);
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill(
window.location.origin + "/odoo/project/1/tasks/8"
);
await animationFrame();
expect(".o_we_replace_title_btn").toHaveCount(1);
expect(".o_we_url_link").toHaveText("Task name | Project name");
expect(".o_we_description_link_preview").toHaveText("Test description");
await click(".o_we_replace_title_btn");
await animationFrame();
expect(".o_we_replace_title_btn").toHaveCount(0);
expect(cleanLinkArtifacts(el.textContent)).toBe("Task name | Project name");
});
test("test external link preview", async () => {
onRpc("/html_editor/link_preview_external", () => {
return {
og_description:
"From ERP to CRM, eCommerce and CMS. Download Odoo or use it in the cloud. Grow Your Business.",
og_image: "https://www.odoo.com/web/image/41207129-1abe7a15/homepage-seo.png",
og_title: "Open Source ERP and CRM | Odoo",
og_type: "website",
og_site_name: "Odoo",
source_url: "http://odoo.com/",
};
});
const { editor } = await setupEditor(`<p>[]</p>`);
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("http://odoo.com/");
await animationFrame();
expect(".o_we_replace_title_btn").toHaveCount(1);
expect(".o_extra_info_card").toHaveCount(1);
expect(".o_we_url_link").toHaveText("Open Source ERP and CRM | Odoo");
expect(".o_we_description_link_preview").toHaveText(
"From ERP to CRM, eCommerce and CMS. Download Odoo or use it in the cloud. Grow Your Business."
);
});
test("test internal metadata cached correctly", async () => {
onRpc("/html_editor/link_preview_internal", () => {
expect.step("/html_editor/link_preview_internal");
return {
description: markup("<p>Test description</p>"),
link_preview_name: "Task name | Project name",
};
});
onRpc("/odoo/cachetest/8", () => new Response("", { status: 200 }));
const { editor } = await setupEditor(`<p>abc[]</p>`);
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill(
window.location.origin + "/odoo/cachetest/8"
);
await animationFrame();
expect.verifySteps(["/html_editor/link_preview_internal"]);
expect(".o_we_url_link").toHaveText("Task name | Project name");
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 1,
focusNode: pNode,
focusOffset: 1,
});
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
const linkNode = queryOne("a");
setSelection({
anchorNode: linkNode,
anchorOffset: 1,
focusNode: linkNode,
focusOffset: 1,
});
await waitFor(".o-we-linkpopover");
expect.verifySteps([]);
});
test("test external metadata cached correctly", async () => {
onRpc("/html_editor/link_preview_external", () => {
expect.step("/html_editor/link_preview_external");
return {
og_description:
"From ERP to CRM, eCommerce and CMS. Download Odoo or use it in the cloud. Grow Your Business.",
og_image: "https://www.odoo.com/web/image/41207129-1abe7a15/homepage-seo.png",
og_title: "Open Source ERP and CRM | Odoo",
og_type: "website",
og_site_name: "Odoo",
source_url: "http://odoo.com/",
};
});
const { editor } = await setupEditor(`<p>[]</p>`);
await insertText(editor, "/link");
await animationFrame();
await click(".o-we-command-name:first");
await contains(".o-we-linkpopover input.o_we_href_input_link").fill("http://odoo.com/");
await animationFrame();
expect.verifySteps(["/html_editor/link_preview_external"]);
expect(".o_we_url_link").toHaveText("Open Source ERP and CRM | Odoo");
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 1,
focusNode: pNode,
focusOffset: 1,
});
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
const linkNode = queryOne("a");
setSelection({
anchorNode: linkNode,
anchorOffset: 1,
focusNode: linkNode,
focusOffset: 1,
});
await waitFor(".o-we-linkpopover");
expect.verifySteps([]);
});
});
describe("link in templates", () => {
test("Should not remove a link with t-attf-href", async () => {
const { el } = await setupEditor('<p>test<a t-attf-href="/test/1">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 0,
focusNode: pNode,
focusOffset: 0,
});
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>[]test<a t-attf-href="/test/1">link</a></p>'
);
});
test("Should not remove a link with t-att-href", async () => {
const { el } = await setupEditor('<p>test<a t-att-href="/test/1">li[]nk</a></p>');
await waitFor(".o-we-linkpopover");
expect(".o-we-linkpopover").toHaveCount(1);
const pNode = queryOne("p");
setSelection({
anchorNode: pNode,
anchorOffset: 0,
focusNode: pNode,
focusOffset: 0,
});
await waitUntil(() => !queryFirst(".o-we-linkpopover"), { timeout: 1500 });
expect(cleanLinkArtifacts(getContent(el))).toBe(
'<p>[]test<a t-att-href="/test/1">link</a></p>'
);
});
});
describe("links with inline image", () => {
test("can add link to inline image + text", async () => {
const { el } = await setupEditor(`<p>ab[cd<img src="${base64Img}">ef]g</p>`);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit("#");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">cd<img src="${base64Img}">ef[]</a>g</p>`
);
});
test("can undo add link to inline image + text", async () => {
const { editor, el } = await setupEditor(`<p>ab[cd<img src="${base64Img}">ef]g</p>`);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-link");
await contains(".o-we-linkpopover input.o_we_href_input_link", { timeout: 1500 }).edit("#");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">cd<img src="${base64Img}">ef[]</a>g</p>`
);
undo(editor);
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(`<p>ab[cd<img src="${base64Img}">ef]g</p>`);
});
test("can remove link from an inline image", async () => {
const { el } = await setupEditor(`<p>ab<a href="#">cd<img src="${base64Img}">ef</a>g</p>`);
await click("img");
await waitFor(".o-we-toolbar");
expect("button[name='unlink']").toHaveCount(1);
await click("button[name='unlink']");
await animationFrame();
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">cd[</a><img src="${base64Img}"><a href="#">]ef</a>g</p>`
);
expect(".o-we-linkpopover").toHaveCount(0);
});
test("can remove link from a selection of an inline image + text", async () => {
const { el } = await setupEditor(
`<p>ab<a href="#">c[d<img src="${base64Img}">e]f</a>g</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">c</a>[d<img src="${base64Img}">e]<a href="#">f</a>g</p>`
);
});
test("can remove link from a selection (ltr) with multiple inline images", async () => {
const { el } = await setupEditor(
`<p>ab<a href="#">c[d<img src="${base64Img}">e<img src="${base64Img}">f]g</a>h</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">c</a>[d<img src="${base64Img}">e<img src="${base64Img}">f]<a href="#">g</a>h</p>`
);
});
test("can remove link from a selection (rtl) with multiple inline images", async () => {
const { el } = await setupEditor(
`<p>ab<a href="#">c]d<img src="${base64Img}">e<img src="${base64Img}">f[g</a>h</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">c</a>]d<img src="${base64Img}">e<img src="${base64Img}">f[<a href="#">g</a>h</p>`
);
});
test("can remove link from a selection (ltr) with multiple inline images acrossing different links", async () => {
const { el } = await setupEditor(
`<p>ab<a href="#">c[d<img src="${base64Img}">e</a>xx<a href="#">f<img src="${base64Img}">g]h</a>i</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">c</a>[d<img src="${base64Img}">exxf<img src="${base64Img}">g]<a href="#">h</a>i</p>`
);
});
test("can remove link from a selection (rtl) with multiple inline images acrossing different links", async () => {
const { el } = await setupEditor(
`<p>ab<a href="#">c]d<img src="${base64Img}">e</a>xx<a href="#">f<img src="${base64Img}">g[h</a>i</p>`
);
await waitFor(".o-we-toolbar");
await click(".o-we-toolbar .fa-unlink");
expect(cleanLinkArtifacts(getContent(el))).toBe(
`<p>ab<a href="#">c</a>]d<img src="${base64Img}">exxf<img src="${base64Img}">g[<a href="#">h</a>i</p>`
);
});
});