Multi-class add/remove, async IO in test runner — 280/831 (34%)

- Parser: add .foo .bar collects multiple class refs into multi-add-class AST
- Compiler: multi-add-class/multi-remove-class emit (do (dom-add-class...) ...)
- Test runner: drives IO suspension chains (wait/fetch/settle) via _driveAsync
  so async HS tests (wait 100ms, settle, fetch) can complete
- Assertion failed: 51→49

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 22:24:16 +00:00
parent cfc7e74a56
commit 5fe97d8481
5 changed files with 106 additions and 14 deletions

View File

@@ -653,6 +653,14 @@
(quote dom-add-class)
(hs-to-sx (nth ast 2))
(nth ast 1)))
((= head (quote multi-add-class))
(let ((target (hs-to-sx (nth ast 1)))
(classes (rest (rest ast))))
(cons (quote do) (map (fn (cls) (list (quote dom-add-class) target cls)) classes))))
((= head (quote multi-remove-class))
(let ((target (hs-to-sx (nth ast 1)))
(classes (rest (rest ast))))
(cons (quote do) (map (fn (cls) (list (quote dom-remove-class) target cls)) classes))))
((= head (quote remove-class))
(list
(quote dom-remove-class)

View File

@@ -666,10 +666,20 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value")))
((cls (get (adv!) "value"))
(extra-classes (list)))
;; Collect additional class refs
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "to" (list (quote me)))))
(list (quote add-class) cls tgt)))
(if (empty? extra-classes)
(list (quote add-class) cls tgt)
(cons (quote multi-add-class) (cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-remove-cmd
@@ -678,10 +688,19 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value")))
((cls (get (adv!) "value"))
(extra-classes (list)))
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "from" (list (quote me)))))
(list (quote remove-class) cls tgt)))
(if (empty? extra-classes)
(list (quote remove-class) cls tgt)
(cons (quote multi-remove-class) (cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-toggle-cmd

View File

@@ -653,6 +653,14 @@
(quote dom-add-class)
(hs-to-sx (nth ast 2))
(nth ast 1)))
((= head (quote multi-add-class))
(let ((target (hs-to-sx (nth ast 1)))
(classes (rest (rest ast))))
(cons (quote do) (map (fn (cls) (list (quote dom-add-class) target cls)) classes))))
((= head (quote multi-remove-class))
(let ((target (hs-to-sx (nth ast 1)))
(classes (rest (rest ast))))
(cons (quote do) (map (fn (cls) (list (quote dom-remove-class) target cls)) classes))))
((= head (quote remove-class))
(list
(quote dom-remove-class)

View File

@@ -666,10 +666,20 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value")))
((cls (get (adv!) "value"))
(extra-classes (list)))
;; Collect additional class refs
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "to" (list (quote me)))))
(list (quote add-class) cls tgt)))
(if (empty? extra-classes)
(list (quote add-class) cls tgt)
(cons (quote multi-add-class) (cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-remove-cmd
@@ -678,10 +688,19 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value")))
((cls (get (adv!) "value"))
(extra-classes (list)))
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "from" (list (quote me)))))
(list (quote remove-class) cls tgt)))
(if (empty? extra-classes)
(list (quote remove-class) cls tgt)
(cons (quote multi-remove-class) (cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-toggle-cmd

View File

@@ -198,16 +198,14 @@ test.describe('Hyperscript behavioral tests', () => {
let result;
try {
result = await Promise.race([
page.evaluate(idx => {
page.evaluate(async (idx) => {
const K = window.SxKernel;
// Thorough cleanup: replace body to kill all event listeners
const newBody = document.createElement('body');
document.documentElement.replaceChild(newBody, document.body);
const thunk = K.eval(`(get (nth _test-registry ${idx}) "thunk")`);
if (!thunk) return { p: false, e: 'no thunk' };
// Capture errors — only from THIS test's execution
let lastErr = null;
const orig = console.error;
console.error = function() {
@@ -215,12 +213,52 @@ test.describe('Hyperscript behavioral tests', () => {
if (m.startsWith('[sx]')) lastErr = m;
orig.apply(console, arguments);
};
// Drive async suspension chains (wait, fetch, etc.)
let pending = 0;
const oldDrive = window._driveAsync;
window._driveAsync = function driveAsync(result) {
if (!result || !result.suspended) return;
pending++;
const req = result.request;
const items = req && (req.items || req);
const op = items && items[0];
const opName = typeof op === 'string' ? op : (op && op.name) || String(op);
const arg = items && items[1];
function doResume(val, delay) {
setTimeout(() => {
try { const r = result.resume(val); pending--; driveAsync(r); }
catch(e) { pending--; }
}, delay);
}
if (opName === 'io-sleep' || opName === 'wait') doResume(null, Math.min(typeof arg === 'number' ? arg : 0, 10));
else if (opName === 'io-fetch') doResume({ok: true, text: ''}, 1);
else if (opName === 'io-settle') doResume(null, 5);
else if (opName === 'io-wait-event') doResume(null, 5);
else pending--;
};
try {
K.callFn(thunk, []);
const r = K.callFn(thunk, []);
// If thunk itself suspended, drive it
if (r && r.suspended) window._driveAsync(r);
// Wait for all pending async chains to settle
if (pending > 0) {
await new Promise(resolve => {
let waited = 0;
const check = () => {
if (pending <= 0 || waited > 2000) resolve();
else { waited += 10; setTimeout(check, 10); }
};
setTimeout(check, 10);
});
}
console.error = orig;
window._driveAsync = oldDrive;
return lastErr ? { p: false, e: lastErr.replace(/[\\"]/g, ' ').slice(0, 150) } : { p: true, e: null };
} catch(e) {
console.error = orig;
window._driveAsync = oldDrive;
return { p: false, e: (e.message || '').replace(/[\\"]/g, ' ').slice(0, 150) };
}
}, i),
@@ -285,9 +323,9 @@ test.describe('Hyperscript behavioral tests', () => {
console.log(` [${info.count}x] ${e}`);
}
// Show samples of "bar" error specifically
const barSamples = results.filter(r => !r.p && (r.e||'').match(/exception: \w+ *$/)).slice(0, 10);
const barSamples = results.filter(r => !r.p && (r.e||'').includes('Assertion failed')).slice(0, 20);
if (barSamples.length > 0) {
console.log(` "bar" error samples (${barSamples.length}):`);
console.log(` Assertion failures (${barSamples.length}):`);
for (const s of barSamples) console.log(` ${s.s}/${s.n}`);
}