From 7b3b421c4df40d9ccfd47745d90876c61490ebba Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 2 Dec 2021 14:23:17 +0000 Subject: [PATCH] [FIX] web: rerender MainComponentsContainer on main component error Previously, when a main component errored during rendering, we would remove it from the list of main components that should be rendered by the MainComponentsContainer to avoid it erroring again on a subsequent rendering (which can cause infinite error loops). Unfortunately, since we did not rerender the MainComponentsContainer after this removal, the erroring main component would remain, and could cause such error loops anyway. This commit fixes that. closes odoo/odoo#80822 X-original-commit: 0a2fa98086dfdaa36b01ca8ef3e22b8b8ffef60c Signed-off-by: Lucas Perais (lpe) Signed-off-by: Aaron Bohy (aab) --- .../src/core/main_components_container.js | 3 +- .../core/main_components_container_tests.js | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 addons/web/static/tests/core/main_components_container_tests.js 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
"); + }); +});