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

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