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,82 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { BaseOptionComponent } from "@html_builder/core/utils";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { reactive, xml } from "@odoo/owl";
import { contains, defineModels, fields, models, onRpc } from "@web/../tests/web_test_helpers";
import { delay } from "@web/core/utils/concurrency";
class Test extends models.Model {
_name = "test";
_records = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
name = fields.Char();
}
describe.current.tags("desktop");
defineModels([Test]);
test.tags("focus required");
test("basic many2many: find tag, select tag, unselect tag", async () => {
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
class TestComponent extends BaseOptionComponent {
static template = xml`<BasicMany2Many selection="props.selection" model="'test'" setSelection="props.setSelection.bind(this)"/>`;
static props = {
selection: Array,
setSelection: Function,
};
}
const selection = reactive([]);
addBuilderOption({
selector: ".test-options-target",
Component: TestComponent,
props: {
selection: selection,
setSelection(newSelection) {
selection.length = 0;
for (const item of newSelection) {
selection.push(item);
}
},
},
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("table tr").toHaveCount(0);
expect(selection).toEqual([]);
await contains(".btn.o-dropdown").click();
expect("input").toHaveCount(1);
await contains("input").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(3);
await contains("span.o-dropdown-item").click();
expect(selection).toEqual([{ id: 1, name: "First", display_name: "First" }]);
expect("table tr").toHaveCount(1);
await contains(".btn.o-dropdown").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(2);
await contains("span.o-dropdown-item").click();
expect(selection).toEqual([
{ id: 1, name: "First", display_name: "First" },
{ id: 2, name: "Second", display_name: "Second" },
]);
expect("table tr").toHaveCount(2);
await contains("button.fa-minus").click();
expect(selection).toEqual([{ id: 2, name: "Second", display_name: "Second" }]);
expect("table tr").toHaveCount(1);
expect("table input").toHaveValue("Second");
});

View file

@ -0,0 +1,853 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
import { undo } from "@html_editor/../tests/_helpers/user_actions";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame, click, Deferred, hover, runAllTimers } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
const falsy = () => false;
test("call a specific action with some params and value", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ params: { mainParam: testParam }, value }) {
expect.step(`customAction ${testParam} ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" actionParam="'myParam'" actionValue="'myValue'">MyAction</BuilderButton>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("[data-action-id='customAction']").toHaveText("MyAction");
await click("[data-action-id='customAction']");
await animationFrame();
// The function `apply` should be called twice (on hover (for preview), then, on click).
expect.verifySteps(["customAction myParam myValue", "customAction myParam myValue"]);
});
test("call a shorthand action", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton classAction="'my-custom-class'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click("[data-class-action='my-custom-class']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("my-custom-class");
});
test("call a shorthand action and a specific action", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ editingElement }) {
expect.step(`customAction`);
editingElement.innerHTML = "c";
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" classAction="'my-custom-class'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click("[data-action-id='customAction'][data-class-action='my-custom-class']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("my-custom-class");
// The function `apply` should be called twice (on hover (for preview), then, on click).
expect.verifySteps(["customAction", "customAction"]);
expect(":iframe .test-options-target").toHaveInnerHTML("c");
});
test("preview a shorthand action and a specific action", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ editingElement }) {
expect.step(`customAction`);
editingElement.innerHTML = "c";
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" classAction="'my-custom-class'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await hover("[data-action-id='customAction'][data-class-action='my-custom-class']");
expect(":iframe .test-options-target").toHaveClass("my-custom-class");
expect.verifySteps(["customAction"]);
expect(":iframe .test-options-target").toHaveInnerHTML("c");
await hover(":iframe .test-options-target");
expect(":iframe .test-options-target").toHaveInnerHTML("b");
expect.verifySteps([]);
});
test("prevent preview of a specific action", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply() {
expect.step(`customAction`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" preview="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains("[data-action-id='customAction']").hover();
expect.verifySteps([]);
await contains("[data-action-id='customAction']").click();
expect.verifySteps(["customAction"]);
});
test("prevent preview of a specific action (2)", async () => {
class CustomAction extends BuilderAction {
static id = "customAction";
setup() {
this.preview = false;
}
apply() {
expect.step(`customAction`);
}
}
addBuilderAction({
CustomAction,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains("[data-action-id='customAction']").hover();
expect.verifySteps([]);
await contains("[data-action-id='customAction']").click();
expect.verifySteps(["customAction"]);
});
test("should toggle when not in a BuilderButtonGroup", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton classAction="'c1'" preview="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-class-action='c1']").click();
expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
await contains("[data-class-action='c1']").click();
expect(":iframe .test-options-target").not.toHaveClass("test-options-target c1");
});
test("should call apply when the button is active and none of its actions have a clean method", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply() {
expect.step(`customAction apply`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" preview="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction']").click();
expect.verifySteps(["customAction apply"]);
await contains("[data-action-id='customAction']").click();
expect.verifySteps(["customAction apply"]);
});
test("should not toggle when in a BuilderButtonGroup", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton classAction="'c1'" preview="false"/>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-class-action='c1']").click();
expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
await contains("[data-class-action='c1']").click();
expect(":iframe .test-options-target").toHaveClass("test-options-target c1");
});
test("clean another action", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton classAction="'my-custom-class1'"/>
<BuilderButton classAction="'my-custom-class2'"/>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click("[data-class-action='my-custom-class1']");
await animationFrame();
expect(":iframe .test-options-target").toHaveAttribute(
"class",
"test-options-target o-paragraph my-custom-class1"
);
await click("[data-class-action='my-custom-class2']");
await animationFrame();
expect(":iframe .test-options-target").toHaveAttribute(
"class",
"test-options-target o-paragraph my-custom-class2"
);
});
test("clean should provide the next action value", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
clean({ nextAction }) {
expect.step(
`customAction clean ${nextAction.params.mainParam} ${nextAction.value}`
);
}
apply() {
expect.step(`customAction apply`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton classAction="'c1'" action="'customAction'"/>
<BuilderButton classAction="'c2'" action="'customAction'" actionParam="'param2'" actionValue="'value2'"/>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click("[data-class-action='c1']");
await click("[data-class-action='c2']");
await animationFrame();
expect.verifySteps([
"customAction apply",
"customAction apply",
"customAction clean param2 value2",
"customAction apply",
"customAction clean param2 value2",
"customAction apply",
]);
});
test("clean should only be called on the currently selected item", async () => {
function makeAction(n) {
const action = class extends BuilderAction {
static id = `customAction${n}`;
clean() {
expect.step(`customAction${n} clean`);
}
apply() {
expect.step(`customAction${n} apply`);
}
};
return { action };
}
addBuilderAction({
customAction1: makeAction(1).action,
customAction2: makeAction(2).action,
customAction3: makeAction(3).action,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton action="'customAction1'" classAction="'c1'" />
<BuilderButton action="'customAction2'" classAction="'c2'" />
<BuilderButton action="'customAction3'" classAction="'c3'" />
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction1']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("c1");
await click("[data-action-id='customAction2']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("c2");
expect.verifySteps([
"customAction1 apply",
"customAction1 apply",
"customAction1 clean",
"customAction2 apply",
"customAction1 clean",
"customAction2 apply",
]);
});
test("clean should be async", async () => {
function makeAction(n) {
const { promise, resolve } = Promise.withResolvers();
const action = class extends BuilderAction {
static id = `customAction${n}`;
async clean() {
expect.step(`customAction${n} clean before promise`);
await promise;
expect.step(`customAction${n} clean after promise`);
}
apply() {
expect.step(`customAction${n} apply`);
}
};
return { action, resolve };
}
const action1 = makeAction(1);
addBuilderAction({
customAction1: action1.action,
customAction2: makeAction(2).action,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup preview="false">
<BuilderButton action="'customAction1'" classAction="'c1'"/>
<BuilderButton action="'customAction2'" />
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction1']");
await animationFrame();
await click("[data-action-id='customAction2']");
await animationFrame();
action1.resolve();
await animationFrame();
expect.verifySteps([
"customAction1 apply",
"customAction1 clean before promise",
"customAction1 clean after promise",
"customAction2 apply",
]);
});
test("add the active class if the condition is met", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton classAction="'my-custom-class1'"/>
<BuilderButton classAction="'my-custom-class2'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target my-custom-class1">b</div>`);
await contains(":iframe .test-options-target").click();
expect("[data-class-action='my-custom-class1']").toHaveClass("active");
expect("[data-class-action='my-custom-class2']").not.toHaveClass("active");
});
test("add classActive to class when active", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton classAction="'my-custom-class1'"
className="'base-class btn1'"
classActive="'active-class'"/>
<BuilderButton classAction="'my-custom-class2'"
className="'base-class btn2'"
classActive="'active-class'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target my-custom-class1">b</div>`);
await contains(":iframe .test-options-target").click();
const permanentClass = "base-class";
const activeClass = "active-class";
expect(".btn1").toHaveClass([permanentClass, activeClass]);
expect(".btn2").toHaveClass(permanentClass);
expect(".btn2").not.toHaveClass(activeClass);
await contains(".btn2").click();
expect(".btn2").toHaveClass([permanentClass, activeClass]);
await contains(".btn2").click();
expect(".btn2").toHaveClass(permanentClass);
expect(".btn2").not.toHaveClass(activeClass);
});
test("apply classAction on multi elements", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton applyTo="'.target-apply'" classAction="'my-custom-class'"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(`
<div class="test-options-target">
<div class="target-apply">a</div>
<div class="target-apply">b</div>
</div>`);
const editableContent = getEditableContent();
await contains(":iframe .test-options-target").click();
expect(editableContent).toHaveInnerHTML(`
<div class="test-options-target">
<div class="target-apply o-paragraph">a</div>
<div class="target-apply o-paragraph">b</div>
</div>`);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(`
<div class="test-options-target">
<div class="target-apply o-paragraph my-custom-class">a</div>
<div class="target-apply o-paragraph my-custom-class">b</div>
</div>`);
});
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'my label'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'my label'">
<BuilderButton applyTo="'.my-custom-class'" classAction="'test'"/>
</BuilderRow>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target o-paragraph"><div class="child-target">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target o-paragraph"><div class="child-target">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect("[data-class-action='test']").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target o-paragraph"><div class="child-target my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect("[data-class-action='test']").toHaveCount(1);
expect("[data-class-action='test']").not.toHaveClass("active");
});
describe("inherited actions", () => {
function makeAction(n, { async, isApplied } = {}) {
const action = class extends BuilderAction {
static id = `customAction${n}`;
isApplied() {
return isApplied?.();
}
clean({ params: { mainParam: testParam }, value }) {
expect.step(`customAction${n} clean ${testParam} ${value}`);
}
apply({ params: { mainParam: testParam }, value }) {
expect.step(`customAction${n} apply ${testParam} ${value}`);
}
};
if (async) {
let resolve;
const promise = new Promise((r) => {
resolve = r;
});
action.prototype.load = async ({ params: { mainParam: testParam }, value }) => {
expect.step(`customAction${n} load ${testParam} ${value}`);
return promise;
};
return { action, resolve };
}
return { action };
}
test("inherit actions for another button", async () => {
addBuilderAction({
customAction1: makeAction(1).action,
customAction2: makeAction(2).action,
customAction3: makeAction(3, { isApplied: falsy }).action,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton action="'customAction1'" actionParam="'myParam1'" actionValue="'myValue1'" classAction="'class1'" id="'c1'">MyAction1</BuilderButton>
<BuilderButton action="'customAction2'" actionParam="'myParam2'" actionValue="'myValue2'">MyAction2</BuilderButton>
</BuilderButtonGroup>
<BuilderButton action="'customAction3'" actionParam="'myParam3'" actionValue="'myValue3'" inheritedActions="['c1']" >MyAction2</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target class1">a</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction3']").hover();
expect.verifySteps([
"customAction1 clean myParam1 myValue1",
"customAction3 apply myParam3 myValue3",
"customAction1 apply myParam1 myValue1",
]);
});
test("inherit actions for another button (with async)", async () => {
const action1 = makeAction(1, { async: true });
const action2 = makeAction(2, { async: true });
const action3 = makeAction(3, { async: true });
const action4 = makeAction(4, { async: true, isApplied: falsy });
addBuilderAction({
customAction1: action1.action,
customAction2: action2.action,
customAction3: action3.action,
customAction4: action4.action,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton action="'customAction1'" actionParam="'myParam1'" actionValue="'myValue1'" classAction="'class1'" id="'c1'">MyAction1</BuilderButton>
<BuilderButton action="'customAction2'" actionParam="'myParam2'" actionValue="'myValue2'">MyAction2</BuilderButton>
</BuilderButtonGroup>
<BuilderButton action="'customAction3'" actionParam="'myParam3'" actionValue="'myValue3'" id="'c3'">MyAction1</BuilderButton>
<BuilderButton action="'customAction4'" actionParam="'myParam4'" actionValue="'myValue4'" inheritedActions="['c1', 'c3']" >MyAction2</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target class1">a</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction4']").hover();
action4.resolve();
action3.resolve();
action1.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
expect.verifySteps([
"customAction4 load myParam4 myValue4",
"customAction1 load myParam1 myValue1",
"customAction3 load myParam3 myValue3",
"customAction1 clean myParam1 myValue1",
"customAction4 apply myParam4 myValue4",
"customAction1 apply myParam1 myValue1",
"customAction3 apply myParam3 myValue3",
]);
});
test("inherit actions for another button (from the context)", async () => {
addBuilderAction({
customAction1: makeAction(1).action,
customAction2: makeAction(2).action,
customAction3: makeAction(3, { isApplied: falsy }).action,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton action="'customAction1'" actionParam="'myParam1'" actionValue="'myValue1'" classAction="'class1'" id="'c1'">MyAction1</BuilderButton>
<BuilderButton action="'customAction2'" actionParam="'myParam2'" actionValue="'myValue2'">MyAction2</BuilderButton>
</BuilderButtonGroup>
<BuilderContext inheritedActions="['c1']">
<BuilderButton action="'customAction3'" actionParam="'myParam3'" actionValue="'myValue3'">MyAction2</BuilderButton>
</BuilderContext>
`,
});
await setupHTMLBuilder(`<div class="test-options-target class1">a</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction3']").hover();
expect.verifySteps([
"customAction1 clean myParam1 myValue1",
"customAction3 apply myParam3 myValue3",
"customAction1 apply myParam1 myValue1",
]);
});
});
describe("Operation", () => {
function makeAsyncActionItem(actionName) {
const item = {};
const promise = new Promise((resolve) => {
item.resolve = resolve;
});
addBuilderAction({
[actionName]: class extends BuilderAction {
static id = actionName;
async load() {
expect.step(`load ${actionName}`);
await promise;
}
async apply({ editingElement }) {
expect.step(`apply ${actionName}`);
editingElement.innerText = editingElement.innerText + `-${actionName}`;
}
},
});
return item;
}
function makeActionItem(actionName) {
addBuilderAction({
[actionName]: class extends BuilderAction {
static id = actionName;
apply({ editingElement }) {
expect.step(actionName);
editingElement.innerText = editingElement.innerText + `-${actionName}`;
}
},
});
}
test("handle async actions with commit and preview (2 quick consecutive hovers)", async () => {
const asyncAction1 = makeAsyncActionItem("asyncAction1");
const asyncAction2 = makeAsyncActionItem("asyncAction2");
const asyncAction3 = makeAsyncActionItem("asyncAction3");
makeActionItem("action1");
makeActionItem("action2");
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'my label'">
<BuilderButton action="'asyncAction1'"/>
<BuilderButton action="'asyncAction2'"/>
<BuilderButton action="'asyncAction3'"/>
<BuilderButton action="'action1'"/>
<BuilderButton action="'action2'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
await contains(":iframe .test-options-target").click();
await hover("[data-action-id='asyncAction1']");
await animationFrame();
hover("[data-action-id='asyncAction2']");
hover("[data-action-id='asyncAction3']");
await runAllTimers();
// we check here that the action2 load operation has been cancelled by
// the action 3.
expect.verifySteps(["load asyncAction1", "load asyncAction3"]);
await animationFrame();
await contains("[data-action-id='asyncAction3']").click();
await hover("[data-action-id='action1']");
await animationFrame();
asyncAction1.resolve();
asyncAction2.resolve();
asyncAction3.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
expect.verifySteps(["load asyncAction3", "apply asyncAction3", "action1"]);
expect(":iframe .test-options-target").toHaveInnerHTML("a-asyncAction3-action1");
// If the code is not working properly, hovering on another action at
// this moment could revert the changes made by asyncAction3 through the
// revert of the preview. In order to test this case, we hover action2.
await hover("[data-action-id='action2']");
await animationFrame();
expect(":iframe .test-options-target").toHaveInnerHTML("a-asyncAction3-action2");
expect.verifySteps(["action2"]);
});
test("handle async actions with commit and preview (separated by running all timers)", async () => {
const asyncAction1 = makeAsyncActionItem("asyncAction1");
const asyncAction2 = makeAsyncActionItem("asyncAction2");
const asyncAction3 = makeAsyncActionItem("asyncAction3");
makeActionItem("action1");
makeActionItem("action2");
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'my label'">
<BuilderButton action="'asyncAction1'"/>
<BuilderButton action="'asyncAction2'"/>
<BuilderButton action="'asyncAction3'"/>
<BuilderButton action="'action1'"/>
<BuilderButton action="'action2'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
await contains(":iframe .test-options-target").click();
await hover("[data-action-id='asyncAction1']");
await runAllTimers();
await hover("[data-action-id='asyncAction2']");
await runAllTimers();
await hover("[data-action-id='asyncAction3']");
await runAllTimers();
await contains("[data-action-id='asyncAction3']").click();
await hover("[data-action-id='action1']");
await runAllTimers();
asyncAction1.resolve();
asyncAction2.resolve();
asyncAction3.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
expect.verifySteps([
"load asyncAction1",
"load asyncAction2",
"load asyncAction3",
"load asyncAction3",
"apply asyncAction3",
"action1",
]);
expect(":iframe .test-options-target").toHaveInnerHTML("a-asyncAction3-action1");
// If the code is not working properly, hovering on another action at
// this moment could revert the changes made by asyncAction3 through the
// revert of the preview. In order to test this case, we hover action2.
await hover("[data-action-id='action2']");
await animationFrame();
expect(":iframe .test-options-target").toHaveInnerHTML("a-asyncAction3-action2");
expect.verifySteps(["action2"]);
});
});
test("click on BuilderButton with inverseAction", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton classAction="'my-custom-class'" inverseAction="true"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(":iframe .test-options-target").not.toHaveClass("my-custom-class");
expect("[data-class-action='my-custom-class']").toHaveClass("active");
await contains("[data-class-action='my-custom-class']").click();
expect(":iframe .test-options-target").toHaveClass("my-custom-class");
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
});
test("do not load when an operation is cleaned", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
isApplied({ editingElement }) {
return editingElement.classList.contains("applied");
}
clean() {
expect.step("clean");
}
async load() {
expect.step("load");
}
apply({ editingElement }) {
expect.step("apply");
editingElement.classList.add("applied");
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButton action="'customAction'" preview="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction']").click();
await contains("[data-action-id='customAction']").click();
expect.verifySteps(["load", "apply", "clean"]);
});
test("click on BuilderButton with async action", async () => {
const def = new Deferred();
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
isApplied({ editingElement }) {
return editingElement.classList.contains("applied");
}
async apply({ editingElement }) {
await def;
editingElement.classList.add("applied");
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton action="'customAction'" preview="false"/>
<BuilderButton classAction="'test'" preview="false"/>
`,
});
const { getEditor } = await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
const editor = getEditor();
await contains(":iframe .test-options-target").click();
await contains("[data-action-id='customAction']").click();
await contains("[data-class-action='test']").click();
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").not.toHaveClass("applied");
def.resolve();
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("test");
expect(":iframe .test-options-target").toHaveClass("applied");
undo(editor);
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").toHaveClass("applied");
undo(editor);
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").not.toHaveClass("applied");
});
class SubTestOption extends BaseOptionComponent {
static template = xml`
<BuilderContext applyTo="this.domState.applyTo">
<BuilderButton classAction="'actionClass'">actionClass</BuilderButton>
</BuilderContext>
`;
static props = {};
setup() {
super.setup();
this.domState = useDomState((el) => ({
applyTo: el.matches(".first") ? ".a" : ".b",
}));
}
}
class TestOption extends BaseOptionComponent {
static template = xml`
<BuilderButton classAction="'secondCase'">secondCase</BuilderButton>
<BuilderContext applyTo="this.domState.applyTo">
<SubTestOption/>
</BuilderContext>
`;
static props = {};
static components = {
SubTestOption,
};
setup() {
super.setup();
this.domState = useDomState((el) => ({
applyTo: el.matches(".secondCase") ? ".second" : ".first",
}));
}
}
test("consecutive dynamic applyTo", async () => {
addBuilderOption({
selector: ".selector",
Component: TestOption,
});
await setupHTMLBuilder(`
<div class="selector">
<div class="first">
<div class="a">a</div>
<div class="b">b</div>
</div>
<div class="second">
<div class="a">a</div>
<div class="b">b</div>
</div>
</div>
`);
await contains(":iframe .selector").click();
await contains("[data-class-action='actionClass']").click();
expect(":iframe .first .a").toHaveClass("actionClass");
expect(":iframe .first .b").not.toHaveClass("actionClass");
await contains("[data-class-action='secondCase']").click();
await contains("[data-class-action='actionClass']").click();
expect(":iframe .second .a").not.toHaveClass("actionClass");
expect(":iframe .second .b").toHaveClass("actionClass");
});

