[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.
This commit is contained in:
Paul Morelle
2022-05-12 17:13:54 +02:00
committed by Géry Debongnie
parent a83731007a
commit 32d8b23b9d
2 changed files with 83 additions and 37 deletions
+50 -24
View File
@@ -72,17 +72,20 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
continue; continue;
} }
} }
let isValid; let whyInvalid;
try { try {
isValid = isValidProp((props as any)[propName], propDef); whyInvalid = whyInvalidProp((props as any)[propName], propDef);
} catch (e) { } catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${ (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message (e as Error).message
})`; })`;
throw e; throw e;
} }
if (!isValid) { if (whyInvalid !== null) {
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`); whyInvalid = whyInvalid.replace(/\${propName}/g, propName);
throw new Error(
`Invalid Prop '${propName}' in component '${ComponentClass.name}': ${whyInvalid}`
);
} }
} }
if (!allowAdditionalProps) { if (!allowAdditionalProps) {
@@ -95,11 +98,11 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, 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) { if (propDef === true) {
return true; return null;
} }
if (typeof propDef === "function") { if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a // 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 // So, even though 1 is not an instance of Number, we want to consider that
// it is valid. // it is valid.
if (typeof prop === "object") { 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) { } else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop // If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor. // matches at least one of its descriptor.
let result = false; let reasons: string[] = [];
for (let i = 0, iLen = propDef.length; i < iLen; i++) { 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 // propsDef is an object
if (propDef.optional && prop === undefined) { if (propDef.optional && prop === undefined) {
return true; return null;
} }
let result = propDef.type ? isValidProp(prop, propDef.type) : true; if (propDef.type) {
if (propDef.validate) { const why = whyInvalidProp(prop, propDef.type);
result = result && propDef.validate(prop); 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) { if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) { 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) { if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape; const shape = propDef.shape;
for (let key in 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) {
for (let propName in prop) { if (!(propName in shape)) {
if (!(propName in shape)) { return `unknown prop \${propName}['${propName}']`;
throw new Error(`unknown prop '${propName}'`);
}
} }
} }
} }
return result; return null;
} }
+33 -13
View File
@@ -134,7 +134,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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; error = e as Error;
} }
expect(error!).toBeDefined(); 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; error = e as Error;
} }
expect(error!).toBeDefined(); 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 () => { test("can validate an optional props", async () => {
@@ -261,7 +267,7 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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 () => { test("can validate an array with given primitive type", async () => {
@@ -350,7 +356,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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 () => { test("can validate an object with simple shape", async () => {
@@ -383,7 +391,7 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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 { try {
props = { p: { id: "1", url: "url" } }; props = { p: { id: "1", url: "url" } };
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
@@ -391,7 +399,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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; error = undefined;
try { try {
props = { p: { id: 1 } }; props = { p: { id: 1 } };
@@ -400,7 +410,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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 () => { test("can validate recursively complicated prop def", async () => {
@@ -446,7 +458,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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", () => { test("can validate optional attributes in nested sub props", () => {
@@ -477,7 +491,7 @@ describe("props validation", () => {
} }
expect(error!).toBeDefined(); expect(error!).toBeDefined();
expect(error!.message).toBe( 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; error = e as Error;
} }
expect(error!).toBeDefined(); 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", () => { test("can validate with a custom validator, and a type", () => {
@@ -529,7 +545,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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); expect(validator).toBeCalledTimes(1);
error = undefined; error = undefined;
try { try {
@@ -538,7 +556,9 @@ describe("props validation", () => {
error = e as Error; error = e as Error;
} }
expect(error!).toBeDefined(); 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); expect(validator).toBeCalledTimes(2);
}); });