From 3d35205533efba497a5222eca7c51e52f280835a Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 06:44:46 +0000 Subject: [PATCH] HS: transition query-ref + multi-prop (+2 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three parts: (a) parser `collect-transitions` recognises `style` tokens (`*prop`) as a continuation, so `transition *width from A to B *height from A to B` chains both transitions instead of dropping the second. (b) Mock `El` class gets `nextSibling`/`previousSibling` (plus `*ElementSibling` aliases) so `transition *W of the next ` can resolve the next-sibling target via host-get. (c) Generator pattern for `const X = await evaluate(() => { const el = document.querySelector(SEL); el.dispatchEvent(new Event(NAME, ...)); return ... })`; optionally prefixed by a destructuring assignment and allowing trailing `expect(...).toBe(...)` junk because `_body_statements` only splits on `;` at depth 0. Remaining `can use initial to transition to original value` needs `on click N` count-filtered events (same mock-sync block as cluster 13). Suite hs-upstream-transition: 13/17 → 15/17. Smoke 0-195: 162/195 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/hyperscript/parser.sx | 7 +++++-- shared/static/wasm/sx/hs-parser.sx | 7 +++++-- spec/tests/test-hyperscript-behavioral.sx | 1 + tests/hs-run-filtered.js | 4 ++++ tests/playwright/generate-sx-tests.py | 20 ++++++++++++++++++++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index aba0bf5d..2f4dd2cc 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -1618,8 +1618,11 @@ (if (and (not (at-end?)) - (= (tp-type) "ident") - (not (hs-keyword? (tp-val)))) + (or + (= (tp-type) "style") + (and + (= (tp-type) "ident") + (not (hs-keyword? (tp-val)))))) (collect-transitions (append acc (list (parse-one-transition)))) acc))) diff --git a/shared/static/wasm/sx/hs-parser.sx b/shared/static/wasm/sx/hs-parser.sx index aba0bf5d..2f4dd2cc 100644 --- a/shared/static/wasm/sx/hs-parser.sx +++ b/shared/static/wasm/sx/hs-parser.sx @@ -1618,8 +1618,11 @@ (if (and (not (at-end?)) - (= (tp-type) "ident") - (not (hs-keyword? (tp-val)))) + (or + (= (tp-type) "style") + (and + (= (tp-type) "ident") + (not (hs-keyword? (tp-val)))))) (collect-transitions (append acc (list (parse-one-transition)))) acc))) diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 9cf2beb5..9847409b 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -12798,6 +12798,7 @@ end") (dom-set-attr _el-div "_" "on click transition *width from 0px to 100px *height from 0px to 100px") (dom-append (dom-body) _el-div) (hs-activate! _el-div) + (dom-dispatch _el-div "click" nil) (assert= (dom-get-style _el-div "width") "100px") (assert= (dom-get-style _el-div "height") "100px") )) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index 7dcc155d..a2176f1a 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -52,6 +52,10 @@ class El { contains(o) { if(o===this)return true; for(const c of this.children)if(c===o||c.contains(o))return true; return false; } cloneNode(d) { const e=new El(this.tagName.toLowerCase()); Object.assign(e.attributes,this.attributes); e.id=this.id; e.className=this.className; e.classList._sync(this.className); for(const k of Object.keys(this.style)){if(typeof this.style[k]!=='function')e.style[k]=this.style[k];} e.textContent=this.textContent; e.innerHTML=this.innerHTML; e.value=this.value; if(d)for(const c of this.children)e.appendChild(c.cloneNode(true)); return e; } focus(){} blur(){} click(){this.dispatchEvent(new Ev('click',{bubbles:true}));} remove(){if(this.parentElement)this.parentElement.removeChild(this);} + get nextSibling(){const p=this.parentElement;if(!p)return null;const i=p.children.indexOf(this);return i>=0&&i0?p.children[i-1]:null;} + get previousElementSibling(){return this.previousSibling;} reset(){ // Form reset: walk descendants, restore defaultValue / defaultChecked / // defaultSelected. diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index e765770a..072b7676 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -1071,6 +1071,26 @@ def parse_dev_body(body, elements, var_names): ops.append(f'(dom-dispatch {target} "{m.group(3)}" {detail_expr})') continue + # [const X = await ]evaluate(() => { const el = document.querySelector(SEL); el.dispatchEvent(new Event(NAME, ...)); return ... }) + # Dispatches an event on a queried element and ignores the return value. + # Stmt may have trailing un-split junk (`expect(...).toBe(...)`) since + # body splitter only breaks on `;` and `})` doesn't always have one. + m = re.match( + r"(?:const\s+\w+\s*=\s*(?:await\s+)?)?" + r"evaluate\(\s*\(\)\s*=>\s*\{\s*" + r"const\s+(\w+)\s*=\s*document\.querySelector\(\s*(['\"])([^'\"]+)\2\s*\)\s*;?\s*" + r"\1\.dispatchEvent\(\s*new\s+(?:Custom)?Event\(\s*(['\"])([^'\"]+)\4" + r"(\s*,\s*\{[^}]*\})?\s*\)\s*\)\s*;?", + stmt_na, re.DOTALL, + ) + if m and seen_html: + sel = re.sub(r'^#work-area\s+', '', m.group(3)) + target = selector_to_sx(sel, elements, var_names) + opts = m.group(6) or '' + detail_expr = _extract_detail_expr(opts) + ops.append(f'(dom-dispatch {target} "{m.group(5)}" {detail_expr})') + continue + # evaluate(() => document.getElementById(ID).METHOD()) — generic # method dispatch (showModal, close, click, focus, blur, reset…). m = re.match(