Repository: odoo-dev/odoo
Pull request #4163: Master mysterious egg loco 3
Author: login: loco-odoo
Created: 2025-03-06T13:09:34Z | Merged: 2025-03-10T13:21:59Z



Diff:
diff --git a/addons/html_builder/static/src/builder.js b/addons/html_builder/static/src/builder.js
index dbd350e9beb55..47138604ec804 100644
--- a/addons/html_builder/static/src/builder.js
+++ b/addons/html_builder/static/src/builder.js
@@ -68,17 +68,17 @@ export class Builder extends Component {
             {
                 Plugins,
                 onChange: ({ isPreviewing }) => {
-                    this.state.canUndo = this.editor.shared.history.canUndo();
-                    this.state.canRedo = this.editor.shared.history.canRedo();
                     if (!isPreviewing) {
+                        this.state.canUndo = this.editor.shared.history.canUndo();
+                        this.state.canRedo = this.editor.shared.history.canRedo();
                         this.updateInvisibleEls();
+                        editorBus.trigger("UPDATE_EDITING_ELEMENT");
+                        editorBus.trigger("DOM_UPDATED");
                     }
-                    editorBus.trigger("UPDATE_EDITING_ELEMENT");
-                    editorBus.trigger("DOM_UPDATED", { isPreviewing });
                 },
                 resources: {
                     on_mobile_preview_clicked: () => {
-                        editorBus.trigger("DOM_UPDATED", { isPreviewing: false });
+                        editorBus.trigger("DOM_UPDATED");
                     },
                     change_current_options_containers_listeners: (currentOptionsContainers) => {
                         this.state.currentOptionsContainers = currentOptionsContainers;
diff --git a/addons/html_builder/static/src/core/building_blocks/utils.js b/addons/html_builder/static/src/core/building_blocks/utils.js
index cb92b76d1aed1..00d1e5e435a0b 100644
--- a/addons/html_builder/static/src/core/building_blocks/utils.js
+++ b/addons/html_builder/static/src/core/building_blocks/utils.js
@@ -192,12 +192,7 @@ export function useSelectableComponent(id, { onItemChange } = {}) {
     }
 
     onMounted(refreshCurrentItem);
-    useBus(env.editorBus, "DOM_UPDATED", (ev) => {
-        if (ev.detail.isPreviewing) {
-            return;
-        }
-        refreshCurrentItem();
-    });
+    useBus(env.editorBus, "DOM_UPDATED", refreshCurrentItem);
     function cleanSelectedItem(...args) {
         if (currentSelectedItem) {
             currentSelectedItem.clean(...args);
diff --git a/addons/html_builder/static/src/core/plugins/drop_zone_plugin.js b/addons/html_builder/static/src/core/plugins/drop_zone_plugin.js
index 646514ad9de16..b0e3bc52a10be 100644
--- a/addons/html_builder/static/src/core/plugins/drop_zone_plugin.js
+++ b/addons/html_builder/static/src/core/plugins/drop_zone_plugin.js
@@ -134,11 +134,11 @@ export class DropZonePlugin extends Plugin {
         }
         const dropZone = position
             ? closest(touching(this.dropZoneElements, position), position) ||
-            closest(this.dropZoneElements, position)
+              closest(this.dropZoneElements, position)
             : closest(this.dropZoneElements, {
-                x: window.innerWidth / 2,
-                y: window.innerHeight / 2,
-            });
+                  x: window.innerWidth / 2,
+                  y: window.innerHeight / 2,
+              });
         if (!dropZone) {
             this.clearDropZone();
             return;
@@ -160,7 +160,6 @@ export class DropZonePlugin extends Plugin {
             this.dispatchTo("on_add_element_handlers", { elementToAdd: elementToAdd });
             scrollToWindow(elementToAdd, { behavior: "smooth", offset: 50 });
             this.dependencies.history.addStep();
-            this.dispatchTo("update_interactions", elementToAdd);
         };
     }
 }
diff --git a/addons/html_builder/static/src/core/plugins/replace/replace_plugin.js b/addons/html_builder/static/src/core/plugins/replace/replace_plugin.js
index 7f5388930ce35..a1b7e9766849b 100644
--- a/addons/html_builder/static/src/core/plugins/replace/replace_plugin.js
+++ b/addons/html_builder/static/src/core/plugins/replace/replace_plugin.js
@@ -50,7 +50,6 @@ export class ReplacePlugin extends Plugin {
             this.dependencies.history.addStep();
             this.dependencies["builder-options"].updateContainers(newSnippet);
             // TODO post snippet drop (onBuild,...)
-            this.dispatchTo("update_interactions", newSnippet);
         }
     }
 }
diff --git a/addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js b/addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js
index 87feb2657f20b..62e7d5a8b3584 100644
--- a/addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js
+++ b/addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js
@@ -75,6 +75,7 @@ class BackgroundImageOptionPlugin extends Plugin {
                     this.applyReplaceBackgroundImage.bind(this)({
                         editingElement: editingElement,
                         loadResult: "",
+                        param: { forceClean: true },
                     });
                 },
             },
