import { withSequence } from "@html_editor/utils/resource"; import { describe, expect, test } from "@odoo/hoot"; import { click, getActiveElement, keyDown, keyUp, manuallyDispatchProgrammaticEvent, pointerDown, pointerUp, press, queryAll, queryAllTexts, queryOne, waitFor, waitForNone, } from "@odoo/hoot-dom"; import { advanceTime, animationFrame, tick } from "@odoo/hoot-mock"; import { contains, onRpc, patchTranslations, patchWithCleanup, defineModels, fields, models, mountView, } from "@web/../tests/web_test_helpers"; import { fontItems, fontSizeItems } 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, simulateDoubleClickSelect, simulateTripleClickSelect, firstClick, secondClick, thirdClick, } from "./_helpers/selection"; import { strong } from "./_helpers/tags"; import { delay } from "@web/core/utils/concurrency"; import { nodeSize } from "@html_editor/utils/position"; import { expectElementCount } from "./_helpers/ui_expectations"; test.tags("desktop"); test("toolbar is only visible when selection is not collapsed in desktop", async () => { const { el } = await setupEditor("
test
"); // set a non-collapsed selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await expectElementCount(".o-we-toolbar", 1); // set a collapsed selection to close toolbar setContent(el, "test[]
"); await expectElementCount(".o-we-toolbar", 0); }); test.tags("mobile"); test("toolbar is also visible when selection is collapsed in mobile", async () => { const { el } = await setupEditor("test
"); // set a non-collapsed selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await expectElementCount(".o-we-toolbar", 1); setContent(el, "test[]
"); await animationFrame(); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar closes when selection leaves editor", async () => { const { el } = await setupEditor("test
"); setContent(el, "[test]
"); await waitFor(".o-we-toolbar"); await click(document.body); moveSelectionOutsideEditor(); await expectElementCount(".o-we-toolbar", 0); }); test("toolbar works: can format bold", async () => { const { el } = await setupEditor("test
"); expect(getContent(el)).toBe("test
"); // set selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await waitFor(".o-we-toolbar"); // click on toggle bold await contains(".btn[name='bold']").click(); expect(getContent(el)).toBe("[test]
"); }); test.tags("iframe"); test("toolbar in an iframe works: can format bold", async () => { const { el } = await setupEditor("test
", { props: { iframe: true } }); expect("iframe").toHaveCount(1); expect(getContent(el)).toBe("test
"); // set selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await waitFor(".o-we-toolbar"); // click on toggle bold await contains(".btn[name='bold']").click(); expect(getContent(el)).toBe("[test]
"); }); test("toolbar buttons react to selection change", async () => { const { el } = await setupEditor("test some text
"); // set selection to open toolbar setContent(el, "[test] some text
"); 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("[test] some text
"); expect(".btn[name='bold']").toHaveClass("active"); // set selection where text is not bold setContent(el, "test some [text]
"); await waitFor(".btn[name='bold']:not(.active)"); expect(".btn[name='bold']").not.toHaveClass("active"); // set selection again where text is bold setContent(el, "[test] some text
"); 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("test [some] some text
"); await waitFor(".o-we-toolbar"); expect(".btn[name='bold']").toHaveClass("active"); // extends selection to include non-bold text setContent(el, "test [some some] text
"); // @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, "test [so]me some text
"); 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("[abc]
"); 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("th[is is a] link test!
"); 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, "th[is is a li]nk test!
"); 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, "th[is is a link tes]t!
"); 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 format buttons should react to format change", async () => { await setupEditor( `b
[test.com]
`); await waitFor(".o-we-toolbar"); expect(".btn[name='link']").not.toHaveClass("disabled"); }); test("toolbar works: can select font", async () => { const { el } = await setupEditor("test
"); expect(getContent(el)).toBe("test
"); // set selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar [name='font']").toHaveText("Paragraph"); 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("[test]
"); await waitFor(".o-we-toolbar"); const items = fontItems; for (const item of items) { 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 items) { // 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("[test]
"); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar [name='font']").toHaveText("Paragraph"); 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("[test]
"); expect(".o-we-toolbar [name='font']").toHaveText("Paragraph"); await press(["ctrl", "y"]); await animationFrame(); expect(getContent(el)).toBe("test
"); expect(getContent(el)).toBe("test
"); 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 await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await waitFor(".o-we-toolbar"); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); expect(inputEl).toHaveValue(getFontSizeFromVar("body-font-size").toString()); await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click(); const sizes = new Set( fontSizeItems.map((item) => getFontSizeFromVar(item.variableName).toString()) ); expect(queryAllTexts(".o_font_size_selector_menu .dropdown-item")).toEqual([...sizes]); const h1Size = getFontSizeFromVar("h1-font-size").toString(); await contains(`.o_font_size_selector_menu .dropdown-item:contains('${h1Size}')`).click(); expect(getContent(el)).toBe(`[test]
`); expect(inputEl).toHaveValue(h1Size); await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click(); const oSmallSize = getFontSizeFromVar("small-font-size").toString(); await contains(`.o_font_size_selector_menu .dropdown-item:contains('${oSmallSize}')`).click(); expect(getContent(el)).toBe(`[test]
`); expect(inputEl).toHaveValue(oSmallSize); }); test("should focus the editable area after selecting a font size item", async () => { const { editor, el } = await setupEditor("[test]
"); await expectElementCount(".o-we-toolbar", 1); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click(); expect(getActiveElement()).toBe(inputEl); await waitFor(".o_font_size_selector_menu .dropdown-item:contains('21')"); await contains(".o_font_size_selector_menu .dropdown-item:contains('21')").click(); expect(getActiveElement()).toBe(editor.editable); expect(getActiveElement()).not.toBe(inputEl); expect(getContent(el)).toBe(`[test]
`); }); test.tags("desktop"); test("toolbar works: display correct font size on select all", async () => { const { el } = await setupEditor("test
"); expect(getContent(el)).toBe("test
"); // set selection to open toolbar await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); 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"); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); await contains(".o-we-toolbar [name='font-size'] .dropdown-toggle").click(); await animationFrame(); const h1Size = getFontSizeFromVar("h1-font-size").toString(); await contains(`.o_font_size_selector_menu .dropdown-item:contains('${h1Size}')`).click(); expect(getContent(el)).toBe(`[test]
`); setContent(el, `te[]st
`); await waitForNone(".o-we-toolbar"); await press(["ctrl", "a"]); // Select all await waitFor(".o-we-toolbar"); expect(inputEl).toHaveValue(`${h1Size}`); }); test("toolbar works: displays correct font size on input", async () => { const { el } = await setupEditor("[test]
"); await waitFor(".o-we-toolbar"); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); expect(iframeEl).toHaveCount(1); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); await contains(inputEl).click(); // Ensure that the input has the default font size value. expect(inputEl).toHaveValue("14"); expect(".o_font_size_selector_menu").toHaveCount(1); // Ensure that the selection is still present in the editable. expect(getContent(el)).toBe(`[test]
`); expect(getActiveElement()).toBe(inputEl); await press("8"); expect(inputEl).toHaveValue("8"); await advanceTime(200); expect(".o_font_size_selector_menu").toHaveCount(1); expect(getContent(el)).toBe(`[test]
`); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar works: font size dropdown closes on Enter and Tab key press", async () => { await setupEditor("[test]
"); await waitFor(".o-we-toolbar"); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); expect(iframeEl).toHaveCount(1); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); await contains(inputEl).click(); expect(".o_font_size_selector_menu").toHaveCount(1); await press("Enter"); await animationFrame(); expect(".o_font_size_selector_menu").toHaveCount(0); await contains(inputEl).click(); expect(".o_font_size_selector_menu").toHaveCount(1); await press("Tab"); await animationFrame(); expect(".o_font_size_selector_menu").toHaveCount(0); }); test("toolbar works: ArrowUp/Down moves focus to font size dropdown", async () => { await setupEditor("[test]
"); await waitFor(".o-we-toolbar"); const iframeEl = queryOne(".o-we-toolbar [name='font-size'] iframe"); expect(iframeEl).toHaveCount(1); const inputEl = iframeEl.contentWindow.document?.querySelector("input"); await contains(inputEl).click(); expect(".o_font_size_selector_menu").toHaveCount(1); expect(getActiveElement()).toBe(inputEl); const fontSizeSelectorMenu = queryOne(".o_font_size_selector_menu"); await press("ArrowDown"); await animationFrame(); expect(".o_font_size_selector_menu").toHaveCount(1); expect(getActiveElement()).toBe(fontSizeSelectorMenu.firstElementChild); await contains(inputEl).click(); expect(".o_font_size_selector_menu").toHaveCount(1); await press("ArrowUp"); await animationFrame(); expect(".o_font_size_selector_menu").toHaveCount(1); expect(getActiveElement()).toBe(fontSizeSelectorMenu.lastElementChild); }); test.tags("desktop"); test("toolbar should not open on keypress tab inside table", async () => { const contentBefore = unformat(`[]ab |
cd |
ab |
cd[] |
[] |
|
|
abcdefghijklmno abcdefghijklmnopqrs abcdefg |
|
to end of last
. setSelection({ anchorNode: firstP.firstChild, anchorOffset: 0, focusNode: lastP.firstChild, focusOffset: nodeSize(lastP.firstChild), }); await animationFrame(); // Get bounding rect of selection range. const range = document.createRange(); range.setStart(lastP.firstChild, 0); range.setEnd(lastP.firstChild, nodeSize(lastP.firstChild)); const rect = range.getBoundingClientRect(); // Simulate mousemove and mouseup events to complete the selection. manuallyDispatchProgrammaticEvent(lastP, "mousemove", { clientX: rect.right, clientY: rect.top, }); manuallyDispatchProgrammaticEvent(lastP, "mousemove", { clientX: rect.right + 5, clientY: rect.top, }); manuallyDispatchProgrammaticEvent(lastP, "mouseup", { clientX: rect.right + 5, clientY: rect.top, }); await animationFrame(); await tick(); expect(firstTd).toHaveClass("o_selected_td"); await expectElementCount(".o-we-toolbar", 1); }); test.tags("desktop"); test("toolbar should close on keypress tab inside table", async () => { const contentBefore = unformat(`
[ab] |
cd |
ab |
cd[] |
test
"); await expectElementCount(".o-we-toolbar", 0); setContent(el, "[test]
"); await expectElementCount(".o-we-toolbar", 1); const selection = document.getSelection(); selection.removeAllRanges(); setContent(el, "abc
"); await expectElementCount(".o-we-toolbar", 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) => !!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("abc
[abc]
[Foo]
[abc]
`); await waitFor(".o-we-toolbar"); expect(".o-we-toolbar").toHaveCount(1); expect(queryAll(".o-we-toolbar .btn-group[name='font']").length).toBe(1); expect(queryAll(".o-we-toolbar .btn-group[name='decoration']").length).toBe(1); setContent( el, `[
]
[Foo]
[test]
"); 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("[abc]
"); // 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("keep the toolbar if the selection crosses two blocks, even if their contents aren't selected", async () => { const { el } = await setupEditor("a
b
"); await expectElementCount(".o-we-toolbar", 0); setContent(el, "[a
]b
"); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 1); // This selection is possible when you double-click at the end of a line. setContent(el, "a[
]b
"); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 1); }); test.tags("desktop"); test("keep the toolbar if the selection crosses two blocks, even if their contents aren't selected (ignore whitespace)", async () => { const { el } = await setupEditor("a
\nb
"); await expectElementCount(".o-we-toolbar", 0); setContent(el, "[a
\n]b
"); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 1); // This selection is possible when you double-click at the end of a line. setContent(el, "a[
\n]b
"); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 1); }); test.tags("desktop"); test("close the toolbar if the selection contains any nodes (traverseNode = [], ignore zws)", async () => { const { el } = await setupEditor(`ab${strong("\u200B", "first")}cd
`); await expectElementCount(".o-we-toolbar", 0); setContent(el, `a[b${strong("\u200B", "first")}c]d
`); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 1); setContent(el, `ab${strong("[\u200B]", "first")}cd
`); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 0); }); test.tags("desktop"); test("should be able to close image cropper while loading the media", async () => { onRpc("/html_editor/get_image_info", () => ({ original: { image_src: "#", }, })); onRpc("/web/image/__odoo__unknown__src__/", async () => { await delay(50); return {}; }); await setupEditor(`[]
test
"); await expectElementCount(".o-we-toolbar", 0); await pointerDown(el); //[]test
setSelection({ anchorNode: el.children[0], anchorOffset: 0 }); await tick(); // selectionChange // Simulate extending the selection with mousedown //[test]
setSelection({ anchorNode: el.children[0], anchorOffset: 0, focusOffset: 1 }); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 0); await pointerUp(el); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should open on mouseup after selecting text (even if mouseup happens outside the editable)", async () => { const { el } = await setupEditor("test
"); await expectElementCount(".o-we-toolbar", 0); await pointerDown(el); //[]test
setSelection({ anchorNode: el.children[0], anchorOffset: 0 }); await tick(); // selectionChange // Simulate extending the selection with mousedown //[test]
setSelection({ anchorNode: el.children[0], anchorOffset: 0, focusOffset: 1 }); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 0); await pointerUp(el.ownerDocument); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should close on mousedown", async () => { const { el } = await setupEditor("[test]
text
"); await waitFor(".o-we-toolbar"); await pointerDown(el); //test
[]text
setSelection({ anchorNode: el.children[1], anchorOffset: 0 }); await tick(); // selectionChange await expectElementCount(".o-we-toolbar", 0); await pointerUp(el); await tick(); expect(getContent(el)).toBe("test
[]text
"); await animationFrame(); await expectElementCount(".o-we-toolbar", 0); }); test("toolbar should close on mousedown (2)", async () => { const { el } = await setupEditor("[test]
"); /** @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 expectElementCount(".o-we-toolbar", 0); await pointerUp(el); setContent(el, "[]test
"); await tick(); await animationFrame(); await expectElementCount(".o-we-toolbar", 0); }); test("toolbar should open on double click", async () => { const { el } = await setupEditor("test
"); const p = el.firstElementChild; await simulateDoubleClickSelect(p); expect(getContent(el)).toBe("[test]
"); // toolbar open after double click is debounced await advanceTime(500); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should open on triple click", async () => { const { el } = await setupEditor("test text
"); const p = el.firstElementChild; await simulateTripleClickSelect(p); expect(getContent(el)).toBe("[test text]
"); // toolbar open after triple click is debounced await advanceTime(500); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should not open between double and triple click", async () => { const { el } = await setupEditor("test text
"); const p = el.firstElementChild; // Double click await firstClick(p); await secondClick(p); expect(getContent(el)).toBe("[test] text
"); await advanceTime(100); // Toolbar is not open yet, waiting for a possible third click await expectElementCount(".o-we-toolbar", 0); // Third click await thirdClick(p); expect(getContent(el)).toBe("[test text]
"); await advanceTime(500); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should not open after triple click while mouse is down", async () => { const { el } = await setupEditor("test text
"); const p = el.firstElementChild; await simulateDoubleClickSelect(p); await pointerDown(p); manuallyDispatchProgrammaticEvent(p, "mousedown", { detail: 3 }); setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 }); await tick(); // selectionChange expect(getContent(el)).toBe("[test text]
"); await advanceTime(500); // Toolbar is not open yet, waiting for mouseup await expectElementCount(".o-we-toolbar", 0); // Mouse up manuallyDispatchProgrammaticEvent(p, "mouseup", { detail: 3 }); manuallyDispatchProgrammaticEvent(p, "click", { detail: 3 }); await advanceTime(500); await expectElementCount(".o-we-toolbar", 1); }); }); describe("keyboard", () => { test("toolbar should not open on keydown Arrow (only after keyup)", async () => { const { el } = await setupEditor("[]test
"); await expectElementCount(".o-we-toolbar", 0); await keyDown(["Shift", "ArrowRight"]); setContent(el, "[t]est
"); await tick(); // selectionChange await animationFrame(); await expectElementCount(".o-we-toolbar", 0); await keyUp(["Shift", "ArrowRight"]); await advanceTime(500); // Toolbar open on keyup is debounced await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should close on keydown Arrow", async () => { const { el } = await setupEditor("[tes]t
"); await waitFor(".o-we-toolbar"); // Toolbar should close on keydown await keyDown(["Shift", "ArrowRight"]); setContent(el, "[test]
"); await tick(); // selectionChange await waitForNone(".o-we-toolbar"); await expectElementCount(".o-we-toolbar", 0); // Toolbar should open after keyup await keyUp(["Shift", "ArrowRight"]); await advanceTime(500); // toolbar open on keyup is debounced await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should not close on keydown shift or control", async () => { await setupEditor("[tes]t
"); await waitFor(".o-we-toolbar"); // Toolbar should not close on keydown shift await keyDown(["Shift"]); await tick(); await expectElementCount(".o-we-toolbar", 1); await keyUp(["Shift"]); await tick(); await expectElementCount(".o-we-toolbar", 1); // Toolbar should not close on keydown ctrl await keyDown(["Control"]); await tick(); await expectElementCount(".o-we-toolbar", 1); await keyUp(["Control"]); await tick(); await expectElementCount(".o-we-toolbar", 1); }); test("toolbar should not open between keystrokes separated by a short interval", async () => { const { el } = await setupEditor("[]test
"); await expectElementCount(".o-we-toolbar", 0); // Keystroke # 1 await keyDown(["Shift", "ArrowRight"]); setContent(el, "[t]est
"); await tick(); // selectionChange await keyUp(["Shift", "ArrowRight"]); await advanceTime(100); await expectElementCount(".o-we-toolbar", 0); // Keystroke # 2 await keyDown(["Shift", "ArrowRight"]); setContent(el, "[te]st
"); await tick(); // selectionChange await keyUp(["Shift", "ArrowRight"]); await advanceTime(100); await expectElementCount(".o-we-toolbar", 0); // Toolbar opens some time after the last keyup await advanceTime(500); await expectElementCount(".o-we-toolbar", 1); }); }); }); test.tags("desktop"); test("dropdown menu should not overflow scroll container", async () => { class Test extends models.Model { name = fields.Char(); txt = fields.Html(); _records = [{ id: 1, name: "Test", txt: "text
".repeat(50) }]; } defineModels([Test]); await mountView({ type: "form", resId: 1, resModel: "test", arch: ` `, }); const top = (rangeOrElement) => rangeOrElement.getBoundingClientRect().top; const bottom = (elementOrRange) => elementOrRange.getBoundingClientRect().bottom; const scrollableElement = queryOne(".o_content"); const editable = queryOne(".odoo-editor-editable"); // Select a paragraph in the middle of the text const fifthParagraph = editable.children[5]; setSelection({ anchorNode: fifthParagraph, anchorOffset: 0, focusNode: fifthParagraph, focusOffset: 1, }); const range = document.getSelection().getRangeAt(0); const toolbar = await waitFor(".o-we-toolbar"); // Toolbar should be above the selection expect(bottom(toolbar)).toBeLessThan(top(range)); // Color selector await contains(".o-we-toolbar .o-select-color-foreground").click(); await expectElementCount(".o_font_color_selector", 1); const colorSelector = queryOne(".o_font_color_selector"); // Scroll down to bring the toolbar close to the top let scrollStep = top(toolbar) - top(scrollableElement); scrollableElement.scrollTop += scrollStep; await animationFrame(); // Toolbar should be below the selection expect(top(toolbar)).toBeGreaterThan(bottom(range)); // Scroll down to make the toolbar overflow the scroll container scrollStep = top(toolbar) - top(scrollableElement); scrollableElement.scrollTop += scrollStep; await animationFrame(); // Toolbar should be invisible expect(toolbar).not.toBeVisible(); // Color selector should be invisible expect(colorSelector).not.toBeVisible(); // Scroll up to make toolbar visible scrollableElement.scrollTop = 0; await animationFrame(); expect(toolbar).toBeVisible(); // Color selector should be visible along with toolbar expect(colorSelector).toBeVisible(); // Font selector await contains(".o-we-toolbar [name='font'] .dropdown-toggle").click(); await expectElementCount(".o_font_selector_menu", 1); const fontSelector = queryOne(".o_font_selector_menu"); // Scroll down again to bring the toolbar close to the top scrollStep = top(toolbar) - top(scrollableElement); scrollableElement.scrollTop += scrollStep; await animationFrame(); // Toolbar should be below the selection expect(top(toolbar)).toBeGreaterThan(bottom(range)); // Scroll down to make the toolbar overflow the scroll container scrollStep = top(toolbar) - top(scrollableElement); scrollableElement.scrollTop += scrollStep; await animationFrame(); // Toolbar should be invisible expect(toolbar).not.toBeVisible(); // Font selector should be invisible expect(fontSelector).not.toBeVisible(); // Scroll up to make toolbar visible scrollableElement.scrollTop -= scrollStep; await animationFrame(); expect(toolbar).toBeVisible(); // Font selector should be visible expect(fontSelector).toBeVisible(); });