Skip to main content

rdom_core/
position.rs

1//! `compare_document_position` — DOM spec bitflag describing how two nodes
2//! relate (precedes / follows / contains / is-contained-by / disconnected).
3//!
4//! Spec: <https://dom.spec.whatwg.org/#dom-node-comparedocumentposition>
5
6use crate::bitflags_like;
7use crate::dom::Dom;
8use crate::node_id::NodeId;
9
10bitflags_like! {
11    /// Bitflags matching MDN's `Node.compareDocumentPosition` return value.
12    /// Multiple bits can be set — e.g. `CONTAINED_BY | FOLLOWING` when the
13    /// other node is a descendant (descendants are considered "following"
14    /// in document order).
15    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16    pub struct DocumentPosition(u16) {
17        DISCONNECTED            = 0b0000_0001;
18        PRECEDING               = 0b0000_0010;
19        FOLLOWING               = 0b0000_0100;
20        CONTAINS                = 0b0000_1000;
21        CONTAINED_BY            = 0b0001_0000;
22        IMPLEMENTATION_SPECIFIC = 0b0010_0000;
23    }
24}
25
26impl<Ext> Dom<Ext> {
27    /// `a.compareDocumentPosition(b)`: a bitmask describing **`b` relative
28    /// to `a`** (DOM §4.4). Same orientation as the web: the bits say where
29    /// the *argument* sits.
30    ///
31    /// - `a == b` → empty bits (0).
32    /// - `b` is a descendant of `a` → `CONTAINED_BY | FOLLOWING` (20).
33    /// - `b` is an ancestor of `a` → `CONTAINS | PRECEDING` (10).
34    /// - `b` comes later in tree order → `FOLLOWING`.
35    /// - `b` comes earlier in tree order → `PRECEDING`.
36    /// - different trees → `DISCONNECTED | IMPLEMENTATION_SPECIFIC | PRECEDING`.
37    ///
38    /// `FOLLOWING` therefore always means "`b` is later in tree order",
39    /// whether `b` is a sibling's subtree or `a`'s own descendant.
40    pub fn compare_document_position(&self, a: NodeId, b: NodeId) -> DocumentPosition {
41        if a == b {
42            return DocumentPosition::empty();
43        }
44        // DOM §4.4 step 6: different trees.
45        const IN_DIFFERENT_TREES: DocumentPosition = DocumentPosition::from_bits_truncate(
46            DocumentPosition::DISCONNECTED.bits()
47                | DocumentPosition::IMPLEMENTATION_SPECIFIC.bits()
48                | DocumentPosition::PRECEDING.bits(),
49        );
50        let (Some(da), Some(db)) = (self.depth_of(a), self.depth_of(b)) else {
51            return IN_DIFFERENT_TREES;
52        };
53        // Bring both cursors to the same depth. If they meet, one node is
54        // an ancestor of the other. No paths are materialized: this runs
55        // per text node per frame while a selection is painted
56        // (`CORE-DOCPOS-ALLOC-1`).
57        let (mut x, mut y) = (a, b);
58        for _ in db..da {
59            x = self.parent_of(x);
60        }
61        for _ in da..db {
62            y = self.parent_of(y);
63        }
64        if x == y {
65            return if da < db {
66                // b is a descendant of a: contained by a and, in tree
67                // order, after it.
68                DocumentPosition::CONTAINED_BY | DocumentPosition::FOLLOWING
69            } else {
70                // b is an ancestor of a: contains a and precedes it.
71                DocumentPosition::CONTAINS | DocumentPosition::PRECEDING
72            };
73        }
74        // Walk up in lockstep until the parents coincide; `x` / `y` are
75        // then the two branches under the lowest common ancestor.
76        let lca = loop {
77            match (
78                self.get_node(x).and_then(|n| n.parent),
79                self.get_node(y).and_then(|n| n.parent),
80            ) {
81                (Some(px), Some(py)) if px == py => break px,
82                (Some(px), Some(py)) => {
83                    x = px;
84                    y = py;
85                }
86                // Different roots.
87                _ => return IN_DIFFERENT_TREES,
88            }
89        };
90        // Which branch comes first among `lca`'s children? The flags
91        // describe b relative to a: a's branch first → b FOLLOWING.
92        let mut cur = self.get_node(lca).and_then(|n| n.first_child);
93        while let Some(c) = cur {
94            if c == x {
95                return DocumentPosition::FOLLOWING;
96            }
97            if c == y {
98                return DocumentPosition::PRECEDING;
99            }
100            cur = self.get_node(c).and_then(|n| n.next_sibling);
101        }
102        debug_assert!(
103            false,
104            "compare_document_position: branches not under their LCA"
105        );
106        DocumentPosition::empty()
107    }
108
109    /// Depth of `id` below its root (root = 0); `None` if `id` is not in
110    /// the arena.
111    fn depth_of(&self, id: NodeId) -> Option<usize> {
112        let mut node = self.get_node(id)?;
113        let mut depth = 0;
114        while let Some(p) = node.parent {
115            node = self.get_node(p)?;
116            depth += 1;
117        }
118        Some(depth)
119    }
120
121    /// Parent of a node known to be live and non-root (callers have
122    /// measured its depth).
123    fn parent_of(&self, id: NodeId) -> NodeId {
124        self.get_node(id)
125            .and_then(|n| n.parent)
126            .expect("parent_of: node has a parent (depth checked by caller)")
127    }
128
129    /// The root of the tree `id` is in; `None` if `id` is not live.
130    pub(crate) fn root_of(&self, id: NodeId) -> Option<NodeId> {
131        let mut cur = id;
132        let mut node = self.get_node(cur)?;
133        while let Some(p) = node.parent {
134            cur = p;
135            node = self.get_node(cur)?;
136        }
137        Some(cur)
138    }
139
140    /// Is `a` equal to `b` structurally (same tag, attrs, classes, text,
141    /// and recursively equal children)? Compares the tree shape — IDs +
142    /// parents are not considered. Iterative over a stack of node pairs,
143    /// so any depth is safe (`P7G-CORE-SMALL-1`).
144    pub fn is_equal_node(&self, a: NodeId, b: NodeId) -> bool {
145        let mut pending = vec![(a, b)];
146        while let Some((a, b)) = pending.pop() {
147            let (Some(na), Some(nb)) = (self.get_node(a), self.get_node(b)) else {
148                return false;
149            };
150            if !Self::same_node_data(&na.data, &nb.data) {
151                return false;
152            }
153            // Children pairwise, in order; a length mismatch is unequal.
154            let (mut ca, mut cb) = (na.first_child, nb.first_child);
155            loop {
156                match (ca, cb) {
157                    (None, None) => break,
158                    (Some(x), Some(y)) => {
159                        pending.push((x, y));
160                        ca = self.get_node(x).and_then(|n| n.next_sibling);
161                        cb = self.get_node(y).and_then(|n| n.next_sibling);
162                    }
163                    _ => return false,
164                }
165            }
166        }
167        true
168    }
169
170    /// `is_equal_node`'s per-node test: same kind, and the same tag /
171    /// attributes / classes or the same data.
172    fn same_node_data(a: &crate::node::NodeData<Ext>, b: &crate::node::NodeData<Ext>) -> bool {
173        use crate::node::NodeData;
174        match (a, b) {
175            (
176                NodeData::Element {
177                    tag: ta,
178                    attrs: aa,
179                    classes: ca,
180                    ..
181                },
182                NodeData::Element {
183                    tag: tb,
184                    attrs: ab,
185                    classes: cb,
186                    ..
187                },
188            ) => ta == tb && aa == ab && ca == cb,
189            (NodeData::Text { data: da }, NodeData::Text { data: db })
190            | (NodeData::Comment { data: da }, NodeData::Comment { data: db }) => da == db,
191            (NodeData::Fragment, NodeData::Fragment) => true,
192            _ => false,
193        }
194    }
195
196    /// Order two boundary points per DOM §5.2 ("position of a boundary
197    /// point relative to another"). `Less` when `a` comes before `b`,
198    /// `Equal` when they are the same point, `None` when the nodes are in
199    /// different trees.
200    ///
201    /// An element position `(el, k)` sits between `el`'s children `k-1`
202    /// and `k`, so it orders against a point inside child `j` by comparing
203    /// `j` with `k` — not by which node contains the other.
204    pub fn compare_boundary_points(
205        &self,
206        a: crate::Position,
207        b: crate::Position,
208    ) -> Option<std::cmp::Ordering> {
209        use std::cmp::Ordering;
210        if a.node == b.node {
211            return Some(a.offset.cmp(&b.offset));
212        }
213        let pos = self.compare_document_position(a.node, b.node);
214        if pos.contains(DocumentPosition::DISCONNECTED) {
215            return None;
216        }
217        // `a.node` follows `b.node` (including `a.node` inside `b.node`):
218        // answer from the other side and flip.
219        if pos.contains(DocumentPosition::PRECEDING) {
220            return self.compare_boundary_points(b, a).map(Ordering::reverse);
221        }
222        // `a.node` is an ancestor of `b.node`: find `a.node`'s child on
223        // the way down to `b.node` and compare its index with `a.offset`.
224        if pos.contains(DocumentPosition::CONTAINED_BY) {
225            // Climb from `b.node` to the child of `a.node` on its path.
226            let mut child = b.node;
227            loop {
228                let parent = self.get_node(child)?.parent?;
229                if parent == a.node {
230                    break;
231                }
232                child = parent;
233            }
234            let index = self.child_index_of(child)?;
235            return Some(if index < a.offset {
236                Ordering::Greater
237            } else {
238                Ordering::Less
239            });
240        }
241        // Plain tree order: `b.node` follows `a.node`.
242        Some(Ordering::Less)
243    }
244
245    /// Index of `id` among its parent's children, `None` for a root or a
246    /// freed node.
247    fn child_index_of(&self, id: NodeId) -> Option<usize> {
248        let parent = self.get_node(id)?.parent?;
249        let mut cur = self.get_node(parent)?.first_child;
250        let mut index = 0;
251        while let Some(c) = cur {
252            if c == id {
253                return Some(index);
254            }
255            index += 1;
256            cur = self.get_node(c)?.next_sibling;
257        }
258        None
259    }
260
261    /// Path from root → this node as `Vec<NodeId>` (inclusive on both ends).
262    /// Empty if the node isn't in the arena.
263    pub fn ancestor_path(&self, id: NodeId) -> Vec<NodeId> {
264        let mut path = Vec::new();
265        let mut cur = Some(id);
266        while let Some(c) = cur {
267            if self.get_node(c).is_none() {
268                return Vec::new();
269            }
270            path.push(c);
271            cur = self.get_node(c).and_then(|n| n.parent);
272        }
273        path.reverse();
274        path
275    }
276
277    /// Lowest common ancestor of `a` and `b` — the deepest node
278    /// that contains both. Returns `None` if `a` and `b` live in
279    /// different arenas or if either node is invalid.
280    ///
281    /// Used by the runtime for click synthesis: when `mousedown`
282    /// fires on one target and `mouseup` on another, the `click`
283    /// event dispatches on their common ancestor (HTML semantics).
284    ///
285    /// When `a == b`, returns `Some(a)`. When one is an ancestor
286    /// of the other, returns the ancestor.
287    pub fn common_ancestor(&self, a: NodeId, b: NodeId) -> Option<NodeId> {
288        let (da, db) = (self.depth_of(a)?, self.depth_of(b)?);
289        let (mut x, mut y) = (a, b);
290        for _ in db..da {
291            x = self.parent_of(x);
292        }
293        for _ in da..db {
294            y = self.parent_of(y);
295        }
296        while x != y {
297            // Same depth: both have parents or neither does. Neither →
298            // two roots → disconnected.
299            x = self.get_node(x)?.parent?;
300            y = self.get_node(y)?.parent?;
301        }
302        Some(x)
303    }
304}
305
306// ─────────────────────────────────────────────────────────────────────
307//  Small bitflags-without-crate helper
308// ─────────────────────────────────────────────────────────────────────
309
310/// Minimal bitflag macro so we don't pull in the `bitflags` crate for
311/// a single use. Generates impls for `|`, `&`, `contains`, `empty`,
312/// `bits`, `all`, `from_bits_truncate`, etc.
313#[macro_export]
314#[doc(hidden)]
315macro_rules! bitflags_like {
316    (
317        $(#[$outer:meta])*
318        $vis:vis struct $name:ident ( $repr:ty ) {
319            $( $flag:ident = $value:expr; )+
320        }
321    ) => {
322        $(#[$outer])*
323        $vis struct $name($repr);
324
325        impl $name {
326            $( pub const $flag: Self = Self($value); )+
327
328            #[inline] pub const fn empty() -> Self { Self(0) }
329            #[inline] pub const fn all() -> Self { Self( $( $value )|+ ) }
330            #[inline] pub const fn bits(self) -> $repr { self.0 }
331            #[inline] pub const fn from_bits_truncate(bits: $repr) -> Self {
332                Self(bits & Self::all().0)
333            }
334            #[inline] pub const fn contains(self, other: Self) -> bool {
335                (self.0 & other.0) == other.0
336            }
337            #[inline] pub const fn is_empty(self) -> bool { self.0 == 0 }
338            /// Clear the bits of `other` from `self`. Equivalent to
339            /// `self & !other` but doesn't need a `Not` impl.
340            #[inline] pub const fn without(self, other: Self) -> Self {
341                Self(self.0 & !other.0)
342            }
343        }
344
345        impl std::ops::BitOr for $name {
346            type Output = Self;
347            #[inline] fn bitor(self, rhs: Self) -> Self { Self(self.0 | rhs.0) }
348        }
349        impl std::ops::BitOrAssign for $name {
350            #[inline] fn bitor_assign(&mut self, rhs: Self) { self.0 |= rhs.0; }
351        }
352        impl std::ops::BitAnd for $name {
353            type Output = Self;
354            #[inline] fn bitand(self, rhs: Self) -> Self { Self(self.0 & rhs.0) }
355        }
356        impl std::ops::BitAndAssign for $name {
357            #[inline] fn bitand_assign(&mut self, rhs: Self) { self.0 &= rhs.0; }
358        }
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use crate::Dom;
366
367    /// The original path-materializing algorithm, kept as the oracle for
368    /// the allocation-free depth walk.
369    fn reference_compare(dom: &Dom, a: NodeId, b: NodeId) -> DocumentPosition {
370        if a == b {
371            return DocumentPosition::empty();
372        }
373        let a_path = dom.ancestor_path(a);
374        let b_path = dom.ancestor_path(b);
375        if a_path.first() != b_path.first() || a_path.is_empty() {
376            return DocumentPosition::DISCONNECTED
377                | DocumentPosition::IMPLEMENTATION_SPECIFIC
378                | DocumentPosition::PRECEDING;
379        }
380        let mut common = 0;
381        while common < a_path.len() && common < b_path.len() && a_path[common] == b_path[common] {
382            common += 1;
383        }
384        if common == a_path.len() {
385            return DocumentPosition::CONTAINED_BY | DocumentPosition::FOLLOWING;
386        }
387        if common == b_path.len() {
388            return DocumentPosition::CONTAINS | DocumentPosition::PRECEDING;
389        }
390        let lca = a_path[common - 1];
391        let mut cur = dom.get_node(lca).and_then(|n| n.first_child);
392        while let Some(c) = cur {
393            if c == a_path[common] {
394                return DocumentPosition::FOLLOWING;
395            }
396            if c == b_path[common] {
397                return DocumentPosition::PRECEDING;
398            }
399            cur = dom.get_node(c).and_then(|n| n.next_sibling);
400        }
401        unreachable!()
402    }
403
404    /// `CORE-DOCPOS-ALLOC-1`: the depth walk agrees with the path oracle
405    /// on every ordered pair of a bushy tree plus a detached subtree.
406    #[test]
407    fn depth_walk_matches_the_path_oracle_on_every_pair() {
408        let mut dom: Dom = Dom::new();
409        let root = dom.root();
410        let mut all = vec![root];
411        // Three levels, three children each: 1 + 3 + 9 + 27 nodes.
412        let mut frontier = vec![root];
413        for _ in 0..3 {
414            let mut next = Vec::new();
415            for &p in &frontier {
416                for _ in 0..3 {
417                    let c = dom.create_element("n");
418                    dom.append_child(p, c).unwrap();
419                    all.push(c);
420                    next.push(c);
421                }
422            }
423            frontier = next;
424        }
425        // A detached subtree with its own root.
426        let loose = dom.create_element("loose");
427        let loose_child = dom.create_element("lc");
428        dom.append_child(loose, loose_child).unwrap();
429        all.push(loose);
430        all.push(loose_child);
431        // A freed node.
432        let gone = dom.create_element("gone");
433        dom.drop_subtree(gone).unwrap();
434        all.push(gone);
435
436        for &a in &all {
437            for &b in &all {
438                assert_eq!(
439                    dom.compare_document_position(a, b),
440                    reference_compare(&dom, a, b),
441                    "pair ({a:?}, {b:?})"
442                );
443                let expected_lca = {
444                    let pa = dom.ancestor_path(a);
445                    let pb = dom.ancestor_path(b);
446                    pa.iter()
447                        .zip(pb.iter())
448                        .take_while(|(x, y)| x == y)
449                        .last()
450                        .map(|(x, _)| *x)
451                };
452                assert_eq!(
453                    dom.common_ancestor(a, b),
454                    expected_lca,
455                    "lca ({a:?}, {b:?})"
456                );
457            }
458        }
459    }
460
461    fn build() -> (Dom, NodeId, NodeId, NodeId, NodeId) {
462        // root
463        //   ├─ a
464        //   │    └─ grandchild
465        //   └─ b
466        let mut dom: Dom = Dom::new();
467        let root = dom.root();
468        let a = dom.create_element("a");
469        let b = dom.create_element("b");
470        let grandchild = dom.create_element("g");
471        dom.append_child(root, a).unwrap();
472        dom.append_child(root, b).unwrap();
473        dom.append_child(a, grandchild).unwrap();
474        (dom, a, b, grandchild, root)
475    }
476
477    #[test]
478    fn self_is_empty() {
479        let (dom, a, _, _, _) = build();
480        assert_eq!(
481            dom.compare_document_position(a, a),
482            DocumentPosition::empty()
483        );
484    }
485
486    /// DOM §4.4 `compareDocumentPosition`: the bits describe `other`
487    /// relative to `this`. `parent.compareDocumentPosition(child)` is
488    /// `CONTAINED_BY | FOLLOWING` (20) in every browser.
489    #[test]
490    fn descendant_argument_is_contained_by_and_following() {
491        let (dom, a, _, g, _) = build();
492        let r = dom.compare_document_position(a, g);
493        assert_eq!(
494            r,
495            DocumentPosition::CONTAINED_BY | DocumentPosition::FOLLOWING
496        );
497    }
498
499    /// `child.compareDocumentPosition(parent)` is `CONTAINS | PRECEDING` (10).
500    #[test]
501    fn ancestor_argument_contains_and_precedes() {
502        let (dom, a, _, g, _) = build();
503        let r = dom.compare_document_position(g, a);
504        assert_eq!(r, DocumentPosition::CONTAINS | DocumentPosition::PRECEDING);
505    }
506
507    /// The containment branch and the sibling branch must agree on what
508    /// FOLLOWING means: "the argument comes later in tree order".
509    #[test]
510    fn following_bit_is_consistent_across_containment_and_siblings() {
511        let (dom, a, b, g, _) = build();
512        // g is inside a, and a precedes b — so g precedes b too.
513        assert!(
514            dom.compare_document_position(a, g)
515                .contains(DocumentPosition::FOLLOWING)
516        );
517        assert!(
518            dom.compare_document_position(a, b)
519                .contains(DocumentPosition::FOLLOWING)
520        );
521        assert!(
522            dom.compare_document_position(g, b)
523                .contains(DocumentPosition::FOLLOWING)
524        );
525    }
526
527    // ── Boundary points (DOM §5.2) ─────────────────────────────────────
528
529    #[test]
530    fn boundary_points_same_node_order_by_offset() {
531        use crate::Position;
532        use std::cmp::Ordering;
533        let (dom, a, _, _, _) = build();
534        assert_eq!(
535            dom.compare_boundary_points(Position::new(a, 0), Position::new(a, 1)),
536            Some(Ordering::Less)
537        );
538        assert_eq!(
539            dom.compare_boundary_points(Position::new(a, 1), Position::new(a, 1)),
540            Some(Ordering::Equal)
541        );
542    }
543
544    /// (ancestor, offset) vs a point inside child `j`: the ancestor point
545    /// is before iff `j >= offset`.
546    #[test]
547    fn boundary_points_ancestor_offset_splits_around_child_index() {
548        use crate::Position;
549        use std::cmp::Ordering;
550        let (dom, a, _, g, root) = build();
551        // root children: [a, b]; g is inside a (child index 0 of root).
552        let in_g = Position::new(g, 0);
553        assert_eq!(
554            dom.compare_boundary_points(Position::new(root, 0), in_g),
555            Some(Ordering::Less)
556        );
557        assert_eq!(
558            dom.compare_boundary_points(Position::new(root, 1), in_g),
559            Some(Ordering::Greater)
560        );
561        // Symmetric.
562        assert_eq!(
563            dom.compare_boundary_points(in_g, Position::new(root, 1)),
564            Some(Ordering::Less)
565        );
566        assert_eq!(
567            dom.compare_boundary_points(in_g, Position::new(a, 0)),
568            Some(Ordering::Greater)
569        );
570    }
571
572    /// An offset past the last child is "after every child" (a Range end
573    /// of `(parent, childCount)`), not an error.
574    #[test]
575    fn boundary_point_offset_beyond_child_count_orders_after_all_children() {
576        use crate::Position;
577        use std::cmp::Ordering;
578        let (dom, a, b, g, root) = build();
579        let end = Position::new(root, 99);
580        for inside in [
581            Position::new(a, 0),
582            Position::new(g, 0),
583            Position::new(b, 0),
584        ] {
585            assert_eq!(
586                dom.compare_boundary_points(end, inside),
587                Some(Ordering::Greater)
588            );
589            assert_eq!(
590                dom.compare_boundary_points(inside, end),
591                Some(Ordering::Less)
592            );
593        }
594    }
595
596    #[test]
597    fn boundary_points_disconnected_is_none() {
598        use crate::Position;
599        let (mut dom, a, _, _, _) = build();
600        let loose = dom.create_element("x");
601        assert_eq!(
602            dom.compare_boundary_points(Position::new(a, 0), Position::new(loose, 0)),
603            None
604        );
605    }
606
607    #[test]
608    fn siblings_ordered_by_position() {
609        let (dom, a, b, _, _) = build();
610        assert!(
611            dom.compare_document_position(a, b)
612                .contains(DocumentPosition::FOLLOWING)
613        );
614        assert!(
615            dom.compare_document_position(b, a)
616                .contains(DocumentPosition::PRECEDING)
617        );
618    }
619
620    #[test]
621    fn disconnected_nodes_flagged() {
622        let mut dom: Dom = Dom::new();
623        let a = dom.create_element("a"); // orphan
624        let b = dom.create_element("b"); // orphan
625        let r = dom.compare_document_position(a, b);
626        assert!(r.contains(DocumentPosition::DISCONNECTED));
627    }
628
629    // ── common_ancestor ──────────────────────────────────────────────
630
631    #[test]
632    fn common_ancestor_self_is_self() {
633        let (dom, a, _, _, _) = build();
634        assert_eq!(dom.common_ancestor(a, a), Some(a));
635    }
636
637    #[test]
638    fn common_ancestor_siblings_is_parent() {
639        let (dom, a, b, _, root) = build();
640        assert_eq!(dom.common_ancestor(a, b), Some(root));
641    }
642
643    #[test]
644    fn common_ancestor_nested_is_ancestor() {
645        // g is descendant of a → common ancestor is a itself.
646        let (dom, a, _, g, _) = build();
647        assert_eq!(dom.common_ancestor(a, g), Some(a));
648        assert_eq!(dom.common_ancestor(g, a), Some(a));
649    }
650
651    #[test]
652    fn common_ancestor_cousins_is_lca() {
653        // root → a → g, root → b. g and b share root.
654        let (dom, _, b, g, root) = build();
655        assert_eq!(dom.common_ancestor(g, b), Some(root));
656    }
657
658    #[test]
659    fn common_ancestor_disconnected_returns_none() {
660        let mut dom: Dom = Dom::new();
661        let a = dom.create_element("a"); // orphan
662        let b = dom.create_element("b"); // orphan
663        assert_eq!(dom.common_ancestor(a, b), None);
664    }
665
666    // ── is_equal_node ────────────────────────────────────────────────
667
668    #[test]
669    fn equal_node_same_tag_and_attrs() {
670        let mut dom: Dom = Dom::new();
671        let a = dom.create_element("div");
672        let b = dom.create_element("div");
673        dom.set_attribute(a, "role", "banner").unwrap();
674        dom.set_attribute(b, "role", "banner").unwrap();
675        assert!(dom.is_equal_node(a, b));
676    }
677
678    #[test]
679    fn unequal_different_tag() {
680        let mut dom: Dom = Dom::new();
681        let a = dom.create_element("div");
682        let b = dom.create_element("span");
683        assert!(!dom.is_equal_node(a, b));
684    }
685
686    #[test]
687    fn unequal_different_attr() {
688        let mut dom: Dom = Dom::new();
689        let a = dom.create_element("div");
690        let b = dom.create_element("div");
691        dom.set_attribute(a, "role", "banner").unwrap();
692        dom.set_attribute(b, "role", "navigation").unwrap();
693        assert!(!dom.is_equal_node(a, b));
694    }
695
696    #[test]
697    fn equal_text_nodes_same_data() {
698        let mut dom: Dom = Dom::new();
699        let a = dom.create_text_node("hi");
700        let b = dom.create_text_node("hi");
701        let c = dom.create_text_node("bye");
702        assert!(dom.is_equal_node(a, b));
703        assert!(!dom.is_equal_node(a, c));
704    }
705
706    #[test]
707    fn equal_with_children() {
708        let mut dom: Dom = Dom::new();
709        let a = dom.create_element("div");
710        let a1 = dom.create_text_node("hello");
711        dom.append_child(a, a1).unwrap();
712
713        let b = dom.create_element("div");
714        let b1 = dom.create_text_node("hello");
715        dom.append_child(b, b1).unwrap();
716
717        assert!(dom.is_equal_node(a, b));
718    }
719
720    #[test]
721    fn unequal_different_child_count() {
722        let mut dom: Dom = Dom::new();
723        let a = dom.create_element("div");
724        let a1 = dom.create_text_node("x");
725        dom.append_child(a, a1).unwrap();
726
727        let b = dom.create_element("div");
728        // no children
729
730        assert!(!dom.is_equal_node(a, b));
731    }
732
733    #[test]
734    fn unequal_different_node_types() {
735        let mut dom: Dom = Dom::new();
736        let a = dom.create_element("div");
737        let b = dom.create_text_node("div");
738        assert!(!dom.is_equal_node(a, b));
739    }
740}