Hyperscript test generator: repeat loop fix, assert= arg order, quote handling

- Don't insert 'then' inside for-in loop bodies or after 'repeat N times'
  (fixes repeat from 1/30 → 5/30)
- Allow HS sources ending with " when they don't contain embedded HTML
  (fixes set from 6/25 → 10/25, enables 18 previously-skipped tests)
- Fix assert= argument order: (actual expected), not (expected actual)
  (error messages now correctly report Expected/Got)

395 → 402/831 (+7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:51:20 +00:00
parent 5948741fb6
commit 429c2b59f9
4 changed files with 562 additions and 541 deletions

View File

@@ -1553,20 +1553,18 @@
cl-collect
(fn
(acc)
(do
(match-kw "then")
(let
((cmd (parse-cmd)))
(if
(nil? cmd)
acc
(let
((acc2 (append acc (list cmd))))
(cond
((match-kw "then") (cl-collect acc2))
((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val)))
(cl-collect acc2))
(true acc2))))))))
(let
((cmd (parse-cmd)))
(if
(nil? cmd)
acc
(let
((acc2 (append acc (list cmd))))
(cond
((match-kw "then") (cl-collect acc2))
((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val)))
(cl-collect acc2))
(true acc2)))))))
(let
((cmds (cl-collect (list))))
(cond

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -305,14 +305,14 @@ def check_to_sx(check, ref):
return f'(assert (not (dom-has-class? {r} "{key}")))'
elif typ == 'innerHTML':
escaped = key.replace('"', '\\"') if isinstance(key, str) else key
return f'(assert= "{escaped}" (dom-inner-html {r}))'
return f'(assert= (dom-inner-html {r}) "{escaped}")'
elif typ == 'textContent':
escaped = key.replace('"', '\\"')
return f'(assert= "{escaped}" (dom-text-content {r}))'
return f'(assert= (dom-text-content {r}) "{escaped}")'
elif typ == 'style':
return f'(assert= "{val}" (dom-get-style {r} "{key}"))'
return f'(assert= (dom-get-style {r} "{key}") "{val}")'
elif typ == 'attr':
return f'(assert= "{val}" (dom-get-attr {r} "{key}"))'
return f'(assert= (dom-get-attr {r} "{key}") "{val}")'
elif typ == 'hasAttr' and val:
return f'(assert (dom-has-attr? {r} "{key}"))'
elif typ == 'hasAttr' and not val:
@@ -324,7 +324,7 @@ def check_to_sx(check, ref):
elif typ == 'hasParent':
return f'(assert (not (nil? (dom-parent {r}))))'
elif typ == 'value':
return f'(assert= "{key}" (dom-get-prop {r} "value"))'
return f'(assert= (dom-get-prop {r} "value") "{key}")'
else:
return f';; SKIP check: {typ} {name}'
@@ -365,16 +365,16 @@ def pw_assertion_to_sx(target, negated, assert_type, args_str):
val = args[0] if args else ''
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
if negated:
return f'(assert (!= "{escaped}" (dom-text-content {target})))'
return f'(assert= "{escaped}" (dom-text-content {target}))'
return f'(assert (!= (dom-text-content {target}) "{escaped}"))'
return f'(assert= (dom-text-content {target}) "{escaped}")'
elif assert_type == 'toHaveAttribute':
attr_name = args[0] if args else ''
if len(args) >= 2:
attr_val = args[1].replace('\\', '\\\\').replace('"', '\\"')
if negated:
return f'(assert (!= "{attr_val}" (dom-get-attr {target} "{attr_name}")))'
return f'(assert= "{attr_val}" (dom-get-attr {target} "{attr_name}"))'
return f'(assert (!= (dom-get-attr {target} "{attr_name}") "{attr_val}"))'
return f'(assert= (dom-get-attr {target} "{attr_name}") "{attr_val}")'
else:
if negated:
return f'(assert (not (dom-has-attr? {target} "{attr_name}")))'
@@ -396,15 +396,15 @@ def pw_assertion_to_sx(target, negated, assert_type, args_str):
val = args[1] if len(args) >= 2 else ''
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
if negated:
return f'(assert (!= "{escaped}" (dom-get-style {target} "{prop}")))'
return f'(assert= "{escaped}" (dom-get-style {target} "{prop}"))'
return f'(assert (!= (dom-get-style {target} "{prop}") "{escaped}"))'
return f'(assert= (dom-get-style {target} "{prop}") "{escaped}")'
elif assert_type == 'toHaveValue':
val = args[0] if args else ''
escaped = val.replace('\\', '\\\\').replace('"', '\\"')
if negated:
return f'(assert (!= "{escaped}" (dom-get-prop {target} "value")))'
return f'(assert= "{escaped}" (dom-get-prop {target} "value"))'
return f'(assert (!= (dom-get-prop {target} "value") "{escaped}"))'
return f'(assert= (dom-get-prop {target} "value") "{escaped}")'
elif assert_type == 'toBeVisible':
if negated:
@@ -505,12 +505,16 @@ def emit_element_setup(lines, elements, var_names):
hs_val = re.sub(r'(then\s*)+then', 'then', hs_val)
# Don't insert 'then' between event name and first command in 'on' handlers
hs_val = re.sub(r'\bon (\w[\w.:+-]*) then\b', r'on \1 ', hs_val)
# Don't insert 'then' inside for-in loop bodies (between collection and body)
hs_val = re.sub(r'(\bin \[.*?\]) then\b', r'\1 ', hs_val)
# Don't insert 'then' after 'times' in repeat N times loops
hs_val = re.sub(r'\btimes then\b', 'times ', hs_val)
hs_val = hs_val.strip()
if not hs_val:
lines.append(f' (dom-append (dom-body) {var})')
continue
if hs_val.startswith('"') or hs_val.endswith('"'):
lines.append(f' ;; HS source has bare quotes — HTML parse artifact')
if hs_val.startswith('"') or (hs_val.endswith('"') and '<' in hs_val):
lines.append(f' ;; HS source has bare quotes or embedded HTML')
lines.append(f' (dom-append (dom-body) {var})')
continue
hs_escaped = hs_val.replace('\\', '\\\\').replace('"', '\\"')
@@ -657,7 +661,7 @@ def generate_eval_only_test(test, idx):
):
hs_expr = extract_hs_expr(m.group(2))
expected_sx = js_val_to_sx(m.group(3))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 1b: Inline — run("expr").toEqual([...])
for m in re.finditer(
@@ -666,7 +670,7 @@ def generate_eval_only_test(test, idx):
):
hs_expr = extract_hs_expr(m.group(2))
expected_sx = js_val_to_sx(m.group(3))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 2: Two-line — var result = await run(`expr`); expect(result).toBe(val)
if not assertions:
@@ -678,10 +682,10 @@ def generate_eval_only_test(test, idx):
hs_expr = extract_hs_expr(run_match.group(2))
for m in re.finditer(r'\.toBe\(([^)]+)\)', body):
expected_sx = js_val_to_sx(m.group(1))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
for m in re.finditer(r'\.toEqual\((\[.*?\])\)', body, re.DOTALL):
expected_sx = js_val_to_sx(m.group(1))
assertions.append(f' (assert= {expected_sx} (eval-hs "{hs_expr}"))')
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
for m in re.finditer(