@@ -107,8 +108,13 @@ class BackgroundImageOptionPlugin extends Plugin {
             onClose.then(resolve);
         });
     }
-    applyReplaceBackgroundImage({ editingElement, loadResult: imageSrc }) {
-        if (!imageSrc) {
+    applyReplaceBackgroundImage({
+        editingElement,
+        loadResult: imageSrc,
+        param: { forceClean = false },
+    }) {
+        if (!forceClean && !imageSrc) {
+            // Do nothing: no images has been selected on the media dialog
             return;
         }
         this.setImageBackground(editingElement, imageSrc);
diff --git a/addons/html_builder/static/src/plugins/process_steps_option_plugin.js b/addons/html_builder/static/src/plugins/process_steps_option_plugin.js
index 0e838ba3940bb..660ca393845f9 100644
--- a/addons/html_builder/static/src/plugins/process_steps_option_plugin.js
+++ b/addons/html_builder/static/src/plugins/process_steps_option_plugin.js
@@ -13,6 +13,11 @@ class ProcessStepsOptionPlugin extends Plugin {
             selector: this.selector,
         },
         builder_actions: this.getActions(),
+        // The reload of the connectors is done at the
+        // 'content_updated_handlers' (each time there is a DOM mutation) and
+        // not at the normalize as there are cases where we want to reload the
+        // connectors even if there were no step added (e.g: a column of the
+        // snippet is being resized).
         content_updated_handlers: (rootEl) =>
             applyFunDependOnSelectorAndExclude(reloadConnectors, rootEl, this.selector),
     };
diff --git a/addons/html_builder/static/src/website_builder/plugins/edit_interaction_plugin.js b/addons/html_builder/static/src/website_builder/plugins/edit_interaction_plugin.js
index c9f118c0093a8..4d45634192027 100644
--- a/addons/html_builder/static/src/website_builder/plugins/edit_interaction_plugin.js
+++ b/addons/html_builder/static/src/website_builder/plugins/edit_interaction_plugin.js
@@ -5,8 +5,7 @@ export class EditInteractionPlugin extends Plugin {
     static id = "edit_interaction";
 
     resources = {
-        update_interactions: this.startInteractions.bind(this),
-        on_remove_handlers: this.stopInteractions.bind(this),
+        normalize_handlers: this.startInteractions.bind(this),
     };
 
     setup() {
@@ -22,8 +21,6 @@ export class EditInteractionPlugin extends Plugin {
 
     updateEditInteraction({ detail: { websiteEditService } }) {
         this.websiteEditService = websiteEditService;
-        const targetEl = this.document.querySelector("#wrapwrap");
-        this.startInteractions(targetEl);
     }
 
     startInteractions(element) {
@@ -32,13 +29,6 @@ export class EditInteractionPlugin extends Plugin {
         }
         this.websiteEditService.update(element, true);
     }
-
-    stopInteractions(element) {
-        if (!this.websiteEditService) {
-            throw new Error("website edit service not loaded");
-        }
-        this.websiteEditService.stop(element);
-    }
 }
 
 registry.category("website-plugins").add(EditInteractionPlugin.id, EditInteractionPlugin);
diff --git a/addons/html_builder/static/src/website_preview/website_builder_action.js b/addons/html_builder/static/src/website_preview/website_builder_action.js
index 300797fb9e686..e7e3f1df5d18e 100644
--- a/addons/html_builder/static/src/website_preview/website_builder_action.js
+++ b/addons/html_builder/static/src/website_preview/website_builder_action.js
@@ -64,10 +64,6 @@ export class WebsiteBuilder extends Component {
         this.setIframeLoaded();
         this.addSystrayItems();
         onWillDestroy(() => {
-            window.parent.document.removeEventListener(
-                "website_edit_loaded",
-                this.updateEditInteraction
-            );
             this.websiteService.useMysterious = false;
             registry.category("systray").remove("website.WebsiteSystrayItem");
         });
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
index 871adbaaac265..5d631219f543b 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_button.test.js
@@ -152,12 +152,12 @@ test("clean another action", async () => {
     await click("[data-class-action='my-custom-class1']");
     expect(":iframe .test-options-target").toHaveAttribute(
         "class",
-        "test-options-target my-custom-class1 o-paragraph"
+        "test-options-target o-paragraph my-custom-class1"
     );
     await click("[data-class-action='my-custom-class2']");
     expect(":iframe .test-options-target").toHaveAttribute(
         "class",
-        "test-options-target my-custom-class2 o-paragraph"
+        "test-options-target o-paragraph my-custom-class2"
     );
 });
 test("clean should provide the next action value", async () => {
@@ -294,8 +294,8 @@ test("apply classAction on multi elements", async () => {
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(`
             <div class="test-options-target">
-                <div class="target-apply my-custom-class o-paragraph">a</div>
-                <div class="target-apply my-custom-class o-paragraph">b</div>
+                <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 () => {
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_checkbox.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_checkbox.test.js
index 3e7c0d5206d1d..5720c79d7e415 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_checkbox.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_checkbox.test.js
@@ -53,7 +53,7 @@ test("hide/display base on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target b my-custom-class o-paragraph">b</div></div>`
+        `<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);
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_colorpicker.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_colorpicker.test.js
index f0ef8702c32f2..76ac64477a77e 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_colorpicker.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_colorpicker.test.js
@@ -59,7 +59,7 @@ test("hide/display base on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target b my-custom-class o-paragraph">b</div></div>`
+        `<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);
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_number_input.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_number_input.test.js
index bb49601c00f0b..f9b33f83edaa3 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_number_input.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_number_input.test.js
@@ -61,7 +61,7 @@ test("hide/display base on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target my-custom-class o-paragraph">b</div></div>`
+        `<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);
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
index a7bdb3edce7bb..8159828c01b7c 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_select_item.test.js
@@ -122,7 +122,7 @@ test("hide/display BuilderSelect based on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target b my-custom-class o-paragraph">b</div></div>`
+        `<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);
@@ -159,7 +159,7 @@ test("hide/display BuilderSelectItem base on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target my-custom-class o-paragraph">b</div></div>`
+        `<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();
diff --git a/addons/html_builder/static/tests/custom_tab/builder_components/builder_text_input.test.js b/addons/html_builder/static/tests/custom_tab/builder_components/builder_text_input.test.js
index 5a41b9a3c6915..d746ff72fec7c 100644
--- a/addons/html_builder/static/tests/custom_tab/builder_components/builder_text_input.test.js
+++ b/addons/html_builder/static/tests/custom_tab/builder_components/builder_text_input.test.js
@@ -38,7 +38,7 @@ test("hide/display base on applyTo", async () => {
 
     await contains("[data-class-action='my-custom-class']").click();
     expect(editableContent).toHaveInnerHTML(
-        `<div class="parent-target"><div class="child-target my-custom-class o-paragraph">b</div></div>`
+        `<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);
diff --git a/addons/html_builder/static/tests/edit_interaction.test.js b/addons/html_builder/static/tests/edit_interaction.test.js
index 81c7eec636c12..2f988b80a6314 100644
--- a/addons/html_builder/static/tests/edit_interaction.test.js
+++ b/addons/html_builder/static/tests/edit_interaction.test.js
@@ -2,35 +2,19 @@ import { expect, test } from "@odoo/hoot";
 import { contains, patchWithCleanup } from "@web/../tests/web_test_helpers";
 import { EditInteractionPlugin } from "@html_builder/website_builder/plugins/edit_interaction_plugin";
 import {
+    addActionOption,
+    addOption,
     confirmAddSnippet,
     defineWebsiteModels,
     openBuilderSidebar,
     setupWebsiteBuilder,
     setupWebsiteBuilderWithSnippet,
 } from "./website_helpers";
-import { animationFrame, click, waitFor } from "@odoo/hoot-dom";
+import { click, waitFor } from "@odoo/hoot-dom";
+import { xml } from "@odoo/owl";
 
 defineWebsiteModels();
 
-test("interactions are started when starting editing", async () => {
-    await setupWebsiteBuilder("", { openEditor: false });
-    let websiteEditService;
-    patchWithCleanup(EditInteractionPlugin.prototype, {
-        setup() {
-            super.setup();
-            this.websiteEditService.update = () => expect.step("update");
-            websiteEditService = this.websiteEditService;
-        },
-    });
-    await openBuilderSidebar();
-    window.parent.document.dispatchEvent(
-        new CustomEvent("transfer_website_edit_service", {
-            detail: { websiteEditService },
-        })
-    );
-    expect.verifySteps(["update"]);
-});
-
 test("dropping a new snippet starts its interaction", async () => {
     await setupWebsiteBuilder("", { openEditor: false });
     patchWithCleanup(EditInteractionPlugin.prototype, {
@@ -41,8 +25,7 @@ test("dropping a new snippet starts its interaction", async () => {
     });
     await openBuilderSidebar();
     await waitFor(".o-website-builder_sidebar.o_builder_sidebar_open");
-    expect.verifySteps([]);
-
+    expect.verifySteps(["update"]);
     await contains(
         `.o-snippets-menu #snippet_groups .o_snippet[data-snippet-group='text'] .o_snippet_thumbnail_area`
     ).click();
@@ -60,54 +43,37 @@ test("replacing a snippet starts the interaction of the new snippet", async () =
     });
     await openBuilderSidebar();
     await waitFor(":iframe [data-snippet='s_text_block']");
-    expect.verifySteps([]);
+    expect.verifySteps(["update"]);
     await click(`:iframe [data-snippet="s_text_block"]`);
     await contains(".btn.o_snippet_replace").click();
     await confirmAddSnippet("s_title");
     expect.verifySteps(["update"]);
 });
 
-test("removing a snippet stops its interaction", async () => {
-    await setupWebsiteBuilderWithSnippet("s_title", { openEditor: false });
-    patchWithCleanup(EditInteractionPlugin.prototype, {
-        setup() {
-            super.setup();
-            this.websiteEditService.stop = () => expect.step("stop");
+test("ensure order of operations when hovering an option", async () => {
+    addActionOption({
+        customAction: {
+            load: async () => {
+                expect.step("load");
+            },
+            apply: ({ editingElement }) => {
+                editingElement.classList.add("new_class");
+                expect.step("apply");
+            },
         },
     });
-    await openBuilderSidebar();
-    await waitFor(":iframe [data-snippet='s_title']");
-    expect.verifySteps([]);
-    await click(`:iframe [data-snippet="s_title"]`);
-    await contains(".btn.oe_snippet_remove").click();
-    await animationFrame();
-    expect.verifySteps(["stop"]);
-});
-
-test("throw if edit interactions are started but website_edit service hasn't started", async () => {
-    await setupWebsiteBuilder("", { openEditor: false });
-    let plugin;
-    patchWithCleanup(EditInteractionPlugin.prototype, {
-        setup() {
-            super.setup();
-            this.websiteEditService = undefined;
-            plugin = this;
-        },
+    addOption({
+        selector: ".test-options-target",
+        template: xml`<BuilderButton action="'customAction'"/>`,
     });
-    await openBuilderSidebar();
-    expect(() => plugin.startInteractions()).toThrow("website edit service not loaded");
-});
-
-test("throw if edit interactions are stopped but website_edit service hasn't started", async () => {
-    await setupWebsiteBuilder("", { openEditor: false });
-    let plugin;
     patchWithCleanup(EditInteractionPlugin.prototype, {
-        setup() {
-            super.setup();
-            this.websiteEditService = undefined;
-            plugin = this;
+        startInteractions() {
+            expect.step("startInteractions");
         },
     });
-    await openBuilderSidebar();
-    expect(() => plugin.stopInteractions()).toThrow("website edit service not loaded");
+    await setupWebsiteBuilder(`<div class="test-options-target">b</div>`);
+    expect.verifySteps(["startInteractions"]);
+    await contains(":iframe .test-options-target").click();
+    await contains("[data-action-id='customAction']").hover();
+    expect.verifySteps(["load", "apply", "startInteractions"]);
 });
diff --git a/addons/html_builder/static/tests/options/background_option.test.js b/addons/html_builder/static/tests/options/background_option.test.js
index 7c0caa3fd31b7..168dc4a22e633 100644
--- a/addons/html_builder/static/tests/options/background_option.test.js
+++ b/addons/html_builder/static/tests/options/background_option.test.js
@@ -159,3 +159,15 @@ test("open the media dialog to toggle the image background but do not choose an
     await contains("[data-action-id='toggleBgImage']").click();
     expect(".modal").toBeDisplayed();
 });
+
+test("remove the background image of a snippet", async () => {
+    await setupWebsiteBuilder(`
+        <section style="background-image: url('/web/image/123/transparent.png'); width: 500px; height:500px">
+            <div class="o_we_shape o_web_editor_Connections_01">
+                AAAA
+            </div>
+        </section>`);
+    await contains(":iframe section").click();
+    await contains("[data-action-id='toggleBgImage']").click();
+    expect(":iframe section").not.toHaveStyle("backgroundImage");
+});
diff --git a/addons/html_builder/static/tests/options/shadow_option.test.js b/addons/html_builder/static/tests/options/shadow_option.test.js
index cf54b3c3e6186..74e708a2eb44a 100644
--- a/addons/html_builder/static/tests/options/shadow_option.test.js
+++ b/addons/html_builder/static/tests/options/shadow_option.test.js
@@ -27,23 +27,23 @@ test("edit box-shadow with ShadowOption", async () => {
     ]);
     expect(queryAllValues('[data-action-id="setShadow"] input')).toEqual(["0", "8", "16", "0"]);
     expect(":iframe .test-options-target").toHaveOuterHTML(
-        '<div class="test-options-target shadow o-paragraph" style="box-shadow: rgba(0, 0, 0, 0.15) 0px 8px 16px 0px !important;">b</div>'
+        '<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, { clean: true });
     expect(":iframe .test-options-target").toHaveOuterHTML(
-        '<div class="test-options-target shadow o-paragraph" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 16px 0px !important;">b</div>'
+        '<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').fill(3);
     expect(":iframe .test-options-target").toHaveOuterHTML(
-        '<div class="test-options-target shadow o-paragraph" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 0px !important;">b</div>'
+        '<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 0px !important;">b</div>'
     );
 
     await contains('[data-action-param="spread"] input').fill(4);
     expect(":iframe .test-options-target").toHaveOuterHTML(
-        '<div class="test-options-target shadow o-paragraph" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 4px !important;">b</div>'
+        '<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 4px !important;">b</div>'
     );
 
     await contains('.options-container button[title="Inset"]').click();
@@ -56,7 +56,7 @@ test("edit box-shadow with ShadowOption", async () => {
     ]);
     expect(queryAllValues('[data-action-id="setShadow"] input')).toEqual(["10", "82", "163", "4"]);
     expect(":iframe .test-options-target").toHaveOuterHTML(
-        '<div class="test-options-target shadow o-paragraph" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 4px inset !important;">b</div>'
+        '<div class="test-options-target o-paragraph shadow" style="box-shadow: rgba(0, 0, 0, 0.15) 10px 82px 163px 4px inset !important;">b</div>'
     );
 
     await contains(".options-container button:contains(None)").click();
diff --git a/addons/html_builder/static/tests/preview_mode.test.js b/addons/html_builder/static/tests/preview_mode.test.js
new file mode 100644
index 0000000000000..9efc9682a9d5b
--- /dev/null
+++ b/addons/html_builder/static/tests/preview_mode.test.js
@@ -0,0 +1,42 @@
+import { Plugin } from "@html_editor/plugin";
+import { after, expect, test } from "@odoo/hoot";
+import { xml } from "@odoo/owl";
+import { contains } from "@web/../tests/web_test_helpers";
+import { registry } from "@web/core/registry";
+import { uniqueId } from "@web/core/utils/functions";
+import { addOption, defineWebsiteModels, setupWebsiteBuilder } from "./website_helpers";
+
+defineWebsiteModels();
+
+test("do not update builder if in preview mode", async () => {
+    const pluginId = uniqueId("test-action-plugin");
+    class P extends Plugin {
+        static id = pluginId;
+        static dependencies = ["history"];
+        resources = {
+            builder_actions: {
+                customAction: {
+                    apply: ({ editingElement }) => {
+                        editingElement.classList.add("applied");
+                        this.dependencies.history.addStep();
+                    },
+                    isApplied: ({ editingElement }) => editingElement.classList.contains("applied"),
+                },
+            },
+        };
+    }
+    registry.category("website-plugins").add(pluginId, P);
+    after(() => {
+        registry.category("website-plugins").remove(P);
+    });
+    addOption({
+        selector: ".test-options-target",
+        template: xml`<BuilderButton id="'id1'" action="'customAction'">b1</BuilderButton>
+        <BuilderButton classAction="'b2_class'" t-if="this.isActiveItem('id1')">b2</BuilderButton>`,
+    });
+    await setupWebsiteBuilder(`<div class="test-options-target">b</div>`);
+    await contains(":iframe .test-options-target").click();
+    await contains("[data-action-id='customAction']").hover();
+    expect("[data-class-action='b2_class']").not.toBeVisible();
+    expect(".o-snippets-top-actions .fa-undo").not.toBeEnabled();
+});
diff --git a/addons/html_editor/static/src/core/history_plugin.js b/addons/html_editor/static/src/core/history_plugin.js
index 077bfab8b5db2..f21faded9a5a7 100644
--- a/addons/html_editor/static/src/core/history_plugin.js
+++ b/addons/html_editor/static/src/core/history_plugin.js
@@ -570,7 +570,6 @@ export class HistoryPlugin extends Plugin {
         if (stepState) {
             this.stepsStates.set(currentStep.id, stepState);
         }
-        
         this.handleObserverRecords();
         const currentMutationsCount = currentStep.mutations.length;
         if (currentMutationsCount === 0) {
@@ -954,6 +953,7 @@ export class HistoryPlugin extends Plugin {
         let applied = false;
         // TODO ABD TODO @phoenix: selection may become obsolete, it should evolve with mutations.
         const selectionToRestore = this.dependencies.selection.preserveSelection();
+        const extraToRestore = {...this.currentStep.extra};
         return () => {
             if (applied) {
                 return;
@@ -967,6 +967,7 @@ export class HistoryPlugin extends Plugin {
             this.handleObserverRecords();
             // TODO ABD TODO @phoenix: evaluate if the selection is not restorable at the desired position
             selectionToRestore.restore();
+            this.currentStep.extra = extraToRestore;
             this.dispatchTo("restore_savepoint_handlers");
         };
     }
@@ -983,7 +984,10 @@ export class HistoryPlugin extends Plugin {
                 revertOperation();
                 revertOperation = this.makeSavePoint();
                 this.isPreviewing = true;
-                return operation(...args);
+                operation(...args);
+                // The operation should be similar than in the 'commit'
+                // (normalize etc...) hence the 'addStep'.
+                this.addStep();
             },
             commit: (...args) => {
                 revertOperation();
@@ -999,9 +1003,6 @@ export class HistoryPlugin extends Plugin {
         };
     }
 
-    getIsPreviewing() {
-        return this.isPreviewing;
-    }
     /**
      * Discard the current draft, and, if necessary, consume and revert
      * reversible steps until the specified step index, and ensure that
diff --git a/addons/html_editor/static/tests/history.test.js b/addons/html_editor/static/tests/history.test.js
index aabba79946e0f..a50b12a21271a 100644
--- a/addons/html_editor/static/tests/history.test.js
+++ b/addons/html_editor/static/tests/history.test.js
@@ -434,18 +434,24 @@ describe("makePreviewableOperation", () => {
             newElem.setAttribute("id", elemId);
             div.appendChild(newElem);
         });
-        const numberOfSteps = history.steps.length;
+        let numberOfSteps = history.steps.length;
         const numberOfCurrentMutations = history.currentStep.mutations.length;
         previewableAddParagraph.preview("first");
+        // step added by the preview
+        numberOfSteps += 1;
         await animationFrame();
         expect(history.steps.length).toBe(numberOfSteps);
         expect("#first").toHaveCount(1);
         previewableAddParagraph.preview("second");
+        // step added by the revert of the first preview and the second preview
+        numberOfSteps += 2;
         await animationFrame();
         expect(history.steps.length).toBe(numberOfSteps);
         expect("#first").toHaveCount(0);
         expect("#second").toHaveCount(1);
         previewableAddParagraph.revert();
+        // step added by the revert
+        numberOfSteps += 1;
         await animationFrame();
         expect("#first").toHaveCount(0);
         expect("#second").toHaveCount(0);
@@ -463,16 +469,20 @@ describe("makePreviewableOperation", () => {
             newElem.setAttribute("id", elemId);
             div.appendChild(newElem);
         });
-        const numberOfSteps = history.steps.length;
+        let numberOfSteps = history.steps.length;
         previewableAddParagraph.preview("first");
+        // step added by the preview
+        numberOfSteps += 1;
         await animationFrame();
         expect(history.steps.length).toBe(numberOfSteps);
         expect("#first").toHaveCount(1);
         previewableAddParagraph.commit("second");
+        // step added by the revert due to the commit and the commit in itself
+        numberOfSteps += 2;
         await animationFrame();
         expect("#first").toHaveCount(0);
         expect("#second").toHaveCount(1);
-        expect(history.steps.length).toBe(numberOfSteps + 1);
+        expect(history.steps.length).toBe(numberOfSteps);
     });
 });
 


Reviews:
user: login: loco-odoo
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T10:10:48Z
user: login: FrancoisGe
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T10:36:58Z

Review comments:
diff_hunk: @@ -64,10 +64,6 @@ export class WebsiteBuilder extends Component {
         this.setIframeLoaded();
         this.addSystrayItems();
         onWillDestroy(() => {
-            window.parent.document.removeEventListener(
-                "website_edit_loaded",
-                this.updateEditInteraction
-            );
path: addons/html_builder/static/src/website_preview/website_builder_action.js
user: login: loco-odoo
body: I suppose it's an oversight; `website_edit_loaded` is never added as an event listener
created_at: 2025-03-10T10:10:48Z
author_association: NONE
diff_hunk: @@ -107,8 +108,13 @@ class BackgroundImageOptionPlugin extends Plugin {
             onClose.then(resolve);
         });
     }
-    applyReplaceBackgroundImage({ editingElement, loadResult: imageSrc }) {
-        if (!imageSrc) {
+    applyReplaceBackgroundImage({
+        editingElement,
+        loadResult: imageSrc,
+        param: { isCleaning = false },
+    }) {
+        if (!isCleaning && !imageSrc) {
+            // Do nothing: no images has been selected on the media dialog
path: addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js
user: login: FrancoisGe
body: Could you explain ?
created_at: 2025-03-10T10:31:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,42 @@
+import { Plugin } from "@html_editor/plugin";
+import { after, expect, test } from "@odoo/hoot";
+import { xml } from "@odoo/owl";
+import { contains } from "@web/../tests/web_test_helpers";
+import { registry } from "@web/core/registry";
+import { uniqueId } from "@web/core/utils/functions";
+import { addOption, defineWebsiteModels, setupWebsiteBuilder } from "./website_helpers";
+
+defineWebsiteModels();
+
+test("do not update DOM if in preview mode", async () => {
path: addons/html_builder/static/tests/preview_mode.test.js
user: login: FrancoisGe
body: website DOM or builder DOM ? 
created_at: 2025-03-10T10:34:35Z
author_association: NONE
diff_hunk: @@ -967,6 +968,7 @@ export class HistoryPlugin extends Plugin {
             this.handleObserverRecords();
             // TODO ABD TODO @phoenix: evaluate if the selection is not restorable at the desired position
             selectionToRestore.restore();
+            this.currentStep.extra = extraToRestore;
path: addons/html_editor/static/src/core/history_plugin.js
user: login: FrancoisGe
body: why you need to do it 2 ?
created_at: 2025-03-10T10:35:42Z
author_association: NONE

PR comments:
user: login: robodoo
created_at: 2025-03-06T13:09:36Z
body: This PR targets the un-managed branch odoo-dev/odoo:master-mysterious-egg, it needs to be retargeted before it can be merged.
author_association: NONE
user: login: robodoo
created_at: 2025-03-10T13:21:58Z
body: This PR targets the un-managed branch odoo-dev/odoo:master-mysterious-egg, it needs to be retargeted before it can be merged.
author_association: NONE