View file

@ -0,0 +1,212 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { describe, expect, test } from "@odoo/hoot";
import { hover } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("change the editingElement of sub widget through `applyTo` prop", async () => {
class CustomAction extends BuilderAction {
static id = "customAction";
apply({ editingElement }) {
expect.step(`customAction ${editingElement.className}`);
}
}
addBuilderAction({
CustomAction,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup applyTo="'.a'">
<BuilderButton action="'customAction'"/>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">
<div class="a">b</div>
</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await hover("[data-action-id='customAction']");
expect.verifySteps(["customAction a o-paragraph"]);
});
test("should propagate actionParam in the context", async () => {
class CustomAction extends BuilderAction {
static id = "customAction";
apply({ params: { mainParam: testParam } }) {
expect.step(`customAction ${testParam}`);
}
}
addBuilderAction({
CustomAction,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup actionParam="'myParam'">
<BuilderButton action="'customAction'"/>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">
<div class="a">b</div>
</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await hover("[data-action-id='customAction']");
expect.verifySteps(["customAction myParam"]);
});
test("prevent preview of all buttons", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup preview="false">
<BuilderButton action="'customAction1'"/>
<BuilderButton action="'customAction2'" preview="true"/>
</BuilderButtonGroup>
<BuilderButtonGroup preview="true">
<BuilderButton action="'customAction3'"/>
</BuilderButtonGroup>
<BuilderButtonGroup>
<BuilderButton action="'customAction4'"/>
</BuilderButtonGroup>`,
});
class CustomAction1 extends BuilderAction {
static id = "customAction1";
apply() {
return expect.step(`customAction1`);
}
}
class CustomAction2 extends BuilderAction {
static id = "customAction2";
apply() {
return expect.step(`customAction2`);
}
}
class CustomAction3 extends BuilderAction {
static id = "customAction3";
apply() {
return expect.step(`customAction3`);
}
}
class CustomAction4 extends BuilderAction {
static id = "customAction4";
apply() {
return expect.step(`customAction4`);
}
}
addBuilderAction({
CustomAction1,
CustomAction2,
CustomAction3,
CustomAction4,
});
await setupHTMLBuilder(`
<div class="test-options-target">
<div class="a">b</div>
</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains("[data-action-id='customAction1']").hover();
expect.verifySteps([]);
await contains("[data-action-id='customAction2']").hover();
expect.verifySteps(["customAction2"]);
await contains("[data-action-id='customAction3']").hover();
expect.verifySteps(["customAction3"]);
await contains("[data-action-id='customAction4']").hover();
expect.verifySteps(["customAction4"]);
});
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderButtonGroup applyTo="'.my-custom-class'">
<BuilderButton classAction="'test'">Test</BuilderButton>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(
`<div class="parent-target o-paragraph"><div class="child-target">b</div></div>`
);
await contains(":iframe .parent-target").click();
expect(".options-container .btn-group").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container .btn-group").toHaveCount(1);
});
test("hide/display base on applyTo - 2", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton applyTo="'.my-custom-class'" classAction="'test'">Test</BuilderButton>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(
`<div class="parent-target o-paragraph"><div class="child-target">b</div></div>`
);
await contains(":iframe .parent-target").click();
expect(".options-container .btn-group").not.toBeVisible();
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container .btn-group").toBeVisible();
});
test("click on BuilderButton with empty value should remove styleAction", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButtonGroup>
<BuilderButton styleAction="'width'" styleActionValue="''"/>
<BuilderButton styleAction="'width'" styleActionValue="'25%'"/>
</BuilderButtonGroup>`,
});
const { contentEl } = await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-style-action='width'][data-style-action-value='25%']").click();
expect(contentEl).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" style="width: 25% !important;">b</div>`
);
await contains("[data-style-action='width'][data-style-action-value='']").click();
expect(contentEl).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" style="">b</div>`
);
});
test("button that matches with the highest priority should be active", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderButtonGroup>
<BuilderButton classAction="'a'" >a</BuilderButton>
<BuilderButton classAction="'a b'">a b</BuilderButton>
<BuilderButton classAction="'a b c'">a b c</BuilderButton>
</BuilderButtonGroup>`,
});
await setupHTMLBuilder(`<div class="test-options-target a b">b</div>`);
await contains(":iframe .test-options-target").click();
expect("[data-class-action='a']").not.toHaveClass("active");
expect("[data-class-action='a b']").toHaveClass("active");
expect("[data-class-action='a b c']").not.toHaveClass("active");
});

View file

@ -0,0 +1,75 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { expect, test, describe } from "@odoo/hoot";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("Click on checkbox", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderCheckbox classAction="'checkbox-action'"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="test-options-target o-paragraph">b</div>`
);
const editableContent = getEditableContent();
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect(".o-checkbox .form-check-input:checked").toHaveCount(0);
expect(editableContent).toHaveInnerHTML(`<div class="test-options-target o-paragraph">b</div>`);
await contains(".o-checkbox").click();
expect(".o-checkbox .form-check-input:checked").toHaveCount(1);
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph checkbox-action">b</div>`
);
await contains(".o-checkbox").click();
expect(".o-checkbox .form-check-input:checked").toHaveCount(0);
expect(editableContent).toHaveInnerHTML(`<div class="test-options-target o-paragraph">b</div>`);
});
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderCheckbox classAction="'checkbox-action'" applyTo="'.my-custom-class'"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target b">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect(".options-container .o-checkbox").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect(".options-container .o-checkbox").toHaveCount(1);
});
test("click on BuilderCheckbox with inverseAction", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderCheckbox classAction="'my-custom-class'" inverseAction="true"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(":iframe .test-options-target").not.toHaveClass("my-custom-class");
expect(".o-checkbox .form-check-input:checked").toHaveCount(1);
await contains(".o-checkbox").click();
expect(":iframe .test-options-target").toHaveClass("my-custom-class");
expect(".o-checkbox .form-check-input:checked").toHaveCount(0);
});

View file

