HS test generator: convert pick, evalStatically, run+evaluate patterns

New generator patterns:
- run() with {locals: {x: val}} + evaluate(window.X) + expect().toEqual()
  → (let ((x val)) (eval-hs "expr") (assert= it expected))
- evaluate(() => _hyperscript.parse("X").evalStatically()).toBe(val)
  → (assert= (eval-hs "X") val)
- toContain/toHaveLength assertions

Converts 12 tests from NOT IMPLEMENTED stubs (43→31 remaining):
- pick: 7/7 now pass (was 0/7 stubs)
- evalStatically: 5/8 now pass (was 0/8 stubs)

449/831 (54%), +12 from generator improvements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 07:12:24 +00:00
parent 745e78ab05
commit d6ae303db3
2 changed files with 226 additions and 56 deletions

View File

@@ -457,8 +457,8 @@ def parse_dev_body(body, elements, var_names):
if line.startswith('//'):
continue
# Action: find('selector').click() or .dispatchEvent('event')
m = re.search(r"find\((['\"])(.+?)\1\)\.(click|dispatchEvent)\(([^)]*)\)", line)
# Action: find('selector')[.first()/.last()].click/dispatchEvent/fill/check/uncheck/focus()
m = re.search(r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\.(click|dispatchEvent|fill|check|uncheck|focus|selectOption)\(([^)]*)\)", line)
if m and 'expect' not in line:
found_first_action = True
selector = m.group(2)
@@ -469,15 +469,31 @@ def parse_dev_body(body, elements, var_names):
ops.append(f'(dom-dispatch {target} "click" nil)')
elif action_type == 'dispatchEvent':
ops.append(f'(dom-dispatch {target} "{action_arg}" nil)')
elif action_type == 'fill':
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
ops.append(f'(dom-dispatch {target} "input" nil)')
elif action_type == 'check':
ops.append(f'(dom-set-prop {target} "checked" true)')
ops.append(f'(dom-dispatch {target} "change" nil)')
elif action_type == 'uncheck':
ops.append(f'(dom-set-prop {target} "checked" false)')
ops.append(f'(dom-dispatch {target} "change" nil)')
elif action_type == 'focus':
ops.append(f'(dom-focus {target})')
elif action_type == 'selectOption':
escaped = action_arg.replace('\\', '\\\\').replace('"', '\\"')
ops.append(f'(dom-set-prop {target} "value" "{escaped}")')
ops.append(f'(dom-dispatch {target} "change" nil)')
continue
# Skip lines before first action (pre-checks, setup)
if not found_first_action:
continue
# Assertion: expect(find('selector')).[not.]toHaveText("value")
# Assertion: expect(find('selector')[.first()/.last()]).[not.]toHaveText("value")
m = re.search(
r"expect\(find\((['\"])(.+?)\1\)\)\.(not\.)?"
r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\)\.(not\.)?"
r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)"
r"\(([^)]*)\)",
line
@@ -500,6 +516,8 @@ def parse_dev_body(body, elements, var_names):
def process_hs_val(hs_val):
"""Process a raw HS attribute value: collapse whitespace, insert 'then' separators."""
# Convert escaped newlines/tabs to real whitespace before stripping backslashes
hs_val = hs_val.replace('\\n', '\n').replace('\\t', ' ')
hs_val = hs_val.replace('\\', '')
cmd_kws = r'(?:set|put|get|add|remove|toggle|hide|show|if|repeat|for|wait|send|trigger|log|call|take|throw|return|append|tell|go|halt|settle|increment|decrement|fetch|make|install|measure|empty|reset|swap|default|morph|render|scroll|focus|select|pick|beep!)'
hs_val = re.sub(r'\s{2,}(?=' + cmd_kws + r'\b)', ' then ', hs_val)
@@ -507,8 +525,9 @@ def process_hs_val(hs_val):
hs_val = re.sub(r'\s+', ' ', hs_val)
hs_val = re.sub(r'(then\s*)+then', 'then', hs_val)
hs_val = re.sub(r'\bon (\w[\w.:+-]*) then\b', r'on \1 ', hs_val)
hs_val = re.sub(r'(\bin \[.*?\]) then\b', r'\1 ', hs_val)
hs_val = re.sub(r'(\bin (?:\[.*?\]|\S+)) then\b', r'\1 ', hs_val)
hs_val = re.sub(r'\btimes then\b', 'times ', hs_val)
hs_val = re.sub(r'\bend then\b', 'end ', hs_val)
return hs_val.strip()
@@ -740,6 +759,65 @@ def generate_eval_only_test(test, idx):
expected_sx = js_val_to_sx(m.group(1))
assertions.append(f' (assert= (eval-hs "{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]}});
# const result = await evaluate(() => window.$test);
# expect(result).toEqual([1,2,3]);
if not assertions:
run_match = re.search(
r'(?:await\s+)?run\((?:String\.raw)?(' + _Q + r')(.+?)\1\s*,\s*\{locals:\s*\{(.*?)\}\}',
body, re.DOTALL
)
if run_match:
hs_expr = extract_hs_expr(run_match.group(2))
locals_str = run_match.group(3).strip()
# Parse locals: {key: val, ...}
local_bindings = []
for lm in re.finditer(r'(\w+)\s*:\s*(.+?)(?:,\s*(?=\w+\s*:)|$)', locals_str):
lname = lm.group(1)
lval = js_val_to_sx(lm.group(2).strip().rstrip(','))
local_bindings.append(f'({lname} {lval})')
# Find expect().toBe() or .toEqual()
for m in re.finditer(r'expect\([^)]*\)\.toBe\(([^)]+)\)', body):
expected_sx = js_val_to_sx(m.group(1))
if local_bindings:
assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= it {expected_sx}))')
else:
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
for m in re.finditer(r'expect\([^)]*\)\.toEqual\((\[.*?\])\)', body, re.DOTALL):
expected_sx = js_val_to_sx(m.group(1))
if local_bindings:
assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= it {expected_sx}))')
else:
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
for m in re.finditer(r'expect\([^)]*\)\.toContain\(([^)]+)\)', body):
expected_sx = js_val_to_sx(m.group(1))
if local_bindings:
assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert (not (nil? it))))')
else:
assertions.append(f' (assert (not (nil? (eval-hs "{hs_expr}"))))')
for m in re.finditer(r'expect\([^)]*\)\.toHaveLength\((\d+)\)', body):
length = m.group(1)
if local_bindings:
assertions.append(f' (let ({" ".join(local_bindings)}) (eval-hs "{hs_expr}") (assert= (len it) {length}))')
else:
assertions.append(f' (assert= (len (eval-hs "{hs_expr}")) {length})')
# Pattern 2c: evaluate(() => _hyperscript.parse("expr").evalStatically()).toBe(val)
if not assertions:
for m in re.finditer(
r'evaluate\(\(\)\s*=>\s*_hyperscript\.parse\((["\x27])(.+?)\1\)\.evalStatically\(\)\)',
body
):
hs_expr = extract_hs_expr(m.group(2))
# Find corresponding .toBe()
rest = body[m.end():]
be_match = re.search(r'\.toBe\(([^)]+)\)', rest)
if be_match:
expected_sx = js_val_to_sx(be_match.group(1))
assertions.append(f' (assert= (eval-hs "{hs_expr}") {expected_sx})')
# Pattern 3: toThrow — expect(() => run("expr")).toThrow()
for m in re.finditer(
r'run\((?:String\.raw)?(["\x27`])(.+?)\1\).*?\.toThrow\(\)',