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(`
b
`); 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(`

b

`); 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(`
b
`); 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( `
b
` ); 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( `
b
` ); 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( `
b
` ); 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(`
b
`); 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(`
b
`); 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(`
b
`); 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(`
b
`); 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(`
b
`); 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(`
b
`); 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, `
NOT IN EDITABLE
IN 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(); }); });