diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx
index 17916bb0..040bd2e8 100644
--- a/spec/tests/test-hyperscript-behavioral.sx
+++ b/spec/tests/test-hyperscript-behavioral.sx
@@ -3425,29 +3425,29 @@
(error "SKIP (untranslated): converts value as Date"))
(deftest "converts value as Fixed"
(assert= (eval-hs "'10.4' as Fixed") "10")
- (assert= (eval-hs "'10.4' as Fixed") "10.49")
+ (assert= (eval-hs "'10.4899' as Fixed:2") "10.49")
)
(deftest "converts value as Float"
(assert= (eval-hs "'10' as Float") 10)
- (assert= (eval-hs "'10' as Float") 10.4)
+ (assert= (eval-hs "'10.4' as Float") 10.4)
)
(deftest "converts value as Int"
(assert= (eval-hs "'10' as Int") 10)
- (assert= (eval-hs "'10' as Int") 10)
+ (assert= (eval-hs "'10.4' as Int") 10)
)
(deftest "converts value as JSONString"
(assert= (eval-hs "{foo:'bar'} as JSONString") "{"foo":"bar"}")
)
(deftest "converts value as Number"
(assert= (eval-hs "'10' as Number") 10)
- (assert= (eval-hs "'10' as Number") 10.4)
+ (assert= (eval-hs "'10.4' as Number") 10.4)
)
(deftest "converts value as Object"
(assert= (host-get (eval-hs-locals "x as Object" (list (list (quote x) {:foo "bar"}))) "foo") "bar")
)
(deftest "converts value as String"
(assert= (eval-hs "10 as String") "10")
- (assert= (eval-hs "10 as String") "true")
+ (assert= (eval-hs "true as String") "true")
)
(deftest "parses string as JSON to object"
(assert= (host-get (eval-hs "'{\"foo\":\"bar\"}' as JSON") "foo") "bar")
@@ -6355,8 +6355,8 @@
(defsuite "hs-upstream-expressions/strings"
(deftest "handles strings properly"
(assert= (eval-hs "\"foo\"") "foo")
- (assert= (eval-hs "\"foo\"") "fo'o")
- (assert= (eval-hs "\"foo\"") "foo")
+ (assert= (eval-hs "\"fo'o\"") "fo'o")
+ (assert= (eval-hs "'foo'") "foo")
)
(deftest "should handle back slashes in non-template content"
(assert= (eval-hs-locals "`https://${foo}`" (list (list (quote foo) "bar"))) "https://bar")
@@ -6365,9 +6365,9 @@
(error "SKIP (untranslated): should handle strings with tags and quotes"))
(deftest "string templates preserve white space"
(assert= (eval-hs "` ${1 + 2} ${1 + 2} `") " 3 3 ")
- (assert= (eval-hs "` ${1 + 2} ${1 + 2} `") "3 3 ")
- (assert= (eval-hs "` ${1 + 2} ${1 + 2} `") "33 ")
- (assert= (eval-hs "` ${1 + 2} ${1 + 2} `") "3 3")
+ (assert= (eval-hs "`${1 + 2} ${1 + 2} `") "3 3 ")
+ (assert= (eval-hs "`${1 + 2}${1 + 2} `") "33 ")
+ (assert= (eval-hs "`${1 + 2} ${1 + 2}`") "3 3")
)
(deftest "string templates work properly"
(assert= (eval-hs "`$1`") "1")
@@ -8479,21 +8479,29 @@
;; ── make (8 tests) ──
(defsuite "hs-upstream-make"
(deftest "can make elements"
- (error "SKIP (untranslated): can make elements"))
+ (assert= (eval-hs "make a
set window.obj to it") "P")
+ )
(deftest "can make elements with id and classes"
- (error "SKIP (untranslated): can make elements with id and classes"))
+ (assert= (eval-hs "make a set window.obj to it") "P")
+ )
(deftest "can make named objects"
- (error "SKIP (untranslated): can make named objects"))
+ (assert= (eval-hs "make a WeakMap called wm then set window.obj to wm") true)
+ )
(deftest "can make named objects w/ global scope"
- (error "SKIP (untranslated): can make named objects w/ global scope"))
+ (assert= (eval-hs "make a WeakMap called $wm") true)
+ )
(deftest "can make named objects with arguments"
- (error "SKIP (untranslated): can make named objects with arguments"))
+ (assert= (eval-hs "make a URL from \"/playground/\", \"https://hyperscript.org/\" called u set window.obj to u") true)
+ )
(deftest "can make objects"
- (error "SKIP (untranslated): can make objects"))
+ (assert= (eval-hs "make a WeakMap then set window.obj to it") true)
+ )
(deftest "can make objects with arguments"
- (error "SKIP (untranslated): can make objects with arguments"))
+ (assert= (eval-hs "make a URL from \"/playground/\", \"https://hyperscript.org/\" set window.obj to it") true)
+ )
(deftest "creates a div by default"
- (error "SKIP (untranslated): creates a div by default"))
+ (assert= (eval-hs "make a <.a/> set window.obj to it") "DIV")
+ )
)
;; ── measure (6 tests) ──
diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py
index a4a51f43..120892ea 100644
--- a/tests/playwright/generate-sx-tests.py
+++ b/tests/playwright/generate-sx-tests.py
@@ -1316,74 +1316,125 @@ def generate_eval_only_test(test, idx):
obj_str = re.sub(r'\s+', ' ', m.group(3)).strip()
assertions.append(f' ;; TODO: assert= (eval-hs "{hs_expr}") against {obj_str}')
- # Pattern 2: Two-line — var result = await run(`expr`, opts); expect(result...).toBe/toEqual(val)
+ # Pattern 2: var result = await run(`expr`, opts); expect(result...).toBe/toEqual(val)
+ # Reassignments are common (`result = await run(...)` repeated for multiple
+ # checks). Walk the body in order, pairing each expect(result) with the
+ # most recent preceding run().
if not assertions:
- run_match = re.search(
- r'(?:var|let|const)\s+\w+\s*=\s*' + _RUN_OPEN + _RUN_ARGS + r'\)',
- body, re.DOTALL
- )
- if run_match:
- hs_expr = extract_hs_expr(run_match.group(2))
- var_name = re.search(r'(?:var|let|const)\s+(\w+)', body).group(1)
- # Capture locals from the run() call, if present. Use balanced-brace
- # extraction so nested {a: {b: 1}} doesn't truncate at the inner }.
- local_pairs = []
- locals_idx = body.find('locals:')
- if locals_idx >= 0:
- # Find the opening { after "locals:"
- open_idx = body.find('{', locals_idx)
- if open_idx >= 0:
- depth = 0
- end_idx = -1
- in_str = None
- for i in range(open_idx, len(body)):
- ch = body[i]
- if in_str:
- if ch == in_str and body[i-1] != '\\':
- in_str = None
- continue
- if ch in ('"', "'", '`'):
- in_str = ch
- continue
- if ch == '{':
- depth += 1
- elif ch == '}':
- depth -= 1
- if depth == 0:
- end_idx = i
- break
- if end_idx > open_idx:
- locals_str = body[open_idx + 1:end_idx].strip()
- for kv in split_top_level(locals_str):
- kv = kv.strip()
- m = re.match(r'^(\w+)\s*:\s*(.+)$', kv, re.DOTALL)
- if m:
- local_pairs.append((m.group(1), js_val_to_sx(m.group(2).strip())))
- # Merge window setups into local_pairs so evaluate() globals are visible to HS.
- merged_pairs = list(window_setups) + local_pairs
- locals_sx = '(list ' + ' '.join(
- f'(list (quote {n}) {v})' for n, v in merged_pairs
- ) + ')' if merged_pairs else None
- def eval_call(expr):
- return f'(eval-hs-locals "{expr}" {locals_sx})' if locals_sx else f'(eval-hs "{expr}")'
- for m in re.finditer(r'expect\((' + re.escape(var_name) + r'(?:\["[^"]+"\]|\.\w+)?)\)\.toBe\(([^)]+)\)', body):
+ decl_match = re.search(r'(?:var|let|const)\s+(\w+)', body)
+ if decl_match:
+ var_name = decl_match.group(1)
+ # Find every run() occurrence (with or without var = prefix), and
+ # capture per-call `{locals: {...}}` opts (balanced-brace).
+ # The trailing `_RUN_ARGS\)` anchors the lazy `(.+?)\1` so it
+ # picks the *outer* HS-source quote, not the first inner `\'`.
+ run_iter = list(re.finditer(
+ r'(?:(?:var|let|const)\s+\w+\s*=\s*|' + re.escape(var_name) + r'\s*=\s*)?' +
+ _RUN_OPEN + _RUN_ARGS + r'\)', body, re.DOTALL
+ ))
+
+ def parse_run_locals(rm):
+ """If the run() match has `, {locals: {...}}` in its args,
+ return (name, sx_value) pairs; else []."""
+ # Args between the closing HS-source quote and run's `)`.
+ args_str = body[rm.end(2) + 1:rm.end() - 1]
+ lm = re.search(r'locals:\s*\{', args_str)
+ if not lm:
+ return []
+ # Balanced-brace from after `locals: {`.
+ start = rm.end(2) + 1 + lm.end()
+ d, in_str, end = 1, None, -1
+ for i in range(start, len(body)):
+ ch = body[i]
+ if in_str:
+ if ch == in_str and body[i - 1] != '\\':
+ in_str = None
+ continue
+ if ch in ('"', "'", '`'):
+ in_str = ch
+ continue
+ if ch == '{':
+ d += 1
+ elif ch == '}':
+ d -= 1
+ if d == 0:
+ end = i
+ break
+ if end < 0:
+ return []
+ pairs = []
+ for kv in split_top_level(body[start:end]):
+ kv = kv.strip()
+ km = re.match(r'^(\w+)\s*:\s*(.+)$', kv, re.DOTALL)
+ if km:
+ pairs.append((km.group(1), js_val_to_sx(km.group(2).strip())))
+ return pairs
+
+ # Pre-compute per-run locals (window_setups + per-call locals).
+ run_data = []
+ for rm in run_iter:
+ local_pairs = parse_run_locals(rm)
+ merged = list(window_setups) + local_pairs
+ run_data.append((rm.start(), rm.end(), extract_hs_expr(rm.group(2)), merged))
+
+ def call_for(hs_expr, pairs):
+ if pairs:
+ locals_sx = '(list ' + ' '.join(
+ f'(list (quote {n}) {v})' for n, v in pairs) + ')'
+ return f'(eval-hs-locals "{hs_expr}" {locals_sx})'
+ return f'(eval-hs "{hs_expr}")'
+
+ def run_at(pos):
+ """Return (hs_expr, pairs) for the most recent run() that ends before `pos`."""
+ last = None
+ for rd in run_data:
+ if rd[1] >= 0 and rd[1] < pos:
+ last = rd
+ return last
+
+ def emit_for(hs_expr, pairs, expected_sx, prop=None):
+ call = call_for(hs_expr, pairs)
+ if prop:
+ return f' (assert= (host-get {call} "{prop}") {expected_sx})'
+ return f' (assert= {call} {expected_sx})'
+
+ for m in re.finditer(
+ r'expect\((' + re.escape(var_name) + r'(?:\["[^"]+"\]|\.\w+)?)\)\.toBe\(([^)]+)\)',
+ body
+ ):
+ rd = run_at(m.start())
+ if rd is None:
+ continue
+ _, _, hs_expr, pairs = rd
accessor = m.group(1)
expected_sx = js_val_to_sx(m.group(2))
- # Check for property access: result["foo"] or result.foo
prop_m = re.search(r'\["([^"]+)"\]|\.(\w+)', accessor[len(var_name):])
- if prop_m:
- prop = prop_m.group(1) or prop_m.group(2)
- assertions.append(f' (assert= (host-get {eval_call(hs_expr)} "{prop}") {expected_sx})')
- else:
- assertions.append(f' (assert= {eval_call(hs_expr)} {expected_sx})')
- for m in re.finditer(r'expect\(' + re.escape(var_name) + r'(?:\.\w+)?\)\.toEqual\((\[.*?\])\)', body, re.DOTALL):
+ prop = prop_m.group(1) or prop_m.group(2) if prop_m else None
+ assertions.append(emit_for(hs_expr, pairs, expected_sx, prop))
+
+ for m in re.finditer(
+ r'expect\(' + re.escape(var_name) + r'(?:\.\w+)?\)\.toEqual\((\[.*?\])\)',
+ body, re.DOTALL
+ ):
+ rd = run_at(m.start())
+ if rd is None:
+ continue
+ _, _, hs_expr, pairs = rd
expected_sx = js_val_to_sx(m.group(1))
- assertions.append(f' (assert= {eval_call(hs_expr)} {expected_sx})')
- # Handle .map(x => x.prop) before toEqual
- for m in re.finditer(r'expect\(' + re.escape(var_name) + r'\.map\(\w+\s*=>\s*\w+\.(\w+)\)\)\.toEqual\((\[.*?\])\)', body, re.DOTALL):
+ assertions.append(emit_for(hs_expr, pairs, expected_sx))
+
+ for m in re.finditer(
+ r'expect\(' + re.escape(var_name) + r'\.map\(\w+\s*=>\s*\w+\.(\w+)\)\)\.toEqual\((\[.*?\])\)',
+ body, re.DOTALL
+ ):
+ rd = run_at(m.start())
+ if rd is None:
+ continue
+ _, _, hs_expr, pairs = rd
prop = m.group(1)
expected_sx = js_val_to_sx(m.group(2))
- assertions.append(f' (assert= (map (fn (x) (get x "{prop}")) {eval_call(hs_expr)}) {expected_sx})')
+ call = call_for(hs_expr, pairs)
+ assertions.append(f' (assert= (map (fn (x) (get x "{prop}")) {call}) {expected_sx})')
# Pattern 2b: run() with locals + evaluate(window.X) + expect().toBe/toEqual
# e.g.: await run(`expr`, {locals: {arr: [1,2,3]}});