about summary refs log tree commit diff stats
path: root/js/baba-yaga/tests/js-interop.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/tests/js-interop.test.js')
-rw-r--r--js/baba-yaga/tests/js-interop.test.js407
1 files changed, 407 insertions, 0 deletions
diff --git a/js/baba-yaga/tests/js-interop.test.js b/js/baba-yaga/tests/js-interop.test.js
new file mode 100644
index 0000000..77c760a
--- /dev/null
+++ b/js/baba-yaga/tests/js-interop.test.js
@@ -0,0 +1,407 @@
+// 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;