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`
Test
`,
});
await setupHTMLBuilder(`
b
`);
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`
Test
`;
static props = {};
}
addBuilderOption({
selector: ".test-options-target",
Component: TestOption,
});
await setupHTMLBuilder(`b
`);
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`
Test
`,
title: "My custom title",
});
await setupHTMLBuilder(`b
`);
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`a`,
});
addBuilderOption({
selector: ".test-options-target",
exclude: ".test-exclude-2",
template: xml`b`,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
c
`,
});
await setupHTMLBuilder(`b
`);
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`
a
`,
});
addBuilderOption({
selector: ".test-options-target",
applyTo: ".test-target-2",
template: xml`b`,
});
await setupHTMLBuilder(`
`);
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`
A`,
});
addBuilderOption({
selector: ".a",
template: xml`
B`,
});
addBuilderOption({
selector: ".main",
template: xml`
C`,
});
await setupHTMLBuilder(``);
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`
Test
`,
});
await setupHTMLBuilder(``);
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`
Test
`,
sequence: 2,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
Test
`,
sequence: 1,
});
addBuilderOption({
selector: ".test-options-target",
template: xml`
Test
`,
sequence: 3,
});
await setupHTMLBuilder(`b
`);
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`
`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`
`,
});
await setupHTMLBuilder(
``
);
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`
`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`
Test
`,
});
await setupHTMLBuilder(
``
);
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`
`,
});
addBuilderOption({
selector: ".parent-target > div",
template: xml`
Test
`,
});
await setupHTMLBuilder(
``
);
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(``);
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`
`,
});
await setupHTMLBuilder(``);
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`
`,
});
addBuilderOption({
selector: ".my-custom-class",
template: xml`
`,
});
await setupHTMLBuilder(``);
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`
`,
});
addBuilderOption({
selector: ".my-custom-class",
template: xml`
`,
});
addBuilderOption({
selector: ".sub-child-target",
template: xml`
`,
});
await setupHTMLBuilder(`
`);
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`
`,
});
patchWithCleanup(OptionsContainer.prototype, {
setup() {
super.setup();
onWillStart(() => {
expect.step("onWillStart");
});
},
});
await setupHTMLBuilder(`
`);
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`
A
`,
});
await setupHTMLBuilder(`
a
`);
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`test
`;
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`Add`,
});
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(`Hello
`);
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`
`,
});
await setupHTMLBuilder(`
`);
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`
Option A
`,
});
addBuilderOption({
selector: ".test-target",
editableOnly: false,
template: xml`
Option B
`,
});
const { getEditor } = await setupHTMLBuilder(``);
const editor = getEditor();
setContent(
editor.editable,
`
`
);
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`
b1
b2
b3
b4
`,
});
await setupHTMLBuilder(`b
`);
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`
x
y
b1
b2
`,
});
await setupHTMLBuilder(`a
`);
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`
b1
b3
`,
});
await setupHTMLBuilder(`b
`);
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`
b1
b2
b3
`,
});
await setupHTMLBuilder(`b
`);
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`
b1
b2
b3
`,
});
await setupHTMLBuilder(`b
`);
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();
});
});