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:
@@ -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
|
||||
|
||||
@@ -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)))')
|
||||
|
||||
Reference in New Issue
Block a user