const fs = require('fs'); const src = fs.readFileSync('shared/static/scripts/sx-browser.js', 'utf8'); global.window = { addEventListener: function(){}, history: { pushState:function(){}, replaceState:function(){} }, location: { pathname:'/', search:'' } }; global.document = { readyState: 'complete', createElement: function() { return { setAttribute:function(){}, appendChild:function(){}, style:{}, addEventListener:function(){} }; }, createDocumentFragment: function() { return { appendChild:function(){}, childNodes:[] }; }, createTextNode: function(t) { return { textContent: t, nodeType: 3 }; }, createElementNS: function() { return { setAttribute:function(){}, appendChild:function(){} }; }, head: { querySelector:function(){return null;}, appendChild:function(){} }, body: { querySelectorAll:function(){return [];}, querySelector:function(){return null;}, getAttribute:function(){return null;} }, querySelectorAll: function(){return [];}, querySelector: function(){return null;}, addEventListener: function(){}, cookie: '' }; global.navigator = { serviceWorker: { register: function() { return { then: function(f) { return { catch: function(){} }; } }; } } }; global.localStorage = { getItem:function(){return null;}, setItem:function(){}, removeItem:function(){} }; global.CustomEvent = function(n,o){ this.type=n; this.detail=(o||{}).detail; }; global.MutationObserver = function(){ return { observe:function(){} }; }; global.HTMLElement = function(){}; global.EventSource = function(){}; // Prevent module.exports detection so it sets global.Sx var _module = module; module = undefined; var patchedSrc = fs.readFileSync('/tmp/sx-browser-patched.js', 'utf8'); eval(patchedSrc); module = _module; var Sx = global.Sx; console.log('Sx loaded:', Sx ? true : false); var env = Object.create(Sx.componentEnv); // Test 1: computed with SX lambda try { var r = Sx.eval(Sx.parse('(let ((a (signal 3)) (b (computed (fn () (* 2 (deref a)))))) (deref b))')[0], env); console.log('TEST 1 computed:', r, '(expected 6)', r === 6 ? 'PASS' : 'FAIL'); } catch(e) { console.log('TEST 1 computed ERROR:', e.message); } // Test 2: swap! with dec try { var r2 = Sx.eval(Sx.parse('(let ((s (signal 10))) (swap! s dec) (deref s))')[0], env); console.log('TEST 2 swap!:', r2, '(expected 9)', r2 === 9 ? 'PASS' : 'FAIL'); } catch(e) { console.log('TEST 2 swap! ERROR:', e.message); } // Test 3: effect with SX lambda try { var r3 = Sx.eval(Sx.parse('(let ((s (signal 0)) (log (list)) (_e (effect (fn () (append! log (deref s)))))) (reset! s 1) (first log))')[0], env); console.log('TEST 3 effect:', r3, '(expected 0)', r3 === 0 ? 'PASS' : 'FAIL'); } catch(e) { console.log('TEST 3 effect ERROR:', e.message); } // Test 4: effect re-runs on change try { var r4 = Sx.eval(Sx.parse('(let ((s (signal 0)) (log (list)) (_e (effect (fn () (append! log (deref s)))))) (reset! s 5) (last log))')[0], env); console.log('TEST 4 effect re-run:', r4, '(expected 5)', r4 === 5 ? 'PASS' : 'FAIL'); } catch(e) { console.log('TEST 4 effect re-run ERROR:', e.message); } // Test 5: on-click handler lambda try { var r5 = Sx.eval(Sx.parse('(let ((s (signal 0)) (f (fn (e) (swap! s inc)))) (f nil) (f nil) (deref s))')[0], env); console.log('TEST 5 lambda call:', r5, '(expected 2)', r5 === 2 ? 'PASS' : 'FAIL'); } catch(e) { console.log('TEST 5 lambda call ERROR:', e.message); } // Test 6: defisland + renderToDom simulation try { // Override DOM stubs with tracking versions var listenCalls = []; // Override methods on the EXISTING document object (don't replace) document.createElement = function(tag) { var el = { tagName: tag, childNodes: [], style: {}, setAttribute: function(k, v) { this['_attr_'+k] = v; }, getAttribute: function(k) { return this['_attr_'+k]; }, appendChild: function(c) { this.childNodes.push(c); return c; }, addEventListener: function(name, fn) { listenCalls.push({el: this, name: name, fn: fn}); }, removeEventListener: function() {}, textContent: '', nodeType: 1 }; return el; }; document.createTextNode = function(t) { return { textContent: String(t), nodeType: 3 }; }; document.createDocumentFragment = function() { return { childNodes: [], appendChild: function(c) { if (c) this.childNodes.push(c); return c; }, nodeType: 11 }; }; var env2 = Object.create(Sx.componentEnv); // Define island Sx.eval(Sx.parse('(defisland ~test-click (&key initial) (let ((count (signal (or initial 0)))) (div (button :on-click (fn (e) (swap! count inc)) "Click") (span (deref count)))))')[0], env2); // Patch domListen to trace calls // The domListen is inside the IIFE closure, so we can't patch it directly. // Instead, patch addEventListener on elements by wrapping createElement var origCE = document.createElement; document.createElement = function(tag) { var el = origCE(tag); var origAEL = el.addEventListener; el.addEventListener = function(name, fn) { console.log(' addEventListener called:', tag, name); listenCalls.push({el: el, name: name, fn: fn}); origAEL.call(el, name, fn); }; return el; }; // Temporarily hook into isCallable and domListen for debugging // We can't patch the closure vars directly, but we can test via eval var testLambda = Sx.eval(Sx.parse('(fn (e) e)')[0], env2); console.log(' lambda type:', typeof testLambda, testLambda ? testLambda._lambda : 'no _lambda'); console.log(' Sx.isTruthy(lambda):', Sx.isTruthy(testLambda)); // Test what render-dom-element does with on-click // Simpler test: just a button with on-click, no island var parsed = Sx.parse('(button :on-click (fn (e) nil) "test")')[0]; console.log(' parsed expr:', JSON.stringify(parsed, function(k,v) { if (v && v._sym) return 'SYM:' + v.name; if (v && v._kw) return 'KW:' + v.name; return v; })); var simpleTest = Sx.renderToDom(parsed, env2, null); console.log(' simple button rendered:', simpleTest ? simpleTest.tagName : 'null'); console.log(' listeners after simple:', listenCalls.length); // Render it var dom = Sx.renderToDom(Sx.parse('(~test-click :initial 0)')[0], env2, null); console.log('TEST 6 island rendered:', dom ? 'yes' : 'no'); console.log(' listeners attached:', listenCalls.length); if (listenCalls.length > 0) { var clickHandler = listenCalls[0]; console.log(' event name:', clickHandler.name); // Simulate click clickHandler.fn({type: 'click'}); // Find the span's text node var container = dom; // div[data-sx-island] var innerDiv = container.childNodes[0]; // the body div console.log(' container tag:', container.tagName); console.log(' container children:', container.childNodes.length); if (innerDiv && innerDiv.childNodes) { console.log(' innerDiv tag:', innerDiv.tagName); console.log(' innerDiv children:', innerDiv.childNodes.length); var button = innerDiv.childNodes[0]; var span = innerDiv.childNodes[1]; console.log(' button tag:', button ? button.tagName : 'none'); console.log(' span tag:', span ? span.tagName : 'none'); if (span && span.childNodes && span.childNodes[0]) { console.log(' span text BEFORE click effect:', span.childNodes[0].textContent); } } // Click again clickHandler.fn({type: 'click'}); if (innerDiv && innerDiv.childNodes) { var span2 = innerDiv.childNodes[1]; if (span2 && span2.childNodes && span2.childNodes[0]) { console.log(' span text AFTER 2 clicks:', span2.childNodes[0].textContent); console.log(' TEST 6:', span2.childNodes[0].textContent === '2' ? 'PASS' : 'FAIL (expected 2, got ' + span2.childNodes[0].textContent + ')'); } } } else { console.log(' TEST 6: FAIL (no listeners attached)'); } } catch(e) { console.log('TEST 6 island ERROR:', e.message); console.log(e.stack.split('\n').slice(0,5).join('\n')); }