ecss/tests/ecss-selector-tests.el
2026-08-25 17:16:55 +08:00

196 lines
9.4 KiB
EmacsLisp

;;; ecss-selector-tests.el --- Selector tests -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Geekinney
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Code:
(require 'ert)
(require 'ecss-selector)
(defun ecss-selector-test--tree ()
"Return a small subject tree for selector tests."
(let* ((label (ecss-subject-create
:type "span" :classes '("label")
:attributes '(("lang" . "en-US"))))
(button (ecss-subject-create
:type "button" :id "save" :classes '("primary" "action")
:attributes '(("data-kind" . "commit")
("data-tags" . "fast safe"))
:states '("hover") :children (list label)))
(cancel (ecss-subject-create
:type "button" :id "cancel" :classes '("action")))
(root (ecss-subject-create
:type "div" :classes '("app")
:children (list button cancel))))
(list root button cancel label)))
(ert-deftest ecss-selector-test-parser-matches-compounds-and-combinators ()
(pcase-let ((`(,_root ,button ,cancel ,label)
(ecss-selector-test--tree)))
(should (ecss-selector-match-p
"div.app > button#save.primary:hover > span.label" label))
(should (ecss-selector-match-p "div .label" label))
(should (ecss-selector-match-p "button + button#cancel" cancel))
(should (ecss-selector-match-p "button ~ button#cancel" cancel))
(should-not (ecss-selector-match-p "button#cancel" button))))
(ert-deftest ecss-selector-test-attribute-operators-and-flags ()
(pcase-let ((`(,_root ,button ,_cancel ,label)
(ecss-selector-test--tree)))
(should (ecss-selector-match-p "[data-kind]" button))
(should (ecss-selector-match-p "[data-kind=commit]" button))
(should (ecss-selector-match-p "[data-tags~=safe]" button))
(should (ecss-selector-match-p "[lang|=en]" label))
(should (ecss-selector-match-p "[data-kind^=com]" button))
(should (ecss-selector-match-p "[data-kind$=MIT i]" button))
(should-not (ecss-selector-match-p "[data-kind$=MIT s]" button))
(should (ecss-selector-match-p "[data-kind*=omm]" button))
(should-not (ecss-selector-match-p "[data-kind=other]" button))))
(ert-deftest ecss-selector-test-functional-pseudos-and-specificity ()
(pcase-let ((`(,root ,button ,_cancel ,_label)
(ecss-selector-test--tree)))
(should (ecss-selector-match-p "button:is(.primary, #other)" button))
(should (ecss-selector-match-p "button:not(.disabled)" button))
(should (ecss-selector-match-p "div:has(span.label)" root))
(should (equal (ecss-selector-specificity
"button:is(.primary, #other)")
'(1 0 1)))
(should (equal (ecss-selector-specificity
"button:where(.primary, #other)")
'(0 0 1)))))
(ert-deftest ecss-selector-test-has-supports-relative-combinators ()
(let* ((direct (ecss-subject-create :type "button" :classes '("direct")))
(nested (ecss-subject-create :type "span" :classes '("nested")))
(wrapper (ecss-subject-create :type "section" :children (list nested)))
(root (ecss-subject-create
:type "div" :children (list direct wrapper)))
(adjacent (ecss-subject-create :type "button" :id "adjacent"))
(later (ecss-subject-create :type "button" :id "later"))
(_siblings (ecss-subject-create
:type "row" :children (list root adjacent later))))
(should (ecss-selector-match-p "div:has(> button.direct)" root))
(should (ecss-selector-match-p "div:has(> section .nested)" root))
(should (ecss-selector-match-p "div:has(+ button#adjacent)" root))
(should (ecss-selector-match-p "div:has(~ button#later)" root))
(should-not (ecss-selector-match-p "div:has(> span.nested)" root))))
(ert-deftest ecss-selector-test-selector-lists-use-matching-branches ()
(pcase-let ((`(,_root ,button ,_cancel ,_label)
(ecss-selector-test--tree)))
(let ((selector (ecss-selector-parse "#other, .primary")))
(should (ecss-selector-match-p selector button))
(should (equal (ecss--selector-matched-specificity
selector button ecss-default-subject-adapter)
'(0 1 0))))))
(ert-deftest ecss-selector-test-structured-tokens-use-value-equality ()
(let ((subject (ecss-subject-create
:type (copy-sequence "button")
:classes (list (copy-sequence "primary"))
:states (list (copy-sequence "focus")))))
(should (ecss-selector-match-p '(:type "button") subject))
(should (ecss-selector-match-p '(:class "primary") subject))
(should (ecss-selector-match-p '(:state "focus") subject))))
(ert-deftest ecss-selector-test-normalize-copies-structured-selector-data ()
(let* ((token (copy-sequence "target"))
(selector (list :class token))
(normalized (ecss-selector-normalize selector)))
(aset (cadr normalized) 0 ?X)
(should (equal selector '(:class "target")))
(aset token 1 ?Y)
(should (equal normalized '(:class "Xarget")))))
(ert-deftest ecss-selector-test-subject-snapshots-mutable-fields ()
(let* ((type (copy-sequence "button"))
(id (copy-sequence "save"))
(classes (list (copy-sequence "primary")))
(attributes (list (cons "role" (copy-sequence "submit"))))
(states (list (copy-sequence "active")))
(subject (ecss-subject-create
:type type :id id :classes classes
:attributes attributes :states states)))
(aset type 0 ?X)
(aset id 0 ?X)
(aset (car classes) 0 ?X)
(aset (cdar attributes) 0 ?X)
(aset (car states) 0 ?X)
(should (equal (ecss-subject-type subject) "button"))
(should (equal (ecss-subject-id subject) "save"))
(should (equal (ecss-subject-classes subject) '("primary")))
(should (equal (ecss-subject-attributes subject) '(("role" . "submit"))))
(should (equal (ecss-subject-states subject) '("active")))))
(ert-deftest ecss-selector-test-caller-owned-subject-adapter ()
(let* ((root (vector "panel" "root" '("app") nil nil nil nil))
(child (vector "button" "save" '("primary")
'(("role" . "submit")) '("active") root nil))
(adapter
(ecss-subject-adapter-create
:type (lambda (node) (aref node 0))
:id (lambda (node) (aref node 1))
:classes (lambda (node) (aref node 2))
:attributes (lambda (node) (aref node 3))
:states (lambda (node) (aref node 4))
:parent (lambda (node) (aref node 5))
:children (lambda (node) (aref node 6)))))
(aset root 6 (list child))
(should (ecss-selector-match-p
"panel.app > button#save[role=submit]:active" child adapter))
(should (ecss-selector-match-p
"panel:has(button.primary)" root adapter))))
(ert-deftest ecss-selector-test-invalid-input-is-rejected ()
(should-error (ecss-selector-parse "") :type 'ecss-invalid-selector)
(should-error (ecss-selector-parse "div >") :type 'ecss-invalid-selector)
(should-error (ecss-selector-parse "[name=value q]")
:type 'ecss-invalid-selector)
(should-error (ecss-selector-normalize '(:unknown value))
:type 'ecss-invalid-selector))
(ert-deftest ecss-selector-test-reports-subject-local-dependencies ()
"Selector dependency classification distinguishes tree-sensitive forms."
(dolist (selector
'("*" ".card" "button#save.primary[role=submit]:active"
"button:is(.primary, [role=submit])"
".card:not(.disabled)" ".card, #fallback"))
(should (ecss-selector-subject-local-p selector)))
(dolist (selector
'("section .card" "section > .card" ".active + .target"
".active ~ .target" "section:has(.card)"))
(should-not (ecss-selector-subject-local-p selector)))
(should-not (ecss-selector-subject-local-p '(:anchor)))
(should-error (ecss-selector-subject-local-p '(:unknown value))
:type 'ecss-invalid-selector))
(ert-deftest ecss-selector-test-comments-escapes-and-quoted-attributes ()
(let ((subject (ecss-subject-create
:type "div" :classes '("card" "a:b")
:attributes '(("title" . "Save now")))))
(should (ecss-selector-match-p "div/* comment */.card" subject))
(should (ecss-selector-match-p ".a\\:b" subject))
(should (ecss-selector-match-p "[title=\"Save now\"]" subject))))
(ert-deftest ecss-selector-test-subject-reparenting-detaches-old-parent ()
(let* ((child (ecss-subject-create :type "button"))
(old-parent (ecss-subject-create
:type "old" :children (list child)))
(new-parent (ecss-subject-create :type "new")))
(ecss-subject-set-children new-parent (list child))
(should (eq (ecss-subject-parent child) new-parent))
(should-not (memq child (ecss-subject-children old-parent)))
(should (memq child (ecss-subject-children new-parent)))))
(ert-deftest ecss-selector-test-subject-parent-construction-is-bidirectional ()
(let* ((parent (ecss-subject-create :type "panel"))
(child (ecss-subject-create :type "button" :parent parent)))
(should (eq (ecss-subject-parent child) parent))
(should (equal (ecss-subject-children parent) (list child)))
(should (ecss-selector-match-p "panel:has(> button)" parent))))
(provide 'ecss-selector-tests)
;;; ecss-selector-tests.el ends here