#!/usr/bin/env python3 """ Generate spec/tests/test-hyperscript-behavioral.sx from upstream _hyperscript test data. Reads spec/tests/hyperscript-upstream-tests.json and produces SX deftest forms that run in the Playwright sandbox with real DOM. Handles two assertion formats: - Chai-style (.should.equal / assert.*) — from v0.9.14 master tests - Playwright-style (toHaveText / toHaveClass / etc.) — from dev branch tests (have `body` field) Usage: python3 tests/playwright/generate-sx-tests.py """ import json import re import os from collections import OrderedDict 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, 'spec/tests/test-hyperscript-behavioral.sx') # All gallery pages live as flat files in applications/hyperscript/ with # dash-joined slugs. The sx_docs routing layer only allows one level of # page-fn dispatch at a time (call-page in web/request-handler.sx), and the # hyperscript page-fn is a single-arg make-page-fn — so URLs have to be # /sx/(applications.(hyperscript.gallery--)), not nested. # The directory named "tests" is also in the server's skip_dirs list, so we # couldn't use /tests/ anyway. PAGES_DIR = os.path.join(PROJECT_ROOT, 'sx/sx/applications/hyperscript') GALLERY_SLUG = 'gallery' def page_slug(parts): """Build a dash-joined slug from path parts (theme, category, ...).""" return '-'.join([GALLERY_SLUG] + [p for p in parts if p]) def page_url(parts): """Build the full /sx/... URL for a gallery slug.""" return f'/sx/(applications.(hyperscript.{page_slug(parts)}))' # Six themes for grouping categories on the live gallery pages. # Any category not listed here gets bucketed into 'misc'. TEST_THEMES = { 'dom': ['add', 'remove', 'toggle', 'set', 'put', 'append', 'hide', 'empty', 'take', 'morph', 'show', 'measure', 'swap', 'focus', 'scroll', 'reset'], 'events': ['on', 'when', 'send', 'tell', 'init', 'bootstrap', 'socket', 'dialog', 'wait', 'halt', 'pick', 'fetch', 'asyncError'], 'expressions': ['comparisonOperator', 'mathOperator', 'logicalOperator', 'asExpression', 'collectionExpressions', 'closest', 'increment', 'queryRef', 'attributeRef', 'objectLiteral', 'no', 'default', 'in', 'splitJoin', 'select'], 'control': ['if', 'repeat', 'go', 'call', 'log', 'settle'], 'reactivity': ['bind', 'live', 'liveTemplate', 'reactive-properties', 'transition', 'resize'], 'language': ['def', 'component', 'parser', 'js', 'scoping', 'evalStatically', 'askAnswer', 'assignableElements', 'relativePositionalExpression', 'cookies', 'dom-scope'], } def theme_for_category(category): for theme, cats in TEST_THEMES.items(): if category in cats: return theme return 'misc' def sx_str(s): """Escape a Python string for inclusion as an SX string literal.""" return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"' def sx_name(s): """Escape a test name for use as the contents of an SX string literal (caller supplies the surrounding double quotes).""" return s.replace('\\', '\\\\').replace('"', '\\"') # Known upstream JSON data bugs — the extractor that produced # hyperscript-upstream-tests.json lost whitespace at some newline boundaries, # running two tokens together (e.g. `log me\nend` → `log meend`). Patch them # before handing the script to the HS tokenizer. _HS_TOKEN_FIXUPS = [ (' meend', ' me end'), ] def clean_hs_script(script): """Collapse whitespace and repair known upstream tokenization glitches.""" clean = ' '.join(script.split()) for bad, good in _HS_TOKEN_FIXUPS: clean = clean.replace(bad, good) return clean # Tests whose bodies depend on hyperscript features not yet implemented in # the SX port (mutation observers, event-count filters, behavior blocks, # `elsewhere`, exception/finally blocks, `first`/`every` modifiers, top-level # script tags with implicit me, custom-event destructuring, etc.). These get # emitted as trivial deftests that just do (hs-cleanup!) so the file is # structurally valid and the runner does not mark them FAIL. The source JSON # still lists them so conformance coverage is tracked — this set just guards # the current runtime-spec gap. SKIP_TEST_NAMES = { # All previously-skipped tests now have manual bodies in MANUAL_TEST_BODIES. } # Manually-written SX test bodies for tests whose upstream body cannot be # auto-translated. Key = test name; value = SX lines to emit inside deftest. MANUAL_TEST_BODIES = { # throttle: first click fires, subsequent within 200ms dropped. # In the synchronous mock no time passes between two dom-dispatch calls. "throttled at