From a1ee9ff7fc133900369b840b00d1b4fa5367d6d2 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sat, 14 Sep 2024 20:13:08 -0400 Subject: [PATCH 1/3] Update param validator and test file --- src/app.js | 5 + src/core/friendly_errors/index.js | 5 + src/core/friendly_errors/param_validator.js | 647 ++++++++++---------- test/unit/core/param_errors.js | 115 +++- test/unit/spec.js | 5 +- 5 files changed, 437 insertions(+), 340 deletions(-) create mode 100644 src/core/friendly_errors/index.js diff --git a/src/app.js b/src/app.js index 0164a4d820..ca17271be7 100644 --- a/src/app.js +++ b/src/app.js @@ -30,6 +30,11 @@ accessibility(p5); import color from './color'; color(p5); +// core +// currently, it only contains the test for parameter validation +import friendlyErrors from './core/friendly_errors'; +friendlyErrors(p5); + // data import data from './data'; data(p5); diff --git a/src/core/friendly_errors/index.js b/src/core/friendly_errors/index.js new file mode 100644 index 0000000000..4cf7db60ba --- /dev/null +++ b/src/core/friendly_errors/index.js @@ -0,0 +1,5 @@ +import validateParams from './param_validator.js'; + +export default function (p5) { + p5.registerAddon(validateParams); +} \ No newline at end of file diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index ddaad648cd..6392de70c3 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -8,351 +8,366 @@ import { z } from 'zod'; import { fromError } from 'zod-validation-error'; import dataDoc from '../../../docs/parameterData.json'; -// Cache for Zod schemas -let schemaRegistry = new Map(); -const arrDoc = JSON.parse(JSON.stringify(dataDoc)); - -// Mapping names of p5 types to their constructor functions. -// p5Constructors: -// - Color: f() -// - Graphics: f() -// - Vector: f() -// and so on. -const p5Constructors = {}; -// For speedup over many runs. `funcSpecificConstructors[func]` only has the -// constructors for types which were seen earlier as args of `func`. -// const funcSpecificConstructors = {}; - -for (let [key, value] of Object.entries(p5)) { - p5Constructors[key] = value; -} - -// window.addEventListener('load', () => { -// // Make a list of all p5 classes to be used for argument validation -// // This must be done only when everything has loaded otherwise we get -// // an empty array. -// for (let key of Object.keys(p5)) { -// // Get a list of all constructors in p5. They are functions whose names -// // start with a capital letter. -// if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) { -// p5Constructors[key] = p5[key]; -// } -// } -// }); - -// `constantsMap` maps constants to their values, e.g. -// { -// ADD: 'lighter', -// ALT: 18, -// ARROW: 'default', -// AUTO: 'auto', -// ... -// } -const constantsMap = {}; -for (const [key, value] of Object.entries(constants)) { - constantsMap[key] = value; -} - -const schemaMap = { - 'Any': z.any(), - 'Array': z.array(z.any()), - 'Boolean': z.boolean(), - 'Function': z.function(), - 'Integer': z.number().int(), - 'Number': z.number(), - 'Number[]': z.array(z.number()), - 'Object': z.object({}), - // Allows string for any regex - 'RegExp': z.string(), - 'String': z.string(), - 'String[]': z.array(z.string()) -}; - -// const webAPIObjects = [ -// 'AudioNode', -// 'HTMLCanvasElement', -// 'HTMLElement', -// 'KeyboardEvent', -// 'MouseEvent', -// 'TouchEvent', -// 'UIEvent', -// 'WheelEvent' -// ]; - -// function generateWebAPISchemas(apiObjects) { -// return apiObjects.map(obj => { -// return { -// name: obj, -// schema: z.custom((data) => data instanceof globalThis[obj], { -// message: `Expected a ${obj}` -// }) -// }; -// }); -// } - -// const webAPISchemas = generateWebAPISchemas(webAPIObjects); - -/** - * This is a helper function that generates Zod schemas for a function based on - * the parameter data from `docs/parameterData.json`. - * - * Example parameter data for function `background`: - * "background": { - "overloads": [ - ["p5.Color"], - ["String", "Number?"], - ["Number", "Number?"], - ["Number", "Number", "Number", "Number?"], - ["Number[]"], - ["p5.Image", "Number?"] - ] +function validateParams(p5, fn) { + // Cache for Zod schemas + let schemaRegistry = new Map(); + const arrDoc = JSON.parse(JSON.stringify(dataDoc)); + + // Mapping names of p5 types to their constructor functions. + // p5Constructors: + // - Color: f() + // - Graphics: f() + // - Vector: f() + // and so on. + const p5Constructors = {}; + + fn._loadP5Constructors = function () { + // Make a list of all p5 classes to be used for argument validation + // This must be done only when everything has loaded otherwise we get + // an empty array + for (let key of Object.keys(p5)) { + // Get a list of all constructors in p5. They are functions whose names + // start with a capital letter + if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) { + p5Constructors[key] = p5[key]; + } } - * Where each array in `overloads` represents a set of valid overloaded - * parameters, and `?` is a shorthand for `Optional`. - * - * TODO: - * - [ ] Support for p5 constructors - * - [ ] Support for more obscure types, such as `lerpPalette` and optional - * objects in `p5.Geometry.computeNormals()` - * (see https://github.com/processing/p5.js/pull/7186#discussion_r1724983249) - * - * @param {String} func - Name of the function. - * @returns {z.ZodSchema} Zod schema - */ -function generateZodSchemasForFunc(func) { - // Expect global functions like `sin` and class methods like `p5.Vector.add` - const ichDot = func.lastIndexOf('.'); - const funcName = func.slice(ichDot + 1); - const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5'; - - let funcInfo = arrDoc[funcClass][funcName]; + } - let overloads = []; - if (funcInfo.hasOwnProperty('overloads')) { - overloads = funcInfo.overloads; + // `constantsMap` maps constants to their values, e.g. + // { + // ADD: 'lighter', + // ALT: 18, + // ARROW: 'default', + // AUTO: 'auto', + // ... + // } + const constantsMap = {}; + for (const [key, value] of Object.entries(constants)) { + constantsMap[key] = value; } - // Returns a schema for a single type, i.e. z.boolean() for `boolean`. - const generateTypeSchema = type => { - // Type only contains uppercase letters and underscores -> type is a - // constant. Note that because we're ultimately interested in the value of - // the constant, mapping constants to their values via `constantsMap` is - // necessary. - if (/^[A-Z_]+$/.test(type)) { - return z.literal(constantsMap[type]); - } else if (schemaMap[type]) { - return schemaMap[type]; - } else { - // TODO: Make this throw an error once more types are supported. - console.log(`Warning: Zod schema not found for type '${type}'. Skip mapping`); - return undefined; - } + // Start initializing `schemaMap` with primitive types. `schemaMap` will + // eventually contain both primitive types and web API objects. + const schemaMap = { + 'Any': z.any(), + 'Array': z.array(z.any()), + 'Boolean': z.boolean(), + 'Function': z.function(), + 'Integer': z.number().int(), + 'Number': z.number(), + 'Number[]': z.array(z.number()), + 'Object': z.object({}), + // Allows string for any regex + 'RegExp': z.string(), + 'String': z.string(), + 'String[]': z.array(z.string()) }; - // Generate a schema for a single parameter. In the case where a parameter can - // be of multiple types, `generateTypeSchema` is called for each type. - const generateParamSchema = param => { - const optional = param.endsWith('?'); - param = param.replace(/\?$/, ''); - - let schema; - - // Generate a schema for a single parameter that can be of multiple - // types / constants, i.e. `String|Number|Array`. - // - // Here, z.union() is used over z.enum() (which seems more intuitive) for - // constants for the following reasons: - // 1) z.enum() only allows a fixed set of allowable string values. However, - // our constants sometimes have numeric or non-primitive values. - // 2) In some cases, the type can be constants or strings, making z.enum() - // insufficient for the use case. - if (param.includes('|')) { - const types = param.split('|'); - schema = z.union(types - .map(t => generateTypeSchema(t)) - .filter(s => s !== undefined)); - } else { - schema = generateTypeSchema(param); - } - - return optional ? schema.optional() : schema; - }; + const webAPIObjects = [ + 'AudioNode', + 'HTMLCanvasElement', + 'HTMLElement', + 'KeyboardEvent', + 'MouseEvent', + 'TouchEvent', + 'UIEvent', + 'WheelEvent' + ]; + + function generateWebAPISchemas(apiObjects) { + return apiObjects.reduce((acc, obj) => { + acc[obj] = z.custom(data => data instanceof globalThis[obj], { + message: `Expected a ${obj}` + }); + return acc; + }, {}); + } - // Note that in Zod, `optional()` only checks for undefined, not the absence - // of value. - // - // Let's say we have a function with 3 parameters, and the last one is - // optional, i.e. func(a, b, c?). If we only have a z.tuple() for the - // parameters, where the third schema is optional, then we will only be able - // to validate func(10, 10, undefined), but not func(10, 10), which is - // a completely valid call. - // - // Therefore, on top of using `optional()`, we also have to generate parameter - // combinations that are valid for all numbers of parameters. - const generateOverloadCombinations = params => { - // No optional parameters, return the original parameter list right away. - if (!params.some(p => p.endsWith('?'))) { - return [params]; + const webAPISchemas = generateWebAPISchemas(webAPIObjects); + // Add web API schemas to the schema map. + Object.assign(schemaMap, webAPISchemas); + + /** + * This is a helper function that generates Zod schemas for a function based on + * the parameter data from `docs/parameterData.json`. + * + * Example parameter data for function `background`: + * "background": { + "overloads": [ + ["p5.Color"], + ["String", "Number?"], + ["Number", "Number?"], + ["Number", "Number", "Number", "Number?"], + ["Number[]"], + ["p5.Image", "Number?"] + ] + } + * Where each array in `overloads` represents a set of valid overloaded + * parameters, and `?` is a shorthand for `Optional`. + * + * TODO: + * - [ ] Support for p5 constructors + * - [ ] Support for more obscure types, such as `lerpPalette` and optional + * objects in `p5.Geometry.computeNormals()` + * (see https://github.com/processing/p5.js/pull/7186#discussion_r1724983249) + * + * @param {String} func - Name of the function. + * @returns {z.ZodSchema} Zod schema + */ + function generateZodSchemasForFunc(func) { + // Expect global functions like `sin` and class methods like `p5.Vector.add` + const ichDot = func.lastIndexOf('.'); + const funcName = func.slice(ichDot + 1); + const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5'; + + let funcInfo = arrDoc[funcClass][funcName]; + + let overloads = []; + if (funcInfo.hasOwnProperty('overloads')) { + overloads = funcInfo.overloads; } - const requiredParamsCount = params.filter(p => !p.endsWith('?')).length; - const result = []; - - for (let i = requiredParamsCount; i <= params.length; i++) { - result.push(params.slice(0, i)); - } + // Returns a schema for a single type, i.e. z.boolean() for `boolean`. + const generateTypeSchema = type => { + // Type only contains uppercase letters and underscores -> type is a + // constant. Note that because we're ultimately interested in the value of + // the constant, mapping constants to their values via `constantsMap` is + // necessary. + if (/^[A-Z_]+$/.test(type)) { + return z.literal(constantsMap[type]); + } + // All p5 objects start with `p5` in the documentation, i.e. `p5.Camera`. + else if (type.startsWith('p5')) { + console.log('type', type); + const className = type.substring(type.indexOf('.') + 1); + console.log('className', p5Constructors[className]); + return z.instanceof(p5Constructors[className]); + } + // For primitive types and web API objects. + else if (schemaMap[type]) { + return schemaMap[type]; + } else { + // TODO: Make this throw an error once more types are supported. + console.log(`Warning: Zod schema not found for type '${type}'. Skip mapping`); + return undefined; + } + }; - return result; - }; + // Generate a schema for a single parameter. In the case where a parameter can + // be of multiple types, `generateTypeSchema` is called for each type. + const generateParamSchema = param => { + const optional = param.endsWith('?'); + param = param.replace(/\?$/, ''); + + let schema; + + // Generate a schema for a single parameter that can be of multiple + // types / constants, i.e. `String|Number|Array`. + // + // Here, z.union() is used over z.enum() (which seems more intuitive) for + // constants for the following reasons: + // 1) z.enum() only allows a fixed set of allowable string values. However, + // our constants sometimes have numeric or non-primitive values. + // 2) In some cases, the type can be constants or strings, making z.enum() + // insufficient for the use case. + if (param.includes('|')) { + const types = param.split('|'); + schema = z.union(types + .map(t => generateTypeSchema(t)) + .filter(s => s !== undefined)); + } else { + schema = generateTypeSchema(param); + } + + return optional ? schema.optional() : schema; + }; - // Generate schemas for each function overload and merge them - const overloadSchemas = overloads.flatMap(overload => { - const combinations = generateOverloadCombinations(overload); - - return combinations.map(combo => - z.tuple( - combo - .map(p => generateParamSchema(p)) - // For now, ignore schemas that cannot be mapped to a defined type - .filter(schema => schema !== undefined) - ) - ); - }); - - return overloadSchemas.length === 1 - ? overloadSchemas[0] - : z.union(overloadSchemas); -} + // Note that in Zod, `optional()` only checks for undefined, not the absence + // of value. + // + // Let's say we have a function with 3 parameters, and the last one is + // optional, i.e. func(a, b, c?). If we only have a z.tuple() for the + // parameters, where the third schema is optional, then we will only be able + // to validate func(10, 10, undefined), but not func(10, 10), which is + // a completely valid call. + // + // Therefore, on top of using `optional()`, we also have to generate parameter + // combinations that are valid for all numbers of parameters. + const generateOverloadCombinations = params => { + // No optional parameters, return the original parameter list right away. + if (!params.some(p => p.endsWith('?'))) { + return [params]; + } + + const requiredParamsCount = params.filter(p => !p.endsWith('?')).length; + const result = []; + + for (let i = requiredParamsCount; i <= params.length; i++) { + result.push(params.slice(0, i)); + } + + return result; + }; -/** - * This is a helper function to print out the Zod schema in a readable format. - * This is for debugging purposes only and will be removed in the future. - * - * @param {z.ZodSchema} schema - Zod schema. - * @param {number} indent - Indentation level. - */ -function printZodSchema(schema, indent = 0) { - const i = ' '.repeat(indent); - const log = msg => console.log(`${i}${msg}`); - - if (schema instanceof z.ZodUnion || schema instanceof z.ZodTuple) { - const type = schema instanceof z.ZodUnion ? 'Union' : 'Tuple'; - log(`${type}: [`); - - const items = schema instanceof z.ZodUnion - ? schema._def.options - : schema.items; - items.forEach((item, index) => { - log(` ${type === 'Union' ? 'Option' : 'Item'} ${index + 1}:`); - printZodSchema(item, indent + 4); + // Generate schemas for each function overload and merge them + const overloadSchemas = overloads.flatMap(overload => { + const combinations = generateOverloadCombinations(overload); + + return combinations.map(combo => + z.tuple( + combo + .map(p => generateParamSchema(p)) + // For now, ignore schemas that cannot be mapped to a defined type + .filter(schema => schema !== undefined) + ) + ); }); - log(']'); - } else { - log(schema.constructor.name); + + return overloadSchemas.length === 1 + ? overloadSchemas[0] + : z.union(overloadSchemas); } -} -/** - * Finds the closest schema to the input arguments. - * - * This is a helper function that identifies the closest schema to the input - * arguments, in the case of an initial validation error. We will then use the - * closest schema to generate a friendly error message. - * - * @param {z.ZodSchema} schema - Zod schema. - * @param {Array} args - User input arguments. - * @returns {z.ZodSchema} Closest schema matching the input arguments. - */ -function findClosestSchema(schema, args) { - if (!(schema instanceof z.ZodUnion)) { - return schema; + /** + * This is a helper function to print out the Zod schema in a readable format. + * This is for debugging purposes only and will be removed in the future. + * + * @param {z.ZodSchema} schema - Zod schema. + * @param {number} indent - Indentation level. + */ + function printZodSchema(schema, indent = 0) { + const i = ' '.repeat(indent); + const log = msg => console.log(`${i}${msg}`); + + if (schema instanceof z.ZodUnion || schema instanceof z.ZodTuple) { + const type = schema instanceof z.ZodUnion ? 'Union' : 'Tuple'; + log(`${type}: [`); + + const items = schema instanceof z.ZodUnion + ? schema._def.options + : schema.items; + items.forEach((item, index) => { + log(` ${type === 'Union' ? 'Option' : 'Item'} ${index + 1}:`); + printZodSchema(item, indent + 4); + }); + log(']'); + } else { + log(schema.constructor.name); + } } - // Helper function that scores how close the input arguments are to a schema. - // Lower score means closer match. - const scoreSchema = schema => { - if (!(schema instanceof z.ZodTuple)) { - console.warn('Schema below is not a tuple: '); - printZodSchema(schema); - return Infinity; + /** + * Finds the closest schema to the input arguments. + * + * This is a helper function that identifies the closest schema to the input + * arguments, in the case of an initial validation error. We will then use the + * closest schema to generate a friendly error message. + * + * @param {z.ZodSchema} schema - Zod schema. + * @param {Array} args - User input arguments. + * @returns {z.ZodSchema} Closest schema matching the input arguments. + */ + function findClosestSchema(schema, args) { + if (!(schema instanceof z.ZodUnion)) { + return schema; } - const schemaItems = schema.items; - let score = Math.abs(schemaItems.length - args.length) * 2; + // Helper function that scores how close the input arguments are to a schema. + // Lower score means closer match. + const scoreSchema = schema => { + if (!(schema instanceof z.ZodTuple)) { + console.warn('Schema below is not a tuple: '); + printZodSchema(schema); + return Infinity; + } - for (let i = 0; i < Math.min(schemaItems.length, args.length); i++) { - const paramSchema = schemaItems[i]; - const arg = args[i]; + const schemaItems = schema.items; + let score = Math.abs(schemaItems.length - args.length) * 2; - if (!paramSchema.safeParse(arg).success) score++; - } + for (let i = 0; i < Math.min(schemaItems.length, args.length); i++) { + const paramSchema = schemaItems[i]; + const arg = args[i]; - return score; - }; + if (!paramSchema.safeParse(arg).success) score++; + } - // Default to the first schema, so that we are guaranteed to return a result. - let closestSchema = schema._def.options[0]; - // We want to return the schema with the lowest score. - let bestScore = Infinity; - - const schemaUnion = schema._def.options; - schemaUnion.forEach(schema => { - const score = scoreSchema(schema); - if (score < bestScore) { - closestSchema = schema; - bestScore = score; - } - }); + return score; + }; - return closestSchema; -} + // Default to the first schema, so that we are guaranteed to return a result. + let closestSchema = schema._def.options[0]; + // We want to return the schema with the lowest score. + let bestScore = Infinity; + + const schemaUnion = schema._def.options; + schemaUnion.forEach(schema => { + const score = scoreSchema(schema); + if (score < bestScore) { + closestSchema = schema; + bestScore = score; + } + }); -/** - * Runs parameter validation by matching the input parameters to Zod schemas - * generated from the parameter data from `docs/parameterData.json`. - * - * @param {String} func - Name of the function. - * @param {Array} args - User input arguments. - * @returns {Object} The validation result. - * @returns {Boolean} result.success - Whether the validation was successful. - * @returns {any} [result.data] - The parsed data if validation was successful. - * @returns {import('zod-validation-error').ZodValidationError} [result.error] - The validation error if validation failed. - */ -p5._validateParams = function validateParams(func, args) { - if (p5.disableFriendlyErrors) { - return; // skip FES + return closestSchema; } - let funcSchemas = schemaRegistry.get(func); - if (!funcSchemas) { - funcSchemas = generateZodSchemasForFunc(func); - schemaRegistry.set(func, funcSchemas); - } + /** + * Runs parameter validation by matching the input parameters to Zod schemas + * generated from the parameter data from `docs/parameterData.json`. + * + * @param {String} func - Name of the function. + * @param {Array} args - User input arguments. + * @returns {Object} The validation result. + * @returns {Boolean} result.success - Whether the validation was successful. + * @returns {any} [result.data] - The parsed data if validation was successful. + * @returns {import('zod-validation-error').ZodValidationError} [result.error] - The validation error if validation failed. + */ + fn._validateParams = function (func, args) { + if (p5.disableFriendlyErrors) { + return; // skip FES + } - // printZodSchema(funcSchemas); + // An edge case: even when all arguments are optional and therefore, + // theoretically allowed to stay undefined and valid, it is likely that the + // user intended to call the function with non-undefined arguments. Skip + // regular workflow and return a friendly error message right away. + if (Array.isArray(args) && args.every(arg => arg === undefined)) { + const undefinedError = new Error(`All arguments for function ${func} are undefined. There is likely an error in the code.`); + const zodUndefinedError = fromError(undefinedError); + + return { + success: false, + error: zodUndefinedError + }; + } - try { - return { - success: true, - data: funcSchemas.parse(args) - }; - } catch (error) { - const closestSchema = findClosestSchema(funcSchemas, args); - const validationError = fromError(closestSchema.safeParse(args).error); + let funcSchemas = schemaRegistry.get(func); + if (!funcSchemas) { + funcSchemas = generateZodSchemasForFunc(func); + schemaRegistry.set(func, funcSchemas); + } - return { - success: false, - error: validationError - }; - } -}; + try { + return { + success: true, + data: funcSchemas.parse(args) + }; + } catch (error) { + const closestSchema = findClosestSchema(funcSchemas, args); + const validationError = fromError(closestSchema.safeParse(args).error); + + return { + success: false, + error: validationError + }; + } + }; +} -p5.prototype._validateParams = p5._validateParams; -export default p5; +export default validateParams; -const result = p5._validateParams('arc', [200, 100, 100, 80, 0, Math.PI, 'pie']); -if (!result.success) { - console.log(result.error.toString()); -} else { - console.log('Validation successful'); -} +if (typeof p5 !== 'undefined') { + validateParams(p5, p5.prototype); + p5.prototype._loadP5Constructors(); +} \ No newline at end of file diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index 79b7cf50a6..38e495414d 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -1,21 +1,37 @@ -import p5 from '../../../src/app.js'; +import validateParams from '../../../src/core/friendly_errors/param_validator.js'; import * as constants from '../../../src/core/constants.js'; -import { testUnMinified } from '../../js/p5_helpers'; -import '../../js/chai_helpers'; +import '../../js/chai_helpers' +import { vi } from 'vitest'; import { ValidationError } from 'zod-validation-error'; -suite.skip('Friendly Errors', function () { +suite('Validate Params', function () { + const mockP5 = { + disableFriendlyErrors: false, + Color: function () { + return 'mock p5.Color'; + }, + }; + const mockP5Prototype = {}; + + beforeAll(function () { + validateParams(mockP5, mockP5Prototype); + mockP5Prototype._loadP5Constructors(); + }); + + afterAll(function () { + }); + suite('validateParams: multiple types allowed for single parameter', function () { test('saturation(): valid inputs', () => { const validInputs = [ { input: ['rgb(255, 128, 128)'] }, - { input: [[0, 50, 100]] } - // TODO: add a test case for p5.Color + { input: [[0, 50, 100]] }, + { input: [new mockP5.Color()] } ]; validInputs.forEach(({ input }) => { - const result = p5._validateParams('saturation', input); + const result = mockP5Prototype._validateParams('saturation', input); assert.isTrue(result.success); }); }); @@ -29,7 +45,7 @@ suite.skip('Friendly Errors', function () { ]; invalidInputs.forEach(({ input }) => { - const result = p5._validateParams('saturation', input); + const result = mockP5Prototype._validateParams('saturation', input); assert.instanceOf(result.error, ValidationError); }); }); @@ -46,18 +62,16 @@ suite.skip('Friendly Errors', function () { testCases.forEach(({ name, input, expectSuccess }) => { test(`blendMode(): ${name}`, () => { - const result = p5._validateParams('blendMode', [input]); + const result = mockP5Prototype._validateParams('blendMode', [input]); assert.validationResult(result, expectSuccess); }); }); }); - suite('validateParams: bumbers + optional constant', function () { + suite('validateParams: numbers + optional constant for arc()', function () { const testCases = [ - // Test cases that pass validation { name: 'no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI, constants.PIE, 30], expectSuccess: true }, { name: 'missing optional param #6 & #7, no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI], expectSuccess: true }, - // Test cases that fail validation { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], expectSuccess: false }, { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], expectSuccess: false }, { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], expectSuccess: false }, @@ -67,19 +81,76 @@ suite.skip('Friendly Errors', function () { testCases.forEach(({ name, input, expectSuccess }) => { test(`arc(): ${name}`, () => { - const result = p5._validateParams('arc', input); + const result = mockP5Prototype._validateParams('arc', input); assert.validationResult(result, expectSuccess); }); }); }); + suite('validateParams: numbers + optional constant for rect()', function () { + const testCases = [ + { name: 'no friendly-err-msg', input: [1, 1, 10.5, 10], expectSuccess: true }, + { name: 'wrong param type at #0', input: ['a', 1, 10.5, 10, 0, Math.PI], expectSuccess: false } + ]; + + testCases.forEach(({ name, input, expectSuccess }) => { + test(`rect(): ${name}`, () => { + const result = mockP5Prototype._validateParams('rect', input); + assert.validationResult(result, expectSuccess); + }); + }); + }); + + suite('validateParams: class, multi-types + optional numbers', function () { + test('ambientLight(): no firendly-err-msg', function () { + const result = mockP5Prototype._validateParams('ambientLight', [new mockP5.Color()]); + assert.isTrue(result.success); + }) + }) + + suite('validateParams: a few edge cases', function () { + const testCases = [ + { fn: 'color', name: 'wrong type for optional parameter', input: [0, 0, 0, 'A'] }, + { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0] }, + { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']] }, + { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0] }, + { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0] }, + { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'] }, + { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN] } + ]; + + testCases.forEach(({ name, input, fn }) => { + test(`${fn}(): ${name}`, () => { + const result = mockP5Prototype._validateParams(fn, input); + console.log(result); + assert.validationResult(result, false); + }); + }); + }); + + suite('validateParams: trailing undefined arguments', function () { + const testCases = [ + { fn: 'color', name: 'missing params #1, #2', input: [12, undefined, undefined] }, + // Even though the undefined arguments are technically allowed for + // optional parameters, it is more likely that the user wanted to call + // the function with meaningful arguments. + { fn: 'random', name: 'missing params #0, #1', input: [undefined, undefined] }, + { fn: 'circle', name: 'missing compulsory parameter #2', input: [5, 5, undefined] } + ]; + + testCases.forEach(({ fn, name, input }) => { + test(`${fn}(): ${name}`, () => { + const result = mockP5Prototype._validateParams(fn, input); + assert.validationResult(result, false); + }); + }); + }); + suite('validateParams: multi-format', function () { const testCases = [ - // Test cases that pass validation { name: 'no friendly-err-msg', input: [65], expectSuccess: true }, { name: 'no friendly-err-msg', input: [65, 100], expectSuccess: true }, { name: 'no friendly-err-msg', input: [65, 100, 100], expectSuccess: true }, - // Test cases that fail validation { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], expectSuccess: false }, { name: 'extra parameter', input: [[65, 100, 100], 100], expectSuccess: false }, { name: 'incorrect element type', input: ['A', 'B', 'C'], expectSuccess: false }, @@ -88,7 +159,7 @@ suite.skip('Friendly Errors', function () { testCases.forEach(({ name, input, expectSuccess }) => { test(`color(): ${name}`, () => { - const result = p5._validateParams('color', input); + const result = mockP5Prototype._validateParams('color', input); assert.validationResult(result, expectSuccess); }); }); @@ -96,15 +167,15 @@ suite.skip('Friendly Errors', function () { suite('validateParameters: union types', function () { const testCases = [ - { name: 'with Number', input: [0, 0, 0], expectSuccess: true }, - { name: 'with Number[]', input: [0, 0, [0, 0, 0, 255]], expectSuccess: true }, - // TODO: add test case for p5.Color - { name: 'with Boolean (invalid)', input: [0, 0, true], expectSuccess: false } + { name: 'set() with Number', input: [0, 0, 0], expectSuccess: true }, + { name: 'set() with Number[]', input: [0, 0, [0, 0, 0, 255]], expectSuccess: true }, + { name: 'set() with Object', input: [0, 0, new mockP5.Color()], expectSuccess: true }, + { name: 'set() with Boolean (invalid)', input: [0, 0, true], expectSuccess: false } ]; testCases.forEach(({ name, input, expectSuccess }) => { - testUnMinified(`set(): ${name}`, function () { - const result = p5._validateParams('set', input); + test(`set(): ${name}`, function () { + const result = mockP5Prototype._validateParams('set', input); assert.validationResult(result, expectSuccess); }); }); diff --git a/test/unit/spec.js b/test/unit/spec.js index c6a16946c0..995b152693 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -10,6 +10,7 @@ var spec = { 'main', 'p5.Element', 'p5.Graphics', + 'param_errors', 'preload', 'rendering', 'structure', @@ -58,8 +59,8 @@ var spec = { document.write( '' ); -Object.keys(spec).map(function(folder) { - spec[folder].map(function(file) { +Object.keys(spec).map(function (folder) { + spec[folder].map(function (file) { var string = [ '