Skip to main content

rdom_core/
query_selector.rs

1//! Matcher + public query_selector API.
2//!
3//! `Dom::query_selector_in(root, selector)` finds the first descendant
4//! matching `selector`. `query_selector_all_in` returns every match.
5//! `matches` tests a single node. `closest` walks ancestors finding the
6//! first match. The DOM-shaped one-arg shortcuts
7//! [`Dom::query_selector`](crate::Dom::query_selector) /
8//! [`Dom::query_selector_all`](crate::Dom::query_selector_all) live on
9//! `Dom` directly and pass `self.root()` as the root_id.
10//!
11//! The matcher evaluates each `ComplexSelector` right-to-left starting from
12//! the candidate element (subject), then walks ancestors/siblings per
13//! combinator. Good enough for small-to-medium subtrees; Phase 4 will add
14//! indexes and selector "bloom" fast-rejection.
15
16use crate::dom::Dom;
17use crate::node::NodeData;
18use crate::node_id::NodeId;
19use crate::selectors::{
20    self, AttrOp, Combinator, CompoundSelector, ParseError, PseudoClass, SelectorList,
21    SimpleSelector,
22};
23
24impl<Ext> Dom<Ext> {
25    /// Whether `id` matches `:placeholder-shown`: it has a non-empty
26    /// `placeholder` attribute and no text content, i.e. it is showing
27    /// its placeholder hint. Also what decides whether `::placeholder`
28    /// rules style anything.
29    pub fn is_placeholder_shown(&self, id: NodeId) -> bool {
30        self.get_attribute(id, "placeholder")
31            .is_some_and(|v| !v.is_empty())
32            && self.text_content(id).is_empty()
33    }
34
35    /// Find the first descendant of `root_id` matching `selector`, in
36    /// document order. Returns `None` if none matches. Errors if the
37    /// selector is malformed.
38    ///
39    /// The DOM-shaped one-arg form is [`Dom::query_selector`]; this
40    /// `_in` form is the explicit-root variant (M4b step 18 rename).
41    pub fn query_selector_in(
42        &self,
43        root_id: NodeId,
44        selector: &str,
45    ) -> Result<Option<NodeId>, ParseError> {
46        let list = selectors::parse(selector)?;
47        let mut found = None;
48        self.walk_descendants(root_id, &mut |id, data| {
49            if found.is_some() {
50                return;
51            }
52            if let NodeData::Element { .. } = data
53                && self.matches_list(id, &list)
54            {
55                found = Some(id);
56            }
57        });
58        Ok(found)
59    }
60
61    /// All descendants of `root_id` matching `selector`, in document
62    /// order. The DOM-shaped one-arg form is
63    /// [`Dom::query_selector_all`]; this `_in` form is the
64    /// explicit-root variant (M4b step 18 rename).
65    pub fn query_selector_all_in(
66        &self,
67        root_id: NodeId,
68        selector: &str,
69    ) -> Result<Vec<NodeId>, ParseError> {
70        let list = selectors::parse(selector)?;
71        let mut out = Vec::new();
72        self.walk_descendants(root_id, &mut |id, data| {
73            if matches!(data, NodeData::Element { .. }) && self.matches_list(id, &list) {
74                out.push(id);
75            }
76        });
77        Ok(out)
78    }
79
80    /// Does `id` match `selector`? Errors on malformed selector.
81    pub fn matches(&self, id: NodeId, selector: &str) -> Result<bool, ParseError> {
82        let list = selectors::parse(selector)?;
83        Ok(self.matches_list(id, &list))
84    }
85
86    /// Walk from `id` (inclusive) up the tree and return the first ancestor
87    /// that matches `selector`. `None` if none does.
88    pub fn closest(&self, id: NodeId, selector: &str) -> Result<Option<NodeId>, ParseError> {
89        let list = selectors::parse(selector)?;
90        let mut cur = Some(id);
91        while let Some(c) = cur {
92            if matches!(
93                self.get_node(c).map(|n| &n.data),
94                Some(NodeData::Element { .. })
95            ) && self.matches_list(c, &list)
96            {
97                return Ok(Some(c));
98            }
99            cur = self.get_node(c).and_then(|n| n.parent);
100        }
101        Ok(None)
102    }
103
104    // ─── Matcher ─────────────────────────────────────────────────────
105
106    /// Does `id` match any selector in `list`?
107    /// Does `id` match any selector in the pre-parsed `list`? Public so
108    /// downstream crates (rdom-tui's cascade) can drive rule matching
109    /// without re-parsing selector strings on every call.
110    pub fn matches_list(&self, id: NodeId, list: &SelectorList) -> bool {
111        list.0
112            .iter()
113            .any(|complex| self.matches_complex(id, complex))
114    }
115
116    fn matches_complex(&self, id: NodeId, complex: &selectors::ComplexSelector) -> bool {
117        // Subject must match.
118        if !self.matches_compound(id, &complex.subject) {
119            return false;
120        }
121        // Walk ancestors/siblings per combinator. Each step's "candidate
122        // pointer" represents the node we're trying to match against the
123        // next compound on the outward path.
124        let mut cur = id;
125        for (comb, compound) in &complex.ancestors {
126            match comb {
127                Combinator::Descendant => {
128                    let mut anc = self.get_node(cur).and_then(|n| n.parent);
129                    let mut matched = None;
130                    while let Some(a) = anc {
131                        if self.matches_compound(a, compound) {
132                            matched = Some(a);
133                            break;
134                        }
135                        anc = self.get_node(a).and_then(|n| n.parent);
136                    }
137                    match matched {
138                        Some(a) => cur = a,
139                        None => return false,
140                    }
141                }
142                Combinator::Child => {
143                    let Some(parent) = self.get_node(cur).and_then(|n| n.parent) else {
144                        return false;
145                    };
146                    if !self.matches_compound(parent, compound) {
147                        return false;
148                    }
149                    cur = parent;
150                }
151                Combinator::AdjacentSibling => {
152                    let Some(prev) = self.get_node(cur).and_then(|n| n.prev_sibling) else {
153                        return false;
154                    };
155                    if !self.matches_compound(prev, compound) {
156                        return false;
157                    }
158                    cur = prev;
159                }
160                Combinator::GeneralSibling => {
161                    let mut sib = self.get_node(cur).and_then(|n| n.prev_sibling);
162                    let mut matched = None;
163                    while let Some(s) = sib {
164                        if self.matches_compound(s, compound) {
165                            matched = Some(s);
166                            break;
167                        }
168                        sib = self.get_node(s).and_then(|n| n.prev_sibling);
169                    }
170                    match matched {
171                        Some(s) => cur = s,
172                        None => return false,
173                    }
174                }
175            }
176        }
177        true
178    }
179
180    fn matches_compound(&self, id: NodeId, compound: &CompoundSelector) -> bool {
181        let Some(node) = self.get_node(id) else {
182            return false;
183        };
184        let NodeData::Element {
185            tag,
186            attrs,
187            classes,
188            ..
189        } = &node.data
190        else {
191            return false;
192        };
193        for s in &compound.simples {
194            match s {
195                SimpleSelector::Universal => {}
196                SimpleSelector::Type(t) => {
197                    if tag != t {
198                        return false;
199                    }
200                }
201                SimpleSelector::Id(v) => {
202                    if attrs.get("id").map(String::as_str) != Some(v.as_str()) {
203                        return false;
204                    }
205                }
206                SimpleSelector::Class(c) => {
207                    if !classes.contains(c) {
208                        return false;
209                    }
210                }
211                SimpleSelector::Attribute { name, op, value } => {
212                    if !match_attribute(attrs, name, *op, value.as_deref()) {
213                        return false;
214                    }
215                }
216                SimpleSelector::Not(inner) => {
217                    if self.matches_list(id, inner) {
218                        return false;
219                    }
220                }
221                SimpleSelector::Where(inner) => {
222                    // Matches like `:is()` — any complex selector in the list
223                    // must match this element as its subject. Specificity is
224                    // handled (as zero) by `rdom-style`.
225                    if !self.matches_list(id, inner) {
226                        return false;
227                    }
228                }
229                SimpleSelector::Pseudo(p) => {
230                    if !self.match_pseudo(id, *p) {
231                        return false;
232                    }
233                }
234            }
235        }
236        true
237    }
238
239    fn match_pseudo(&self, id: NodeId, p: PseudoClass) -> bool {
240        let Some(node) = self.get_node(id) else {
241            return false;
242        };
243        match p {
244            PseudoClass::FirstChild => {
245                // No previous *element* sibling.
246                self.prev_element_sibling_id(id).is_none() && node.parent.is_some()
247            }
248            PseudoClass::LastChild => {
249                self.next_element_sibling_id(id).is_none() && node.parent.is_some()
250            }
251            PseudoClass::OnlyChild => {
252                node.parent.is_some()
253                    && self.prev_element_sibling_id(id).is_none()
254                    && self.next_element_sibling_id(id).is_none()
255            }
256            PseudoClass::Empty => {
257                // Selectors 4 §14.2: no element children and no text
258                // children with non-empty data — comments and zero-length
259                // text nodes do not count; whitespace-only text does
260                // (Level 3, and browsers today).
261                let mut c = node.first_child;
262                while let Some(cid) = c {
263                    let Some(cn) = self.get_node(cid) else {
264                        return false;
265                    };
266                    match &cn.data {
267                        NodeData::Element { .. } => return false,
268                        NodeData::Text { data } if !data.is_empty() => return false,
269                        _ => {}
270                    }
271                    c = cn.next_sibling;
272                }
273                true
274            }
275            PseudoClass::Root => id == self.root(),
276            // Selectors 4 §9.2 / §9.4 / §13.3: `:hover`, `:active` and
277            // `:focus-within` match the element holding the state and
278            // every ancestor of it (the flat tree is the node tree:
279            // rdom has no shadow roots).
280            PseudoClass::Hover => self.hovered().is_some_and(|h| self.is_ancestor(id, h)),
281            PseudoClass::Active => self.active().is_some_and(|h| self.is_ancestor(id, h)),
282            PseudoClass::Focus => self.focused() == Some(id),
283            PseudoClass::FocusVisible => self.focus_visible && self.focused() == Some(id),
284            PseudoClass::FocusWithin => self.focused().is_some_and(|h| self.is_ancestor(id, h)),
285            PseudoClass::Checked => self
286                .get_node(id)
287                .map(|n| match &n.data {
288                    NodeData::Element { attrs, .. } => attrs.contains_key("checked"),
289                    _ => false,
290                })
291                .unwrap_or(false),
292            PseudoClass::PlaceholderShown => self.is_placeholder_shown(id),
293            PseudoClass::Indeterminate => self
294                .get_node(id)
295                .map(|n| match &n.data {
296                    NodeData::Element { tag, attrs, .. } => {
297                        // v1: only `<progress>` without `value`
298                        // attribute. Checkbox `indeterminate` IDL
299                        // property + orphan radios deferred to
300                        // polish.
301                        tag == "progress" && !attrs.contains_key("value")
302                    }
303                    _ => false,
304                })
305                .unwrap_or(false),
306            PseudoClass::Open => self
307                .get_node(id)
308                .map(|n| match &n.data {
309                    NodeData::Element { attrs, .. } => attrs.contains_key("open"),
310                    _ => false,
311                })
312                .unwrap_or(false),
313            PseudoClass::Disabled => self.is_actually_disabled(id),
314            PseudoClass::Enabled => self.is_enabled_control(id),
315            PseudoClass::Valid => self.constraint_validity(id) == Some(true),
316            PseudoClass::Invalid => self.constraint_validity(id) == Some(false),
317            PseudoClass::Required => self.is_required_control(id),
318            PseudoClass::Optional => self.is_optional_control(id),
319        }
320    }
321
322    fn prev_element_sibling_id(&self, id: NodeId) -> Option<NodeId> {
323        let mut cur = self.get_node(id).and_then(|n| n.prev_sibling);
324        while let Some(c) = cur {
325            let n = self.get_node(c)?;
326            if matches!(n.data, NodeData::Element { .. }) {
327                return Some(c);
328            }
329            cur = n.prev_sibling;
330        }
331        None
332    }
333
334    fn next_element_sibling_id(&self, id: NodeId) -> Option<NodeId> {
335        let mut cur = self.get_node(id).and_then(|n| n.next_sibling);
336        while let Some(c) = cur {
337            let n = self.get_node(c)?;
338            if matches!(n.data, NodeData::Element { .. }) {
339                return Some(c);
340            }
341            cur = n.next_sibling;
342        }
343        None
344    }
345}
346
347/// HTML §4.16.2 "case-sensitivity of selectors": attribute selectors on
348/// an HTML element treat the values of these attributes as ASCII
349/// case-insensitive (a `match`: this runs on every attribute-selector
350/// test of the cascade). Every rdom element is an HTML element in an HTML
351/// document. (HTML exempts `type` in its rendering section's `ol[type]`
352/// rules via the `s` flag, which rdom does not parse.)
353fn is_html_case_insensitive_attr(name: &str) -> bool {
354    matches!(
355        name,
356        "accept"
357            | "accept-charset"
358            | "align"
359            | "alink"
360            | "axis"
361            | "bgcolor"
362            | "charset"
363            | "checked"
364            | "clear"
365            | "codetype"
366            | "color"
367            | "compact"
368            | "declare"
369            | "defer"
370            | "dir"
371            | "direction"
372            | "disabled"
373            | "enctype"
374            | "face"
375            | "frame"
376            | "hreflang"
377            | "http-equiv"
378            | "lang"
379            | "language"
380            | "link"
381            | "media"
382            | "method"
383            | "multiple"
384            | "nohref"
385            | "noresize"
386            | "noshade"
387            | "nowrap"
388            | "readonly"
389            | "rel"
390            | "rev"
391            | "rules"
392            | "scope"
393            | "scrolling"
394            | "selected"
395            | "shape"
396            | "target"
397            | "text"
398            | "type"
399            | "valign"
400            | "valuetype"
401            | "vlink"
402    )
403}
404
405fn match_attribute(
406    attrs: &std::collections::BTreeMap<String, String>,
407    name: &str,
408    op: Option<AttrOp>,
409    want: Option<&str>,
410) -> bool {
411    let Some(have) = attrs.get(name) else {
412        return false;
413    };
414    let Some(op) = op else { return true }; // `[name]` — presence only.
415    let want = want.unwrap_or("");
416    if is_html_case_insensitive_attr(name) {
417        match_value::<AsciiCaseInsensitive>(op, have, want)
418    } else {
419        match_value::<CaseSensitive>(op, have, want)
420    }
421}
422
423/// How [`match_value`] compares attribute-value bytes. Comparing bytes
424/// is sound for UTF-8: ASCII case folding never touches the bytes of a
425/// multi-byte character, so a match starts and ends on char boundaries
426/// whenever `want` is valid UTF-8.
427trait ValueCase {
428    fn eq(a: &[u8], b: &[u8]) -> bool;
429
430    /// `want` (non-empty) occurs in `have`.
431    fn contains(have: &str, want: &str) -> bool {
432        have.as_bytes()
433            .windows(want.len())
434            .any(|win| Self::eq(win, want.as_bytes()))
435    }
436}
437
438struct CaseSensitive;
439impl ValueCase for CaseSensitive {
440    fn eq(a: &[u8], b: &[u8]) -> bool {
441        a == b
442    }
443
444    fn contains(have: &str, want: &str) -> bool {
445        have.contains(want)
446    }
447}
448
449/// HTML §4.16.2: ASCII case-insensitive, without allocating.
450struct AsciiCaseInsensitive;
451impl ValueCase for AsciiCaseInsensitive {
452    fn eq(a: &[u8], b: &[u8]) -> bool {
453        a.eq_ignore_ascii_case(b)
454    }
455}
456
457/// Selectors 4 §6.1 / §6.2: the attribute-value operators, comparing
458/// per `C`.
459fn match_value<C: ValueCase>(op: AttrOp, have: &str, want: &str) -> bool {
460    let (h, w) = (have.as_bytes(), want.as_bytes());
461    let starts = |h: &[u8]| h.get(..w.len()).is_some_and(|p| C::eq(p, w));
462    match op {
463        AttrOp::Exact => C::eq(h, w),
464        AttrOp::Includes => have
465            .split_ascii_whitespace()
466            .any(|tok| C::eq(tok.as_bytes(), w)),
467        AttrOp::DashMatch => C::eq(h, w) || (starts(h) && h.get(w.len()) == Some(&b'-')),
468        AttrOp::Prefix => !w.is_empty() && starts(h),
469        AttrOp::Suffix => !w.is_empty() && h.len() >= w.len() && C::eq(&h[h.len() - w.len()..], w),
470        AttrOp::Substring => !w.is_empty() && C::contains(have, want),
471    }
472}
473
474// ─── Tests ───────────────────────────────────────────────────────────
475
476#[cfg(test)]
477mod tests {
478    use crate::{Dom, NodeId};
479
480    // Build:
481    //   root
482    //     div#a.outer
483    //       span.first
484    //       span.mid lang="en"
485    //       p.last
486    //         em "leaf"
487    fn build() -> (Dom, [NodeId; 5]) {
488        let mut dom: Dom = Dom::new();
489        let root = dom.root();
490        let div = dom.create_element("div");
491        dom.set_attribute(div, "id", "a").unwrap();
492        dom.add_class(div, "outer").unwrap();
493
494        let s1 = dom.create_element("span");
495        dom.add_class(s1, "first").unwrap();
496
497        let s2 = dom.create_element("span");
498        dom.add_class(s2, "mid").unwrap();
499        dom.set_attribute(s2, "lang", "en-US").unwrap();
500
501        let p = dom.create_element("p");
502        dom.add_class(p, "last").unwrap();
503
504        let em = dom.create_element("em");
505        let t = dom.create_text_node("leaf");
506        dom.append_child(em, t).unwrap();
507        dom.append_child(p, em).unwrap();
508
509        dom.append_child(div, s1).unwrap();
510        dom.append_child(div, s2).unwrap();
511        dom.append_child(div, p).unwrap();
512        dom.append_child(root, div).unwrap();
513
514        (dom, [div, s1, s2, p, em])
515    }
516
517    #[test]
518    fn matches_type() {
519        let (dom, [div, ..]) = build();
520        assert!(dom.matches(div, "div").unwrap());
521        assert!(!dom.matches(div, "span").unwrap());
522    }
523
524    #[test]
525    fn matches_id() {
526        let (dom, [div, s1, ..]) = build();
527        assert!(dom.matches(div, "#a").unwrap());
528        assert!(!dom.matches(s1, "#a").unwrap());
529    }
530
531    #[test]
532    fn matches_class() {
533        let (dom, [_, s1, ..]) = build();
534        assert!(dom.matches(s1, ".first").unwrap());
535        assert!(!dom.matches(s1, ".missing").unwrap());
536    }
537
538    #[test]
539    fn matches_attribute_variants() {
540        let (dom, [_, _, s2, ..]) = build();
541        assert!(dom.matches(s2, "[lang]").unwrap());
542        assert!(dom.matches(s2, "[lang=en-US]").unwrap());
543        assert!(dom.matches(s2, "[lang|=en]").unwrap());
544        assert!(dom.matches(s2, "[lang^=en]").unwrap());
545        assert!(dom.matches(s2, "[lang$=US]").unwrap());
546        assert!(dom.matches(s2, "[lang*=n-U]").unwrap());
547        assert!(!dom.matches(s2, "[lang=fr]").unwrap());
548    }
549
550    #[test]
551    fn matches_compound() {
552        let (dom, [div, ..]) = build();
553        assert!(dom.matches(div, "div#a.outer").unwrap());
554        assert!(!dom.matches(div, "div#b.outer").unwrap());
555    }
556
557    #[test]
558    fn query_selector_descendant() {
559        let (dom, [_, s1, ..]) = build();
560        let root = dom.root();
561        assert_eq!(dom.query_selector_in(root, "div .first").unwrap(), Some(s1));
562    }
563
564    #[test]
565    fn query_selector_child_combinator() {
566        let (dom, [_, _, _, _, em]) = build();
567        let root = dom.root();
568        // em is inside p which is inside div — only matches as descendant, not child of div.
569        assert!(dom.query_selector_in(root, "div > em").unwrap().is_none());
570        assert_eq!(dom.query_selector_in(root, "p > em").unwrap(), Some(em));
571    }
572
573    #[test]
574    fn query_selector_adjacent_sibling() {
575        let (dom, [_, _, s2, ..]) = build();
576        let root = dom.root();
577        assert_eq!(
578            dom.query_selector_in(root, ".first + .mid").unwrap(),
579            Some(s2)
580        );
581        assert!(
582            dom.query_selector_in(root, ".first + .last")
583                .unwrap()
584                .is_none()
585        );
586    }
587
588    #[test]
589    fn query_selector_general_sibling() {
590        let (dom, [_, _, _, p, _]) = build();
591        let root = dom.root();
592        assert_eq!(
593            dom.query_selector_in(root, ".first ~ .last").unwrap(),
594            Some(p)
595        );
596    }
597
598    #[test]
599    fn query_selector_all_returns_document_order() {
600        let (dom, [_, s1, s2, ..]) = build();
601        let root = dom.root();
602        let spans = dom.query_selector_all_in(root, "span").unwrap();
603        assert_eq!(spans, vec![s1, s2]);
604    }
605
606    #[test]
607    fn query_selector_list_union() {
608        let (dom, [_, _, _, p, em]) = build();
609        let root = dom.root();
610        let r = dom.query_selector_all_in(root, "p, em").unwrap();
611        assert_eq!(r, vec![p, em]);
612    }
613
614    #[test]
615    fn not_pseudo_excludes_matches() {
616        let (dom, _) = build();
617        let root = dom.root();
618        let r = dom.query_selector_all_in(root, "span:not(.first)").unwrap();
619        assert_eq!(r.len(), 1);
620    }
621
622    #[test]
623    fn where_pseudo_matches_like_is() {
624        let (dom, [_div, s1, _s2, p, em]) = build();
625        let root = dom.root();
626        // :where(list) matches an element matching any item in the list.
627        let r = dom
628            .query_selector_all_in(root, ":where(.first, .last)")
629            .unwrap();
630        assert_eq!(r, vec![s1, p]);
631        // Combinators inside :where() are honored (em is a descendant of div).
632        assert!(dom.matches(em, ":where(div em)").unwrap());
633        assert!(!dom.matches(s1, ":where(div em)").unwrap());
634    }
635
636    #[test]
637    fn first_and_last_child_pseudos() {
638        let (dom, [div, s1, _, p, em]) = build();
639        let root = dom.root();
640        // Every element that IS the first child of its parent: div (first of
641        // root), s1 (first of div), em (first of p). Document-order pick: div.
642        assert_eq!(
643            dom.query_selector_in(root, ":first-child").unwrap(),
644            Some(div)
645        );
646        // `span:first-child` scopes to spans — only s1 qualifies.
647        assert_eq!(
648            dom.query_selector_in(root, "span:first-child").unwrap(),
649            Some(s1)
650        );
651        // Last children of each parent: div, p, em.
652        let lasts = dom.query_selector_all_in(root, ":last-child").unwrap();
653        assert!(lasts.contains(&p));
654        assert!(lasts.contains(&em));
655    }
656
657    #[test]
658    fn only_child_pseudo() {
659        let (dom, [_, _, _, _, em]) = build();
660        let root = dom.root();
661        // em is the only child of p.
662        assert_eq!(
663            dom.query_selector_in(root, "em:only-child").unwrap(),
664            Some(em)
665        );
666    }
667
668    #[test]
669    fn empty_pseudo() {
670        let mut dom: Dom = Dom::new();
671        let root = dom.root();
672        let empty = dom.create_element("div");
673        let not_empty = dom.create_element("div");
674        let t = dom.create_text_node("x");
675        dom.append_child(not_empty, t).unwrap();
676        dom.append_child(root, empty).unwrap();
677        dom.append_child(root, not_empty).unwrap();
678        let r = dom.query_selector_all_in(root, "div:empty").unwrap();
679        assert_eq!(r, vec![empty]);
680    }
681
682    /// Selectors 4 §14.2 (`P7G-CORE-SMALL-1`): `:empty` means no element
683    /// children and no text children with non-empty data — a zero-length
684    /// text node, a comment do not count; whitespace-only text does
685    /// (Level 3, and browsers today).
686    #[test]
687    fn empty_ignores_zero_length_text_and_comments_but_not_whitespace() {
688        let mut dom: Dom = Dom::new();
689        let root = dom.root();
690        let with = |dom: &mut Dom, kids: &[Option<&str>]| {
691            let div = dom.create_element("div");
692            for k in kids {
693                let child = match k {
694                    Some(text) => dom.create_text_node(text),
695                    None => dom.create_comment("c"),
696                };
697                dom.append_child(div, child).unwrap();
698            }
699            dom.append_child(root, div).unwrap();
700            div
701        };
702        let zero_length = with(&mut dom, &[Some("")]);
703        let comment = with(&mut dom, &[None]);
704        let both = with(&mut dom, &[Some(""), None, Some("")]);
705        let space = with(&mut dom, &[Some(" ")]);
706        let text = with(&mut dom, &[Some(""), Some("x")]);
707        for id in [zero_length, comment, both] {
708            assert!(dom.matches(id, ":empty").unwrap(), "{id:?}");
709        }
710        for id in [space, text] {
711            assert!(!dom.matches(id, ":empty").unwrap(), "{id:?}");
712        }
713    }
714
715    /// CSS Syntax 3 §4.2: non-ASCII code points are ident code points,
716    /// so unquoted values, classes and ids may use them.
717    #[test]
718    fn non_ascii_identifiers_match() {
719        let mut dom: Dom = Dom::new();
720        let root = dom.root();
721        let el = dom.create_element("p");
722        dom.set_attribute(el, "lang", "én-CA").unwrap();
723        dom.set_attribute(el, "id", "naïve").unwrap();
724        dom.add_class(el, "café").unwrap();
725        dom.append_child(root, el).unwrap();
726        for sel in ["[lang|=én]", "p.café", "#naïve", "[lang^=é]"] {
727            assert!(dom.matches(el, sel).unwrap(), "{sel}");
728        }
729    }
730
731    #[test]
732    fn root_pseudo() {
733        let (dom, _) = build();
734        let root = dom.root();
735        // `:root` matches only the document root. query_selector scans
736        // descendants, so it won't find root itself — use matches / closest.
737        // But our root is a Fragment, not an Element. Build a new dom with
738        // an element root to exercise :root.
739        let mut dom2: Dom = Dom::with_root_tag("html");
740        let root2 = dom2.root();
741        let body = dom2.create_element("body");
742        dom2.append_child(root2, body).unwrap();
743        assert!(dom2.matches(root2, ":root").unwrap());
744        assert!(!dom2.matches(body, ":root").unwrap());
745        // In the original fragment-rooted tree, the root is a fragment so
746        // matches_compound returns false regardless.
747        assert!(!dom.matches(root, ":root").unwrap());
748    }
749
750    #[test]
751    fn matches_with_chain() {
752        let (dom, [_, _, _, _, em]) = build();
753        // em inside .outer via descendant combinator.
754        assert!(dom.matches(em, ".outer em").unwrap());
755        // child: em's parent is p, not .outer directly.
756        assert!(!dom.matches(em, ".outer > em").unwrap());
757    }
758
759    #[test]
760    fn hover_pseudo_follows_set_hovered() {
761        let (mut dom, [div, _, _, _, em]) = build();
762        // Nothing hovered → no matches.
763        assert!(!dom.matches(div, ":hover").unwrap());
764        // Hover div — only div matches.
765        dom.set_hovered(Some(div));
766        assert!(dom.matches(div, ":hover").unwrap());
767        assert!(!dom.matches(em, ":hover").unwrap());
768        // Clear hover.
769        dom.set_hovered(None);
770        assert!(!dom.matches(div, ":hover").unwrap());
771    }
772
773    /// Selectors 4 §9.2: an element also matches `:hover` while one of
774    /// its descendants is the hovered element (`P7G-HOVER-ANCESTORS-1`).
775    #[test]
776    fn hover_matches_the_ancestors_of_the_hovered_element() {
777        let (mut dom, [div, s1, s2, p, em]) = build();
778        dom.set_hovered(Some(em));
779        assert!(dom.matches(em, ":hover").unwrap(), "the hovered element");
780        assert!(dom.matches(p, ":hover").unwrap(), "its parent");
781        assert!(dom.matches(div, ":hover").unwrap(), "its grandparent");
782        assert!(dom.matches(em, ".outer:hover em").unwrap());
783        assert!(!dom.matches(s1, ":hover").unwrap(), "a sibling subtree");
784        assert!(!dom.matches(s2, ":hover").unwrap(), "a sibling subtree");
785        dom.set_hovered(None);
786        assert!(!dom.matches(div, ":hover").unwrap());
787    }
788
789    /// Selectors 4 §9.4: `:active` matches the activated element and,
790    /// like `:hover`, its ancestors.
791    #[test]
792    fn active_matches_the_activated_element_and_its_ancestors() {
793        let (mut dom, [div, s1, _, p, em]) = build();
794        assert!(!dom.matches(em, ":active").unwrap());
795        dom.set_active(Some(em));
796        assert_eq!(dom.active(), Some(em));
797        assert!(dom.matches(em, ":active").unwrap());
798        assert!(dom.matches(p, ":active").unwrap());
799        assert!(dom.matches(div, "div:active").unwrap());
800        assert!(!dom.matches(s1, ":active").unwrap());
801        dom.set_active(None);
802        assert!(!dom.matches(div, ":active").unwrap());
803        assert!(!dom.matches(em, ":active").unwrap());
804    }
805
806    #[test]
807    fn focus_pseudo_follows_set_focused() {
808        let (mut dom, [div, s1, _, _, _]) = build();
809        dom.set_focused(Some(s1));
810        assert!(dom.matches(s1, ":focus").unwrap());
811        assert!(!dom.matches(div, ":focus").unwrap());
812    }
813
814    #[test]
815    fn focus_within_matches_focused_and_ancestor_elements() {
816        // `:focus-within` matches the focused element AND every
817        // ancestor element in the chain. Mirrors CSS Selectors L4
818        // §10.1.4. The Document root is not an element so it
819        // won't return true via `matches()` (which filters to
820        // elements), but every element ancestor between root and
821        // the focused node does.
822        //
823        // Tree shape (from `build`):
824        //   root → div.outer → p.last → em
825        let (mut dom, [div, _s1, _s2, p, em]) = build();
826        dom.set_focused(Some(em));
827        assert!(dom.matches(em, ":focus-within").unwrap(), "focused itself");
828        assert!(dom.matches(p, ":focus-within").unwrap(), "parent");
829        assert!(dom.matches(div, ":focus-within").unwrap(), "grandparent");
830    }
831
832    #[test]
833    fn focus_within_does_not_match_siblings_or_other_subtrees() {
834        // Focus is on em (under p.last). Siblings of p (s1.first,
835        // s2.mid) are in a different subtree — they must NOT match.
836        let (mut dom, [_div, s1, s2, _p, em]) = build();
837        dom.set_focused(Some(em));
838        assert!(!dom.matches(s1, ":focus-within").unwrap());
839        assert!(!dom.matches(s2, ":focus-within").unwrap());
840    }
841
842    #[test]
843    fn focus_within_clears_when_focus_cleared() {
844        let (mut dom, [div, _, _, _, em]) = build();
845        dom.set_focused(Some(em));
846        assert!(dom.matches(div, ":focus-within").unwrap());
847        dom.set_focused(None);
848        assert!(!dom.matches(div, ":focus-within").unwrap());
849        assert!(!dom.matches(em, ":focus-within").unwrap());
850    }
851
852    #[test]
853    fn hover_focus_combine_with_other_selectors() {
854        let (mut dom, [_, s1, _, _, _]) = build();
855        dom.set_hovered(Some(s1));
856        // span:hover
857        assert!(dom.matches(s1, "span:hover").unwrap());
858        // span.first:hover
859        assert!(dom.matches(s1, "span.first:hover").unwrap());
860        // Non-hovered elements don't match.
861        dom.set_hovered(None);
862        assert!(!dom.matches(s1, "span:hover").unwrap());
863    }
864
865    #[test]
866    fn checked_pseudo_matches_attribute_presence() {
867        let mut dom: Dom<()> = Dom::new();
868        let root = dom.root();
869        let cb = dom.create_element("input");
870        dom.set_attribute(cb, "type", "checkbox").unwrap();
871        dom.append_child(root, cb).unwrap();
872
873        // Absent → no match.
874        assert!(!dom.matches(cb, ":checked").unwrap());
875
876        // Empty value (HTML boolean shorthand) → match.
877        dom.set_attribute(cb, "checked", "").unwrap();
878        assert!(dom.matches(cb, ":checked").unwrap());
879
880        // Any value still matches (presence-only, like the HTML
881        // boolean attribute model).
882        dom.set_attribute(cb, "checked", "false").unwrap();
883        assert!(dom.matches(cb, ":checked").unwrap());
884
885        // Removed → no match.
886        dom.remove_attribute(cb, "checked").unwrap();
887        assert!(!dom.matches(cb, ":checked").unwrap());
888    }
889
890    #[test]
891    fn checked_pseudo_combines_with_type_attribute_selector() {
892        let mut dom: Dom<()> = Dom::new();
893        let root = dom.root();
894        let cb = dom.create_element("input");
895        dom.set_attribute(cb, "type", "checkbox").unwrap();
896        dom.set_attribute(cb, "checked", "").unwrap();
897        dom.append_child(root, cb).unwrap();
898
899        assert!(dom.matches(cb, "[type=checkbox]:checked").unwrap());
900        assert!(!dom.matches(cb, "[type=radio]:checked").unwrap());
901    }
902
903    #[test]
904    fn placeholder_shown_matches_when_attribute_set_and_content_empty() {
905        let mut dom: Dom<()> = Dom::new();
906        let root = dom.root();
907        let inp = dom.create_element("input");
908        dom.set_attribute(inp, "placeholder", "Search...").unwrap();
909        dom.append_child(root, inp).unwrap();
910
911        // No text content → match.
912        assert!(dom.matches(inp, ":placeholder-shown").unwrap());
913
914        // Add some content → no match.
915        let t = dom.create_text_node("hi");
916        dom.append_child(inp, t).unwrap();
917        assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
918    }
919
920    #[test]
921    fn placeholder_shown_requires_non_empty_placeholder_attribute() {
922        let mut dom: Dom<()> = Dom::new();
923        let root = dom.root();
924        let inp = dom.create_element("input");
925        dom.append_child(root, inp).unwrap();
926        // No placeholder → no match.
927        assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
928
929        // Empty placeholder → still no match (HTML rule — blank
930        // placeholder doesn't count).
931        dom.set_attribute(inp, "placeholder", "").unwrap();
932        assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
933    }
934
935    #[test]
936    fn placeholder_shown_with_whitespace_is_empty_content() {
937        // Content::text() concatenates raw strings — whitespace is
938        // NOT collapsed for this test. Text node with just spaces
939        // is non-empty; matches browsers for real whitespace.
940        let mut dom: Dom<()> = Dom::new();
941        let root = dom.root();
942        let inp = dom.create_element("input");
943        dom.set_attribute(inp, "placeholder", "Hint").unwrap();
944        let t = dom.create_text_node(" ");
945        dom.append_child(inp, t).unwrap();
946        dom.append_child(root, inp).unwrap();
947        // Space is non-empty text → doesn't match.
948        assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
949    }
950
951    // ── :indeterminate + :open (Polish #4) ────────────────────────
952
953    #[test]
954    fn indeterminate_matches_progress_without_value() {
955        let mut dom: Dom<()> = Dom::new();
956        let root = dom.root();
957        let p = dom.create_element("progress");
958        dom.append_child(root, p).unwrap();
959        assert!(dom.matches(p, ":indeterminate").unwrap());
960        dom.set_attribute(p, "value", "0.5").unwrap();
961        assert!(!dom.matches(p, ":indeterminate").unwrap());
962    }
963
964    #[test]
965    fn indeterminate_does_not_match_other_tags() {
966        let mut dom: Dom<()> = Dom::new();
967        let root = dom.root();
968        let m = dom.create_element("meter");
969        dom.append_child(root, m).unwrap();
970        // `<meter>` without value is NOT indeterminate (unlike
971        // progress) — meter always represents a known measurement.
972        assert!(!dom.matches(m, ":indeterminate").unwrap());
973    }
974
975    #[test]
976    fn open_matches_elements_with_open_attribute() {
977        let mut dom: Dom<()> = Dom::new();
978        let root = dom.root();
979        let d = dom.create_element("details");
980        dom.append_child(root, d).unwrap();
981        assert!(!dom.matches(d, ":open").unwrap());
982        dom.set_attribute(d, "open", "").unwrap();
983        assert!(dom.matches(d, ":open").unwrap());
984    }
985
986    #[test]
987    fn open_works_on_dialog_as_well() {
988        let mut dom: Dom<()> = Dom::new();
989        let root = dom.root();
990        let d = dom.create_element("dialog");
991        dom.set_attribute(d, "open", "").unwrap();
992        dom.append_child(root, d).unwrap();
993        assert!(dom.matches(d, ":open").unwrap());
994    }
995
996    #[test]
997    fn open_can_combine_with_other_selectors() {
998        let mut dom: Dom<()> = Dom::new();
999        let root = dom.root();
1000        let d = dom.create_element("details");
1001        dom.set_attribute(d, "open", "").unwrap();
1002        dom.append_child(root, d).unwrap();
1003        assert!(dom.matches(d, "details:open").unwrap());
1004        assert!(!dom.matches(d, "dialog:open").unwrap());
1005    }
1006
1007    /// HTML §4.16.2: attribute selectors treat the values of `type`,
1008    /// `method`, `enctype`, `lang`, … on HTML elements as ASCII
1009    /// case-insensitive; every other attribute stays case-sensitive.
1010    #[test]
1011    fn html_case_insensitive_attribute_values_match_regardless_of_case() {
1012        let mut dom: Dom = Dom::new();
1013        let root = dom.root();
1014        let cb = dom.create_element("input");
1015        dom.set_attribute(cb, "type", "CheckBox").unwrap();
1016        dom.set_attribute(cb, "data-kind", "Big").unwrap();
1017        dom.append_child(root, cb).unwrap();
1018        let form = dom.create_element("form");
1019        dom.set_attribute(form, "method", "POST").unwrap();
1020        dom.set_attribute(form, "lang", "EN-us").unwrap();
1021        dom.append_child(root, form).unwrap();
1022
1023        assert!(dom.matches(cb, "input[type=checkbox]").unwrap());
1024        assert!(dom.matches(cb, "[type^=check]").unwrap());
1025        assert!(dom.matches(cb, ":not([type=radio])").unwrap());
1026        assert!(dom.matches(form, "[method=post]").unwrap());
1027        assert!(dom.matches(form, "[lang|=en]").unwrap());
1028        assert!(
1029            !dom.matches(cb, "[data-kind=big]").unwrap(),
1030            "attributes outside the HTML list stay case-sensitive"
1031        );
1032        assert!(dom.matches(cb, "[data-kind=Big]").unwrap());
1033    }
1034
1035    /// Every attribute operator honors HTML §4.16.2's case-insensitive
1036    /// values — `=`, `~=`, `|=`, `^=`, `$=`, `*=` — on a listed attribute
1037    /// (`rel`, `lang`, `type`), and stays case-sensitive on others.
1038    #[test]
1039    fn every_attribute_operator_is_ascii_case_insensitive_on_listed_attributes() {
1040        let mut dom: Dom = Dom::new();
1041        let root = dom.root();
1042        let a = dom.create_element("a");
1043        dom.set_attribute(a, "rel", "NoOpener External").unwrap();
1044        dom.set_attribute(a, "lang", "EN-GB").unwrap();
1045        dom.set_attribute(a, "type", "Text/HTML").unwrap();
1046        dom.set_attribute(a, "title", "Text/HTML").unwrap();
1047        dom.append_child(root, a).unwrap();
1048        for sel in [
1049            "[type='text/html']",
1050            "[rel~=external]",
1051            "[rel~=NOOPENER]",
1052            "[lang|=en]",
1053            "[lang|=en-gb]",
1054            "[type^=TEXT]",
1055            "[type$='/html']",
1056            "[type*='T/h']",
1057        ] {
1058            assert!(dom.matches(a, sel).unwrap(), "{sel}");
1059        }
1060        for sel in [
1061            "[rel~=noop]",
1062            "[lang|=e]",
1063            "[lang|=gb]",
1064            "[type^=html]",
1065            "[type$=text]",
1066            "[type*=xml]",
1067            "[type^='']",
1068        ] {
1069            assert!(!dom.matches(a, sel).unwrap(), "{sel}");
1070        }
1071        for sel in [
1072            "[title='text/html']",
1073            "[title^=text]",
1074            "[title$='/html']",
1075            "[title*='t/h']",
1076        ] {
1077            assert!(
1078                !dom.matches(a, sel).unwrap(),
1079                "{sel}: title is case-sensitive"
1080            );
1081        }
1082        assert!(dom.matches(a, "[title*='t/H']").unwrap());
1083        // Non-ASCII letters never fold.
1084        dom.set_attribute(a, "lang", "ÉN").unwrap();
1085        assert!(!dom.matches(a, "[lang|='én']").unwrap());
1086        assert!(dom.matches(a, "[lang|='ÉN']").unwrap());
1087    }
1088
1089    #[test]
1090    fn closest_walks_up() {
1091        let (dom, [div, _, _, _, em]) = build();
1092        // closest(".outer") from em returns div.
1093        assert_eq!(dom.closest(em, ".outer").unwrap(), Some(div));
1094        // closest("#nope") from em returns None.
1095        assert!(dom.closest(em, "#nope").unwrap().is_none());
1096        // closest("em") from em returns em (inclusive self).
1097        assert_eq!(dom.closest(em, "em").unwrap(), Some(em));
1098    }
1099
1100    #[test]
1101    fn invalid_selector_errors() {
1102        let (dom, _) = build();
1103        let root = dom.root();
1104        assert!(dom.query_selector_in(root, ":nope").is_err());
1105        assert!(dom.query_selector_all_in(root, "").is_err());
1106    }
1107
1108    // ── P7-VALIDATION-SELECTORS-1 ─────────────────────────────────────
1109
1110    /// HTML §4.16.3: `:required` matches an `<input>` (in a state
1111    /// `required` applies to), `<select>` or `<textarea>` with
1112    /// `required`; `:optional` the rest of those three.
1113    #[test]
1114    fn required_and_optional_match_the_three_form_controls() {
1115        let mut dom: Dom = Dom::new();
1116        let root = dom.root();
1117        let mk = |dom: &mut Dom, tag: &str, attrs: &[(&str, &str)]| {
1118            let e = dom.create_element(tag);
1119            for (k, v) in attrs {
1120                dom.set_attribute(e, k, v).unwrap();
1121            }
1122            dom.append_child(root, e).unwrap();
1123            e
1124        };
1125        let req_text = mk(&mut dom, "input", &[("required", "")]);
1126        let req_box = mk(&mut dom, "input", &[("type", "checkbox"), ("required", "")]);
1127        let req_range = mk(&mut dom, "input", &[("type", "range"), ("required", "")]);
1128        let req_hidden = mk(&mut dom, "input", &[("type", "hidden"), ("required", "")]);
1129        let plain = mk(&mut dom, "input", &[]);
1130        let req_select = mk(&mut dom, "select", &[("required", "")]);
1131        let area = mk(&mut dom, "textarea", &[]);
1132        let req_div = mk(&mut dom, "div", &[("required", "")]);
1133        let button = mk(&mut dom, "button", &[("required", "")]);
1134        for id in [req_text, req_box, req_select] {
1135            assert!(dom.matches(id, ":required").unwrap(), "{id:?}");
1136            assert!(!dom.matches(id, ":optional").unwrap(), "{id:?}");
1137        }
1138        for id in [req_range, req_hidden, plain, area] {
1139            assert!(dom.matches(id, ":optional").unwrap(), "{id:?}");
1140            assert!(!dom.matches(id, ":required").unwrap(), "{id:?}");
1141        }
1142        for id in [req_div, button] {
1143            assert!(!dom.matches(id, ":required").unwrap(), "{id:?}");
1144            assert!(!dom.matches(id, ":optional").unwrap(), "{id:?}");
1145        }
1146    }
1147
1148    /// Test backend: a candidate is invalid while it has `data-bad`.
1149    /// Selectors 4 §13.2: `:focus-visible` matches the focused element
1150    /// while the UA judges its focus should be evident
1151    /// (`Dom::focus_visible`, on until a backend says otherwise); it
1152    /// never matches an unfocused element.
1153    #[test]
1154    fn focus_visible_matches_the_focused_element_while_focus_is_evident() {
1155        let mut dom: Dom = Dom::new();
1156        let root = dom.root();
1157        let a = dom.create_element("button");
1158        let b = dom.create_element("button");
1159        dom.append_child(root, a).unwrap();
1160        dom.append_child(root, b).unwrap();
1161        assert!(dom.focus_visible(), "focus is evident by default");
1162        assert!(
1163            !dom.matches(a, ":focus-visible").unwrap(),
1164            "nothing focused"
1165        );
1166
1167        dom.set_focused(Some(a));
1168        assert!(dom.matches(a, ":focus-visible").unwrap());
1169        assert!(!dom.matches(b, ":focus-visible").unwrap());
1170
1171        dom.set_focus_visible(false);
1172        assert!(dom.matches(a, ":focus").unwrap());
1173        assert!(!dom.matches(a, ":focus-visible").unwrap());
1174        assert!(dom.matches(a, ":focus:not(:focus-visible)").unwrap());
1175    }
1176
1177    fn bad_attr_hook(dom: &Dom, id: NodeId) -> bool {
1178        !dom.has_attribute(id, "data-bad")
1179    }
1180
1181    /// `P7G-PUBLIC-SURFACE-2`: rdom-core has no validity states, and a
1182    /// `Dom` without a validity hook answers "valid" for every candidate
1183    /// — in a debug build as in a release build (it used to panic in
1184    /// debug only). The documented contract (`Dom::set_validity_hook`)
1185    /// is that a backend matching these pseudo-classes installs one.
1186    #[test]
1187    fn a_candidate_without_a_validity_hook_is_valid_in_every_build() {
1188        let mut dom: Dom = Dom::new();
1189        let root = dom.root();
1190        let input = dom.create_element("input");
1191        dom.append_child(root, input).unwrap();
1192        assert!(!dom.matches(input, ":invalid").unwrap());
1193        assert!(dom.matches(input, ":valid").unwrap());
1194    }
1195
1196    /// Elements that are not candidates answer `:valid` / `:invalid`
1197    /// without consulting the hook, so they need none.
1198    #[test]
1199    fn non_candidates_match_neither_validity_class_without_a_hook() {
1200        let mut dom: Dom = Dom::new();
1201        let root = dom.root();
1202        let div = dom.create_element("div");
1203        dom.append_child(root, div).unwrap();
1204        let hidden = dom.create_element("input");
1205        dom.set_attribute(hidden, "type", "hidden").unwrap();
1206        dom.append_child(root, hidden).unwrap();
1207        for id in [div, hidden] {
1208            assert!(!dom.matches(id, ":valid").unwrap());
1209            assert!(!dom.matches(id, ":invalid").unwrap());
1210        }
1211    }
1212
1213    /// HTML §4.16.3: `:valid` / `:invalid` match candidates for
1214    /// constraint validation by the backend's verdict (the validity
1215    /// hook), forms by their owned candidates and fieldsets by their
1216    /// descendant candidates; barred and other elements match neither.
1217    #[test]
1218    fn valid_and_invalid_follow_the_validity_hook() {
1219        let mut dom: Dom = Dom::new();
1220        let root = dom.root();
1221        let form = dom.create_element("form");
1222        dom.append_child(root, form).unwrap();
1223        let fieldset = dom.create_element("fieldset");
1224        dom.append_child(form, fieldset).unwrap();
1225        let bad = dom.create_element("input");
1226        dom.set_attribute(bad, "data-bad", "").unwrap();
1227        dom.append_child(fieldset, bad).unwrap();
1228        let good = dom.create_element("textarea");
1229        dom.append_child(form, good).unwrap();
1230        let barred = dom.create_element("input");
1231        dom.set_attribute(barred, "data-bad", "").unwrap();
1232        dom.set_attribute(barred, "disabled", "").unwrap();
1233        dom.append_child(form, barred).unwrap();
1234        let div = dom.create_element("div");
1235        dom.append_child(form, div).unwrap();
1236        let empty_form = dom.create_element("form");
1237        dom.append_child(root, empty_form).unwrap();
1238
1239        dom.set_validity_hook(Some(bad_attr_hook));
1240        assert!(dom.matches(bad, ":invalid").unwrap());
1241        assert!(!dom.matches(bad, ":valid").unwrap());
1242        assert!(dom.matches(good, ":valid").unwrap());
1243        assert!(dom.matches(fieldset, ":invalid").unwrap());
1244        assert!(dom.matches(form, ":invalid").unwrap());
1245        assert!(dom.matches(empty_form, ":valid").unwrap());
1246        for id in [barred, div] {
1247            assert!(!dom.matches(id, ":valid").unwrap(), "{id:?}");
1248            assert!(!dom.matches(id, ":invalid").unwrap(), "{id:?}");
1249        }
1250        assert_eq!(dom.constraint_validity(bad), Some(false));
1251        assert_eq!(dom.constraint_validity(div), None);
1252
1253        dom.remove_attribute(bad, "data-bad").unwrap();
1254        assert!(dom.matches(form, ":valid").unwrap());
1255        assert!(dom.matches(fieldset, ":valid").unwrap());
1256    }
1257}