Files
rose-ash/test-sx-web/test-signals.js
giles 31a6e708fc
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m0s
more plans
2026-03-09 18:07:23 +00:00

185 lines
7.8 KiB
JavaScript

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'));
}