// js-interop.test.js - Tests for JavaScript interop functionality import { describe, it, expect } from 'bun:test'; import { createLexer } from '../src/core/lexer.js'; import { createParser } from '../src/core/parser.js'; import { createInterpreter } from '../src/core/interpreter.js'; // Helper function to run Baba Yaga code with JS interop function runBabaCode(code, jsBridgeConfig = {}) { const lexer = createLexer(code); const tokens = lexer.allTokens(); const parser = createParser(tokens); const ast = parser.parse(); const host = { jsBridgeConfig: { allowedFunctions: new Set([ 'JSON.parse', 'JSON.stringify', 'Math.abs', 'Math.floor', 'Math.ceil', 'Math.round', 'Math.min', 'Math.max', 'Math.random', 'console.log', 'console.warn', 'console.error', 'Date.now', 'performance.now', 'testFunction', 'testAsyncFunction', 'testErrorFunction' ]), ...jsBridgeConfig }, io: { out: () => {}, // Silent for tests debug: () => {} } }; // Add test functions to global scope for testing global.testFunction = (x) => x * 2; global.testAsyncFunction = async (x) => Promise.resolve(x + 10); global.testErrorFunction = () => { throw new Error('Test error'); }; // The JS bridge will create its own default sandbox // We'll add test functions to the allowed functions, but let the bridge handle the sandbox const interpreter = createInterpreter(ast, host); interpreter.interpret(); return interpreter.scope.get('result'); } describe('JavaScript Interop - Basic Function Calls', () => { it('should call JavaScript Math.abs function', () => { const code = ` result : io.callJS "Math.abs" [-5]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); expect(result.value.value).toBe(5); }); it('should call JavaScript JSON.parse function', () => { const code = ` jsonStr : "{\\"name\\": \\"Alice\\", \\"age\\": 30}"; result : io.callJS "JSON.parse" [jsonStr]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const parsed = result.value; expect(parsed.type).toBe('JSValue'); expect(parsed.value.name).toBe('Alice'); expect(parsed.value.age).toBe(30); }); it('should call JavaScript JSON.stringify function', () => { const code = ` data : {name: "Bob", age: 25}; jsObj : io.tableToObject data; result : io.callJS "JSON.stringify" [jsObj]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const jsonStr = result.value; expect(jsonStr.type).toBe('JSValue'); expect(typeof jsonStr.value).toBe('string'); expect(jsonStr.value).toContain('Bob'); expect(jsonStr.value).toContain('25'); }); it('should handle function call errors gracefully', () => { const code = ` result : io.callJS "nonexistentFunction" [42]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Err'); const errorMsg = result.value; expect(errorMsg).toContain('not allowed'); }); it('should handle JavaScript errors in called functions', () => { const code = ` result : io.callJS "testErrorFunction" []; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Err'); const errorMsg = result.value; expect(errorMsg).toContain('Test error'); }); }); describe('JavaScript Interop - Property Access', () => { it('should get property from JavaScript object', () => { const code = ` jsObj : io.callJS "JSON.parse" ["{\\"x\\": 42, \\"y\\": 24}"]; result : when jsObj is Ok obj then io.getProperty obj "x" Err msg then Err msg; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); expect(result.value.value).toBe(42); }); it('should handle missing properties gracefully', () => { const code = ` jsObj : io.callJS "JSON.parse" ["{\\"x\\": 42}"]; result : when jsObj is Ok obj then io.getProperty obj "missing" Err msg then Err msg; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); expect(result.value).toBe(null); }); it('should check if property exists', () => { const code = ` jsObj : io.callJS "JSON.parse" ["{\\"name\\": \\"test\\"}"]; hasName : when jsObj is Ok obj then io.hasProperty obj "name" Err _ then false; hasMissing : when jsObj is Ok obj then io.hasProperty obj "missing" Err _ then false; result : {hasName: hasName, hasMissing: hasMissing}; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Object'); expect(result.properties.get('hasName')).toBe(true); expect(result.properties.get('hasMissing')).toBe(false); }); }); describe('JavaScript Interop - Array Conversion', () => { it('should convert JavaScript array to Baba Yaga list', () => { const code = ` jsArray : io.callJS "JSON.parse" ["[1, 2, 3, 4, 5]"]; result : when jsArray is Ok arr then io.jsArrayToList arr Err msg then Err msg; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const list = result.value; expect(Array.isArray(list)).toBe(true); expect(list.length).toBe(5); expect(list[0].value).toBe(1); expect(list[4].value).toBe(5); }); it('should convert Baba Yaga list to JavaScript array', () => { const code = ` babaList : [10, 20, 30]; jsArray : io.listToJSArray babaList; result : io.callJS "JSON.stringify" [jsArray]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const jsonStr = result.value; expect(jsonStr.type).toBe('JSValue'); expect(jsonStr.value).toBe('[10,20,30]'); }); }); describe('JavaScript Interop - Object/Table Conversion', () => { it('should convert Baba Yaga table to JavaScript object', () => { const code = ` babaTable : {name: "Alice", age: 30, active: true}; jsObj : io.tableToObject babaTable; result : io.callJS "JSON.stringify" [jsObj]; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const jsonStr = result.value; expect(jsonStr.type).toBe('JSValue'); const parsed = JSON.parse(jsonStr.value); expect(parsed.name).toBe('Alice'); expect(parsed.age).toBe(30); expect(parsed.active).toBe(true); }); it('should convert JavaScript object to Baba Yaga table', () => { const code = ` jsObj : io.callJS "JSON.parse" ["{\\"x\\": 100, \\"y\\": 200}"]; result : when jsObj is Ok obj then io.objectToTable obj Err msg then Err msg; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const table = result.value; expect(table.type).toBe('Object'); expect(table.properties.get('x').value).toBe(100); expect(table.properties.get('y').value).toBe(200); }); }); describe('JavaScript Interop - Error Handling', () => { it('should track and retrieve last JavaScript error', () => { const code = ` // Cause an error errorResult : io.callJS "testErrorFunction" []; // For now, just test that we can cause an error // The error tracking functions have syntax issues in Baba Yaga result : {errorResult: errorResult}; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Object'); // Error result should be Err const errorResult = result.properties.get('errorResult'); expect(errorResult.type).toBe('Result'); expect(errorResult.variant).toBe('Err'); }); }); describe('JavaScript Interop - Real-world Usage Patterns', () => { it('should implement safe JSON parsing pattern', () => { const code = ` parseJSON : jsonString -> when (validate.type "String" jsonString) is false then Err "Input must be a string" true then when (io.callJS "JSON.parse" [jsonString]) is Ok parsed then Ok (io.objectToTable parsed) Err msg then Err ("JSON parse error: " .. msg); // Test valid JSON validResult : parseJSON "{\\"name\\": \\"Bob\\", \\"age\\": 25}"; // Test invalid JSON invalidResult : parseJSON "invalid json"; result : {valid: validResult, invalid: invalidResult}; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Object'); // Valid result should be Ok const validResult = result.properties.get('valid'); expect(validResult.type).toBe('Result'); expect(validResult.variant).toBe('Ok'); // Invalid result should be Err const invalidResult = result.properties.get('invalid'); expect(invalidResult.type).toBe('Result'); expect(invalidResult.variant).toBe('Err'); }); it('should implement safe mathematical operations', () => { const code = ` // Test each operation individually to avoid curried function issues minResult : io.callJS "Math.min" [10, 5]; maxResult : io.callJS "Math.max" [10, 5]; absResult : io.callJS "Math.abs" [-7]; result : {min: minResult, max: maxResult, abs: absResult}; result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Object'); // All results should be Ok const minResult = result.properties.get('min'); expect(minResult.type).toBe('Result'); expect(minResult.variant).toBe('Ok'); expect(minResult.value.value).toBe(5); const maxResult = result.properties.get('max'); expect(maxResult.type).toBe('Result'); expect(maxResult.variant).toBe('Ok'); expect(maxResult.value.value).toBe(10); const absResult = result.properties.get('abs'); expect(absResult.type).toBe('Result'); expect(absResult.variant).toBe('Ok'); expect(absResult.value.value).toBe(7); }); it('should handle complex nested data structures', () => { const code = ` complexData : { users: [ {name: "Alice", scores: [85, 92, 78]}, {name: "Bob", scores: [90, 87, 95]} ], meta: { total: 2, created: "2024-01-01" } }; // Convert to JS and back jsObj : io.tableToObject complexData; jsonStr : io.callJS "JSON.stringify" [jsObj]; result : when jsonStr is Ok str then when (io.callJS "JSON.parse" [str]) is Ok parsed then io.objectToTable parsed Err msg then Err ("Parse failed: " .. msg) Err msg then Err ("Stringify failed: " .. msg); result; `; const result = runBabaCode(code); expect(result).toBeDefined(); expect(result.type).toBe('Result'); expect(result.variant).toBe('Ok'); const roundTripped = result.value; expect(roundTripped.type).toBe('Object'); expect(roundTripped.properties.has('users')).toBe(true); expect(roundTripped.properties.has('meta')).toBe(true); // Check nested structure integrity const users = roundTripped.properties.get('users'); expect(Array.isArray(users)).toBe(true); expect(users.length).toBe(2); const alice = users[0]; expect(alice.type).toBe('Object'); expect(alice.properties.get('name')).toBe('Alice'); }); }); // Clean up global test functions global.testFunction = undefined; global.testAsyncFunction = undefined; global.testErrorFunction = undefined;