1use 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 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 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 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 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 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 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 if !self.matches_compound(id, &complex.subject) {
119 return false;
120 }
121 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 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 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 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 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 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
347fn 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 }; 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
423trait ValueCase {
428 fn eq(a: &[u8], b: &[u8]) -> bool;
429
430 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
449struct AsciiCaseInsensitive;
451impl ValueCase for AsciiCaseInsensitive {
452 fn eq(a: &[u8], b: &[u8]) -> bool {
453 a.eq_ignore_ascii_case(b)
454 }
455}
456
457fn 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#[cfg(test)]
477mod tests {
478 use crate::{Dom, NodeId};
479
480 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 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 let r = dom
628 .query_selector_all_in(root, ":where(.first, .last)")
629 .unwrap();
630 assert_eq!(r, vec![s1, p]);
631 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 assert_eq!(
643 dom.query_selector_in(root, ":first-child").unwrap(),
644 Some(div)
645 );
646 assert_eq!(
648 dom.query_selector_in(root, "span:first-child").unwrap(),
649 Some(s1)
650 );
651 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 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 #[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 #[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 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 assert!(!dom.matches(root, ":root").unwrap());
748 }
749
750 #[test]
751 fn matches_with_chain() {
752 let (dom, [_, _, _, _, em]) = build();
753 assert!(dom.matches(em, ".outer em").unwrap());
755 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 assert!(!dom.matches(div, ":hover").unwrap());
764 dom.set_hovered(Some(div));
766 assert!(dom.matches(div, ":hover").unwrap());
767 assert!(!dom.matches(em, ":hover").unwrap());
768 dom.set_hovered(None);
770 assert!(!dom.matches(div, ":hover").unwrap());
771 }
772
773 #[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 #[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 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 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 assert!(dom.matches(s1, "span:hover").unwrap());
858 assert!(dom.matches(s1, "span.first:hover").unwrap());
860 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 assert!(!dom.matches(cb, ":checked").unwrap());
875
876 dom.set_attribute(cb, "checked", "").unwrap();
878 assert!(dom.matches(cb, ":checked").unwrap());
879
880 dom.set_attribute(cb, "checked", "false").unwrap();
883 assert!(dom.matches(cb, ":checked").unwrap());
884
885 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 assert!(dom.matches(inp, ":placeholder-shown").unwrap());
913
914 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 assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
928
929 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 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 assert!(!dom.matches(inp, ":placeholder-shown").unwrap());
949 }
950
951 #[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 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 #[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 #[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 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 assert_eq!(dom.closest(em, ".outer").unwrap(), Some(div));
1094 assert!(dom.closest(em, "#nope").unwrap().is_none());
1096 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 #[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]
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 #[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 #[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 #[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}