diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index d6cd1ce5f5..02a0fbd8ad 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -2722,7 +2722,13 @@ function renderer3D(p5, fn) { `buildComputeShader() is only available with the WebGPU renderer. ${webGPUAddonMessage}` ); } - return this.baseComputeShader().modify(cb, context, { hook: 'iteration' }); + const wasInternal = this._isUserCall; + this._isUserCall = false; + try { + return this.baseComputeShader().modify(cb, context, { hook: 'iteration' }); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildComputeShader', markExperimental('webgpu', p5)); diff --git a/src/friendly_errors/param_validator.js b/src/friendly_errors/param_validator.js index 8510831344..5cab134c75 100644 --- a/src/friendly_errors/param_validator.js +++ b/src/friendly_errors/param_validator.js @@ -18,6 +18,7 @@ let documentationData = dataDoc; function validateParams(p5, fn, lifecycles) { // Cache for Zod schemas let schemaRegistry = new Map(); + let strandsSchemaRegistry = new Map(); // Mapping names of p5 types to their constructor functions. // p5Constructors: @@ -200,6 +201,17 @@ function validateParams(p5, fn, lifecycles) { // For primitive types and web API objects. else if (schemaMap[baseType]) { typeSchema = schemaMap[baseType]; + if ( + p5._isStrandsContextActive && + ['Number', 'Integer', 'Boolean', 'String'].includes(baseType) + ) { + typeSchema = z.union([ + typeSchema, + z.custom(val => val && val.isStrandsNode, { + message: 'Expected StrandsNode' + }) + ]); + } } // Tuple types else if ( @@ -572,7 +584,7 @@ function validateParams(p5, fn, lifecycles) { errorStackParser.parse(Error()).slice(3) ); const locationStr = getFriendlyStack(stacktrace, true); - FES.log`${locationStr} ${message}`(); + FES.log`${locationStr ? locationStr + ' ' : ''}${message}`(); } return message; }; @@ -615,11 +627,21 @@ function validateParams(p5, fn, lifecycles) { }; } - let funcSchemas = schemaRegistry.get(func); + let activeRegistry = p5._isStrandsContextActive ? strandsSchemaRegistry : schemaRegistry; + let funcSchemas = activeRegistry.get(func); + if (!funcSchemas) { - funcSchemas = generateZodSchemasForFunc(func); + // Check _strandsSignatures for explicit bypass entries (value === true) + // that only apply inside an active strands context. + if (p5._isStrandsContextActive && p5._strandsSignatures?.has(func)) { + funcSchemas = z.any(); + } + + if (!funcSchemas) { + funcSchemas = generateZodSchemasForFunc(func); + } if (!funcSchemas) return; - schemaRegistry.set(func, funcSchemas); + activeRegistry.set(func, funcSchemas); } if (z.validate(funcSchemas, args)) { diff --git a/src/friendly_errors/stacktrace.js b/src/friendly_errors/stacktrace.js index 015e429705..a32433d83b 100644 --- a/src/friendly_errors/stacktrace.js +++ b/src/friendly_errors/stacktrace.js @@ -279,11 +279,22 @@ export const processStack = (error, stacktrace) => { let isInternal = false; let p5FileName, friendlyStack; + // Helper to strip query strings and hashes from URLs for consistent matching + const cleanUrl = url => (typeof url === 'string' ? url.split('?')[0].split('#')[0] : ''); + // Intentionally throw an error that we catch so that we can check the name // of the current file. Any errors we see from this file, we treat as // internal errors. const testStacktrace = errorStackParser.parse(Error()); - p5FileName = testStacktrace[0].fileName; + p5FileName = cleanUrl(testStacktrace[0]?.fileName); + const p5BaseName = p5FileName ? p5FileName.split('/').slice(-1)[0] : ''; + const isP5File = url => { + const cleaned = cleanUrl(url); + if (!cleaned) return false; + if (cleaned === p5FileName) return true; + if (p5BaseName && cleaned.split('/').slice(-1)[0] === p5BaseName) return true; + return false; + }; for (let i = stacktrace.length - 1; i >= 0; i--) { let splitted = stacktrace[i].functionName.split('.'); @@ -291,15 +302,6 @@ export const processStack = (error, stacktrace) => { // remove everything below an entry point function (setup, draw, etc). // (it's usually the internal initialization calls) friendlyStack = stacktrace.slice(0, i + 1); - // We call the error "internal" if the source of the error was a - // function from within the p5.js library file, but called from the - // user's code directly. We only need to check the topmost frame in - // the stack trace since any function internal to p5 should pass this - // check, not just public p5 functions. - if (stacktrace[0].fileName === p5FileName) { - isInternal = true; - break; - } break; } } @@ -309,6 +311,14 @@ export const processStack = (error, stacktrace) => { // entire stacktrace for friendlyStack if (!friendlyStack) friendlyStack = stacktrace; + // We call the error "internal" if the source of the error was a + // function from within the p5.js library file, but called from the + // user's code directly. When error is null (called from param_validator), + // it is always an internal validation call. + if (error === null || isP5File(stacktrace[0]?.fileName)) { + isInternal = true; + } + if (isInternal) { // the frameIndex property is added before the filter, so frameIndex // corresponds to the index of a frame in the original stacktrace. @@ -319,7 +329,15 @@ export const processStack = (error, stacktrace) => { frame.frameIndex = index; return frame; }) - .filter(frame => frame.fileName !== p5FileName); + .filter(frame => { + const cleaned = cleanUrl(frame.fileName); + return ( + cleaned && + !isP5File(frame.fileName) && + cleaned !== '' && + !cleaned.startsWith('eval') + ); + }); // a weird case, if for some reason we can't identify the function called // from user's code @@ -332,16 +350,19 @@ export const processStack = (error, stacktrace) => { friendlyStack[0].lineNumber && friendlyStack[0].columnNumber ) { + const cleanFile = cleanUrl(friendlyStack[0].fileName); locationObj = { - location: `${friendlyStack[0].fileName}:${ + location: `${cleanFile}:${ friendlyStack[0].lineNumber }:${friendlyStack[0].columnNumber}`, - file: friendlyStack[0].fileName.split('/').slice(-1), + file: cleanFile.split('/').slice(-1)[0], line: friendlyStack[0].lineNumber }; // if already handled by another part of the FES, don't handle again - if (p5._fesLogCache[locationObj.location]) return [true, null]; + if (typeof p5 !== 'undefined' && p5?._fesLogCache?.[locationObj.location]) { + return [true, null]; + } } // Library error @@ -394,8 +415,16 @@ export function printFriendlyStack(friendlyStack) { } export function getFriendlyStack(stacktrace, compact = false) { + if (!stacktrace || !stacktrace.length || !stacktrace[0]?.fileName) { + return null; + } + const rawFile = stacktrace[0].fileName; + const cleanFile = (typeof rawFile === 'string' ? rawFile.split('?')[0].split('#')[0] : '').split('/').slice(-1)[0]; + if (!cleanFile || cleanFile === '' || cleanFile.startsWith('eval')) { + return null; + } if (compact) { - return `[${stacktrace[0].fileName.split('/').slice(-1)}, line ${stacktrace[0].lineNumber}]`; + return `[${cleanFile}, line ${stacktrace[0].lineNumber}]`; } else { if (stacktrace.length > 1) { let stacktraceMsg = ''; diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 627e834e9c..e95cc4788a 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -125,7 +125,6 @@ function strands(p5, fn) { ctx.backend = backend; ctx.active = active; ctx.renderer = renderer; - ctx.baseShader = baseShader; ctx.previousFES = p5.disableFriendlyErrors; ctx.tempProps = []; ctx._noiseOctaves = null; @@ -144,7 +143,7 @@ function strands(p5, fn) { // the pass is done since FES is switched off while it runs (see below). ctx.experimentalFeaturesUsed = new Set(); if (active) { - p5.disableFriendlyErrors = true; + p5._isStrandsContextActive = true; } } @@ -167,7 +166,6 @@ function strands(p5, fn) { ctx.backend = undefined; ctx.active = false; ctx.renderer = null; - ctx.baseShader = null; ctx.previousFES = p5.disableFriendlyErrors; ctx.tempProps = []; ctx._noiseOctaves = null; @@ -193,7 +191,7 @@ function strands(p5, fn) { * @return {Error[]} Errors from temporary writes that could not be undone. */ function deinitStrandsContext(ctx) { - p5.disableFriendlyErrors = ctx.previousFES; + p5._isStrandsContextActive = false; // Undo hooks, hook aliases and options.hook properties (incl. #8549's // p5.Graphics.prototype entries) exactly, newest first. const restoreErrors = restoreTemporaryProperties(ctx); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 37d219e2f2..104cb76b4b 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -251,6 +251,8 @@ function installInstanceIndexAccessor(strandsContext) { */ function augmentFn(fn, p5, name, value) { fn[name] = value; + p5?._strandsSignatures?.set(name, true); + p5?._strandsSignatures?.set(`p5.${name}`, true); const GraphicsProto = p5?.Graphics?.prototype; if (GraphicsProto) { GraphicsProto[name] = value; @@ -364,6 +366,8 @@ function resolveShaderName(strandsContext, name, apiName) { // User nodes ////////////////////////////////////////////// export function initGlobalStrandsAPI(p5, fn, strandsContext) { + p5._strandsSignatures = new Map(); + // We augment the strands node with operations programatically // this means methods like .add, .sub, etc can be chained for (const { name, arity, opCode } of OperatorTable) { @@ -392,6 +396,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Unique Functions ////////////////////////////////////////////// + p5._strandsSignatures.set('discard', true); augmentFn(fn, p5, 'discard', function () { build.statementNode(strandsContext, StatementType.DISCARD); }); @@ -408,11 +413,13 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ); } } + p5._strandsSignatures.set('break', true); augmentFn(fn, p5, 'break', function () { rejectLoopExitWithHelpers('break'); build.statementNode(strandsContext, StatementType.BREAK); }); p5.break = fn.break; + p5._strandsSignatures.set('instanceID', true); augmentFn(fn, p5, 'instanceID', function () { const node = build.variableNode( strandsContext, @@ -426,6 +433,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { p5.strandsIf = function (conditionNode, ifBody) { return new StrandsConditional(strandsContext, conditionNode, ifBody); }; + p5._strandsSignatures.set('strandsIf', true); augmentFn(fn, p5, 'strandsIf', p5.strandsIf); p5.strandsFor = function ( initialCb, @@ -443,10 +451,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { initialVars ).build(); }; + p5._strandsSignatures.set('strandsFor', true); augmentFn(fn, p5, 'strandsFor', p5.strandsFor); p5.strandsTernary = function (condition, ifTrue, ifFalse) { return buildTernary(strandsContext, condition, ifTrue, ifFalse); }; + p5._strandsSignatures.set('strandsTernary', true); augmentFn(fn, p5, 'strandsTernary', p5.strandsTernary); // Both sides are built; allow helper reads here, but reject updates. p5.strandsOperand = function (operator, operand) { @@ -506,6 +516,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return valueNode; }; + p5._strandsSignatures.set('strandsEarlyReturn', true); augmentFn(fn, p5, 'strandsEarlyReturn', p5.strandsEarlyReturn); p5.strandsNode = function (...args) { if (args.length === 1 && args[0] instanceof StrandsNode) { @@ -877,10 +888,11 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { ////////////////////////////////////////////// // Builtins, uniforms, variable constructors - ////////////////////////////////////////////// for (const [functionName, overrides] of Object.entries( strandsBuiltinFunctions )) { + p5._strandsSignatures.set(functionName, true); + p5._strandsSignatures.set(`p5.${functionName}`, true); const isp5Function = overrides[0].isp5Function; if (isp5Function) { const originalFn = fn[functionName]; @@ -915,6 +927,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Alias lerp to GLSL mix in strands context const originalLerp = fn.lerp; + p5._strandsSignatures.set('lerp', true); augmentFn(fn, p5, 'lerp', function (...args) { if (strandsContext.active) { return this.mix(...args); @@ -924,6 +937,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalMap = fn.map; + p5._strandsSignatures.set('map', true); augmentFn(fn, p5, 'map', function (...args) { if (!strandsContext.active) { return originalMap.apply(this, args); @@ -945,6 +959,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalColor = fn.color; + p5._strandsSignatures.set('color', true); augmentFn(fn, p5, 'color', function (...args) { if (!strandsContext.active) { return originalColor.apply(this, args); @@ -961,6 +976,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return createStrandsNode(id, dimension, strandsContext); }); const originalLerpColor = fn.lerpColor; + p5._strandsSignatures.set('lerpColor', true); augmentFn(fn, p5, 'lerpColor', function (...args) { if (!strandsContext.active) { return originalLerpColor.apply(this, args); @@ -970,6 +986,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); // Component accessors: extract scalar channels from a vec4 color const originalRed = fn.red; + p5._strandsSignatures.set('red', true); augmentFn(fn, p5, 'red', function (...args) { if (!strandsContext.active) { return originalRed.apply(this, args); @@ -978,6 +995,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalGreen = fn.green; + p5._strandsSignatures.set('green', true); augmentFn(fn, p5, 'green', function (...args) { if (!strandsContext.active) { return originalGreen.apply(this, args); @@ -986,6 +1004,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalBlue = fn.blue; + p5._strandsSignatures.set('blue', true); augmentFn(fn, p5, 'blue', function (...args) { if (!strandsContext.active) { return originalBlue.apply(this, args); @@ -994,6 +1013,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalAlpha = fn.alpha; + p5._strandsSignatures.set('alpha', true); augmentFn(fn, p5, 'alpha', function (...args) { if (!strandsContext.active) { return originalAlpha.apply(this, args); @@ -1054,6 +1074,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return instance.vec3(h, s, l); }; const originalHue = fn.hue; + p5._strandsSignatures.set('hue', true); augmentFn(fn, p5, 'hue', function (...args) { if (!strandsContext.active) return originalHue.apply(this, args); const colorNode = p5.strandsNode(args[0]); @@ -1061,6 +1082,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalSaturation = fn.saturation; + p5._strandsSignatures.set('saturation', true); augmentFn(fn, p5, 'saturation', function (...args) { if (!strandsContext.active) return originalSaturation.apply(this, args); const colorNode = p5.strandsNode(args[0]); @@ -1068,6 +1090,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalBrightness = fn.brightness; + p5._strandsSignatures.set('brightness', true); augmentFn(fn, p5, 'brightness', function (...args) { if (!strandsContext.active) return originalBrightness.apply(this, args); const colorNode = p5.strandsNode(args[0]); @@ -1075,12 +1098,14 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); const originalLightness = fn.lightness; + p5._strandsSignatures.set('lightness', true); augmentFn(fn, p5, 'lightness', function (...args) { if (!strandsContext.active) return originalLightness.apply(this, args); const colorNode = p5.strandsNode(args[0]); return _rgb2hsl(this, this.vec3(colorNode.x, colorNode.y, colorNode.z)).z; }); + p5._strandsSignatures.set('getTexture', true); augmentFn(fn, p5, 'getTexture', function (...rawArgs) { if (strandsContext.active) { const { id, dimension } = strandsContext.backend.createGetTextureCall( @@ -1096,6 +1121,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Add texture function as alias for getTexture with p5 fallback const originalTexture = fn.texture; + p5._strandsSignatures.set('texture', true); augmentFn(fn, p5, 'texture', function (...args) { if (strandsContext.active) { return this.getTexture(...args); @@ -1112,6 +1138,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalRandomSeed = fn.randomSeed; const originalMillis = fn.millis; + p5._strandsSignatures.set('noiseDetail', true); augmentFn(fn, p5, 'noiseDetail', function (lod, falloff = 0.5) { if (!strandsContext.active) { return originalNoiseDetail.apply(this, arguments); @@ -1121,6 +1148,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext._noiseAmpFalloff = falloff; }); + p5._strandsSignatures.set('noise', true); augmentFn(fn, p5, 'noise', function (...args) { if (!strandsContext.active) { return originalNoise.apply(this, args); // fallback to regular p5.js noise @@ -1249,6 +1277,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { { type: 'Number' } ]; + p5._strandsSignatures.set('randomSeed', true); augmentFn(fn, p5, 'randomSeed', function (seed) { if (!strandsContext.active) { return originalRandomSeed.apply(this, arguments); @@ -1256,6 +1285,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext._randomSeed = seed; }); + p5._strandsSignatures.set('random', true); augmentFn(fn, p5, 'random', function (...args) { if (!strandsContext.active) { return originalRandom.apply(this, args); @@ -1353,6 +1383,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } }); + p5._strandsSignatures.set('randomGaussian', true); augmentFn(fn, p5, 'randomGaussian', function (...args) { if (!strandsContext.active) { return originalRandomGaussian.apply(this, args); @@ -1369,6 +1400,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return z.mult(stdDev).add(mean); }); + p5._strandsSignatures.set('millis', true); augmentFn(fn, p5, 'millis', function (...args) { if (!strandsContext.active) { return originalMillis.apply(this, args); @@ -1417,6 +1449,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { typeAliases.push(pascalTypeName.slice(0, 4)); } } + p5._strandsSignatures.set(`uniform${pascalTypeName}`, true); augmentFn( fn, p5, @@ -1441,6 +1474,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } ); // Shared variables with smart context detection + p5._strandsSignatures.set(`shared${pascalTypeName}`, true); augmentFn(fn, p5, `shared${pascalTypeName}`, function (name) { const shaderName = resolveShaderName( strandsContext, @@ -1464,6 +1498,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }); // Alias varying* as shared* for backward compatibility + p5._strandsSignatures.set(`varying${pascalTypeName}`, true); augmentFn( fn, p5, @@ -1474,11 +1509,15 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { for (const typeAlias of typeAliases) { // For compatibility, also alias uniformVec2 as uniformVector2, what we initially // documented these as + p5._strandsSignatures.set(`uniform${typeAlias}`, true); augmentFn(fn, p5, `uniform${typeAlias}`, fn[`uniform${pascalTypeName}`]); + p5._strandsSignatures.set(`varying${typeAlias}`, true); augmentFn(fn, p5, `varying${typeAlias}`, fn[`varying${pascalTypeName}`]); + p5._strandsSignatures.set(`shared${typeAlias}`, true); augmentFn(fn, p5, `shared${typeAlias}`, fn[`shared${pascalTypeName}`]); } const originalp5Fn = fn[typeInfo.fnName]; + p5._strandsSignatures.set(typeInfo.fnName, true); augmentFn(fn, p5, typeInfo.fnName, function (...args) { if (strandsContext.active) { if ( @@ -1646,6 +1685,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } // Storage buffer uniform function for compute shaders + p5?._strandsSignatures?.set('uniformStorage', true); + p5?._strandsSignatures?.set('p5.uniformStorage', true); fn.uniformStorage = function (name, bufferOrSchema) { const shaderName = resolveShaderName( strandsContext, diff --git a/src/webgl/material.js b/src/webgl/material.js index d7a375d93d..a22328505c 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -742,7 +742,13 @@ function material(p5, fn) { * @returns {p5.Shader} The material shader */ fn.buildFilterShader = function (callback, scope) { - return this.baseFilterShader().modify(callback, scope); + const wasInternal = this._isUserCall; + this._isUserCall = false; + try { + return this.baseFilterShader().modify(callback, scope); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildFilterShader', markExperimental('p5.strands', p5)); @@ -1571,7 +1577,13 @@ function material(p5, fn) { * @returns {p5.Shader} The material shader. */ fn.buildMaterialShader = function (cb, scope) { - return this.baseMaterialShader().modify(cb, scope); + const wasInternal = this._isUserCall; + this._isUserCall = false; // Temporarily un-suppress FES so calls inside cb are validated + try { + return this.baseMaterialShader().modify(cb, scope); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildMaterialShader', markExperimental('p5.strands', p5)); @@ -1790,7 +1802,13 @@ function material(p5, fn) { * @returns {p5.Shader} The normal shader. */ fn.buildNormalShader = function (cb, scope) { - return this.baseNormalShader().modify(cb, scope); + const wasInternal = this._isUserCall; + this._isUserCall = false; + try { + return this.baseNormalShader().modify(cb, scope); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildNormalShader', markExperimental('p5.strands', p5)); @@ -1955,7 +1973,13 @@ function material(p5, fn) { * @returns {p5.Shader} The color shader. */ fn.buildColorShader = function (cb, scope) { - return this.baseColorShader().modify(cb, scope); + const wasInternal = this._isUserCall; + this._isUserCall = false; + try { + return this.baseColorShader().modify(cb, scope); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildColorShader', markExperimental('p5.strands', p5)); @@ -2213,7 +2237,13 @@ function material(p5, fn) { * @returns {p5.Shader} The stroke shader. */ fn.buildStrokeShader = function (cb, scope) { - return this.baseStrokeShader().modify(cb, scope); + const wasInternal = this._isUserCall; + this._isUserCall = false; + try { + return this.baseStrokeShader().modify(cb, scope); + } finally { + this._isUserCall = wasInternal; + } }; p5.registerDecorator('p5.prototype.buildStrokeShader', markExperimental('p5.strands', p5)); diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index be7b112b8c..3f0c839970 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -29,7 +29,9 @@ suite('Validate Params', function () { return 'mock p5.Graphics'; }, _error: () => {}, - registerDecorator: () => {} + registerDecorator: () => {}, + _strandsSignatures: new Map([['p5.map', true]]), + _isStrandsContextActive: false }; const mockP5Prototype = {}; @@ -472,4 +474,19 @@ suite('Validate Params', function () { assert.isFalse(result.success); }); }); + + suite('validateParams: math functions without strands context', function () { + test('map(): fails with string parameters outside strands', function () { + const result = mockP5Prototype._validate('p5.map', ['a', 'b']); + assert.isFalse(result.success); + assert.isTrue(result.error.toString().includes('Expected at least 5 arguments')); + }); + + test('map(): bypasses FES validation when inside strands context', function () { + mockP5._isStrandsContextActive = true; + const result = mockP5Prototype._validate('p5.map', ['a', 'b']); + assert.isTrue(result.success); + mockP5._isStrandsContextActive = false; // reset + }); + }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 9ad13fdce9..9ad5c098a4 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -102,21 +102,21 @@ suite('p5.RendererGL', function () { }); test('shader creation logs a warning', function() { - const shader = myp5.buildMaterialShader(() => {}); + myp5.buildMaterialShader(() => {}); expect(logSpy).toHaveBeenCalled(); expect(logSpy.mock.calls.length).toEqual(1); }); test('warning logs only once with multiple shaders', function() { - const shader = myp5.buildMaterialShader(() => {}); - const shader2 = myp5.buildMaterialShader(() => {}); + myp5.buildMaterialShader(() => {}); + myp5.buildMaterialShader(() => {}); expect(logSpy).toHaveBeenCalled(); expect(logSpy.mock.calls.length).toEqual(1); }); test('warning logs only once with multiple strands calls', function() { - const shader = myp5.buildMaterialShader(() => {}); - const shader2 = myp5.buildFilterShader(() => {}); + myp5.buildMaterialShader(() => {}); + myp5.buildFilterShader(() => {}); expect(logSpy).toHaveBeenCalled(); expect(logSpy.mock.calls.length).toEqual(1); }); @@ -134,7 +134,7 @@ suite('p5.RendererGL', function () { }); test('no warnings are logged', function() { - const shader = myp5.buildMaterialShader(() => {}); + myp5.buildMaterialShader(() => {}); expect(logSpy).not.toHaveBeenCalled(); }); });