diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index c5f0a188..3e6f7f35 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -1803,9 +1803,35 @@ (fn (p) (nth p 1)) (sort (fn (a b) (if (> (first a) (first b)) true false)) pairs))))) -(define hs-split-by (fn (s sep) (split s sep))) +(define hs-split-by (fn (s sep) (if (nil? s) nil (split s sep)))) -(define hs-joined-by (fn (col sep) (join sep col))) +(define + hs-joined-by + (fn + (col sep) + (cond + ((nil? col) nil) + ((list? col) + (join sep (map (fn (x) (if (nil? x) "" (str x))) col))) + (true col)))) + +(define + hs-sorted-by + (fn + (coll key-fn) + (if + (not (list? coll)) + coll + (sort + (fn + (a b) + (let + ((ka (key-fn a)) (kb (key-fn b))) + (cond + ((nil? ka) (not (nil? kb))) + ((nil? kb) false) + (true (< ka kb))))) + coll)))) (define hs-sorted-by diff --git a/shared/static/wasm/sx/dom.sx b/shared/static/wasm/sx/dom.sx index 99b93362..87568932 100644 --- a/shared/static/wasm/sx/dom.sx +++ b/shared/static/wasm/sx/dom.sx @@ -195,19 +195,22 @@ "Query DOM and return an SX list (not a host NodeList)." (let ((node-list (if (nil? sel) (host-call (dom-document) "querySelectorAll" root) (host-call root "querySelectorAll" sel)))) - (if - (nil? node-list) - (list) - (let - ((n (host-get node-list "length")) (result (list))) + (cond + ((nil? node-list) (list)) + ((list? node-list) node-list) + (true (let - loop - ((i 0)) + ((n (host-get node-list "length")) (result (list))) (when - (< i n) - (append! result (host-call node-list "item" i)) - (loop (+ i 1)))) - result))))) + (not (nil? n)) + (let + loop + ((i 0)) + (when + (< i n) + (append! result (host-call node-list "item" i)) + (loop (+ i 1))))) + result)))))) (define dom-query-by-id (fn (id) (host-call (dom-document) "getElementById" id))) diff --git a/shared/static/wasm/sx/hs-runtime.sx b/shared/static/wasm/sx/hs-runtime.sx index c5f0a188..3e6f7f35 100644 --- a/shared/static/wasm/sx/hs-runtime.sx +++ b/shared/static/wasm/sx/hs-runtime.sx @@ -1803,9 +1803,35 @@ (fn (p) (nth p 1)) (sort (fn (a b) (if (> (first a) (first b)) true false)) pairs))))) -(define hs-split-by (fn (s sep) (split s sep))) +(define hs-split-by (fn (s sep) (if (nil? s) nil (split s sep)))) -(define hs-joined-by (fn (col sep) (join sep col))) +(define + hs-joined-by + (fn + (col sep) + (cond + ((nil? col) nil) + ((list? col) + (join sep (map (fn (x) (if (nil? x) "" (str x))) col))) + (true col)))) + +(define + hs-sorted-by + (fn + (coll key-fn) + (if + (not (list? coll)) + coll + (sort + (fn + (a b) + (let + ((ka (key-fn a)) (kb (key-fn b))) + (cond + ((nil? ka) (not (nil? kb))) + ((nil? kb) false) + (true (< ka kb))))) + coll)))) (define hs-sorted-by diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index b88f637f..3610ef10 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -1309,7 +1309,10 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) ".divs") 1) "click" nil) (assert (not (dom-has-class? (dom-query ".divs") "foo"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) ".divs") 1) "foo")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) ".divs") 2) "foo"))) )) (deftest "can target another div" (hs-cleanup!) @@ -10816,6 +10819,7 @@ (hs-activate! _el-div) (dom-dispatch (dom-query ".divs") "click" nil) (assert= (dom-text-content (dom-query ".divs")) "foo") + (assert= (dom-text-content (nth (dom-query-all (dom-body) ".divs") 1)) "foo") )) (deftest "can set into id ref" (hs-cleanup!) @@ -11035,6 +11039,7 @@ (hs-activate! _el-trigger) (dom-dispatch (dom-query-by-id "trigger") "click" nil) (assert (dom-has-class? (dom-query ".item") "done")) + (assert (dom-has-class? (nth (dom-query-all (dom-body) ".item") 1) "done")) )) (deftest "can settle me no transition" (hs-cleanup!) @@ -11467,8 +11472,12 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (dom-has-class? _el-div "unselected")) (assert (not (dom-has-class? _el-div "\bselected\b"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "\bselected\b")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "unselected"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 2) "unselected")) )) (deftest "can take a class and swap it with another via with" (hs-cleanup!) @@ -11482,8 +11491,12 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-class? _el-div "\bselected\b"))) (assert (dom-has-class? _el-div "unselected")) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "\bselected\b")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "unselected"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 2) "unselected")) )) (deftest "can take a class for other elements" (hs-cleanup!) @@ -11498,7 +11511,9 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-d3) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-class? _el-div "foo"))) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "foo"))) (assert (dom-has-class? (dom-query-by-id "d3") "foo")) )) (deftest "can take a class from other elements" @@ -11513,7 +11528,10 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-class? _el-div "foo"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "foo")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "div") 2) "foo"))) )) (deftest "can take a class from other forms" (hs-cleanup!) @@ -11527,7 +11545,10 @@ (dom-append (dom-body) _el-form1) (dom-append (dom-body) _el-form2) (hs-activate! _el-form1) + (dom-dispatch (nth (dom-query-all (dom-body) "form") 1) "click" nil) (assert (not (dom-has-class? _el-form "foo"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "form") 1) "foo")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "form") 2) "foo"))) )) (deftest "can take an attribute for other elements" (hs-cleanup!) @@ -11542,7 +11563,9 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-d3) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-attr? _el-div "data-foo"))) + (assert (not (dom-has-attr? (nth (dom-query-all (dom-body) "div") 1) "data-foo"))) (assert= (dom-get-attr (dom-query-by-id "d3") "data-foo") "") )) (deftest "can take an attribute from other elements" @@ -11558,7 +11581,10 @@ (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) (assert= (dom-get-attr _el-div "data-foo") "bar") + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-attr? _el-div "data-foo"))) + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 1) "data-foo") "") + (assert (not (dom-has-attr? (nth (dom-query-all (dom-body) "div") 2) "data-foo"))) )) (deftest "can take an attribute value from other elements and set specific values instead" (hs-cleanup!) @@ -11572,7 +11598,10 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert= (dom-get-attr _el-div "data-foo") "qux") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 1) "data-foo") "baz") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 2) "data-foo") "qux") )) (deftest "can take an attribute value from other elements and set value from an expression instead" (hs-cleanup!) @@ -11587,7 +11616,10 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert= (dom-get-attr _el-div "data-foo") "qux") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 1) "data-foo") "baz") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 2) "data-foo") "qux") )) (deftest "can take an attribute with specific value from other elements" (hs-cleanup!) @@ -11602,7 +11634,10 @@ (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) (assert= (dom-get-attr _el-div "data-foo") "bar") + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-attr? _el-div "data-foo"))) + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 1) "data-foo") "baz") + (assert (not (dom-has-attr? (nth (dom-query-all (dom-body) "div") 2) "data-foo"))) )) (deftest "can take multiple classes from other elements" (hs-cleanup!) @@ -11617,7 +11652,11 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-class? _el-div "foo"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "foo")) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "bar")) + (assert (not (dom-has-class? (nth (dom-query-all (dom-body) "div") 2) "bar"))) )) (deftest "can take multiple classes from specific element" (hs-cleanup!) @@ -11633,8 +11672,12 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert (not (dom-has-class? _el-div "foo"))) (assert (not (dom-has-class? _el-div "bar"))) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "foo")) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 1) "bar")) + (assert (dom-has-class? (nth (dom-query-all (dom-body) "div") 2) "bar")) )) (deftest "giving may follow the from clause as an alternative to with" (hs-cleanup!) @@ -11648,7 +11691,10 @@ (dom-append (dom-body) _el-div1) (dom-append (dom-body) _el-div2) (hs-activate! _el-div1) + (dom-dispatch (nth (dom-query-all (dom-body) "div") 1) "click" nil) (assert= (dom-get-attr _el-div "data-foo") "qux") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 1) "data-foo") "baz") + (assert= (dom-get-attr (nth (dom-query-all (dom-body) "div") 2) "data-foo") "qux") )) ) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index f467a97f..9a39fcdb 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -176,17 +176,30 @@ function parseHTMLFragments(html) { function mt(e,s) { if(!e||!e.tagName)return false; s = s.trim(); - if(s.startsWith('#'))return e.id===s.slice(1); - if(s.startsWith('.'))return e.classList.contains(s.slice(1)); - if(s.startsWith('[')) { - const m = s.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/); + // Strip pseudo-classes we handle at the fnd() level (nth-of-type, first/last-of-type) + const pseudo = s.match(/^(.*?):(nth-of-type|first-of-type|last-of-type)(?:\((\d+)\))?$/); + const base = pseudo ? pseudo[1] : s; + if(base.startsWith('#'))return e.id===base.slice(1); + if(base.startsWith('.'))return e.classList.contains(base.slice(1)); + if(base.startsWith('[')) { + const m = base.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/); if(m) return m[2] !== undefined ? e.getAttribute(m[1]) === m[2] : e.hasAttribute(m[1]); } - if(s.includes('.')) { const [tag, cls] = s.split('.'); return e.tagName.toLowerCase() === tag && e.classList.contains(cls); } - if(s.includes('#')) { const [tag, id] = s.split('#'); return e.tagName.toLowerCase() === tag && e.id === id; } - return e.tagName.toLowerCase() === s.toLowerCase(); + if(base.includes('.')) { const [tag, cls] = base.split('.'); return e.tagName.toLowerCase() === tag && e.classList.contains(cls); } + if(base.includes('#')) { const [tag, id] = base.split('#'); return e.tagName.toLowerCase() === tag && e.id === id; } + return e.tagName.toLowerCase() === base.toLowerCase(); +} +function fnd(e,s) { + const pseudo = s && typeof s==='string' ? s.match(/^(.*?):(nth-of-type|first-of-type|last-of-type)(?:\((\d+)\))?$/) : null; + if (pseudo) { + const all = fndAll(e, pseudo[1]); + if (pseudo[2] === 'first-of-type') return all[0] || null; + if (pseudo[2] === 'last-of-type') return all[all.length - 1] || null; + const idx = parseInt(pseudo[3] || '1') - 1; + return all[idx] || null; + } + for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; } -function fnd(e,s) { for(const c of(e.children||[])){if(mt(c,s))return c;const f=fnd(c,s);if(f)return f;} return null; } function fndAll(e,s) { const r=[];for(const c of(e.children||[])){if(mt(c,s))r.push(c);r.push(...fndAll(c,s));}r.item=function(i){return r[i]||null;};return r; } const _body = new El('body'); diff --git a/tests/playwright/generate-sx-tests.py b/tests/playwright/generate-sx-tests.py index 87549424..4b4c9642 100644 --- a/tests/playwright/generate-sx-tests.py +++ b/tests/playwright/generate-sx-tests.py @@ -796,16 +796,19 @@ def parse_dev_body(body, elements, var_names): def add_action(stmt): am = re.search( - r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?" + r"find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\)|\.nth\((\d+)\))?" r"\.(click|dispatchEvent|fill|check|uncheck|focus|selectOption)\(([^)]*)\)", stmt, ) if not am or 'expect' in stmt: return False selector = am.group(2) - action_type = am.group(3) - action_arg = am.group(4).strip("'\"") + nth_idx = am.group(3) + action_type = am.group(4) + action_arg = am.group(5).strip("'\"") target = selector_to_sx(selector, elements, var_names) + if nth_idx is not None: + target = f'(nth (dom-query-all (dom-body) "{selector}") {nth_idx})' if action_type == 'click': ops.append(f'(dom-dispatch {target} "click" nil)') elif action_type == 'dispatchEvent': @@ -830,7 +833,7 @@ def parse_dev_body(body, elements, var_names): def add_assertion(stmt): em = re.search( - r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\))?\)\.(not\.)?" + r"expect\(find\((['\"])(.+?)\1\)(?:\.(?:first|last)\(\)|\.nth\((\d+)\))?\)\.(not\.)?" r"(toHaveText|toHaveClass|toHaveCSS|toHaveAttribute|toHaveValue|toBeVisible|toBeHidden|toBeChecked)" r"\(((?:[^()]|\([^()]*\))*)\)", stmt, @@ -838,10 +841,13 @@ def parse_dev_body(body, elements, var_names): if not em: return False selector = em.group(2) - negated = bool(em.group(3)) - assert_type = em.group(4) - args_str = em.group(5) + nth_idx = em.group(3) + negated = bool(em.group(4)) + assert_type = em.group(5) + args_str = em.group(6) target = selector_to_sx(selector, elements, var_names) + if nth_idx is not None: + target = f'(nth (dom-query-all (dom-body) "{selector}") {nth_idx})' sx = pw_assertion_to_sx(target, negated, assert_type, args_str) if sx: ops.append(sx)