[FIX] runtime/fibers: render a parent when self is in error

Have a Component A which instantiate another one (B) which also have some Children (C).
Have B be the one to handle the errors from the Children via the onError hook.
This hook should call a props coming from A. The real error handler is on A then, passed to B via props.

This handler naturally sets a flag on A, and triggers a render.

Before this commit, the mounting of A never resolved as the fiber that was recycled in A
was still part of a root one which was flagged as being in error.

After this commit, the A component mounts correctly.

Closes #1298
This commit is contained in:
Lucas Perais
2022-11-30 15:22:51 +01:00
parent 203ac7ac66
commit b1d95eae11
4 changed files with 253 additions and 2 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (fibersInError.has(current)) {
if (fibersInError.has(root)) {
fibersInError.delete(current);
fibersInError.delete(root);
current.appliedToDom = false;
@@ -1195,6 +1195,131 @@ exports[`can catch errors onError in class inheritance is not called if no rethr
}"
`;
exports[`can catch errors re-render parent when self is in error - 2 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Classic\`, true, false, false, false);
const comp2 = app.createComponent(\`Classic\`, true, false, false, false);
return function template(ctx, node, key = \\"\\") {
const b2 = comp1({hasBoom: true,state: ctx['reactive']}, key + \`__1\`, node, this, null);
const b3 = comp2({hasBoom: false,state: ctx['reactive']}, key + \`__2\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors re-render parent when self is in error - 2 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`BoomWrapper\`, true, false, false, false);
let block3 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['props'].hasBoom) {
b2 = comp1({state: ctx['props'].state}, key + \`__1\`, node, this, null);
} else {
let txt1 = ctx['props'].state.safeTree;
b3 = block3([txt1]);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors re-render parent when self is in error - 2 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Boom\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['state'].errorTree==='error') {
b2 = comp1({}, key + \`__1\`, node, this, null);
} else {
b3 = text(ctx['state'].errorTree);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors re-render parent when self is in error - 2 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors re-render parent when self is in error 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Classic\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`can catch errors re-render parent when self is in error 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { bind } = helpers;
const comp1 = app.createComponent(\`BoomWrapper\`, true, false, false, false);
let block3 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (!ctx['inError']) {
b2 = comp1({onError: bind(this, ctx['onErrorAsProps'])}, key + \`__1\`, node, this, null);
} else {
b3 = block3();
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors re-render parent when self is in error 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Boom\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`can catch errors re-render parent when self is in error 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`errors and promises a rendering error in a sub component will reject the mount promise 1`] = `
"function anonymous(app, bdom, helpers
) {
+126
View File
@@ -9,6 +9,7 @@ import {
onRendered,
onWillUnmount,
useState,
reactive,
xml,
} from "../../src/index";
import {
@@ -785,6 +786,131 @@ describe("can catch errors", () => {
expect(mockConsoleWarn).toBeCalledTimes(0);
});
test("re-render parent when self is in error", async () => {
class Boom extends Component {
static template = xml`<div />`;
setup() {
onWillStart(() => {
throw new Error("Boom Error");
});
}
}
const steps: string[] = [];
class BoomWrapper extends Component {
static template = xml`<Boom />`;
static components = { Boom };
setup() {
onError(() => {
steps.push("onError in child");
this.props.onError();
});
}
}
class Classic extends Component {
static template = xml`<BoomWrapper t-if="!inError" onError.bind="onErrorAsProps"/><div t-else="" />`;
static components = { BoomWrapper };
inError: Boolean = false;
setup() {
onMounted(() => {
steps.push("mounted");
});
}
onErrorAsProps() {
this.inError = true;
this.render(true);
}
}
class App extends Component {
static template = xml`<Classic />`;
static components = { Classic };
}
await mount(App, fixture);
expect(steps).toEqual(["onError in child", "mounted"]);
});
test("re-render parent when self is in error - 2", async () => {
class Boom extends Component {
static template = xml`<div />`;
setup() {
onWillStart(() => {
throw new Error("Boom Error");
});
}
}
const steps: string[] = [];
class BoomWrapper extends Component {
static template = xml`<Boom t-if="state.errorTree === 'error'" /><t t-else="" t-esc="state.errorTree" />`;
static components = { Boom };
state: any;
setup() {
this.state = useState(this.props.state);
onError(() => {
steps.push("onError");
this.state.onError();
});
onWillRender(() => {
steps.push(`BoomWrapper willRender`);
});
}
}
let classicId = 0;
class Classic extends Component {
static template = xml`
<BoomWrapper t-if="props.hasBoom" state="props.state"/>
<div t-else="" t-esc="props.state.safeTree" />
`;
static components = { BoomWrapper };
id: Number = 0;
state: any;
inError: Boolean = false;
setup() {
this.id = classicId++;
onMounted(() => {
steps.push(`mounted ${this.id}`);
});
onWillRender(() => {
steps.push(`Classic willRender ${this.id}`);
});
}
}
class App extends Component {
reactive: any;
setup() {
this.reactive = reactive({
errorTree: "error",
safeTree: "safe",
onError() {
this.safeTree = "safe2";
this.errorTree = "errorHandled";
},
});
}
static template = xml`<Classic hasBoom="true" state="reactive" /><Classic hasBoom="false" state="reactive"/>`;
static components = { Classic };
}
await mount(App, fixture);
expect(steps).toEqual([
"Classic willRender 0",
"Classic willRender 1",
"BoomWrapper willRender",
"onError",
"Classic willRender 1",
"BoomWrapper willRender",
"mounted 1",
"mounted 0",
]);
expect(fixture.innerHTML).toBe("errorHandled<div>safe2</div>");
});
test("can catch an error in the willStart call", async () => {
class ErrorComponent extends Component {
static template = xml`<div>Some text</div>`;
+1 -1
View File
@@ -316,7 +316,7 @@ describe("t-call", () => {
expect(fixture.innerHTML).toBe("childaaronchildlucas");
});
test.only("t-call-context: ComponentNode is not looked up in the context", async () => {
test("t-call-context: ComponentNode is not looked up in the context", async () => {
let child: any;
class Child extends Component {
static template = xml`<t t-slot="default"/>`;