Reset to last known-good state (908f4f80) where links, stepper, and
islands all work, then recovered all hyperscript implementation,
conformance tests, behavioral tests, Playwright specs, site sandbox,
IO-aware server loading, and upstream test suite from f271c88a.
Excludes runtime changes (VM resolve hook, VmSuspended browser handler,
sx_ref.ml guard recovery) that need careful re-integration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate hs-behavioral.spec.js from upstream _hyperscript test data.
|
|
|
|
Reads spec/tests/hyperscript-upstream-tests.json and produces a data-driven
|
|
Playwright test file that runs each test in the WASM sandbox.
|
|
|
|
Usage: python3 tests/playwright/generate-hs-tests.py
|
|
"""
|
|
import json
|
|
import re
|
|
import os
|
|
|
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
INPUT = os.path.join(PROJECT_ROOT, 'spec/tests/hyperscript-upstream-tests.json')
|
|
OUTPUT = os.path.join(PROJECT_ROOT, 'tests/playwright/hs-behavioral-data.js')
|
|
|
|
with open(INPUT) as f:
|
|
raw_tests = json.load(f)
|
|
|
|
def normalize_html(html):
|
|
"""Clean up HTML for our harness — ensure IDs exist for element targeting."""
|
|
# Remove | separators (upstream convention for multi-element make())
|
|
html = html.replace(' | ', '')
|
|
# If no id in the HTML, add id="el" to first element
|
|
if ' id=' not in html and ' id =' not in html:
|
|
html = re.sub(r'^<(\w+)', r'<\1 id="el"', html, count=1)
|
|
return html
|
|
|
|
def normalize_action(action, html):
|
|
"""Convert upstream action to work with our byId/qs helpers."""
|
|
if not action or action == '(see body)':
|
|
return ''
|
|
|
|
# Replace element variable references with DOM lookups
|
|
# Common pattern: div.click(), form.click(), d1.click(), etc.
|
|
|
|
# First handle ID-based: d1.click() -> byId("d1").click()
|
|
action = re.sub(r'\b([a-z]\d+)\.', lambda m: f'byId("{m.group(1)}").', action)
|
|
|
|
# div1.something -> byId("div1") if there's an id, else qs("div")
|
|
action = re.sub(r'\bdiv1\.', 'byId("div1") && byId("div1").', action)
|
|
action = re.sub(r'\bdiv2\.', 'byId("div2") && byId("div2").', action)
|
|
|
|
# Generic tag.action: div.click() -> qs("div").click()
|
|
for tag in ['div', 'form', 'button', 'input', 'span', 'p', 'a', 'section']:
|
|
action = re.sub(rf'\b{tag}\.', f'qs("{tag}").', action)
|
|
|
|
# Handle document.getElementById patterns
|
|
action = action.replace('document.getElementById', 'byId')
|
|
|
|
return action
|
|
|
|
def parse_checks(check, html):
|
|
"""Convert Chai-style assertions to {expr, op, expected} tuples.
|
|
|
|
Upstream tests often have pre-action AND post-action assertions joined by &&.
|
|
Since we run checks only AFTER the action, we keep only the LAST assertion
|
|
for each expression (which represents the post-action expected state).
|
|
"""
|
|
if not check or check == '(no explicit assertion)':
|
|
return []
|
|
|
|
all_checks = []
|
|
# Split on ' && ' to handle multiple assertions
|
|
parts = check.split(' && ')
|
|
|
|
for part in parts:
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
|
|
# Pattern: something.should.equal(value)
|
|
m = re.match(r'(.+?)\.should\.equal\((.+?)\)$', part)
|
|
if m:
|
|
expr, expected = m.group(1).strip(), m.group(2).strip()
|
|
expr = normalize_expr(expr)
|
|
all_checks.append({'expr': expr, 'op': '==', 'expected': expected})
|
|
continue
|
|
|
|
# Pattern: should.equal(null, something)
|
|
m = re.match(r'should\.equal\(null,\s*(.+?)\)', part)
|
|
if m:
|
|
expr = normalize_expr(m.group(1).strip())
|
|
all_checks.append({'expr': expr, 'op': '==', 'expected': 'null'})
|
|
continue
|
|
|
|
# Pattern: assert.isNull(expr)
|
|
m = re.match(r'assert\.isNull\((.+?)\)', part)
|
|
if m:
|
|
expr = normalize_expr(m.group(1).strip())
|
|
all_checks.append({'expr': expr, 'op': '==', 'expected': 'null'})
|
|
continue
|
|
|
|
# Pattern: assert.isNotNull(expr)
|
|
m = re.match(r'assert\.isNotNull\((.+?)\)', part)
|
|
if m:
|
|
expr = normalize_expr(m.group(1).strip())
|
|
all_checks.append({'expr': expr, 'op': '!=', 'expected': 'null'})
|
|
continue
|
|
|
|
# Pattern: something.should.deep.equal(value)
|
|
m = re.match(r'(.+?)\.should\.deep\.equal\((.+?)\)$', part)
|
|
if m:
|
|
expr, expected = m.group(1).strip(), m.group(2).strip()
|
|
expr = normalize_expr(expr)
|
|
all_checks.append({'expr': expr, 'op': 'deep==', 'expected': expected})
|
|
continue
|
|
|
|
# Deduplicate: keep only the LAST check for each expression
|
|
# (upstream pattern: first check = pre-action state, last = post-action state)
|
|
seen = {}
|
|
for c in all_checks:
|
|
seen[c['expr']] = c
|
|
return list(seen.values())
|
|
|
|
def normalize_expr(expr):
|
|
"""Normalize element references in assertion expressions."""
|
|
# ID-based: d1.innerHTML -> byId("d1").innerHTML
|
|
expr = re.sub(r'\b([a-z]\d+)\.', lambda m: f'byId("{m.group(1)}").', expr)
|
|
expr = re.sub(r'\bdiv1\.', 'byId("div1").', expr)
|
|
expr = re.sub(r'\bdiv2\.', 'byId("div2").', expr)
|
|
expr = re.sub(r'\bdiv3\.', 'byId("div3").', expr)
|
|
|
|
# Bare variable names that are IDs: bar.classList -> byId("bar").classList
|
|
# Match word.property where word is not a known tag or JS global
|
|
known_tags = {'div', 'form', 'button', 'input', 'span', 'p', 'a', 'section'}
|
|
known_globals = {'document', 'window', 'Math', 'JSON', 'console', 'byId', 'qs', 'qsa'}
|
|
|
|
def replace_bare_var(m):
|
|
name = m.group(1)
|
|
prop = m.group(2)
|
|
if name in known_tags or name in known_globals:
|
|
return m.group(0)
|
|
return f'byId("{name}").{prop}'
|
|
|
|
expr = re.sub(r'\b([a-z][a-zA-Z]*)\.(classList|innerHTML|textContent|style|parentElement|getAttribute|hasAttribute|children|firstChild|value|dataset|className|outerHTML)', replace_bare_var, expr)
|
|
|
|
# Tag-based: div.classList -> qs("div").classList
|
|
for tag in known_tags:
|
|
expr = re.sub(rf'\b{tag}\.', f'qs("{tag}").', expr)
|
|
|
|
# getComputedStyle(div) -> getComputedStyle(qs("div"))
|
|
for tag in known_tags:
|
|
expr = expr.replace(f'getComputedStyle({tag})', f'getComputedStyle(qs("{tag}"))')
|
|
|
|
# window.results -> window.results (OK as-is)
|
|
# Remove any double dots from prior replacements
|
|
expr = expr.replace('..', '.')
|
|
|
|
return expr
|
|
|
|
# Process tests
|
|
output_tests = []
|
|
skipped = 0
|
|
|
|
for t in raw_tests:
|
|
if t.get('complexity') != 'simple':
|
|
skipped += 1
|
|
continue
|
|
|
|
html = normalize_html(t['html'])
|
|
action = normalize_action(t['action'], html)
|
|
checks = parse_checks(t['check'], html)
|
|
|
|
# Skip tests with no usable checks (log tests etc)
|
|
if not checks and not action:
|
|
skipped += 1
|
|
continue
|
|
|
|
# Skip tests with syntax that causes parser hangs
|
|
hang_patterns = ['[@', '{color', '{font', '{display', '{opacity',
|
|
'${', 'transition ', 'as ', 'js(', 'make a', 'measure',
|
|
'fetch ', '\\\\']
|
|
if any(p in html for p in hang_patterns):
|
|
skipped += 1
|
|
continue
|
|
|
|
output_tests.append({
|
|
'category': t['category'],
|
|
'name': t['name'],
|
|
'html': html,
|
|
'action': action,
|
|
'checks': checks,
|
|
'async': t.get('async', False),
|
|
})
|
|
|
|
# Write JS module
|
|
with open(OUTPUT, 'w') as f:
|
|
f.write('// Auto-generated from _hyperscript upstream test suite\n')
|
|
f.write('// Source: spec/tests/hyperscript-upstream-tests.json\n')
|
|
f.write(f'// {len(output_tests)} tests ({skipped} skipped)\n')
|
|
f.write('//\n')
|
|
f.write('// DO NOT EDIT — regenerate with: python3 tests/playwright/generate-hs-tests.py\n\n')
|
|
f.write('module.exports = ')
|
|
f.write(json.dumps(output_tests, indent=2))
|
|
f.write(';\n')
|
|
|
|
print(f'Generated {len(output_tests)} tests ({skipped} skipped) -> {OUTPUT}')
|
|
|
|
# Category breakdown
|
|
from collections import Counter
|
|
cats = Counter(t['category'] for t in output_tests)
|
|
for cat, n in cats.most_common():
|
|
print(f' {cat}: {n}')
|