@ -0,0 +1,346 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { undo } from "@html_editor/../tests/_helpers/user_actions";
import { before, describe, expect, test } from "@odoo/hoot";
import { animationFrame, click, Deferred, hover, press, tick, waitFor } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("should apply backgroundColor to the editing element", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['solid']" styleAction="'background-color'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await click(".o-overlay-item [data-color='o-color-1']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("test-options-target bg-o-color-1");
});
test("should apply color to the editing element", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['solid']" styleAction="'color'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await click(".o-overlay-item [data-color='o-color-1']");
await animationFrame();
expect(":iframe .test-options-target").toHaveClass("test-options-target text-o-color-1");
});
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderColorPicker applyTo="'.my-custom-class'" styleAction="'background-color'"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target b">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect(".options-container .o_we_color_preview").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect(".options-container .o_we_color_preview").toHaveCount(1);
});
test("apply color to a different style than color or backgroundColor", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['solid']" styleAction="'border-top-color'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item [data-color='#FF0000']").click();
expect(":iframe .test-options-target").toHaveStyle({
borderTopColor: "rgb(255, 0, 0)",
});
expect(".we-bg-options-container .o_we_color_preview").toHaveStyle({
"background-color": "rgb(255, 0, 0)",
});
});
test("apply custom action", async () => {
const styleName = "border-top-color";
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
async load() {
expect.step("load");
}
async apply({ editingElement }) {
expect.step(
`apply ${getComputedStyle(editingElement).getPropertyValue(styleName)}`
);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['solid']" styleAction="'${styleName}'" action="'customAction'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item [data-color='#FF0000']").click();
// Applied twice for hover (preview) and click (commit).
expect.verifySteps(["load", "apply rgb(255, 0, 0)", "load", "apply rgb(255, 0, 0)"]);
});
test("apply custom async action", async () => {
const def = new Deferred();
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue() {
return "";
}
async apply({ editingElement }) {
await def;
editingElement.classList.add("applied");
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderColorPicker action="'customAction'" enabledTabs="['solid']"/>
<BuilderButton classAction="'test'" preview="false"/>
`,
});
const { getEditor } = await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
const editor = getEditor();
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item [data-color='#FF0000']").click();
await contains("[data-class-action='test']").click();
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").not.toHaveClass("applied");
def.resolve();
await tick();
expect(":iframe .test-options-target").toHaveClass("test");
expect(":iframe .test-options-target").toHaveClass("applied");
undo(editor);
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").toHaveClass("applied");
undo(editor);
expect(":iframe .test-options-target").not.toHaveClass("test");
expect(":iframe .test-options-target").not.toHaveClass("applied");
});
test("should revert preview on escape", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['solid']" styleAction="'background-color'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(":iframe .test-options-target").toHaveStyle({ "background-color": "rgba(0, 0, 0, 0)" });
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await hover(".o-overlay-item [data-color='#FF0000']");
expect(":iframe .test-options-target").toHaveStyle({ "background-color": "rgb(255, 0, 0)" });
await press("escape");
expect(":iframe .test-options-target").toHaveStyle({ "background-color": "rgba(0, 0, 0, 0)" });
});
test("should mark default color as selected when it is selected", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker enabledTabs="['custom']" styleAction="'background-color'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item [data-color='900']").click();
expect(":iframe .test-options-target").toHaveClass("bg-900");
await contains(".we-bg-options-container .o_we_color_preview").click();
expect(".o-overlay-item [data-color='900']").toHaveClass("selected");
});
test("should apply transparent color if no color is defined", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
expect.step("getValue");
return editingElement.dataset.color;
}
apply({ editingElement, value }) {
expect.step("apply");
editingElement.dataset.color = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker action="'customAction'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item button:contains('Custom')").click();
expect.verifySteps(["getValue"]);
expect(".o-overlay-item .o_hex_input").toHaveValue("#FFFFFF00");
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
await contains(".o-overlay-item .o_color_pick_area").click({ top: "50%", left: "50%" });
expect(".o-overlay-item .o_hex_input").not.toHaveValue("#FFFFFF00");
expect(":iframe .test-options-target").toHaveAttribute("data-color");
expect.verifySteps(["apply"]); // Preview
await contains(".options-container-header").click(); // Close the popover by clicking outside.
expect.verifySteps(["apply", "getValue"]); // Commit
});
describe("Custom colorpicker: preview and commit", () => {
before(() => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.dataset.color;
}
apply({ editingElement, value }) {
expect.step("apply");
editingElement.dataset.color = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderColorPicker action="'customAction'"/>`,
});
});
/****************************************
*************** POINTER ***************
***************************************/
test("should preview while modifying custom pickers with mouse", async () => {
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item button:contains('Custom')").click();
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
await contains(".o-overlay-item .o_color_pick_area").click({ top: "50%", left: "50%" });
expect(":iframe .test-options-target").toHaveAttribute("data-color");
expect.verifySteps(["apply"]); // Only once: preview
await contains(".o-overlay-item .o_color_slider").click({ top: "50%", left: "50%" });
expect.verifySteps(["apply"]); // Only once: preview
await contains(".o-overlay-item .o_opacity_slider").click({ top: "50%", left: "50%" });
expect.verifySteps(["apply"]); // Only once: preview
expect(":iframe .test-options-target").toHaveAttribute("data-color");
// Make sure it was just a preview: close with escape
await press("escape"); // Undo preview
await animationFrame();
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
});
test("should commit when popover is closed by clicking outside", async () => {
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await contains(".o-overlay-item button:contains('Custom')").click();
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
await contains(".o-overlay-item .o_color_pick_area").click({ top: "50%", left: "50%" });
expect(":iframe .test-options-target").toHaveAttribute("data-color");
expect.verifySteps(["apply"]); // Only once: preview
await contains(".options-container-header").click(); // Close the popover by clicking outside.
expect.verifySteps(["apply"]); // Commit
expect(":iframe .test-options-target").toHaveAttribute("data-color");
});
/****************************************
*************** KEYBOARD ***************
***************************************/
const prepareKeyboardSetup = async () => {
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains(".we-bg-options-container .o_we_color_preview").click();
await waitFor(".o-overlay-item button:contains('Custom')");
await press("Tab");
await press("Enter");
await waitFor(".o-overlay-item .o_color_pick_area");
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
// Press shift+tab until it gets to the colorpicker area.
for (let i = 0; i < 5; i++) {
await press("Tab", { shiftKey: true });
}
expect(".o-overlay-item .o_color_pick_area .o_picker_pointer").toBeFocused();
};
test("should preview while modifying custom pickers with keyboard", async () => {
await prepareKeyboardSetup();
await press("ArrowRight");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
await press("ArrowDown");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
expect(":iframe .test-options-target").toHaveAttribute("data-color");
await press("Tab"); // focus color slider
expect(".o-overlay-item .o_color_slider .o_slider_pointer").toBeFocused();
await press("ArrowUp");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
await press("Tab"); // focus opacity slider
expect(".o-overlay-item .o_opacity_slider .o_opacity_pointer").toBeFocused();
await press("ArrowDown");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
// Make sure it was just a preview: close with escape
await press("escape");
await animationFrame();
expect(":iframe .test-options-target").not.toHaveAttribute("data-color");
});
test("should commit when validating with 'Enter'", async () => {
await prepareKeyboardSetup();
await press("ArrowRight");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
await press("ArrowDown");
await animationFrame();
expect.verifySteps(["apply"]); // Preview
expect(":iframe .test-options-target").toHaveAttribute("data-color");
await press("Enter"); // Validate
await animationFrame();
expect.verifySteps(["apply"]); // Commit
await press("escape");
await animationFrame();
expect(":iframe .test-options-target").toHaveAttribute("data-color");
});
});

View file

@ -0,0 +1,35 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { expect, test, describe } from "@odoo/hoot";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("should pass the context", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ params: { mainParam: testParam }, value }) {
expect.step(`customAction ${testParam} ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderContext action="'customAction'" actionParam="'myParam'">
<BuilderButton actionValue="'myValue'">MyAction</BuilderButton>
</BuilderContext>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container button").click();
// The function `apply` should be called twice (on hover (for preview), then, on click).
expect.verifySteps(["customAction myParam myValue", "customAction myParam myValue"]);
});

View file

@ -0,0 +1,192 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { expect, test, describe } from "@odoo/hoot";
import { queryOne } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
const { DateTime } = luxon;
describe.current.tags("desktop");
const TIME_TOLERANCE = 2;
// To avoid indeterminism in tests, we use a tolerance
function isExpectedDateTime({
dateString,
expectedDateTime = DateTime.now(),
tolerance = TIME_TOLERANCE,
}) {
const actualTimestamp = DateTime.fromFormat(dateString, "MM/dd/yyyy HH:mm:ss").toUnixInteger();
const expectedTimestamp = expectedDateTime.toUnixInteger();
const difference = Math.abs(actualTimestamp - expectedTimestamp);
return difference <= tolerance;
}
test("opens DateTimePicker on focus, closes on blur", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").click();
expect(".o_datetime_picker").toBeDisplayed();
await contains(".options-container").click();
expect(".o_datetime_picker").not.toHaveCount();
});
test("defaults to now if undefined", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'" acceptEmptyDate="false"/>`,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderCheckbox classAction="'checkbox-action'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
let dateString = queryOne(".we-bg-options-container input.o-hb-input-base").value;
expect(isExpectedDateTime({ dateString })).toBe(true);
await contains(".we-bg-options-container input.form-check-input").click();
dateString = queryOne(".we-bg-options-container input.o-hb-input-base").value;
expect(isExpectedDateTime({ dateString })).toBe(true);
});
test("defaults to last one when invalid date provided", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target" data-date="1554219400">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".we-bg-options-container input").toHaveValue("04/02/2019 16:36:40");
await contains(".we-bg-options-container input").edit("INVALID DATE");
expect(".we-bg-options-container input").toHaveValue("04/02/2019 16:36:40");
await contains(".we-bg-options-container input").edit("04/01/2019 10:00:00");
expect(".we-bg-options-container input").toHaveValue("04/01/2019 10:00:00");
await contains(".we-bg-options-container input").edit("INVALID DATE");
expect(".we-bg-options-container input").toHaveValue("04/01/2019 10:00:00");
});
test("defaults to last one when invalid date provided (date)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker type="'date'" dataAttributeAction="'date'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target" data-date="1554219400">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".we-bg-options-container input").toHaveValue("04/02/2019");
await contains(".we-bg-options-container input").edit("INVALID DATE");
expect(".we-bg-options-container input").toHaveValue("04/02/2019");
await contains(".we-bg-options-container input").edit("04/01/2019 10:00:00");
expect(".we-bg-options-container input").toHaveValue("04/01/2019");
await contains(".we-bg-options-container input").edit("INVALID DATE");
expect(".we-bg-options-container input").toHaveValue("04/01/2019");
});
test("defaults to now when no date is selected", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'" acceptEmptyDate="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").edit("04/01/2019 10:00:00");
expect(".we-bg-options-container input").toHaveValue("04/01/2019 10:00:00");
await contains(".we-bg-options-container input").edit("");
const dateString = queryOne(".we-bg-options-container input").value;
expect(isExpectedDateTime({ dateString })).toBe(true);
});
test("defaults to now when clicking on clear button", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'" acceptEmptyDate="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").edit("04/01/2019 10:00:00");
expect(".we-bg-options-container input").toHaveValue("04/01/2019 10:00:00");
for (let i = 0; i < 3; i++) {
await contains(".we-bg-options-container input").click();
await contains(".o_datetime_buttons button .fa-eraser").click();
await contains(".options-container").click();
const dateString = queryOne(".we-bg-options-container input").value;
expect(isExpectedDateTime({ dateString })).toBe(true);
}
});
test("selects a date and properly applies it", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'" acceptEmptyDate="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").click();
await contains(".o_date_item_cell.o_today + .o_date_item_cell").click();
await contains(".options-container").click();
const dateString = queryOne(".we-bg-options-container input").value;
const expectedDateTime = DateTime.now().plus({ days: 1 });
expect(isExpectedDateTime({ dateString, expectedDateTime })).toBe(true);
const expectedDateTimestamp = expectedDateTime.toUnixInteger();
const dateTimestamp = parseFloat(queryOne(":iframe .test-options-target").dataset.date);
expect(Math.abs(expectedDateTimestamp - dateTimestamp)).toBeLessThan(TIME_TOLERANCE);
});
test("selects a date and synchronize the input field, while still in preview", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'" acceptEmptyDate="false"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").click();
await contains(".o_date_item_cell.o_today + .o_date_item_cell").click();
const dateString = queryOne(".we-bg-options-container input").value;
const expectedDateTime = DateTime.now().plus({ days: 1 });
expect(isExpectedDateTime({ dateString, expectedDateTime })).toBe(true);
const expectedDateTimestamp = expectedDateTime.toUnixInteger();
const dateTimestamp = parseFloat(queryOne(":iframe .test-options-target").dataset.date);
expect(Math.abs(expectedDateTimestamp - dateTimestamp)).toBeLessThan(TIME_TOLERANCE);
});
test("edit a date with the datetime picker should correctly apply the mutation", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderDateTimePicker dataAttributeAction="'date'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-date="1554219400">b</div>
<div class="another-target">c</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container input").click();
await contains(".o_date_item_cell:contains('9')").click();
expect(".we-bg-options-container input").toHaveValue("04/09/2019 16:36:40");
await contains(".o_datetime_buttons .btn:contains('apply')").click();
expect(".we-bg-options-container input").toHaveValue("04/09/2019 16:36:40");
expect(":iframe .test-options-target").toHaveAttribute("data-date", "1554824200");
// refresh the Edit tab
await contains(":iframe .another-target").click();
await contains(":iframe .test-options-target").click();
expect(".we-bg-options-container input").toHaveValue("04/09/2019 16:36:40");
expect(":iframe .test-options-target").toHaveAttribute("data-date", "1554824200");
});

View file

