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,201 @@
|
|||
import {
|
||||
setupHTMLBuilder,
|
||||
getDragHelper,
|
||||
waitForEndOfOperation,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { BuilderOptionsPlugin } from "@html_builder/core/builder_options_plugin";
|
||||
import { Operation } from "@html_builder/core/operation";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
animationFrame,
|
||||
click,
|
||||
Deferred,
|
||||
queryAll,
|
||||
queryAllTexts,
|
||||
queryOne,
|
||||
waitFor,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { loadBundle } from "@web/core/assets";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const snippetContent = [
|
||||
`<div name="Button A" data-oe-thumbnail="buttonA.svg" data-oe-snippet-id="123">
|
||||
<a class="btn btn-primary" href="#" data-snippet="s_button">Button A</a>
|
||||
</div>`,
|
||||
`<div name="Button B" data-oe-thumbnail="buttonB.svg" data-oe-snippet-id="123">
|
||||
<a class="btn btn-primary" href="#" data-snippet="s_button">Button B</a>
|
||||
</div>`,
|
||||
];
|
||||
|
||||
const dropzoneSelectors = [
|
||||
{
|
||||
selector: "*",
|
||||
dropNear: "p",
|
||||
},
|
||||
];
|
||||
|
||||
test("Display inner content snippet", async () => {
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
});
|
||||
const snippetInnerContentSelector = ".o-snippets-menu #snippet_content .o_snippet";
|
||||
expect(snippetInnerContentSelector).toHaveCount(2);
|
||||
expect(queryAllTexts(snippetInnerContentSelector)).toEqual(["Button A", "Button B"]);
|
||||
const thumbnailImgUrls = queryAll(
|
||||
`${snippetInnerContentSelector} .o_snippet_thumbnail_img`
|
||||
).map((thumbnail) => thumbnail.style.backgroundImage);
|
||||
expect(thumbnailImgUrls).toEqual(['url("buttonA.svg")', 'url("buttonB.svg")']);
|
||||
});
|
||||
|
||||
test("Drag & drop inner content block", async () => {
|
||||
const { contentEl } = await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML(`<div><p>Text</p></div>`);
|
||||
expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled();
|
||||
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-website-builder_sidebar [name='Button A'] .o_snippet_thumbnail"
|
||||
).drag();
|
||||
expect(":iframe .oe_drop_zone:nth-child(1)").toHaveCount(1);
|
||||
expect(":iframe .oe_drop_zone:nth-child(3)").toHaveCount(1);
|
||||
|
||||
expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled();
|
||||
|
||||
await moveTo(":iframe .oe_drop_zone");
|
||||
expect(":iframe .oe_drop_zone.invisible:nth-child(1)").toHaveCount(1);
|
||||
expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled();
|
||||
|
||||
await drop(getDragHelper());
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML(
|
||||
`<div>\ufeff<a class="btn btn-primary" href="#" data-snippet="s_button" data-name="Button A">\ufeffButton A\ufeff</a>\ufeff<p>Text</p></div>`
|
||||
);
|
||||
expect(".o-website-builder_sidebar .fa-undo").toBeEnabled();
|
||||
});
|
||||
|
||||
test("Drag & drop inner content block + undo/redo", async () => {
|
||||
const { contentEl } = await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML(`<div><p>Text</p></div>`);
|
||||
expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled();
|
||||
expect(".o-website-builder_sidebar .fa-repeat").not.toBeEnabled();
|
||||
|
||||
await click(".o-website-builder_sidebar .fa-undo");
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-website-builder_sidebar [name='Button A'] .o_snippet_thumbnail"
|
||||
).drag();
|
||||
await moveTo(":iframe .oe_drop_zone");
|
||||
await drop(getDragHelper());
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML(
|
||||
`<div>\ufeff<a class="btn btn-primary" href="#" data-snippet="s_button" data-name="Button A">\ufeffButton A\ufeff</a>\ufeff<p>Text</p></div>`
|
||||
);
|
||||
expect(".o-website-builder_sidebar .fa-undo").toBeEnabled();
|
||||
expect(".o-website-builder_sidebar .fa-repeat").not.toBeEnabled();
|
||||
|
||||
await click(".o-website-builder_sidebar .fa-undo");
|
||||
await animationFrame();
|
||||
expect(contentEl).toHaveInnerHTML(`<div><p>Text</p></div>`);
|
||||
expect(".o-website-builder_sidebar .fa-undo").not.toBeEnabled();
|
||||
expect(".o-website-builder_sidebar .fa-repeat").toBeEnabled();
|
||||
});
|
||||
|
||||
test("Drag inner content and drop it outside of a dropzone", async () => {
|
||||
const { contentEl, builderEl } = await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML(`<div><p>Text</p></div>`);
|
||||
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-website-builder_sidebar [name='Button A'] .o_snippet_thumbnail"
|
||||
).drag();
|
||||
expect(":iframe .oe_drop_zone:nth-child(1)").toHaveCount(1);
|
||||
expect(":iframe .oe_drop_zone:nth-child(3)").toHaveCount(1);
|
||||
|
||||
await moveTo(builderEl);
|
||||
await drop(getDragHelper());
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML(`<div><p>Text</p></div>`);
|
||||
});
|
||||
|
||||
test("A snippet should appear disabled if there is nowhere to drop it", async () => {
|
||||
const { contentEl } = await setupHTMLBuilder("", {
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML("");
|
||||
expect(".o_block_tab .o_snippet.o_disabled").toHaveCount(2);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("click just after drop is redispatched in next operation", async () => {
|
||||
const nextDef = new Deferred();
|
||||
patchWithCleanup(Operation.prototype, {
|
||||
next(fn, ...args) {
|
||||
const originalFn = fn;
|
||||
fn = async () => {
|
||||
await originalFn();
|
||||
nextDef.resolve();
|
||||
};
|
||||
expect.step(`next${args[0]?.shouldInterceptClick ? " should intercept" : ""}`);
|
||||
const res = super.next(fn, ...args);
|
||||
return res;
|
||||
},
|
||||
});
|
||||
patchWithCleanup(BuilderOptionsPlugin.prototype, {
|
||||
async onClick(ev) {
|
||||
expect.step("onClick");
|
||||
super.onClick(ev);
|
||||
},
|
||||
updateContainers(...args) {
|
||||
expect.step("updateContainers");
|
||||
super.updateContainers(...args);
|
||||
},
|
||||
});
|
||||
await setupHTMLBuilder("", {
|
||||
styleContent: /*css*/ `
|
||||
.o_loading_screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
section {
|
||||
height: 100%; /* to easily target */
|
||||
}`,
|
||||
});
|
||||
|
||||
// TODO: the next lines replicate website's `insertCategorySnippet` helper.
|
||||
// It should be moved to html_builder.
|
||||
await contains(".o-snippets-menu #snippet_groups .o_snippet_thumbnail_area").click();
|
||||
await animationFrame();
|
||||
await loadBundle("html_builder.iframe_add_dialog", {
|
||||
targetDoc: queryOne("iframe.o_add_snippet_iframe").contentDocument,
|
||||
js: false,
|
||||
});
|
||||
await waitFor(".o_add_snippet_dialog iframe.show.o_add_snippet_iframe");
|
||||
await contains(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap"
|
||||
).click();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["next should intercept"]); // On snippet selected
|
||||
|
||||
await waitFor(":iframe .o_loading_screen");
|
||||
await click(":iframe", { position: { x: 200, y: 50 }, relative: true });
|
||||
expect.verifySteps(["next"]); // On click
|
||||
await nextDef;
|
||||
expect.verifySteps(["updateContainers"]); // End of drop, on addStep()
|
||||
await animationFrame();
|
||||
expect.verifySteps(["onClick", "next", "updateContainers"]); // On click redispatched
|
||||
await animationFrame();
|
||||
expect(".o-snippets-tabs .o-hb-tab.active").toHaveText("Style");
|
||||
});
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
import {
|
||||
addBuilderPlugin,
|
||||
addDropZoneSelector,
|
||||
createTestSnippets,
|
||||
getDragHelper,
|
||||
getSnippetStructure,
|
||||
setupHTMLBuilder,
|
||||
setupHTMLBuilderWithDummySnippet,
|
||||
waitForEndOfOperation,
|
||||
waitForSnippetDialog,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { Builder } from "@html_builder/builder";
|
||||
import { Plugin } from "@html_editor/plugin";
|
||||
import { beforeEach, expect, test, describe } from "@odoo/hoot";
|
||||
import { animationFrame, click, queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
|
||||
import { contains, onRpc, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
let snippets;
|
||||
beforeEach(() => {
|
||||
snippets = {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-o-image-preview="" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-oe-keywords="" data-o-snippet-group="a"><section class="s_snippet_group" data-snippet="s_snippet_group"></section></div>',
|
||||
'<div name="B" data-o-image-preview="" data-oe-thumbnail="b.svg" data-oe-snippet-id="123" data-oe-keywords="" data-o-snippet-group="b"><section class="s_snippet_group" data-snippet="s_snippet_group"></section></div>',
|
||||
'<div name="C" data-o-image-preview="" data-oe-thumbnail="c.svg" data-oe-snippet-id="123" data-oe-keywords="" data-o-snippet-group="c"><section class="s_snippet_group" data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
};
|
||||
addDropZoneSelector({
|
||||
selector: "*",
|
||||
dropNear: "section",
|
||||
});
|
||||
});
|
||||
|
||||
test("display group snippet", async () => {
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets,
|
||||
});
|
||||
const snippetGroupsSelector = ".o-snippets-menu #snippet_groups .o_snippet";
|
||||
expect(snippetGroupsSelector).toHaveCount(3);
|
||||
expect(queryAllTexts(snippetGroupsSelector)).toEqual(["A", "B", "C"]);
|
||||
const thumbnailImgUrls = queryAll(`${snippetGroupsSelector} .o_snippet_thumbnail_img`).map(
|
||||
(thumbnail) => thumbnail.style.backgroundImage
|
||||
);
|
||||
expect(thumbnailImgUrls).toEqual(['url("a.svg")', 'url("b.svg")', 'url("c.svg")']);
|
||||
});
|
||||
|
||||
test("install an app from snippet group", async () => {
|
||||
patchWithCleanup(Builder.prototype, {
|
||||
setup() {
|
||||
this.props.installSnippetModule = ({ moduleId }) => {
|
||||
expect(moduleId).toEqual("111");
|
||||
expect.step(`button_immediate_install`);
|
||||
};
|
||||
super.setup(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-module-id="111" data-module-display-name="module_A" data-oe-thumbnail="a.svg"><section class="s_snippet_group" data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
},
|
||||
});
|
||||
await click(`.o-snippets-menu #snippet_groups .o_snippet .btn.o_install_btn`);
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal-body").toHaveText(
|
||||
"Do you want to install module_A App?\nMore info about this app."
|
||||
);
|
||||
|
||||
await contains(".modal .btn-primary:contains('Save and Install')").click();
|
||||
expect.verifySteps([`button_immediate_install`]);
|
||||
});
|
||||
|
||||
test("install an app from snippet structure", async () => {
|
||||
patchWithCleanup(Builder.prototype, {
|
||||
setup() {
|
||||
this.props.installSnippetModule = ({ moduleId }) => {
|
||||
expect(moduleId).toEqual("111");
|
||||
expect.step(`button_immediate_install`);
|
||||
};
|
||||
super.setup(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
const snippetsDescription = createTestSnippets({
|
||||
snippets: [
|
||||
{
|
||||
name: "Test 1",
|
||||
moduleDisplayName: "Test 1 module",
|
||||
groupName: "a",
|
||||
innerHTML: "Yop",
|
||||
moduleId: 111,
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
moduleDisplayName: "Test 2 module",
|
||||
groupName: "a",
|
||||
innerHTML: "Hello",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: snippetsDescription.map((snippetDesc) =>
|
||||
getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
await waitForSnippetDialog();
|
||||
expect(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap .o_snippet_preview_install_btn"
|
||||
).toHaveCount(1);
|
||||
expect(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap:has(.o_snippet_preview_install_btn) .s_test"
|
||||
).toHaveText("Yop");
|
||||
|
||||
await click(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap .o_snippet_preview_install_btn"
|
||||
);
|
||||
await animationFrame();
|
||||
expect(".o_dialog:not(:has(.o_inactive_modal)) .modal-body").toHaveText(
|
||||
"Do you want to install Test 1 module App?\nMore info about this app."
|
||||
);
|
||||
await contains(
|
||||
".o_dialog:not(:has(.o_inactive_modal)) .btn-primary:contains('Save and Install')"
|
||||
).click();
|
||||
expect.verifySteps([`button_immediate_install`]);
|
||||
});
|
||||
|
||||
test("open add snippet dialog + switch snippet category", async () => {
|
||||
const snippets = [
|
||||
{ name: "Test", groupName: "a", innerHTML: "Yop" },
|
||||
{ name: "Test", groupName: "a", innerHTML: "Hello" },
|
||||
{ name: "Test", groupName: "b", innerHTML: "Nice" },
|
||||
];
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
'<div name="B" data-oe-thumbnail="b.svg" data-oe-snippet-id="123" data-o-snippet-group="b"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: createTestSnippets({ snippets, withName: false }).map(
|
||||
(snippetDesc) => getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(queryAllTexts(".o-snippets-menu #snippet_groups .o_snippet")).toEqual(["A", "B"]);
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
await waitForSnippetDialog();
|
||||
expect(queryAllTexts(".o_add_snippet_dialog aside .list-group .list-group-item")).toEqual([
|
||||
"A",
|
||||
"B",
|
||||
]);
|
||||
expect(".o_add_snippet_dialog aside .list-group .list-group-item.active").toHaveText("A");
|
||||
|
||||
expect(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap"
|
||||
).toHaveCount(2);
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
createTestSnippets({ snippets, withName: true })
|
||||
.filter((s) => s.groupName === "a")
|
||||
.map((s) => s.content)
|
||||
);
|
||||
|
||||
await click(".o_add_snippet_dialog aside .list-group .list-group-item:contains('B')");
|
||||
await animationFrame();
|
||||
expect(".o_add_snippet_dialog aside .list-group .list-group-item.active").toHaveText("B");
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
createTestSnippets({ snippets, withName: true })
|
||||
.filter((s) => s.groupName === "b")
|
||||
.map((s) => s.content)
|
||||
);
|
||||
});
|
||||
|
||||
test("search snippet in add snippet dialog", async () => {
|
||||
const snippets = [
|
||||
{ name: "gravy", groupName: "a", innerHTML: "content 1", keywords: ["jumper"] },
|
||||
{ name: "bandage", groupName: "a", innerHTML: "content 2", keywords: ["order"] },
|
||||
{
|
||||
name: "banana",
|
||||
groupName: "b",
|
||||
innerHTML: "content 3",
|
||||
keywords: ["grape", "orange"],
|
||||
},
|
||||
];
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
'<div name="B" data-oe-thumbnail="b.svg" data-oe-snippet-id="123" data-o-snippet-group="b"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: createTestSnippets({ snippets, withName: false }).map(
|
||||
(snippetDesc) => getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
await waitForSnippetDialog();
|
||||
expect("aside .list-group .list-group-item").toHaveCount(2);
|
||||
const snippetsDescriptionProcessed = createTestSnippets({ snippets, withName: true });
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
snippetsDescriptionProcessed.filter((s) => s.groupName === "a").map((s) => s.content)
|
||||
);
|
||||
|
||||
// Search base on snippet name
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("Ban");
|
||||
expect("aside .list-group .list-group-item").toHaveCount(0);
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
[snippetsDescriptionProcessed[1], snippetsDescriptionProcessed[2]].map((s) => s.content)
|
||||
);
|
||||
|
||||
// Search base on snippet name and keywords
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("gra");
|
||||
expect("aside .list-group .list-group-item").toHaveCount(0);
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
[snippetsDescriptionProcessed[0], snippetsDescriptionProcessed[2]].map((s) => s.content)
|
||||
);
|
||||
|
||||
// Search base on keywords
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("or");
|
||||
expect("aside .list-group .list-group-item").toHaveCount(0);
|
||||
expect(
|
||||
queryAll(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).map((el) => el.innerHTML)
|
||||
).toEqual(
|
||||
[snippetsDescriptionProcessed[1], snippetsDescriptionProcessed[2]].map((s) => s.content)
|
||||
);
|
||||
});
|
||||
|
||||
test("search snippet by class", async () => {
|
||||
const snippets = [
|
||||
{
|
||||
name: "foo_bar",
|
||||
groupName: "a",
|
||||
innerHTML: "content 1",
|
||||
snippet: "s_foo_bar",
|
||||
additionalClassOnRoot: "s_additional_class",
|
||||
keywords: [],
|
||||
},
|
||||
// The second snippet includes a child class selector using innerHTML.
|
||||
// Using createTestSnippets with withName toggles the data-name automatically.
|
||||
{
|
||||
name: "foo",
|
||||
groupName: "a",
|
||||
innerHTML: `<div class="s_class_on_child">content 2</div>`,
|
||||
snippet: "s_foo",
|
||||
keywords: [],
|
||||
},
|
||||
{
|
||||
name: "bar",
|
||||
groupName: "a",
|
||||
innerHTML: "content 3",
|
||||
snippet: "s_bar",
|
||||
keywords: [],
|
||||
},
|
||||
];
|
||||
|
||||
const snippetsForSetup = createTestSnippets({ snippets, withName: false });
|
||||
const snippetsDescriptionProcessed = createTestSnippets({ snippets, withName: true });
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: snippetsForSetup.map((snippetDesc) =>
|
||||
getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
await waitForSnippetDialog();
|
||||
|
||||
// Search among classes of root node
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("s_bar");
|
||||
expect(
|
||||
queryFirst(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).innerHTML
|
||||
).toEqual(snippetsDescriptionProcessed[2].content);
|
||||
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("s_additional_class");
|
||||
expect(
|
||||
queryFirst(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).innerHTML
|
||||
).toEqual(snippetsDescriptionProcessed[0].content);
|
||||
|
||||
// Search among classes of child nodes
|
||||
await contains(".o_add_snippet_dialog aside input[type='search']").edit("s_class_on_child");
|
||||
expect(
|
||||
queryFirst(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap > div"
|
||||
).innerHTML
|
||||
).toEqual(snippetsDescriptionProcessed[1].content);
|
||||
});
|
||||
|
||||
test("add snippet dialog with imagePreview", async () => {
|
||||
const snippets = [
|
||||
{ name: "gravy", groupName: "a", innerHTML: "content 1" },
|
||||
{ name: "banana", groupName: "a", innerHTML: "content 2", imagePreview: "banana.png" },
|
||||
];
|
||||
|
||||
await setupHTMLBuilder("<div><p>Text</p></div>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
'<div name="B" data-oe-thumbnail="b.svg" data-oe-snippet-id="123" data-o-snippet-group="b"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: createTestSnippets({ snippets, withName: false }).map(
|
||||
(snippetDesc) => getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
const previewSnippetIframeSelector =
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap";
|
||||
await waitForSnippetDialog();
|
||||
expect(`${previewSnippetIframeSelector}`).toHaveCount(2);
|
||||
const snippetsDescriptionProcessed = createTestSnippets({ snippets, withName: true });
|
||||
expect(`${previewSnippetIframeSelector}:first > div`).toHaveInnerHTML(
|
||||
snippetsDescriptionProcessed[0].content
|
||||
);
|
||||
expect(
|
||||
`${previewSnippetIframeSelector}:nth-child(1) .s_dialog_preview_image img`
|
||||
).toHaveAttribute("data-src", snippetsDescriptionProcessed[1].imagePreview);
|
||||
});
|
||||
|
||||
test("insert snippet structure", async () => {
|
||||
const snippets = [{ name: "Test", groupName: "a", innerHTML: "Yop" }];
|
||||
|
||||
const { contentEl } = await setupHTMLBuilder("<section><p>Text</p></section>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: createTestSnippets({ snippets, withName: false }).map(
|
||||
(snippetDesc) => getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML(`<section><p>Text</p></section>`);
|
||||
|
||||
await click(".o-snippets-menu #snippet_groups .o_snippet_thumbnail .o_snippet_thumbnail_area");
|
||||
await waitForSnippetDialog();
|
||||
const previewSelector =
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap";
|
||||
expect(previewSelector).toHaveCount(1);
|
||||
|
||||
await contains(previewSelector).click();
|
||||
expect(".o_add_snippet_dialog").toHaveCount(0);
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML(
|
||||
`<section><p>Text</p></section>${
|
||||
createTestSnippets({ snippets, withName: true })[0].content
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
test("Drag & drop snippet structure", async () => {
|
||||
const snippets = [{ name: "Test", groupName: "a", innerHTML: "Yop" }];
|
||||
|
||||
const { contentEl } = await setupHTMLBuilder("<section><p>Text</p></section>", {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: createTestSnippets({ snippets, withName: false }).map(
|
||||
(snippetDesc) => getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(contentEl).toHaveInnerHTML(`<section><p>Text</p></section>`);
|
||||
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-snippets-menu #snippet_groups .o_snippet_thumbnail"
|
||||
).drag();
|
||||
expect(":iframe .oe_drop_zone:nth-child(1)").toHaveCount(1);
|
||||
expect(":iframe .oe_drop_zone:nth-child(3)").toHaveCount(1);
|
||||
|
||||
await moveTo(":iframe .oe_drop_zone");
|
||||
expect(":iframe .oe_drop_zone.o_dropzone_highlighted:nth-child(1)").toHaveCount(1);
|
||||
await drop(getDragHelper());
|
||||
expect(":iframe section[data-snippet='s_snippet_group']:nth-child(1)").toHaveCount(1);
|
||||
expect(".o_add_snippet_dialog").toHaveCount(1);
|
||||
|
||||
await waitForSnippetDialog();
|
||||
const previewSelector =
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap";
|
||||
expect(previewSelector).toHaveCount(1);
|
||||
|
||||
await contains(previewSelector).click();
|
||||
expect(".o_add_snippet_dialog").toHaveCount(0);
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML(
|
||||
`${
|
||||
createTestSnippets({ snippets, withName: true })[0].content
|
||||
}<section><p>Text</p></section>`
|
||||
);
|
||||
});
|
||||
|
||||
test("Cancel snippet drag & drop over sidebar", async () => {
|
||||
const { contentEl } = await setupHTMLBuilderWithDummySnippet();
|
||||
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-snippets-menu #snippet_groups .o_snippet_thumbnail"
|
||||
).drag();
|
||||
expect(":iframe .oe_drop_zone").toHaveCount(1);
|
||||
|
||||
// Specifying an explicit target should not be needed, but the test
|
||||
// sometimes fails, probably because the snippet is partially touching the
|
||||
// iframe. We drop on the "mobile" button to be as far as possible from the
|
||||
// iframe.
|
||||
await moveTo(".o-website-builder_sidebar button[data-action=mobile]");
|
||||
await drop(getDragHelper());
|
||||
expect(".o_add_snippet_dialog").toHaveCount(0);
|
||||
await waitForEndOfOperation();
|
||||
|
||||
expect(contentEl).toHaveInnerHTML("");
|
||||
});
|
||||
|
||||
test("Renaming custom snippets don't make an orm call", async () => {
|
||||
class TestSetupEditorPlugin extends Plugin {
|
||||
static id = "test.setup_editor_plugin";
|
||||
resources = {
|
||||
snippet_preview_dialog_bundles: ["web.assets_frontend"],
|
||||
};
|
||||
}
|
||||
addBuilderPlugin(TestSetupEditorPlugin);
|
||||
|
||||
// Stub rename_snippet RPC to succeed if it is called
|
||||
onRpc("ir.ui.view", "rename_snippet", ({ args }) => true);
|
||||
|
||||
const customSnippets = createTestSnippets({
|
||||
snippets: [
|
||||
{
|
||||
name: "Dummy Section",
|
||||
groupName: "custom",
|
||||
keywords: ["dummy"],
|
||||
content: `
|
||||
<section data-snippet="s_dummy">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<p>TEST</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
},
|
||||
],
|
||||
withName: true,
|
||||
});
|
||||
const snippets = {
|
||||
snippet_groups: [
|
||||
'<div name="Custom" data-oe-snippet-id="123" data-o-snippet-group="custom"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: customSnippets.map((snippetDesc) => getSnippetStructure(snippetDesc)),
|
||||
snippet_custom: customSnippets.map((snippetDesc) => getSnippetStructure(snippetDesc)),
|
||||
};
|
||||
|
||||
await setupHTMLBuilder(
|
||||
`<section data-name="Dummy Section" data-snippet="s_dummy">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<p>TEST</p>
|
||||
<p><a class="btn">BUTTON</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`,
|
||||
{ snippets }
|
||||
);
|
||||
|
||||
await contains(
|
||||
".o-website-builder_sidebar .o_snippets_container .o_snippet[name='Custom'] button"
|
||||
).click();
|
||||
await animationFrame();
|
||||
|
||||
// Throw if any render_public_asset RPC happens during rename
|
||||
onRpc("ir.ui.view", "render_public_asset", () => {
|
||||
throw new Error("shouldn't make an rpc call on snippet rename");
|
||||
});
|
||||
|
||||
await contains(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_custom_snippet_edit button > .fa-pencil"
|
||||
).click();
|
||||
expect(".o-overlay-item .modal-dialog:contains('Rename the block')").toHaveCount(1);
|
||||
await contains(".o-overlay-item .modal-dialog input#inputConfirmation").fill("new custom name");
|
||||
await contains(".o-overlay-item .modal-dialog footer>button:contains('Save')").click();
|
||||
expect(
|
||||
".o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_custom_snippet_edit>span:contains('new custom name')"
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
import {
|
||||
addBuilderAction,
|
||||
addBuilderOption,
|
||||
setupHTMLBuilder,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { Builder } from "@html_builder/builder";
|
||||
import { BuilderAction } from "@html_builder/core/builder_action";
|
||||
import { SavePlugin } from "@html_builder/core/save_plugin";
|
||||
import { BaseOptionComponent } from "@html_builder/core/utils";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-dom";
|
||||
import { useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
class TestAction extends BuilderAction {
|
||||
static id = "testAction";
|
||||
isApplied({ editingElement }) {
|
||||
return editingElement.classList.contains("applied");
|
||||
}
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.toggle("applied");
|
||||
expect.step("apply");
|
||||
}
|
||||
}
|
||||
beforeEach(() => {
|
||||
addBuilderAction({
|
||||
TestAction,
|
||||
});
|
||||
});
|
||||
|
||||
test("apply is called if clean is not defined", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`<BuilderButton action="'testAction'">Click</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
await contains("[data-action-id='testAction']").click();
|
||||
expect("[data-action-id='testAction']").toHaveClass("active");
|
||||
expect.verifySteps(["apply", "apply"]); // preview, apply
|
||||
await contains("[data-action-id='testAction']").click();
|
||||
expect("[data-action-id='testAction']").not.toHaveClass("active");
|
||||
expect.verifySteps(["apply"]); // clean
|
||||
});
|
||||
|
||||
test("custom action and shorthand action: clean actions are independent, apply is called on custom action if clean is not defined", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`<BuilderButton action="'testAction'" classAction="'custom-class'">Click</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
await contains("[data-action-id='testAction']").click();
|
||||
expect("[data-action-id='testAction']").toHaveClass("active");
|
||||
expect.verifySteps(["apply", "apply"]); // preview, apply
|
||||
await contains("[data-action-id='testAction']").click();
|
||||
expect("[data-action-id='testAction']").not.toHaveClass("active");
|
||||
expect.verifySteps(["apply"]); // clean
|
||||
});
|
||||
|
||||
test("Prepare is triggered on props updated", async () => {
|
||||
const newPropDeferred = new Deferred();
|
||||
let prepareDeferred = new Promise((r) => r());
|
||||
class TestOption extends BaseOptionComponent {
|
||||
static template = xml`<BuilderCheckbox action="'customAction'" actionParam="state.param"/>`;
|
||||
static props = {};
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({ param: "old param" });
|
||||
newPropDeferred.then(() => {
|
||||
this.state.param = "new param";
|
||||
});
|
||||
}
|
||||
}
|
||||
class CustomAction extends BuilderAction {
|
||||
static id = "customAction";
|
||||
async prepare() {
|
||||
await prepareDeferred;
|
||||
expect.step("prepare");
|
||||
}
|
||||
apply() {}
|
||||
}
|
||||
addBuilderAction({
|
||||
CustomAction,
|
||||
});
|
||||
addBuilderOption({
|
||||
Component: TestOption,
|
||||
selector: ".test-options-target",
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Homepage</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
expect.verifySteps(["prepare"]);
|
||||
prepareDeferred = new Deferred();
|
||||
// Update prop
|
||||
newPropDeferred.resolve();
|
||||
await animationFrame();
|
||||
expect.verifySteps([]);
|
||||
prepareDeferred.resolve();
|
||||
await animationFrame();
|
||||
expect.verifySteps(["prepare"]);
|
||||
});
|
||||
|
||||
test("Data Attribute action works with non string values", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`<BuilderButton dataAttributeAction="'customerOrderIds'" dataAttributeActionValue="[100, 200]">Click</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
await contains(".we-bg-options-container button:contains('Click')").click();
|
||||
expect(".we-bg-options-container button:contains('Click')").toHaveClass("active");
|
||||
expect(":iframe .s_test").toHaveAttribute("data-customer-order-ids", "100,200");
|
||||
});
|
||||
|
||||
describe("isPreviewing is passed to action's apply and clean", () => {
|
||||
beforeEach(async () => {
|
||||
addBuilderAction({
|
||||
IsPreviewingAction: class extends BuilderAction {
|
||||
static id = "isPreviewing";
|
||||
isApplied({ editingElement }) {
|
||||
return editingElement.classList.contains("o_applied");
|
||||
}
|
||||
|
||||
getValue({ editingElement }) {
|
||||
return editingElement.dataset.value;
|
||||
}
|
||||
|
||||
apply({ isPreviewing, editingElement, value }) {
|
||||
expect.step(`apply ${isPreviewing}`);
|
||||
editingElement.classList.add("o_applied");
|
||||
editingElement.dataset.value = value;
|
||||
}
|
||||
|
||||
clean({ isPreviewing, editingElement }) {
|
||||
expect.step(`clean ${isPreviewing}`);
|
||||
editingElement.classList.remove("o_applied");
|
||||
delete editingElement.dataset.value;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("useClickableBuilderComponent", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'isPreviewing'" actionValue="true">Toggle</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Homepage</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// apply
|
||||
await contains("[data-action-id='isPreviewing']").click();
|
||||
expect.verifySteps(["apply true", "apply false"]);
|
||||
|
||||
// Hover something else, making sure we have a preview on next click
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// clean
|
||||
await contains("[data-action-id='isPreviewing']").click();
|
||||
expect.verifySteps(["clean true", "clean false"]);
|
||||
});
|
||||
|
||||
test("useInputBuilderComponent", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderTextInput action="'isPreviewing'"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Homepage</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// apply
|
||||
await contains("[data-action-id='isPreviewing'] input").edit("truthy");
|
||||
expect.verifySteps(["apply true", "apply false"]);
|
||||
});
|
||||
|
||||
test("useColorPickerBuilderComponent", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderColorPicker action="'isPreviewing'"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Homepage</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// apply
|
||||
await contains(".o_we_color_preview").click();
|
||||
await contains("button:contains(Custom)").click();
|
||||
await contains("button[data-color='600']").click();
|
||||
expect.verifySteps(["apply true", "apply false"]);
|
||||
});
|
||||
|
||||
test("BuilderMany2One", async () => {
|
||||
class Test extends models.Model {
|
||||
_name = "test";
|
||||
_records = [
|
||||
{ id: 1, name: "First" },
|
||||
{ id: 2, name: "Second" },
|
||||
{ id: 3, name: "Third" },
|
||||
];
|
||||
name = fields.Char();
|
||||
}
|
||||
onRpc("test", "name_search", () => [
|
||||
[1, "First"],
|
||||
[2, "Second"],
|
||||
[3, "Third"],
|
||||
]);
|
||||
|
||||
defineModels([Test]);
|
||||
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderMany2One action="'isPreviewing'" model="'test'" limit="10" allowUnselect="true"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Homepage</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// apply
|
||||
await contains(".o_select_menu button").click();
|
||||
await contains(".o_select_menu button").click(); // issue with select menu + builder many2one in tests: does not load on first open
|
||||
await contains(".o_select_menu button").click();
|
||||
await contains(".o_select_menu_item[data-choice-index='0']").click();
|
||||
expect.verifySteps(["apply true", "apply false"]);
|
||||
|
||||
// clean
|
||||
await contains(".o_select_menu + button > .oi-close").click();
|
||||
expect.verifySteps(["clean true", "clean false"]);
|
||||
});
|
||||
});
|
||||
|
||||
test("reload action: apply, clean save and reload are called in the right order (async)", async () => {
|
||||
let reloadDef, applyDef, cleanDef;
|
||||
patchWithCleanup(SavePlugin.prototype, {
|
||||
async save() {
|
||||
expect.step("save sync");
|
||||
await super.save();
|
||||
expect.step("save async");
|
||||
},
|
||||
async saveView() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 10));
|
||||
},
|
||||
});
|
||||
patchWithCleanup(Builder.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.editor.config.reloadEditor = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
expect.step("reload");
|
||||
reloadDef.resolve();
|
||||
};
|
||||
},
|
||||
});
|
||||
addBuilderAction({
|
||||
TestReloadAction: class extends BuilderAction {
|
||||
static id = "testReload";
|
||||
setup() {
|
||||
this.reload = {};
|
||||
}
|
||||
isApplied({ editingElement }) {
|
||||
return editingElement.dataset.applied === "true";
|
||||
}
|
||||
async apply({ editingElement }) {
|
||||
expect.step("apply sync");
|
||||
await applyDef;
|
||||
expect.step("apply async");
|
||||
editingElement.dataset.applied = "true";
|
||||
}
|
||||
async clean({ editingElement }) {
|
||||
expect.step("clean sync");
|
||||
await cleanDef;
|
||||
expect.step("clean async");
|
||||
editingElement.dataset.applied = "false";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'testReload'">Click</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="test-options-target">Test</section>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
|
||||
// Apply
|
||||
reloadDef = new Deferred();
|
||||
applyDef = new Deferred();
|
||||
await contains("[data-action-id='testReload']").click();
|
||||
expect.verifySteps(["apply sync"]);
|
||||
applyDef.resolve();
|
||||
await reloadDef;
|
||||
expect.verifySteps(["apply async", "save sync", "save async", "reload"]);
|
||||
|
||||
// Clean
|
||||
reloadDef = new Deferred();
|
||||
cleanDef = new Deferred();
|
||||
await contains("[data-action-id='testReload']").click();
|
||||
expect.verifySteps(["clean sync"]);
|
||||
cleanDef.resolve();
|
||||
await reloadDef;
|
||||
expect.verifySteps(["clean async", "save sync", "save async", "reload"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import {
|
||||
addBuilderPlugin,
|
||||
addBuilderOption,
|
||||
addBuilderAction,
|
||||
setupHTMLBuilder,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { BuilderAction } from "@html_builder/core/builder_action";
|
||||
import { Plugin } from "@html_editor/plugin";
|
||||
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("Undo/Redo correctly restores the stored container target", async () => {
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.remove();
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'customAction'">Test</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`
|
||||
<div data-name="Target 1" class="test-options-target target1">
|
||||
Homepage
|
||||
</div>
|
||||
<div data-name="Target 2" class="test-options-target target2">
|
||||
Homepage2
|
||||
</div>
|
||||
|
||||
`);
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
await contains(":iframe .target2").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains(".o-snippets-top-actions .fa-repeat").click();
|
||||
expect(".options-container").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Undo/Redo multiple actions always restores the action container target", async () => {
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("test");
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'customAction'">Test</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`
|
||||
<div data-name="Target 1" class="test-options-target target1">
|
||||
Homepage
|
||||
</div>
|
||||
<div data-name="Target 2" class="test-options-target target2">
|
||||
Homepage2
|
||||
</div>
|
||||
|
||||
`);
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
await contains(":iframe .target2").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
expect(":iframe .test-options-target.test").toHaveCount(2);
|
||||
// Undo everything.
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
expect(":iframe .test-options-target.test").toHaveCount(0);
|
||||
// Redo everything.
|
||||
await contains(".o-snippets-top-actions .fa-repeat").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains(".o-snippets-top-actions .fa-repeat").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
expect(":iframe .test-options-target.test").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("Undo/Redo an action that activates another target restores the old one on undo and the new one on redo", async () => {
|
||||
let editor;
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("test");
|
||||
editor.shared.builderOptions.setNextTarget(editingElement.nextElementSibling);
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'customAction'">Test</BuilderButton>`,
|
||||
});
|
||||
const { getEditor } = await setupHTMLBuilder(`
|
||||
<div data-name="Target 1" class="test-options-target target1">
|
||||
Homepage
|
||||
</div>
|
||||
<div data-name="Target 2" class="test-options-target target2">
|
||||
Homepage2
|
||||
</div>
|
||||
|
||||
`);
|
||||
editor = getEditor();
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
// Undo everything.
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains(".o-snippets-top-actions .fa-repeat").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
});
|
||||
|
||||
test("Undo/Redo an action that deactivates the containers restores the old one on undo and deactivates again on redo", async () => {
|
||||
let editor;
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("test");
|
||||
editor.shared.builderOptions.setNextTarget(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'customAction'">Test</BuilderButton>`,
|
||||
});
|
||||
const { getEditor } = await setupHTMLBuilder(`
|
||||
<div data-name="Target 1" class="test-options-target target1">
|
||||
Homepage
|
||||
</div>
|
||||
`);
|
||||
editor = getEditor();
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
expect(".options-container").toHaveCount(0);
|
||||
expect("button[data-name='blocks']").toHaveClass("active");
|
||||
// Undo everything.
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 1");
|
||||
await contains(".o-snippets-top-actions .fa-repeat").click();
|
||||
expect(".options-container").toHaveCount(0);
|
||||
expect("button[data-name='blocks']").toHaveClass("active");
|
||||
});
|
||||
|
||||
test("Containers fallback to a valid ancestor if the target disappears and restore it on undo", async () => {
|
||||
addBuilderAction({
|
||||
targetAction: class extends BuilderAction {
|
||||
static id = "targetAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.remove();
|
||||
}
|
||||
},
|
||||
ancestorAction: class extends BuilderAction {
|
||||
static id = "ancestorAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.remove();
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'targetAction'">Test</BuilderButton>`,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-ancestor",
|
||||
template: xml`<BuilderButton action="'ancestorAction'">Ancestor selected</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`
|
||||
<div data-name="Ancestor" class="test-ancestor">
|
||||
Hey I'm an ancestor
|
||||
<div data-name="Target 1" class="test-options-target target1">
|
||||
Homepage
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`);
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container[data-container-title='Ancestor']").toHaveCount(1);
|
||||
expect(".options-container[data-container-title='Target 1']").toHaveCount(1);
|
||||
await contains("[data-action-id='targetAction']").click();
|
||||
expect(".options-container[data-container-title='Ancestor']").toHaveCount(1);
|
||||
expect(".options-container[data-container-title='Target 1']").toHaveCount(0);
|
||||
expect("[data-action-id='ancestorAction']").toHaveCount(1);
|
||||
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect(".options-container[data-container-title='Ancestor']").toHaveCount(1);
|
||||
expect(".options-container[data-container-title='Target 1']").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Do not activate/update containers if the element clicked is excluded", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton classAction="'test'">Test</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`
|
||||
<div data-name="Target 1" class="test-options-target target1 o_we_no_overlay">
|
||||
Homepage
|
||||
</div>
|
||||
<div data-name="Target 2" class="test-options-target target2">
|
||||
Homepage2
|
||||
</div>
|
||||
|
||||
`);
|
||||
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveCount(0);
|
||||
await contains(":iframe .target2").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
expect(".options-container [data-class-action='test']").toHaveCount(1);
|
||||
await contains(":iframe .target1").click();
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Target 2");
|
||||
});
|
||||
|
||||
test("Do not show parent container for no_parent_containers targets", async () => {
|
||||
class TestPlugin extends Plugin {
|
||||
static id = "test";
|
||||
resources = {
|
||||
no_parent_containers: ".test-child-target",
|
||||
};
|
||||
}
|
||||
addBuilderPlugin(TestPlugin);
|
||||
addBuilderOption({
|
||||
selector: ".test-parent-target",
|
||||
template: xml`<BuilderButton classAction="'test'">Test</BuilderButton>`,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-child-target",
|
||||
template: xml`<BuilderButton classAction="'test'">Test</BuilderButton>`,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-grand-child-target",
|
||||
template: xml`<BuilderButton classAction="'test'">Test</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`
|
||||
<div data-name="Parent" class="test-parent-target">
|
||||
Parent
|
||||
<div data-name="Child" class="test-child-target">
|
||||
Child
|
||||
<div data-name="Grand-child" class="test-grand-child-target">
|
||||
Grand-Child
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await contains(":iframe .test-child-target").click();
|
||||
expect(".options-container").toHaveCount(1);
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Child");
|
||||
// Try with several layers
|
||||
await contains(":iframe .test-grand-child-target").click();
|
||||
expect(".options-container").toHaveCount(2);
|
||||
expect(".options-container:eq(0)").toHaveAttribute("data-container-title", "Child");
|
||||
expect(".options-container:eq(1)").toHaveAttribute("data-container-title", "Grand-child");
|
||||
// Make sure the parent's options still appear for itself.
|
||||
await contains(":iframe .test-parent-target").click();
|
||||
expect(".options-container").toHaveCount(1);
|
||||
expect(".options-container").toHaveAttribute("data-container-title", "Parent");
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { xml } from "@odoo/owl";
|
||||
import { contains, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("clean for save of option with selector that matches an element on the page", async () => {
|
||||
onRpc("ir.ui.view", "save", ({ args }) => true);
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderButtonGroup>
|
||||
<BuilderButton classAction="'x'"/>
|
||||
</BuilderButtonGroup>
|
||||
`,
|
||||
cleanForSave: (_) => {
|
||||
expect.step("clean for save option");
|
||||
},
|
||||
});
|
||||
const { getEditor } = await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
|
||||
const editor = getEditor();
|
||||
await contains(":iframe .test-options-target").click();
|
||||
// Add an option to mark the document as 'dirty' and trigger a "clean for
|
||||
// save" at the save of the page.
|
||||
await contains("[data-class-action='x']").click();
|
||||
await editor.shared.savePlugin.save();
|
||||
expect.verifySteps(["clean for save option"]);
|
||||
});
|
||||
|
||||
test("clean for save of option with selector and exclude that matches an element on the page", async () => {
|
||||
onRpc("ir.ui.view", "save", ({ args }) => true);
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderButtonGroup>
|
||||
<BuilderButton classAction="'x'"/>
|
||||
</BuilderButtonGroup>
|
||||
`,
|
||||
exclude: "div",
|
||||
cleanForSave: (_) => {
|
||||
expect.step("clean for save option");
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderButtonGroup>
|
||||
<BuilderButton classAction="'y'"/>
|
||||
</BuilderButtonGroup>
|
||||
`,
|
||||
});
|
||||
const { getEditor } = await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
|
||||
const editor = getEditor();
|
||||
await contains(":iframe .test-options-target").click();
|
||||
// Add an option to mark the document as 'dirty' and trigger a "clean for
|
||||
// save" at the save of the page.
|
||||
await contains("[data-class-action='y']").click();
|
||||
await editor.shared.savePlugin.save();
|
||||
// Do not expect for a clean for save as the element on the page matches the
|
||||
// 'exclude' of the option having the 'cleanForSave'.
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
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 { xml } from "@odoo/owl";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
// TODO: test composite with each spec: prepare, load, getValue
|
||||
// TODO: test reloadComposite
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("can call 2 separate actions with composite action", async () => {
|
||||
class Action1 extends BuilderAction {
|
||||
static id = "action1";
|
||||
isApplied({ editingElement, params: { mainParam: cls } }) {
|
||||
return editingElement.classList.contains(cls);
|
||||
}
|
||||
apply({ editingElement, params: { mainParam: cls } }) {
|
||||
editingElement.classList.toggle(cls);
|
||||
expect.step(`action1: ${cls}`);
|
||||
}
|
||||
}
|
||||
class Action2 extends BuilderAction {
|
||||
static id = "action2";
|
||||
isApplied({ editingElement, params: { mainParam: cls } }) {
|
||||
return editingElement.classList.contains(cls);
|
||||
}
|
||||
apply({ editingElement, params: { mainParam: cls } }) {
|
||||
editingElement.classList.toggle(cls);
|
||||
expect.step(`action2: ${cls}`);
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
Action1,
|
||||
Action2,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`
|
||||
<BuilderButton
|
||||
action="'composite'"
|
||||
actionParam="[
|
||||
{ action: 'action1', actionParam: { mainParam: 'class1' } },
|
||||
{ action: 'action2', actionParam: { mainParam: 'class2' } },
|
||||
]">
|
||||
Click
|
||||
</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect(":iframe .s_test").toHaveClass("class1 class2");
|
||||
expect.verifySteps([
|
||||
"action1: class1", // preview
|
||||
"action2: class2", // preview
|
||||
"action1: class1", // apply
|
||||
"action2: class2", // apply
|
||||
]);
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect.verifySteps(["action1: class1", "action2: class2"]); // clean
|
||||
});
|
||||
|
||||
test("can call the same action twice with composite action", async () => {
|
||||
class Action1 extends BuilderAction {
|
||||
static id = "action1";
|
||||
isApplied({ editingElement, params: { mainParam: cls } }) {
|
||||
return editingElement.classList.contains(cls);
|
||||
}
|
||||
apply({ editingElement, params: { mainParam: cls } }) {
|
||||
editingElement.classList.toggle(cls);
|
||||
expect.step(`action1: ${cls}`);
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
Action1,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`
|
||||
<BuilderButton
|
||||
action="'composite'"
|
||||
actionParam="[
|
||||
{ action: 'action1', actionParam: { mainParam: 'class1' } },
|
||||
{ action: 'action1', actionParam: { mainParam: 'class2' } },
|
||||
]">
|
||||
Click
|
||||
</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect(":iframe .s_test").toHaveClass("class1 class2");
|
||||
expect.verifySteps([
|
||||
"action1: class1", // preview
|
||||
"action1: class2", // preview
|
||||
"action1: class1", // apply
|
||||
"action1: class2", // apply
|
||||
]);
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect.verifySteps(["action1: class1", "action1: class2"]); // clean
|
||||
});
|
||||
|
||||
test("composite action's isApplied returns false if no action defined it", async () => {
|
||||
class Action1 extends BuilderAction {
|
||||
static id = "action1";
|
||||
apply({ params: { mainParam: cls } }) {
|
||||
expect.step(`action: ${cls}`);
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
Action1,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`
|
||||
<BuilderButton
|
||||
action="'composite'"
|
||||
actionParam="[
|
||||
{ action: 'action1', actionParam: { mainParam: 'class1' } },
|
||||
{ action: 'action1', actionParam: { mainParam: 'class2' } },
|
||||
]">
|
||||
Click
|
||||
</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
expect("[data-action-id='composite']").not.toHaveClass("active");
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect("[data-action-id='composite']").not.toHaveClass("active");
|
||||
expect.verifySteps([
|
||||
"action: class1", // preview
|
||||
"action: class2", // preview
|
||||
"action: class1", // apply
|
||||
"action: class2", // apply
|
||||
]);
|
||||
});
|
||||
|
||||
test("composite action's isApplied returns true if at least one action defined it", async () => {
|
||||
class Action1 extends BuilderAction {
|
||||
static id = "action1";
|
||||
apply() {}
|
||||
}
|
||||
class Action2 extends BuilderAction {
|
||||
static id = "action2";
|
||||
isApplied({ editingElement, params: { mainParam: cls } }) {
|
||||
return editingElement.classList.contains(cls);
|
||||
}
|
||||
apply({ editingElement, params: { mainParam: cls } }) {
|
||||
editingElement.classList.add(cls);
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
Action1,
|
||||
Action2,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".s_test",
|
||||
template: xml`
|
||||
<BuilderButton
|
||||
action="'composite'"
|
||||
actionParam="[
|
||||
{ action: 'action1', actionParam: { mainParam: 'class1' } },
|
||||
{ action: 'action2', actionParam: { mainParam: 'class2' } },
|
||||
]">
|
||||
Click
|
||||
</BuilderButton>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<section class="s_test">Test</section>`);
|
||||
await contains(":iframe .s_test").click();
|
||||
expect("[data-action-id='composite']").not.toHaveClass("active");
|
||||
await contains("[data-action-id='composite']").click();
|
||||
expect("[data-action-id='composite']").toHaveClass("active");
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
addBuilderOption,
|
||||
addBuilderPlugin,
|
||||
setupHTMLBuilder,
|
||||
dummyBase64Img,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { Plugin } from "@html_editor/plugin";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { xml } from "@odoo/owl";
|
||||
import { contains, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("Do not set contenteditable to true on elements inside o_not_editable", async () => {
|
||||
class TestPlugin extends Plugin {
|
||||
static id = "testPlugin";
|
||||
resources = {
|
||||
force_editable_selector: ".target",
|
||||
};
|
||||
}
|
||||
addBuilderPlugin(TestPlugin);
|
||||
await setupHTMLBuilder(`
|
||||
<section>
|
||||
<div class="o_not_editable">
|
||||
<div class="target">
|
||||
Hello
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
expect(":iframe .target").not.toHaveAttribute("contenteditable", "true");
|
||||
});
|
||||
|
||||
test("Media should not be replaceable if not inside a savable zone", async () => {
|
||||
await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<header id="top" data-anchor="true" data-name="Header">
|
||||
<i class="fa fa-shopping-cart fa-stack target" data-oe-model="ir.ui.view" data-oe-id="786" data-oe-field="arch" data-oe-xpath="/data/xpath/li[1]/a[1]"/>
|
||||
</header>`,
|
||||
styleContent: `
|
||||
.fa {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
`,
|
||||
});
|
||||
expect(":iframe .target").toHaveClass("o_editable_media");
|
||||
await contains(":iframe #wrapwrap .target").click();
|
||||
expect("span:contains('Double-click to edit')").toHaveCount(0);
|
||||
await contains(":iframe #wrapwrap .target").dblclick();
|
||||
expect(".modal-content:contains(Select a media)").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("clone of editable media inside not editable area should be editable", async () => {
|
||||
onRpc("/html_editor/get_image_info", () => ({}));
|
||||
addBuilderOption({
|
||||
selector: "section",
|
||||
template: xml`<BuilderButton classAction="'test'">Test</BuilderButton>`,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: "img",
|
||||
template: xml`<BuilderButton classAction="'test'">Test Image</BuilderButton>`,
|
||||
});
|
||||
const { waitDomUpdated } = await setupHTMLBuilder(`
|
||||
<section>
|
||||
<div class="o_not_editable">
|
||||
<img class="o_editable_media" src="${dummyBase64Img}"/>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
await contains(":iframe img").click();
|
||||
await waitDomUpdated();
|
||||
expect(".options-container[data-container-title='Image']").toBeDisplayed();
|
||||
await contains(".oe_snippet_clone").click();
|
||||
await contains(":iframe section:last-of-type img").click();
|
||||
expect(".options-container[data-container-title='Image']").toBeDisplayed();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("should not allow edition of date and datetime fields", async () => {
|
||||
await setupHTMLBuilder(
|
||||
`<time data-oe-model="blog.post" data-oe-id="3" data-oe-field="post_date" data-oe-type="datetime" data-oe-expression="blog_post.post_date" data-oe-original="2025-07-30 09:54:36" data-oe-original-with-format="07/30/2025 09:54:36" data-oe-original-tz="Europe/Brussels">
|
||||
Jul 30, 2025
|
||||
</time>`
|
||||
);
|
||||
expect(":iframe time").toHaveProperty("isContentEditable", false);
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
setupHTMLBuilder,
|
||||
waitForEndOfOperation,
|
||||
confirmAddSnippet,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const dropzone = (hovered = false) => {
|
||||
const highlightClass = hovered ? " o_dropzone_highlighted" : "";
|
||||
return `<div class="oe_drop_zone oe_insert${highlightClass}" data-editor-message-default="true" data-editor-message="DRAG BUILDING BLOCKS HERE"></div>`;
|
||||
};
|
||||
|
||||
test("wrapper element has the 'DRAG BUILDING BLOCKS HERE' message", async () => {
|
||||
const { contentEl } = await setupHTMLBuilder("");
|
||||
expect(contentEl).toHaveAttribute("data-editor-message", "DRAG BUILDING BLOCKS HERE");
|
||||
});
|
||||
|
||||
test("drop beside dropzone inserts the snippet", async () => {
|
||||
const { contentEl } = await setupHTMLBuilder();
|
||||
const { moveTo, drop } = await contains(
|
||||
".o-snippets-menu #snippet_groups .o_snippet_thumbnail"
|
||||
).drag();
|
||||
await moveTo(contentEl.ownerDocument.body);
|
||||
// The dropzone is not hovered, so not highlighted.
|
||||
expect(contentEl).toHaveInnerHTML(dropzone());
|
||||
await drop();
|
||||
await confirmAddSnippet();
|
||||
expect(".o_add_snippet_dialog").toHaveCount(0);
|
||||
await waitForEndOfOperation();
|
||||
expect(contentEl)
|
||||
.toHaveInnerHTML(`<section class="s_test" data-snippet="s_test" data-name="Test">
|
||||
<div class="test_a o-paragraph">
|
||||
<br>
|
||||
</div>
|
||||
</section>`);
|
||||
});
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { setSelection } from "@html_editor/../tests/_helpers/selection";
|
||||
import { expandToolbar } from "@html_editor/../tests/_helpers/toolbar";
|
||||
import { insertText, pasteHtml } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import { FontPlugin } from "@html_editor/main/font/font_plugin";
|
||||
import { isTextNode } from "@html_editor/utils/dom_info";
|
||||
import { parseHTML } from "@html_editor/utils/html";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
manuallyDispatchProgrammaticEvent,
|
||||
waitFor,
|
||||
queryOne,
|
||||
waitForNone,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("should add an icon from the media modal dialog", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>x</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.document.querySelector("p");
|
||||
editor.shared.selection.focusEditable();
|
||||
editor.shared.selection.setSelection({
|
||||
anchorNode: p,
|
||||
anchorOffset: 1,
|
||||
focusNode: p,
|
||||
focusOffset: 1,
|
||||
});
|
||||
await insertText(editor, "/image");
|
||||
await animationFrame();
|
||||
await contains(".o-we-command").click();
|
||||
await contains(".modal .modal-body .nav-item:nth-child(3) a").click();
|
||||
await contains(".modal .modal-body .fa-heart").click();
|
||||
expect(p).toHaveInnerHTML(`x<span class="fa fa-heart" contenteditable="false">\u200b</span>`);
|
||||
});
|
||||
|
||||
test("should delete text forward", async () => {
|
||||
const keyPress = async (editor, key) => {
|
||||
await manuallyDispatchProgrammaticEvent(editor.editable, "keydown", { key });
|
||||
await manuallyDispatchProgrammaticEvent(editor.editable, "keyup", { key });
|
||||
};
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p><p>def</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
editor.shared.selection.setSelection({ anchorNode: p, anchorOffset: 1 });
|
||||
await keyPress(editor, "delete");
|
||||
// paragraphs get merged
|
||||
expect(p).toHaveInnerHTML("abcdef");
|
||||
await keyPress(editor, "delete");
|
||||
// following character gets deleted
|
||||
expect(p).toHaveInnerHTML("abcef");
|
||||
});
|
||||
|
||||
test("unsplittable node predicates should not crash when called with text node argument", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p>`);
|
||||
const editor = getEditor();
|
||||
const textNode = editor.editable.querySelector("p").firstChild;
|
||||
expect(isTextNode(textNode)).toBe(true);
|
||||
expect(() =>
|
||||
editor.resources.unsplittable_node_predicates.forEach((p) => p(textNode))
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test("should set contenteditable to false on .o_not_editable elements", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`
|
||||
<div class="o_not_editable">
|
||||
<p>abc</p>
|
||||
</div>
|
||||
`);
|
||||
const editor = getEditor();
|
||||
const div = editor.editable.querySelector("div.o_not_editable");
|
||||
expect(div).toHaveAttribute("contenteditable", "false");
|
||||
|
||||
// Add a snippet-like element
|
||||
const snippetHtml = `
|
||||
<section class="o_not_editable">
|
||||
<p>abc</p>
|
||||
</section>
|
||||
`;
|
||||
const snippet = parseHTML(editor.document, snippetHtml).firstChild;
|
||||
div.after(snippet);
|
||||
editor.shared.history.addStep();
|
||||
// Normalization should set contenteditable to false
|
||||
expect(snippet).toHaveAttribute("contenteditable", "false");
|
||||
});
|
||||
|
||||
test("should preserve iframe in the toolbar's font size input", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`
|
||||
<section class="s_text_block pt40 pb40 o_colored_level" data-snippet="s_text_block" data-name="Text">
|
||||
<div class="container s_allow_columns">
|
||||
<p>Some text.</p>
|
||||
<p>Some more text.</p>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
const p2 = p.nextElementSibling;
|
||||
// Activate the text block snippet.
|
||||
click(p);
|
||||
|
||||
// Select the word "more".
|
||||
editor.shared.selection.setSelection({
|
||||
anchorNode: p2.firstChild,
|
||||
anchorOffset: 5,
|
||||
focusNode: p2.firstChild,
|
||||
focusOffset: 9,
|
||||
});
|
||||
await waitFor(".o-we-toolbar");
|
||||
// Get the font size selector input.
|
||||
let iframeEl = queryOne(".o-we-toolbar [name='font_size_selector'] iframe");
|
||||
let inputEl = iframeEl.contentWindow.document?.querySelector("input");
|
||||
// Change the font style from paragraph to paragraph.
|
||||
await contains(".o-we-toolbar .btn[name='font'].dropdown-toggle").click();
|
||||
await waitFor(".btn[name='font'].dropdown-toggle.show");
|
||||
await contains(".dropdown-menu [name='p']").click();
|
||||
iframeEl = queryOne(".o-we-toolbar [name='font_size_selector'] iframe");
|
||||
let newInputEl = iframeEl.contentWindow.document?.querySelector("input");
|
||||
expect(newInputEl).toBe(inputEl); // The input shouldn't have been changed.
|
||||
|
||||
// Select the first word "text".
|
||||
editor.shared.selection.setSelection({
|
||||
anchorNode: p.firstChild,
|
||||
anchorOffset: 5,
|
||||
focusNode: p.firstChild,
|
||||
focusOffset: 9,
|
||||
});
|
||||
await waitFor(".o-we-toolbar");
|
||||
// Get the font size selector input.
|
||||
iframeEl = queryOne(".o-we-toolbar [name='font_size_selector'] iframe");
|
||||
inputEl = iframeEl.contentWindow.document?.querySelector("input");
|
||||
// Change the font style from paragraph to header 1.
|
||||
await contains(".o-we-toolbar .btn[name='font'].dropdown-toggle").click();
|
||||
await waitFor(".btn[name='font'].dropdown-toggle.show");
|
||||
await contains(".dropdown-menu [name='h2']").click();
|
||||
iframeEl = queryOne(".o-we-toolbar [name='font_size_selector'] iframe");
|
||||
newInputEl = iframeEl.contentWindow.document?.querySelector("input");
|
||||
expect(newInputEl).toBe(inputEl); // The input shouldn't have been changed.
|
||||
});
|
||||
|
||||
test("should apply default table classes on paste", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p><br></p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.document.querySelector("p");
|
||||
editor.shared.selection.focusEditable();
|
||||
editor.shared.selection.setSelection({
|
||||
anchorNode: p,
|
||||
anchorOffset: 0,
|
||||
});
|
||||
pasteHtml(editor, `<table><tr><td>1234</td></tr></table>`);
|
||||
expect(editor.document.querySelector("table")).toHaveClass("table table-bordered");
|
||||
});
|
||||
|
||||
describe("toolbar dropdowns", () => {
|
||||
const setup = async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 });
|
||||
await waitFor(".o-we-toolbar");
|
||||
await expandToolbar();
|
||||
return { editor, p };
|
||||
};
|
||||
|
||||
const focusAndClick = async (selector) => {
|
||||
const target = await waitFor(selector);
|
||||
manuallyDispatchProgrammaticEvent(target, "mousedown");
|
||||
manuallyDispatchProgrammaticEvent(target, "focus");
|
||||
await animationFrame();
|
||||
// Dropdown menu needs another animation frame to be closed after the
|
||||
// toolbar is closed.
|
||||
await animationFrame();
|
||||
expect(target).toBeVisible();
|
||||
manuallyDispatchProgrammaticEvent(target, "mouseup");
|
||||
manuallyDispatchProgrammaticEvent(target, "click");
|
||||
};
|
||||
|
||||
test("list dropdown should not close on click", async () => {
|
||||
const { editor } = await setup();
|
||||
click(".o-we-toolbar .btn[name='list_selector']");
|
||||
const bulletedListButtonSelector = ".dropdown-menu button[name='bulleted_list']";
|
||||
await focusAndClick(bulletedListButtonSelector);
|
||||
await animationFrame();
|
||||
expect(bulletedListButtonSelector).toBeVisible();
|
||||
expect(bulletedListButtonSelector).toHaveClass("active");
|
||||
expect(!!editor.editable.querySelector("ul li")).toBe(true);
|
||||
});
|
||||
|
||||
test("text alignment dropdown should not close on click", async () => {
|
||||
const { p } = await setup();
|
||||
click(".o-we-toolbar .btn[name='text_align']");
|
||||
const alignCenterButtonSelector = ".dropdown-menu button.fa-align-center";
|
||||
await focusAndClick(alignCenterButtonSelector);
|
||||
await animationFrame();
|
||||
expect(alignCenterButtonSelector).toBeVisible();
|
||||
expect(alignCenterButtonSelector).toHaveClass("active");
|
||||
expect(p).toHaveStyle("text-align: center");
|
||||
});
|
||||
|
||||
test("font style dropdown should close only after click", async () => {
|
||||
const { editor } = await setup();
|
||||
click(".o-we-toolbar .btn[name='font']");
|
||||
await focusAndClick(".dropdown-menu .dropdown-item[name='h2']");
|
||||
await animationFrame();
|
||||
expect(!!editor.editable.querySelector("h2")).toBe(true);
|
||||
});
|
||||
|
||||
test("font size dropdown should close only after click", async () => {
|
||||
patchWithCleanup(FontPlugin.prototype, {
|
||||
get fontSizeItems() {
|
||||
return [{ name: "test", className: "test-font-size" }];
|
||||
},
|
||||
});
|
||||
const { p } = await setup();
|
||||
click(".o-we-toolbar .btn[name='font_size_selector']");
|
||||
await focusAndClick(".dropdown-menu .dropdown-item");
|
||||
await animationFrame();
|
||||
expect(p.firstChild).toHaveClass("test-font-size");
|
||||
});
|
||||
|
||||
test("font selector dropdown should not have normal as an option", async () => {
|
||||
await setup();
|
||||
click(".o-we-toolbar .btn[name='font']");
|
||||
await animationFrame();
|
||||
expect(".o_font_selector_menu .o-dropdown-item[name='div']").toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("font types", () => {
|
||||
test("Header 1 Display 1 to 4 are available", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 });
|
||||
await waitFor(".o-we-toolbar");
|
||||
click(".o-we-toolbar .btn[name='font']");
|
||||
await waitFor(".o_font_selector_menu");
|
||||
const expectedButtons = [
|
||||
"Header 1 Display 1",
|
||||
"Header 1 Display 2",
|
||||
"Header 1 Display 3",
|
||||
"Header 1 Display 4",
|
||||
];
|
||||
expectedButtons.forEach((button) => {
|
||||
expect(`.o_font_selector_menu .o-dropdown-item:contains('${button}')`).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
test("'Light' is available", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 });
|
||||
await waitFor(".o-we-toolbar");
|
||||
click(".o-we-toolbar .btn[name='font']");
|
||||
await waitFor(".o_font_selector_menu");
|
||||
expect(`.o_font_selector_menu .o-dropdown-item:contains('Light')`).toHaveCount(1);
|
||||
click(".o_font_selector_menu .o-dropdown-item:contains('Light')");
|
||||
await waitForNone(".o_font_selector_menu");
|
||||
expect(".o-we-toolbar .btn[name='font']").toHaveText("Light");
|
||||
expect(editor.editable.querySelector("p")).toHaveClass("lead");
|
||||
});
|
||||
test("'Small' is available", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(`<p>abc</p>`);
|
||||
const editor = getEditor();
|
||||
const p = editor.editable.querySelector("p");
|
||||
setSelection({ anchorNode: p, anchorOffset: 0, focusOffset: 1 });
|
||||
await waitFor(".o-we-toolbar");
|
||||
click(".o-we-toolbar .btn[name='font']");
|
||||
await waitFor(".o_font_selector_menu");
|
||||
expect(`.o_font_selector_menu .o-dropdown-item:contains('Small')`).toHaveCount(1);
|
||||
click(".o_font_selector_menu .o-dropdown-item:contains('Small')");
|
||||
await waitForNone(".o_font_selector_menu");
|
||||
expect(".o-we-toolbar .btn[name='font']").toHaveText("Small");
|
||||
expect(editor.editable.querySelector("p")).toHaveClass("small");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { undo } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
describe("replicate changes", () => {
|
||||
test("translated elements", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
queryOne(":iframe .test-2 span").append(" ici");
|
||||
const editor = getEditor();
|
||||
editor.shared.history.addStep();
|
||||
expect(":iframe span:contains(Contactez-nous ici)").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("link and non-link elements", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(
|
||||
`
|
||||
<div class="test-4">
|
||||
<a data-oe-xpath="/t[1]/nav[1]/div[1]/div[1]/t[2]/ul[1]/li[2]/a[1]/" href="/blog/travel-1" data-oe-model="blog.blog" data-oe-id="1" data-oe-field="name" data-oe-type="char" data-oe-expression="nav_blog.name">Travel</a>
|
||||
</div>
|
||||
`,
|
||||
{
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<b data-oe-xpath="/t[1]/nav[1]/div[1]/ul[1]/li[3]/a[1]/b[1]" data-oe-model="blog.blog" data-oe-id="1" data-oe-field="name" data-oe-type="char" data-oe-expression="nav_blog.name">Travel</b>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<a data-oe-xpath="/t[1]/div[1]/div[1]/b[1]/a[1]" href="/blog/travel-1" data-oe-model="blog.post" data-oe-id="1" data-oe-field="blog_id" data-oe-type="many2one" data-oe-expression="blog_post.blog_id" data-oe-many2one-id="1" data-oe-many2one-model="blog.blog">Travel</a>
|
||||
</div>
|
||||
<div class="test-3">
|
||||
<span data-oe-xpath="/t[1]/nav[1]/div[1]/div[1]/t[2]/ul[1]/li[2]/a[1]/span[1]" data-oe-model="blog.blog" data-oe-id="1" data-oe-field="name" data-oe-type="char" data-oe-expression="nav_blog.name">Travel</span>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
);
|
||||
const editor = getEditor();
|
||||
queryOne(":iframe .test-1 b").append(" Abroad");
|
||||
editor.shared.history.addStep();
|
||||
expect(":iframe .test-1 b").toHaveText("Travel Abroad");
|
||||
expect(":iframe .test-2 a").toHaveText("Travel Abroad");
|
||||
expect(":iframe .test-3 span").toHaveText("Travel Abroad");
|
||||
expect(":iframe .test-4 a").toHaveInnerHTML("\u{FEFF}Travel Abroad\u{FEFF}"); // link in editable get feff
|
||||
|
||||
queryOne(":iframe .test-4 a").append("!"); // the feff should not be forwarded
|
||||
editor.shared.history.addStep();
|
||||
expect(":iframe .test-1 b").toHaveText("Travel Abroad!");
|
||||
expect(":iframe .test-2 a").toHaveText("Travel Abroad!");
|
||||
expect(":iframe .test-3 span").toHaveText("Travel Abroad!");
|
||||
expect(":iframe .test-4 a").toHaveInnerHTML("\u{FEFF}Travel Abroad!\u{FEFF}");
|
||||
});
|
||||
|
||||
test("menu items", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<span data-oe-model="website.menu" data-oe-id="5" data-oe-field="name" data-oe-type="char" data-oe-expression="submenu.name">Home</span>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<span data-oe-model="website.menu" data-oe-id="5" data-oe-field="name" data-oe-type="char" data-oe-expression="submenu.name">Home</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
queryOne(":iframe .test-1 span").append("y");
|
||||
const editor = getEditor();
|
||||
editor.shared.history.addStep();
|
||||
expect(":iframe span:contains(Homey)").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("contact", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<span data-oe-xpath="/t[1]/div[1]/div[2]/span[1]" data-oe-model="blog.post" data-oe-id="1" data-oe-field="author_id" data-oe-type="contact" data-oe-expression="blog_post.author_id" data-oe-many2one-id="3" data-oe-many2one-model="res.partner" data-oe-contact-options="{"widget": "contact", "fields": ["name"], "tagName": "span", "expression": "blog_post.author_id", "type": "contact", "inherit_branding": true, "translate": false}">
|
||||
<address class="o_portal_address mb-0">
|
||||
<div>
|
||||
<span itemprop="name">YourCompany, Mitchell Admin</span>
|
||||
</div>
|
||||
<div class="gap-2" itemscope="itemscope" itemtype="http://schema.org/PostalAddress">
|
||||
<div itemprop="telephone"></div>
|
||||
</div>
|
||||
</address>
|
||||
</span>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<span data-oe-xpath="/t[1]/div[1]/div[2]/span[1]" data-oe-model="blog.post" data-oe-id="1" data-oe-field="author_id" data-oe-type="contact" data-oe-expression="blog_post.author_id" data-oe-many2one-id="3" data-oe-many2one-model="res.partner" data-oe-contact-options="{"widget": "contact", "fields": ["name"], "tagName": "span", "expression": "blog_post.author_id", "type": "contact", "inherit_branding": true, "translate": false}">
|
||||
<address class="o_portal_address mb-0">
|
||||
<div>
|
||||
<span itemprop="name">YourCompany, Mitchell Admin</span>
|
||||
</div>
|
||||
<div class="gap-2" itemscope="itemscope" itemtype="http://schema.org/PostalAddress">
|
||||
<div itemprop="telephone"></div>
|
||||
</div>
|
||||
</address>
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
queryOne(":iframe .test-1 > *").append("changed");
|
||||
const editor = getEditor();
|
||||
editor.shared.history.addStep();
|
||||
expect(":iframe .test-1 > *").toHaveText(/changed/);
|
||||
expect(":iframe .test-2 > *").toHaveText(/changed/);
|
||||
});
|
||||
|
||||
test("should not add o_dirty marks on the ones receiving the replicated changes", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
<div class="test-3">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
const span1 = queryOne(":iframe .test-1 span");
|
||||
const span2 = queryOne(":iframe .test-2 span");
|
||||
const span3 = queryOne(":iframe .test-3 span");
|
||||
|
||||
const editor = getEditor();
|
||||
span2.append(" ici");
|
||||
editor.shared.history.addStep();
|
||||
expect(span1).not.toHaveClass("o_dirty");
|
||||
expect(span2).toHaveClass("o_dirty");
|
||||
expect(span3).not.toHaveClass("o_dirty");
|
||||
expect([span1, span2, span3]).toHaveText("Contactez-nous ici");
|
||||
|
||||
span1.append("!");
|
||||
editor.shared.history.addStep();
|
||||
expect(span1).toHaveClass("o_dirty");
|
||||
expect(span2).toHaveClass("o_dirty");
|
||||
expect(span3).not.toHaveClass("o_dirty");
|
||||
expect([span1, span2, span3]).toHaveText("Contactez-nous ici!");
|
||||
|
||||
undo(editor);
|
||||
expect(span1).not.toHaveClass("o_dirty");
|
||||
expect(span2).toHaveClass("o_dirty");
|
||||
expect(span3).not.toHaveClass("o_dirty");
|
||||
expect([span1, span2, span3]).toHaveText("Contactez-nous ici");
|
||||
});
|
||||
|
||||
test("changing several of occurences at the same time should converge to the same value", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder("", {
|
||||
headerContent: `
|
||||
<div class="test-1">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
<div class="test-2">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
<div class="test-3">
|
||||
<span data-oe-model="ir.ui.view" data-oe-id="600" data-oe-field="arch_db" data-oe-translation-state="translated" data-oe-translation-source-sha="4242">Contactez-nous</span>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
const span1 = queryOne(":iframe .test-1 span");
|
||||
const span2 = queryOne(":iframe .test-2 span");
|
||||
const span3 = queryOne(":iframe .test-3 span");
|
||||
|
||||
span2.append(" ici");
|
||||
span1.append("!");
|
||||
const editor = getEditor();
|
||||
editor.shared.history.addStep();
|
||||
expect(span1).toHaveClass("o_dirty");
|
||||
expect(span2).toHaveClass("o_dirty");
|
||||
expect(span3).not.toHaveClass("o_dirty");
|
||||
expect([span2, span3]).toHaveText(span1.textContent); // all the same text
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,544 @@
|
|||
import { Builder } from "@html_builder/builder";
|
||||
import { CORE_PLUGINS } from "@html_builder/core/core_plugins";
|
||||
import { Img } from "@html_builder/core/img";
|
||||
import { SetupEditorPlugin } from "@html_builder/core/setup_editor_plugin";
|
||||
import { unformat } from "@html_editor/../tests/_helpers/format";
|
||||
import { setContent } from "@html_editor/../tests/_helpers/selection";
|
||||
import { insertText } from "@html_editor/../tests/_helpers/user_actions";
|
||||
import { LocalOverlayContainer } from "@html_editor/local_overlay_container";
|
||||
import { Plugin } from "@html_editor/plugin";
|
||||
import { withSequence } from "@html_editor/utils/resource";
|
||||
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { after } from "@odoo/hoot";
|
||||
import { animationFrame, waitForNone, queryOne, waitFor, advanceTime, tick } from "@odoo/hoot-dom";
|
||||
import { Component, onMounted, useRef, useState, useSubEnv, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { loadBundle } from "@web/core/assets";
|
||||
import { isBrowserFirefox } from "@web/core/browser/feature_detection";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uniqueId } from "@web/core/utils/functions";
|
||||
|
||||
export function patchWithCleanupImg() {
|
||||
const defaultImg =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z9DwHwAGBQKA3H7sNwAAAABJRU5ErkJggg==";
|
||||
patchWithCleanup(Img, {
|
||||
template: xml`<img t-att-data-src="props.src" t-att-alt="props.alt" t-att-class="props.class" t-att-style="props.style" t-att="props.attrs" src="${defaultImg}"/>`,
|
||||
});
|
||||
patchWithCleanup(Img.prototype, {
|
||||
loadImage: () => {},
|
||||
getSvg: function () {
|
||||
this.isSvg = () => false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getSnippetView(snippets) {
|
||||
const { snippet_groups, snippet_custom, snippet_structure, snippet_content } = snippets;
|
||||
return `
|
||||
<snippets id="snippet_groups" string="Categories">
|
||||
${(snippet_groups || []).join("")}
|
||||
</snippets>
|
||||
<snippets id="snippet_structure" string="Structure">
|
||||
${(snippet_structure || []).join("")}
|
||||
</snippets>
|
||||
<snippets id="snippet_custom" string="Custom">
|
||||
${(snippet_custom || []).join("")}
|
||||
</snippets>
|
||||
<snippets id="snippet_content" string="Inner Content">
|
||||
${(snippet_content || []).join("")}
|
||||
</snippets>`;
|
||||
}
|
||||
|
||||
export function getInnerContent({
|
||||
name,
|
||||
content,
|
||||
keywords = [],
|
||||
imagePreview = "",
|
||||
thumbnail = "",
|
||||
}) {
|
||||
keywords = keywords.join(", ");
|
||||
return `<div name="${name}" data-oe-type="snippet" data-oe-snippet-id="456" data-o-image-preview="${imagePreview}" data-oe-thumbnail="${thumbnail}" data-oe-keywords="${keywords}">${content}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates snippet structure HTML for test fixtures
|
||||
* @param {Object} options - Snippet structure configuration
|
||||
* @param {string} options.name - The display name of the snippet
|
||||
* @param {string} options.content - The HTML content of the snippet
|
||||
* @param {string[]} [options.keywords=[]] - Search keywords for the snippet
|
||||
* @param {string} options.groupName - The snippet group (category) name
|
||||
* @param {string} [options.imagePreview=""] - URL to preview image
|
||||
* @param {string|number} [options.moduleId=""] - Module ID if snippet belongs to a module
|
||||
* @param {string} [options.moduleDisplayName=""] - Human-readable module name
|
||||
* @returns {string} HTML string for the snippet structure
|
||||
*/
|
||||
export function getSnippetStructure({
|
||||
name,
|
||||
content,
|
||||
keywords = [],
|
||||
groupName,
|
||||
imagePreview = "",
|
||||
moduleId = "",
|
||||
moduleDisplayName = "",
|
||||
}) {
|
||||
keywords = keywords.join(", ");
|
||||
return `<div name="${name}" data-oe-snippet-id="123" data-o-image-preview="${imagePreview}" data-oe-keywords="${keywords}" data-o-group="${groupName}" data-module-id="${moduleId}" data-module-display-name="${moduleDisplayName}">${content}</div>`;
|
||||
}
|
||||
|
||||
class BuilderContainer extends Component {
|
||||
static template = xml`
|
||||
<div class="d-flex h-100 w-100" t-ref="container">
|
||||
<div class="o_website_preview flex-grow-1" t-ref="website_preview">
|
||||
<div class="o_iframe_container">
|
||||
<iframe class="h-100 w-100" t-ref="iframe" t-on-load="onLoad"/>
|
||||
<div t-if="this.state.isMobile" class="o_mobile_preview_layout">
|
||||
<img alt="phone" src="/html_builder/static/img/phone.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LocalOverlayContainer localOverlay="overlayRef" identifier="env.localOverlayContainerKey"/>
|
||||
<div t-if="state.isEditing" t-att-class="{'o_builder_sidebar_open': state.isEditing and state.showSidebar}" class="o-website-builder_sidebar border-start border-dark">
|
||||
<Builder t-props="this.getBuilderProps()"/>
|
||||
</div>
|
||||
</div>`;
|
||||
static components = { Builder, LocalOverlayContainer };
|
||||
static props = {
|
||||
content: String,
|
||||
editableSelector: String,
|
||||
headerContent: String,
|
||||
Plugins: Array,
|
||||
onEditorLoad: Function,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.state = useState({ isMobile: false, isEditing: false, showSidebar: true });
|
||||
this.iframeRef = useRef("iframe");
|
||||
const originalIframeLoaded = new Promise((resolve) => {
|
||||
this._originalIframeLoadedResolve = resolve;
|
||||
});
|
||||
this.iframeLoaded = new Promise((resolve) => {
|
||||
onMounted(async () => {
|
||||
if (isBrowserFirefox()) {
|
||||
await originalIframeLoaded;
|
||||
}
|
||||
|
||||
const el = this.iframeRef.el;
|
||||
el.contentDocument.body.innerHTML = `<div id="wrapwrap">${this.props.headerContent}<div id="wrap" class="oe_structure oe_empty" data-oe-model="ir.ui.view" data-oe-id="539" data-oe-field="arch">${this.props.content}</div></div>`;
|
||||
resolve(el);
|
||||
});
|
||||
});
|
||||
useSubEnv({
|
||||
builderRef: useRef("container"),
|
||||
});
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this._originalIframeLoadedResolve();
|
||||
}
|
||||
|
||||
getBuilderProps() {
|
||||
return {
|
||||
onEditorLoad: this.props.onEditorLoad,
|
||||
closeEditor: () => {},
|
||||
snippetsName: "",
|
||||
toggleMobile: () => {
|
||||
this.state.isMobile = !this.state.isMobile;
|
||||
},
|
||||
overlayRef: () => {},
|
||||
editableSelector: this.props.editableSelector,
|
||||
iframeLoaded: this.iframeLoaded,
|
||||
isMobile: this.state.isMobile,
|
||||
Plugins: this.props.Plugins,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class IrUiView extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
render_public_asset() {
|
||||
throw new Error("This should be implemented by some helper");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef { import("@html_editor/editor").Editor } Editor
|
||||
*
|
||||
* @param {String} content
|
||||
* @param {Object} options
|
||||
* @param {String} options.headerContent
|
||||
* @param {*} options.snippetContent
|
||||
* @param {*} options.dropzoneSelectors
|
||||
* @param {*} options.snippets
|
||||
* @param {*} options.styleContent
|
||||
* @returns {Promise<{
|
||||
* getEditor: () => Editor,
|
||||
* getEditableContent: () => HTMLElement,
|
||||
* contentEl: HTMLElement,
|
||||
* builderEl: HTMLElement,
|
||||
* waitDomUpdated: () => Promise<void>
|
||||
* }>}
|
||||
}}
|
||||
*/
|
||||
export async function setupHTMLBuilder(
|
||||
content = "",
|
||||
{
|
||||
editableSelector = "#wrapwrap",
|
||||
headerContent = "",
|
||||
snippetContent,
|
||||
dropzoneSelectors,
|
||||
snippets,
|
||||
styleContent,
|
||||
} = {}
|
||||
) {
|
||||
defineMailModels();
|
||||
defineModels([IrUiView]);
|
||||
|
||||
patchWithCleanupImg();
|
||||
|
||||
if (!snippets) {
|
||||
snippets = {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: [
|
||||
getSnippetStructure({
|
||||
name: "Test",
|
||||
groupName: "a",
|
||||
content: `<section class="s_test" data-snippet="s_test" data-name="Test">
|
||||
<div class="test_a"></div>
|
||||
</section>`,
|
||||
}),
|
||||
],
|
||||
// TODO: maybe we should use the same structure as in the snippets?
|
||||
snippet_content: snippetContent || [
|
||||
`<section class="s_test" data-snippet="s_test" data-name="Test">
|
||||
<div class="test_a"></div>
|
||||
</section>`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
patchWithCleanup(IrUiView.prototype, {
|
||||
render_public_asset: () => getSnippetView(snippets),
|
||||
});
|
||||
|
||||
const Plugins = [...CORE_PLUGINS];
|
||||
|
||||
if (dropzoneSelectors) {
|
||||
const pluginId = uniqueId("test-dropzone-selector");
|
||||
|
||||
class P extends Plugin {
|
||||
static id = pluginId;
|
||||
resources = {
|
||||
dropzone_selector: dropzoneSelectors,
|
||||
};
|
||||
}
|
||||
Plugins.push(P);
|
||||
}
|
||||
|
||||
const BuilderPlugins = registry.category("builder-plugins").getAll();
|
||||
Plugins.push(...BuilderPlugins);
|
||||
|
||||
let lastUpdatePromise;
|
||||
const waitDomUpdated = async () => {
|
||||
// The tick ensures that lastUpdatePromise has correctly been assigned
|
||||
await tick();
|
||||
await lastUpdatePromise;
|
||||
await animationFrame();
|
||||
};
|
||||
patchWithCleanup(Builder.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
patchWithCleanup(this.env.editorBus, {
|
||||
trigger(eventName, detail) {
|
||||
if (eventName === "DOM_UPDATED") {
|
||||
lastUpdatePromise = detail.updatePromise;
|
||||
}
|
||||
return super.trigger(eventName, detail);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let _resolve;
|
||||
const prom = new Promise((resolve) => {
|
||||
_resolve = resolve;
|
||||
});
|
||||
|
||||
let editableContent;
|
||||
// hack to get a promise that resolves when editor is ready
|
||||
patchWithCleanup(SetupEditorPlugin.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
_resolve();
|
||||
editableContent = this.getEditableElements(
|
||||
'.oe_structure.oe_empty, [data-oe-type="html"]'
|
||||
)[0];
|
||||
},
|
||||
});
|
||||
|
||||
let attachedEditor;
|
||||
const comp = await mountWithCleanup(BuilderContainer, {
|
||||
props: {
|
||||
content,
|
||||
editableSelector,
|
||||
headerContent,
|
||||
Plugins,
|
||||
onEditorLoad: (editor) => {
|
||||
attachedEditor = editor;
|
||||
},
|
||||
},
|
||||
});
|
||||
await comp.iframeLoaded;
|
||||
if (styleContent) {
|
||||
const iframeDocument = queryOne(":iframe");
|
||||
const styleEl = iframeDocument.createElement("style");
|
||||
styleEl.textContent = styleContent;
|
||||
iframeDocument.head.appendChild(styleEl);
|
||||
}
|
||||
comp.state.isEditing = true;
|
||||
await prom;
|
||||
await animationFrame();
|
||||
return {
|
||||
getEditor: () => attachedEditor,
|
||||
getEditableContent: () => editableContent,
|
||||
contentEl: comp.iframeRef.el.contentDocument.body.firstChild.firstChild,
|
||||
builderEl: comp.env.builderRef.el.querySelector(".o-website-builder_sidebar"),
|
||||
waitDomUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
export function addBuilderPlugin(Plugin) {
|
||||
registry.category("builder-plugins").add(Plugin.id, Plugin);
|
||||
after(() => {
|
||||
registry.category("builder-plugins").remove(Plugin.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function addBuilderOption({
|
||||
selector,
|
||||
exclude,
|
||||
applyTo,
|
||||
template,
|
||||
Component,
|
||||
sequence,
|
||||
cleanForSave,
|
||||
props,
|
||||
editableOnly,
|
||||
title,
|
||||
reloadTarget,
|
||||
}) {
|
||||
const pluginId = uniqueId("test-option");
|
||||
const option = {
|
||||
pluginId,
|
||||
OptionComponent: Component,
|
||||
template,
|
||||
selector,
|
||||
exclude,
|
||||
applyTo,
|
||||
sequence,
|
||||
cleanForSave,
|
||||
props,
|
||||
editableOnly,
|
||||
title,
|
||||
reloadTarget,
|
||||
};
|
||||
|
||||
const P = {
|
||||
[pluginId]: class extends Plugin {
|
||||
static id = pluginId;
|
||||
resources = {
|
||||
builder_options: sequence ? withSequence(sequence, option) : option,
|
||||
};
|
||||
},
|
||||
}[pluginId];
|
||||
|
||||
addBuilderPlugin(P);
|
||||
}
|
||||
|
||||
export function addBuilderAction(actions = {}) {
|
||||
const pluginId = uniqueId("test-action-plugin");
|
||||
class P extends Plugin {
|
||||
static id = pluginId;
|
||||
resources = {
|
||||
builder_actions: actions,
|
||||
};
|
||||
}
|
||||
addBuilderPlugin(P);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dragged helper when drag and dropping snippets.
|
||||
*/
|
||||
export function getDragHelper() {
|
||||
return document.body.querySelector(".o_draggable_dragging .o_snippet_thumbnail");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dragged helper when drag and dropping elements from the page.
|
||||
*/
|
||||
export function getDragMoveHelper() {
|
||||
return document.body.querySelector(".o_drag_move_helper");
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the loading element added by the mutex to be removed, indicating
|
||||
* that the operation is over.
|
||||
*/
|
||||
export async function waitForEndOfOperation() {
|
||||
await advanceTime(500);
|
||||
await waitForNone(":iframe .o_loading_screen");
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
export function addDropZoneSelector(selector) {
|
||||
const pluginId = uniqueId("test-dropzone-selector");
|
||||
|
||||
class P extends Plugin {
|
||||
static id = pluginId;
|
||||
resources = {
|
||||
dropzone_selector: [selector],
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("builder-plugins").add(pluginId, P);
|
||||
after(() => {
|
||||
registry.category("builder-plugins").remove(P);
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForSnippetDialog() {
|
||||
await animationFrame();
|
||||
await loadBundle("web.assets_frontend", {
|
||||
targetDoc: queryOne("iframe.o_add_snippet_iframe").contentDocument,
|
||||
js: false,
|
||||
});
|
||||
await loadBundle("html_builder.iframe_add_dialog", {
|
||||
targetDoc: queryOne("iframe.o_add_snippet_iframe").contentDocument,
|
||||
js: false,
|
||||
});
|
||||
await waitFor(".o_add_snippet_dialog iframe.show.o_add_snippet_iframe");
|
||||
}
|
||||
|
||||
export async function modifyText(editor, editableContent) {
|
||||
setContent(editableContent, '<h1 class="title">H[]ello</h1>');
|
||||
editor.shared.history.addStep();
|
||||
await insertText(editor, "1");
|
||||
}
|
||||
|
||||
// Snippet Testing Helpers
|
||||
// Use createTestSnippets() for most cases to replace repetitive getSnippetsDescription functions
|
||||
// Use getBasicSection() for simple HTML section generation
|
||||
|
||||
/**
|
||||
* Creates a basic HTML section structure for test snippets
|
||||
* @param {string} content - The content to place inside the section
|
||||
* @param {Object} [options={}] - Configuration options
|
||||
* @param {string} [options.name] - Name attribute for the section (data-name)
|
||||
* @param {string} [options.snippet="s_test"] - Snippet class and data-snippet value
|
||||
* @param {string} [options.additionalClassOnRoot=""] - Additional CSS classes for the root element
|
||||
* @returns {string} Formatted HTML section element
|
||||
*/
|
||||
export function getBasicSection(
|
||||
content,
|
||||
{ name, snippet = "s_test", additionalClassOnRoot = "" } = {}
|
||||
) {
|
||||
let classes = snippet;
|
||||
if (additionalClassOnRoot) {
|
||||
classes += ` ${additionalClassOnRoot}`;
|
||||
}
|
||||
return unformat(
|
||||
`<section class="${classes}" data-snippet="${snippet}" ${
|
||||
name ? `data-name="${name}"` : ""
|
||||
}><div class="test_a o-paragraph">${content}</div></section>`
|
||||
);
|
||||
}
|
||||
|
||||
export function createTestSnippets({ snippets: snippetConfigs = [], withName = false }) {
|
||||
return snippetConfigs.map((snippetConfig) => {
|
||||
const {
|
||||
name,
|
||||
groupName = "a",
|
||||
content,
|
||||
innerHTML,
|
||||
keywords = [],
|
||||
imagePreview = "",
|
||||
moduleId,
|
||||
moduleDisplayName,
|
||||
additionalClassOnRoot,
|
||||
snippet: snippetId,
|
||||
} = snippetConfig;
|
||||
|
||||
const finalContent =
|
||||
content ||
|
||||
getBasicSection(innerHTML || name, {
|
||||
name: withName ? name : "",
|
||||
snippet: snippetId || "s_test",
|
||||
additionalClassOnRoot,
|
||||
});
|
||||
|
||||
const snippet = {
|
||||
name,
|
||||
groupName,
|
||||
content: finalContent,
|
||||
keywords,
|
||||
imagePreview,
|
||||
moduleId,
|
||||
moduleDisplayName,
|
||||
};
|
||||
|
||||
return snippet;
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmAddSnippet(snippetName) {
|
||||
let previewSelector = `.o_add_snippet_dialog .o_add_snippet_iframe:iframe .o_snippet_preview_wrap`;
|
||||
if (snippetName) {
|
||||
previewSelector += ":has([data-snippet='" + snippetName + "'])";
|
||||
}
|
||||
await waitForSnippetDialog();
|
||||
await contains(previewSelector).click();
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
export const dummyBase64Img =
|
||||
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA\n AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n 9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
||||
|
||||
export const exampleContent = '<h1 class="title">Hello</h1>';
|
||||
|
||||
export const wrapExample = `<div id="wrap" data-oe-model="ir.ui.view" data-oe-id="539" data-oe-field="arch">${exampleContent}</div>`;
|
||||
|
||||
export async function setupHTMLBuilderWithDummySnippet(content) {
|
||||
const snippetEl = `<section class="s_test" data-snippet="s_test" data-name="Test">
|
||||
<div class="test_a"></div>
|
||||
</section>`;
|
||||
|
||||
const snippetsDescription = createTestSnippets({
|
||||
snippets: [
|
||||
{
|
||||
name: "Test",
|
||||
groupName: "a",
|
||||
content: snippetEl,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const snippetsStructure = {
|
||||
snippets: {
|
||||
snippet_groups: [
|
||||
'<div name="A" data-oe-thumbnail="a.svg" data-oe-snippet-id="123" data-o-snippet-group="a"><section data-snippet="s_snippet_group"></section></div>',
|
||||
],
|
||||
snippet_structure: snippetsDescription.map((snippetDesc) =>
|
||||
getSnippetStructure(snippetDesc)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return await setupHTMLBuilder(content || "", snippetsStructure);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { setupHTMLBuilder, dummyBase64Img } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("image field should not be editable, but the image can be replaced", async () => {
|
||||
await setupHTMLBuilder(
|
||||
`<div data-oe-model="product.product" data-oe-id="12" data-oe-field="image_1920" data-oe-type="image" data-oe-expression="product_image.image_1920">
|
||||
<img src="${dummyBase64Img}" alt="Product Image" style="max-width: 100%;"/>
|
||||
</div>`
|
||||
);
|
||||
expect(":iframe img").toHaveProperty("isContentEditable", false);
|
||||
await contains(":iframe img").click();
|
||||
expect("span:contains('Double-click to edit')").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { Img } from "@html_builder/core/img";
|
||||
import { ImgGroup } from "@html_builder/core/img_group";
|
||||
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
defineMailModels(); // meh
|
||||
test("ImgGroup's inner Img components should not be blocked before src load", async () => {
|
||||
const defs = {
|
||||
img1: new Deferred(),
|
||||
img2: new Deferred(),
|
||||
img3: new Deferred(),
|
||||
};
|
||||
patchWithCleanup(Img.prototype, {
|
||||
loadImage() {
|
||||
const def = defs[this.props.class];
|
||||
return Promise.all([super.loadImage(), def]);
|
||||
},
|
||||
});
|
||||
class Container extends Component {
|
||||
static components = { ImgGroup, Img };
|
||||
static template = xml`
|
||||
<ImgGroup>
|
||||
<t t-foreach="Object.keys(defs)" t-as="key" t-key="key">
|
||||
<Img src="''" class="key"/>
|
||||
</t>
|
||||
</ImgGroup>`;
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
this.defs = defs;
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Container);
|
||||
|
||||
for (const key in defs) {
|
||||
expect("img").toHaveCount(0);
|
||||
defs[key].resolve();
|
||||
await animationFrame();
|
||||
}
|
||||
expect("img").toHaveCount(3);
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { animationFrame, press } from "@odoo/hoot-dom";
|
||||
import { contains, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("should prevent edition in many2one field", async () => {
|
||||
await setupHTMLBuilder(
|
||||
`<a data-oe-model="blog.post" data-oe-id="3" data-oe-field="blog_id" data-oe-type="many2one" data-oe-expression="blog_post.blog_id" data-oe-many2one-id="1" data-oe-many2one-model="blog.blog">
|
||||
Travel
|
||||
</a>`
|
||||
);
|
||||
expect(":iframe a").toHaveProperty("isContentEditable", false);
|
||||
});
|
||||
|
||||
test.tags("desktop"); // NavigationItem only react to mouvemove which is not triggered in test for mobile
|
||||
test("Preview changes of many2one option", async () => {
|
||||
onRpc(
|
||||
"ir.qweb.field.contact",
|
||||
"get_record_to_html",
|
||||
({ args: [[id]], kwargs }) => `<span>The ${kwargs.options.option} of ${id}</span>`
|
||||
);
|
||||
|
||||
await setupHTMLBuilder(`
|
||||
<div>
|
||||
<span class="span-1" data-oe-model="blog.post" data-oe-id="3" data-oe-field="author_id" data-oe-type="contact" data-oe-many2one-id="3" data-oe-many2one-model="res.partner" data-oe-contact-options='{"option": "Name"}'>
|
||||
<span>The Name of 3</span>
|
||||
</span>
|
||||
<span class="span-2" data-oe-model="blog.post" data-oe-id="3" data-oe-field="author_id" data-oe-type="contact" data-oe-many2one-id="3" data-oe-many2one-model="res.partner" data-oe-contact-options='{"option": "Address"}'>
|
||||
<span>The Address of 3</span>
|
||||
</span>
|
||||
<span class="span-3" data-oe-model="blog.post" data-oe-id="6" data-oe-field="author_id" data-oe-type="contact" data-oe-many2one-id="3" data-oe-many2one-model="res.partner" data-oe-contact-options='{"option": "Address"}'>
|
||||
<span>The Address of 3</span>
|
||||
</span>
|
||||
<span class="span-4" data-oe-model="blog.post" data-oe-id="3" data-oe-field="author_id" data-oe-type="other" data-oe-many2one-id="3" data-oe-many2one-model="res.partner">Other</span>
|
||||
<div>
|
||||
`);
|
||||
|
||||
await contains(":iframe .span-1").click();
|
||||
expect("button.btn.dropdown").toHaveCount(1);
|
||||
await contains("button.btn.dropdown").click();
|
||||
await contains("span.o-dropdown-item.dropdown-item").hover();
|
||||
expect(":iframe span.span-1 > span").toHaveText("The Name of 1");
|
||||
expect(":iframe span.span-2 > span").toHaveText("The Address of 1");
|
||||
expect(":iframe span.span-3 > span").toHaveText("The Address of 3"); // author of other post is not changed
|
||||
expect(":iframe span.span-4").toHaveText("Hermit");
|
||||
|
||||
await press("esc"); // This causes the dropdown to close, and thus the preview to be reverted
|
||||
await animationFrame();
|
||||
expect(":iframe span.span-1 > span").toHaveText("The Name of 3");
|
||||
expect(":iframe span.span-2 > span").toHaveText("The Address of 3");
|
||||
expect(":iframe span.span-4").toHaveText("Other");
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { click, queryOne } from "@odoo/hoot-dom";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("should not allow edition of currency sign of monetary fields", async () => {
|
||||
await setupHTMLBuilder(
|
||||
`<span data-oe-model="product.template" data-oe-id="9" data-oe-field="list_price" data-oe-type="monetary" data-oe-expression="product.list_price">
|
||||
$ <span class="oe_currency_value">750.00</span>
|
||||
</span>`
|
||||
);
|
||||
expect(":iframe span[data-oe-type]").toHaveProperty("isContentEditable", false);
|
||||
expect(":iframe span.oe_currency_value").toHaveProperty("isContentEditable", true);
|
||||
});
|
||||
|
||||
test("clicking on the monetary field should select the amount", async () => {
|
||||
const { getEditor } = await setupHTMLBuilder(
|
||||
`<span data-oe-model="product.template" data-oe-id="9" data-oe-field="list_price" data-oe-type="monetary" data-oe-expression="product.list_price">
|
||||
$<span class="span-in-currency"/> <span class="oe_currency_value">750.00</span>
|
||||
</span>`
|
||||
);
|
||||
const editor = getEditor();
|
||||
await click(":iframe span.span-in-currency");
|
||||
expect(
|
||||
editor.shared.selection.areNodeContentsFullySelected(
|
||||
queryOne(":iframe span.oe_currency_value")
|
||||
)
|
||||
).toBe(true, { message: "value of monetary field is selected" });
|
||||
});
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
import {
|
||||
addBuilderOption,
|
||||
addBuilderAction,
|
||||
setupHTMLBuilder,
|
||||
} from "@html_builder/../tests/helpers";
|
||||
import { BuilderAction } from "@html_builder/core/builder_action";
|
||||
import { Operation } from "@html_builder/core/operation";
|
||||
import { HistoryPlugin } from "@html_editor/core/history_plugin";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, Deferred, delay, hover, press, tick } from "@odoo/hoot-dom";
|
||||
import { xml } from "@odoo/owl";
|
||||
import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
describe("Operation", () => {
|
||||
test("handle 3 concurrent cancellable operations (with delay)", async () => {
|
||||
const operation = new Operation();
|
||||
function makeCall(data) {
|
||||
let resolve;
|
||||
const promise = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
async function load() {
|
||||
expect.step(`load before ${data}`);
|
||||
await promise;
|
||||
expect.step(`load after ${data}`);
|
||||
}
|
||||
function apply() {
|
||||
expect.step(`apply ${data}`);
|
||||
}
|
||||
|
||||
operation.next(apply, { load, cancellable: true });
|
||||
return {
|
||||
resolve,
|
||||
};
|
||||
}
|
||||
const call1 = makeCall(1);
|
||||
await delay();
|
||||
const call2 = makeCall(2);
|
||||
await delay();
|
||||
const call3 = makeCall(3);
|
||||
await delay();
|
||||
call1.resolve();
|
||||
call2.resolve();
|
||||
call3.resolve();
|
||||
await operation.mutex.getUnlockedDef();
|
||||
expect.verifySteps([
|
||||
//
|
||||
"load before 1",
|
||||
"load after 1",
|
||||
"load before 3",
|
||||
"load after 3",
|
||||
"apply 3",
|
||||
]);
|
||||
});
|
||||
test("handle 3 concurrent cancellable operations (without delay)", async () => {
|
||||
const operation = new Operation();
|
||||
function makeCall(data) {
|
||||
let resolve;
|
||||
const promise = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
async function load() {
|
||||
expect.step(`load before ${data}`);
|
||||
await promise;
|
||||
expect.step(`load after ${data}`);
|
||||
}
|
||||
function apply() {
|
||||
expect.step(`apply ${data}`);
|
||||
}
|
||||
|
||||
operation.next(apply, { load, cancellable: true });
|
||||
return {
|
||||
resolve,
|
||||
};
|
||||
}
|
||||
const call1 = makeCall(1);
|
||||
const call2 = makeCall(2);
|
||||
const call3 = makeCall(3);
|
||||
call1.resolve();
|
||||
call2.resolve();
|
||||
call3.resolve();
|
||||
await operation.mutex.getUnlockedDef();
|
||||
expect.verifySteps(["load before 3", "load after 3", "apply 3"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Block editable", () => {
|
||||
test("Doing an operation should block the editable during its execution", async () => {
|
||||
const customActionDef = new Deferred();
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
load() {
|
||||
return customActionDef;
|
||||
}
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("custom-action");
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderButton action="'customAction'"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<div class="test-options-target">TEST</div>`, {
|
||||
loadIframeBundles: true,
|
||||
});
|
||||
|
||||
await contains(":iframe .test-options-target").click();
|
||||
await contains("[data-action-id='customAction']").click();
|
||||
expect(":iframe .o_loading_screen:not(.o_we_ui_loading)").toHaveCount(1);
|
||||
await advanceTime(50); // cancelTime=50 trigger by the preview
|
||||
await advanceTime(500); // setTimeout in addLoadingElement
|
||||
expect(":iframe .o_loading_screen.o_we_ui_loading").toHaveCount(1);
|
||||
|
||||
customActionDef.resolve();
|
||||
await tick();
|
||||
expect(":iframe .o_loading_screen.o_we_ui_loading").toHaveCount(0);
|
||||
expect(":iframe .test-options-target").toHaveClass("custom-action");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Async operations", () => {
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(HistoryPlugin.prototype, {
|
||||
makePreviewableAsyncOperation(operation) {
|
||||
const res = super.makePreviewableAsyncOperation(operation);
|
||||
const revert = res.revert;
|
||||
res.revert = async () => {
|
||||
await revert();
|
||||
expect.step("revert");
|
||||
};
|
||||
return res;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("In clickable component, revert is awaited before applying the next apply", async () => {
|
||||
const applyDelay = 1000;
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
async apply({ editingElement, value }) {
|
||||
await new Promise((resolve) => setTimeout(resolve, applyDelay));
|
||||
editingElement.classList.add(value);
|
||||
expect.step("apply first");
|
||||
}
|
||||
},
|
||||
customAction2: class extends BuilderAction {
|
||||
static id = "customAction2";
|
||||
apply({ editingElement, value }) {
|
||||
editingElement.classList.add(value);
|
||||
expect.step("apply second");
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderRow label.translate="Type">
|
||||
<BuilderSelect>
|
||||
<BuilderSelectItem action="'customAction'" actionValue="'first'">first</BuilderSelectItem>
|
||||
<BuilderSelectItem action="'customAction2'" actionValue="'second'">second</BuilderSelectItem>
|
||||
</BuilderSelect>
|
||||
</BuilderRow>
|
||||
`,
|
||||
});
|
||||
|
||||
await setupHTMLBuilder(`<div class="test-options-target">TEST</div>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
await contains(".options-container [data-label='Type'] .btn-secondary ").click();
|
||||
await hover(".popover [data-action-value='first']");
|
||||
await hover(".popover [data-action-value='second']");
|
||||
await advanceTime(applyDelay + 50);
|
||||
expect.verifySteps(["apply first", "revert", "apply second"]);
|
||||
expect(":iframe .test-options-target").toHaveClass("second");
|
||||
expect(":iframe .test-options-target").not.toHaveClass("first");
|
||||
// Escape the select to trigger an explicit revert. Otherwise, the test
|
||||
// sometimes fails with an unverified step.
|
||||
await press(["Escape"]);
|
||||
expect.verifySteps(["revert"]);
|
||||
});
|
||||
|
||||
test("In ColorPicker, revert is awaited before applying the next apply", async () => {
|
||||
const applyDelay = 1000;
|
||||
addBuilderAction({
|
||||
customAction: class extends BuilderAction {
|
||||
static id = "customAction";
|
||||
async apply({ editingElement }) {
|
||||
let color =
|
||||
getComputedStyle(editingElement).getPropertyValue("background-color");
|
||||
if (color === "rgb(255, 0, 0)") {
|
||||
color = "red";
|
||||
await new Promise((resolve) => setTimeout(resolve, applyDelay));
|
||||
} else {
|
||||
color = "blue";
|
||||
}
|
||||
editingElement.classList.add(color);
|
||||
expect.step(`apply ${color}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<BuilderRow>
|
||||
<BuilderColorPicker enabledTabs="['solid']" styleAction="'background-color'" action="'customAction'"/>
|
||||
</BuilderRow>`,
|
||||
});
|
||||
|
||||
await setupHTMLBuilder(`<div class="test-options-target">TEST</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']").hover();
|
||||
await contains(".o-overlay-item [data-color='#0000FF']").hover();
|
||||
await advanceTime(applyDelay + 50);
|
||||
expect(":iframe .test-options-target").toHaveClass("blue");
|
||||
expect(":iframe .test-options-target").not.toHaveClass("red");
|
||||
expect.verifySteps(["apply red", "revert", "apply blue"]);
|
||||
// Escape the colorpicker to trigger an explicit revert. Otherwise, the
|
||||
// test sometimes fails with an unverified step.
|
||||
await press(["Escape"]);
|
||||
expect.verifySteps(["revert"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Operation that will fail", () => {
|
||||
test("html builder must not be blocked if a preview crashes", async () => {
|
||||
expect.errors(1);
|
||||
class TestAction extends BuilderAction {
|
||||
static id = "testAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("fail");
|
||||
throw new Error("This action should crash");
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
TestAction,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderButton action="'testAction'"/>
|
||||
<BuilderButton classAction="'test'"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
await contains("[data-action-id='testAction']").hover();
|
||||
await contains("[data-class-action='test']").click();
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph test">b</div>'
|
||||
);
|
||||
expect.verifyErrors(["This action should crash"]);
|
||||
});
|
||||
|
||||
test("html builder must not be blocked when a failed action is commit", async () => {
|
||||
expect.errors(2);
|
||||
class TestAction extends BuilderAction {
|
||||
static id = "testAction";
|
||||
apply({ editingElement }) {
|
||||
editingElement.classList.add("fail");
|
||||
throw new Error("This action should crash");
|
||||
}
|
||||
}
|
||||
addBuilderAction({
|
||||
TestAction,
|
||||
});
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`
|
||||
<BuilderButton action="'testAction'"/>
|
||||
<BuilderButton classAction="'test'"/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
await contains("[data-action-id='testAction']").click();
|
||||
await contains("[data-class-action='test']").click();
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph test">b</div>'
|
||||
);
|
||||
// preview + commit
|
||||
expect.verifyErrors(["This action should crash", "This action should crash"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
after,
|
||||
before,
|
||||
BEGIN,
|
||||
END,
|
||||
SNIPPET_SPECIFIC,
|
||||
splitBetween,
|
||||
} from "@html_builder/utils/option_sequence";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
const ARBITRARY_FAKE_POSITION = 7777777777;
|
||||
|
||||
test("before throws if position doesn't exist", async () => {
|
||||
expect(() => before(ARBITRARY_FAKE_POSITION)).toThrow();
|
||||
});
|
||||
|
||||
test("before throws if position is BEGIN", async () => {
|
||||
expect(() => before(BEGIN)).toThrow();
|
||||
});
|
||||
|
||||
test("before returns a smaller position", async () => {
|
||||
expect(before(SNIPPET_SPECIFIC)).toBeLessThan(SNIPPET_SPECIFIC);
|
||||
expect(before(END)).toBeLessThan(END);
|
||||
});
|
||||
|
||||
test("after throws if position doesn't exist", async () => {
|
||||
expect(() => after(ARBITRARY_FAKE_POSITION)).toThrow();
|
||||
});
|
||||
|
||||
test("after throws if position is END", async () => {
|
||||
expect(() => after(END)).toThrow();
|
||||
});
|
||||
|
||||
test("after returns a bigger position", async () => {
|
||||
expect(after(SNIPPET_SPECIFIC)).toBeGreaterThan(SNIPPET_SPECIFIC);
|
||||
expect(after(BEGIN)).toBeGreaterThan(BEGIN);
|
||||
});
|
||||
|
||||
test("splitBetween correctly splits to the right values", async () => {
|
||||
expect(splitBetween(0, 3, 2)).toMatch([1, 2]);
|
||||
expect(splitBetween(0, 10, 2)).toMatch([10 / 3, (2 * 10) / 3]);
|
||||
expect(splitBetween(0, 8, 7)).toMatch([1, 2, 3, 4, 5, 6, 7]);
|
||||
expect(splitBetween(1, 5, 3)).toMatch([2, 3, 4]);
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { animationFrame, clear, click, fill, waitFor } from "@odoo/hoot-dom";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
const websiteContent = `
|
||||
<div class="s_rating pt16 pb16" data-icon="fa-star" data-snippet="s_rating" data-name="Rating">
|
||||
<strong class="s_rating_title">Quality</strong>
|
||||
<div class="s_rating_icons o_not_editable">
|
||||
<span class="s_rating_active_icons">
|
||||
<i class="fa fa-star"></i>
|
||||
<i class="fa fa-star"></i>
|
||||
<i class="fa fa-star"></i>
|
||||
</span>
|
||||
<span class="s_rating_inactive_icons">
|
||||
<i class="fa fa-star-o"></i>
|
||||
<i class="fa fa-star-o"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
test("change rating score", async () => {
|
||||
await setupHTMLBuilder(websiteContent);
|
||||
expect(":iframe .s_rating .s_rating_active_icons i").toHaveCount(3);
|
||||
expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(2);
|
||||
await contains(":iframe .s_rating").click();
|
||||
await contains(".options-container [data-action-id='activeIconsNumber'] input").click();
|
||||
await clear();
|
||||
await fill("1");
|
||||
await animationFrame();
|
||||
expect(":iframe .s_rating .s_rating_active_icons i").toHaveCount(1);
|
||||
await contains(".options-container [data-action-id='totalIconsNumber'] input").click();
|
||||
await clear();
|
||||
await fill("4");
|
||||
await animationFrame();
|
||||
expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(3);
|
||||
expect(":iframe .s_rating").toHaveInnerHTML(
|
||||
`<strong class="s_rating_title">Quality</strong>
|
||||
<div class="s_rating_icons o_not_editable" contenteditable="false">
|
||||
<span class="s_rating_active_icons">
|
||||
<i class="fa fa-star" contenteditable="false">
|
||||
​
|
||||
</i>
|
||||
</span>
|
||||
<span class="s_rating_inactive_icons">
|
||||
<i class="fa fa-star-o" contenteditable="false">
|
||||
​
|
||||
</i>
|
||||
<i class="fa fa-star-o" contenteditable="false">
|
||||
​
|
||||
</i>
|
||||
<i class="fa fa-star-o" contenteditable="false">
|
||||
​
|
||||
</i>
|
||||
</span>
|
||||
</div>`
|
||||
);
|
||||
});
|
||||
test("Ensure order of operations when clicking very fast on two options", async () => {
|
||||
await setupHTMLBuilder(websiteContent);
|
||||
await contains(":iframe .s_rating").click();
|
||||
await waitFor("[data-label='Icon']");
|
||||
expect("[data-label='Icon'] .dropdown-toggle").toHaveText("Stars");
|
||||
expect(":iframe .s_rating").not.toHaveAttribute("data-active-custom-icon");
|
||||
await click(".options-container [data-action-id='customIcon']");
|
||||
await click(".options-container [data-class-action='fa-2x']");
|
||||
await animationFrame();
|
||||
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
|
||||
await contains(".modal-dialog .fa-glass").click();
|
||||
expect(":iframe .s_rating").toHaveAttribute("data-active-custom-icon", "fa fa-glass");
|
||||
expect("[data-label='Icon'] .dropdown-toggle").toHaveText("Custom");
|
||||
expect(":iframe .s_rating_icons").toHaveClass("fa-2x");
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect("[data-label='Icon'] .dropdown-toggle").toHaveText("Custom");
|
||||
expect(":iframe .s_rating").toHaveAttribute("data-active-custom-icon", "fa fa-glass");
|
||||
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
|
||||
await contains(".o-snippets-top-actions .fa-undo").click();
|
||||
expect("[data-label='Icon'] .dropdown-toggle").toHaveText("Stars");
|
||||
expect(":iframe .s_rating").not.toHaveAttribute("data-active-custom-icon");
|
||||
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("change width of separator", async () => {
|
||||
await setupHTMLBuilder(`
|
||||
<div class="s_hr">
|
||||
<hr class="w-100">
|
||||
</div>
|
||||
`);
|
||||
await contains(":iframe .s_hr").click();
|
||||
await contains("div:contains('Width') button:contains('100%')").click();
|
||||
expect("[data-class-action='mx-auto']").toHaveCount(0);
|
||||
await contains(".o_popover [data-class-action='w-50']").click();
|
||||
expect("[data-class-action='mx-auto']").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("section with containers should not be contenteditable, but there containers should, unless outside o_editable", async () => {
|
||||
await setupHTMLBuilder(
|
||||
`<section><div class="container"><span class="inside">in</span></div></section>`,
|
||||
{
|
||||
headerContent: `<section><div class="container"><span class="outside">out</span></div></section>`,
|
||||
}
|
||||
);
|
||||
expect(":iframe section:has(.inside)").toHaveProperty("isContentEditable", false);
|
||||
expect(":iframe .inside").toHaveProperty("isContentEditable", true);
|
||||
|
||||
expect(":iframe section:has(.outside)").toHaveProperty("isContentEditable", false);
|
||||
expect(":iframe .outside").toHaveProperty("isContentEditable", false);
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { queryAllTexts, queryAllValues, waitFor } from "@odoo/hoot-dom";
|
||||
import { xml } from "@odoo/owl";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("edit box-shadow with ShadowOption", async () => {
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
template: xml`<ShadowOption/>`,
|
||||
});
|
||||
await setupHTMLBuilder(`<div class="test-options-target">b</div>`);
|
||||
await contains(":iframe .test-options-target").click();
|
||||
await waitFor(".hb-row");
|
||||
expect(queryAllTexts(".hb-row .hb-row-label")).toEqual(["Shadow"]);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph">b</div>'
|
||||
);
|
||||
|
||||
await contains('.options-container button[title="Outset"]').click();
|
||||
expect(queryAllTexts(".hb-row .hb-row-label")).toEqual([
|
||||
"Shadow",
|
||||
"Color",
|
||||
"Offset (X, Y)",
|
||||
"Blur",
|
||||
"Spread",
|
||||
]);
|
||||
expect(queryAllValues('[data-action-id="setShadow"] input')).toEqual(["0", "8", "16", "0"]);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 0px 8px 16px 0px !important;">b</div>'
|
||||
);
|
||||
|
||||
await contains('[data-action-param="offsetX"] input').fill(10);
|
||||
await contains('[data-action-param="offsetY"] input').fill(2);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 16px 0px !important;">b</div>'
|
||||
);
|
||||
|
||||
await contains('[data-action-param="blur"] input').clear();
|
||||
await contains('[data-action-param="blur"] input').fill(10.5);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 10.5px 0px !important;">b</div>'
|
||||
);
|
||||
|
||||
await contains('[data-action-param="spread"] input').fill(".4");
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 10.5px 0.4px !important;">b</div>'
|
||||
);
|
||||
|
||||
await contains('.options-container button[title="Inset"]').click();
|
||||
expect(queryAllTexts(".hb-row .hb-row-label")).toEqual([
|
||||
"Shadow",
|
||||
"Color",
|
||||
"Offset (X, Y)",
|
||||
"Blur",
|
||||
"Spread",
|
||||
]);
|
||||
expect(queryAllValues('[data-action-id="setShadow"] input')).toEqual([
|
||||
"10",
|
||||
"82",
|
||||
"10.5",
|
||||
"0.4",
|
||||
]);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 10.5px 0.4px inset !important;">b</div>'
|
||||
);
|
||||
|
||||
await contains(".options-container button:contains(None)").click();
|
||||
expect(queryAllTexts(".hb-row .hb-row-label")).toEqual(["Shadow"]);
|
||||
expect(":iframe .test-options-target").toHaveOuterHTML(
|
||||
'<div class="test-options-target o-paragraph" style="">b</div>'
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { addBuilderOption, setupHTMLBuilder } from "@html_builder/../tests/helpers";
|
||||
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import { xml } from "@odoo/owl";
|
||||
import { contains } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
describe("useDomState", () => {
|
||||
test("Should not update the state of an async useDomState if a new step has been made", async () => {
|
||||
let currentResolve;
|
||||
addBuilderOption({
|
||||
selector: ".test-options-target",
|
||||
Component: class extends BaseOptionComponent {
|
||||
static template = xml`<div t-att-data-letter="getLetter()"/>`;
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.state = useDomState(async () => {
|
||||
const letter = await new Promise((resolve) => {
|
||||
currentResolve = resolve;
|
||||
});
|
||||
return {
|
||||
delay: `${letter}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
getLetter() {
|
||||
expect.step(`state: ${this.state.delay}`);
|
||||
return this.state.delay;
|
||||
}
|
||||
},
|
||||
});
|
||||
const { getEditor } = await setupHTMLBuilder(`<div class="test-options-target">a</div>`);
|
||||
await animationFrame();
|
||||
await contains(":iframe .test-options-target").click();
|
||||
const editor = getEditor();
|
||||
const resolve1 = currentResolve;
|
||||
resolve1("x");
|
||||
await animationFrame();
|
||||
|
||||
editor.editable.querySelector(".test-options-target").textContent = "b";
|
||||
editor.shared.history.addStep();
|
||||
const resolve2 = currentResolve;
|
||||
editor.editable.querySelector(".test-options-target").textContent = "c";
|
||||
editor.shared.history.addStep();
|
||||
const resolve3 = currentResolve;
|
||||
|
||||
resolve3("z");
|
||||
await animationFrame();
|
||||
resolve2("y");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["state: x", "state: z"]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue