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..2fa02d5525 100644
--- a/src/core/friendly_errors/param_validator.js
+++ b/src/core/friendly_errors/param_validator.js
@@ -8,351 +8,367 @@ 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();
+
+ // 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(),
+ 'Object': z.object({}),
+ 'String': 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',
+ 'RegExp',
+ '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 = dataDoc[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 => {
+ const isArray = type.endsWith('[]');
+ const baseType = isArray ? type.slice(0, -2) : type;
+
+ let typeSchema;
+
+ // 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(baseType)) {
+ typeSchema = z.literal(constantsMap[baseType]);
+ }
+ // All p5 objects start with `p5` in the documentation, i.e. `p5.Camera`.
+ else if (baseType.startsWith('p5')) {
+ console.log('type', baseType);
+ const className = baseType.substring(baseType.indexOf('.') + 1);
+ console.log('className', p5Constructors[className]);
+ typeSchema = z.instanceof(p5Constructors[className]);
+ }
+ // For primitive types and web API objects.
+ else if (schemaMap[baseType]) {
+ typeSchema = schemaMap[baseType];
+ } else {
+ throw new Error(`Unsupported type '${type}' in parameter validation. Please report this issue.`);
+ }
+
+ return isArray ? z.array(typeSchema) : typeSchema;
+ };
- 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 isOptional = 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 isOptional ? 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..04249c2199 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('p5.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('p5.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('p5.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('p5.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('p5.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(`p5.${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(`p5.${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('p5.color', input);
assert.validationResult(result, expectSuccess);
});
});
@@ -96,17 +167,34 @@ 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('p5.set', input);
assert.validationResult(result, expectSuccess);
});
});
});
+
+ suite('validateParams: web API objects', function () {
+ const audioContext = new AudioContext();
+ const gainNode = audioContext.createGain();
+
+ const testCases = [
+ { fn: 'mouseMoved', name: 'no friendly-err-msg', input: [new MouseEvent('click')] },
+ { fn: 'p5.MediaElement.connect', name: 'no friendly-err-msg', input: [gainNode] }
+ ];
+
+ testCases.forEach(({ fn, name, input }) => {
+ test(`${fn}(): ${name}`, function () {
+ const result = mockP5Prototype._validateParams(fn, input);
+ assert.validationResult(result, true);
+ });
+ });
+ });
});
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 = [
'