Restore hyperscript work on stable site base (908f4f80)
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>
This commit is contained in:
205
tests/playwright/generate-hs-tests.py
Normal file
205
tests/playwright/generate-hs-tests.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/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}')
|
||||
Reference in New Issue
Block a user