From c16d8ed6dec5163a4467207c8b1a655178b7c030 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Wed, 20 Oct 2021 16:08:31 +0200 Subject: [PATCH] [IMP] component,qweb: props validation We re-add the possibility to validate props when dev mode is active. No change in the API right Now. The dev mode is activated via the configure method of App class. --- src/app.ts | 32 +- src/component/props_validation.ts | 120 ++ src/qweb/compiler.ts | 11 + src/qweb/template_helpers.ts | 4 + .../props_validation.test.ts.snap | 458 ++++++ tests/components/basics.test.ts | 2 +- tests/components/event_handling.test.ts | 2 +- tests/components/hooks.test.ts | 27 +- tests/components/lifecycle.test.ts | 4 +- tests/components/props_validation.test.ts | 1339 ++++++++--------- tests/helpers.ts | 1 + tests/misc/async_root.test.ts | 6 +- tests/misc/portal.test.ts | 3 +- tests/qweb/error_handling.test.ts | 6 +- tests/qweb/t_tag.test.ts | 7 +- 15 files changed, 1308 insertions(+), 714 deletions(-) create mode 100644 src/component/props_validation.ts create mode 100644 tests/components/__snapshots__/props_validation.test.ts.snap diff --git a/src/app.ts b/src/app.ts index 85060cf5..569bf7fe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,13 +5,24 @@ import { TemplateSet } from "./qweb/template_helpers"; // reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f +interface Config { + dev?: boolean; + env?: { [key: string]: any }; + translatableAttributes?: string[]; + translateFn?: (s: string) => string; +} + +export const DEV_MSG = `Owl is running in 'dev' mode. + +This is not suitable for production use. +See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`; + export class App extends TemplateSet { Root: T; props: any; env: any = {}; scheduler = new Scheduler(window.requestAnimationFrame.bind(window)); root: ComponentNode | null = null; - dev: boolean = true; constructor(Root: T, props?: any) { super(); @@ -19,15 +30,20 @@ export class App extends TemplateSet { this.props = props; } - configure(params: any) { - if (params.env) { - this.env = params.env; + configure(config: Config) { + if (config.dev) { + this.dev = config.dev; + console.info(DEV_MSG); } - if (params.translateFn) { - this.translateFn = params.translateFn; + + if (config.env) { + this.env = config.env; } - if (params.translatableAttributes) { - this.translatableAttributes = params.translatableAttributes; + if (config.translateFn) { + this.translateFn = config.translateFn; + } + if (config.translatableAttributes) { + this.translatableAttributes = config.translatableAttributes; } } diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts new file mode 100644 index 00000000..3de1322a --- /dev/null +++ b/src/component/props_validation.ts @@ -0,0 +1,120 @@ +import { Component } from "./component"; + +//------------------------------------------------------------------------------ +// Prop validation helper +//------------------------------------------------------------------------------ + +/** + * Validate the component props (or next props) against the (static) props + * description. This is potentially an expensive operation: it may needs to + * visit recursively the props and all the children to check if they are valid. + * This is why it is only done in 'dev' mode. + */ + +// message: { type: String, default: "hello" }; + +export const validateProps = function (name: string | typeof Component, props: any, parent?: any) { + const ComponentClass = + typeof name !== "string" ? name : parent.constructor.components[name as string]; + + const propsDef = (ComponentClass).props; + if (propsDef instanceof Array) { + // list of strings (prop names) + for (let i = 0, l = propsDef.length; i < l; i++) { + const propName = propsDef[i]; + if (propName[propName.length - 1] === "?") { + // optional prop + break; + } + if (!(propName in props)) { + throw new Error(`Missing props '${propsDef[i]}' (component '${ComponentClass.name}')`); + } + } + for (let key in props) { + if (!propsDef.includes(key) && !propsDef.includes(key + "?")) { + throw new Error(`Unknown prop '${key}' given to component '${ComponentClass.name}'`); + } + } + } else if (propsDef) { + // propsDef is an object now + for (let propName in propsDef) { + if (props[propName] === undefined) { + if (propsDef[propName] && !propsDef[propName].optional) { + throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`); + } else { + continue; + } + } + let isValid; + try { + isValid = isValidProp(props[propName], propsDef[propName]); + } catch (e) { + e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`; + throw e; + } + if (!isValid) { + throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`); + } + } + for (let propName in props) { + if (!(propName in propsDef)) { + throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`); + } + } + } +}; + +/** + * Check if an invidual prop value matches its (static) prop definition + */ +function isValidProp(prop: any, propDef: any): boolean { + if (propDef === true) { + return true; + } + if (typeof propDef === "function") { + // Check if a value is constructed by some Constructor. Note that there is a + // slight abuse of language: we want to consider primitive values as well. + // + // So, even though 1 is not an instance of Number, we want to consider that + // it is valid. + if (typeof prop === "object") { + return prop instanceof propDef; + } + return typeof prop === propDef.name.toLowerCase(); + } else if (propDef instanceof Array) { + // If this code is executed, this means that we want to check if a prop + // matches at least one of its descriptor. + let result = false; + for (let i = 0, iLen = propDef.length; i < iLen; i++) { + result = result || isValidProp(prop, propDef[i]); + } + return result; + } + // propsDef is an object + if (propDef.optional && prop === undefined) { + return true; + } + let result = propDef.type ? isValidProp(prop, propDef.type) : true; + if (propDef.validate) { + result = result && propDef.validate(prop); + } + if (propDef.type === Array && propDef.element) { + for (let i = 0, iLen = prop.length; i < iLen; i++) { + result = result && isValidProp(prop[i], propDef.element); + } + } + if (propDef.type === Object && propDef.shape) { + const shape = propDef.shape; + for (let key in shape) { + result = result && isValidProp(prop[key], shape[key]); + } + if (result) { + for (let propName in prop) { + if (!(propName in shape)) { + throw new Error(`unknown prop '${propName}'`); + } + } + } + } + return result; +} diff --git a/src/qweb/compiler.ts b/src/qweb/compiler.ts index 89a8d3c4..9dc92983 100644 --- a/src/qweb/compiler.ts +++ b/src/qweb/compiler.ts @@ -32,6 +32,7 @@ export interface CompileOptions { name?: string; translateFn?: (s: string) => string; translatableAttributes?: string[]; + dev?: boolean; } export function compileTemplate(template: string, options?: CompileOptions): TemplateFunction { @@ -177,6 +178,7 @@ export class QWebCompiler { target = new CodeTarget("main"); templateName: string; template: string; + dev: boolean; translateFn: (s: string) => string; translatableAttributes: string[]; ast: AST; @@ -186,6 +188,7 @@ export class QWebCompiler { this.template = template; this.translateFn = options.translateFn || ((s: string) => s); this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS; + this.dev = options.dev || false; this.ast = parse(template); if (options.name) { this.templateName = options.name; @@ -925,6 +928,14 @@ export class QWebCompiler { } else { expr = `\`${ast.name}\``; } + + if (this.dev) { + const propVar = this.generateId("props"); + this.addLine(`const ${propVar} = ${propString}`); + this.addLine(`helpers.validateProps(${expr}, ${propVar}, ctx)`); + propString = propVar; + } + let blockArgs = `${expr}, ${propString}, key + \`${key}\`, node, ctx`; // slots diff --git a/src/qweb/template_helpers.ts b/src/qweb/template_helpers.ts index 2978969b..39781897 100644 --- a/src/qweb/template_helpers.ts +++ b/src/qweb/template_helpers.ts @@ -1,6 +1,7 @@ import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom"; import { compileTemplate, Template } from "./compiler"; import { component } from "../component/component_node"; +import { validateProps } from "../component/props_validation"; const bdom = { text, createBlock, list, multi, html, toggler, component }; @@ -94,6 +95,7 @@ export const UTILS = { prepareList, setContextValue, shallowEqual, + validateProps, }; export class TemplateSet { @@ -102,6 +104,7 @@ export class TemplateSet { translateFn?: (s: string) => string; translatableAttributes?: string[]; utils: typeof UTILS; + dev?: boolean; constructor() { const call = (subTemplate: string, ctx: any, parent: any) => { @@ -128,6 +131,7 @@ export class TemplateSet { } const templateFn = compileTemplate(rawTemplate, { name, + dev: this.dev, translateFn: this.translateFn, translatableAttributes: this.translatableAttributes, }); diff --git a/tests/components/__snapshots__/props_validation.test.ts.snap b/tests/components/__snapshots__/props_validation.test.ts.snap new file mode 100644 index 00000000..96fc3aae --- /dev/null +++ b/tests/components/__snapshots__/props_validation.test.ts.snap @@ -0,0 +1,458 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`props validation can validate a prop with multiple types 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate a prop with multiple types 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an array with given primitive type 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an array with given primitive type 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an array with multiple sub element types 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an array with multiple sub element types 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an array with multiple sub element types 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an object with simple shape 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an optional props 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate an optional props 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate recursively complicated prop def 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation can validate recursively complicated prop def 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation props are validated in dev mode (code snapshot) 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {message: 1} + helpers.validateProps(\`Child\`, props2, ctx) + let b2 = component(\`Child\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation props are validated whenever component is updated 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['state'].p} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 4`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 5`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types 6`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 4`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 5`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validate simple types, alternate form 6`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const props2 = {p: ctx['p']} + helpers.validateProps(\`SubComp\`, props2, ctx) + let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`props validation validation is only done in dev mode 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`SubComp\`, {}, key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index e200d8fb..ff9c7644 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -750,7 +750,7 @@ describe("basics", () => { static template = xml`abcdef`; } class Parent extends Component { - static template = xml`` + static template = xml``; static components = { Child }; state = useState({ flag: false }); } diff --git a/tests/components/event_handling.test.ts b/tests/components/event_handling.test.ts index 63cfb7c3..857f3e7e 100644 --- a/tests/components/event_handling.test.ts +++ b/tests/components/event_handling.test.ts @@ -320,6 +320,6 @@ describe("event handling", () => { } await mount(Parent, fixture); - (fixture.querySelector('.item')).click(); + (fixture.querySelector(".item")).click(); }); }); diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts index b01ee886..e767feae 100644 --- a/tests/components/hooks.test.ts +++ b/tests/components/hooks.test.ts @@ -1,9 +1,19 @@ -import { App, Component, mount, useRef, useState, useComponent, onMounted, onPatched, onWillStart, onWillUpdateProps, onWillPatch, xml, onWillUnmount } from "../../src"; import { - makeTestFixture, - nextTick, - snapshotEverything, -} from "../helpers"; + App, + Component, + mount, + useRef, + useState, + useComponent, + onMounted, + onPatched, + onWillStart, + onWillUpdateProps, + onWillPatch, + xml, + onWillUnmount, +} from "../../src"; +import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; @@ -85,7 +95,12 @@ describe("hooks", () => { const app = new App(Test); await app.mount(fixture); app.destroy(); - expect(steps).toEqual(["hook:mounted2", "hook:mounted1", "hook:willUnmount1", "hook:willUnmount2"]); + expect(steps).toEqual([ + "hook:mounted2", + "hook:mounted1", + "hook:willUnmount1", + "hook:willUnmount2", + ]); }); describe("autofocus hook", () => { diff --git a/tests/components/lifecycle.test.ts b/tests/components/lifecycle.test.ts index 1be37813..f119c259 100644 --- a/tests/components/lifecycle.test.ts +++ b/tests/components/lifecycle.test.ts @@ -1070,7 +1070,7 @@ describe("lifecycle hooks", () => { class Child extends Component { static template = xml`
child
`; setup() { - useLogLifecycle(steps) + useLogLifecycle(steps); } } @@ -1110,7 +1110,7 @@ describe("lifecycle hooks", () => { "Child:render", "Parent:willPatch", "Child:mounted", - "Parent:patched" + "Parent:patched", ]); Object.freeze(steps); }); diff --git a/tests/components/props_validation.test.ts b/tests/components/props_validation.test.ts index 849b0640..f6e0a21b 100644 --- a/tests/components/props_validation.test.ts +++ b/tests/components/props_validation.test.ts @@ -1,772 +1,741 @@ -// import { Component, Env } from "../../src/component/component"; -// import { makeTestFixture, makeTestEnv, nextTick } from "../helpers"; -// import { useState } from "../../src/hooks"; -// import { QWeb } from "../../src/qweb"; -// import { xml } from "../../src/tags"; +import { makeTestFixture, nextTick, snapshotApp } from "../helpers"; +import { Component, useState, xml } from "../../src"; +import { App, DEV_MSG } from "../../src/app"; +import { validateProps } from "../../src/component/props_validation"; -// //------------------------------------------------------------------------------ -// // Setup and helpers -// //------------------------------------------------------------------------------ +let fixture: HTMLElement; -// let fixture: HTMLElement; -// let env: Env; -// let dev: boolean = false; +const info = console.info; -// beforeEach(() => { -// fixture = makeTestFixture(); -// env = makeTestEnv(); -// Component.env = env; -// dev = QWeb.dev; -// QWeb.dev = true; -// }); +beforeAll(() => { + console.info = (message: any) => { + if (message === DEV_MSG) { + return; + } + info(message); + }; +}); -// afterEach(() => { -// fixture.remove(); -// QWeb.dev = dev; -// }); +afterAll(() => { + console.info = info; +}); -// class Widget extends Component {} +beforeEach(() => { + fixture = makeTestFixture(); +}); + +async function mountApp(Root: any, dev: boolean = true) { + const app = new App(Root); + app.configure({ dev }); + await app.mount(fixture); + snapshotApp(app); + return app; +} //------------------------------------------------------------------------------ -// Tests +// Props validation //------------------------------------------------------------------------------ -describe.skip("props validation", () => { + +describe("props validation", () => { test("validation is only done in dev mode", async () => { - // class TestWidget extends Widget { - // static props = ["message"]; - // static template = xml`
hey
`; - // } - // class Parent extends Widget { - // static components = { TestWidget }; - // static template = xml`
`; - // } - // let error; - // QWeb.dev = true; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`); - // error = undefined; - // QWeb.dev = false; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); + class SubComp extends Component { + static props = ["message"]; + static template = xml`
hey
`; + } + class Parent extends Component { + static components = { SubComp }; + static template = xml`
`; + } + let error; + + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`); + error = undefined; + + try { + await mountApp(Parent, false); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); }); test("props: list of strings", async () => { - // class TestWidget extends Widget { - // static props = ["message"]; - // static template = xml`
hey
`; - // } - // class Parent extends Widget { - // static components = { TestWidget }; - // static template = xml`
`; - // } - // let error; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`); + class SubComp extends Component { + static props = ["message"]; + static template = xml`
hey
`; + } + class Parent extends Component { + static components = { SubComp }; + static template = xml`
`; + } + + let error; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe(`Missing props 'message' (component 'SubComp')`); }); test("validate simple types", async () => { - // const Tests = [ - // { type: Number, ok: 1, ko: "1" }, - // { type: Boolean, ok: true, ko: "1" }, - // { type: String, ok: "1", ko: 1 }, - // { type: Object, ok: {}, ko: "1" }, - // { type: Date, ok: new Date(), ko: "1" }, - // { type: Function, ok: () => {}, ko: "1" }, - // ]; - // let props; - // class Parent extends Component { - // static template = xml`
`; - // get p() { - // return props.p; - // } - // } - // for (let test of Tests) { - // let TestWidget = class extends Widget { - // static template = xml`
hey
`; - // static props = { p: test.type }; - // }; - // Parent.components = { TestWidget }; - // let error; - // props = {}; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe(`Missing props 'p' (component '_a')`); - // error = undefined; - // props = { p: test.ok }; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // props = { p: test.ko }; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); - // } + const Tests = [ + { type: Number, ok: 1, ko: "1" }, + { type: Boolean, ok: true, ko: "1" }, + { type: String, ok: "1", ko: 1 }, + { type: Object, ok: {}, ko: "1" }, + { type: Date, ok: new Date(), ko: "1" }, + { type: Function, ok: () => {}, ko: "1" }, + ]; + let props: { p?: any }; + class Parent extends Component { + static template = xml`
`; + get p() { + return props.p; + } + } + for (let test of Tests) { + const SubComp = class extends Component { + static template = xml`
hey
`; + static props = { p: test.type }; + }; + (Parent as any).components = { SubComp }; + + let error; + props = {}; + + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe(`Missing props 'p' (component '_a')`); + error = undefined; + props = { p: test.ok }; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + props = { p: test.ko }; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); + } }); test("validate simple types, alternate form", async () => { - // const Tests = [ - // { type: Number, ok: 1, ko: "1" }, - // { type: Boolean, ok: true, ko: "1" }, - // { type: String, ok: "1", ko: 1 }, - // { type: Object, ok: {}, ko: "1" }, - // { type: Date, ok: new Date(), ko: "1" }, - // { type: Function, ok: () => {}, ko: "1" }, - // ]; - // let props; - // class Parent extends Component { - // static template = xml`
`; - // get p() { - // return props.p; - // } - // } - // for (let test of Tests) { - // let TestWidget = class extends Component { - // static props = { p: { type: test.type } }; - // static template = xml`
hey
`; - // }; - // Parent.components = { TestWidget }; - // let error; - // props = {}; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe(`Missing props 'p' (component '_a')`); - // error = undefined; - // props = { p: test.ok }; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // props = { p: test.ko }; - // try { - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); - // } + const Tests = [ + { type: Number, ok: 1, ko: "1" }, + { type: Boolean, ok: true, ko: "1" }, + { type: String, ok: "1", ko: 1 }, + { type: Object, ok: {}, ko: "1" }, + { type: Date, ok: new Date(), ko: "1" }, + { type: Function, ok: () => {}, ko: "1" }, + ]; + let props: { p?: any }; + class Parent extends Component { + static template = xml`
`; + get p() { + return props.p; + } + } + for (let test of Tests) { + const SubComp = class extends Component { + static props = { p: { type: test.type } }; + static template = xml`
hey
`; + }; + (Parent as any).components = { SubComp }; + let error; + props = {}; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe(`Missing props 'p' (component '_a')`); + error = undefined; + props = { p: test.ok }; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + props = { p: test.ko }; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); + } }); test("can validate a prop with multiple types", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { p: [String, Boolean] }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: "string" }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: true }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: 1 }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); + class SubComp extends Component { + static template = xml`
hey
`; + static props = { p: [String, Boolean] }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: "string" }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: true }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: 1 }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); }); test("can validate an optional props", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { p: { type: String, optional: true } }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: "key" }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = {}; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: 1 }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); + class SubComp extends Component { + static template = xml`
hey
`; + static props = { p: { type: String, optional: true } }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: "key" }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = {}; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: 1 }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); }); test("can validate an array with given primitive type", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { p: { type: Array, element: String } }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: [] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: ["string"] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: [1] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // error = undefined; - // try { - // props = { p: ["string", 1] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } + class SubComp extends Component { + static template = xml`
hey
`; + static props = { p: { type: Array, element: String } }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: [] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: ["string"] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: [1] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + error = undefined; + try { + props = { p: ["string", 1] }; + await mountApp(Parent); + } catch (e) { + error = e; + } }); test("can validate an array with multiple sub element types", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { p: { type: Array, element: [String, Boolean] } }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: [] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: ["string"] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: [false, true, "string"] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: [true, 1] }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); + class SubComp extends Component { + static template = xml`
hey
`; + static props = { p: { type: Array, element: [String, Boolean] } }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: [] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: ["string"] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: [false, true, "string"] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: [true, 1] }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); }); test("can validate an object with simple shape", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { - // p: { type: Object, shape: { id: Number, url: String } }, - // }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: { id: 1, url: "url" } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: { id: 1, url: "url", extra: true } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')"); - // try { - // props = { p: { id: "1", url: "url" } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); - // error = undefined; - // try { - // props = { p: { id: 1 } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); + class SubComp extends Component { + static template = xml`
hey
`; + static props = { + p: { type: Object, shape: { id: Number, url: String } }, + }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: { id: 1, url: "url" } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: { id: 1, url: "url", extra: true } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')"); + try { + props = { p: { id: "1", url: "url" } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + error = undefined; + try { + props = { p: { id: 1 } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); }); test("can validate recursively complicated prop def", async () => { - // class TestWidget extends Component { - // static template = xml`
hey
`; - // static props = { - // p: { - // type: Object, - // shape: { - // id: Number, - // url: [Boolean, { type: Array, element: Number }], - // }, - // }, - // }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // get p() { - // return props.p; - // } - // } - // let error; - // let props; - // try { - // props = { p: { id: 1, url: true } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: { id: 1, url: [12] } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // props = { p: { id: 1, url: [12, true] } }; - // const p = new Parent(); - // await p.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); + class SubComp extends Component { + static template = xml`
hey
`; + static props = { + p: { + type: Object, + shape: { + id: Number, + url: [Boolean, { type: Array, element: Number }], + }, + }, + }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + get p() { + return props.p; + } + } + let error; + let props: { p?: any }; + try { + props = { p: { id: 1, url: true } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: { id: 1, url: [12] } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + props = { p: { id: 1, url: [12, true] } }; + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'p' in component 'SubComp'"); }); test("can validate optional attributes in nested sub props", () => { - // class TestComponent extends Component { - // static props = { - // myprop: { - // type: Array, - // element: { - // type: Object, - // shape: { - // num: { type: Number, optional: true }, - // }, - // }, - // }, - // }; - // } - // let error; - // try { - // QWeb.utils.validateProps(TestComponent, { myprop: [{}] }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe( - // "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')" - // ); + class TestComponent extends Component { + static props = { + myprop: { + type: Array, + element: { + type: Object, + shape: { + num: { type: Number, optional: true }, + }, + }, + }, + }; + } + let error; + try { + validateProps(TestComponent as any, { myprop: [{}] }); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + validateProps(TestComponent as any, { myprop: [{ a: 1 }] }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe( + "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')" + ); }); test("can validate with a custom validator", () => { - // class TestComponent extends Component { - // static props = { - // size: { - // validate: (e) => ["small", "medium", "large"].includes(e), - // }, - // }; - // } - // let error; - // try { - // QWeb.utils.validateProps(TestComponent, { size: "small" }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // try { - // QWeb.utils.validateProps(TestComponent, { size: "abcdef" }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'"); + class TestComponent extends Component { + static props = { + size: { + validate: (e: string) => ["small", "medium", "large"].includes(e), + }, + }; + } + let error; + try { + validateProps(TestComponent as any, { size: "small" }); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + try { + validateProps(TestComponent as any, { size: "abcdef" }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'"); }); test("can validate with a custom validator, and a type", () => { - // const validator = jest.fn((n) => 0 <= n && n <= 10); - // class TestComponent extends Component { - // static props = { - // n: { - // type: Number, - // validate: validator, - // }, - // }; - // } - // let error; - // try { - // QWeb.utils.validateProps(TestComponent, { n: 3 }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeUndefined(); - // expect(validator).toBeCalledTimes(1); - // try { - // QWeb.utils.validateProps(TestComponent, { n: "str" }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); - // expect(validator).toBeCalledTimes(1); - // error = null; - // try { - // QWeb.utils.validateProps(TestComponent, { n: 100 }); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); - // expect(validator).toBeCalledTimes(2); + const validator = jest.fn((n) => 0 <= n && n <= 10); + class TestComponent extends Component { + static props = { + n: { + type: Number, + validate: validator, + }, + }; + } + let error; + try { + validateProps(TestComponent as any, { n: 3 }); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(validator).toBeCalledTimes(1); + try { + validateProps(TestComponent as any, { n: "str" }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(validator).toBeCalledTimes(1); + error = null; + try { + validateProps(TestComponent as any, { n: 100 }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(validator).toBeCalledTimes(2); }); test("props are validated in dev mode (code snapshot)", async () => { - // env.qweb.addTemplates(` - // - //
- // - //
- //
- //
`); - // class Child extends Widget { - // static props = ["message"]; - // } - // class App extends Widget { - // static components = { Child }; - // } - // const app = new App(); - // await app.mount(fixture); - // expect(fixture.innerHTML).toBe("
1
"); - // // need to make sure there are 2 call to update props. one at component - // // creation, and one at update time. - // expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot(); + class Child extends Component { + static props = ["message"]; + static template = xml`
`; + } + class Parent extends Component { + static components = { Child }; + static template = xml`
`; + } + await mountApp(Parent); + expect(fixture.innerHTML).toBe("
1
"); }); test("props: list of strings with optional props", async () => { - // class TestWidget extends Widget { - // static props = ["message", "someProp?"]; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { someProp: 1 }); - // }).toThrow(); - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: 1 }); - // }).not.toThrow(); + class SubComp extends Component { + static props = ["message", "someProp?"]; + } + expect(() => { + validateProps(SubComp as any, { someProp: 1 }); + }).toThrow(); + expect(() => { + validateProps(SubComp as any, { message: 1 }); + }).not.toThrow(); }); test("props: can be defined with a boolean", async () => { - // class TestWidget extends Widget { - // static props = { message: true }; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, {}); - // }).toThrow(); + class SubComp extends Component { + static props = { message: true }; + } + expect(() => { + validateProps(SubComp as any, {}); + }).toThrow(); }); test("props with type array, and no element", async () => { - // class TestWidget extends Widget { - // static props = { myprop: { type: Array } }; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { myprop: [1] }); - // }).not.toThrow(); - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { myprop: 1 }); - // }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`); + class SubComp extends Component { + static props = { myprop: { type: Array } }; + } + expect(() => { + validateProps(SubComp as any, { myprop: [1] }); + }).not.toThrow(); + expect(() => { + validateProps(SubComp as any, { myprop: 1 }); + }).toThrow(`Invalid Prop 'myprop' in component 'SubComp'`); }); test("props with type object, and no shape", async () => { - // class TestWidget extends Widget { - // static props = { myprop: { type: Object } }; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } }); - // }).not.toThrow(); - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { myprop: false }); - // }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`); + class SubComp extends Component { + static props = { myprop: { type: Object } }; + } + expect(() => { + validateProps(SubComp as any, { myprop: { a: 3 } }); + }).not.toThrow(); + expect(() => { + validateProps(SubComp as any, { myprop: false }); + }).toThrow(`Invalid Prop 'myprop' in component 'SubComp'`); }); test("props: extra props cause an error", async () => { - // class TestWidget extends Widget { - // static props = ["message"]; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: 1, flag: true }); - // }).toThrow(); + class SubComp extends Component { + static props = ["message"]; + } + expect(() => { + validateProps(SubComp as any, { message: 1, flag: true }); + }).toThrow(); }); test("props: extra props cause an error, part 2", async () => { - // class TestWidget extends Widget { - // static props = { message: true }; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: 1, flag: true }); - // }).toThrow(); + class SubComp extends Component { + static props = { message: true }; + } + expect(() => { + validateProps(SubComp as any, { message: 1, flag: true }); + }).toThrow(); }); test("props: optional prop do not cause an error", async () => { - // class TestWidget extends Widget { - // static props = ["message?"]; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: 1 }); - // }).not.toThrow(); + class SubComp extends Component { + static props = ["message?"]; + } + expect(() => { + validateProps(SubComp as any, { message: 1 }); + }).not.toThrow(); }); test("optional prop do not cause an error if value is undefined", async () => { - // class TestWidget extends Widget { - // static props = { message: { type: String, optional: true } }; - // } - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: undefined }); - // }).not.toThrow(); - // expect(() => { - // QWeb.utils.validateProps(TestWidget, { message: null }); - // }).toThrow(); + class SubComp extends Component { + static props = { message: { type: String, optional: true } }; + } + expect(() => { + validateProps(SubComp as any, { message: undefined }); + }).not.toThrow(); + expect(() => { + validateProps(SubComp as any, { message: null }); + }).toThrow(); }); test("missing required boolean prop causes an error", async () => { - // class TestWidget extends Widget { - // static props = ["p"]; - // static template = xml`hey`; - // } - // class App extends Widget { - // static template = xml`
`; - // static components = { TestWidget }; - // } - // const w = new App(undefined, {}); - // let error; - // try { - // await w.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Missing props 'p' (component 'TestWidget')"); + class SubComp extends Component { + static props = ["p"]; + static template = xml`hey`; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + } + let error; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Missing props 'p' (component 'SubComp')"); }); test("props are validated whenever component is updated", async () => { - // let error; - // class TestWidget extends Component { - // static props = { p: { type: Number } }; - // static template = xml`
`; - // async __updateProps() { - // try { - // await Component.prototype.__updateProps.apply(this, arguments); - // } catch (e) { - // error = e; - // } - // } - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // state: any = useState({ p: 1 }); - // } - // const w = new Parent(); - // await w.mount(fixture); - // expect(fixture.innerHTML).toBe("
1
"); - // w.state.p = undefined; - // await nextTick(); - // expect(error).toBeDefined(); - // expect(error.message).toBe("Missing props 'p' (component 'TestWidget')"); + let error; + class SubComp extends Component { + static props = { p: { type: Number } }; + static template = xml`
`; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + state: any = { p: 1 }; + } + const app = await mountApp(Parent); + expect(fixture.innerHTML).toBe("
1
"); + try { + (app as any).root.component.state.p = undefined; + await (app as any).root.component.render(); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Missing props 'p' (component 'SubComp')"); }); - test("default values are applied before validating props at update", async () => { - // class TestWidget extends Component { - // static props = { p: { type: Number } }; - // static template = xml`
`; - // static defaultProps = { p: 4 }; - // } - // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; - // state: any = useState({ p: 1 }); - // } - // const w = new Parent(); - // await w.mount(fixture); - // expect(fixture.innerHTML).toBe("
1
"); - // w.state.p = undefined; - // await nextTick(); - // expect(fixture.innerHTML).toBe("
4
"); + test.skip("default values are applied before validating props at update", async () => { + // need to do something about errors catched in render + class SubComp extends Component { + static props = { p: { type: Number } }; + static template = xml`
`; + static defaultProps = { p: 4 }; + } + class Parent extends Component { + static template = xml`
`; + static components = { SubComp }; + state: any = useState({ p: 1 }); + } + + const app = await mountApp(Parent); + expect(fixture.innerHTML).toBe("
1
"); + (app as any).root.component.state.p = undefined; + await nextTick(); + expect(fixture.innerHTML).toBe("
4
"); }); test("mix of optional and mandatory", async () => { - // class Child extends Component { - // static props = { - // optional: { type: String, optional: true }, - // mandatory: Number, - // }; - // static template = xml`
`; - // } - // class App extends Component { - // static components = { Child }; - // static template = xml`
`; - // } - // const w = new App(undefined, {}); - // let error; - // try { - // await w.mount(fixture); - // } catch (e) { - // error = e; - // } - // expect(error).toBeDefined(); - // expect(error.message).toBe("Missing props 'mandatory' (component 'Child')"); + class Child extends Component { + static props = { + optional: { type: String, optional: true }, + mandatory: Number, + }; + static template = xml`
`; + } + class Parent extends Component { + static components = { Child }; + static template = xml`
`; + } + let error; + try { + await mountApp(Parent); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Missing props 'mandatory' (component 'Child')"); }); }); +//------------------------------------------------------------------------------ +// Default props +//------------------------------------------------------------------------------ + describe.skip("default props", () => { test("can set default values", async () => { - // class TestWidget extends Component { + // class SubComp extends Component { // static defaultProps = { p: 4 }; // static template = xml`
`; // } // class Parent extends Component { - // static template = xml`
`; - // static components = { TestWidget }; + // static template = xml`
`; + // static components = { SubComp }; // } // const w = new Parent(); // await w.mount(fixture); @@ -774,13 +743,13 @@ describe.skip("default props", () => { }); test("default values are also set whenever component is updated", async () => { - // class TestWidget extends Widget { + // class SubComp extends Component { // static template = xml`
`; // static defaultProps = { p: 4 }; // } - // class Parent extends Widget { - // static template = xml`
`; - // static components = { TestWidget }; + // class Parent extends Component { + // static template = xml`
`; + // static components = { SubComp }; // state: any = useState({ p: 1 }); // } // const w = new Parent(); @@ -792,14 +761,14 @@ describe.skip("default props", () => { }); test("can set default required boolean values", async () => { - // class TestWidget extends Widget { + // class SubComp extends Component { // static props = ["p", "q"]; // static defaultProps = { p: true, q: false }; // static template = xml`heyhey`; // } - // class App extends Widget { - // static template = xml`
`; - // static components = { TestWidget }; + // class App extends Component { + // static template = xml`
`; + // static components = { SubComp }; // } // const w = new App(undefined, {}); // await w.mount(fixture); diff --git a/tests/helpers.ts b/tests/helpers.ts index 2f84d669..372bbb36 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -46,6 +46,7 @@ export function snapshotApp(app: App) { snapshotTemplateCode(template, { translateFn: app.translateFn, translatableAttributes: app.translatableAttributes, + dev: app.dev, }); } diff --git a/tests/misc/async_root.test.ts b/tests/misc/async_root.test.ts index 5e43a1af..5dd29e5a 100644 --- a/tests/misc/async_root.test.ts +++ b/tests/misc/async_root.test.ts @@ -33,7 +33,7 @@ describe("Asyncroot", () => { `; - static components = { Child, AsyncChild, /*AsyncRoot*/ }; + static components = { Child, AsyncChild /*AsyncRoot*/ }; state = useState({ val: 0 }); updateApp() { @@ -79,7 +79,7 @@ describe("Asyncroot", () => { `; - static components = { Child, AsyncChild, /*AsyncRoot*/ }; + static components = { Child, AsyncChild /*AsyncRoot*/ }; state = useState({ val: 0 }); updateApp() { @@ -132,7 +132,7 @@ describe("Asyncroot", () => { `; - static components = { Child, AsyncChild, /*AsyncRoot*/ }; + static components = { Child, AsyncChild /*AsyncRoot*/ }; state = useState({ val: 0 }); updateApp() { diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts index bb346b46..85a79f2c 100644 --- a/tests/misc/portal.test.ts +++ b/tests/misc/portal.test.ts @@ -405,7 +405,8 @@ describe("Portal", () => { error = e; } expect(error).toBeDefined(); - const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g + const regexp = + /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g; expect(error.message).toMatch(regexp); }); diff --git a/tests/qweb/error_handling.test.ts b/tests/qweb/error_handling.test.ts index 461145bb..fdf98816 100644 --- a/tests/qweb/error_handling.test.ts +++ b/tests/qweb/error_handling.test.ts @@ -15,7 +15,9 @@ describe("error handling", () => { test("cannot add twice the same template", () => { const context = new TestContext(); context.addTemplate("test", ``); - expect(() => context.addTemplate("test", "
", { allowDuplicate: true })).not.toThrow("already defined"); + expect(() => context.addTemplate("test", "
", { allowDuplicate: true })).not.toThrow( + "already defined" + ); expect(() => context.addTemplate("test", "
")).toThrow("already defined"); }); @@ -37,4 +39,4 @@ describe("error handling", () => { "Unknown QWeb directive: 't-best-beer'" ); }); -}); \ No newline at end of file +}); diff --git a/tests/qweb/t_tag.test.ts b/tests/qweb/t_tag.test.ts index 258a8f73..0ff5f40b 100644 --- a/tests/qweb/t_tag.test.ts +++ b/tests/qweb/t_tag.test.ts @@ -1,7 +1,4 @@ -import { - renderToString, - snapshotEverything, -} from "../helpers"; +import { renderToString, snapshotEverything } from "../helpers"; snapshotEverything(); @@ -28,4 +25,4 @@ describe("qweb t-tag", () => { const expected = `
gooseberry
`; expect(renderToString(template, { tag: "div" })).toBe(expected); }); -}); \ No newline at end of file +});