HS test generator: window/document binding + JS function-expr setups

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" <sx>)` 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 11:58:32 +00:00
parent adb06ed1fd
commit 7330bc1a36
2 changed files with 83 additions and 13 deletions

View File

@@ -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 '<a>New Content</a>' 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

View File

@@ -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)))')