// @vitest-environment jsdom import * as Helpers from './helpers'; import type { INodeExecutionData } from '../src/interfaces'; import { Workflow } from '../src/workflow'; // Engine-parity tests for behaviour of `$json` arrays beyond plain indexed access. describe('Expression — array proxy semantics (engine parity)', () => { const workflow = new Workflow({ id: '1', nodes: [ { name: 'node', typeVersion: 1, type: 'test.set', id: 'uuid-1234', position: [0, 0], parameters: {}, }, ], connections: {}, active: false, nodeTypes: Helpers.NodeTypes(), }); const expression = workflow.expression; beforeAll(async () => { await expression.acquireIsolate(); }); afterAll(async () => { await expression.releaseIsolate(); }); const evaluate = (value: string, json: unknown) => { const data: INodeExecutionData[] = [{ json: json as INodeExecutionData['json'] }]; return expression.getParameterValue(value, null, 0, 0, 'node', data, 'manual', {}); }; // Both engines reject property-descriptor access from inside an expression: // `getOwnPropertyDescriptor` is on the sanitizer's blocklist, so the // expression is rejected before evaluation. Documented so a future // divergence is caught; neither engine intends to expose the data this way. it('Object.getOwnPropertyDescriptor on $json properties is not exposed via expressions', () => { expect(() => evaluate('={{ Object.getOwnPropertyDescriptor($json.arr, "0") }}', { arr: [10, 20, 30] }), ).toThrow(/due to security concerns/); }); it('spread syntax materialises the array via Symbol.iterator', () => { expect(evaluate('={{ [...$json.arr] }}', { arr: [10, 20, 30] })).toEqual([10, 20, 30]); }); it('for…of iterates the array elements', () => { const expr = '={{ (() => { const out = []; for (const x of $json.arr) out.push(x); return out; })() }}'; expect(evaluate(expr, { arr: [10, 20, 30] })).toEqual([10, 20, 30]); }); it('toString returns the canonical comma-joined string (matches native Array)', () => { expect(evaluate('={{ $json.arr.toString() }}', { arr: [10, 20, 30] })).toBe('10,20,30'); }); it('implicit string coercion uses Array.prototype.toString', () => { expect(evaluate('={{ "items: " + $json.arr }}', { arr: [1, 2, 3] })).toBe('items: 1,2,3'); }); it('reverse returns a reversed copy without mutating a subsequent sibling read', () => { expect(evaluate('={{ [$json.arr.reverse(), $json.arr] }}', { arr: [1, 2, 3] })).toEqual([ [3, 2, 1], [1, 2, 3], ]); }); it('reverse does not mutate a preceding sibling read', () => { expect(evaluate('={{ [$json.arr, $json.arr.reverse()] }}', { arr: [1, 2, 3] })).toEqual([ [1, 2, 3], [3, 2, 1], ]); }); describe('mutating array methods return a mutated copy', () => { it('sort() returns the sorted array', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi', 'Orange'] }; expect(evaluate('={{ $json.arr.sort() }}', json)).toEqual([ 'Apple', 'Kiwi', 'Mango', 'Orange', ]); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi', 'Orange']); }); it('sort() forwards the comparator', () => { const json = { arr: [3, 1, 10, 2] }; expect(evaluate('={{ $json.arr.sort((a, b) => b - a) }}', json)).toEqual([10, 3, 2, 1]); expect(json.arr).toEqual([3, 1, 10, 2]); }); it('splice() returns the removed elements', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi', 'Orange'] }; expect(evaluate('={{ $json.arr.splice(0, 2) }}', json)).toEqual(['Mango', 'Apple']); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi', 'Orange']); }); it('fill() returns the filled array', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi', 'Orange'] }; expect(evaluate('={{ $json.arr.fill("X", 0, 2) }}', json)).toEqual([ 'X', 'X', 'Kiwi', 'Orange', ]); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi', 'Orange']); }); it('shift() returns the removed first element', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi'] }; expect(evaluate('={{ $json.arr.shift() }}', json)).toBe('Mango'); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi']); }); it('unshift() returns the new length', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi'] }; expect(evaluate('={{ $json.arr.unshift("Peach", "Grape") }}', json)).toBe(5); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi']); }); it('copyWithin() returns the copied-within array', () => { const json = { arr: ['Mango', 'Apple', 'Kiwi', 'Orange'] }; expect(evaluate('={{ $json.arr.copyWithin(0, 2, 4) }}', json)).toEqual([ 'Kiwi', 'Orange', 'Kiwi', 'Orange', ]); expect(json.arr).toEqual(['Mango', 'Apple', 'Kiwi', 'Orange']); }); }); });