From f8bcb56df08dce426aa06128aaf4f39a92ef3186 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sat, 19 Oct 2024 22:16:26 -0400 Subject: [PATCH 1/3] update --- src/core/friendly_errors/sketch_verifier.js | 152 +++++++++++++++++++- test/unit/core/sketch_overrides.js | 124 ++++++++++++---- 2 files changed, 249 insertions(+), 27 deletions(-) diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index f90eb4f204..bdd0b9f14d 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -1,11 +1,66 @@ import * as acorn from 'acorn'; import * as walk from 'acorn-walk'; +import * as constants from '../constants'; /** * @for p5 * @requires core */ function sketchVerifier(p5, fn) { + // List of functions to ignore as they either are meant to be re-defined or + // generate false positive outputs. + const ignoreFunction = [ + 'setup', + 'draw', + 'preload', + 'deviceMoved', + 'deviceTurned', + 'deviceShaken', + 'doubleClicked', + 'mousePressed', + 'mouseReleased', + 'mouseMoved', + 'mouseDragged', + 'mouseClicked', + 'mouseWheel', + 'touchStarted', + 'touchMoved', + 'touchEnded', + 'keyPressed', + 'keyReleased', + 'keyTyped', + 'windowResized', + 'name', + 'parent', + 'toString', + 'print', + 'stop', + 'onended' + ]; + + // Mapping names of p5 types to their constructor functions. + // p5Constructors: + // - Color: f() + // - Graphics: f() + // - Vector: f() + // and so on. + const p5Constructors = {}; + + fn.loadP5Constructors = function () { + console.log("loading..."); + // 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]; + } + } + console.log('p5Constructors:', p5Constructors); + } + /** * Fetches the contents of a script element in the user's sketch. * @@ -111,11 +166,105 @@ function sketchVerifier(p5, fn) { return userDefinitions; } + /** + * Checks user-defined variables and functions for conflicts with p5.js + * constants and global functions. + * + * This function performs two main checks: + * 1. Verifies if any user definition conflicts with p5.js constants. + * 2. Checks if any user definition conflicts with global functions from + * p5.js renderer classes. + * + * If a conflict is found, it reports a friendly error message and halts + * further checking. + * + * @param {Object} userDefinitions - An object containing user-defined variables and functions. + * @param {Array<{name: string, line: number}>} userDefinitions.variables - Array of user-defined variable names and their line numbers. + * @param {Array<{name: string, line: number}>} userDefinitions.functions - Array of user-defined function names and their line numbers. + * @returns {boolean} - Returns true if a conflict is found, false otherwise. + */ + fn.checkForConstsAndFuncs = function (userDefinitions) { + const allDefinitions = [ + ...userDefinitions.variables, + ...userDefinitions.functions + ]; + console.log(allDefinitions); + + function generateFriendlyError(errorType, name, line) { + const url = `https://p5js.org/reference/#/p5/${name}`; + const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. JavaScript does not declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; + return message; + } + + function checkForRedefinition(name, libValue, line, type) { + console.log('name:', name, 'libValue:', libValue, 'line:', line, 'type:', type); + try { + const userValue = eval(name); + if (libValue !== userValue) { + let message = generateFriendlyError(type, name, line); + console.log(message); + return true; + } + } catch (e) { + // If eval fails, the function hasn't been redefined + return false; + } + return false; + } + + for (let { name, line } of allDefinitions) { + const libDefinition = constants[name]; + if (libDefinition !== undefined) { + if (checkForRedefinition(name, libDefinition, line, "Constant")) { + return true; + } + } + } + + // Get the names of all p5.js functions which are available globally + const classesWithGlobalFns = ['Renderer', 'Renderer2D', 'RendererGL']; + const globalFunctions = new Set( + classesWithGlobalFns.flatMap(className => + Object.keys(p5Constructors[className]?.prototype || {}) + ) + ); + + for (let { name, line } of allDefinitions) { + if (!ignoreFunction.includes(name) && globalFunctions.has(name)) { + for (let className of classesWithGlobalFns) { + const prototypeFunc = p5Constructors[className]?.prototype[name]; + if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { + return true; + } + } + } + } + + // Additional check for other p5 constructors + const otherP5ConstructorKeys = Object.keys(p5Constructors).filter( + key => !classesWithGlobalFns.includes(key) + ); + for (let { name, line } of allDefinitions) { + for (let key of otherP5ConstructorKeys) { + if (p5Constructors[key].prototype[name] !== undefined) { + const prototypeFunc = p5Constructors[key].prototype[name]; + if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { + return true; + } + } + } + } + + return false; + } + fn.run = async function () { const userCode = await fn.getUserCode(); const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); - return userDefinedVariablesAndFuncs; + if (fn.checkForConstsAndFuncs(userDefinedVariablesAndFuncs)) { + return; + } } } @@ -123,4 +272,5 @@ export default sketchVerifier; if (typeof p5 !== 'undefined') { sketchVerifier(p5, p5.prototype); + p5.prototype.loadP5Constructors(); } \ No newline at end of file diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js index cf16edbff7..cafac58395 100644 --- a/test/unit/core/sketch_overrides.js +++ b/test/unit/core/sketch_overrides.js @@ -1,13 +1,17 @@ import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js'; -suite('Validate Params', function () { +suite('Sketch Verifier', function () { const mockP5 = { - _validateParameters: vi.fn() + _validateParameters: vi.fn(), + Render: function () { + return 'mock render'; + }, }; const mockP5Prototype = {}; beforeAll(function () { sketchVerifier(mockP5, mockP5Prototype); + mockP5Prototype.loadP5Constructors(); }); afterAll(function () { @@ -179,42 +183,110 @@ suite('Validate Params', function () { }); }); + suite('checkForConstsAndFuncs()', function () { + test('Detects conflict with p5.js constant', function () { + const userDefinitions = { + variables: [{ name: 'PI', line: 1 }], + functions: [] + }; + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); + + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + + expect(result).toBe(true); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Constant "PI" on line 1 is being redeclared and conflicts with a p5.js constant')); + + consoleSpy.mockRestore(); + }); + + test('Detects conflict with p5.js global function', function () { + const userDefinitions = { + variables: [], + functions: [{ name: 'setup', line: 2 }] + }; + //const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); + + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + + expect(result).toBe(true); + //expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Function "setup" on line 2 is being redeclared and conflicts with a p5.js function')); + + //consoleSpy.mockRestore(); + }); + + test('Returns false when no conflicts are found', function () { + const userDefinitions = { + variables: [{ name: 'img', line: 1 }], + functions: [{ name: 'cut', line: 2 }] + }; + + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + + expect(result).toBe(false); + }); + }); + suite('run()', function () { - test('Returns extracted variables and functions', async function () { + test('Extracts user-defined variables and functions and checks for conflicts', async function () { const mockScript = ` let x = 5; const y = 10; function foo() {} const bar = () => {}; + class MyClass {} `; mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); - - const result = await mockP5Prototype.run(); - const expectedResult = { - "functions": [ - { - "line": 3, - "name": "foo", - }, - { - "line": 4, - "name": "bar", - }, + mockP5Prototype.extractUserDefinedVariablesAndFuncs = vi.fn(() => ({ + variables: [ + { name: 'x', line: 1 }, + { name: 'y', line: 2 }, + { name: 'MyClass', line: 5 } ], - "variables": [ - { - "line": 1, - "name": "x", - }, - { - "line": 2, - "name": "y", - }, + functions: [ + { name: 'foo', line: 3 }, + { name: 'bar', line: 4 } + ] + })); + mockP5Prototype.checkForConstsAndFuncs = vi.fn(() => false); + + await mockP5Prototype.run(); + + expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); + expect(mockP5Prototype.extractUserDefinedVariablesAndFuncs).toHaveBeenCalledWith(mockScript); + expect(mockP5Prototype.checkForConstsAndFuncs).toHaveBeenCalledWith({ + variables: [ + { name: 'x', line: 1 }, + { name: 'y', line: 2 }, + { name: 'MyClass', line: 5 } ], - }; + functions: [ + { name: 'foo', line: 3 }, + { name: 'bar', line: 4 } + ] + }); + }); + + test('Stops execution when a conflict is found', async function () { + const mockScript = ` + let PI = 3.14; + function setup() {} + `; + mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); + mockP5Prototype.extractUserDefinedVariablesAndFuncs = vi.fn(() => ({ + variables: [{ name: 'PI', line: 1 }], + functions: [{ name: 'setup', line: 2 }] + })); + mockP5Prototype.checkForConstsAndFuncs = vi.fn(() => true); + + const result = await mockP5Prototype.run(); expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); - expect(result).toEqual(expectedResult); + expect(mockP5Prototype.extractUserDefinedVariablesAndFuncs).toHaveBeenCalledWith(mockScript); + expect(mockP5Prototype.checkForConstsAndFuncs).toHaveBeenCalledWith({ + variables: [{ name: 'PI', line: 1 }], + functions: [{ name: 'setup', line: 2 }] + }); + expect(result).toBeUndefined(); }); }); }); \ No newline at end of file From d3e8f33d8a2291abd4d84e07e36d53c8d954058f Mon Sep 17 00:00:00 2001 From: miaoye que Date: Mon, 21 Oct 2024 09:59:54 -0400 Subject: [PATCH 2/3] more updates --- src/core/friendly_errors/sketch_verifier.js | 13 ++++--- test/unit/core/sketch_overrides.js | 42 +++++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index bdd0b9f14d..b2e2ba6107 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -47,7 +47,6 @@ function sketchVerifier(p5, fn) { const p5Constructors = {}; fn.loadP5Constructors = function () { - console.log("loading..."); // 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 @@ -58,7 +57,6 @@ function sketchVerifier(p5, fn) { p5Constructors[key] = p5[key]; } } - console.log('p5Constructors:', p5Constructors); } /** @@ -188,18 +186,22 @@ function sketchVerifier(p5, fn) { ...userDefinitions.variables, ...userDefinitions.functions ]; - console.log(allDefinitions); + // Helper function that generates a friendly error message that contains + // the type of redefinition (constant or function), the name of the + // redefinition, the line number in user's code, and a link to its + // reference on the p5.js website. function generateFriendlyError(errorType, name, line) { const url = `https://p5js.org/reference/#/p5/${name}`; const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. JavaScript does not declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; return message; } + // Helper function that checks if a user definition has already been defined + // in the p5.js library, either as a constant or as a function. function checkForRedefinition(name, libValue, line, type) { - console.log('name:', name, 'libValue:', libValue, 'line:', line, 'type:', type); try { - const userValue = eval(name); + const userValue = eval("name"); if (libValue !== userValue) { let message = generateFriendlyError(type, name, line); console.log(message); @@ -212,6 +214,7 @@ function sketchVerifier(p5, fn) { return false; } + // Checks for constant redefinitions. for (let { name, line } of allDefinitions) { const libDefinition = constants[name]; if (libDefinition !== undefined) { diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js index cafac58395..f219d0142c 100644 --- a/test/unit/core/sketch_overrides.js +++ b/test/unit/core/sketch_overrides.js @@ -3,10 +3,9 @@ import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js suite('Sketch Verifier', function () { const mockP5 = { _validateParameters: vi.fn(), - Render: function () { - return 'mock render'; - }, + Renderer: function () { }, }; + mockP5.Renderer.prototype.rect = vi.fn(); const mockP5Prototype = {}; beforeAll(function () { @@ -184,34 +183,53 @@ suite('Sketch Verifier', function () { }); suite('checkForConstsAndFuncs()', function () { + // Set up for this suite of tests + let consoleSpy; + + beforeEach(function () { + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); + }); + + afterEach(function () { + consoleSpy.mockRestore(); + }); + test('Detects conflict with p5.js constant', function () { const userDefinitions = { variables: [{ name: 'PI', line: 1 }], functions: [] }; - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); expect(result).toBe(true); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Constant "PI" on line 1 is being redeclared and conflicts with a p5.js constant')); - - consoleSpy.mockRestore(); }); test('Detects conflict with p5.js global function', function () { const userDefinitions = { variables: [], - functions: [{ name: 'setup', line: 2 }] + functions: [{ name: 'rect', line: 2 }] }; - //const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { }); - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); expect(result).toBe(true); - //expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Function "setup" on line 2 is being redeclared and conflicts with a p5.js function')); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Function "rect" on line 2 is being redeclared and conflicts with a p5.js function')); + }); + + test('allows redefinition of whitelisted functions', function () { + const userDefinitions = { + variables: [], + functions: [ + { name: 'setup', line: 1 }, + { name: 'draw', line: 2 }, + { name: 'preload', line: 3 } + ] + }; - //consoleSpy.mockRestore(); + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + + expect(result).toBe(false); + expect(consoleSpy).not.toHaveBeenCalled(); }); test('Returns false when no conflicts are found', function () { From b42960eac2e4c6a731db6b0668a23314b9469bcd Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sat, 26 Oct 2024 13:01:51 -0400 Subject: [PATCH 3/3] update --- .../friendly_errors/friendly_errors_utils.js | 31 ++++++++ src/core/friendly_errors/sketch_verifier.js | 78 ++++++------------- test/unit/core/sketch_overrides.js | 52 +++++++++---- 3 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 src/core/friendly_errors/friendly_errors_utils.js diff --git a/src/core/friendly_errors/friendly_errors_utils.js b/src/core/friendly_errors/friendly_errors_utils.js new file mode 100644 index 0000000000..be4c2b682d --- /dev/null +++ b/src/core/friendly_errors/friendly_errors_utils.js @@ -0,0 +1,31 @@ +/** + * Loads and returns a mapping of p5 constructor names to their corresponding + * functions. This function iterates through all properties of the p5 object + * identifying constructor functions (those starting with a capital letter) and + * storing them in an object. + * @returns {Object} An object mapping p5 constructor names to their functions. + */ +function loadP5Constructors(p5) { + // Mapping names of p5 types to their constructor functions. + // p5Constructors: + // - Color: f() + // - Graphics: f() + // - Vector: f() + // and so on. + const p5Constructors = {}; + + // 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]; + } + } + + return p5Constructors; +} + +export { loadP5Constructors }; \ No newline at end of file diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index b2e2ba6107..033868aaf6 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -1,6 +1,7 @@ -import * as acorn from 'acorn'; -import * as walk from 'acorn-walk'; +import { parse } from 'acorn'; +import { simple as walk } from 'acorn-walk'; import * as constants from '../constants'; +import { loadP5Constructors } from './friendly_errors_utils'; /** * @for p5 @@ -38,27 +39,6 @@ function sketchVerifier(p5, fn) { 'onended' ]; - // 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]; - } - } - } - /** * Fetches the contents of a script element in the user's sketch. * @@ -119,13 +99,13 @@ function sketchVerifier(p5, fn) { const lineOffset = -1; try { - const ast = acorn.parse(code, { + const ast = parse(code, { ecmaVersion: 2021, sourceType: 'module', locations: true // This helps us get the line number. }); - walk.simple(ast, { + walk(ast, { VariableDeclarator(node) { if (node.id.type === 'Identifier') { const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type) @@ -181,7 +161,7 @@ function sketchVerifier(p5, fn) { * @param {Array<{name: string, line: number}>} userDefinitions.functions - Array of user-defined function names and their line numbers. * @returns {boolean} - Returns true if a conflict is found, false otherwise. */ - fn.checkForConstsAndFuncs = function (userDefinitions) { + fn.checkForConstsAndFuncs = function (userDefinitions, p5Constructors) { const allDefinitions = [ ...userDefinitions.variables, ...userDefinitions.functions @@ -193,7 +173,7 @@ function sketchVerifier(p5, fn) { // reference on the p5.js website. function generateFriendlyError(errorType, name, line) { const url = `https://p5js.org/reference/#/p5/${name}`; - const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. JavaScript does not declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; + const message = `${errorType} "${name}" on line ${line} is being redeclared and conflicts with a p5.js ${errorType.toLowerCase()}. JavaScript does not support declaring a ${errorType.toLowerCase()} more than once. p5.js reference: ${url}.`; return message; } @@ -224,36 +204,19 @@ function sketchVerifier(p5, fn) { } } - // Get the names of all p5.js functions which are available globally - const classesWithGlobalFns = ['Renderer', 'Renderer2D', 'RendererGL']; + // The new rules for attaching anything to global are (if true for both of + // the following): + // - It is a member of p5.prototype + // - Its name does not start with `_` const globalFunctions = new Set( - classesWithGlobalFns.flatMap(className => - Object.keys(p5Constructors[className]?.prototype || {}) - ) + Object.keys(p5.prototype).filter(key => !key.startsWith('_')) ); for (let { name, line } of allDefinitions) { if (!ignoreFunction.includes(name) && globalFunctions.has(name)) { - for (let className of classesWithGlobalFns) { - const prototypeFunc = p5Constructors[className]?.prototype[name]; - if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { - return true; - } - } - } - } - - // Additional check for other p5 constructors - const otherP5ConstructorKeys = Object.keys(p5Constructors).filter( - key => !classesWithGlobalFns.includes(key) - ); - for (let { name, line } of allDefinitions) { - for (let key of otherP5ConstructorKeys) { - if (p5Constructors[key].prototype[name] !== undefined) { - const prototypeFunc = p5Constructors[key].prototype[name]; - if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { - return true; - } + const prototypeFunc = p5.prototype[name]; + if (prototypeFunc && checkForRedefinition(name, prototypeFunc, line, "Function")) { + return true; } } } @@ -262,10 +225,18 @@ function sketchVerifier(p5, fn) { } fn.run = async function () { + const p5Constructors = await new Promise(resolve => { + if (document.readyState === 'complete') { + resolve(loadP5Constructors(p5)); + } else { + window.addEventListener('load', () => resolve(loadP5Constructors(p5))); + } + }); + const userCode = await fn.getUserCode(); const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); - if (fn.checkForConstsAndFuncs(userDefinedVariablesAndFuncs)) { + if (fn.checkForConstsAndFuncs(userDefinedVariablesAndFuncs, p5Constructors)) { return; } } @@ -275,5 +246,4 @@ export default sketchVerifier; if (typeof p5 !== 'undefined') { sketchVerifier(p5, p5.prototype); - p5.prototype.loadP5Constructors(); } \ No newline at end of file diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js index f219d0142c..3ea274b242 100644 --- a/test/unit/core/sketch_overrides.js +++ b/test/unit/core/sketch_overrides.js @@ -1,16 +1,22 @@ import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js'; +import { loadP5Constructors } from '../../../src/core/friendly_errors/friendly_errors_utils.js'; suite('Sketch Verifier', function () { const mockP5 = { _validateParameters: vi.fn(), - Renderer: function () { }, + Color: function () { }, + Vector: function () { }, + prototype: { + rect: function () { }, + ellipse: function () { }, + } }; - mockP5.Renderer.prototype.rect = vi.fn(); const mockP5Prototype = {}; + let p5Constructors; beforeAll(function () { + p5Constructors = loadP5Constructors(mockP5); sketchVerifier(mockP5, mockP5Prototype); - mockP5Prototype.loadP5Constructors(); }); afterAll(function () { @@ -199,7 +205,7 @@ suite('Sketch Verifier', function () { variables: [{ name: 'PI', line: 1 }], functions: [] }; - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions, p5Constructors); expect(result).toBe(true); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Constant "PI" on line 1 is being redeclared and conflicts with a p5.js constant')); @@ -210,13 +216,13 @@ suite('Sketch Verifier', function () { variables: [], functions: [{ name: 'rect', line: 2 }] }; - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions, p5Constructors); expect(result).toBe(true); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Function "rect" on line 2 is being redeclared and conflicts with a p5.js function')); }); - test('allows redefinition of whitelisted functions', function () { + test('Allows redefinition of whitelisted functions', function () { const userDefinitions = { variables: [], functions: [ @@ -226,7 +232,7 @@ suite('Sketch Verifier', function () { ] }; - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions, p5Constructors); expect(result).toBe(false); expect(consoleSpy).not.toHaveBeenCalled(); @@ -238,14 +244,27 @@ suite('Sketch Verifier', function () { functions: [{ name: 'cut', line: 2 }] }; - const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions); + const result = mockP5Prototype.checkForConstsAndFuncs(userDefinitions, p5Constructors); expect(result).toBe(false); }); }); suite('run()', function () { + let originalReadyState; + + beforeEach(function () { + originalReadyState = document.readyState; + vi.spyOn(window, 'addEventListener'); + }); + + afterEach(function () { + Object.defineProperty(document, 'readyState', { value: originalReadyState }); + vi.restoreAllMocks(); + }); + test('Extracts user-defined variables and functions and checks for conflicts', async function () { + Object.defineProperty(document, 'readyState', { value: 'complete' }); const mockScript = ` let x = 5; const y = 10; @@ -281,10 +300,15 @@ suite('Sketch Verifier', function () { { name: 'foo', line: 3 }, { name: 'bar', line: 4 } ] - }); + }, + expect.any(Object) // This is the p5Constructors object + ); + expect(window.addEventListener).not.toHaveBeenCalled(); }); test('Stops execution when a conflict is found', async function () { + Object.defineProperty(document, 'readyState', { value: 'complete' }); + const mockScript = ` let PI = 3.14; function setup() {} @@ -300,10 +324,12 @@ suite('Sketch Verifier', function () { expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); expect(mockP5Prototype.extractUserDefinedVariablesAndFuncs).toHaveBeenCalledWith(mockScript); - expect(mockP5Prototype.checkForConstsAndFuncs).toHaveBeenCalledWith({ - variables: [{ name: 'PI', line: 1 }], - functions: [{ name: 'setup', line: 2 }] - }); + expect(mockP5Prototype.checkForConstsAndFuncs).toHaveBeenCalledWith( + { + variables: [{ name: 'PI', line: 1 }], + functions: [{ name: 'setup', line: 2 }] + }, + expect.any(Object)); // This is the p5Constructors object expect(result).toBeUndefined(); }); });