@ -0,0 +1,332 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { BuilderList } from "@html_builder/core/building_blocks/builder_list";
import { expect, test, describe } from "@odoo/hoot";
import { Component, onError, xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
const defaultValue = { value: "75", title: "default title" };
const defaultValueStr = JSON.stringify(defaultValue).replaceAll('"', "'");
function defaultValueWithIds(ids) {
return ids.map((id) => ({
...defaultValue,
_id: id.toString(),
}));
}
test("writes a list of numbers to a data attribute", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderList
dataAttributeAction="'list'"
itemShape="{ value: 'number', title: 'text' }"
default="${defaultValueStr}"
/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
await contains(".we-bg-options-container input[type=number]").edit("35");
await contains(".we-bg-options-container input[type=text]").edit("a thing");
await contains(".we-bg-options-container .builder_list_add_item").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
value: "35",
title: "a thing",
_id: "0",
id: "a thing",
},
...defaultValueWithIds([1, 2]),
])
);
});
test("supports arbitrary number of text and number inputs on entries", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderList
dataAttributeAction="'list'"
itemShape="{ a: 'number', b: 'text', c: 'text', d: 'number' }"
default="{ a: '4', b: '3', c: '2', d: '1' }"
/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
expect(".we-bg-options-container input[type=number]").toHaveCount(2);
expect(".we-bg-options-container input[type=text]").toHaveCount(2);
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
a: "4",
b: "3",
c: "2",
d: "1",
_id: "0",
},
])
);
});
test("delete an item", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderList
dataAttributeAction="'list'"
itemShape="{ value: 'number', title: 'text' }"
default="${defaultValueStr}"
/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify(defaultValueWithIds([0]))
);
await contains(".we-bg-options-container .builder_list_remove_item").click();
expect(":iframe .test-options-target").toHaveAttribute("data-list", JSON.stringify([]));
});
test("reorder items", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderList
dataAttributeAction="'list'"
itemShape="{ value: 'number', title: 'text' }"
default="${defaultValueStr}"
/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
function expectOrder(ids) {
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify(defaultValueWithIds(ids))
);
}
expectOrder([0, 1, 2]);
const rowSelector = (id) => `.we-bg-options-container .o_row_draggable[data-id="${id}"]`;
const rowHandleSelector = (id) => `${rowSelector(id)} .o_handle_cell`;
await contains(rowHandleSelector(0)).dragAndDrop(rowSelector(1));
expectOrder([1, 0, 2]);
await contains(rowHandleSelector(1)).dragAndDrop(rowSelector(2));
expectOrder([0, 2, 1]);
await contains(rowHandleSelector(1)).dragAndDrop(rowSelector(0));
expectOrder([1, 0, 2]);
await contains(rowHandleSelector(2)).dragAndDrop(rowSelector(0));
expectOrder([1, 2, 0]);
await contains(rowHandleSelector(2)).dragAndDrop(rowSelector(0));
expectOrder([1, 0, 2]);
await contains(rowHandleSelector(0)).dragAndDrop(rowSelector(1));
expectOrder([0, 1, 2]);
});
async function testBuilderListFaultyProps(template) {
class Test extends Component {
static template = xml`${template}`;
static components = { BuilderList };
static props = ["*"];
setup() {
onError(() => {
expect.step("threw");
});
}
}
addBuilderOption({
selector: ".test-options-target",
Component: Test,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect.verifySteps(["threw"]);
}
test("throws error on empty shape", async () => {
await testBuilderListFaultyProps(`
<BuilderList
dataAttributeAction="'list'"
itemShape="{}"
default="{}"
/>
`);
});
test("throws error on wrong item shape types", async () => {
await testBuilderListFaultyProps(`
<BuilderList
dataAttributeAction="'list'"
itemShape="{ a: 'doesnotexist' }"
default="{ a: '1' }"
/>
`);
});
test("throws error on wrong properties default value", async () => {
await testBuilderListFaultyProps(`
<BuilderList
dataAttributeAction="'list'"
itemShape="{ a: 'number' }"
default="{ b: '1' }"
/>
`);
});
test("throws error on missing default value with a custom itemShape", async () => {
await testBuilderListFaultyProps(`
<BuilderList
dataAttributeAction="'list'"
itemShape="{ a: 'number', b: 'text' }"
/>
`);
});
test("throws error if itemShape contains reserved key '_id'", async () => {
await testBuilderListFaultyProps(`
<BuilderList
dataAttributeAction="'list'"
itemShape="{ _id: 'number' }"
default="{ _id: '1' }"
/>
`);
});
test("hides hiddenProperties from options", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderList
dataAttributeAction="'list'"
itemShape="{ a: 'number', b: 'text', c: 'number', d: 'text' }"
default="{ a: '4', b: 'three', c: '2', d: 'one' }"
hiddenProperties="['b', 'c']"
/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .builder_list_add_item").click();
expect(".we-bg-options-container input[type=number]").toHaveCount(1);
expect(".we-bg-options-container input[type=text]").toHaveCount(1);
await contains(".we-bg-options-container input[type=number]").edit("35");
await contains(".we-bg-options-container input[type=text]").edit("a thing");
await contains(".we-bg-options-container .builder_list_add_item").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
a: "35",
b: "three",
c: "2",
d: "a thing",
_id: "0",
id: "a thing",
},
{
a: "4",
b: "three",
c: "2",
d: "one",
_id: "1",
},
])
);
});
test("do not lose id when adjusting 'selected'", async () => {
class Test extends Component {
static template = xml`
<BuilderList
dataAttributeAction="'list'"
addItemTitle="'Add'"
itemShape="{ display_name: 'text', selected: 'boolean' }"
default="{ display_name: 'Extra', selected: false }"
records="availableRecords" />`;
static components = { BuilderList };
static props = ["*"];
setup() {
this.availableRecords = JSON.stringify([
{ id: 1, display_name: "A" },
{ id: 2, display_name: "B" },
]);
}
}
addBuilderOption({
selector: ".test-options-target",
Component: Test,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".we-bg-options-container .bl-dropdown-toggle").click();
await contains(".o_popover .o-hb-select-dropdown-item").click();
await contains(".we-bg-options-container .bl-dropdown-toggle").click();
await contains(".o_popover .o-hb-select-dropdown-item").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
id: 1,
display_name: "A",
_id: "0",
},
{
id: 2,
display_name: "B",
_id: "1",
},
])
);
await contains(".we-bg-options-container .o-hb-checkbox input").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
id: 1,
display_name: "A",
_id: "0",
selected: true,
},
{
id: 2,
display_name: "B",
_id: "1",
},
])
);
await contains(".we-bg-options-container .o-hb-checkbox input").click();
expect(":iframe .test-options-target").toHaveAttribute(
"data-list",
JSON.stringify([
{
id: 1,
display_name: "A",
_id: "0",
selected: false,
},
{
id: 2,
display_name: "B",
_id: "1",
},
])
);
});

View file

