HS test generator: fix toHaveCSS, locals, and \"-escapes — +28 tests
Generator changes (tests/playwright/generate-sx-tests.py):
- toHaveCSS regex: balance parens so `'rgb(255, 0, 0)'` is captured intact
(was truncating at first `)`)
- Map browser-computed colors `rgb(R,G,B)` back to CSS keywords
(red/green/blue/black/white) — our DOM mock returns the inline value
- js_val_to_sx now handles object literals `{a: 1, b: {c: 2}}` → `{:a 1 :b {:c 2}}`
- Pattern 2 (`var x = await run(...)`) now captures locals via balanced-brace
scan and emits `eval-hs-locals` instead of `eval-hs`
- Pattern 1 with locals: emit `eval-hs-locals` (was wrapping in `let`, which
doesn't reach the inner HS env)
- Stop collapsing `\"` → `"` in raw HTML (line 218): the backslash escapes
are legitimate in single-quoted `_='...'` HS attribute values containing
nested HS scripts
Test-framework changes (regenerated into spec/tests/test-hyperscript-behavioral.sx):
- `_hs-wrap-body`: returns expression value if non-nil, else `it`. Lets bare
expressions (`foo.foo`) and `it`-mutating scripts (`pick first 3 of arr;
set $test to it`) both round-trip through the same wrapper
- `eval-hs-locals` now injects locals via `(let ((name (quote val)) ...) sx)`
rather than `apply handler (cons nil vals)` — works around a JIT loop on
some compiled forms (e.g. `bar.doh of foo` with undefined `bar`)
Also synced lib/hyperscript/*.sx → shared/static/wasm/sx/hs-*.sx (the WASM
test runner reads from the wasm/sx/ copies).
Net per-cluster pass counts (vs prior baseline):
- put: 23 → 29 (+6)
- set: 21 → 28 (+7)
- show: 7 → 15 (+8)
- expressions/propertyAccess: 3 → 9 (+6)
- expressions/possessiveExpression: 17 → 18 (+1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,8 +214,10 @@ def parse_html(html):
|
||||
# Remove | separators
|
||||
html = html.replace(' | ', '')
|
||||
|
||||
# Fix escaped attribute delimiters from JSON extraction (\" → ")
|
||||
html = html.replace('\\"', '"')
|
||||
# Note: previously we collapsed `\"` → `"` here, but that destroys legitimate
|
||||
# HS string escapes inside single-quoted `_='...'` attributes (e.g. nested
|
||||
# button HTML in `properly processes hyperscript X` tests). HTMLParser handles
|
||||
# backslashes in attribute values as literal characters, so we leave them.
|
||||
|
||||
elements = []
|
||||
stack = []
|
||||
@@ -680,6 +682,17 @@ def pw_assertion_to_sx(target, negated, assert_type, args_str):
|
||||
elif assert_type == 'toHaveCSS':
|
||||
prop = args[0] if args else ''
|
||||
val = args[1] if len(args) >= 2 else ''
|
||||
# Browsers normalize colors to rgb()/rgba(); our DOM mock returns the
|
||||
# raw inline value. Map common rgb() forms back to keywords.
|
||||
rgb_to_name = {
|
||||
'rgb(255, 0, 0)': 'red',
|
||||
'rgb(0, 255, 0)': 'green',
|
||||
'rgb(0, 0, 255)': 'blue',
|
||||
'rgb(0, 0, 0)': 'black',
|
||||
'rgb(255, 255, 255)': 'white',
|
||||
}
|
||||
if val in rgb_to_name:
|
||||
val = rgb_to_name[val]
|
||||
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
|
||||
if negated:
|
||||
return f'(assert (!= (dom-get-style {target} "{prop}") "{escaped}"))'
|
||||
@@ -764,7 +777,7 @@ def parse_dev_body(body, elements, var_names):
|
||||
m = re.search(
|
||||
r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\)\.(not\.)?"
|
||||
r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)"
|
||||
r"\(([^)]*)\)",
|
||||
r"\(((?:[^()]|\([^()]*\))*)\)",
|
||||
line
|
||||
)
|
||||
if m:
|
||||
@@ -956,6 +969,24 @@ def js_val_to_sx(val):
|
||||
return '(list)'
|
||||
items = [js_val_to_sx(x.strip()) for x in split_top_level(inner)]
|
||||
return '(list ' + ' '.join(items) + ')'
|
||||
# Objects: { foo: "bar", baz: 1 } → {:foo "bar" :baz 1}
|
||||
if val.startswith('{') and val.endswith('}'):
|
||||
inner = val[1:-1].strip()
|
||||
if not inner:
|
||||
return '{}'
|
||||
parts = []
|
||||
for kv in split_top_level(inner):
|
||||
kv = kv.strip()
|
||||
if not kv:
|
||||
continue
|
||||
# key: value (key is identifier or quoted string)
|
||||
m = re.match(r'^(?:"([^"]+)"|\'([^\']+)\'|(\w+))\s*:\s*(.+)$', kv, re.DOTALL)
|
||||
if not m:
|
||||
return f'"{val}"'
|
||||
key = m.group(1) or m.group(2) or m.group(3)
|
||||
v = js_val_to_sx(m.group(4))
|
||||
parts.append(f':{key} {v}')
|
||||
return '{' + ' '.join(parts) + '}'
|
||||
try:
|
||||
float(val)
|
||||
return val
|
||||
@@ -1044,12 +1075,13 @@ def generate_eval_only_test(test, idx):
|
||||
me_match = re.search(r'\bme:\s*(\d+)', opts_str)
|
||||
locals_match = re.search(r'locals:\s*\{([^}]+)\}', opts_str)
|
||||
if locals_match:
|
||||
local_bindings = []
|
||||
local_pairs = []
|
||||
for lm in re.finditer(r'(\w+)\s*:\s*([^,}]+)', locals_match.group(1)):
|
||||
lname = lm.group(1)
|
||||
lval = js_val_to_sx(lm.group(2).strip())
|
||||
local_bindings.append(f'({lname} {lval})')
|
||||
assertions.append(f' (let ({" ".join(local_bindings)}) (assert= (eval-hs "{hs_expr}") {expected_sx}))')
|
||||
local_pairs.append((lname, lval))
|
||||
locals_sx = '(list ' + ' '.join(f'(list (quote {n}) {v})' for n, v in local_pairs) + ')' if local_pairs else '(list)'
|
||||
assertions.append(f' (assert= (eval-hs-locals "{hs_expr}" {locals_sx}) {expected_sx})')
|
||||
elif me_match:
|
||||
me_val = me_match.group(1)
|
||||
assertions.append(f' (assert= (eval-hs-with-me "{hs_expr}" {me_val}) {expected_sx})')
|
||||
@@ -1088,6 +1120,43 @@ def generate_eval_only_test(test, idx):
|
||||
if run_match:
|
||||
hs_expr = extract_hs_expr(run_match.group(2))
|
||||
var_name = re.search(r'(?:var|let|const)\s+(\w+)', body).group(1)
|
||||
# Capture locals from the run() call, if present. Use balanced-brace
|
||||
# extraction so nested {a: {b: 1}} doesn't truncate at the inner }.
|
||||
local_pairs = []
|
||||
locals_idx = body.find('locals:')
|
||||
if locals_idx >= 0:
|
||||
# Find the opening { after "locals:"
|
||||
open_idx = body.find('{', locals_idx)
|
||||
if open_idx >= 0:
|
||||
depth = 0
|
||||
end_idx = -1
|
||||
in_str = None
|
||||
for i in range(open_idx, len(body)):
|
||||
ch = body[i]
|
||||
if in_str:
|
||||
if ch == in_str and body[i-1] != '\\':
|
||||
in_str = None
|
||||
continue
|
||||
if ch in ('"', "'", '`'):
|
||||
in_str = ch
|
||||
continue
|
||||
if ch == '{':
|
||||
depth += 1
|
||||
elif ch == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
end_idx = i
|
||||
break
|
||||
if end_idx > open_idx:
|
||||
locals_str = body[open_idx + 1:end_idx].strip()
|
||||
for kv in split_top_level(locals_str):
|
||||
kv = kv.strip()
|
||||
m = re.match(r'^(\w+)\s*:\s*(.+)$', kv, re.DOTALL)
|
||||
if m:
|
||||
local_pairs.append((m.group(1), js_val_to_sx(m.group(2).strip())))
|
||||
locals_sx = '(list ' + ' '.join(f'(list (quote {n}) {v})' for n, v in local_pairs) + ')' if local_pairs else None
|
||||
def eval_call(expr):
|
||||
return f'(eval-hs-locals "{expr}" {locals_sx})' if locals_sx else f'(eval-hs "{expr}")'
|
||||
for m in re.finditer(r'expect\((' + re.escape(var_name) + r'(?:\["[^"]+"\]|\.\w+)?)\)\.toBe\(([^)]+)\)', body):
|
||||
accessor = m.group(1)
|
||||
expected_sx = js_val_to_sx(m.group(2))
|
||||
@@ -1095,17 +1164,17 @@ def generate_eval_only_test(test, idx):
|
||||
prop_m = re.search(r'\["([^"]+)"\]|\.(\w+)', accessor[len(var_name):])
|
||||
if prop_m:
|
||||
prop = prop_m.group(1) or prop_m.group(2)
|
||||
assertions.append(f' (assert= (host-get (eval-hs "{hs_expr}") "{prop}") {expected_sx})')
|
||||
assertions.append(f' (assert= (host-get {eval_call(hs_expr)} "{prop}") {expected_sx})')
|
||||
else:
|
||||
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
|
||||
assertions.append(f' (assert= {eval_call(hs_expr)} {expected_sx})')
|
||||
for m in re.finditer(r'expect\(' + re.escape(var_name) + r'(?:\.\w+)?\)\.toEqual\((\[.*?\])\)', body, re.DOTALL):
|
||||
expected_sx = js_val_to_sx(m.group(1))
|
||||
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
|
||||
assertions.append(f' (assert= {eval_call(hs_expr)} {expected_sx})')
|
||||
# Handle .map(x => x.prop) before toEqual
|
||||
for m in re.finditer(r'expect\(' + re.escape(var_name) + r'\.map\(\w+\s*=>\s*\w+\.(\w+)\)\)\.toEqual\((\[.*?\])\)', body, re.DOTALL):
|
||||
prop = m.group(1)
|
||||
expected_sx = js_val_to_sx(m.group(2))
|
||||
assertions.append(f' (assert= (map (fn (x) (get x "{prop}")) (eval-hs "{hs_expr}")) {expected_sx})')
|
||||
assertions.append(f' (assert= (map (fn (x) (get x "{prop}")) {eval_call(hs_expr)}) {expected_sx})')
|
||||
|
||||
# Pattern 2b: run() with locals + evaluate(window.X) + expect().toBe/toEqual
|
||||
# e.g.: await run(`expr`, {locals: {arr: [1,2,3]}});
|
||||
@@ -1480,13 +1549,24 @@ output.append('(define hs-cleanup!')
|
||||
output.append(' (fn ()')
|
||||
output.append(' (dom-set-inner-html (dom-body) "")))')
|
||||
output.append('')
|
||||
output.append(';; Evaluate a hyperscript expression and return the last-expression value.')
|
||||
output.append(';; Compiles the expression, wraps in a thunk, evaluates, returns result.')
|
||||
output.append(';; Evaluate a hyperscript expression and return either the expression')
|
||||
output.append(';; value or `it` (whichever is non-nil). Multi-statement scripts that')
|
||||
output.append(';; mutate `it` (e.g. `pick first 3 of arr; set $test to it`) get `it` back;')
|
||||
output.append(';; bare expressions (e.g. `foo.foo`) get the expression value back.')
|
||||
output.append('(define _hs-wrap-body')
|
||||
output.append(' (fn (sx)')
|
||||
output.append(' (list (quote let)')
|
||||
output.append(' (list (list (quote it) nil) (list (quote event) nil))')
|
||||
output.append(' (list (quote let)')
|
||||
output.append(' (list (list (quote _ret) sx))')
|
||||
output.append(' (list (quote if) (list (quote nil?) (quote _ret)) (quote it) (quote _ret))))))')
|
||||
output.append('')
|
||||
output.append('(define eval-hs')
|
||||
output.append(' (fn (src)')
|
||||
output.append(' (let ((sx (hs-to-sx (hs-compile src))))')
|
||||
output.append(' (let ((handler (eval-expr-cek')
|
||||
output.append(' (list (quote fn) (list (quote me)) (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))')
|
||||
output.append(' (list (quote fn) (list (quote me))')
|
||||
output.append(' (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))')
|
||||
output.append(' (guard')
|
||||
output.append(' (_e')
|
||||
output.append(' (true')
|
||||
@@ -1497,18 +1577,16 @@ output.append(' (raise _e))))')
|
||||
output.append(' (handler nil))))))')
|
||||
output.append('')
|
||||
output.append(';; Evaluate a hyperscript expression with locals. bindings = list of (symbol value).')
|
||||
output.append(';; The locals are injected as fn params so they resolve in the handler body.')
|
||||
output.append(';; Locals are injected as a `let` wrapping the compiled body, then evaluated')
|
||||
output.append(';; in a fresh CEK env. Avoids `apply` (whose JIT path can loop on some forms).')
|
||||
output.append('(define eval-hs-locals')
|
||||
output.append(' (fn (src bindings)')
|
||||
output.append(' (let ((sx (hs-to-sx (hs-compile src))))')
|
||||
output.append(' (let ((names (map (fn (b) (first b)) bindings))')
|
||||
output.append(' (vals (map (fn (b) (nth b 1)) bindings)))')
|
||||
output.append(' (let ((param-list (cons (quote me) names)))')
|
||||
output.append(' (let ((wrapper (list (quote fn) param-list')
|
||||
output.append(' (list (quote let)')
|
||||
output.append(' (list (list (quote it) nil) (list (quote event) nil))')
|
||||
output.append(' sx (quote it)))))')
|
||||
output.append(' (let ((handler (eval-expr-cek wrapper)))')
|
||||
output.append(' ;; Build (let ((name1 (quote val1)) ...) <wrap-body>)')
|
||||
output.append(' (let ((let-binds (map (fn (b) (list (first b) (list (quote quote) (nth b 1)))) bindings)))')
|
||||
output.append(' (let ((wrapped (list (quote let) let-binds (_hs-wrap-body sx))))')
|
||||
output.append(' (let ((thunk (list (quote fn) (list (quote me)) wrapped)))')
|
||||
output.append(' (let ((handler (eval-expr-cek thunk)))')
|
||||
output.append(' (guard')
|
||||
output.append(' (_e')
|
||||
output.append(' (true')
|
||||
@@ -1516,14 +1594,14 @@ output.append(' (if')
|
||||
output.append(' (and (list? _e) (= (first _e) "hs-return"))')
|
||||
output.append(' (nth _e 1)')
|
||||
output.append(' (raise _e))))')
|
||||
output.append(' (apply handler (cons nil vals))))))))))')
|
||||
output.append(' (handler nil)))))))))')
|
||||
output.append('')
|
||||
output.append(';; Evaluate with a specific me value (for "I am between" etc.)')
|
||||
output.append('(define eval-hs-with-me')
|
||||
output.append(' (fn (src me-val)')
|
||||
output.append(' (let ((sx (hs-to-sx (hs-compile src))))')
|
||||
output.append(' (let ((handler (eval-expr-cek')
|
||||
output.append(' (list (quote fn) (list (quote me)) (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))')
|
||||
output.append(' (list (quote fn) (list (quote me)) (_hs-wrap-body sx)))))')
|
||||
output.append(' (guard')
|
||||
output.append(' (_e')
|
||||
output.append(' (true')
|
||||
|
||||
Reference in New Issue
Block a user