From 32d8b23b9dcc0f1c8e3f4ea809d04f8ee08d6149 Mon Sep 17 00:00:00 2001 From: Paul Morelle Date: Thu, 12 May 2022 17:13:54 +0200 Subject: [PATCH] [IMP] props_validation: have clearer error messages With this commit, props validation error messages will include a more developer-friendly error message, avoiding the need to investigate in the Developer Tools why a complex props structure is invalid. --- src/component/props_validation.ts | 74 +++++++++++++++-------- tests/components/props_validation.test.ts | 46 ++++++++++---- 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts index 20e674b2..bc293cc9 100644 --- a/src/component/props_validation.ts +++ b/src/component/props_validation.ts @@ -72,17 +72,20 @@ export function validateProps

(name: string | ComponentConstructor

, props: continue; } } - let isValid; + let whyInvalid; try { - isValid = isValidProp((props as any)[propName], propDef); + whyInvalid = whyInvalidProp((props as any)[propName], propDef); } catch (e) { (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${ (e as Error).message })`; throw e; } - if (!isValid) { - throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`); + if (whyInvalid !== null) { + whyInvalid = whyInvalid.replace(/\${propName}/g, propName); + throw new Error( + `Invalid Prop '${propName}' in component '${ComponentClass.name}': ${whyInvalid}` + ); } } if (!allowAdditionalProps) { @@ -95,11 +98,11 @@ export function validateProps

(name: string | ComponentConstructor

, props: } /** - * Check if an invidual prop value matches its (static) prop definition + * Check why an invidual prop value doesn't match its (static) prop definition */ -function isValidProp(prop: any, propDef: any): boolean { +function whyInvalidProp(prop: any, propDef: any): string | null { if (propDef === true) { - return true; + return null; } if (typeof propDef === "function") { // Check if a value is constructed by some Constructor. Note that there is a @@ -108,43 +111,66 @@ function isValidProp(prop: any, propDef: any): boolean { // 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; + if (prop instanceof propDef) { + return null; + } + return `\${propName} is not an instance of ${propDef.name}`; } - return typeof prop === propDef.name.toLowerCase(); + if (typeof prop === propDef.name.toLowerCase()) { + return null; + } + return `type of \${propName} is not ${propDef.name}`; } 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; + let reasons: string[] = []; for (let i = 0, iLen = propDef.length; i < iLen; i++) { - result = result || isValidProp(prop, propDef[i]); + const why = whyInvalidProp(prop, propDef[i]); + if (why === null) { + return null; + } + reasons.push(why); + } + if (reasons.length > 1) { + return reasons.slice(0, -1).join(", ") + " and " + reasons[reasons.length - 1]; + } else { + return reasons[0]; } - return result; } // propsDef is an object if (propDef.optional && prop === undefined) { - return true; + return null; } - let result = propDef.type ? isValidProp(prop, propDef.type) : true; - if (propDef.validate) { - result = result && propDef.validate(prop); + if (propDef.type) { + const why = whyInvalidProp(prop, propDef.type); + if (why !== null) { + return why; + } + } + if (propDef.validate && !propDef.validate(prop)) { + return "${propName} could not be validated by `validate` function"; } if (propDef.type === Array && propDef.element) { for (let i = 0, iLen = prop.length; i < iLen; i++) { - result = result && isValidProp(prop[i], propDef.element); + const why = whyInvalidProp(prop[i], propDef.element); + if (why !== null) { + return why.replace(/\${propName}/g, `\${propName}[${i}]`); + } } } if (propDef.type === Object && propDef.shape) { const shape = propDef.shape; for (let key in shape) { - result = result && isValidProp(prop[key], shape[key]); + const why = whyInvalidProp(prop[key], shape[key]); + if (why !== null) { + return why.replace(/\${propName}/g, `\${propName}['${key}']`); + } } - if (result) { - for (let propName in prop) { - if (!(propName in shape)) { - throw new Error(`unknown prop '${propName}'`); - } + for (let propName in prop) { + if (!(propName in shape)) { + return `unknown prop \${propName}['${propName}']`; } } } - return result; + return null; } diff --git a/tests/components/props_validation.test.ts b/tests/components/props_validation.test.ts index c0369943..adb3de8e 100644 --- a/tests/components/props_validation.test.ts +++ b/tests/components/props_validation.test.ts @@ -134,7 +134,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component '_a'"); + expect(error!.message).toBe( + `Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}` + ); } }); @@ -184,7 +186,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component '_a'"); + expect(error!.message).toBe( + `Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}` + ); } }); @@ -223,7 +227,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe( + "Invalid Prop 'p' in component 'SubComp': type of p is not String and type of p is not Boolean" + ); }); test("can validate an optional props", async () => { @@ -261,7 +267,7 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp': type of p is not String"); }); test("can validate an array with given primitive type", async () => { @@ -350,7 +356,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe( + "Invalid Prop 'p' in component 'SubComp': type of p[1] is not String and type of p[1] is not Boolean" + ); }); test("can validate an object with simple shape", async () => { @@ -383,7 +391,7 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid prop 'p' in component SubComp (unknown prop 'extra')"); + expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp': unknown prop p['extra']"); try { props = { p: { id: "1", url: "url" } }; await mount(Parent, fixture, { dev: true }); @@ -391,7 +399,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe( + "Invalid Prop 'p' in component 'SubComp': type of p['id'] is not Number" + ); error = undefined; try { props = { p: { id: 1 } }; @@ -400,7 +410,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe( + "Invalid Prop 'p' in component 'SubComp': type of p['url'] is not String" + ); }); test("can validate recursively complicated prop def", async () => { @@ -446,7 +458,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'p' in component 'SubComp'"); + expect(error!.message).toBe( + "Invalid Prop 'p' in component 'SubComp': p['url'] is not an instance of Boolean and type of p['url'][1] is not Number" + ); }); test("can validate optional attributes in nested sub props", () => { @@ -477,7 +491,7 @@ describe("props validation", () => { } expect(error!).toBeDefined(); expect(error!.message).toBe( - "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')" + "Invalid Prop 'myprop' in component 'TestComponent': unknown prop myprop[0]['a']" ); }); @@ -502,7 +516,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'size' in component 'TestComponent'"); + expect(error!.message).toBe( + "Invalid Prop 'size' in component 'TestComponent': size could not be validated by `validate` function" + ); }); test("can validate with a custom validator, and a type", () => { @@ -529,7 +545,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(error!.message).toBe( + "Invalid Prop 'n' in component 'TestComponent': type of n is not Number" + ); expect(validator).toBeCalledTimes(1); error = undefined; try { @@ -538,7 +556,9 @@ describe("props validation", () => { error = e as Error; } expect(error!).toBeDefined(); - expect(error!.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(error!.message).toBe( + "Invalid Prop 'n' in component 'TestComponent': n could not be validated by `validate` function" + ); expect(validator).toBeCalledTimes(2); });