diff --git a/addons/web/static/src/core/main_components_container.js b/addons/web/static/src/core/main_components_container.js index 8a8b4a4a7d0..477f3f5e250 100644 --- a/addons/web/static/src/core/main_components_container.js +++ b/addons/web/static/src/core/main_components_container.js @@ -10,8 +10,9 @@ export class MainComponentsContainer extends Component { } handleComponentError(error, C) { - // remove the faulty component + // remove the faulty component and rerender without it this.Components.splice(this.Components.indexOf(C), 1); + this.render(); /** * we rethrow the error to notify the user something bad happened. * We do it after a tick to make sure owl can properly finish its diff --git a/addons/web/static/tests/core/main_components_container_tests.js b/addons/web/static/tests/core/main_components_container_tests.js new file mode 100644 index 00000000000..3f853d01ca9 --- /dev/null +++ b/addons/web/static/tests/core/main_components_container_tests.js @@ -0,0 +1,136 @@ +/** @odoo-module **/ +import { MainComponentsContainer } from "@web/core/main_components_container"; +import { registry } from "@web/core/registry"; +import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env"; +import { patch, unpatch } from "@web/core/utils/patch"; +import { getFixture, nextTick } from "../helpers/utils"; + +const { mount, Component } = owl; +const { useState } = owl.hooks; +const { xml } = owl.tags; +const mainComponentsRegistry = registry.category("main_components"); +let container; +let target; + +QUnit.module("Components", (hooks) => { + hooks.beforeEach(async () => { + target = getFixture(); + clearRegistryWithCleanup(mainComponentsRegistry); + }); + hooks.afterEach(() => { + if (container) { + container.unmount(); + container = undefined; + } + }); + + QUnit.module("MainComponentsContainer"); + + QUnit.test("simple rendering", async function (assert) { + const env = await makeTestEnv(); + + class MainComponentA extends Component {} + MainComponentA.template = xml`MainComponentA`; + + class MainComponentB extends Component {} + MainComponentB.template = xml`MainComponentB`; + + mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} }); + mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} }); + container = await mount(MainComponentsContainer, { env, target, props: {} }); + assert.equal( + container.el.outerHTML, + "
MainComponentAMainComponentB
" + ); + }); + + QUnit.test("unmounts erroring main component", async function (assert) { + const env = await makeTestEnv(); + + let compA; + class MainComponentA extends Component { + setup() { + compA = this; + this.state = useState({ shouldThrow: false }); + } + get error() { + throw new Error("BOOM"); + } + } + MainComponentA.template = xml`MainComponentA`; + + class MainComponentB extends Component {} + MainComponentB.template = xml`MainComponentB`; + + mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} }); + mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} }); + container = await mount(MainComponentsContainer, { env, target, props: {} }); + assert.equal( + container.el.outerHTML, + "
MainComponentAMainComponentB
" + ); + + const handler = (ev) => { + assert.step(ev.reason.message); + // need to preventDefault to remove error from console (so python test pass) + ev.preventDefault(); + }; + window.addEventListener("unhandledrejection", handler); + patch(QUnit, "MainComponentsContainer QUnit patch", { + onUnhandledRejection: () => {}, + }); + compA.state.shouldThrow = true; + await nextTick(); + window.removeEventListener("unhandledrejection", handler); + // unpatch QUnit asap so any other errors can be caught by it + unpatch(QUnit, "MainComponentsContainer QUnit patch"); + assert.verifySteps(["BOOM"]); + + assert.equal(container.el.outerHTML, "
MainComponentB
"); + }); + + QUnit.test("unmounts erroring main component: variation", async function (assert) { + const env = await makeTestEnv(); + + class MainComponentA extends Component {} + MainComponentA.template = xml`MainComponentA`; + + let compB; + class MainComponentB extends Component { + setup() { + compB = this; + this.state = useState({ shouldThrow: false }); + } + get error() { + throw new Error("BOOM"); + } + } + MainComponentB.template = xml`MainComponentB`; + + mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} }); + mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} }); + container = await mount(MainComponentsContainer, { env, target, props: {} }); + assert.equal( + container.el.outerHTML, + "
MainComponentAMainComponentB
" + ); + + const handler = (ev) => { + assert.step(ev.reason.message); + // need to preventDefault to remove error from console (so python test pass) + ev.preventDefault(); + }; + window.addEventListener("unhandledrejection", handler); + patch(QUnit, "MainComponentsContainer QUnit patch", { + onUnhandledRejection: () => {}, + }); + compB.state.shouldThrow = true; + await nextTick(); + window.removeEventListener("unhandledrejection", handler); + // unpatch QUnit asap so any other errors can be caught by it + unpatch(QUnit, "MainComponentsContainer QUnit patch"); + assert.verifySteps(["BOOM"]); + + assert.equal(container.el.outerHTML, "
MainComponentA
"); + }); +});