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"; //------------------------------------------------------------------------------ // Setup and helpers //------------------------------------------------------------------------------ let fixture: HTMLElement; let env: Env; let dev: boolean = false; beforeEach(() => { fixture = makeTestFixture(); env = makeTestEnv(); Component.env = env; dev = QWeb.dev; QWeb.dev = true; }); afterEach(() => { fixture.remove(); QWeb.dev = dev; }); class Widget extends Component {} //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ 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(); }); 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')`); }); 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(`Props 'p' of invalid type 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(`Props 'p' of invalid type 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(`Props 'p' of invalid type in component 'TestWidget'`); }); 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(`Props 'p' of invalid type in component 'TestWidget'`); }); 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; } }); 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(`Props 'p' of invalid type in component 'TestWidget'`); }); 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).toBeUndefined(); 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(`Props 'p' of invalid type 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(`Props 'p' of invalid type in component 'TestWidget'`); }); 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(`Props 'p' of invalid type in component 'TestWidget'`); }); 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(); }); 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(); }); test("props: can be defined with a boolean", async () => { class TestWidget extends Widget { static props = { message: true }; } expect(() => { QWeb.utils.validateProps(TestWidget, {}); }).toThrow(); }); 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(); }); 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(); }); 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(); }); 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(); }); 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')"); }); 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')"); }); 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
"); }); }); describe("default props", () => { test("can set default values", async () => { class TestWidget extends Component { static defaultProps = { p: 4 }; static template = xml`
`; } class Parent extends Component { static template = xml`
`; static components = { TestWidget }; } const w = new Parent(); await w.mount(fixture); expect(fixture.innerHTML).toBe("
4
"); }); test("default values are also set whenever component is updated", async () => { class TestWidget extends Widget { static template = xml`
`; static defaultProps = { p: 4 }; } class Parent extends Widget { 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("can set default required boolean values", async () => { class TestWidget extends Widget { static props = ["p", "q"]; static defaultProps = { p: true, q: false }; static template = xml`heyhey`; } class App extends Widget { static template = xml`
`; static components = { TestWidget }; } const w = new App(undefined, {}); await w.mount(fixture); expect(fixture.innerHTML).toBe("
heyhey
"); }); });