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

937 lines
35 KiB
JavaScript

import { describe, expect, test } from "@odoo/hoot";
import {
click,
keyDown,
keyUp,
manuallyDispatchProgrammaticEvent,
pointerDown,
pointerUp,
press,
queryAll,
queryAllTexts,
waitFor,
waitForNone,
waitUntil,
} from "@odoo/hoot-dom";
import { advanceTime, animationFrame, tick } from "@odoo/hoot-mock";
import { contains, patchTranslations, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { fontSizeItems, fontItems } from "../src/main/font/font_plugin";
import { Plugin } from "../src/plugin";
import { MAIN_PLUGINS } from "../src/plugin_sets";
import { convertNumericToUnit, getCSSVariableValue, getHtmlStyle } from "../src/utils/formatting";
import { setupEditor } from "./_helpers/editor";
import { unformat } from "./_helpers/format";
import {
getContent,
moveSelectionOutsideEditor,
setContent,
setSelection,
} from "./_helpers/selection";
import { withSequence } from "@html_editor/utils/resource";
import { strong } from "./_helpers/tags";
test.tags("desktop");
test("toolbar is only visible when selection is not collapsed in desktop", async () => {
const { el } = await setupEditor("<p>test</p>");
// set a non-collapsed selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(1);
// set a collapsed selection to close toolbar
setContent(el, "<p>test[]</p>");
await waitUntil(() => !document.querySelector(".o-we-toolbar"));
expect(".o-we-toolbar").toHaveCount(0);
});
test.tags("mobile");
test("toolbar is also visible when selection is collapsed in mobile", async () => {
const { el } = await setupEditor("<p>test</p>");
// set a non-collapsed selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(1);
setContent(el, "<p>test[]</p>");
await animationFrame();
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar closes when selection leaves editor", async () => {
const { el } = await setupEditor("<p>test</p>");
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
await click(document.body);
moveSelectionOutsideEditor();
await waitForNone(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(0);
});
test("toolbar works: can format bold", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(getContent(el)).toBe("<p>test</p>");
// set selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
// click on toggle bold
await contains(".btn[name='bold']").click();
expect(getContent(el)).toBe("<p><strong>[test]</strong></p>");
});
test.tags("iframe");
test("toolbar in an iframe works: can format bold", async () => {
const { el } = await setupEditor("<p>test</p>", { props: { iframe: true } });
expect("iframe").toHaveCount(1);
expect(getContent(el)).toBe("<p>test</p>");
// set selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
// click on toggle bold
await contains(".btn[name='bold']").click();
expect(getContent(el)).toBe("<p><strong>[test]</strong></p>");
});
test("toolbar buttons react to selection change", async () => {
const { el } = await setupEditor("<p>test some text</p>");
// set selection to open toolbar
setContent(el, "<p>[test] some text</p>");
await waitFor(".o-we-toolbar");
// check that bold button is not active
expect(".btn[name='bold']").not.toHaveClass("active");
// click on toggle bold
await contains(".btn[name='bold']").click();
expect(getContent(el)).toBe("<p><strong>[test]</strong> some text</p>");
expect(".btn[name='bold']").toHaveClass("active");
// set selection where text is not bold
setContent(el, "<p><strong>test</strong> some [text]</p>");
await waitFor(".btn[name='bold']:not(.active)");
expect(".btn[name='bold']").not.toHaveClass("active");
// set selection again where text is bold
setContent(el, "<p><strong>[test]</strong> some text</p>");
await waitFor(".btn[name='bold'].active");
expect(".btn[name='bold']").toHaveClass("active");
});
test("toolbar buttons react to selection change (2)", async () => {
const { el } = await setupEditor("<p><strong>test [some]</strong> some text</p>");
await waitFor(".o-we-toolbar");
expect(".btn[name='bold']").toHaveClass("active");
// extends selection to include non-bold text
setContent(el, "<p><strong>test [some</strong> some] text</p>");
// @todo @phoenix: investigate why waiting for animation frame is (sometimes) not enough
await waitFor(".btn[name='bold']:not(.active)");
expect(".btn[name='bold']").not.toHaveClass("active");
// change selection to come back into bold text
setContent(el, "<p><strong>test [so]me</strong> some text</p>");
await waitFor(".btn[name='bold'].active");
expect(".btn[name='bold']").toHaveClass("active");
});
test("toolbar list buttons react to selection change", async () => {
const { el } = await setupEditor("<ul><li>[abc]</li></ul>");
await waitFor(".o-we-toolbar");
expect(".btn[name='bulleted_list']").toHaveClass("active");
expect(".btn[name='numbered_list']").not.toHaveClass("active");
expect(".btn[name='checklist']").not.toHaveClass("active");
// Toggle to numbered list
await click(".btn[name='numbered_list']");
await waitFor(".btn[name='numbered_list'].active");
expect(getContent(el)).toBe("<ol><li>[abc]</li></ol>");
expect(".btn[name='bulleted_list']").not.toHaveClass("active");
expect(".btn[name='numbered_list']").toHaveClass("active");
expect(".btn[name='checklist']").not.toHaveClass("active");
// Toggle to checklist
await click(".btn[name='checklist']");
await waitFor(".btn[name='checklist'].active");
expect(getContent(el)).toBe('<ul class="o_checklist"><li>[abc]</li></ul>');
expect(".btn[name='bulleted_list']").not.toHaveClass("active");
expect(".btn[name='numbered_list']").not.toHaveClass("active");
expect(".btn[name='checklist']").toHaveClass("active");
// Toggle list off
await click(".btn[name='checklist']");
await waitFor(".btn[name='checklist']:not(.active)");
expect(getContent(el)).toBe("<p>[abc]</p>");
expect(".btn[name='bulleted_list']").not.toHaveClass("active");
expect(".btn[name='numbered_list']").not.toHaveClass("active");
expect(".btn[name='checklist']").not.toHaveClass("active");
});
test("toolbar link buttons react to selection change", async () => {
const { el } = await setupEditor("<p>th[is is a] <a>link</a> test!</p>");
await waitFor(".o-we-toolbar");
expect(".btn[name='link']").toHaveCount(1);
expect(".btn[name='link']").not.toHaveClass("active");
expect(".btn[name='unlink']").toHaveCount(0);
setContent(el, "<p>th[is is a <a>li]nk</a> test!</p>");
await waitFor(".btn[name='link'].active");
expect(".btn[name='link']").toHaveCount(1);
expect(".btn[name='link']").toHaveClass("active");
expect(".btn[name='unlink']").toHaveCount(1);
setContent(el, "<p>th[is is a <a>link</a> tes]t!</p>");
await waitFor(".btn[name='link']:not(.active)");
expect(".btn[name='link']").toHaveCount(1);
expect(".btn[name='link']").not.toHaveClass("active");
expect(".btn[name='unlink']").toHaveCount(1);
});
test("toolbar works: can select font", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(getContent(el)).toBe("<p>test</p>");
// set selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar [name='font']").toHaveText("Normal");
await contains(".o-we-toolbar [name='font'] .dropdown-toggle").click();
await contains(".o_font_selector_menu .dropdown-item:contains('Header 2')").click();
expect(getContent(el)).toBe("<h2>[test]</h2>");
expect(".o-we-toolbar [name='font']").toHaveText("Header 2");
});
test("toolbar works: show the right font name", async () => {
await setupEditor("<p>[test]</p>");
await waitFor(".o-we-toolbar");
for (const item of fontItems) {
await contains(".o-we-toolbar [name='font'] .dropdown-toggle").click();
await animationFrame();
const name = item.name.toString();
let selector = `.o_font_selector_menu .dropdown-item:contains('${name}')`;
for (const tempItem of fontItems) {
// we need to exclude the font names which have the current name as a substring.
if (tempItem === item) {
continue;
}
const tempItemName = tempItem.name.toString();
if (tempItemName.includes(name)) {
selector += `:not(:contains(${tempItemName}))`;
}
}
await contains(selector).click();
await animationFrame();
expect(".o-we-toolbar [name='font']").toHaveText(name);
}
});
test("toolbar works: show the right font name after undo", async () => {
const { el } = await setupEditor("<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar [name='font']").toHaveText("Normal");
await contains(".o-we-toolbar [name='font'] .dropdown-toggle").click();
await contains(".o_font_selector_menu .dropdown-item:contains('Header 2')").click();
expect(getContent(el)).toBe("<h2>[test]</h2>");
expect(".o-we-toolbar [name='font']").toHaveText("Header 2");
await press(["ctrl", "z"]);
await animationFrame();
expect(getContent(el)).toBe("<p>[test]</p>");
expect(".o-we-toolbar [name='font']").toHaveText("Normal");
await press(["ctrl", "y"]);
await animationFrame();
expect(getContent(el)).toBe("<h2>[test]</h2>");
expect(".o-we-toolbar [name='font']").toHaveText("Header 2");
});
test("toolbar works: can select font size", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(getContent(el)).toBe("<p>test</p>");
const style = getHtmlStyle(document);
const getFontSizeFromVar = (cssVar) => {
const strValue = getCSSVariableValue(cssVar, style);
const remValue = parseFloat(strValue);
const pxValue = convertNumericToUnit(remValue, "rem", "px", style);
return Math.round(pxValue);
};
// set selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar [name='font-size']").toHaveText(
getFontSizeFromVar("body-font-size").toString()
);
await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click();
const sizes = new Set(
fontSizeItems.map((item) => {
return getFontSizeFromVar(item.variableName).toString();
})
);
expect(queryAllTexts(".o_font_selector_menu .dropdown-item")).toEqual([...sizes]);
const h1Size = getFontSizeFromVar("h1-font-size").toString();
await contains(`.o_font_selector_menu .dropdown-item:contains('${h1Size}')`).click();
expect(getContent(el)).toBe(`<p><span class="h1-fs">[test]</span></p>`);
expect(".o-we-toolbar [name='font-size']").toHaveText(h1Size);
await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click();
const oSmallSize = getFontSizeFromVar("small-font-size").toString();
await contains(`.o_font_selector_menu .dropdown-item:contains('${oSmallSize}')`).click();
expect(getContent(el)).toBe(`<p><span class="o_small-fs">[test]</span></p>`);
expect(".o-we-toolbar [name='font-size']").toHaveText(oSmallSize);
});
test.tags("desktop");
test("toolbar works: display correct font size on select all", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(getContent(el)).toBe("<p>test</p>");
// set selection to open toolbar
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
const style = getHtmlStyle(document);
const getFontSizeFromVar = (cssVar) => {
const strValue = getCSSVariableValue(cssVar, style);
const remValue = parseFloat(strValue);
const pxValue = convertNumericToUnit(remValue, "rem", "px", style);
return Math.round(pxValue);
};
await waitFor(".o-we-toolbar");
await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click();
await animationFrame();
const h1Size = getFontSizeFromVar("h1-font-size").toString();
await contains(`.o_font_selector_menu .dropdown-item:contains('${h1Size}')`).click();
expect(getContent(el)).toBe(`<p><span class="h1-fs">[test]</span></p>`);
setContent(el, `<p><span class="h1-fs">te[]st</span></p>`);
await waitForNone(".o-we-toolbar");
await press(["ctrl", "a"]); // Select all
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar [name='font-size']").toHaveText(`${h1Size}`);
});
test.tags("desktop");
test("toolbar should not open on keypress tab inside table", async () => {
const contentBefore = unformat(`
<table>
<tbody>
<tr>
<td><p>[]ab</p></td>
<td><p>cd</p></td>
</tr>
</tbody>
</table>
`);
const contentAfter = unformat(`
<table>
<tbody>
<tr>
<td><p>ab</p></td>
<td><p>cd[]</p></td>
</tr>
</tbody>
</table>
`);
const { el } = await setupEditor(contentBefore);
await press("Tab");
expect(getContent(el)).toBe(contentAfter);
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
test("toolbar open on single selected cell in table", async () => {
const contentBefore = unformat(`
<table class="table table-bordered o_table">
<tbody>
<tr>
<td><p>[]<br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
`);
const { el } = await setupEditor(contentBefore);
const targetTd = el.querySelector("td");
const mouseDownPositionX = targetTd.getBoundingClientRect().left + 10;
const mouseDownPositionY = targetTd.getBoundingClientRect().top + 10;
const mouseMoveDiff = 40;
manuallyDispatchProgrammaticEvent(targetTd, "mousedown", {
clientX: mouseDownPositionX,
clientY: mouseDownPositionY,
});
// Simulate mousemove horizontally for 40px.
manuallyDispatchProgrammaticEvent(targetTd, "mousemove", {
clientX: mouseDownPositionX + mouseMoveDiff,
clientY: mouseDownPositionY,
});
manuallyDispatchProgrammaticEvent(targetTd, "mouseup", {
clientX: mouseDownPositionX + mouseMoveDiff,
clientY: mouseDownPositionY,
});
await animationFrame();
await tick();
expect(targetTd).toHaveClass("o_selected_td");
expect(".o-we-toolbar").toHaveCount(1);
});
test.tags("desktop");
test("toolbar should close on keypress tab inside table", async () => {
const contentBefore = unformat(`
<table>
<tbody>
<tr>
<td><p>[ab]</p></td>
<td><p>cd</p></td>
</tr>
</tbody>
</table>
`);
const contentAfter = unformat(`
<table>
<tbody>
<tr>
<td><p>ab</p></td>
<td><p>cd[]</p></td>
</tr>
</tbody>
</table>
`);
const { el } = await setupEditor(contentBefore);
await waitFor(".o-we-toolbar");
await press("Tab");
expect(getContent(el)).toBe(contentAfter);
await waitForNone(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(0);
});
test("toolbar buttons shouldn't be active without text node in the selection", async () => {
await setupEditor("<div>[<p><br></p>]</div>");
await waitFor(".o-we-toolbar");
expect(queryAll(".o-we-toolbar .btn.active").length).toBe(0);
});
test("toolbar behave properly if selection has no range", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[test]</p>");
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(1);
const selection = document.getSelection();
selection.removeAllRanges();
setContent(el, "<p>abc</p>");
await waitUntil(() => !document.querySelector(".o-we-toolbar"));
expect(".o-we-toolbar").toHaveCount(0);
});
test("toolbar correctly show namespace button group and stop showing when namespace change", async () => {
class TestPlugin extends Plugin {
static id = "TestPlugin";
resources = {
toolbar_namespaces: [
{
id: "aNamespace",
isApplied: (nodeList) => {
return !!nodeList.find((node) => node.tagName === "DIV");
},
},
],
user_commands: { id: "test_cmd", run: () => null },
toolbar_groups: withSequence(24, { id: "test_group", namespace: "aNamespace" }),
toolbar_items: [
{
id: "test_btn",
groupId: "test_group",
commandId: "test_cmd",
title: "Test Button",
icon: "fa-square",
},
],
};
}
const { el } = await setupEditor("<div>[<section><p>abc</p></section><div>d]ef</div></div>", {
config: { Plugins: [...MAIN_PLUGINS, TestPlugin] },
});
await waitFor(".o-we-toolbar");
expect(".btn-group[name='test_group']").toHaveCount(1);
setContent(el, "<div><section><p>[abc]</p></section><div>def</div></div>");
await animationFrame();
expect(".btn-group[name='test_group']").toHaveCount(0);
});
test("toolbar does not evaluate isActive when namespace does not match", async () => {
class TestPlugin extends Plugin {
static id = "TestPlugin";
resources = {
user_commands: { id: "test_cmd", run: () => null },
toolbar_groups: withSequence(24, { id: "test_group", namespace: "image" }),
toolbar_items: [
{
id: "test_btn",
groupId: "test_group",
commandId: "test_cmd",
title: "Test Button",
icon: "fa-square",
isActive: () => expect.step("image format evaluated"),
},
],
};
}
await setupEditor(
`
<div>
<p>[Foo]</p>
<img class="img-fluid" src="/web/static/img/logo.png">
</div>
`,
{
config: { Plugins: [...MAIN_PLUGINS, TestPlugin] },
}
);
await waitFor(".o-we-toolbar");
expect.verifySteps([]);
await click("img");
await animationFrame();
expect.verifySteps(["image format evaluated"]);
});
test("plugins can create buttons with text in toolbar", async () => {
class TestPlugin extends Plugin {
static id = "TestPlugin";
resources = {
user_commands: { id: "test_cmd", run: () => null },
toolbar_groups: withSequence(24, { id: "test_group" }),
toolbar_items: [
{
id: "test_btn",
groupId: "test_group",
commandId: "test_cmd",
title: "Test Button",
text: "Text button",
},
],
};
}
await setupEditor(`<div> <p class="foo">[Foo]</p> </div>`, {
config: { Plugins: [...MAIN_PLUGINS, TestPlugin] },
});
await waitFor(".o-we-toolbar");
expect("button[name='test_btn']").toHaveText("Text button");
});
test("toolbar buttons should have rounded corners at the edges of a group", async () => {
await setupEditor("<p>[test]</p>");
await waitFor(".o-we-toolbar");
const buttonGroups = queryAll(".o-we-toolbar .btn-group");
for (const group of buttonGroups) {
for (let i = 0; i < group.children.length; i++) {
const button = group.children[i];
const computedStyle = getComputedStyle(button);
const borderRadius = Object.fromEntries(
["top-left", "top-right", "bottom-left", "bottom-right"].map((corner) => [
corner,
Number.parseInt(computedStyle[`border-${corner}-radius`]),
])
);
// Should have rounded corners on the left only if first button
if (i === 0) {
expect(borderRadius["top-left"]).toBeGreaterThan(0);
expect(borderRadius["bottom-left"]).toBeGreaterThan(0);
} else {
expect(borderRadius["top-left"]).toBe(0);
expect(borderRadius["bottom-left"]).toBe(0);
}
// Should have rounded corners on the right only if last button
if (i === group.children.length - 1) {
expect(borderRadius["top-right"]).toBeGreaterThan(0);
expect(borderRadius["bottom-right"]).toBeGreaterThan(0);
} else {
expect(borderRadius["top-right"]).toBe(0);
expect(borderRadius["bottom-right"]).toBe(0);
}
}
}
});
test("toolbar buttons should have title attribute", async () => {
await setupEditor("<ul><li>[abc]</li></ul>");
const toolbar = await waitFor(".o-we-toolbar");
for (const button of toolbar.querySelectorAll("button")) {
expect(button).toHaveAttribute("title");
}
});
test("toolbar buttons should have title attribute with translated text", async () => {
// Retrieve toolbar buttons descriptions in English
const { editor, plugins } = await setupEditor("");
// item.label could be a LazyTranslatedString so we ensure it is a string with toString()
const titles = plugins
.get("toolbar")
.getButtons()
.map((item) => item.title.toString());
editor.destroy();
// Patch translations to return "Translated" for these terms
patchTranslations(Object.fromEntries(titles.map((title) => [title, "Translated"])));
// Instantiate a new editor.
const { plugins: postPatchPlugins } = await setupEditor("<p>[abc]</p>");
// Check that every registered button has the result of the call to _t
postPatchPlugins
.get("toolbar")
.getButtons()
.forEach((item) => {
// item.label could be a LazyTranslatedString so we ensure it is a string with toString()
expect(item.title.toString()).toBe("Translated");
});
await waitFor(".o-we-toolbar");
// Check that every button has a title attribute with the translated description
for (const button of queryAll(".o-we-toolbar button")) {
expect(button).toHaveAttribute("title", "Translated");
}
});
test.tags("desktop");
test("close the toolbar if the selection contains any nodes (traverseNode = [])", async () => {
const { el } = await setupEditor("<p>a</p><p>b</p>");
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[a</p><p>]b</p>");
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(1);
// This selection is possible when you double-click at the end of a line.
setContent(el, "<p>a[</p><p>]b</p>");
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
test.tags("desktop");
test("close the toolbar if the selection contains any nodes (traverseNode = [], ignore whitespace)", async () => {
const { el } = await setupEditor("<p>a</p>\n<p>b</p>");
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, "<p>[a</p>\n<p>]b</p>");
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(1);
// This selection is possible when you double-click at the end of a line.
setContent(el, "<p>a[</p>\n<p>]b</p>");
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
test.tags("desktop");
test("close the toolbar if the selection contains any nodes (traverseNode = [], ignore zws)", async () => {
const { el } = await setupEditor(`<p>ab${strong("\u200B", "first")}cd</p>`);
expect(".o-we-toolbar").toHaveCount(0);
setContent(el, `<p>a[b${strong("\u200B", "first")}c]d</p>`);
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(1);
setContent(el, `<p>ab${strong("[\u200B]", "first")}cd</p>`);
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
describe.tags("desktop");
describe("toolbar open and close on user interaction", () => {
describe("mouse", () => {
test("toolbar should not open while mousedown (only after mouseup)", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(".o-we-toolbar").toHaveCount(0);
await pointerDown(el);
// <p>[]test</p>
setSelection({ anchorNode: el.children[0], anchorOffset: 0 });
await tick(); // selectionChange
// Simulate extending the selection with mousedown
// <p>[test]</p>
setSelection({ anchorNode: el.children[0], anchorOffset: 0, focusOffset: 1 });
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
await pointerUp(el);
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should open on mouseup after selecting text (even if mouseup happens outside the editable)", async () => {
const { el } = await setupEditor("<p>test</p>");
expect(".o-we-toolbar").toHaveCount(0);
await pointerDown(el);
// <p>[]test</p>
setSelection({ anchorNode: el.children[0], anchorOffset: 0 });
await tick(); // selectionChange
// Simulate extending the selection with mousedown
// <p>[test]</p>
setSelection({ anchorNode: el.children[0], anchorOffset: 0, focusOffset: 1 });
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
await pointerUp(el.ownerDocument);
await waitFor(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should close on mousedown", async () => {
const { el } = await setupEditor("<p>[test]</p><p>text</p>");
await waitFor(".o-we-toolbar");
await pointerDown(el);
// <p>test</p><p>[]text</p>
setSelection({ anchorNode: el.children[1], anchorOffset: 0 });
await tick(); // selectionChange
await waitForNone(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(0);
await pointerUp(el);
await tick();
expect(getContent(el)).toBe("<p>test</p><p>[]text</p>");
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
test("toolbar should close on mousedown (2)", async () => {
const { el } = await setupEditor("<p>[test]</p>");
/** @todo fix warnings */
patchWithCleanup(console, { warn: () => {} });
await waitFor(".o-we-toolbar");
// Mousedown on the selected text: it does not change the selection until mouseup
await pointerDown(el);
await tick();
await waitForNone(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(0);
await pointerUp(el);
setContent(el, "<p>[]test</p>");
await tick();
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
});
const firstClick = async (target) => {
manuallyDispatchProgrammaticEvent(target, "mousedown", { detail: 1 });
setSelection({ anchorNode: target, anchorOffset: 0 });
await tick(); // selectionChange
manuallyDispatchProgrammaticEvent(target, "mouseup", { detail: 1 });
manuallyDispatchProgrammaticEvent(target, "click", { detail: 1 });
await tick();
};
const secondClick = async (target) => {
manuallyDispatchProgrammaticEvent(target, "mousedown", { detail: 2 });
const document = target.ownerDocument;
document.getSelection().modify("extend", "forward", "word");
await tick(); // selectionChange
manuallyDispatchProgrammaticEvent(target, "mouseup", { detail: 2 });
manuallyDispatchProgrammaticEvent(target, "click", { detail: 2 });
await tick();
};
const thirdClick = async (target) => {
manuallyDispatchProgrammaticEvent(target, "mousedown", { detail: 3 });
const document = target.ownerDocument;
document.getSelection().modify("extend", "forward", "paragraphboundary");
await tick(); // selectionChange
manuallyDispatchProgrammaticEvent(target, "mouseup", { detail: 3 });
manuallyDispatchProgrammaticEvent(target, "click", { detail: 3 });
await tick();
};
test("toolbar should open on double click", async () => {
const { el } = await setupEditor("<p>test</p>");
const p = el.firstElementChild;
// Double click
await firstClick(p);
await secondClick(p);
expect(getContent(el)).toBe("<p>[test]</p>");
// toolbar open after double click is debounced
await advanceTime(500);
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should open on triple click", async () => {
const { el } = await setupEditor("<p>test text</p>");
const p = el.firstElementChild;
// Triple click
await firstClick(p);
await secondClick(p);
await thirdClick(p);
expect(getContent(el)).toBe("<p>[test text]</p>");
// toolbar open after triple click is debounced
await advanceTime(500);
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should not open between double and triple click", async () => {
const { el } = await setupEditor("<p>test text</p>");
const p = el.firstElementChild;
// Double click
await firstClick(p);
await secondClick(p);
expect(getContent(el)).toBe("<p>[test] text</p>");
await advanceTime(100);
// Toolbar is not open yet, waiting for a possible third click
expect(".o-we-toolbar").toHaveCount(0);
// Third click
await thirdClick(p);
expect(getContent(el)).toBe("<p>[test text]</p>");
await advanceTime(500);
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should not open after triple click while mouse is down", async () => {
const { el } = await setupEditor("<p>test text</p>");
const p = el.firstElementChild;
await firstClick(p);
await secondClick(p);
await pointerDown(p);
manuallyDispatchProgrammaticEvent(p, "mousedown", { detail: 3 });
setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 });
await tick(); // selectionChange
expect(getContent(el)).toBe("<p>[test text]</p>");
await advanceTime(500);
// Toolbar is not open yet, waiting for mouseup
expect(".o-we-toolbar").toHaveCount(0);
// Mouse up
manuallyDispatchProgrammaticEvent(p, "mouseup", { detail: 3 });
manuallyDispatchProgrammaticEvent(p, "click", { detail: 3 });
await advanceTime(500);
expect(".o-we-toolbar").toHaveCount(1);
});
});
describe("keyboard", () => {
test("toolbar should not open on keydown Arrow (only after keyup)", async () => {
const { el } = await setupEditor("<p>[]test</p>");
expect(".o-we-toolbar").toHaveCount(0);
await keyDown(["Shift", "ArrowRight"]);
setContent(el, "<p>[t]est</p>");
await tick(); // selectionChange
await animationFrame();
expect(".o-we-toolbar").toHaveCount(0);
await keyUp(["Shift", "ArrowRight"]);
await advanceTime(500); // Toolbar open on keyup is debounced
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should close on keydown Arrow", async () => {
const { el } = await setupEditor("<p>[tes]t</p>");
await waitFor(".o-we-toolbar");
// Toolbar should close on keydown
await keyDown(["Shift", "ArrowRight"]);
setContent(el, "<p>[test]</p>");
await tick(); // selectionChange
await waitForNone(".o-we-toolbar");
expect(".o-we-toolbar").toHaveCount(0);
// Toolbar should open after keyup
await keyUp(["Shift", "ArrowRight"]);
await advanceTime(500); // toolbar open on keyup is debounced
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should not close on keydown shift or control", async () => {
await setupEditor("<p>[tes]t</p>");
await waitFor(".o-we-toolbar");
// Toolbar should not close on keydown shift
await keyDown(["Shift"]);
await tick();
expect(".o-we-toolbar").toHaveCount(1);
await keyUp(["Shift"]);
await tick();
expect(".o-we-toolbar").toHaveCount(1);
// Toolbar should not close on keydown ctrl
await keyDown(["Control"]);
await tick();
expect(".o-we-toolbar").toHaveCount(1);
await keyUp(["Control"]);
await tick();
expect(".o-we-toolbar").toHaveCount(1);
});
test("toolbar should not open between keystrokes separated by a short interval", async () => {
const { el } = await setupEditor("<p>[]test</p>");
expect(".o-we-toolbar").toHaveCount(0);
// Keystroke # 1
await keyDown(["Shift", "ArrowRight"]);
setContent(el, "<p>[t]est</p>");
await tick(); // selectionChange
await keyUp(["Shift", "ArrowRight"]);
await advanceTime(100);
expect(".o-we-toolbar").toHaveCount(0);
// Keystroke # 2
await keyDown(["Shift", "ArrowRight"]);
setContent(el, "<p>[te]st</p>");
await tick(); // selectionChange
await keyUp(["Shift", "ArrowRight"]);
await advanceTime(100);
expect(".o-we-toolbar").toHaveCount(0);
// Toolbar opens some time after the last keyup
await advanceTime(500);
expect(".o-we-toolbar").toHaveCount(1);
});
});
});