@ -0,0 +1,125 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame, Deferred } from "@odoo/hoot-mock";
import { xml } from "@odoo/owl";
import { contains, defineModels, fields, models, onRpc } from "@web/../tests/web_test_helpers";
import { delay } from "@web/core/utils/concurrency";
class Test extends models.Model {
_name = "test";
_records = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
name = fields.Char();
}
describe.current.tags("desktop");
defineModels([Test]);
test("many2many: find tag, select tag, unselect tag", async () => {
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderMany2Many dataAttributeAction="'test'" model="'test'" limit="10"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="test-options-target">b</div>`
);
const editableContent = getEditableContent();
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("table tr").toHaveCount(0);
expect(editableContent).toHaveInnerHTML(`<div class="test-options-target o-paragraph">b</div>`);
await contains(".btn.o-dropdown").click();
expect("input").toHaveCount(1);
await contains("input").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(3);
await contains("span.o-dropdown-item").click();
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" data-test="[{&quot;id&quot;:1,&quot;display_name&quot;:&quot;First&quot;,&quot;name&quot;:&quot;First&quot;}]">b</div>`
);
expect("table tr").toHaveCount(1);
await contains(".btn.o-dropdown").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(2);
await contains("span.o-dropdown-item").click();
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" data-test="[{&quot;id&quot;:1,&quot;display_name&quot;:&quot;First&quot;,&quot;name&quot;:&quot;First&quot;},{&quot;id&quot;:2,&quot;display_name&quot;:&quot;Second&quot;,&quot;name&quot;:&quot;Second&quot;}]">b</div>`
);
expect("table tr").toHaveCount(2);
await contains("button.fa-minus").click();
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" data-test="[{&quot;id&quot;:2,&quot;display_name&quot;:&quot;Second&quot;,&quot;name&quot;:&quot;Second&quot;}]">b</div>`
);
expect("table tr").toHaveCount(1);
expect("table input").toHaveValue("Second");
});
test("many2many: async load", async () => {
const defWillLoad = new Deferred();
const defDidApply = new Deferred();
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
addBuilderAction({
testAction: class extends BuilderAction {
static id = "testAction";
async load({ value }) {
expect.step("load");
await defWillLoad;
return value;
}
apply({ editingElement, value }) {
editingElement.dataset.test = value;
defDidApply.resolve();
}
getValue({ editingElement }) {
return editingElement.dataset.test;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderMany2Many action="'testAction'" model="'test'" limit="10"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="test-options-target">b</div>`
);
const editableContent = getEditableContent();
await contains(":iframe .test-options-target").click();
await contains(".btn.o-dropdown").click();
expect("input").toHaveCount(1);
await contains("input").click();
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(3);
await contains("span.o-dropdown-item").click();
expect.verifySteps(["load"]);
expect(editableContent).toHaveInnerHTML(`<div class="test-options-target o-paragraph">b</div>`);
defWillLoad.resolve();
await defDidApply;
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" data-test="[{&quot;id&quot;:1,&quot;display_name&quot;:&quot;First&quot;,&quot;name&quot;:&quot;First&quot;}]">b</div>`
);
});

View file

@ -0,0 +1,130 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { BaseOptionComponent, useGetItemValue } from "@html_builder/core/utils";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame, Deferred } from "@odoo/hoot-mock";
import { xml } from "@odoo/owl";
import { contains, defineModels, fields, models, onRpc } from "@web/../tests/web_test_helpers";
class Test extends models.Model {
_name = "test";
_records = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
name = fields.Char();
}
describe.current.tags("desktop");
defineModels([Test]);
test("many2one: async load", async () => {
const defWillLoad = new Deferred();
const defDidApply = new Deferred();
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
addBuilderAction({
testAction: class extends BuilderAction {
static id = "testAction";
setup() {
this.preview = false;
}
async load({ value }) {
expect.step("load");
await defWillLoad;
return value;
}
apply({ editingElement, value }) {
editingElement.dataset.test = value;
defDidApply.resolve();
}
getValue({ editingElement }) {
return editingElement.dataset.test;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderMany2One action="'testAction'" model="'test'" limit="10"/>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="test-options-target">b</div>`
);
const editableContent = getEditableContent();
await contains(":iframe .test-options-target").click();
await contains(".btn.o-dropdown").click();
expect("input").toHaveCount(1);
await contains("input").click();
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(3);
await contains("span.o-dropdown-item").click();
expect.verifySteps(["load"]);
expect(editableContent).toHaveInnerHTML(`<div class="test-options-target o-paragraph">b</div>`);
defWillLoad.resolve();
await defDidApply;
expect(editableContent).toHaveInnerHTML(
`<div class="test-options-target o-paragraph" data-test="{&quot;id&quot;:1,&quot;display_name&quot;:&quot;First&quot;,&quot;name&quot;:&quot;First&quot;}">b</div>`
);
});
test("dependency definition should not be outdated", async () => {
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
addBuilderAction({
testAction: class extends BuilderAction {
static id = "testAction";
apply({ editingElement, value }) {
editingElement.dataset.test = value;
}
getValue({ editingElement }) {
return editingElement.dataset.test;
}
},
});
class TestMany2One extends BaseOptionComponent {
static template = xml`
<BuilderMany2One action="'testAction'" model="'test'" limit="10" id="'test_many2one_opt'"/>
<BuilderRow t-if="getItemValueJSON('test_many2one_opt')?.id === 2"><span>Dependant</span></BuilderRow>
`;
setup() {
super.setup();
this.getItemValue = useGetItemValue();
}
getItemValueJSON(id) {
const value = this.getItemValue(id);
return value && JSON.parse(value);
}
}
addBuilderOption({
selector: ".test-options-target",
Component: TestMany2One,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await contains(".btn.o-dropdown").click();
await contains("span.o-dropdown-item:contains(First)").click();
expect("span:contains(Dependant)").toHaveCount(0);
await contains(".btn.o-dropdown").click();
await contains("span.o-dropdown-item:contains(Second)").click();
expect("span:contains(Dependant)").toHaveCount(1);
await contains(".btn.o-dropdown").click();
await contains("span.o-dropdown-item:contains(Third)").click();
expect("span:contains(Dependant)").toHaveCount(0);
});

View file

@ -0,0 +1,952 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { describe, expect, test } from "@odoo/hoot";
import {
advanceTime,
animationFrame,
clear,
click,
fill,
freezeTime,
queryFirst,
} from "@odoo/hoot-dom";
import { Deferred } from "@odoo/hoot-mock";
import { xml } from "@odoo/owl";
import { contains, defineModels, models } from "@web/../tests/web_test_helpers";
import { delay } from "@web/core/utils/concurrency";
describe.current.tags("desktop");
test("should get the initial value of the input", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ params }) {
expect.step(`customAction ${params}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
const input = queryFirst(".options-container input");
expect(input).toHaveValue("10");
});
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderNumberInput applyTo="'.my-custom-class'" action="'customAction'"/>`,
});
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue() {
return "10";
}
},
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect("[data-action-id='customAction']").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect("[data-action-id='customAction']").toHaveCount(1);
expect("[data-action-id='customAction'] input").toHaveValue("10");
});
test("input with classAction and styleAction", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput classAction="'testAction'" styleAction="'--custom-property'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("2");
expect(":iframe .test-options-target").toHaveStyle({
"--custom-property": "2",
});
});
test("input kept on async action", async () => {
const def = new Deferred();
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.dataset.test;
}
async apply({ editingElement, value }) {
await def;
editingElement.dataset.test = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target" data-test="1">Hello</div>`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("2");
await contains(".options-container input").fill(3, { confirm: false });
def.resolve();
await animationFrame();
expect(".options-container input").toHaveValue("23");
});
test("input should remove invalid char", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
setup() {
this.preview = false;
}
getValue({ editingElement }) {
return editingElement.dataset.test;
}
async apply({ editingElement, value }) {
editingElement.dataset.test = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
addBuilderOption({
selector: ".test-options-target-composable",
template: xml`<BuilderNumberInput action="'customAction'" composable="true"/>`,
});
await setupHTMLBuilder(
`<div class="test-options-target" data-test="1">Hello</div><div class="test-options-target-composable" data-test="2">World</div>`
);
// Single
await contains(":iframe .test-options-target").click();
await contains(".options-container:first input").edit("-1-2-");
await animationFrame();
expect(".options-container:first input").toHaveValue("-12");
await contains(".options-container:first input").edit("3-4-5");
await animationFrame();
expect(".options-container:first input").toHaveValue("345");
await contains(".options-container:first input").edit(" .$a?,6.b$?,7.$?c, ");
await animationFrame();
expect(".options-container:first input").toHaveValue("0.67");
// Composable
await contains(":iframe .test-options-target-composable").click();
await contains(".options-container:last input").edit("-12 12 -12 12");
await animationFrame();
expect(".options-container:last input").toHaveValue("-12 12 -12 12");
await contains(".options-container:last input").edit("3?/4.5 34,/?5");
await animationFrame();
expect(".options-container:last input").toHaveValue("34.5 34.5");
await contains(".options-container:last input").edit(" 6bc7 6//7 6$a7 6d7 ");
await animationFrame();
expect(".options-container:last input").toHaveValue("67 67 67 67");
});
describe("default value", () => {
test("should use the default value when there is no value onChange", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ value }) {
expect.step(`customAction ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" default="20"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
const input = queryFirst(".options-container input");
input.value = "";
input.dispatchEvent(new Event("input"));
await delay();
input.dispatchEvent(new Event("change"));
await delay();
expect.verifySteps(["customAction 20", "customAction 20"]);
expect(input).toHaveValue("20");
});
test("clear BuilderNumberInput without default value", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" />`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction'] input");
expect("[data-action-id='customAction'] input").toHaveValue("10");
await clear();
expect("[data-action-id='customAction'] input").toHaveValue("");
expect(":iframe .test-options-target").toHaveInnerHTML("0"); //Check that default value is used during preview
await click(".options-container");
expect("[data-action-id='customAction'] input").toHaveValue("0");
expect(":iframe .test-options-target").toHaveInnerHTML("0");
});
test("clear BuilderNumberInput with default value", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" default="1"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction'] input");
expect("[data-action-id='customAction'] input").toHaveValue("10");
await clear();
await click(".options-container");
expect("[data-action-id='customAction'] input").toHaveValue("1");
expect(":iframe .test-options-target").toHaveInnerHTML("1");
});
test("clear BuilderNumberInput with null default value", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerText;
}
apply({ editingElement, value }) {
editingElement.innerText = value;
if (value === null) {
editingElement.innerText = "10";
}
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" default="null"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction'] input");
expect("[data-action-id='customAction'] input").toHaveValue(10);
await contains("[data-action-id='customAction'] input").edit("5");
expect(":iframe .test-options-target").toHaveInnerHTML("5");
await clear();
await click(".options-container");
await animationFrame();
expect(":iframe .test-options-target").toHaveInnerHTML("10");
expect("[data-action-id='customAction'] input").toHaveValue("10");
});
});
describe("operations", () => {
test("should preview changes", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
await fill("2");
expect.verifySteps(["customAction 102"]);
expect(":iframe .test-options-target").toHaveInnerHTML("102");
expect(".o-snippets-top-actions .fa-undo").not.toBeEnabled();
expect(".o-snippets-top-actions .fa-repeat").not.toBeEnabled();
});
test("should commit changes", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
await fill("2");
expect.verifySteps(["customAction 102"]);
expect(":iframe .test-options-target").toHaveInnerHTML("102");
await click(document.body);
await animationFrame();
expect.verifySteps(["customAction 102"]);
expect(".o-snippets-top-actions .fa-undo").toBeEnabled();
expect(".o-snippets-top-actions .fa-repeat").not.toBeEnabled();
});
test("should commit changes after an undo", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await click(".options-container input");
await fill(2);
expect(":iframe .test-options-target").toHaveInnerHTML("102");
await click(document.body);
expect.verifySteps(["customAction 102", "customAction 102"]);
await animationFrame();
click(".o-snippets-top-actions .fa-undo");
await animationFrame();
expect(":iframe .test-options-target").toHaveInnerHTML("10");
await click(".options-container input");
await fill("2");
expect(":iframe .test-options-target").toHaveInnerHTML("102");
await click(document.body);
expect.verifySteps(["customAction 102", "customAction 102"]);
});
test("should not commit on input if no preview", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" preview="false"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await click(".options-container input");
await fill(2);
expect(":iframe .test-options-target").toHaveInnerHTML("10");
await click(document.body);
expect.verifySteps(["customAction 102"]);
expect(":iframe .test-options-target").toHaveInnerHTML("102");
});
});
describe("keyboard triggers", () => {
test("input should step up or down from by the step prop", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" step="2"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
// simulate arrow up
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime();
expect(":iframe .test-options-target").toHaveInnerHTML("12");
// simulate arrow down
await contains(".options-container input").keyDown("ArrowDown");
await advanceTime();
expect(":iframe .test-options-target").toHaveInnerHTML("10");
expect.verifySteps(["customAction 12", "customAction 10"]);
});
test("multi values: apply change on each value with up or down", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" composable="true"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10 4 0</div>
`);
await contains(":iframe .test-options-target").click();
// simulate arrow up
await contains(".options-container input").focus();
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime();
expect(":iframe .test-options-target").toHaveInnerHTML("11 5 1");
// simulate arrow down
await contains(".options-container input").keyDown("ArrowDown");
await advanceTime();
expect(":iframe .test-options-target").toHaveInnerHTML("10 4 0");
expect.verifySteps(["customAction 11 5 1", "customAction 10 4 0"]);
});
test("up on empty BuilderNumberInput gives 1", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.dataset.number;
}
apply({ editingElement, value }) {
editingElement.dataset.number = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" />`,
});
await setupHTMLBuilder(`<div class="test-options-target">Non empty div.</div>`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction'] input");
await clear();
expect("[data-action-id='customAction'] input").toHaveValue("");
await contains("[data-action-id='customAction'] input").keyDown("ArrowUp");
expect("[data-action-id='customAction'] input").toHaveValue("1");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "1");
});
test("down on empty BuilderNumberInput gives -1", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.dataset.number;
}
apply({ editingElement, value }) {
editingElement.dataset.number = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" />`,
});
await setupHTMLBuilder(`<div class="test-options-target">Non empty div.</div>`);
await contains(":iframe .test-options-target").click();
await click("[data-action-id='customAction'] input");
await clear();
expect("[data-action-id='customAction'] input").toHaveValue("");
await contains("[data-action-id='customAction'] input").keyDown("ArrowDown");
await animationFrame();
expect("[data-action-id='customAction'] input").toHaveValue("-1");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "-1");
});
test("apply preview on keydown and debounce commit operation", async () => {
freezeTime();
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").focus();
// Simulate a single keydown hold down for a while.
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(500); // Default browser delay between 1st & 2nd keydown.
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(50);
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(50);
expect(":iframe .test-options-target").toHaveInnerHTML("13");
// 3 previews
expect.verifySteps(["customAction 11", "customAction 12", "customAction 13"]);
await advanceTime(560); // Debounce = 550
// 1 commit
expect.verifySteps(["customAction 13"]);
});
});
describe("unit & saveUnit", () => {
test("should handle unit", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" unit="'px'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">5px</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
const input = queryFirst(".options-container input");
expect(input).toHaveValue("5");
await fill(1);
expect.verifySteps(["customAction 51px"]);
expect(":iframe .test-options-target").toHaveInnerHTML("51px");
});
test("should handle saveUnit", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" unit="'s'" saveUnit="'ms'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">5000ms</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
const input = queryFirst(".options-container input");
expect(input).toHaveValue("5");
await fill("7");
expect.verifySteps(["customAction 57000ms"]);
expect(":iframe .test-options-target").toHaveInnerHTML("57000ms");
});
test("should handle saveUnit even without explicit unit", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" unit="'s'" saveUnit="'ms'"/>`,
});
// note that 5000 has no unit of measure
await setupHTMLBuilder(`
<div class="test-options-target">5000</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
const input = queryFirst(".options-container input");
expect(input).toHaveValue("5");
});
test("should handle empty saveUnit", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" unit="'px'" saveUnit="''"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">5</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
const input = queryFirst(".options-container input");
expect(input).toHaveValue("5");
await fill(1);
expect.verifySteps(["customAction 51"]);
expect(":iframe .test-options-target").toHaveInnerHTML("51");
});
test("should handle savedUnit", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerText;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerText = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" unit="'s'" saveUnit="'ms'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">5s</div>
`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await click(".options-container input");
const input = queryFirst(".options-container input");
expect(input).toHaveValue("5");
await fill("7");
expect.verifySteps(["customAction 57000ms"]);
expect(":iframe .test-options-target").toHaveInnerHTML("57000ms");
});
});
describe("sanitized values", () => {
test("don't allow multi values by default", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("33 4 0", { instantly: true });
expect(".options-container input").toHaveValue("33");
expect(":iframe .test-options-target").toHaveInnerHTML("33");
});
test("use min when the given value is smaller", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ value }) {
expect.step(`customAction ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" min="0"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("-1", { instantly: true });
expect.verifySteps(["customAction 0", "customAction 0"]); // input, change
expect(".options-container input").toHaveValue("0");
});
test("use max when the given value is bigger", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ value }) {
expect.step(`customAction ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" max="10"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">3</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("11", { instantly: true });
await animationFrame();
expect.verifySteps(["customAction 0", "customAction 10"]); // input, change
expect(".options-container input").toHaveValue("10");
});
test("multi values: trailing space in BuilderNumberInput is ignored", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ value }) {
expect.step(`customAction ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput action="'customAction'" composable="true"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").fill("3 4 5 ", { instantly: true });
expect.verifySteps(["customAction 3 4 5", "customAction 3 4 5"]); // input, change
expect(".options-container input").toHaveValue("3 4 5");
});
test("after input, displayed value is cleaned to match only numbers", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit(" a&$*+>");
expect(".options-container input").toHaveValue("0");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "0");
});
test("after input, displayed value is cleaned to match only numbers (default=null)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'" default="null"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit(" a&$*+>");
expect(".options-container input").toHaveValue("");
expect(":iframe .test-options-target").not.toHaveAttribute("data-number");
});
test("after copy / pasting, displayed value is cleaned to match only numbers", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit(" a&$*-3+>", { instantly: true });
expect(".options-container input").toHaveValue("-3");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "-3");
});
test("accept decimal numbers", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("3.3");
expect(".options-container input").toHaveValue("3.3");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "3.3");
});
test("BuilderNumberInput transforms , into .", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("3,3");
expect(".options-container input").toHaveValue("3.3");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "3.3");
});
test("displays the correct value (no floating point precision error)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'" step="0.1"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("0.2");
expect(".options-container input").toHaveValue("0.2");
// simulate arrow keys
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime();
expect(".options-container input").toHaveValue("0.3");
await contains(".options-container input").keyDown("ArrowDown");
await advanceTime();
expect(".options-container input").toHaveValue("0.2");
});
test("rounds the number to 3 decimals", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target" data-number="10">Test</div>
`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("3.33333333333");
expect(".options-container input").toHaveValue("3.333");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "3.333");
await contains(".options-container input").edit("1.284778323");
expect(".options-container input").toHaveValue("1.285");
expect(":iframe .test-options-target").toHaveAttribute("data-number", "1.285");
});
test("should save font with full precision in rem and display to correct value in px", async () => {
class WebEditorAssets extends models.Model {
_name = "web_editor.assets";
make_scss_customization() {}
}
defineModels([WebEditorAssets]);
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderNumberInput dataAttributeAction="'number'" unit="'px'" saveUnit="'rem'"/>`,
});
await setupHTMLBuilder(`<div class="test-options-target">Test</div>`);
await contains(":iframe .test-options-target").click();
await contains(".options-container input").edit("19");
expect(".options-container input").toHaveValue("19");
});
});

View file

@ -0,0 +1,145 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { HistoryPlugin } from "@html_editor/core/history_plugin";
import { expect, test, describe } from "@odoo/hoot";
import { advanceTime, animationFrame, click, freezeTime, waitFor } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { delay } from "@web/core/utils/concurrency";
describe.current.tags("desktop");
test("should commit changes", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.innerHTML;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.innerHTML = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRange action="'customAction'" displayRangeValue="true"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
const input = await waitFor(".options-container input");
input.value = 50;
input.dispatchEvent(new Event("input"));
await delay();
input.dispatchEvent(new Event("change"));
await delay();
expect.verifySteps(["customAction 50", "customAction 50"]);
expect(":iframe .test-options-target").toHaveInnerHTML("50");
await click(document.body);
await animationFrame();
expect(".o-snippets-top-actions .fa-undo").toBeEnabled();
expect(".o-snippets-top-actions .fa-repeat").not.toBeEnabled();
});
test("range input should step up or down with arrow keys", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.textContent;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.textContent = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRange action="'customAction'" step="2" displayRangeValue="true"/>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
// Simulate ArrowUp
await contains(".options-container input").keyDown("ArrowUp");
expect(":iframe .test-options-target").toHaveInnerHTML("12");
// Simulate ArrowRight
await contains(".options-container input").keyDown("ArrowRight");
expect(":iframe .test-options-target").toHaveInnerHTML("14");
// Simulate ArrowDown
await contains(".options-container input").keyDown("ArrowDown");
expect(":iframe .test-options-target").toHaveInnerHTML("12");
// Simulate ArrowLeft
await contains(".options-container input").keyDown("ArrowLeft");
expect(":iframe .test-options-target").toHaveInnerHTML("10");
expect.verifySteps([
"customAction 12",
"customAction 14",
"customAction 12",
"customAction 10",
]);
});
test("keeping an arrow key pressed should commit only once", async () => {
patchWithCleanup(HistoryPlugin.prototype, {
makePreviewableAsyncOperation(...args) {
const res = super.makePreviewableAsyncOperation(...args);
const commit = res.commit;
res.commit = async (...args) => {
expect.step(`commit ${args[0][0].actionValue}`);
commit(...args);
};
return res;
},
});
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue({ editingElement }) {
return editingElement.textContent;
}
apply({ editingElement, value }) {
expect.step(`customAction ${value}`);
editingElement.textContent = value;
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRange action="'customAction'" step="2" displayRangeValue="true"/>`,
});
freezeTime();
await setupHTMLBuilder(`
<div class="test-options-target">10</div>
`);
await contains(":iframe .test-options-target").click();
// Simulate a long press on ArrowUp
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(500);
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(50);
await contains(".options-container input").keyDown("ArrowUp");
await advanceTime(50);
await contains(".options-container input").keyDown("ArrowUp");
expect(":iframe .test-options-target").toHaveInnerHTML("18");
expect.verifySteps([
"customAction 12",
"customAction 14",
"customAction 16",
"customAction 18",
]);
await advanceTime(550);
expect.verifySteps(["commit 18", "customAction 18"]);
});

View file

@ -0,0 +1,318 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { describe, expect, test } from "@odoo/hoot";
import {
advanceTime,
animationFrame,
hover,
queryAllTexts,
queryOne,
waitFor,
} from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains, defineStyle } from "@web/../tests/web_test_helpers";
import { OPEN_DELAY } from "@web/core/tooltip/tooltip_service";
function reapplyCollapseTransition() {
defineStyle(/* css */ `
hoot-fixture:not(.allow-transitions) * .hb-collapse-content {
transition: height 0.35s ease !important;
}
`);
}
describe.current.tags("desktop");
test("show row title", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'my label'">row text</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".hb-row .text-nowrap").toHaveText("my label");
});
test("show row tooltip", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'my label'" tooltip="'my tooltip'">row text</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".hb-row .text-nowrap").toHaveText("my label");
expect(".o-tooltip").not.toHaveCount();
await hover(".hb-row .text-nowrap");
await advanceTime(OPEN_DELAY);
await waitFor(".o-tooltip");
expect(".o-tooltip").toHaveText("my tooltip");
await contains(":iframe .test-options-target").hover();
expect(".o-tooltip").not.toHaveCount();
});
test("hide empty row and display row with content", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 2'">
<BuilderButton applyTo="':not(.my-custom-class)'" classAction="'test'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 3'">
<BuilderButton applyTo="'.my-custom-class'" classAction="'test'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="parent-target"><div class="child-target">b</div></div>`);
const selectorRowLabel = ".options-container .hb-row:not(.d-none) .hb-row-label";
await contains(":iframe .parent-target").click();
expect(queryAllTexts(selectorRowLabel)).toEqual(["Row 1", "Row 2"]);
await contains("[data-class-action='my-custom-class']").click();
expect(queryAllTexts(selectorRowLabel)).toEqual(["Row 1", "Row 3"]);
});
/* ================= Collapse template ================= */
const collapseOptionTemplate = ({
dependency = false,
expand = false,
observeCollapseContent = false,
} = {}) => xml`
<BuilderRow label="'Test Collapse'" expand="${expand}" observeCollapseContent="${observeCollapseContent}">
<BuilderButton classAction="'a'" ${
dependency ? "id=\"'test_opt'\"" : ""
}>A</BuilderButton>
<t t-set-slot="collapse">
<BuilderRow level="1" label="'B'" ${
dependency ? "t-if=\"isActiveItem('test_opt')\"" : ""
}>
<BuilderButton classAction="'b'">B</BuilderButton>
</BuilderRow>
</t>
</BuilderRow>`;
describe("BuilderRow with collapse content", () => {
test("expand=false is collapsed by default", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate(),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
});
test("expand=true is expanded by default", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({ dependency: false, expand: true }),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
});
test("Toggler button is not visible if no dependency is active", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({
dependency: true,
observeCollapseContent: true,
}),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(0);
});
test("expand=true works when a dependency becomes active", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({ dependency: true, expand: true }),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
await contains(".options-container button[data-class-action='a']").click();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(1);
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
expect(".options-container button[data-class-action='b']").toBeVisible();
});
test("Collapse works with several dependencies", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Test Collapse'" expand="true">
<BuilderSelect>
<BuilderSelectItem classAction="'a'" id="'test_opt'">A</BuilderSelectItem>
<BuilderSelectItem classAction="'c'" id="'random_opt'">C</BuilderSelectItem>
</BuilderSelect>
<t t-set-slot="collapse">
<BuilderRow level="1" t-if="isActiveItem('test_opt')" label="'B'">
<BuilderButton classAction="'b'">B</BuilderButton>
</BuilderRow>
<BuilderRow level="1" t-if="isActiveItem('random_opt')" label="'D'">
<BuilderButton classAction="'d'">D</BuilderButton>
</BuilderRow>
</t>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(0);
await contains(".options-container .dropdown-toggle").click();
await contains(".dropdown-menu [data-class-action='a']").click();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(1);
expect(".options-container button[data-class-action='b']").toBeVisible();
expect(".options-container button[data-class-action='d']").not.toHaveCount();
await contains(".options-container .dropdown-toggle").click();
await contains(".dropdown-menu [data-class-action='c']").click();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(1);
expect(".options-container button[data-class-action='b']").not.toHaveCount();
expect(".options-container button[data-class-action='d']").toBeVisible();
});
test("Click on toggler collapses / expands the BuilderRow", async () => {
reapplyCollapseTransition();
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate(),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").toHaveCount(0);
await contains(".o_hb_collapse_toggler:not(.d-none)").click();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
expect(".options-container button[data-class-action='b']").toBeVisible();
await contains(".o_hb_collapse_toggler:not(.d-none)").click();
advanceTime(400); // wait for the collapse transition to be over
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").toHaveCount(0);
});
test("Click on toggler collapses / expands the BuilderRow (with observeCollapseContent)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({ observeCollapseContent: true }),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").not.toBeVisible();
await contains(".o_hb_collapse_toggler:not(.d-none)").click();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
expect(".options-container button[data-class-action='b']").toBeVisible();
await contains(".o_hb_collapse_toggler:not(.d-none)").click();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
advanceTime(400); // wait for the collapse transition to be over
await animationFrame();
expect(".options-container button[data-class-action='b']").not.toBeVisible();
});
test("Click header row's label collapses / expands the BuilderRow", async () => {
reapplyCollapseTransition();
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate(),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").toHaveCount(0);
await contains("[data-label='Test Collapse'] span:contains('Test Collapse')").click();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
expect(".options-container button[data-class-action='b']").toBeVisible();
await contains("[data-label='Test Collapse'] span:contains('Test Collapse')").click();
advanceTime(400); // wait for the collapse transition to be over
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").toHaveCount(0);
});
test("Click header row's label collapses / expands the BuilderRow (with observeCollapseContent)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({ observeCollapseContent: true }),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
expect(".options-container button[data-class-action='b']").not.toBeVisible();
await contains("[data-label='Test Collapse'] span:contains('Test Collapse')").click();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveClass("active");
expect(".options-container button[data-class-action='b']").toBeVisible();
await contains("[data-label='Test Collapse'] span:contains('Test Collapse')").click();
expect(".o_hb_collapse_toggler:not(.d-none)").not.toHaveClass("active");
advanceTime(400); // wait for the collapse transition to be over
await animationFrame();
expect(".options-container button[data-class-action='b']").not.toBeVisible();
});
test("Two BuilderRows with collapse content on the same option are toggled independently", async () => {
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate({ dependency: true, expand: true }),
});
addBuilderOption({
selector: ".test-options-target",
template: collapseOptionTemplate(),
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(1);
await contains(".options-container [data-class-action='a']:first").click();
await animationFrame();
expect(".o_hb_collapse_toggler:not(.d-none)").toHaveCount(2);
expect(".o_hb_collapse_toggler:not(.d-none):first").toHaveClass("active");
expect(".o_hb_collapse_toggler:not(.d-none):not(.d-none):last").not.toHaveClass("active");
await contains(".options-container .o_hb_collapse_toggler:not(.d-none):last").click();
expect(".o_hb_collapse_toggler:not(.d-none):first").toHaveClass("active");
expect(".o_hb_collapse_toggler:not(.d-none):last").toHaveClass("active");
await contains(".options-container .o_hb_collapse_toggler:not(.d-none):first").click();
expect(".o_hb_collapse_toggler:not(.d-none):first").not.toHaveClass("active");
expect(".o_hb_collapse_toggler:not(.d-none):last").toHaveClass("active");
});
});
describe.tags("desktop");
describe("HTML builder tests", () => {
test("add tooltip when label is too long", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'Supercalifragilisticexpalidocious'">Palais chatouille</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
await hover("[data-label='Supercalifragilisticexpalidocious'] .text-truncate");
await advanceTime(OPEN_DELAY);
await waitFor(".o-tooltip");
const label = queryOne("[data-label='Supercalifragilisticexpalidocious'] .text-truncate");
expect(label.scrollWidth).toBeGreaterThan(label.clientWidth); // the text is longer than the available width.
expect(".o-tooltip").toHaveText("Supercalifragilisticexpalidocious");
await contains(":iframe .test-options-target").hover();
expect(".o-tooltip").toHaveCount(0);
});
});

View file

@ -0,0 +1,336 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { setSelection } from "@html_editor/../tests/_helpers/selection";
import { expect, test, describe } from "@odoo/hoot";
import {
animationFrame,
click,
press,
queryAllTexts,
queryFirst,
runAllTimers,
tick,
} from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("call a specific action with some params and value (BuilderSelectItem)", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ params: { mainParam: testParam }, value }) {
expect.step(`customAction ${testParam} ${value}`);
}
},
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderSelect>
<BuilderSelectItem action="'customAction'" actionParam="'myParam'" actionValue="'myValue'">MyAction</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
await click(".we-bg-options-container .dropdown");
await animationFrame();
expect(".popover [data-action-id='customAction']").toHaveText("MyAction");
await click(".popover [data-action-id='customAction']");
await animationFrame();
// The function `apply` should be called twice (on hover (for preview), then, on click).
expect.verifySteps(["customAction myParam myValue", "customAction myParam myValue"]);
});
test("set the label of the select from the active select item and be updated on undo/redo", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderSelect attributeAction="'customAttribute'">
<BuilderSelectItem attributeActionValue="null">None</BuilderSelectItem>
<BuilderSelectItem attributeActionValue="'a'">A</BuilderSelectItem>
<BuilderSelectItem attributeActionValue="'b'">B</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test-options-target" customAttribute="a">x</div>`);
setSelection({
anchorNode: queryFirst(":iframe .test-options-target").childNodes[0],
anchorOffset: 0,
});
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".we-bg-options-container .dropdown").toHaveText("A");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-overlay-item [data-attribute-action-value='b']").click();
expect(".we-bg-options-container .dropdown").toHaveText("B");
await animationFrame();
expect(".o-overlay-item [data-attribute-action-value='b']").not.toHaveCount();
await contains(".o-snippets-top-actions .fa-undo").click();
expect(".we-bg-options-container .dropdown").toHaveText("A");
await contains(".o-snippets-top-actions .fa-repeat").click();
expect(".we-bg-options-container .dropdown").toHaveText("B");
});
test("consider the priority of the select item", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderSelect>
<BuilderSelectItem classAction="''">None</BuilderSelectItem>
<BuilderSelectItem classAction="'a'">A</BuilderSelectItem>
<BuilderSelectItem classAction="'a b'">A B</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test-options-target a">x</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(".we-bg-options-container .dropdown").toHaveText("A");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-overlay-item [data-class-action='']").click();
expect(".we-bg-options-container .dropdown").toHaveText("None");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-overlay-item [data-class-action='a b']").click();
expect(".we-bg-options-container .dropdown").toHaveText("A B");
});
test("hide/display BuilderSelect based on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderSelect applyTo="'.my-custom-class'">
<BuilderSelectItem classAction="'a'">A</BuilderSelectItem>
<BuilderSelectItem classAction="'b'">B</BuilderSelectItem>
</BuilderSelect>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target b">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect(".options-container button.dropdown-toggle").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target b o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect(".options-container button.dropdown-toggle").toHaveCount(1);
await runAllTimers();
expect(".options-container button.dropdown-toggle").toHaveText("B");
});
test("hide/display BuilderSelectItem base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderSelect>
<BuilderSelectItem classAction="'a'">A</BuilderSelectItem>
<BuilderSelectItem applyTo="'.my-custom-class'" classAction="'b'">B</BuilderSelectItem>
<BuilderSelectItem classAction="'c'">C</BuilderSelectItem>
</BuilderSelect>`,
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target o-paragraph">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect(".options-container button.dropdown-toggle").toHaveCount(1);
await contains(".options-container button.dropdown-toggle").click();
expect(queryAllTexts(".o-dropdown--menu div.o-dropdown-item")).toEqual(["A", "C"]);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
await contains(".options-container button.dropdown-toggle").click();
expect(queryAllTexts(".o-dropdown--menu div.o-dropdown-item")).toEqual(["A", "B", "C"]);
});
test("hide/display BuilderSelect base on applyTo in BuilderSelectItem", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderSelect>
<BuilderSelectItem applyTo="'.my-custom-class'" classAction="'a'">A</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="parent-target"><div class="child-target b">b</div></div>`);
await contains(":iframe .parent-target").click();
expect(".options-container button.dropdown-toggle").not.toBeVisible();
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container button.dropdown-toggle").toBeVisible();
});
test("use BuilderSelect with styleAction", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`
<BuilderSelect styleAction="'border-style'">
<BuilderSelectItem styleActionValue="'dotted'">dotted</BuilderSelectItem>
<BuilderSelectItem styleActionValue="'inset'">inset</BuilderSelectItem>
<BuilderSelectItem styleActionValue="'none'">none</BuilderSelectItem>
</BuilderSelect>`,
});
const { getEditableContent } = await setupHTMLBuilder(`<div class="parent-target">b</div>`);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(".we-bg-options-container .dropdown").toHaveText("none");
await contains(".options-container button.dropdown-toggle").click();
expect(queryAllTexts(".o-dropdown--menu div.o-dropdown-item")).toEqual([
"dotted",
"inset",
"none",
]);
await contains(".o-dropdown--menu div.o-dropdown-item:contains(dotted)").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target o-paragraph" style="border-style: dotted;">b</div>`
);
expect(".we-bg-options-container .dropdown").toHaveText("dotted");
});
test("do not put inline style on an element which already has this style through css stylesheets", async () => {
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect applyTo="'hr'" styleAction="'border-top-style'">
<BuilderSelectItem styleActionValue="'dotted'">dotted</BuilderSelectItem>
<BuilderSelectItem styleActionValue="'inset'">inset</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`
<div class="test">
<hr class="w-100">
</div>
`);
await contains(":iframe .test").click();
expect(".we-bg-options-container .dropdown").toHaveText("inset");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-dropdown--menu div.o-dropdown-item:contains('dotted')").click();
expect(":iframe hr").toHaveStyle({ "border-top-style": "dotted" });
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-dropdown--menu div.o-dropdown-item:contains('inset')").click();
expect(":iframe hr").not.toHaveStyle("border-top-style", { inline: true });
});
test("revert a preview when cancelling a BuilderSelect by clicking outside of it", async () => {
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect dataAttributeAction="'choice'">
<BuilderSelectItem dataAttributeActionValue="'0'">0</BuilderSelectItem>
<BuilderSelectItem dataAttributeActionValue="'1'">1</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test">Test</div>`);
await contains(":iframe .test").click();
expect(":iframe .test").not.toHaveAttribute("data-choice");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-dropdown--menu div.o-dropdown-item:contains('0')").hover();
expect(":iframe .test").toHaveAttribute("data-choice", "0");
await click(".we-bg-options-container");
expect(":iframe .test").not.toHaveAttribute("data-choice");
});
test("revert a preview when cancelling a BuilderSelect with escape", async () => {
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect dataAttributeAction="'choice'">
<BuilderSelectItem dataAttributeActionValue="'0'">0</BuilderSelectItem>
<BuilderSelectItem dataAttributeActionValue="'1'">1</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test">Test</div>`);
await contains(":iframe .test").click();
expect(":iframe .test").not.toHaveAttribute("data-choice");
await contains(".we-bg-options-container .dropdown").click();
await contains(".o-dropdown--menu div.o-dropdown-item:contains('0')").hover();
expect(":iframe .test").toHaveAttribute("data-choice", "0");
await press("escape");
expect(":iframe .test").not.toHaveAttribute("data-choice");
});
test("preview when cycling through options with the keyboard", async () => {
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect dataAttributeAction="'choice'">
<BuilderSelectItem dataAttributeActionValue="'0'">0</BuilderSelectItem>
<BuilderSelectItem dataAttributeActionValue="'1'">1</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test">Test</div>`);
await contains(":iframe .test").click();
expect(":iframe .test").not.toHaveAttribute("data-choice");
await contains(".we-bg-options-container .dropdown").press("enter");
await press("arrowdown");
expect(":iframe .test").toHaveAttribute("data-choice", "0");
});
test("revert a preview selected with the keyboard when cancelling with escape", async () => {
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect dataAttributeAction="'choice'">
<BuilderSelectItem dataAttributeActionValue="'0'">0</BuilderSelectItem>
<BuilderSelectItem dataAttributeActionValue="'1'">1</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test">Test</div>`);
await contains(":iframe .test").click();
expect(":iframe .test").not.toHaveAttribute("data-choice");
await contains(".we-bg-options-container .dropdown").press("enter");
await press("arrowdown");
expect(".o-dropdown--menu div.o-dropdown-item:contains('0')").toBeFocused();
await press("escape");
await tick();
expect(":iframe .test").not.toHaveAttribute("data-choice");
});
test("isApplied shouldn't be called when the element is removed from the DOM", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
isApplied({ editingElement: el }) {
expect(el.isConnected).toBe(true);
}
},
});
addBuilderOption({
selector: ".test",
template: xml`
<BuilderSelect action="'customAction'">
<BuilderSelectItem actionParam="'0'">0</BuilderSelectItem>
<BuilderSelectItem actionParam="'1'">1</BuilderSelectItem>
</BuilderSelect>`,
});
await setupHTMLBuilder(`<div class="test">Test</div>`);
await contains(":iframe .test").click();
await contains(".fa-trash ").click();
expect(":iframe .test").toHaveCount(0);
});

