import { Component, Env, mount, STATUS } from "../../src/component/component"; import { useState } from "../../src/hooks"; import { xml } from "../../src/tags"; import { makeTestEnv, makeTestFixture, nextTick } from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers //------------------------------------------------------------------------------ // We create before each test: // - fixture: a div, appended to the DOM, intended to be the target of dom // manipulations. Note that it is removed after each test. // - env: a WEnv, necessary to create new components let fixture: HTMLElement; let env: Env; beforeEach(() => { fixture = makeTestFixture(); env = makeTestEnv(); Component.env = env; }); afterEach(() => { fixture.remove(); }); describe("component error handling (catchError)", () => { /** * This test suite requires often to wait for 3 ticks. Here is why: * - First tick is to let the app render and crash. * - When we crash, we call the catchError handler in a setTimeout (because we * need to wait for the previous rendering to be completely stopped). So, we * need to wait for the second tick. * - Then, when the handler changes the state, we need to wait for the interface * to be rerendered. * */ test("can catch an error in a component render function", async () => { const consoleError = console.error; console.error = jest.fn(); const handler = jest.fn(); env.qweb.on("error", null, handler); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("no component catching error lead to full app destruction", async () => { expect.assertions(6); const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class App extends Component { static template = xml`
`; static components = { ErrorComponent }; state = useState({ flag: false }); async render() { try { await super.render(); } catch (e) { expect(e.message).toMatch( /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/ ); } } } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
heyfalse
"); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(app.__owl__.status).toBe(STATUS.DESTROYED); expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function (parent mounted)", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the initial call of a component render function (parent updated)", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
hey
`; } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ flag: false }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); app.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the constructor call of a component render function", async () => { const handler = jest.fn(); env.qweb.on("error", null, handler); const consoleError = console.error; console.error = jest.fn(); env.qweb.addTemplates(`
Error handled
Some text
`); class ErrorComponent extends Component { constructor(parent) { super(parent); throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; expect(handler).toBeCalledTimes(1); }); test("can catch an error in the willStart call", async () => { const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
Some text
`; async willStart() { // we wait a little bit to be in a different stack frame await nextTick(); throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
Error handled
"); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test.skip("can catch an error in the mounted call", async () => { // we do not catch error in mounted anymore console.error = jest.fn(); env.qweb.addTemplates(`
Error handled
Some text
`); class ErrorComponent extends Component { mounted() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
Error handled
"); }); test.skip("can catch an error in the willPatch call", async () => { // we do not catch error in willPatch anymore const consoleError = console.error; console.error = jest.fn(); class ErrorComponent extends Component { static template = xml`
`; willPatch() { throw new Error("NOOOOO"); } } class ErrorBoundary extends Component { static template = xml`
Error handled
`; state = useState({ error: false }); catchError() { this.state.error = true; } } class App extends Component { static template = xml`
`; state = useState({ message: "abc" }); static components = { ErrorBoundary, ErrorComponent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
abc
abc
"); app.state.message = "def"; await nextTick(); await nextTick(); await nextTick(); expect(fixture.innerHTML).toBe("
def
Error handled
"); expect(console.error).toHaveBeenCalledTimes(1); console.error = consoleError; }); test("a rendering error will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class App extends Component { static template = xml`
`; } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in mounted call will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
abc
`; mounted() { throw new Error("boom"); } } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in willPatch call will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
`; val = 3; willPatch() { throw new Error("boom"); } } const app = new App(); await app.mount(fixture); app.val = 4; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("an error in patched call will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); class App extends Component { static template = xml`
`; val = 3; patched() { throw new Error("boom"); } } const app = new App(); await app.mount(fixture); app.val = 4; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("boom"); expect(fixture.innerHTML).toBe(""); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error in a sub component will reject the mount promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class Child extends Component { static template = xml`
`; } class App extends Component { static template = xml`
`; static components = { Child }; } const app = new App(); let error; try { await app.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error will reject the render promise", async () => { const consoleError = console.error; console.error = jest.fn(() => {}); // we do not catch error in willPatch anymore class App extends Component { static template = xml`
`; flag = false; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe("
"); app.flag = true; let error; try { await app.render(); } catch (e) { error = e; } expect(error).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); expect(console.error).toBeCalledTimes(0); console.error = consoleError; }); test("a rendering error will reject the render promise (with sub components)", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } let error; try { const parent = new Parent(); await parent.mount(fixture); } catch (e) { error = e; } expect(error).toBeDefined(); const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g; expect(error.message).toMatch(regexp); }); test("simple catchError", async () => { class Boom extends Component { static template = xml`
`; } class Parent extends Component { static template = xml`
Error
`; static components = { Boom }; error = false; catchError(error) { this.error = error; this.render(); } } await mount(Parent, { target: fixture }); expect(fixture.innerHTML).toBe("
Error
"); }); test("catchError in catchError", async () => { class Boom extends Component { static template = xml`
`; } class Child extends Component { static template = xml`
`; static components = { Boom }; catchError(error) { throw error; } } class Parent extends Component { static template = xml`
Error
`; static components = { Child }; error = false; catchError(error) { this.error = error; this.render(); } } await mount(Parent, { target: fixture }); expect(fixture.innerHTML).toBe("
Error
"); }); test("errors in mounted and in willUnmount", async () => { expect.assertions(1); class Example extends Component { static template = xml`
`; val; mounted() { throw new Error("Error in mounted"); this.val = { foo: "bar" }; } willUnmount() { console.log(this.val.foo); } } try { await mount(Example, { target: fixture }); } catch (e) { expect(e.message).toBe("Error in mounted"); } }); });