[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) <lpe@odoo.com>
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Samuel Degueldre
2021-12-03 09:44:04 +00:00
parent 711780944a
commit 7b3b421c4d
2 changed files with 138 additions and 1 deletions
@@ -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
@@ -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`<span>MainComponentA</span>`;
class MainComponentB extends Component {}
MainComponentB.template = xml`<span>MainComponentB</span>`;
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
container = await mount(MainComponentsContainer, { env, target, props: {} });
assert.equal(
container.el.outerHTML,
"<div><span>MainComponentA</span><span>MainComponentB</span></div>"
);
});
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`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentA</span>`;
class MainComponentB extends Component {}
MainComponentB.template = xml`<span>MainComponentB</span>`;
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
container = await mount(MainComponentsContainer, { env, target, props: {} });
assert.equal(
container.el.outerHTML,
"<div><span>MainComponentA</span><span>MainComponentB</span></div>"
);
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, "<div><span>MainComponentB</span></div>");
});
QUnit.test("unmounts erroring main component: variation", async function (assert) {
const env = await makeTestEnv();
class MainComponentA extends Component {}
MainComponentA.template = xml`<span>MainComponentA</span>`;
let compB;
class MainComponentB extends Component {
setup() {
compB = this;
this.state = useState({ shouldThrow: false });
}
get error() {
throw new Error("BOOM");
}
}
MainComponentB.template = xml`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentB</span>`;
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
container = await mount(MainComponentsContainer, { env, target, props: {} });
assert.equal(
container.el.outerHTML,
"<div><span>MainComponentA</span><span>MainComponentB</span></div>"
);
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, "<div><span>MainComponentA</span></div>");
});
});