import { QuickJSWorker } from '../dist'; import fc from 'fast-check'; describe('Data Serialization & Eval Arguments', () => { let qjs: QuickJSWorker; const hasNegativeZero = (value: unknown): boolean => { if (Object.is(value, -0)) return true; if (Array.isArray(value)) return value.some(hasNegativeZero); if (value && typeof value === 'object') { return Object.values(value as Record).some(hasNegativeZero); } return false; }; beforeEach(() => { qjs = new QuickJSWorker(); }); afterEach(async () => { if (qjs && !qjs.isClosed()) { await qjs.close(); } }); describe('Invariant: Data Round-trip', () => { let qjs: QuickJSWorker; beforeAll(() => { qjs = new QuickJSWorker(); }); afterAll(async () => await qjs.close()); it('should preserve value identity for all JSON-serializable data', async () => { await fc.assert( fc.asyncProperty( fc.jsonValue().filter((data) => !hasNegativeZero(data)), async (data) => { const result = await qjs.eval('(x) => x', { args: [data] }); expect(result).toEqual(data); }), { numRuns: 100 } ); }); }); describe('Round Trip Data Serialization', () => { const identityScript = '(x) => x'; it('should serialize Strings correctly', async () => { const input = "Hello, 🌍!"; const result = await qjs.eval(identityScript, { args: [input] }); expect(result).toBe(input); }); it('should serialize Numbers correctly', async () => { const inputInt = 42; const inputFloat = 3.14159; const resInt = await qjs.eval(identityScript, { args: [inputInt] }); const resFloat = await qjs.eval(identityScript, { args: [inputFloat] }); expect(resInt).toBe(inputInt); expect(resFloat).toBe(inputFloat); }); it('should serialize Booleans correctly', async () => { expect(await qjs.eval(identityScript, { args: [true] })).toBe(true); expect(await qjs.eval(identityScript, { args: [false] })).toBe(false); }); it('should serialize Null correctly', async () => { const result = await qjs.eval(identityScript, { args: [null] }); expect(result).toBeNull(); }); it('should serialize Undefined correctly', async () => { const result = await qjs.eval(identityScript, { args: [undefined] }); expect(result).toBeUndefined(); }); it('should serialize Arrays correctly', async () => { const input = [1, "two", true, null]; const result = await qjs.eval(identityScript, { args: [input] }); expect(result).toEqual(input); }); it('should round-trip numeric arrays through the specialized fast path', async () => { const input = Array.from({ length: 128 }, (_, i) => i + 0.25); const result = await qjs.eval(identityScript, { args: [input] }); expect(result).toEqual(input); }); it('should serialize JSON Objects correctly', async () => { const input = { foo: "bar", nested: { val: 123 } }; const result = await qjs.eval(identityScript, { args: [input] }); expect(result).toEqual(input); }); it('should round-trip flat primitive objects through the specialized fast path', async () => { const input = { foo: "bar", ok: true, count: 123, nil: null, missing: undefined, }; const result = await qjs.eval(identityScript, { args: [input] }); expect(result).toEqual(input); }); it('should serialize Dates correctly', async () => { const input = new Date("2023-01-01T12:00:00Z"); const result = await qjs.eval(identityScript, { args: [input] }); // NOTE: Passed as a direct argument, the native bridge preserves the Date type. // We use Duck Typing check because cross-context instances can confuse 'instanceof' expect(result).not.toBeNull(); expect(typeof result).toBe('object'); expect(typeof (result as Date).toISOString).toBe('function'); expect((result as Date).toISOString()).toBe(input.toISOString()); }); it('should serialize Buffers (Uint8Array) correctly', async () => { const input = Buffer.from([0x01, 0x02, 0xff]); const result = await qjs.eval(identityScript, { args: [input] }); expect(Buffer.isBuffer(result) || result instanceof Uint8Array).toBe(true); expect(Buffer.compare(input, result as Buffer)).toBe(0); }); it('should preserve nested dates and buffers inside complex structures', async () => { const input = { a: [1, 2], b: "string", c: new Date(1000), d: Buffer.from([10]), nested: { ok: true, when: new Date("2024-01-02T03:04:05.000Z"), bytes: new Uint8Array([5, 6, 7]), }, }; const result = await qjs.eval(identityScript, { args: [input] }); expect(result.a).toEqual([1, 2]); expect(result.b).toBe("string"); expect(typeof result.c?.toISOString).toBe('function'); expect(result.c.toISOString()).toBe(input.c.toISOString()); expect(Buffer.from(result.d)).toEqual(input.d); expect(result.nested.ok).toBe(true); expect(typeof result.nested.when?.toISOString).toBe('function'); expect(result.nested.when.toISOString()).toBe(input.nested.when.toISOString()); expect(Buffer.from(result.nested.bytes)).toEqual(Buffer.from([5, 6, 7])); }); }); describe('Function Arguments in Eval', () => { it('should pass multiple arguments in correct order', async () => { const script = '(a, b, c) => [a, b, c]'; const args = [1, "second", true]; const result = await qjs.eval(script, { args }); expect(result).toHaveLength(3); expect(result[0]).toBe(1); expect(result[1]).toBe("second"); expect(result[2]).toBe(true); }); it('should handle empty argument lists', async () => { const script = '() => "called"'; const result = await qjs.eval(script, { args: [] }); expect(result).toBe("called"); }); it('should pass arguments to evalSync as well', () => { const script = '(a, b) => a + b'; const result = qjs.evalSync(script, { args: [10, 20] }); expect(result).toBe(30); }); it('should allow arguments to be modified inside QuickJS without affecting Node original', async () => { const inputObj = { val: 1 }; const script = '(obj) => { obj.val = 999; return obj; }'; const result = await qjs.eval(script, { args: [inputObj] }); // Result from QJS should have new value expect(result.val).toBe(999); // Original Node object should remain untouched (serialization copies data) expect(inputObj.val).toBe(1); }); }); });