1236 lines
46 KiB
JavaScript
1236 lines
46 KiB
JavaScript
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("<p>test</p>");
|
|
|
|
// set a non-collapsed selection to open toolbar
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
setContent(el, "<p>[test]</p>");
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
|
|
// set a collapsed selection to close toolbar
|
|
setContent(el, "<p>test[]</p>");
|
|
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("<p>test</p>");
|
|
|
|
// set a non-collapsed selection to open toolbar
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
setContent(el, "<p>[test]</p>");
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
|
|
setContent(el, "<p>test[]</p>");
|
|
await animationFrame();
|
|
await expectElementCount(".o-we-toolbar", 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 expectElementCount(".o-we-toolbar", 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
|
|
await expectElementCount(".o-we-toolbar", 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
|
|
await expectElementCount(".o-we-toolbar", 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 format buttons should react to format change", async () => {
|
|
await setupEditor(
|
|
`<div class="o-paragraph">[\ufeff<a href="http://test.com">\ufefftest.com\ufeff</a>\ufeff ]</div>`
|
|
);
|
|
await waitFor(".o-we-toolbar");
|
|
expect(".btn[name='bold']").not.toHaveClass("active");
|
|
await contains(".btn[name='bold']").click();
|
|
await animationFrame();
|
|
expect(".btn[name='bold']").toHaveClass("active");
|
|
});
|
|
|
|
test("toolbar disable link button when selection cross blocks", async () => {
|
|
await setupEditor("<div>[<div>a<p>b</p></div>]</div>");
|
|
await waitFor(".o-we-toolbar");
|
|
expect(".btn[name='link']").toHaveClass("disabled");
|
|
});
|
|
|
|
test("toolbar enable link button when selection has only link", async () => {
|
|
await setupEditor(`<p>[<a href="test.com">test.com</a>]</p>`);
|
|
|
|
await waitFor(".o-we-toolbar");
|
|
expect(".btn[name='link']").not.toHaveClass("disabled");
|
|
});
|
|
|
|
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
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
setContent(el, "<p>[test]</p>");
|
|
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("<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");
|
|
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("<p>[test]</p>");
|
|
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("<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("Paragraph");
|
|
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
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
setContent(el, "<p>[test]</p>");
|
|
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(`<p><span class="h1-fs">[test]</span></p>`);
|
|
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(`<p><span class="o_small-fs">[test]</span></p>`);
|
|
expect(inputEl).toHaveValue(oSmallSize);
|
|
});
|
|
|
|
test("should focus the editable area after selecting a font size item", async () => {
|
|
const { editor, el } = await setupEditor("<p>[test]</p>");
|
|
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(`<p><span class="h2-fs">[test]</span></p>`);
|
|
});
|
|
|
|
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
|
|
await expectElementCount(".o-we-toolbar", 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");
|
|
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(`<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(inputEl).toHaveValue(`${h1Size}`);
|
|
});
|
|
|
|
test("toolbar works: displays correct font size on input", async () => {
|
|
const { el } = await setupEditor("<p>[test]</p>");
|
|
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(`<p>[test]</p>`);
|
|
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(`<p><span style="font-size: 8px;">[test]</span></p>`);
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
});
|
|
|
|
test("toolbar works: font size dropdown closes on Enter and Tab key press", async () => {
|
|
await setupEditor("<p>[test]</p>");
|
|
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("<p>[test]</p>");
|
|
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(`
|
|
<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();
|
|
await expectElementCount(".o-we-toolbar", 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");
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
});
|
|
|
|
test("should select table single cell when entire content is selected via mouse movement", async () => {
|
|
const content = unformat(`
|
|
<table class="table table-bordered o_table" style="width: 250px;">
|
|
<tbody>
|
|
<tr>
|
|
<td style="width: 200px;">
|
|
<p>abcdefghijklmno</p>
|
|
<p>abcdefghijklmnopqrs</p>
|
|
<p>abcdefg</p>
|
|
</td>
|
|
<td style="width: 50px;"><p><br></p></td>
|
|
</tr>
|
|
<tr>
|
|
<td><p><br></p></td>
|
|
<td><p><br></p></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`);
|
|
|
|
const { el } = await setupEditor(content);
|
|
|
|
const firstTd = el.querySelector("td");
|
|
const firstP = firstTd.firstChild;
|
|
const lastP = firstTd.lastChild;
|
|
|
|
// Simulate mousedown at the top of the first paragraph.
|
|
const rectStart = firstP.getBoundingClientRect();
|
|
manuallyDispatchProgrammaticEvent(firstP, "mousedown", {
|
|
clientX: rectStart.left,
|
|
clientY: rectStart.top,
|
|
});
|
|
|
|
// Set selection from start of first <p> to end of last <p>.
|
|
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(`
|
|
<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 expectElementCount(".o-we-toolbar", 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>");
|
|
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
setContent(el, "<p>[test]</p>");
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
|
|
const selection = document.getSelection();
|
|
selection.removeAllRanges();
|
|
|
|
setContent(el, "<p>abc</p>");
|
|
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("<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("toolbar should open with image namespace the selection spans an image and whitespace", async () => {
|
|
const { el } = await setupEditor(`<p>[abc]</p>`);
|
|
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,
|
|
`<p>[
|
|
<img>
|
|
]</p>`
|
|
);
|
|
await animationFrame();
|
|
await waitFor(".o-we-toolbar");
|
|
expect(queryAll(".o-we-toolbar .btn-group[name='font']").length).toBe(0);
|
|
expect(queryAll(".o-we-toolbar .btn-group[name='decoration']").length).toBe(0);
|
|
});
|
|
|
|
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("keep the toolbar if the selection crosses two blocks, even if their contents aren't selected", async () => {
|
|
const { el } = await setupEditor("<p>a</p><p>b</p>");
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
setContent(el, "<p>[a</p><p>]b</p>");
|
|
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, "<p>a[</p><p>]b</p>");
|
|
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("<p>a</p>\n<p>b</p>");
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
setContent(el, "<p>[a</p>\n<p>]b</p>");
|
|
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, "<p>a[</p>\n<p>]b</p>");
|
|
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(`<p>ab${strong("\u200B", "first")}cd</p>`);
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
setContent(el, `<p>a[b${strong("\u200B", "first")}c]d</p>`);
|
|
await tick(); // selectionChange
|
|
await animationFrame();
|
|
await expectElementCount(".o-we-toolbar", 1);
|
|
|
|
setContent(el, `<p>ab${strong("[\u200B]", "first")}cd</p>`);
|
|
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(`<p>[<img src="#">]</p>`);
|
|
await waitFor(".o-we-toolbar");
|
|
|
|
await click('div[name="image_transform"] > .btn');
|
|
await animationFrame();
|
|
|
|
await click('.btn[name="image_crop"]');
|
|
await animationFrame();
|
|
|
|
await waitFor('.btn[title="Discard"]', { timeout: 1000 });
|
|
await click('.btn[title="Discard"]');
|
|
await animationFrame();
|
|
|
|
// Cropper should get closed while the cropper still loading the image.
|
|
expect('.btn[title="Discard"]').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>");
|
|
await expectElementCount(".o-we-toolbar", 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();
|
|
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("<p>test</p>");
|
|
await expectElementCount(".o-we-toolbar", 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();
|
|
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("<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 expectElementCount(".o-we-toolbar", 0);
|
|
|
|
await pointerUp(el);
|
|
await tick();
|
|
expect(getContent(el)).toBe("<p>test</p><p>[]text</p>");
|
|
await animationFrame();
|
|
await expectElementCount(".o-we-toolbar", 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 expectElementCount(".o-we-toolbar", 0);
|
|
|
|
await pointerUp(el);
|
|
setContent(el, "<p>[]test</p>");
|
|
await tick();
|
|
await animationFrame();
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
});
|
|
|
|
test("toolbar should open on double click", async () => {
|
|
const { el } = await setupEditor("<p>test</p>");
|
|
const p = el.firstElementChild;
|
|
|
|
await simulateDoubleClickSelect(p);
|
|
expect(getContent(el)).toBe("<p>[test]</p>");
|
|
// 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("<p>test text</p>");
|
|
const p = el.firstElementChild;
|
|
|
|
await simulateTripleClickSelect(p);
|
|
expect(getContent(el)).toBe("<p>[test text]</p>");
|
|
// 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("<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
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
// Third click
|
|
await thirdClick(p);
|
|
expect(getContent(el)).toBe("<p>[test text]</p>");
|
|
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("<p>test text</p>");
|
|
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("<p>[test text]</p>");
|
|
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("<p>[]test</p>");
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
await keyDown(["Shift", "ArrowRight"]);
|
|
setContent(el, "<p>[t]est</p>");
|
|
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("<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");
|
|
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("<p>[tes]t</p>");
|
|
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("<p>[]test</p>");
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
// Keystroke # 1
|
|
await keyDown(["Shift", "ArrowRight"]);
|
|
setContent(el, "<p>[t]est</p>");
|
|
await tick(); // selectionChange
|
|
await keyUp(["Shift", "ArrowRight"]);
|
|
await advanceTime(100);
|
|
await expectElementCount(".o-we-toolbar", 0);
|
|
|
|
// Keystroke # 2
|
|
await keyDown(["Shift", "ArrowRight"]);
|
|
setContent(el, "<p>[te]st</p>");
|
|
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: "<p>text</p>".repeat(50) }];
|
|
}
|
|
|
|
defineModels([Test]);
|
|
await mountView({
|
|
type: "form",
|
|
resId: 1,
|
|
resModel: "test",
|
|
arch: `
|
|
<form>
|
|
<field name="name"/>
|
|
<field name="txt" widget="html"/>
|
|
</form>`,
|
|
});
|
|
|
|
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();
|
|
});
|