/** * Node Expression Evaluation Tests * * Tests the integration of expression parameters with the Node base class. * Verifies that expressions are evaluated correctly and results are type-coerced. * * @jest-environment jsdom */ /* eslint-env jest */ const Node = require('../src/node'); // Helper to create expression parameter function createExpressionParameter(expression, fallback, version = 1) { return { mode: 'expression', expression, fallback, version }; } describe('Node Expression Evaluation', () => { let mockContext; let node; beforeEach(() => { // Create mock context with Variables mockContext = { updateIteration: 0, nodeIsDirty: jest.fn(), styles: { resolveColor: jest.fn((color) => color) }, editorConnection: { sendWarning: jest.fn(), clearWarning: jest.fn() }, getDefaultValueForInput: jest.fn(() => undefined), Variables: { x: 10, count: 5, isAdmin: true, message: 'Hello' } }; // Create a test node node = new Node(mockContext, 'test-node-1'); node.name = 'TestNode'; node.nodeScope = { componentOwner: { name: 'TestComponent' } }; // Register test inputs with different types node.registerInputs({ numberInput: { type: 'number', default: 0, set: jest.fn() }, stringInput: { type: 'string', default: '', set: jest.fn() }, booleanInput: { type: 'boolean', default: false, set: jest.fn() }, colorInput: { type: 'color', default: '#000000', set: jest.fn() }, anyInput: { type: undefined, default: null, set: jest.fn() } }); }); describe('_evaluateExpressionParameter', () => { describe('Basic evaluation', () => { it('returns simple values as-is', () => { expect(node._evaluateExpressionParameter(42, 'numberInput')).toBe(42); expect(node._evaluateExpressionParameter('hello', 'stringInput')).toBe('hello'); expect(node._evaluateExpressionParameter(true, 'booleanInput')).toBe(true); expect(node._evaluateExpressionParameter(null, 'anyInput')).toBe(null); expect(node._evaluateExpressionParameter(undefined, 'anyInput')).toBe(undefined); }); it('evaluates expression parameters', () => { const expr = createExpressionParameter('10 + 5', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(15); }); it('uses fallback on evaluation error', () => { const expr = createExpressionParameter('undefined.foo', 100); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(100); }); it('uses fallback when no input definition exists', () => { const expr = createExpressionParameter('10 + 5', 999); const result = node._evaluateExpressionParameter(expr, 'nonexistentInput'); expect(result).toBe(999); }); it('coerces result to expected port type', () => { const expr = createExpressionParameter('"42"', 0); // String expression const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(42); // Coerced to number expect(typeof result).toBe('number'); }); }); describe('Type coercion integration', () => { it('coerces string expressions to numbers', () => { const expr = createExpressionParameter('"123"', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(123); }); it('coerces number expressions to strings', () => { const expr = createExpressionParameter('456', ''); const result = node._evaluateExpressionParameter(expr, 'stringInput'); expect(result).toBe('456'); expect(typeof result).toBe('string'); }); it('coerces boolean expressions correctly', () => { const expr = createExpressionParameter('1', false); const result = node._evaluateExpressionParameter(expr, 'booleanInput'); expect(result).toBe(true); }); it('validates color expressions', () => { const expr = createExpressionParameter('"#ff0000"', '#000000'); const result = node._evaluateExpressionParameter(expr, 'colorInput'); expect(result).toBe('#ff0000'); }); it('uses fallback for invalid color expressions', () => { const expr = createExpressionParameter('"not-a-color"', '#000000'); const result = node._evaluateExpressionParameter(expr, 'colorInput'); expect(result).toBe('#000000'); }); }); describe('Error handling', () => { it('handles syntax errors gracefully', () => { const expr = createExpressionParameter('10 +', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(0); // Fallback }); it('handles reference errors gracefully', () => { const expr = createExpressionParameter('unknownVariable', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(0); // Fallback }); it('sends warning to editor on error', () => { const expr = createExpressionParameter('undefined.foo', 0); node._evaluateExpressionParameter(expr, 'numberInput'); expect(mockContext.editorConnection.sendWarning).toHaveBeenCalledWith( 'TestComponent', 'test-node-1', 'expression-error-numberInput', expect.objectContaining({ showGlobally: true, message: expect.stringContaining('Expression error') }) ); }); it('clears warnings on successful evaluation', () => { const expr = createExpressionParameter('10 + 5', 0); node._evaluateExpressionParameter(expr, 'numberInput'); expect(mockContext.editorConnection.clearWarning).toHaveBeenCalledWith( 'TestComponent', 'test-node-1', 'expression-error-numberInput' ); }); }); describe('Context integration', () => { it('has access to Variables', () => { const expr = createExpressionParameter('Variables.x * 2', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(20); // Variables.x = 10, * 2 = 20 }); it('evaluates complex expressions with Variables', () => { const expr = createExpressionParameter('Variables.isAdmin ? "Admin" : "User"', 'User'); const result = node._evaluateExpressionParameter(expr, 'stringInput'); expect(result).toBe('Admin'); // Variables.isAdmin = true }); it('handles arithmetic with Variables', () => { const expr = createExpressionParameter('Variables.count + Variables.x', 0); const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(15); // 5 + 10 = 15 }); }); describe('Edge cases', () => { it('handles undefined fallback', () => { const expr = createExpressionParameter('invalid syntax +', undefined); const result = node._evaluateExpressionParameter(expr, 'anyInput'); expect(result).toBeUndefined(); }); it('handles null expression result', () => { const expr = createExpressionParameter('null', 'fallback'); const result = node._evaluateExpressionParameter(expr, 'stringInput'); expect(result).toBe('null'); // Coerced to string }); it('handles complex object expressions', () => { mockContext.data = { items: [1, 2, 3] }; const expr = createExpressionParameter('data.items.length', 0); node.context = mockContext; const result = node._evaluateExpressionParameter(expr, 'numberInput'); expect(result).toBe(3); }); it('handles empty string expression', () => { const expr = createExpressionParameter('', 'fallback'); const result = node._evaluateExpressionParameter(expr, 'stringInput'); // Empty expression evaluates to undefined, uses fallback expect(result).toBe('fallback'); }); it('handles multi-line expressions', () => { const expr = createExpressionParameter( `Variables.x > 5 ? "Greater" : "Lesser"`, 'Unknown' ); const result = node._evaluateExpressionParameter(expr, 'stringInput'); expect(result).toBe('Greater'); }); }); }); describe('setInputValue with expressions', () => { describe('Integration with input setters', () => { it('evaluates expressions before calling input setter', () => { const expr = createExpressionParameter('Variables.x * 2', 0); node.setInputValue('numberInput', expr); const input = node.getInput('numberInput'); expect(input.set).toHaveBeenCalledWith(20); // Evaluated result }); it('passes simple values directly to setter', () => { node.setInputValue('numberInput', 42); const input = node.getInput('numberInput'); expect(input.set).toHaveBeenCalledWith(42); }); it('stores evaluated value in _inputValues', () => { const expr = createExpressionParameter('Variables.count', 0); node.setInputValue('numberInput', expr); // _inputValues should store the expression, not the evaluated result // (This allows re-evaluation on context changes) expect(node._inputValues['numberInput']).toEqual(expr); }); it('works with string input type', () => { const expr = createExpressionParameter('Variables.message', 'default'); node.setInputValue('stringInput', expr); const input = node.getInput('stringInput'); expect(input.set).toHaveBeenCalledWith('Hello'); }); it('works with boolean input type', () => { const expr = createExpressionParameter('Variables.isAdmin', false); node.setInputValue('booleanInput', expr); const input = node.getInput('booleanInput'); expect(input.set).toHaveBeenCalledWith(true); }); }); describe('Maintains existing behavior', () => { it('maintains existing unit handling', () => { // Set initial value with unit node.setInputValue('numberInput', { value: 10, unit: 'px' }); // Update with unitless value node.setInputValue('numberInput', 20); const input = node.getInput('numberInput'); expect(input.set).toHaveBeenLastCalledWith({ value: 20, unit: 'px' }); }); it('maintains existing color resolution', () => { mockContext.styles.resolveColor = jest.fn((color) => '#resolved'); node.setInputValue('colorInput', '#ff0000'); const input = node.getInput('colorInput'); expect(input.set).toHaveBeenCalledWith('#resolved'); }); it('handles non-existent input gracefully', () => { // Should not throw expect(() => { node.setInputValue('nonexistent', 42); }).not.toThrow(); }); }); describe('Expression evaluation errors', () => { it('uses fallback when expression fails', () => { const expr = createExpressionParameter('undefined.prop', 999); node.setInputValue('numberInput', expr); const input = node.getInput('numberInput'); expect(input.set).toHaveBeenCalledWith(999); // Fallback }); it('sends warning on expression error', () => { const expr = createExpressionParameter('syntax error +', 0); node.setInputValue('numberInput', expr); expect(mockContext.editorConnection.sendWarning).toHaveBeenCalled(); }); }); }); });