mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
new code editor
This commit is contained in:
345
packages/noodl-runtime/test/node-expression-evaluation.test.js
Normal file
345
packages/noodl-runtime/test/node-expression-evaluation.test.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user