mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[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:
@@ -25,7 +25,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
current.children = [];
|
current.children = [];
|
||||||
current.childrenMap = {};
|
current.childrenMap = {};
|
||||||
current.bdom = null;
|
current.bdom = null;
|
||||||
if (fibersInError.has(current)) {
|
if (fibersInError.has(root)) {
|
||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
fibersInError.delete(root);
|
fibersInError.delete(root);
|
||||||
current.appliedToDom = false;
|
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`] = `
|
exports[`errors and promises a rendering error in a sub component will reject the mount promise 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
onRendered,
|
onRendered,
|
||||||
onWillUnmount,
|
onWillUnmount,
|
||||||
useState,
|
useState,
|
||||||
|
reactive,
|
||||||
xml,
|
xml,
|
||||||
} from "../../src/index";
|
} from "../../src/index";
|
||||||
import {
|
import {
|
||||||
@@ -785,6 +786,131 @@ describe("can catch errors", () => {
|
|||||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
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 () => {
|
test("can catch an error in the willStart call", async () => {
|
||||||
class ErrorComponent extends Component {
|
class ErrorComponent extends Component {
|
||||||
static template = xml`<div>Some text</div>`;
|
static template = xml`<div>Some text</div>`;
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ describe("t-call", () => {
|
|||||||
expect(fixture.innerHTML).toBe("childaaronchildlucas");
|
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;
|
let child: any;
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
static template = xml`<t t-slot="default"/>`;
|
static template = xml`<t t-slot="default"/>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user