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``, }); await setupHTMLBuilder(`
TEST
`, { 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` first second `, }); await setupHTMLBuilder(`
TEST
`); 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` `, }); await setupHTMLBuilder(`
TEST
`); 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` `, }); await setupHTMLBuilder(`
b
`); 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( '
b
' ); 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` `, }); await setupHTMLBuilder(`
b
`); 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( '
b
' ); // preview + commit expect.verifyErrors(["This action should crash", "This action should crash"]); }); });