View file

@ -0,0 +1,49 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { expect, test, describe } from "@odoo/hoot";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("hide/display base on applyTo", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>`,
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderTextInput applyTo="'.my-custom-class'" action="'customAction'"/>`,
});
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
getValue() {
return "customValue";
}
},
});
const { getEditableContent } = await setupHTMLBuilder(
`<div class="parent-target"><div class="child-target">b</div></div>`
);
const editableContent = getEditableContent();
await contains(":iframe .parent-target").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph">b</div></div>`
);
expect("[data-class-action='my-custom-class']").not.toHaveClass("active");
expect("[data-action-id='customAction']").toHaveCount(0);
await contains("[data-class-action='my-custom-class']").click();
expect(editableContent).toHaveInnerHTML(
`<div class="parent-target"><div class="child-target o-paragraph my-custom-class">b</div></div>`
);
expect("[data-class-action='my-custom-class']").toHaveClass("active");
expect("[data-action-id='customAction']").toHaveCount(1);
expect("[data-action-id='customAction'] input").toHaveValue("customValue");
});

View file

@ -0,0 +1,87 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { xml } from "@odoo/owl";
import { contains, defineModels, fields, models, onRpc } from "@web/../tests/web_test_helpers";
import { delay } from "@web/core/utils/concurrency";
class Test extends models.Model {
_name = "test";
_records = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
name = fields.Char();
}
class TestBase extends models.Model {
_name = "test.base";
_records = [
{
id: 1,
rel: [],
},
];
rel = fields.Many2many({
relation: "test",
string: "Test",
});
}
describe.current.tags("desktop");
defineModels([Test, TestBase]);
test("model many2many: find tag, select tag, unselect tag", async () => {
onRpc("test", "name_search", () => [
[1, "First"],
[2, "Second"],
[3, "Third"],
]);
addBuilderOption({
selector: ".test-options-target",
template: xml`<ModelMany2Many baseModel="'test.base'" m2oField="'rel'" recordId="1"/>`,
});
const { getEditor } = await setupHTMLBuilder(
`<div class="test-options-target" data-res-model="test.base" data-res-id="1">b</div>`
);
await contains(":iframe .test-options-target").click();
const modelEdit = getEditor().shared.cachedModel.useModelEdit({
model: "test.base",
recordId: 1,
});
expect(".options-container").toBeDisplayed();
expect("table tr").toHaveCount(0);
expect(modelEdit.get("rel")).toEqual([]);
await contains(".btn.o-dropdown").click();
expect("input").toHaveCount(1);
await contains("input").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(3);
await contains("span.o-dropdown-item").click();
expect(modelEdit.get("rel")).toEqual([{ id: 1, name: "First", display_name: "First" }]);
expect("table tr").toHaveCount(1);
await contains(".btn.o-dropdown").click();
await delay(300); // debounce
await animationFrame();
expect("span.o-dropdown-item").toHaveCount(2);
await contains("span.o-dropdown-item").click();
expect(modelEdit.get("rel")).toEqual([
{ id: 1, name: "First", display_name: "First" },
{ id: 2, name: "Second", display_name: "Second" },
]);
expect("table tr").toHaveCount(2);
await contains("button.fa-minus").click();
expect(modelEdit.get("rel")).toEqual([{ id: 2, name: "Second", display_name: "Second" }]);
expect("table tr").toHaveCount(1);
expect("table input").toHaveValue("Second");
await contains(".o-snippets-tabs button").click();
await contains(":iframe .test-options-target").click();
expect("table tr").toHaveCount(1);
expect("table input").toHaveValue("Second");
});

View file

@ -0,0 +1,241 @@
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
import { describe, expect, test } from "@odoo/hoot";
import { fill } from "@odoo/hoot-dom";
import { xml } from "@odoo/owl";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
describe("classAction", () => {
test("should reset when cliking on an empty classAction", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton classAction="''"/>
<BuilderButton classAction="'x'"/>
</BuilderButtonGroup>
`,
});
await setupHTMLBuilder(`<div class="test-options-target x">a</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("[data-class-action='x']").toHaveClass("active");
await contains("[data-class-action='']").click();
expect(":iframe .test-options-target").not.toHaveClass("x");
});
test("set multiples classes", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup>
<BuilderButton classAction="'x'"/>
<BuilderButton classAction="'x y z'"/>
</BuilderButtonGroup>
`,
});
await setupHTMLBuilder(`<div class="test-options-target x">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("[data-class-action='x']").toHaveClass("active");
expect("[data-class-action='x y z']").not.toHaveClass("active");
await contains("[data-class-action='x y z']").click();
expect(":iframe .test-options-target").toHaveClass("x y z");
expect("[data-class-action='x']").not.toHaveClass("active");
expect("[data-class-action='x y z']").toHaveClass("active");
await contains("[data-class-action='x']").click();
expect(":iframe .test-options-target").toHaveClass("x");
expect(":iframe .test-options-target").not.toHaveClass("y z");
expect("[data-class-action='x']").toHaveClass("active");
expect("[data-class-action='x y z']").not.toHaveClass("active");
});
test("toggle class when not inside a BuilderButtonGroup", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton classAction="'x'"/>
<BuilderButtonGroup>
<BuilderButton classAction="'y'"/>
</BuilderButtonGroup>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
await contains("[data-class-action='x']").click();
expect(":iframe .test-options-target").toHaveClass("x");
await contains("[data-class-action='x']").click();
expect(":iframe .test-options-target").not.toHaveClass("x");
await contains("[data-class-action='y']").click();
expect(":iframe .test-options-target").toHaveClass("y");
await contains("[data-class-action='y']").click();
expect(":iframe .test-options-target").toHaveClass("y");
});
});
describe("styleAction", () => {
test("should set a plain style", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderNumberInput styleAction="'width'" unit="'px'"
/>
`,
});
await setupHTMLBuilder(`<div class="test-options-target" style="width: 10px;">a</div>`);
await contains(":iframe .test-options-target").click();
expect("input").toHaveValue("10");
expect(".options-container").toBeDisplayed();
expect(":iframe .test-options-target").toHaveStyle({ width: "10px" });
expect(":iframe .test-options-target").toHaveAttribute("style", "width: 10px;"); // no !important
await contains("input").click();
await fill("1");
expect("input").toHaveValue("101");
expect(":iframe .test-options-target").toHaveStyle({ width: "101px" });
expect(":iframe .test-options-target").toHaveAttribute("style", "width: 101px;"); // no !important
await contains("input").edit("");
expect(":iframe .test-options-target").toHaveAttribute("style", "width: 0px;");
});
test("should set a style with its associated class", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderNumberInput styleAction="{ mainParam: 'border-width', extraClass: 'border' }" unit="'px'" min="0" composable="true"
/>
`,
});
await setupHTMLBuilder(`<div class="test-options-target border">a</div>`, {
styleContent: ".border { border: solid; border-width: 1px !important; }",
});
await contains(":iframe .test-options-target").click();
expect("input").toHaveValue("1");
expect(".options-container").toBeDisplayed();
expect(":iframe .test-options-target").not.toHaveAttribute("style");
await contains("input").click();
await fill("2");
expect("input").toHaveValue("12");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-width: 12px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("0");
expect(":iframe .test-options-target").not.toHaveAttribute("style");
expect(":iframe .test-options-target").not.toHaveClass("border");
await contains("input").edit("1");
expect(":iframe .test-options-target").toHaveAttribute("style", "");
expect(":iframe .test-options-target").toHaveClass("border");
});
test("should set a composite style with its associated class", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderNumberInput styleAction="{ mainParam: 'border-width', extraClass: 'border' }" unit="'px'" min="0" composable="true"
/>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">a</div>`, {
styleContent: ".border { border: solid; border-width: 1px !important; }",
});
await contains(":iframe .test-options-target").click();
expect("input").toHaveValue("0");
expect(".options-container").toBeDisplayed();
expect(":iframe .test-options-target").not.toHaveAttribute("style");
await contains("input").edit("10");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-width: 10px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("10 20");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-width: 10px 20px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("10 20 30");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-width: 10px 20px 30px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("10 20 30 40");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-width: 10px 20px 30px 40px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("10 1");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-bottom-width: 10px !important; border-top-width: 10px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("1 10");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-left-width: 10px !important; border-right-width: 10px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("1 10 10");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-left-width: 10px !important; border-bottom-width: 10px !important; border-right-width: 10px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
await contains("input").edit("1 1 1 10");
expect(":iframe .test-options-target").toHaveAttribute(
"style",
"border-left-width: 10px !important;"
);
expect(":iframe .test-options-target").toHaveClass("border");
});
test("button isApplied is properly computed with percentage width values", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButtonGroup styleAction="'width'">
<BuilderButton styleActionValue="''">Default</BuilderButton>
<BuilderButton styleActionValue="'50%'">50%</BuilderButton>
</BuilderButtonGroup>
`,
});
await setupHTMLBuilder(`<div class="test-options-target x">a</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeDisplayed();
expect("[data-style-action-value='']").toHaveClass("active");
expect("[data-style-action-value='50%']").not.toHaveClass("active");
expect(":iframe .test-options-target").toHaveOuterHTML(
`<div class="test-options-target x o-paragraph"> a </div>`
);
await contains("[data-style-action-value='50%']").click();
expect(":iframe .test-options-target").toHaveOuterHTML(
`<div class="test-options-target x o-paragraph" style="width: 50% !important;"> a </div>`
);
expect("[data-style-action-value='']").not.toHaveClass("active");
expect("[data-style-action-value='50%']").toHaveClass("active");
});
});

View file

