From 3316d402fd5d0d8fad6d02501e00b82cabfcfa1f Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 12:10:19 +0000 Subject: [PATCH] =?UTF-8?q?HS:=20null-safety=20piece=201=20=E2=80=94=20eva?= =?UTF-8?q?l-hs-error=20recognizer=20+=20helper=20(+18=20tests=20unlocked)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add recognizer for expect(await error("HS")).toBe("MSG") pattern in generate-sx-tests.py, plus eval-hs-error SX helper in the generated test file. All 18 runtimeErrors tests now generate real test cases instead of SKIP stubs. Co-Authored-By: Claude Sonnet 4.6 --- spec/tests/test-hyperscript-behavioral.sx | 91 ++++++++++++++++++----- tests/playwright/generate-sx-tests.py | 40 ++++++++++ 2 files changed, 113 insertions(+), 18 deletions(-) diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 3a867216..cee9cb43 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -88,6 +88,27 @@ (raise _e)))) (handler me-val)))))) +;; Evaluate a hyperscript expression, catch the first error raised, and +;; return its message string. Used by runtimeErrors tests. +;; Returns nil if no error is raised (test would then fail equality). +(define eval-hs-error + (fn (src) + (let ((sx (hs-to-sx (hs-compile src)))) + (let ((handler (eval-expr-cek + (list (quote fn) (list (quote me)) + (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx))))) + (guard + (_e + (true + (if + (string? _e) + _e + (if + (and (list? _e) (= (first _e) "hs-return")) + nil + (str _e))))) + (begin (handler nil) nil)))))) + ;; ── add (19 tests) ── (defsuite "hs-upstream-add" (deftest "can add a value to a set" @@ -2153,41 +2174,75 @@ ;; ── core/runtimeErrors (18 tests) ── (defsuite "hs-upstream-core/runtimeErrors" (deftest "reports basic function invocation null errors properly" - (error "SKIP (untranslated): reports basic function invocation null errors properly")) + (assert= (eval-hs-error "x()") "'x' is null") + (assert= (eval-hs-error "x.y()") "'x' is null") + (assert= (eval-hs-error "x.y.z()") "'x.y' is null") + ) (deftest "reports basic function invocation null errors properly w/ of" - (error "SKIP (untranslated): reports basic function invocation null errors properly w/ of")) + (assert= (eval-hs-error "z() of y of x") "'z' is null") + ) (deftest "reports basic function invocation null errors properly w/ possessives" - (error "SKIP (untranslated): reports basic function invocation null errors properly w/ possessives")) + (assert= (eval-hs-error "x's y()") "'x' is null") + (assert= (eval-hs-error "x's y's z()") "'x's y' is null") + ) (deftest "reports null errors on add command properly" - (error "SKIP (untranslated): reports null errors on add command properly")) + (assert= (eval-hs-error "add .foo to #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "add @foo to #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "add {display:none} to #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on decrement command properly" - (error "SKIP (untranslated): reports null errors on decrement command properly")) + (assert= (eval-hs-error "decrement #doesntExist's innerHTML") "'#doesntExist' is null") + ) (deftest "reports null errors on default command properly" - (error "SKIP (untranslated): reports null errors on default command properly")) + (assert= (eval-hs-error "default #doesntExist's innerHTML to 'foo'") "'#doesntExist' is null") + ) (deftest "reports null errors on hide command properly" - (error "SKIP (untranslated): reports null errors on hide command properly")) + (assert= (eval-hs-error "hide #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on increment command properly" - (error "SKIP (untranslated): reports null errors on increment command properly")) + (assert= (eval-hs-error "increment #doesntExist's innerHTML") "'#doesntExist' is null") + ) (deftest "reports null errors on measure command properly" - (error "SKIP (untranslated): reports null errors on measure command properly")) + (assert= (eval-hs-error "measure #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on put command properly" - (error "SKIP (untranslated): reports null errors on put command properly")) + (assert= (eval-hs-error "put 'foo' into #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' into #doesntExist's innerHTML") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' into #doesntExist.innerHTML") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' before #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' after #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' at the start of #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "put 'foo' at the end of #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on remove command properly" - (error "SKIP (untranslated): reports null errors on remove command properly")) + (assert= (eval-hs-error "remove .foo from #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "remove @foo from #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "remove #doesntExist from #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on send command properly" - (error "SKIP (untranslated): reports null errors on send command properly")) + (assert= (eval-hs-error "send 'foo' to #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on sets properly" - (error "SKIP (untranslated): reports null errors on sets properly")) + (assert= (eval-hs-error "set x's y to true") "'x' is null") + (assert= (eval-hs-error "set x's @y to true") "'x' is null") + ) (deftest "reports null errors on settle command properly" - (error "SKIP (untranslated): reports null errors on settle command properly")) + (assert= (eval-hs-error "settle #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on show command properly" - (error "SKIP (untranslated): reports null errors on show command properly")) + (assert= (eval-hs-error "show #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on toggle command properly" - (error "SKIP (untranslated): reports null errors on toggle command properly")) + (assert= (eval-hs-error "toggle .foo on #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "toggle between .foo and .bar on #doesntExist") "'#doesntExist' is null") + (assert= (eval-hs-error "toggle @foo on #doesntExist") "'#doesntExist' is null") + ) (deftest "reports null errors on transition command properly" - (error "SKIP (untranslated): reports null errors on transition command properly")) + (assert= (eval-hs-error "transition #doesntExist's *visibility to 0") "'#doesntExist' is null") + ) (deftest "reports null errors on trigger command properly" - (error "SKIP (untranslated): reports null errors on trigger command properly")) + (assert= (eval-hs-error "trigger 'foo' on #doesntExist") "'#doesntExist' is null") + ) ) ;; ── core/scoping (20 tests) ── diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 3efec6bc..3256d59d 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -2333,6 +2333,25 @@ def generate_eval_only_test(test, idx): hs_expr = extract_hs_expr(m.group(2)) assertions.append(f' (assert-throws (eval-hs "{hs_expr}"))') + # Pattern 4: eval-hs-error — expect(await error("expr")).toBe("msg") + # These test that running HS raises an error with a specific message string. + for m in re.finditer( + r'(?:const\s+\w+\s*=\s*)?(?:await\s+)?error\((["\x27`])(.+?)\1\)' + r'(?:[^;]|\n)*?(?:expect\([^)]*\)\.toBe\(([^)]+)\)|\.toBe\(([^)]+)\))', + body, re.DOTALL + ): + hs_expr = extract_hs_expr(m.group(2)) + expected_raw = (m.group(3) or m.group(4) or '').strip() + # Strip only the outermost JS string delimiter (double or single quote) + # without touching inner quotes inside the string value. + if len(expected_raw) >= 2 and expected_raw[0] == expected_raw[-1] and expected_raw[0] in ('"', "'"): + inner = expected_raw[1:-1] + expected_sx = '"' + inner.replace('\\', '\\\\').replace('"', '\\"') + '"' + else: + expected_sx = js_val_to_sx(expected_raw) + hs_escaped = hs_expr.replace('\\', '\\\\').replace('"', '\\"') + assertions.append(f' (assert= (eval-hs-error "{hs_escaped}") {expected_sx})') + if not assertions: return None # Can't convert this body pattern @@ -2692,6 +2711,27 @@ output.append(' (nth _e 1)') output.append(' (raise _e))))') output.append(' (handler me-val))))))') output.append('') +output.append(';; Evaluate a hyperscript expression, catch the first error raised, and') +output.append(';; return its message string. Used by runtimeErrors tests.') +output.append(';; Returns nil if no error is raised (test would then fail equality).') +output.append('(define eval-hs-error') +output.append(' (fn (src)') +output.append(' (let ((sx (hs-to-sx (hs-compile src))))') +output.append(' (let ((handler (eval-expr-cek') +output.append(' (list (quote fn) (list (quote me))') +output.append(' (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))') +output.append(' (guard') +output.append(' (_e') +output.append(' (true') +output.append(' (if') +output.append(' (string? _e)') +output.append(' _e') +output.append(' (if') +output.append(' (and (list? _e) (= (first _e) "hs-return"))') +output.append(' nil') +output.append(' (str _e)))))') +output.append(' (begin (handler nil) nil))))))') +output.append('') # Group by category categories = OrderedDict()