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,283 @@
import { expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { setupEditor } from "../_helpers/editor";
import { getContent } from "../_helpers/selection";
import { insertText } from "../_helpers/user_actions";
import { unformat } from "../_helpers/format";
import { press, waitFor, queryOne } from "@odoo/hoot-dom";
import { expectElementCount } from "../_helpers/ui_expectations";
function expectContentToBe(el, html) {
expect(getContent(el)).toBe(unformat(html));
}
test.tags("desktop");
test("can add a table using the powerbox and keyboard", async () => {
const { el, editor } = await setupEditor("<p>a[]</p>");
await expectElementCount(".o-we-powerbox", 0);
expectContentToBe(el, `<p>a[]</p>`);
// open powerbox
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await expectElementCount(".o-we-tablepicker", 0);
// filter to get table command in first position
await insertText(editor, "table");
await animationFrame();
// press enter to open tablepicker
await press("Enter");
await waitFor(".o-we-tablepicker");
await expectElementCount(".o-we-powerbox", 0);
// press enter to validate current dimension (3x3)
await press("Enter");
await animationFrame();
await expectElementCount(".o-we-powerbox", 0);
await expectElementCount(".o-we-tablepicker", 0);
expectContentToBe(
el,
`<p>a</p>
<table class="table table-bordered o_table">
<tbody>
<tr>
<td><p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
<p><br></p>`
);
});
test.tags("desktop");
test("can close table picker with escape", async () => {
const { el, editor } = await setupEditor("<p>a[]</p>");
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await insertText(editor, "table");
expectContentToBe(el, "<p>a/table[]</p>");
await animationFrame();
await press("Enter");
await expectElementCount(".o-we-tablepicker", 1);
expectContentToBe(el, "<p>a[]</p>");
await press("escape");
await animationFrame();
await expectElementCount(".o-we-tablepicker", 0);
});
test.tags("iframe", "desktop");
test("in iframe, can add a table using the powerbox and keyboard", async () => {
const { el, editor } = await setupEditor("<p>a[]</p>", {
props: { iframe: true },
});
await expectElementCount(".o-we-powerbox", 0);
expect(getContent(el)).toBe(`<p>a[]</p>`);
expect(":iframe .o_table").toHaveCount(0);
// open powerbox
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await expectElementCount(".o-we-tablepicker", 0);
// filter to get table command in first position
await insertText(editor, "table");
await animationFrame();
// press enter to open tablepicker
await press("Enter");
await waitFor(".o-we-tablepicker");
await expectElementCount(".o-we-powerbox", 0);
// press enter to validate current dimension (3x3)
await press("Enter");
await animationFrame();
await expectElementCount(".o-we-powerbox", 0);
await expectElementCount(".o-we-tablepicker", 0);
expect(":iframe .o_table").toHaveCount(1);
});
test.tags("desktop");
test("Expand columns in the correct direction in 'rtl'", async () => {
const { editor } = await setupEditor("<p>a[]</p>", {
config: {
direction: "rtl",
},
});
await insertText(editor, "/table");
await press("Enter");
await waitFor(".o-we-tablepicker");
// Initially we have 3 columns
const tablePickerOverlay = queryOne(".overlay");
expect(tablePickerOverlay).toHaveStyle({ right: /px$/ });
const right = tablePickerOverlay.style.right;
const width3Columns = tablePickerOverlay.getBoundingClientRect().width;
expect(".o-we-cell.active").toHaveCount(9);
// Add one column -> we have 4 columns
await press("ArrowLeft");
await animationFrame();
expect(tablePickerOverlay.getBoundingClientRect().width).toBeGreaterThan(width3Columns);
expect(tablePickerOverlay).toHaveStyle({ right });
expect(".o-we-cell.active").toHaveCount(12);
// Remove one column -> we have 3 columns
await press("ArrowRight");
await animationFrame();
expect(".o-we-cell.active").toHaveCount(9);
expect(tablePickerOverlay).toHaveStyle({ right });
// Remove one column -> we have 2 columns
await press("ArrowRight");
await animationFrame();
expect(tablePickerOverlay.getBoundingClientRect().width).toBeLessThan(width3Columns);
expect(tablePickerOverlay).toHaveStyle({ right });
expect(".o-we-cell.active").toHaveCount(6);
});
test.tags("desktop");
test("add table inside empty list", async () => {
const { el, editor } = await setupEditor("<ul><li>[]<br></li></ul>");
// open powerbox
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await expectElementCount(".o-we-tablepicker", 0);
// filter to get table command in first position
await insertText(editor, "table");
await animationFrame();
// press enter to open tablepicker
await press("Enter");
await waitFor(".o-we-tablepicker");
await expectElementCount(".o-we-powerbox", 0);
// press enter to validate current dimension (3x3)
await press("Enter");
await animationFrame();
await expectElementCount(".o-we-powerbox", 0);
await expectElementCount(".o-we-tablepicker", 0);
expectContentToBe(
el,
`<ul>
<li>
<br>
<table class="table table-bordered o_table">
<tbody>
<tr>
<td><p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>`
);
});
test.tags("desktop");
test("add table inside non-empty list", async () => {
const { el, editor } = await setupEditor("<ul><li>abc[]</li></ul>");
// open powerbox
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await expectElementCount(".o-we-tablepicker", 0);
// filter to get table command in first position
await insertText(editor, "table");
await animationFrame();
// press enter to open tablepicker
await press("Enter");
await waitFor(".o-we-tablepicker");
await expectElementCount(".o-we-powerbox", 0);
// press enter to validate current dimension (3x3)
await press("Enter");
await animationFrame();
await expectElementCount(".o-we-powerbox", 0);
await expectElementCount(".o-we-tablepicker", 0);
expectContentToBe(
el,
`<ul>
<li>
abc
<table class="table table-bordered o_table">
<tbody>
<tr>
<td><p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>`
);
});
test.tags("desktop");
test("should close the table picker when any key except arrow keys pressed", async () => {
const { el, editor } = await setupEditor("<p>a[]</p>");
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await insertText(editor, "table");
expectContentToBe(el, "<p>a/table[]</p>");
await animationFrame();
await press("Enter");
await expectElementCount(".o-we-tablepicker", 1);
expectContentToBe(el, "<p>a[]</p>");
await insertText(editor, "b");
await animationFrame();
await expectElementCount(".o-we-tablepicker", 0);
expectContentToBe(el, "<p>ab[]</p>");
await insertText(editor, "/");
await waitFor(".o-we-powerbox");
await insertText(editor, "table");
expectContentToBe(el, "<p>ab/table[]</p>");
await animationFrame();
await press("Enter");
await expectElementCount(".o-we-tablepicker", 1);
expectContentToBe(el, "<p>ab[]</p>");
await insertText(editor, "/");
await animationFrame();
await expectElementCount(".o-we-tablepicker", 0);
});

View file

@ -0,0 +1,756 @@
import { findInSelection } from "@html_editor/utils/selection";
import { describe, expect, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { setupEditor, testEditor } from "../_helpers/editor";
import { unformat } from "../_helpers/format";
import { undo } from "../_helpers/user_actions";
import { getContent } from "../_helpers/selection";
function addRow(position) {
return (editor) => {
const selection = editor.shared.selection.getEditableSelection();
editor.shared.table.addRow(position, findInSelection(selection, "tr"));
};
}
function addColumn(position) {
return (editor) => {
const selection = editor.shared.selection.getEditableSelection();
editor.shared.table.addColumn(position, findInSelection(selection, "td, th"));
};
}
function turnIntoHeader(row) {
return (editor) => {
if (!row) {
const selection = editor.shared.selection.getEditableSelection();
row = findInSelection(selection, "tr");
}
editor.shared.table.turnIntoHeader(row);
};
}
function turnIntoRow(row) {
return (editor) => {
if (!row) {
const selection = editor.shared.selection.getEditableSelection();
row = findInSelection(selection, "tr");
}
editor.shared.table.turnIntoRow(row);
};
}
function moveRow(position, row) {
return (editor) => {
if (!row) {
const selection = editor.shared.selection.getEditableSelection();
row = findInSelection(selection, "tr");
}
editor.shared.table.moveRow(position, row);
};
}
function removeRow(row) {
return (editor) => {
if (!row) {
const selection = editor.shared.selection.getEditableSelection();
row = findInSelection(selection, "tr");
}
editor.shared.table.removeRow(row);
};
}
function removeColumn(cell) {
return (editor) => {
if (!cell) {
const selection = editor.shared.selection.getEditableSelection();
cell = findInSelection(selection, "td");
}
editor.shared.table.removeColumn(cell);
};
}
describe("row", () => {
describe("convert", () => {
test("should convert the first row to table header", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<td style="width: 20px;">ab[]</td>
<td style="width: 25px;">cd</td>
<td style="width: 30px;">ef</td>
</tr>
<tr style="height: 30px;">
<td>ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: turnIntoHeader(),
contentAfter: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<th class="o_table_header" style="width: 20px;">ab[]</th>
<th class="o_table_header" style="width: 25px;">cd</th>
<th class="o_table_header" style="width: 30px;">ef</th>
</tr>
<tr style="height: 30px;">
<td>ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should convert table header to normal row", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<td style="width: 20px;">ab[]</td>
<td style="width: 25px;">cd</td>
<td style="width: 30px;">ef</td>
</tr>
<tr style="height: 30px;">
<td>ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: turnIntoRow(),
contentAfter: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<td style="width: 20px;">ab[]</td>
<td style="width: 25px;">cd</td>
<td style="width: 30px;">ef</td>
</tr>
<tr style="height: 30px;">
<td>ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
});
describe("above", () => {
test("should add a row above the top row", async () => {
await testEditor({
contentBefore:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef[]</td>' +
"</tr></tbody></table>",
stepFunction: addRow("before"),
contentAfter:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;"><p><br></p></td>' +
'<td style="width: 25px;"><p><br></p></td>' +
'<td style="width: 30px;"><p><br></p></td>' +
"</tr>" +
'<tr style="height: 20px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef[]</td>" +
"</tr></tbody></table>",
});
});
test("should add a row above the middle row", async () => {
await testEditor({
contentBefore:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef[]</td>" +
"</tr></tbody></table>",
stepFunction: addRow("before"),
contentAfter:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef[]</td>" +
"</tr></tbody></table>",
});
});
});
describe("below", () => {
test("should add a row below the bottom row", async () => {
await testEditor({
contentBefore:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef[]</td>' +
"</tr></tbody></table>",
stepFunction: addRow("after"),
contentAfter:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef[]</td>' +
"</tr>" +
'<tr style="height: 20px;">' +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"</tr></tbody></table>",
});
});
test("should add a row below the middle row", async () => {
await testEditor({
contentBefore:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef[]</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addRow("after"),
contentAfter:
'<table><tbody><tr style="height: 20px;">' +
'<td style="width: 20px;">ab</td>' +
'<td style="width: 25px;">cd</td>' +
'<td style="width: 30px;">ef[]</td>' +
"</tr>" +
'<tr style="height: 20px;">' +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"<td><p><br></p></td>" +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
});
describe("move", () => {
test("should move header row down and convert it to normal row", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<th class="o_table_header" style="width: 20px;">ab[]</th>
<th class="o_table_header" style="width: 25px;">cd</th>
<th class="o_table_header" style="width: 30px;">ef</th>
</tr>
<tr style="height: 30px;">
<td>gh</td>
<td>ij</td>
<td>kl</td>
</tr>
</tbody>
</table>
`),
stepFunction: moveRow("down"),
contentAfter: unformat(`
<table>
<tbody>
<tr style="height: 30px;">
<th class="o_table_header" style="width: 20px;">gh</th>
<th class="o_table_header" style="width: 25px;">ij</th>
<th class="o_table_header" style="width: 30px;">kl</th>
</tr>
<tr style="height: 20px;">
<td style="width: 20px;">ab[]</td>
<td style="width: 25px;">cd</td>
<td style="width: 30px;">ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should move second row up and convert it to header row", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr style="height: 20px;">
<th class="o_table_header" style="width: 20px;">ab</th>
<th class="o_table_header" style="width: 25px;">cd</th>
<th class="o_table_header" style="width: 30px;">ef</th>
</tr>
<tr style="height: 30px;">
<td>gh</td>
<td>ij</td>
<td>kl[]</td>
</tr>
</tbody>
</table>
`),
stepFunction: moveRow("up"),
contentAfter: unformat(`
<table>
<tbody>
<tr style="height: 30px;">
<th class="o_table_header" style="width: 20px;">gh</th>
<th class="o_table_header" style="width: 25px;">ij</th>
<th class="o_table_header" style="width: 30px;">kl[]</th>
</tr>
<tr style="height: 20px;">
<td style="width: 20px;">ab</td>
<td style="width: 25px;">cd</td>
<td style="width: 30px;">ef</td>
</tr>
</tbody>
</table>
`),
});
});
});
describe("removal", () => {
test("should remove a row based on selection", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
<tr>
<td>ef</td> <td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: removeRow(),
// @todo @phoenix: consider changing the behavior and placing the cursor
// inside the td (normalize deep)
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>[]ef</td> <td>gh</td>
</tr>
</tbody>
</table>
`),
});
});
test("should remove the row passed as argument", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
<tr>
<td>ef</td> <td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: (editor) => {
// Select the second row
const row = editor.editable.querySelectorAll("tr")[1];
removeRow(row)(editor);
},
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
</tbody>
</table>
`),
});
});
test("should remove the table upon sole row removal", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
</tbody>
</table>
`),
stepFunction: removeRow(),
contentAfter: "<p>[]<br></p>",
});
});
});
});
describe("column", () => {
describe("left", () => {
test("should add a column left of the leftmost column", async () => {
await testEditor({
contentBefore:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 40px;">ab[]</td>' +
'<td style="width: 50px;">cd</td>' +
'<td style="width: 60px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("before"),
contentAfter:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 32px;"><p><br></p></td>' +
'<td style="width: 32px;">ab[]</td>' +
'<td style="width: 40px;">cd</td>' +
'<td style="width: 45px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td><p><br></p></td>" +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
test("should add a `TH` column before", async () => {
await testEditor({
contentBefore:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<th style="width: 40px;">ab[]</th>' +
'<th style="width: 50px;">cd</th>' +
'<th style="width: 60px;">ef</th>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("before"),
contentAfter:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<th style="width: 32px;"><p><br></p></th>' +
'<th style="width: 32px;">ab[]</th>' +
'<th style="width: 40px;">cd</th>' +
'<th style="width: 45px;">ef</th>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td><p><br></p></td>" +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
test("should add a column left of the middle column", async () => {
await testEditor({
contentBefore:
'<table style="width: 200px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 50px;">ab</td>' +
'<td style="width: 65px;">cd</td>' +
'<td style="width: 85px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd[]</td>" +
"<td>ef</td>" +
"</tr>" +
'<tr style="height: 40px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("before"),
contentAfter:
'<table style="width: 200px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 38px;">ab</td>' +
'<td style="width: 49px;"><p><br></p></td>' +
'<td style="width: 49px;">cd</td>' +
'<td style="width: 63px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td><p><br></p></td>" +
"<td>cd[]</td>" +
"<td>ef</td>" +
"</tr>" +
'<tr style="height: 40px;">' +
"<td>ab</td>" +
"<td><p><br></p></td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
});
describe("right", () => {
test("should add a column right of the rightmost column", async () => {
await testEditor({
contentBefore:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 40px;">ab</td>' +
'<td style="width: 50px;">cd</td>' +
'<td style="width: 60px;">ef[]</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("after"),
contentAfter:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 29px;">ab</td>' +
'<td style="width: 36px;">cd</td>' +
'<td style="width: 41px;">ef[]</td>' +
// size was slightly adjusted to
// preserve table width in view on
// fractional division results
'<td style="width: 43px;"><p><br></p></td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"<td><p><br></p></td>" +
"</tr></tbody></table>",
});
});
test("should add a `TH` column after", async () => {
await testEditor({
contentBefore:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<th style="width: 40px;">ab</th>' +
'<th style="width: 50px;">cd[]</th>' +
'<th style="width: 60px;">ef</th>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("after"),
contentAfter:
'<table style="width: 150px;"><tbody><tr style="height: 20px;">' +
'<th style="width: 30px;">ab</th>' +
'<th style="width: 38px;">cd[]</th>' +
'<th style="width: 38px;"><p><br></p></th>' +
'<th style="width: 43px;">ef</th>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td><p><br></p></td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
test("should add a column right of the middle column", async () => {
await testEditor({
contentBefore:
'<table style="width: 200px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 50px;">ab</td>' +
'<td style="width: 65px;">cd</td>' +
'<td style="width: 85px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd[]</td>" +
"<td>ef</td>" +
"</tr>" +
'<tr style="height: 40px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
stepFunction: addColumn("after"),
contentAfter:
'<table style="width: 200px;"><tbody><tr style="height: 20px;">' +
'<td style="width: 38px;">ab</td>' +
'<td style="width: 49px;">cd</td>' +
'<td style="width: 49px;"><p><br></p></td>' +
'<td style="width: 63px;">ef</td>' +
"</tr>" +
'<tr style="height: 30px;">' +
"<td>ab</td>" +
"<td>cd[]</td>" +
"<td><p><br></p></td>" +
"<td>ef</td>" +
"</tr>" +
'<tr style="height: 40px;">' +
"<td>ab</td>" +
"<td>cd</td>" +
"<td><p><br></p></td>" +
"<td>ef</td>" +
"</tr></tbody></table>",
});
});
});
describe("removal", () => {
test("should remove a column based on selection", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
<tr>
<td>ef</td> <td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: removeColumn(),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>[]cd</td>
</tr>
<tr>
<td>gh</td>
</tr>
</tbody>
</table>
`),
});
});
test("should remove the column passed as argument", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td> <td>cd</td>
</tr>
<tr>
<td>ef</td> <td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: (editor) => {
// Select the second cell
const cell = editor.editable.querySelectorAll("td")[1];
removeColumn(cell)(editor);
},
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td>
</tr>
<tr>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should remove the table upon sole column removal", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr> <td>[]ab</td> </tr>
<tr> <td>cd</td> </tr>
</tbody>
</table>
`),
stepFunction: removeColumn(),
contentAfter: "<p>[]<br></p>",
});
});
});
});
describe("tab", () => {
test("should add a new row on press tab at the end of a table", async () => {
const contentBefore = unformat(`
<table><tbody>
<tr style="height: 20px;">
<td style="width: 20px;">ab</td>
<td>cd</td>
<td>ef[]</td>
</tr>
</tbody></table>`);
const { el, editor } = await setupEditor(contentBefore);
await press("Tab");
const expectedContent = unformat(`
<table><tbody>
<tr style="height: 20px;">
<td style="width: 20px;">ab</td>
<td>cd</td>
<td>ef</td>
</tr>
<tr style="height: 20px;">
<td><p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody></table>`);
expect(getContent(el)).toBe(expectedContent);
// Check that it was registed as a history step.
undo(editor);
expect(getContent(el)).toBe(contentBefore);
});
test("should not select whole text of the next cell", async () => {
await testEditor({
contentBefore:
'<table><tbody><tr style="height: 20px;"><td style="width: 20px;">ab</td><td>[cd]</td><td>ef</td></tr></tbody></table>',
stepFunction: () => press("Tab"),
contentAfter:
'<table><tbody><tr style="height: 20px;"><td style="width: 20px;">ab</td><td>cd</td><td>ef[]</td></tr></tbody></table>',
});
});
});

View file

@ -0,0 +1,85 @@
import { expect, test } from "@odoo/hoot";
import { setupEditor } from "../_helpers/editor";
import { click, manuallyDispatchProgrammaticEvent, queryAll, queryFirst } from "@odoo/hoot-dom";
import { animationFrame, tick } from "@odoo/hoot-mock";
import { setSelection } from "../_helpers/selection";
import { execCommand } from "../_helpers/userCommands";
import { expandToolbar } from "../_helpers/toolbar";
import { expectElementCount } from "../_helpers/ui_expectations";
import { deleteBackward } from "../_helpers/user_actions";
function insertTable(editor, cols, rows) {
execCommand(editor, "insertTable", { cols, rows });
}
test("can insert a table", async () => {
const { el, editor } = await setupEditor("<p>hello[]</p>", {});
insertTable(editor, 4, 3);
expect(el.querySelectorAll("tr").length).toBe(3);
expect(el.querySelectorAll("td").length).toBe(12);
});
test("can color cells", async () => {
await setupEditor(`
<table>
<tbody>
<tr>
<td>[ab</td>
<td>c]</td>
<td>ef</td>
</tr>
</tbody>
</table>`);
await expandToolbar();
expect(".o_font_color_selector").toHaveCount(0);
await click(".o-select-color-background");
await animationFrame();
expect(".o_font_color_selector").toHaveCount(1);
await click(".o_color_button[data-color='#6BADDE']");
await animationFrame();
await expectElementCount(".o-we-toolbar", 1);
expect(".o_font_color_selector").toHaveCount(0); // selector closed
// Collapse selection to deselect cells
setSelection({ anchorNode: queryFirst("td"), anchorOffset: 0 });
await tick();
const cells = queryAll("td");
expect(cells[0]).toHaveStyle({ "background-color": "rgba(107, 173, 222, 0.6)" });
expect(cells[1]).toHaveStyle({ "background-color": "rgba(107, 173, 222, 0.6)" });
expect(cells[2]).not.toHaveStyle({ "background-color": "rgba(107, 173, 222, 0.6)" });
});
test("remove text from single selected cell", async () => {
const { editor } = await setupEditor(`
<table class="table table-bordered o_table">
<tbody>
<tr>
<td><p>[]abc</p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>`);
const firstP = queryFirst("td p");
const { left, top } = firstP.getBoundingClientRect();
manuallyDispatchProgrammaticEvent(firstP, "mousedown", {
detail: 3,
clientX: left,
clientY: top,
});
await animationFrame();
manuallyDispatchProgrammaticEvent(firstP, "mouseup", {
detail: 3,
clientX: left,
clientY: top,
});
await animationFrame();
deleteBackward(editor);
expect(queryFirst("td p")).toHaveOuterHTML("<p><br></p>");
});

View file

@ -0,0 +1,344 @@
import { describe, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { testEditor } from "../_helpers/editor";
import { unformat } from "../_helpers/format";
describe("move selection with tab/shift+tab", () => {
describe("tab", () => {
test("should move cursor to the next th", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<th>[]ab</th>
<th>cd</th>
<th>ef</th>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<th>ab</th>
<th>cd[]</th>
<th>ef</th>
</tr>
</tbody>
</table>
`),
});
});
test("should move cursor to the next td", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<th>[]ab</th>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<th>ab</th>
<td>cd[]</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should move cursor to the end of next cell", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd[]</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test.tags("iframe", "desktop");
test("in iframe, desktop: should move cursor to the end of next cell in an iframe", async () => {
await testEditor({
props: { iframe: true },
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd[]</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test.tags("iframe", "mobile");
test("in iframe, mobile: should move cursor to the end of next cell in an iframe", async () => {
await testEditor({
props: { iframe: true, mobile: true },
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[]ab</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd[]</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should move cursor to the end of next cell in the row below", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>[cd]</td>
</tr>
<tr>
<td>ef</td>
<td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd</td>
</tr>
<tr>
<td>ef[]</td>
<td>gh</td>
</tr>
</tbody>
</table>
`),
});
});
test("move cursor to end of next cell when selection is inside table", async () => {
await testEditor({
contentBefore: unformat(`
<ul>
<li>
<br>
<table>
<tbody>
<tr>
<td><p>[]ab</p></td>
<td><p>cd</p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>
`),
stepFunction: async () => press("Tab"),
contentAfter: unformat(`
<ul>
<li>
<br>
<table>
<tbody>
<tr>
<td><p>ab</p></td>
<td><p>cd[]</p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>
`),
});
});
});
describe("shift+tab", () => {
test("should move cursor to the end of previous cell", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>[]cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press(["Shift", "Tab"]),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab[]</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("should move cursor to the end of previous cell in the row above", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd</td>
</tr>
<tr>
<td>[ef]</td>
<td>gh</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press(["Shift", "Tab"]),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>ab</td>
<td>cd[]</td>
</tr>
<tr>
<td>ef</td>
<td>gh</td>
</tr>
</tbody>
</table>
`),
});
});
test("should not cursor if there is no previous cell", async () => {
await testEditor({
contentBefore: unformat(`
<table>
<tbody>
<tr>
<td>[ab]</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
stepFunction: async () => press(["Shift", "Tab"]),
contentAfter: unformat(`
<table>
<tbody>
<tr>
<td>[ab]</td>
<td>cd</td>
<td>ef</td>
</tr>
</tbody>
</table>
`),
});
});
test("move cursor to end of previous cell when selection is inside table", async () => {
await testEditor({
contentBefore: unformat(`
<ul>
<li>
<br>
<table>
<tbody>
<tr>
<td><p>ab</p></td>
<td><p>[]cd</p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>
`),
stepFunction: async () => press(["Shift", "Tab"]),
contentAfter: unformat(`
<ul>
<li>
<br>
<table>
<tbody>
<tr>
<td><p>ab[]</p></td>
<td><p>cd</p></td>
</tr>
</tbody>
</table>
<br>
</li>
</ul>
`),
});
});
});
});

File diff suppressed because it is too large Load diff