@ -0,0 +1,701 @@
import {
addBuilderAction,
addBuilderOption,
setupHTMLBuilder,
} from "@html_builder/../tests/helpers";
import { BuilderAction } from "@html_builder/core/builder_action";
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
import { OptionsContainer } from "@html_builder/sidebar/option_container";
import { setContent, setSelection } from "@html_editor/../tests/_helpers/selection";
import { redo, undo } from "@html_editor/../tests/_helpers/user_actions";
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
import { Component, onWillStart, xml } from "@odoo/owl";
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("Open custom tab with template option", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 1'">
Test
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target" data-name="Yop">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(queryAllTexts(".options-container > div")).toEqual(["Yop", "Row 1\nTest"]);
});
test("Open custom tab with Component option", async () => {
class TestOption extends BaseOptionComponent {
static template = xml`
<BuilderRow label="'Row 1'">
Test
</BuilderRow>`;
static props = {};
}
addBuilderOption({
selector: ".test-options-target",
Component: TestOption,
});
await setupHTMLBuilder(`<div class="test-options-target" data-name="Yop">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(queryAllTexts(".options-container > div")).toEqual(["Yop", "Row 1\nTest"]);
});
test("OptionContainer should display custom title", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 1'">
Test
</BuilderRow>`,
title: "My custom title",
});
await setupHTMLBuilder(`<div class="test-options-target" data-name="Yop">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(queryAllTexts(".options-container > div")).toEqual(["My custom title", "Row 1\nTest"]);
});
test("Don't display option base on exclude", async () => {
addBuilderOption({
selector: ".test-options-target",
exclude: ".test-exclude",
template: xml`<BuilderRow label="'Row 1'">a</BuilderRow>`,
});
addBuilderOption({
selector: ".test-options-target",
exclude: ".test-exclude-2",
template: xml`<BuilderRow label="'Row 2'">b</BuilderRow>`,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`<BuilderRow label="'Row 3'">
<BuilderButton classAction="'test-exclude-2'">c</BuilderButton>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="test-options-target test-exclude">b</div>`);
await contains(":iframe .test-options-target").click();
expect(queryAllTexts(".options-container .hb-row")).toEqual(["Row 2\nb", "Row 3\nc"]);
await contains("[data-class-action='test-exclude-2']").click();
expect(queryAllTexts(".options-container .hb-row")).toEqual(["Row 3\nc"]);
});
test("Don't display option base on applyTo", async () => {
addBuilderOption({
selector: ".test-options-target",
applyTo: ".test-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton classAction="'test-target-2'">a</BuilderButton>
</BuilderRow>`,
});
addBuilderOption({
selector: ".test-options-target",
applyTo: ".test-target-2",
template: xml`<BuilderRow label="'Row 2'">b</BuilderRow>`,
});
await setupHTMLBuilder(`
<div class="test-options-target">
<div class="test-target">b</div>
</div>`);
await contains(":iframe .test-options-target").click();
expect(queryAllTexts(".options-container .hb-row")).toEqual(["Row 1\na"]);
await contains("[data-class-action='test-target-2']").click();
await animationFrame();
expect(queryAllTexts(".options-container .hb-row")).toEqual(["Row 1\na", "Row 2\nb"]);
});
test("basic multi options containers", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 1'">A</BuilderRow>`,
});
addBuilderOption({
selector: ".a",
template: xml`
<BuilderRow label="'Row 2'">B</BuilderRow>`,
});
addBuilderOption({
selector: ".main",
template: xml`
<BuilderRow label="'Row 3'">C</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="main"><p class="test-options-target a">b</p></div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toHaveCount(2);
expect(queryAllTexts(".options-container:first .we-bg-options-container > div > div")).toEqual([
"Row 3",
"C",
]);
expect(
queryAllTexts(".options-container:nth-child(2) .we-bg-options-container > div > div")
).toEqual(["Row 1", "A", "Row 2", "B"]);
});
test("option that matches several elements", async () => {
addBuilderOption({
selector: ".a",
template: xml`<BuilderRow label="'Row'">
<BuilderButton classAction="'my-custom-class'">Test</BuilderButton>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="a"><div class="a test-target">b</div></div>`);
await contains(":iframe .test-target").click();
expect(".options-container:not(.d-none)").toHaveCount(2);
expect(queryAllTexts(".options-container:not(.d-none)")).toEqual([
"Block\nRow\nTest",
"Block\nRow\nTest",
]);
});
test("Snippets options respect sequencing", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 2'">
Test
</BuilderRow>`,
sequence: 2,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 1'">
Test
</BuilderRow>`,
sequence: 1,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderRow label="'Row 3'">
Test
</BuilderRow>`,
sequence: 3,
});
await setupHTMLBuilder(`<div class="test-options-target" data-name="Yop">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(queryAllTexts(".options-container .we-bg-options-container > div > div")).toEqual([
"Row 1",
"Test",
"Row 2",
"Test",
"Row 3",
"Test",
]);
});
test("hide empty OptionContainer and display OptionContainer with content", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`<BuilderRow label="'Row 3'">
<BuilderButton applyTo="'.my-custom-class'" classAction="'test'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(
`<div class="parent-target"><div><div class="child-target">b</div></div></div>`
);
await contains(":iframe .parent-target > div").click();
expect(".options-container:not(.d-none)").toHaveCount(1);
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container:not(.d-none)").toHaveCount(2);
});
test("hide empty OptionContainer and display OptionContainer with content (with BuilderButtonGroup)", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`
<BuilderRow label="'Row 2'">
<BuilderButtonGroup>
<BuilderButton applyTo="'.my-custom-class'" classAction="'test'">Test</BuilderButton>
</BuilderButtonGroup>
</BuilderRow>`,
});
await setupHTMLBuilder(
`<div class="parent-target"><div><div class="child-target">b</div></div></div>`
);
await contains(":iframe .parent-target > div").click();
expect(".options-container:not(.d-none)").toHaveCount(1);
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container:not(.d-none)").toHaveCount(2);
expect(".options-container:not(.d-none):nth-child(2)").toHaveText("Block\nRow 2\nTest");
});
test("hide empty OptionContainer and display OptionContainer with content (with BuilderButtonGroup) - 2", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`
<BuilderRow label="'Row 2'">
<BuilderButtonGroup applyTo="'.my-custom-class'">
<BuilderButton classAction="'test'">Test</BuilderButton>
</BuilderButtonGroup>
</BuilderRow>`,
});
await setupHTMLBuilder(
`<div class="parent-target"><div><div class="child-target">b</div></div></div>`
);
await contains(":iframe .parent-target > div").click();
expect(".options-container:not(.d-none)").toHaveCount(1);
await contains("[data-class-action='my-custom-class']").click();
expect(".options-container:not(.d-none)").toHaveCount(2);
expect(".options-container:not(.d-none):nth-child(2)").toHaveText("Block\nRow 2\nTest");
});
test("fallback on the 'Blocks' tab if no option match the selected element", async () => {
await setupHTMLBuilder(`<div class="parent-target"><div class="child-target">b</div></div>`);
await contains(":iframe .parent-target > div").click();
expect(".o-snippets-tabs button:contains('Blocks')").toHaveClass("active");
});
test("display empty message if no option container is visible", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.invalid'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="parent-target"><div class="child-target">b</div></div>`);
await contains(":iframe .parent-target > div").click();
await animationFrame();
expect(".o_customize_tab").toHaveText("Select a block on your page to style it.");
});
test("hide/display option base on selector", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".my-custom-class",
template: xml`<BuilderRow label="'Row 2'">
<BuilderButton classAction="'test'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`<div class="parent-target"><div class="child-target">b</div></div>`);
await contains(":iframe .parent-target").click();
expect("[data-class-action='test']").not.toHaveCount();
await contains("[data-class-action='my-custom-class']").click();
expect("[data-class-action='test']").toBeVisible();
});
test("hide/display option container base on selector", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".my-custom-class",
template: xml`<BuilderRow label="'Row 2'">
<BuilderButton classAction="'test'"/>
</BuilderRow>`,
});
addBuilderOption({
selector: ".sub-child-target",
template: xml`<BuilderRow label="'Row 3'">
<BuilderButton classAction="'another-custom-class'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`
<div class="parent-target">
<div class="child-target">
<div class="sub-child-target">b</div>
</div>
</div>`);
await contains(":iframe .sub-child-target").click();
expect("[data-class-action='test']").not.toHaveCount();
const selectorRowLabel = ".options-container .hb-row:not(.d-none) .hb-row-label";
expect(queryAllTexts(selectorRowLabel)).toEqual(["Row 1", "Row 3"]);
await contains("[data-class-action='my-custom-class']").click();
expect("[data-class-action='test']").toBeVisible();
expect(queryAllTexts(selectorRowLabel)).toEqual(["Row 1", "Row 2", "Row 3"]);
});
test("don't rerender the OptionsContainer every time you click on the same element", async () => {
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'">
<BuilderButton applyTo="'.child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
patchWithCleanup(OptionsContainer.prototype, {
setup() {
super.setup();
onWillStart(() => {
expect.step("onWillStart");
});
},
});
await setupHTMLBuilder(`
<div class="parent-target">
<div class="child-target">
<div class="sub-child-target">b</div>
</div>
</div>`);
await contains(":iframe .sub-child-target").click();
expect("[data-class-action='test']").not.toHaveCount();
expect.verifySteps(["onWillStart"]);
await contains(":iframe .sub-child-target").click();
expect.verifySteps([]);
});
test("no need to define 'isApplied' method for custom action if the widget already has a generic action", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ editingElement, value }) {
editingElement.textContent = value;
}
},
});
addBuilderOption({
selector: ".s_test",
template: xml`
<BuilderRow label.translate="Type">
<BuilderSelect>
<BuilderSelectItem classAction="'A-class'" action="'customAction'" actionParam="'A'">A</BuilderSelectItem>
</BuilderSelect>
</BuilderRow>
`,
});
await setupHTMLBuilder(`
<div class="s_test A-class">
a
</div>`);
await contains(":iframe .s_test").click();
expect(".options-container [data-class-action='A-class']").toHaveText("A");
});
test("useDomState callback shouldn't be called when the editingElement is removed", async () => {
let editor;
let count = 0;
class TestOption extends Component {
static template = xml`<div class="test_option">test</div>`;
static props = {};
setup() {
useDomState(() => {
expect.step(`useDomState ${count}`);
return {
count: (count = count + 1),
};
});
}
}
addBuilderOption({
selector: ".s_test",
editableOnly: false,
Component: TestOption,
});
addBuilderOption({
selector: "*",
template: xml`<BuilderButton action="'addTestSnippet'">Add</BuilderButton>`,
});
addBuilderAction({
addTestSnippet: class extends BuilderAction {
static id = "addTestSnippet";
apply({ editingElement }) {
const testEl = document.createElement("div");
testEl.classList.add("s_test", "alert-info");
testEl.textContent = "test";
editingElement.after(testEl);
editor.shared["builderOptions"].setNextTarget(testEl);
}
},
});
const { getEditor } = await setupHTMLBuilder(`<div class="s_dummy">Hello</div>`);
editor = getEditor();
await contains(":iframe .s_dummy").click();
await contains("[data-action-id='addTestSnippet']").click();
expect(".options-container .test_option").toHaveCount(1);
expect.verifySteps(["useDomState 0"]);
undo(editor);
await animationFrame();
expect(".options-container .test_option").toHaveCount(0);
expect.verifySteps([]);
redo(editor);
await animationFrame();
expect(".options-container .test_option").toHaveCount(1);
expect.verifySteps(["useDomState 1"]);
});
test("Update editing elements at dom change with multiple levels of applyTo", async () => {
addBuilderAction({
customAction: class extends BuilderAction {
static id = "customAction";
apply({ editingElement }) {
const createdEl = editingElement.cloneNode(true);
const parentEl = editingElement.parentElement;
parentEl.appendChild(createdEl);
}
},
});
addBuilderOption({
selector: ".parent-target",
template: xml`<BuilderRow label="'Row 1'" applyTo="'.child-target'">
<BuilderButton action="'customAction'" />
<BuilderButton applyTo="'.sub-child-target'" classAction="'my-custom-class'"/>
</BuilderRow>`,
});
await setupHTMLBuilder(`
<div class="parent-target">
<div class="child-target">
<div class="sub-child-target">b</div>
</div>
</div>`);
await contains(":iframe .parent-target").click();
await contains("[data-action-id='customAction']").click();
await contains("[data-class-action='my-custom-class']").click();
expect(":iframe .sub-child-target").toHaveClass("my-custom-class");
});
test("An option should only appear if its target is inside an editable area, unless specified otherwise", async () => {
addBuilderOption({
selector: ".test-target",
template: xml`
<BuilderButton classAction="'dummy-class-a'">Option A</BuilderButton>
`,
});
addBuilderOption({
selector: ".test-target",
editableOnly: false,
template: xml`
<BuilderButton classAction="'dummy-class-b'">Option B</BuilderButton>
`,
});
const { getEditor } = await setupHTMLBuilder(`<div></div>`);
const editor = getEditor();
setContent(
editor.editable,
`<div class="content">
<div class="test-target test-not-editable">NOT IN EDITABLE</div>
</div>
<div class="content o_editable">
<div class="test-target test-editable">IN EDITABLE</div>
</div>`
);
editor.shared.history.addStep();
await contains(":iframe .test-not-editable").click();
expect(queryAllTexts(".options-container [data-class-action]")).toEqual(["Option B"]);
await contains(":iframe .test-editable").click();
expect(queryAllTexts(".options-container [data-class-action]")).toEqual([
"Option A",
"Option B",
]);
});
describe("isActiveItem", () => {
test("a button should not be visible if its dependency isn't (with undo)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton attributeAction="'my-attribute1'" attributeActionValue="'x'" id="'id1'">b1</BuilderButton>
<BuilderButton attributeAction="'my-attribute1'" attributeActionValue="'y'" id="'id2'">b2</BuilderButton>
<BuilderButton attributeAction="'my-attribute2'" attributeActionValue="'1'" t-if="this.isActiveItem('id1')">b3</BuilderButton>
<BuilderButton attributeAction="'my-attribute2'" attributeActionValue="'2'" t-if="this.isActiveItem('id2')">b4</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
setSelection({
anchorNode: queryFirst(":iframe .test-options-target").childNodes[0],
anchorOffset: 0,
});
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).not.toHaveCount();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).not.toHaveCount();
await contains(
"[data-attribute-action='my-attribute1'][data-attribute-action-value='x']"
).click();
expect(":iframe .test-options-target").toHaveAttribute("my-attribute1", "x");
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).not.toHaveCount();
await contains(
"[data-attribute-action='my-attribute1'][data-attribute-action-value='y']"
).click();
expect(":iframe .test-options-target").toHaveAttribute("my-attribute1", "y");
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).not.toHaveCount();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).toBeVisible();
await contains(".fa-undo").click();
expect(":iframe .test-options-target").toHaveAttribute("my-attribute1", "x");
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).not.toHaveCount();
await contains(".fa-undo").click();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).not.toHaveCount();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).not.toHaveCount();
});
test("a button should not be visible if its dependency isn't (in a BuilderSelect with priority)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderSelect>
<BuilderSelectItem classAction="'a'" id="'x'">x</BuilderSelectItem>
<BuilderSelectItem classAction="'a b'" id="'y'">y</BuilderSelectItem>
</BuilderSelect>
<BuilderButton classAction="'b1'" t-if="this.isActiveItem('x')">b1</BuilderButton>
<BuilderButton classAction="'b2'" t-if="this.isActiveItem('y')">b2</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target a">a</div>`);
setSelection({
anchorNode: queryFirst(":iframe .test-options-target").childNodes[0],
anchorOffset: 0,
});
await contains(":iframe .test-options-target").click();
await animationFrame();
expect(".options-container").toBeVisible();
expect(".we-bg-options-container .dropdown").toHaveText("x");
expect("[data-class-action='b1']").toBeVisible();
expect("[data-class-action='b2']").not.toHaveCount();
await contains(".we-bg-options-container .dropdown").click();
await contains("[data-class-action='a b']").click();
expect(".we-bg-options-container .dropdown").toHaveText("y");
expect("[data-class-action='b1']").not.toHaveCount();
expect("[data-class-action='b2']").toBeVisible();
});
test("a button should not be visible if the dependency is active", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton attributeAction="'my-attribute1'" attributeActionValue="'x'" id="'id1'">b1</BuilderButton>
<BuilderButton attributeAction="'my-attribute2'" attributeActionValue="'1'" t-if="!this.isActiveItem('id1')">b3</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).toBeVisible();
await contains(
"[data-attribute-action='my-attribute1'][data-attribute-action-value='x']"
).click();
expect(":iframe .test-options-target").toHaveAttribute("my-attribute1", "x");
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).not.toHaveCount();
});
test("a button should not be visible if the dependency is active (when a dependency is added after a dependent)", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton attributeAction="'my-attribute2'" attributeActionValue="'1'" t-if="this.isActiveItem('id')">b1</BuilderButton>
<BuilderButton attributeAction="'my-attribute2'" attributeActionValue="'2'" t-if="!this.isActiveItem('id')">b2</BuilderButton>
<BuilderRow label="'dependency'">
<BuilderButton attributeAction="'my-attribute1'" attributeActionValue="'x'" id="'id'">b3</BuilderButton>
</BuilderRow>
`,
});
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
await contains(":iframe .test-options-target").click();
expect(".options-container").toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).not.toHaveCount();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).toBeVisible();
await contains(
"[data-attribute-action='my-attribute1'][data-attribute-action-value='x']"
).click();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='1']"
).toBeVisible();
expect(
"[data-attribute-action='my-attribute2'][data-attribute-action-value='2']"
).not.toHaveCount();
});
test("a button should not be visible if its dependency is removed from the DOM", async () => {
addBuilderOption({
selector: ".test-options-target",
template: xml`
<BuilderButton classAction="'my-class1'" id="'id1'">b1</BuilderButton>
<BuilderButton classAction="'my-class2'" id="'id2'" t-if="this.isActiveItem('id1')">b2</BuilderButton>
<BuilderButton classAction="'my-class3'" t-if="this.isActiveItem('id2')">b3</BuilderButton>
`,
});
await setupHTMLBuilder(`<div class="test-options-target my-class1 my-class2">b</div>`);
await contains(":iframe .test-options-target").click();
await contains("[data-class-action='my-class1']").click();
// Wait 2 animation frames: one for id2 to be removed and another for
// id3 to be removed.
await animationFrame();
expect("[data-class-action='my-class3']").not.toHaveCount();
});
});