From 7330bc1a363f1f17cde4b1a1f5c92c6311a839df Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 23 Apr 2026 11:58:32 +0000 Subject: [PATCH] HS test generator: window/document binding + JS function-expr setups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related changes for the `evaluate(() => window.X = Y)` setup pattern: 1. extract_window_setups now also matches the single-expression form `evaluate(() => window.X = Y)` (no braces), in addition to the block form `evaluate(() => { window.X = Y; ... })`. 2. js_expr_to_sx now recognises `function(args) { return X; }` (and `function(args) { X; }`) in addition to arrow functions, so e.g. `window.select2 = function(){ return "select2"; }` translates to `(fn () "select2")`. 3. generate_test_chai / generate_test_pw (HTML+click test generators) inject `(host-set! (host-global "window") "X" )` for each window setup found in the test body, so HS code that reads `window.X` sees the right value at activation time. 4. Test-helper preamble now defines `window` and `document` as `(host-global "window")` / `(host-global "document")`, so HS expressions like `window.tmp` resolve through the host instead of erroring on an unbound `window` symbol. Net effect on suites smoke-tested: nominal, because most affected tests hit a separate `if/then/else` parser bug — the `then` keyword inserter in process_hs_val turns multi-line if blocks into ones the HS parser collapses to "always run the body". Fixing that is the next iteration. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/tests/test-hyperscript-behavioral.sx | 41 +++++++++++++++-- tests/playwright/generate-sx-tests.py | 55 ++++++++++++++++++----- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 1cfe27d5..fffc03d1 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -4,6 +4,11 @@ ;; ── Test helpers ────────────────────────────────────────────────── +;; Bind `window` and `document` as plain SX symbols so HS code that +;; references them (e.g. `window.tmp`) can resolve through the host. +(define window (host-global "window")) +(define document (host-global "document")) + (define hs-test-el (fn (tag hs-src) (let ((el (dom-create-element tag))) @@ -320,6 +325,7 @@ (defsuite "hs-upstream-append" (deftest "append preserves existing content rather than overwriting it" (hs-cleanup!) + (host-set! (host-global "window") "clicks" 0) (let ((_el-div (dom-create-element "div")) (_el-btn1 (dom-create-element "button"))) (dom-set-attr _el-div "_" "on click append 'New Content' to me") (dom-set-attr _el-btn1 "id" "btn1") @@ -610,6 +616,7 @@ )) (deftest "install throws when the path resolves to a non-function" (hs-cleanup!) + (host-set! (host-global "window") "NotABehavior" {:hello "world"}) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "install NotABehavior") (dom-append (dom-body) _el-div) @@ -1139,6 +1146,7 @@ )) (deftest "can call functions w/ dollar signs" (hs-cleanup!) + (host-set! (host-global "window") "called" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click call $()") (dom-append (dom-body) _el-div) @@ -1147,6 +1155,7 @@ )) (deftest "can call functions w/ underscores" (hs-cleanup!) + (host-set! (host-global "window") "called" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click call global_function()") (dom-append (dom-body) _el-div) @@ -1155,6 +1164,7 @@ )) (deftest "can call global javascript functions" (hs-cleanup!) + (host-set! (host-global "window") "calledWith" null) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click call globalFunction(\"foo\")") (dom-append (dom-body) _el-div) @@ -1172,6 +1182,7 @@ )) (deftest "can call no argument functions" (hs-cleanup!) + (host-set! (host-global "window") "called" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click call globalFunction()") (dom-append (dom-body) _el-div) @@ -1184,6 +1195,7 @@ (defsuite "hs-upstream-core/api" (deftest "processNodes does not reinitialize a node already processed" (hs-cleanup!) + (host-set! (host-global "window") "global_int" 0) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click set window.global_int to window.global_int + 1") (dom-append (dom-body) _el-div) @@ -1226,6 +1238,7 @@ (defsuite "hs-upstream-core/bootstrap" (deftest "can call functions" (hs-cleanup!) + (host-set! (host-global "window") "calledWith" null) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click call globalFunction(\"foo\")") (dom-append (dom-body) _el-div) @@ -1963,6 +1976,7 @@ )) (deftest "can invoke functions w/ numbers in name" (hs-cleanup!) + (host-set! (host-global "window") "select2" (fn () "select2")) (let ((_el-button (dom-create-element "button"))) (dom-set-attr _el-button "_" "on click put select2() into me") (dom-append (dom-body) _el-button) @@ -2354,6 +2368,7 @@ )) (deftest "set favors local variables over global variables" (hs-cleanup!) + (host-set! (host-global "window") "foo" 12) (let ((_el-d1 (dom-create-element "div"))) (dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "_" "on click 1 set foo to 20 then set @out to foo") @@ -5207,7 +5222,7 @@ (assert= (eval-hs-locals "getObj().greet()" (list (list (quote getObj) (fn () {:greet (fn () "hi")})))) "hi") ) (deftest "can invoke function on object" - (assert= (eval-hs-locals "obj.getValue()" (list (list (quote obj) {:value "foo" :getValue "function () { return this.value }"}))) "foo") + (assert= (eval-hs-locals "obj.getValue()" (list (list (quote obj) {:value "foo" :getValue (fn () (host-get this "value"))}))) "foo") ) (deftest "can invoke function on object w/ async arg" (error "SKIP (untranslated): can invoke function on object w/ async arg")) @@ -5229,6 +5244,7 @@ ) (deftest "can pass multiple arguments" (hs-cleanup!) + (host-set! (host-global "window") "add" (fn (a b c) (+ a b c))) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click put add(1, 2, 3) into me") (dom-append (dom-body) _el-div) @@ -6376,10 +6392,10 @@ (assert= (eval-hs "`${1 + 2}`") "3") ) (deftest "string templates work w/ props" - (assert= (eval-hs "`$window.foo`") "foo") + (assert= (eval-hs-locals "`$window.foo`" (list (list (quote foo) "foo"))) "foo") ) (deftest "string templates work w/ props w/ braces" - (assert= (eval-hs "`${window.foo}`") "foo") + (assert= (eval-hs-locals "`${window.foo}`" (list (list (quote foo) "foo"))) "foo") ) ) @@ -7247,6 +7263,7 @@ (error "SKIP (skip-list): don't throw passes through 404 response")) (deftest "submits the fetch parameters to the event handler" (hs-cleanup!) + (host-set! (host-global "window") "headerCheckPassed" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click fetch \"/test\" {headers: {\"X-CustomHeader\": \"foo\"}} then put it into my.innerHTML end") (dom-append (dom-body) _el-div) @@ -7703,6 +7720,8 @@ )) (deftest "if on new line does not join w/ else" (hs-cleanup!) + (host-set! (host-global "window") "tmp" false) + (host-set! (host-global "window") "tmp" true) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click if window.tmp then else then if window.tmp then end put \"foo\" into me then end") (dom-append (dom-body) _el-div) @@ -7723,6 +7742,8 @@ )) (deftest "if properly supports nested if statements and end block" (hs-cleanup!) + (host-set! (host-global "window") "tmp" false) + (host-set! (host-global "window") "tmp" true) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click if window.tmp then put \"foo\" into me then else if not window.tmp then // do nothing then end catch e then // just here for the parsing... then") (dom-append (dom-body) _el-div) @@ -8155,6 +8176,7 @@ (defsuite "hs-upstream-js" (deftest "can access values from _hyperscript" (hs-cleanup!) + (host-set! (host-global "window") "testSuccess" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click set t to true then js(t) window.testSuccess = t end") (dom-append (dom-body) _el-div) @@ -8163,6 +8185,7 @@ )) (deftest "can deal with empty input list" (hs-cleanup!) + (host-set! (host-global "window") "testSuccess" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click js() window.testSuccess = true end") (dom-append (dom-body) _el-div) @@ -8207,6 +8230,7 @@ )) (deftest "can run js" (hs-cleanup!) + (host-set! (host-global "window") "testSuccess" false) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click js window.testSuccess = true end") (dom-append (dom-body) _el-div) @@ -8215,6 +8239,7 @@ )) (deftest "can run js at the top level" (hs-cleanup!) + (host-set! (host-global "window") "testSuccess" false) (let ((_el-script (dom-create-element "script"))) (dom-set-attr _el-script "type" "text/hyperscript") (dom-append (dom-body) _el-script) @@ -8630,6 +8655,7 @@ )) (deftest "morph preserves element identity" (hs-cleanup!) + (host-set! (host-global "window") "_savedRef" (host-call document "querySelector" "#target")) (let ((_el-target (dom-create-element "div")) (_el-go (dom-create-element "button"))) (dom-set-attr _el-target "id" "target") (dom-set-inner-html _el-target "old") @@ -8644,6 +8670,7 @@ )) (deftest "morph preserves matched child identity" (hs-cleanup!) + (host-set! (host-global "window") "_savedChild" (host-call document "querySelector" "#child")) (let ((_el-target (dom-create-element "div")) (_el-child (dom-create-element "div")) (_el-go (dom-create-element "button"))) (dom-set-attr _el-target "id" "target") (dom-set-attr _el-child "id" "child") @@ -9262,6 +9289,7 @@ )) (deftest "Can use functions defined outside of the current element" (hs-cleanup!) + (host-set! (host-global "window") "foo" (fn () "foo")) (let ((_el-d1 (dom-create-element "div"))) (dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "_" "on click foo() then put result into my bar") @@ -9271,6 +9299,7 @@ )) (deftest "Can use indirect functions with a function root" (hs-cleanup!) + (host-set! (host-global "window") "bar" (fn () {:foo (fn () "foo")})) (let ((_el-d1 (dom-create-element "div"))) (dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "_" "on click bar().foo() then put the result into my bar") @@ -9280,6 +9309,7 @@ )) (deftest "Can use indirect functions with a symbol root" (hs-cleanup!) + (host-set! (host-global "window") "bar" {:foo (fn () "foo")}) (let ((_el-d1 (dom-create-element "div"))) (dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "_" "on click bar.foo() then put the result into my bar") @@ -9289,6 +9319,7 @@ )) (deftest "Can use nested indirect functions with a symbol root" (hs-cleanup!) + (host-set! (host-global "window") "bar" (fn () {:foo (fn () "foo")})) (let ((_el-d1 (dom-create-element "div"))) (dom-set-attr _el-d1 "id" "d1") (dom-set-attr _el-d1 "_" "on click window.bar().foo() then put the result into my bar") @@ -10943,6 +10974,7 @@ )) (deftest "can set many properties at once with object literal" (hs-cleanup!) + (host-set! (host-global "window") "obj" {:foo 1}) (let ((_el-div (dom-create-element "div"))) (dom-set-attr _el-div "_" "on click set {bar: 2, baz: 3} on obj") (dom-append (dom-body) _el-div) @@ -11780,6 +11812,9 @@ )) (deftest "async expressions in a loop resolve correctly" (hs-cleanup!) + (host-set! (host-global "window") "asyncFn" (fn (v) (host-call Promise "resolve" "got:\" + v) + const tmpl = document.querySelector('#work-area script[type=\"text/hyperscript-template\"]') + return _hyperscript(\"render tmpl with items: items, asyncFn: asyncFn then put it into window.res" {:locals "{ items: [1, 2, 3], asyncFn: window.asyncFn, tmpl }"}))) (let ((_el-script (dom-create-element "script"))) (dom-set-attr _el-script "type" "text/hyperscript-template") (dom-set-inner-html _el-script "#for x in items diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 168e4da6..92868c30 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -910,6 +910,11 @@ def generate_test_chai(test, elements, var_names, idx): lines.append(f' (deftest "{sx_name(test["name"])}"') lines.append(' (hs-cleanup!)') + # `evaluate(() => window.X = Y)` setups in the test body — inject as + # globals before activation so HS code can read them. + for name, sx_val in extract_window_setups(test.get('body', '') or ''): + lines.append(f' (host-set! (host-global "window") "{name}" {sx_val})') + # Compile HS script blocks as setup (def functions etc.) for script in hs_scripts: clean = clean_hs_script(script) @@ -942,6 +947,10 @@ def generate_test_pw(test, elements, var_names, idx): lines.append(f' (deftest "{sx_name(test["name"])}"') lines.append(' (hs-cleanup!)') + # `evaluate(() => window.X = Y)` setups — see generate_test_chai. + for name, sx_val in extract_window_setups(test.get('body', '') or ''): + lines.append(f' (host-set! (host-global "window") "{name}" {sx_val})') + bindings = [f'({var_names[i]} (dom-create-element "{el["tag"]}"))' for i, el in enumerate(elements)] lines.append(f' (let ({" ".join(bindings)})') @@ -1062,6 +1071,19 @@ def js_expr_to_sx(expr): return None return f'(fn ({" ".join(params)}) {body_sx})' + # function-expression form: `function(args) { return X; }` (or `{ X; }`). + fm = re.match( + r'^function\s*\(([^)]*)\)\s*\{\s*(?:return\s+)?(.+?)\s*;?\s*\}\s*$', + expr, re.DOTALL, + ) + if fm: + args_str = fm.group(1).strip() + params = [a.strip() for a in args_str.split(',') if a.strip()] if args_str else [] + body_sx = js_expr_to_sx(fm.group(2).strip()) + if body_sx is None: + return None + return f'(fn ({" ".join(params)}) {body_sx})' + # Balanced outer parens unwrap (after arrow check, so `(x)` alone works). if expr.startswith('(') and expr.endswith(')'): depth = 0 @@ -1153,15 +1175,16 @@ def js_expr_to_sx(expr): def extract_window_setups(body): - """Find `evaluate(() => { window.NAME = VALUE; ... })` blocks and return - a list of (name, sx_value) pairs. Skips assignments we can't translate. + """Find `evaluate(() => { window.NAME = VALUE; ... })` (block form) and + `evaluate(() => window.NAME = VALUE)` (single-expression form) and + return a list of (name, sx_value) pairs. Skips assignments we can't + translate. """ setups = [] - # Each evaluate body may contain multiple `window.X = Y` (semicolon-separated). - # Match the inner braces of evaluate(() => { ... }), with balanced braces. + + # Block form: evaluate(() => { window.X = Y; ... }) for em in re.finditer(r'evaluate\(\s*\(\)\s*=>\s*\{', body): start = em.end() - # Find matching close brace. depth, i, in_str = 1, start, None while i < len(body) and depth > 0: ch = body[i] @@ -1177,16 +1200,23 @@ def extract_window_setups(body): i += 1 if depth != 0: continue - inner = body[start:i - 1] - # Split on top-level semicolons. - for stmt in split_top_level_chars(inner, ';'): + for stmt in split_top_level_chars(body[start:i - 1], ';'): sm = re.match(r'\s*window\.(\w+)\s*=\s*(.+?)\s*$', stmt, re.DOTALL) if not sm: continue - name = sm.group(1) sx_val = js_expr_to_sx(sm.group(2).strip()) if sx_val is not None: - setups.append((name, sx_val)) + setups.append((sm.group(1), sx_val)) + + # Single-expression form: evaluate(() => window.X = Y) — no braces. + for em in re.finditer( + r'evaluate\(\s*\(\)\s*=>\s*window\.(\w+)\s*=\s*([^)]+?)\)', + body, re.DOTALL, + ): + sx_val = js_expr_to_sx(em.group(2).strip()) + if sx_val is not None: + setups.append((em.group(1), sx_val)) + return setups @@ -1835,6 +1865,11 @@ output.append(';; DO NOT EDIT — regenerate with: python3 tests/playwright/gene output.append('') output.append(';; ── Test helpers ──────────────────────────────────────────────────') output.append('') +output.append(';; Bind `window` and `document` as plain SX symbols so HS code that') +output.append(';; references them (e.g. `window.tmp`) can resolve through the host.') +output.append('(define window (host-global "window"))') +output.append('(define document (host-global "document"))') +output.append('') output.append('(define hs-test-el') output.append(' (fn (tag hs-src)') output.append(' (let ((el (dom-create-element tag)))')