replace stale web_editor with html_editor and html_builder for 19.0

web_editor was removed in Odoo 19.0 and replaced by html_editor
and html_builder. The old web_editor was incorrectly included in
the 19.0 vanilla import.

🤖 assisted by claude
This commit is contained in:
Ernad Husremovic 2026-03-09 15:31:13 +01:00
parent 4b94f0abc5
commit f866779561
1513 changed files with 396049 additions and 358525 deletions

View file

@ -0,0 +1,462 @@
import { beforeEach, expect, test, describe } from "@odoo/hoot";
import { setSelection } from "./_helpers/selection";
import { click, hover, queryOne, waitFor, waitForNone } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fields,
models,
mountView,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { animationFrame } from "@odoo/hoot-mock";
import { unformat } from "./_helpers/format";
import { Plugin } from "@html_editor/plugin";
import { Component, onMounted, onWillUnmount, xml } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { setupEditor } from "./_helpers/editor";
import { MAIN_PLUGINS } from "@html_editor/plugin_sets";
import { parseHTML } from "@html_editor/utils/html";
import { closestScrollableY } from "@web/core/utils/scrolling";
import { Wysiwyg } from "@html_editor/wysiwyg";
import { insertText } from "./_helpers/user_actions";
class Test extends models.Model {
name = fields.Char();
txt = fields.Html();
_records = [
{ id: 1, name: "Test", txt: "<p>text</p>".repeat(50) },
{
id: 2,
name: "Test",
txt: unformat(`
<table><tbody>
<tr>
<td><p>cell 0</p></td>
<td><p>cell 1</p></td>
</tr>
</tbody></table>
${"<p>text</p>".repeat(50)}`),
},
{ id: 3, name: "Test", txt: "<p>text</p>" },
];
}
defineModels([Test]);
test.tags("desktop");
test("Toolbar should not overflow scroll container", async () => {
const top = (elementOrRange) => elementOrRange.getBoundingClientRect().top;
const bottom = (elementOrRange) => elementOrRange.getBoundingClientRect().bottom;
await mountView({
type: "form",
resId: 1,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html"/>
</form>`,
});
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));
// 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));
// Toolbar should not overflow the scroll container
expect(top(toolbar)).toBeGreaterThan(top(scrollableElement));
// 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();
// Scroll up to make the toolbar visible again
scrollableElement.scrollTop -= scrollStep;
await animationFrame();
expect(toolbar).toBeVisible();
});
test.tags("desktop");
test("Toolbar should be visible after scroll bar is added", async () => {
await mountView({
type: "form",
resId: 3,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html" options="{'height': 300}"/>
</form>`,
});
// At this point there's no scroll bar around the editable
const p = queryOne(".odoo-editor-editable p");
// Add text: this creates a vertical scroll bar in the editable
const morePs = parseHTML(document, "<p>more text</p>".repeat(20));
p.after(...morePs.childNodes);
// Select first paragraph
setSelection({ anchorNode: p, anchorOffset: 0, focusNode: p, focusOffset: 1 });
// Toolbar should be visible
const toolbar = await waitFor(".o-we-toolbar");
expect(toolbar).toBeVisible();
});
test.tags("desktop");
test("Toolbar should not overflow scroll container at the bottom", async () => {
await mountView({
type: "form",
resId: 1,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html" options="{'height': 300}"/>
</form>`,
});
const lastP = queryOne(".odoo-editor-editable p:last-child");
// Scroll down to bottom
lastP.scrollIntoView();
// Select last paragraph
setSelection({ anchorNode: lastP, anchorOffset: 0, focusNode: lastP, focusOffset: 1 });
// Toolbar should be visible
const toolbar = await waitFor(".o-we-toolbar");
expect(toolbar).toBeVisible();
// Scroll up so that toolbar overflows the bottom of the editable
const scrollableElement = closestScrollableY(lastP);
scrollableElement.scrollTop -= 100;
// Toolbar should be hidden
await waitFor(".o-we-toolbar:not(:visible)");
expect(toolbar).not.toBeVisible();
});
test.tags("desktop");
test("Toolbar visibility should be updated when editable is resized", async () => {
await mountView({
type: "form",
resId: 1,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html" options="{'height': 300}"/>
</form>`,
});
const lastP = queryOne(".odoo-editor-editable p:last-child");
// Scroll down to bottom
lastP.scrollIntoView();
// Select last paragraph
setSelection({ anchorNode: lastP, anchorOffset: 0, focusNode: lastP, focusOffset: 1 });
// Toolbar should be visible
const toolbar = await waitFor(".o-we-toolbar");
expect(toolbar).toBeVisible();
// Resize editable (which is the scroll container)
const editable = queryOne(".odoo-editor-editable");
editable.style.height = "150px";
// Toolbar now overflows the bottom of the container and should be hidden
await waitFor(".o-we-toolbar:not(:visible)");
expect(toolbar).not.toBeVisible();
});
describe("powerbox", () => {
let editor;
beforeEach(() =>
patchWithCleanup(Wysiwyg.prototype, {
setup() {
super.setup();
editor = this.editor;
},
})
);
test.tags("desktop");
test("Powerbox should be visible in a editable with small height", async () => {
await mountView({
type: "form",
resId: 3,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html" options="{'height': 100}"/>
</form>`,
});
// Put cursor at end of first paragraph an insert "/"
setSelection({ anchorNode: queryOne(".odoo-editor-editable p"), anchorOffset: 1 });
insertText(editor, "/");
// Powerbox should be visible
const powerbox = await waitFor(".o-we-powerbox");
expect(powerbox).toBeVisible();
});
test.tags("desktop");
test("Powerbox should be visible in a editable with small height (2)", async () => {
await mountView({
type: "form",
resId: 1,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html" options="{'height': 100}"/>
</form>`,
});
// Put cursor at end of third paragraph an insert "/"
const thirdP = queryOne(".odoo-editor-editable p:nth-child(3)");
setSelection({ anchorNode: thirdP, anchorOffset: 1 });
insertText(editor, "/");
// Powerbox should be visible
const powerbox = await waitFor(".o-we-powerbox");
expect(powerbox).toBeVisible();
});
});
test.tags("desktop");
test("Table column control should always be displayed on top of the table", async () => {
const top = (el) => el.getBoundingClientRect().top;
const bottom = (el) => el.getBoundingClientRect().bottom;
await mountView({
type: "form",
resId: 2,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html"/>
</form>`,
});
const scrollableElement = queryOne(".o_content");
const table = queryOne(".odoo-editor-editable table");
await hover(".odoo-editor-editable td");
let columnControl = await waitFor(".o-we-table-menu[data-type='column']");
// Table column control displayed on hover should be above the table
expect(bottom(columnControl)).toBeLessThan(top(table));
// Scroll down so that the table is close to the top
const distanceToTop = top(table) - top(scrollableElement);
scrollableElement.scrollTop += distanceToTop;
await animationFrame();
await hover(".odoo-editor-editable td");
columnControl = await waitFor(".o-we-table-menu[data-type='column']");
// Table column control still above the table,
// even though the table is close to the top
// of its container, but it should be hidden
expect(bottom(columnControl)).toBeLessThan(top(table));
expect(columnControl).not.toBeVisible();
});
test.tags("desktop");
test("Table menu should close on scroll", async () => {
await mountView({
type: "form",
resId: 2,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html"/>
</form>`,
});
const scrollableElement = queryOne(".o_content");
await hover(".odoo-editor-editable td");
const columnControl = await waitFor(".o-we-table-menu[data-type='column']");
await click(columnControl);
await animationFrame();
// Column menu should be displayed.
expect(".o-dropdown--menu").toBeVisible();
// Scroll down
scrollableElement.scrollTop += 10;
await waitForNone(".o-dropdown--menu");
// Column menu should not be visible.
expect(".o-dropdown--menu").not.toHaveCount();
});
test.tags("desktop");
test("Table menu should only show on contenteditable true tables", async () => {
await mountView({
type: "form",
resId: 2,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html"/>
</form>`,
});
// check that table menu is visible
await hover(".odoo-editor-editable td");
await waitFor(".o-we-table-menu[data-type='column']");
expect(".o-we-table-menu[data-type='column']").toBeVisible();
// hover away set the table as not editable
await hover(".o_control_panel");
queryOne("table").setAttribute("contenteditable", "false");
// chack that table menu is now not visible
await hover(".odoo-editor-editable td");
await waitForNone(".o-we-table-menu[data-type='column']");
expect(".o-we-table-menu[data-type='column']").not.toHaveCount();
});
test("Toolbar should keep stable while extending down the selection", async () => {
const top = (el) => el.getBoundingClientRect().top;
const left = (el) => el.getBoundingClientRect().left;
await mountView({
type: "form",
resId: 1,
resModel: "test",
arch: `
<form>
<field name="name"/>
<field name="txt" widget="html"/>
</form>`,
});
const editable = queryOne(".odoo-editor-editable");
// Select inner content of a paragraph in the middle of the text
const fifthParagraph = editable.children[5];
const textNode = fifthParagraph.firstChild;
setSelection({
anchorNode: textNode,
anchorOffset: 0,
focusNode: textNode,
focusOffset: textNode.length,
});
const toolbar = await waitFor(".o-we-toolbar");
const referenceTop = top(toolbar);
const referenceLeft = left(toolbar);
const extendSelection = (focusNode, focusOffset) => {
setSelection({ anchorNode: textNode, anchorOffset: 0, focusNode, focusOffset });
};
// Extend the selection to the beginning of the following paragraph. This
// simulates the selection obtained by moving the mouse while mousedown.
const sixthParagraph = fifthParagraph.nextElementSibling;
extendSelection(sixthParagraph, 0);
await animationFrame();
// Toolbar should not move
expect(top(toolbar)).toBe(referenceTop);
expect(left(toolbar)).toBe(referenceLeft);
// Extend selection to end of paragraph
const textNodeSixthParagraph = sixthParagraph.firstChild;
extendSelection(textNodeSixthParagraph, textNodeSixthParagraph.length);
await animationFrame();
// Toolbar should not move
expect(top(toolbar)).toBe(referenceTop);
expect(left(toolbar)).toBe(referenceLeft);
});
test("overlay don't close when click on child overlay", async () => {
class MySubOverlay extends Component {
static template = xml`<button class="my-suboverlay">Overlay</button>`;
static props = {};
}
class MyOverlay extends Component {
static template = xml`<div class="my-overlay">Overlay</div>`;
static props = {};
setup() {
const overlayService = useService("overlay");
let remove;
onMounted(() => {
remove = overlayService.add(MySubOverlay, {});
});
onWillUnmount(() => remove?.());
}
}
class MyPlugin extends Plugin {
static id = "my.plugin";
static dependencies = ["overlay"];
setup() {
this.overlay = this.dependencies.overlay.createOverlay(MyOverlay, {});
this.overlay.open({ target: this.editable });
}
destroy() {
this.overlay.close();
}
}
const { editor } = await setupEditor("<div>edit</div>", {
config: { Plugins: [...MAIN_PLUGINS, MyPlugin] },
});
await waitFor(".my-overlay");
await contains(".my-suboverlay").click();
await animationFrame();
expect(document.activeElement).toBe(queryOne(".my-suboverlay"));
expect(".my-overlay").toHaveCount(1);
editor.destroy();
await animationFrame();
await setupEditor("<div>edit</div>", {
config: { Plugins: [...MAIN_PLUGINS, MyPlugin] },
props: {
iframe: true,
},
});
await waitFor(".my-overlay");
await contains(".my-suboverlay").click();
await animationFrame();
expect(document.activeElement).toBe(queryOne(".my-suboverlay"));
expect(".my-overlay").toHaveCount(1);
});