From e1ff7ac6a4d4311523b5dfa512a7ad61921f2d3a Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 8 Jun 2023 14:34:45 +0200 Subject: [PATCH] [FIX] runtime: do not crash when capturing context with getter When attempting to capture a rendering context that contains an enumerable getter, there is a crash because we attempt to write a value on the getter. This commit fixes that by manually climbing the prototype chain to copy the values instead, and ignoring getters. --- src/runtime/template_helpers.ts | 16 ++++++- .../__snapshots__/t_call.test.ts.snap | 45 +++++++++++++++++++ tests/components/t_call.test.ts | 22 +++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/runtime/template_helpers.ts b/src/runtime/template_helpers.ts index 239e1029..53bae4e3 100644 --- a/src/runtime/template_helpers.ts +++ b/src/runtime/template_helpers.ts @@ -8,6 +8,12 @@ import { OwlError } from "./error_handling"; import type { ComponentNode } from "./component_node"; const ObjectCreate = Object.create; +const ObjectGetPrototypeOf = Object.getPrototypeOf; +const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; +const ObjectDefineProperty = Object.defineProperty; +const ObjectEntries = Object.entries; +const hasOwnProperty = (obj: Object, prop: PropertyKey) => + Object.prototype.hasOwnProperty.call(obj, prop); /** * This file contains utility functions that will be injected in each template, * to perform various useful tasks in the compiled code. @@ -49,8 +55,14 @@ function callSlot( function capture(ctx: any): any { const result = ObjectCreate(ctx); - for (let k in ctx) { - result[k] = ctx[k]; + let current = ctx; + while (current && current !== Object.prototype) { + for (const [key, descriptor] of ObjectEntries(ObjectGetOwnPropertyDescriptors(current))) { + if (!hasOwnProperty(result, key) && "value" in descriptor) { + ObjectDefineProperty(result, key, descriptor); + } + } + current = ObjectGetPrototypeOf(current); } return result; } diff --git a/tests/components/__snapshots__/t_call.test.ts.snap b/tests/components/__snapshots__/t_call.test.ts.snap index b5825bdc..1d8ca3fc 100644 --- a/tests/components/__snapshots__/t_call.test.ts.snap +++ b/tests/components/__snapshots__/t_call.test.ts.snap @@ -1,5 +1,50 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`t-call component with an enumerable getter, t-call inside slot 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { capture, markRaw } = helpers; + const callTemplate_1 = app.getTemplate(\`sub\`); + const comp1 = app.createComponent(\`Child\`, true, true, false, []); + + function slot1(ctx, node, key = \\"\\") { + return callTemplate_1.call(this, ctx, node, key + \`__1\`); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null); + } +}" +`; + +exports[`t-call component with an enumerable getter, t-call inside slot 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['foo']; + return block1([txt1]); + } +}" +`; + +exports[`t-call component with an enumerable getter, t-call inside slot 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + return callSlot(ctx, node, key, 'default', false, {}); + } +}" +`; + exports[`t-call dynamic t-call 1`] = ` "function anonymous(app, bdom, helpers ) { diff --git a/tests/components/t_call.test.ts b/tests/components/t_call.test.ts index 2080560e..a8d05c0d 100644 --- a/tests/components/t_call.test.ts +++ b/tests/components/t_call.test.ts @@ -425,4 +425,26 @@ describe("t-call", () => { await nextTick(); expect(fixture.innerHTML).toBe("Bchild"); }); + + test("component with an enumerable getter, t-call inside slot", async () => { + class Child extends Component { + static template = xml``; + } + class Parent extends Component { + static components = { Child }; + static template = xml``; + } + // simulate adding a getter with patch in odoo: getter will be enumarable + Object.defineProperty(Parent.prototype, "foo", { + get() { + return 1; + }, + enumerable: true, + }); + const app = new App(Parent); + app.addTemplate("sub", `
`); + + await app.mount(fixture); + expect(fixture.innerHTML).toBe("
1
"); + }); });