mirror of
https://github.com/bringout/oca-ocb-web.git
synced 2026-04-19 04:12:01 +02:00
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:
parent
4b94f0abc5
commit
f866779561
1513 changed files with 396049 additions and 358525 deletions
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
|
@ -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="[{"id":1,"display_name":"First","name":"First"}]">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="[{"id":1,"display_name":"First","name":"First"},{"id":2,"display_name":"Second","name":"Second"}]">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="[{"id":2,"display_name":"Second","name":"Second"}]">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="[{"id":1,"display_name":"First","name":"First"}]">b</div>`
|
||||
);
|
||||
});
|
||||
|
|
@ -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="{"id":1,"display_name":"First","name":"First"}">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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue