use crate::diff::longest_increasing_subsequence::{
get_longest_increasing_subsequence, KeyAndChildIdx,
};
use crate::event::{EventHandler, EventName};
use crate::{AttributeValue, Patch, PatchSpecialAttribute};
use crate::{VElement, VirtualNode};
use std::collections::{HashMap, VecDeque};
use std::mem;
mod longest_increasing_subsequence;
pub fn diff<'a>(old: &'a VirtualNode, new: &'a VirtualNode) -> Vec<Patch<'a>> {
let old_node_idx = 0;
let mut diff_queue: VecDeque<DiffJob> = VecDeque::new();
diff_queue.push_back(DiffJob {
old_node_idx,
old,
new,
});
let mut ctx = DiffContext::new(old, new);
diff_recursive(&mut ctx);
ctx.take_patches()
}
enum Job<'a> {
Diff(DiffJob<'a>),
ProcessDeleted(DeleteJob<'a>),
}
#[derive(Copy, Clone)]
struct DiffJob<'a> {
old_node_idx: u32,
old: &'a VirtualNode,
new: &'a VirtualNode,
}
struct DeleteJob<'a> {
old_node_idx: u32,
old: &'a VirtualNode,
}
use self::diff_ctx::DiffContext;
mod diff_ctx {
use super::*;
pub(super) struct DiffContext<'a> {
job_queue: VecDeque<Job<'a>>,
patches: Vec<Patch<'a>>,
next_old_node_idx: u32,
}
impl<'a> DiffContext<'a> {
pub fn new(old: &'a VirtualNode, new: &'a VirtualNode) -> Self {
let mut job_queue = VecDeque::new();
job_queue.push_back(Job::Diff(DiffJob {
old_node_idx: 0,
old,
new,
}));
Self {
job_queue,
patches: Vec::new(),
next_old_node_idx: 1,
}
}
pub fn next_job(&mut self) -> Option<Job<'a>> {
self.job_queue.pop_front()
}
pub fn push_diff_job(&mut self, diff_job: DiffJob<'a>) {
self.job_queue.push_back(Job::Diff(diff_job))
}
pub fn push_delete_job(&mut self, delete_job: DeleteJob<'a>) {
self.job_queue.push_back(Job::ProcessDeleted(delete_job))
}
pub fn push_patch(&mut self, patch: Patch<'a>) {
self.patches.push(patch);
}
pub fn next_old_node_idx(&self) -> u32 {
self.next_old_node_idx
}
pub fn increment_old_node_idx(&mut self, increment: usize) {
self.next_old_node_idx += increment as u32;
}
pub fn take_patches(self) -> Vec<Patch<'a>> {
self.patches
}
}
}
fn diff_recursive(ctx: &mut DiffContext) {
let job = ctx.next_job();
match job {
Some(Job::Diff(diff_job)) => process_diff_job(ctx, diff_job),
Some(Job::ProcessDeleted(delete_job)) => process_delete_job(ctx, delete_job),
None => return,
};
diff_recursive(ctx);
}
fn process_diff_job<'a>(ctx: &mut DiffContext<'a>, diff_job: DiffJob<'a>) {
let old = diff_job.old;
let new = diff_job.new;
let old_node_idx = diff_job.old_node_idx;
let node_variants_different = mem::discriminant(old) != mem::discriminant(new);
let mut element_tags_different = false;
if let (VirtualNode::Element(old_element), VirtualNode::Element(new_element)) = (old, new) {
element_tags_different = old_element.tag != new_element.tag;
}
let should_fully_replace_node = node_variants_different || element_tags_different;
if should_fully_replace_node {
replace_node(diff_job, ctx);
return;
}
match (old, new) {
(VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
if old_text != new_text {
ctx.push_patch(Patch::ChangeText(old_node_idx, &new_text));
}
}
(VirtualNode::Element(old_element), VirtualNode::Element(new_element)) => {
let mut attributes_to_add: HashMap<&str, &AttributeValue> = HashMap::new();
let mut attributes_to_remove: Vec<&str> = vec![];
let mut events_to_add = HashMap::new();
let mut events_to_remove = vec![];
find_attributes_to_add(
old_node_idx,
&mut attributes_to_add,
old_element,
new_element,
ctx,
);
find_attributes_to_remove(
&mut attributes_to_add,
&mut attributes_to_remove,
old_element,
new_element,
);
find_events_to_add(&mut events_to_add, old_element, new_element);
find_events_to_remove(
&mut events_to_add,
&mut events_to_remove,
old_element,
new_element,
);
if attributes_to_add.len() > 0 {
ctx.push_patch(Patch::AddAttributes(old_node_idx, attributes_to_add));
}
if attributes_to_remove.len() > 0 {
ctx.push_patch(Patch::RemoveAttributes(old_node_idx, attributes_to_remove));
}
if events_to_remove.len() > 0 {
ctx.push_patch(Patch::RemoveEvents(old_node_idx, events_to_remove));
}
if events_to_add.len() > 0 {
ctx.push_patch(Patch::AddEvents(old_node_idx, events_to_add));
}
maybe_push_inner_html_patch(new, old_element, new_element, old_node_idx, ctx);
maybe_push_on_create_element_patch(new, old_element, new_element, old_node_idx, ctx);
maybe_push_on_remove_element_patch(old, old_element, new_element, old_node_idx, ctx);
generate_patches_for_children(old_node_idx, old_element, new_element, ctx);
}
(VirtualNode::Text(_), VirtualNode::Element(_))
| (VirtualNode::Element(_), VirtualNode::Text(_)) => {
unreachable!("Unequal variant discriminants should already have been handled");
}
};
}
fn process_delete_job<'a>(ctx: &mut DiffContext<'a>, delete_job: DeleteJob<'a>) {
if let VirtualNode::Element(element_node) = delete_job.old {
if element_node.events.len() > 0 {
ctx.push_patch(Patch::RemoveAllVirtualEventsWithNodeIdx(
delete_job.old_node_idx,
));
}
if element_node
.special_attributes
.on_remove_element_key()
.is_some()
{
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnRemoveElem(delete_job.old_node_idx, delete_job.old),
));
}
maybe_push_delete_jobs_for_children(ctx, delete_job.old);
}
}
fn replace_node<'a>(diff_job: DiffJob<'a>, ctx: &mut DiffContext<'a>) {
if let Some(elem) = diff_job.old.as_velement_ref() {
if elem.events.has_events() {
ctx.push_patch(Patch::RemoveAllVirtualEventsWithNodeIdx(
diff_job.old_node_idx,
));
}
}
match diff_job.old.as_velement_ref() {
Some(elem) if elem.special_attributes.on_remove_element_key().is_some() => {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnRemoveElem(diff_job.old_node_idx, diff_job.old),
));
}
_ => {}
};
ctx.push_patch(Patch::Replace {
old_idx: diff_job.old_node_idx,
new_node: diff_job.new,
});
maybe_push_delete_jobs_for_children(ctx, diff_job.old);
}
fn maybe_push_delete_jobs_for_children<'a>(ctx: &mut DiffContext<'a>, node: &'a VirtualNode) {
if let VirtualNode::Element(old_element_node) = node {
let node_idx_of_first_child = ctx.next_old_node_idx();
ctx.increment_old_node_idx(old_element_node.children.len());
for (idx, child) in old_element_node.children.iter().enumerate() {
let cur_node_idx = node_idx_of_first_child + idx as u32;
ctx.push_delete_job(DeleteJob {
old_node_idx: cur_node_idx,
old: &child,
});
}
}
}
fn find_attributes_to_add<'a>(
cur_node_idx: u32,
attributes_to_add: &mut HashMap<&'a str, &'a AttributeValue>,
old_element: &VElement,
new_element: &'a VElement,
ctx: &mut DiffContext<'a>,
) {
for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
if new_attr_name == "key" {
continue;
}
match old_element.attrs.get(new_attr_name) {
Some(ref old_attr_val) => {
if old_attr_val != &new_attr_val {
attributes_to_add.insert(new_attr_name, new_attr_val);
} else if new_attr_name == "value" {
ctx.push_patch(Patch::ValueAttributeUnchanged(cur_node_idx, new_attr_val));
} else if new_attr_name == "checked" {
ctx.push_patch(Patch::CheckedAttributeUnchanged(cur_node_idx, new_attr_val));
}
}
None => {
attributes_to_add.insert(new_attr_name, new_attr_val);
}
};
}
}
fn find_attributes_to_remove<'a>(
attributes_to_add: &mut HashMap<&str, &AttributeValue>,
attributes_to_remove: &mut Vec<&'a str>,
old_element: &'a VElement,
new_element: &VElement,
) {
for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
if old_attr_name == "key" {
continue;
}
if attributes_to_add.get(&old_attr_name[..]).is_some() {
continue;
};
match new_element.attrs.get(old_attr_name) {
Some(ref new_attr_val) => {
if new_attr_val != &old_attr_val {
attributes_to_remove.push(old_attr_name);
}
}
None => {
attributes_to_remove.push(old_attr_name);
}
};
}
}
fn find_events_to_add<'a>(
events_to_add: &mut HashMap<&'a EventName, &'a EventHandler>,
old_element: &VElement,
new_element: &'a VElement,
) {
for (new_event_name, new_event) in new_element.events.iter() {
if !old_element.events.contains_key(new_event_name) {
events_to_add.insert(new_event_name, new_event);
}
}
}
fn find_events_to_remove<'a>(
events_to_add: &mut HashMap<&'a EventName, &'a EventHandler>,
events_to_remove: &mut Vec<(&'a EventName, &'a EventHandler)>,
old_element: &'a VElement,
new_element: &'a VElement,
) {
for (old_event_name, old_event) in old_element.events.iter() {
if events_to_add.contains_key(old_event_name) {
continue;
};
if new_element.events.contains_key(old_event_name) {
continue;
}
events_to_remove.push((old_event_name, old_event));
}
}
fn maybe_push_inner_html_patch<'a>(
new: &'a VirtualNode,
old_element: &VElement,
new_element: &VElement,
old_node_idx: u32,
ctx: &mut DiffContext<'a>,
) {
match (
old_element.special_attributes.dangerous_inner_html.as_ref(),
new_element.special_attributes.dangerous_inner_html.as_ref(),
) {
(None, Some(_)) => {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::SetDangerousInnerHtml(old_node_idx, new),
));
}
(Some(old_inner), Some(new_inner)) => {
if old_inner != new_inner {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::SetDangerousInnerHtml(old_node_idx, new),
));
}
}
(Some(_), None) => {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::RemoveDangerousInnerHtml(old_node_idx),
));
}
(None, None) => {}
};
}
fn maybe_push_on_create_element_patch<'a>(
new: &'a VirtualNode,
old_element: &VElement,
new_element: &VElement,
old_node_idx: u32,
ctx: &mut DiffContext<'a>,
) {
match (
old_element.special_attributes.on_create_element_key(),
new_element.special_attributes.on_create_element_key(),
) {
(None, Some(_)) => {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnCreateElemOnExistingNode(old_node_idx, new),
));
}
(Some(old_id), Some(new_id)) => {
if new_id != old_id {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnCreateElemOnExistingNode(old_node_idx, new),
));
}
}
(Some(_), None) | (None, None) => {}
};
}
fn maybe_push_on_remove_element_patch<'a>(
old: &'a VirtualNode,
old_element: &VElement,
new_element: &VElement,
old_node_idx: u32,
ctx: &mut DiffContext<'a>,
) {
let old_on_remove_elem_key = old_element.special_attributes.on_remove_element_key();
let should_call_on_remove_elem = match (
old_on_remove_elem_key,
new_element.special_attributes.on_remove_element_key(),
) {
(Some(_), None) => true,
(Some(old_id), Some(new_id)) => old_id != new_id,
_ => false,
};
if should_call_on_remove_elem {
ctx.push_patch(Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnRemoveElem(old_node_idx, old),
));
}
}
fn generate_patches_for_children<'a, 'b>(
parent_old_node_idx: u32,
old_element: &'a VElement,
new_element: &'a VElement,
ctx: &mut DiffContext<'a>,
) {
let old_child_count = old_element.children.len();
let mut key_to_old_child_idx: HashMap<ElementKey, usize> = HashMap::new();
let mut key_to_new_child_idx: HashMap<ElementKey, usize> = HashMap::new();
let mut old_non_keyed_and_no_longer_keyed_nodes: VecDeque<usize> = Default::default();
let mut new_node_keys: HashMap<usize, ElementKey> = HashMap::new();
let mut old_tracked_indices = TrackedImplicitlyKeyableIndices::default();
let mut new_tracked_indices = TrackedImplicitlyKeyableIndices::default();
let mut old_child_indices_of_preserved_keys = vec![];
let node_idx_of_first_child = ctx.next_old_node_idx();
ctx.increment_old_node_idx(old_element.children.len());
for (idx, new_child) in new_element.children.iter().enumerate() {
let implicit_key = new_tracked_indices.get_and_increment(new_child);
let new_key = node_key(new_child, implicit_key);
if let Some(new_key) = new_key {
new_node_keys.insert(idx, new_key);
key_to_new_child_idx.insert(new_key, idx);
}
}
for (idx, old_child) in old_element.children.iter().enumerate() {
let implicit_key = old_tracked_indices.get_and_increment(old_child);
let old_key = node_key(old_child, implicit_key);
match old_key {
Some(old_key) if key_to_new_child_idx.contains_key(&old_key) => {
key_to_old_child_idx.insert(old_key, idx);
}
_ => {
old_non_keyed_and_no_longer_keyed_nodes.push_back(idx);
}
}
}
for new_child_idx in 0..new_element.children.len() {
let key = new_node_keys.get(&new_child_idx);
let Some(key) = key else {
continue;
};
let old_key_child_idx = key_to_old_child_idx.get(key);
let old_key_child_idx = if let Some(k) = old_key_child_idx {
k
} else {
continue;
};
old_child_indices_of_preserved_keys.push(KeyAndChildIdx {
key: *key,
child_idx: *old_key_child_idx,
});
}
let longest_increasing: HashMap<ElementKey, usize> =
get_longest_increasing_subsequence(&old_child_indices_of_preserved_keys)
.into_iter()
.map(|k| (k.key, k.child_idx))
.collect();
enum InsertBeforeOrMoveBefore<'a> {
InsertBefore(&'a VirtualNode),
MoveBefore(u32),
}
enum PlaceBeforeKind {
Insert,
Move,
}
let mut insert_before = vec![];
let mut move_before = vec![];
let mut insert_before_or_move = vec![];
let mut jobs: Vec<(usize, DiffJob)> = vec![];
let mut new_tracked_indices = TrackedImplicitlyKeyableIndices::default();
for (new_child_idx, new_child_node) in new_element.children.iter().enumerate() {
let implicit_key = new_tracked_indices.get_and_increment(new_child_node);
let key = node_key(new_child_node, implicit_key);
let old_child_idx = key.as_ref().and_then(|key| longest_increasing.get(key));
match old_child_idx {
Some(old_child_idx) => {
let old_idx = node_idx_of_first_child + *old_child_idx as u32;
let mut previous = None;
for insert_or_move in &insert_before_or_move {
match insert_or_move {
InsertBeforeOrMoveBefore::InsertBefore(insert) => {
if matches!(previous, Some(PlaceBeforeKind::Move)) {
maybe_push_move_before(ctx, old_idx, &mut move_before);
}
insert_before.push(*insert);
previous = Some(PlaceBeforeKind::Insert);
}
InsertBeforeOrMoveBefore::MoveBefore(m) => {
if matches!(previous, Some(PlaceBeforeKind::Insert)) {
maybe_push_insert_before(ctx, old_idx, &mut insert_before);
}
move_before.push(*m);
previous = Some(PlaceBeforeKind::Move);
}
}
}
insert_before_or_move.clear();
maybe_push_insert_before(ctx, old_idx, &mut insert_before);
maybe_push_move_before(ctx, old_idx, &mut move_before);
let old_idx = node_idx_of_first_child + *old_child_idx as u32;
let job = DiffJob {
old_node_idx: old_idx,
old: &old_element.children[*old_child_idx],
new: new_child_node,
};
jobs.push((*old_child_idx, job));
}
None => {
let old_child_idx = key.and_then(|key| key_to_old_child_idx.get(&key));
if let Some(old_child_idx) = old_child_idx {
let old_idx = node_idx_of_first_child + *old_child_idx as u32;
insert_before_or_move.push(InsertBeforeOrMoveBefore::MoveBefore(old_idx));
let job = DiffJob {
old_node_idx: old_idx,
old: &old_element.children[*old_child_idx],
new: new_child_node,
};
jobs.push((*old_child_idx, job));
} else {
if let Some(old_child_idx) = old_non_keyed_and_no_longer_keyed_nodes.pop_front()
{
let old_idx = node_idx_of_first_child + old_child_idx as u32;
if old_child_idx != new_child_idx {
insert_before_or_move
.push(InsertBeforeOrMoveBefore::MoveBefore(old_idx));
}
let job = DiffJob {
old_node_idx: old_idx,
old: &old_element.children[old_child_idx],
new: new_child_node,
};
jobs.push((old_child_idx, job));
} else {
insert_before_or_move
.push(InsertBeforeOrMoveBefore::InsertBefore(new_child_node));
}
}
}
}
}
jobs.sort_by(|a, b| a.0.cmp(&b.0));
let mut to_remove = vec![];
for child_idx in 0..old_child_count {
if jobs.get(0).map(|j| j.0) == Some(child_idx) {
let job = jobs.remove(0);
ctx.push_diff_job(job.1);
} else {
let node_idx = node_idx_of_first_child + child_idx as u32;
to_remove.push(node_idx);
ctx.push_delete_job(DeleteJob {
old_node_idx: node_idx,
old: &old_element.children[child_idx],
});
}
}
if to_remove.len() > 0 {
ctx.push_patch(Patch::RemoveChildren {
parent_old_node_idx,
to_remove,
});
}
let mut appends = vec![];
let mut moves = vec![];
let mut previous = None;
for item in insert_before_or_move {
match item {
InsertBeforeOrMoveBefore::InsertBefore(new_node) => {
if matches!(previous, Some(PlaceBeforeKind::Move)) {
ctx.push_patch(Patch::MoveToEndOfSiblings {
parent_old_node_idx,
siblings_to_move: moves.clone(),
});
moves.clear();
}
appends.push(new_node);
previous = Some(PlaceBeforeKind::Insert);
}
InsertBeforeOrMoveBefore::MoveBefore(m) => {
if matches!(previous, Some(PlaceBeforeKind::Insert)) {
ctx.push_patch(Patch::AppendChildren {
parent_old_node_idx,
new_nodes: appends.clone(),
});
appends.clear();
}
moves.push(m);
previous = Some(PlaceBeforeKind::Move);
}
}
}
if appends.len() >= 1 {
ctx.push_patch(Patch::AppendChildren {
parent_old_node_idx,
new_nodes: appends.clone(),
});
} else if moves.len() >= 1 {
ctx.push_patch(Patch::MoveToEndOfSiblings {
parent_old_node_idx,
siblings_to_move: moves.clone(),
});
}
}
fn maybe_push_insert_before<'a>(
ctx: &mut DiffContext<'a>,
old_idx: u32,
new_nodes: &mut Vec<&'a VirtualNode>,
) {
if new_nodes.len() == 0 {
return;
}
ctx.push_patch(Patch::InsertBefore {
anchor_old_node_idx: old_idx,
new_nodes: new_nodes.clone(),
});
new_nodes.clear();
}
fn maybe_push_move_before<'a>(ctx: &mut DiffContext<'a>, old_idx: u32, move_before: &mut Vec<u32>) {
if move_before.len() == 0 {
return;
}
ctx.push_patch(Patch::MoveNodesBefore {
anchor_old_node_idx: old_idx,
to_move: move_before.clone(),
});
move_before.clear();
}
fn node_key<'a>(
node: &'a VirtualNode,
implicit_elem_key: Option<ElementKeyImplicit<'a>>,
) -> Option<ElementKey<'a>> {
let elem = node.as_velement_ref()?;
let explicit_key = elem
.attrs
.get("key")
.and_then(|k| k.as_string().map(String::as_str))
.map(ElementKey::Explicit);
if explicit_key.is_some() {
return explicit_key;
}
implicit_elem_key.map(ElementKey::Implicit)
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(test, derive(Debug))]
enum ElementKey<'a> {
Explicit(&'a str),
Implicit(ElementKeyImplicit<'a>),
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(test, derive(Debug))]
struct ElementKeyImplicit<'a> {
tag: &'a str,
idx_amongst_siblings_with_same_tag: usize,
}
#[derive(Default)]
struct TrackedImplicitlyKeyableIndices<'a>(HashMap<&'a str, usize>);
impl<'a> TrackedImplicitlyKeyableIndices<'a> {
pub fn get_and_increment(&mut self, node: &'a VirtualNode) -> Option<ElementKeyImplicit<'a>> {
let elem = node.as_velement_ref()?;
if elem.attrs.get("key").is_some() {
return None;
}
let tag = elem.tag.as_str();
let idx = self.0.entry(tag).or_insert(0);
let current = *idx;
*idx += 1;
Some(ElementKeyImplicit {
tag,
idx_amongst_siblings_with_same_tag: current,
})
}
}
#[cfg(test)]
mod diff_test_case;
#[cfg(test)]
mod tests {
use super::diff_test_case::*;
use super::*;
use crate::event::EventName;
use crate::{html, EventAttribFn, PatchSpecialAttribute, VText, VirtualNode};
use std::collections::HashMap;
use virtual_node::IterableNodes;
#[test]
fn replace_node() {
DiffTestCase {
old: html! { <div> </div> },
new: html! { <span> </span> },
expected: vec![Patch::Replace {
old_idx: 0,
new_node: &html! { <span></span> },
}],
}
.test();
DiffTestCase {
old: html! { <div> <b></b> </div> },
new: html! { <div> <strong></strong> </div> },
expected: vec![Patch::Replace {
old_idx: 1,
new_node: &html! { <strong></strong> },
}],
}
.test();
DiffTestCase {
old: html! { <div> <b>1</b> <em></em> </div> },
new: html! { <div> <i>{"1"} {"2"}</i> <br /> </div>},
expected: vec![
Patch::Replace {
old_idx: 1,
new_node: &html! { <i>{"1"} {"2"}</i> },
},
Patch::Replace {
old_idx: 2,
new_node: &html! { <br /> },
},
],
}
.test();
DiffTestCase {
old: html! {
<div>
<span>
<div>
<strong></strong>
</div>
</span>
<em>
<div>
<img />
</div>
</em>
</div>
},
new: html! {
<div>
<span>
<div>
<em></em>
</div>
</span>
<em>
<div>
<i></i>
</div>
</em>
</div>
},
expected: vec![
Patch::Replace {
old_idx: 5,
new_node: &VirtualNode::element("em"),
},
Patch::Replace {
old_idx: 6,
new_node: &VirtualNode::element("i"),
},
],
}
.test();
}
#[test]
fn replace_node_proper_old_node_idx() {
DiffTestCase {
old: html! {
<div>
<div><em></em></div>
<div></div>
</div>
},
new: html! {
<div>
<span></span>
<strong></strong>
</div>
},
expected: vec![
Patch::Replace {
old_idx: 1,
new_node: &html! { <span></span> },
},
Patch::Replace {
old_idx: 2,
new_node: &html! { <strong></strong> },
},
],
}
.test();
DiffTestCase {
old: html! {
<div>
<div>
<em></em>
</div>
<div></div>
</div>
},
new: html! {
<div>
<div>
<span></span>
</div>
<div></div>
</div>
},
expected: vec![Patch::Replace {
old_idx: 3,
new_node: &html! { <span></span> },
}],
}
.test();
}
#[test]
fn add_children() {
DiffTestCase {
old: html! { <div> <b></b> </div> },
new: html! { <div> <b></b> <span></span> </div> },
expected: vec![Patch::AppendChildren {
parent_old_node_idx: 0,
new_nodes: vec![&html! { <span></span> }],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<span><em></em></span>
<div>
<br />
</div>
</div>
},
new: html! {
<div>
<span><em></em></span>
<div>
<br />
<div><br /></div>
<div></div>
</div>
</div>
},
expected: vec![Patch::AppendChildren {
parent_old_node_idx: 2,
new_nodes: vec![&html! { <div><br /></div> }, &html! { <div></div> }],
}],
}
.test();
}
#[test]
fn replace_and_append() {
DiffTestCase {
old: html! {
<div>
<span><em></em></span>
<div>
<br />
</div>
</div>
},
new: html! {
<div>
<i></i>
<div>
<br />
<div><br /></div>
<div></div>
</div>
</div>
},
expected: vec![
Patch::Replace {
old_idx: 1,
new_node: &html! { <i></i>},
},
Patch::AppendChildren {
parent_old_node_idx: 2,
new_nodes: vec![&html! { <div><br /></div> }, &html! { <div></div> }],
},
],
}
.test();
}
#[test]
fn truncate_children() {
DiffTestCase {
old: html! { <div> <b></b> <span></span> </div> },
new: html! { <div> </div> },
expected: vec![Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![1, 2],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<span>
<b></b>
<i></i>
</span>
<div>
<em></em>
</div>
<strong></strong>
</div>
},
new: html! {
<div>
<span>
<b></b>
</span>
<div>
</div>
</div>
},
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![3],
},
Patch::RemoveChildren {
parent_old_node_idx: 1,
to_remove: vec![5],
},
Patch::RemoveChildren {
parent_old_node_idx: 2,
to_remove: vec![6],
},
],
}
.test();
}
#[test]
fn replace_and_truncate() {
DiffTestCase {
old: html! {
<div>
<b>
<i></i>
<em></em>
</b>
<b></b>
</div>
},
new: html! {
<div>
<b>
<i></i>
</b>
<strong></strong>
</div>
},
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 1,
to_remove: vec![4],
},
Patch::Replace {
old_idx: 2,
new_node: &html! { <strong></strong> },
},
],
}
.test();
}
#[test]
fn remove_first_sibling_that_has_children_then_move_child_of_second_sibling() {
DiffTestCase {
old: html! {
<div>
<span>
<source />
<source />
</span>
<div>
<link />
<param />
<area />
<br />
</div>
</div>
},
new: html! {
<div>
<div>
<param />
<area />
<link />
<br />
</div>
</div>
},
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![1],
},
Patch::MoveNodesBefore {
anchor_old_node_idx: 8,
to_move: vec![5],
},
],
}
.test();
}
#[test]
fn add_attributes() {
let mut attributes = HashMap::new();
let id = "hello".into();
attributes.insert("id", &id);
DiffTestCase {
old: html! { <div> </div> },
new: html! { <div id="hello"> </div> },
expected: vec![Patch::AddAttributes(0, attributes.clone())],
}
.test();
DiffTestCase {
old: html! { <div id="foobar"> </div> },
new: html! { <div id="hello"> </div> },
expected: vec![Patch::AddAttributes(0, attributes)],
}
.test();
}
#[test]
fn remove_attributes() {
DiffTestCase {
old: html! { <div id="hey-there"></div> },
new: html! { <div> </div> },
expected: vec![Patch::RemoveAttributes(0, vec!["id"])],
}
.test();
}
#[test]
fn change_attribute() {
let mut attributes = HashMap::new();
let id = "changed".into();
attributes.insert("id", &id);
DiffTestCase {
old: html! { <div id="hey-there"></div> },
new: html! { <div id="changed"> </div> },
expected: vec![Patch::AddAttributes(0, attributes)],
}
.test();
}
#[test]
fn replace_text_node() {
DiffTestCase {
old: html! { Old },
new: html! { New },
expected: vec![Patch::ChangeText(0, &VText::new("New"))],
}
.test();
}
#[test]
fn always_pushes_patch_for_value() {
DiffTestCase {
old: html! { <input value="abc" /> },
new: html! { <input value="abc" /> },
expected: vec![Patch::ValueAttributeUnchanged(0, &"abc".into())],
}
.test();
DiffTestCase {
old: html! { <textarea value="abc" /> },
new: html! { <textarea value="abc" /> },
expected: vec![Patch::ValueAttributeUnchanged(0, &"abc".into())],
}
.test();
DiffTestCase {
old: html! { <textarea value="abc" /> },
new: html! { <textarea value="def" /> },
expected: vec![Patch::AddAttributes(
0,
vec![("value", &"def".into())].into_iter().collect(),
)],
}
.test();
}
#[test]
fn always_pushes_patch_for_checked() {
for checkedness in [false, true] {
DiffTestCase {
old: html! { <input checked={checkedness} /> },
new: html! { <input checked={checkedness} /> },
expected: vec![Patch::CheckedAttributeUnchanged(0, &checkedness.into())],
}
.test();
}
for old_checkedness in [false, true] {
let new_checkedness = !old_checkedness;
DiffTestCase {
old: html! { <input checked=old_checkedness /> },
new: html! { <input checked=new_checkedness /> },
expected: vec![Patch::AddAttributes(
0,
vec![("checked", &new_checkedness.into())]
.into_iter()
.collect(),
)],
}
.test();
}
}
#[test]
fn on_create_elem() {
let old = VirtualNode::element("div");
let mut new = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut new, "150");
let mut expected = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut expected, "150");
DiffTestCase {
old,
new,
expected: vec![Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnCreateElemOnExistingNode(0, &expected),
)],
}
.test();
}
#[test]
fn nested_on_create_element() {
DiffTestCase {
old: html! {
<div>
<span>
<div></div>
</span>
<br />
<em>
<br />
<div>
</div>
</em>
</div>
},
new: html! {
<div>
<span key="150" on_create_element=|_: web_sys::Element|{} >
<div></div>
</span>
<br />
<em key="200" on_create_element=|_: web_sys::Element|{} >
<br />
<div key="250" on_create_element=|_: web_sys::Element|{} >
</div>
</em>
</div>
},
expected: vec![
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnCreateElemOnExistingNode(
1,
&html! {
<span key="150" on_create_element=|_: web_sys::Element|{} >
<div></div>
</span>
},
)),
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnCreateElemOnExistingNode(
3,
&html! {
<em key="200" on_create_element=|_: web_sys::Element|{} >
<br />
<div key="250" on_create_element=|_: web_sys::Element|{} >
</div>
</em>
},
)),
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnCreateElemOnExistingNode(
6,
&html! {
<div key="250" on_create_element=|_: web_sys::Element|{} >
</div>
},
)),
],
}
.test();
}
#[test]
fn same_on_create_elem_id() {
let mut old = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut old, "70");
let mut new = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut new, "70");
DiffTestCase {
old,
new,
expected: vec![],
}
.test();
}
#[test]
fn different_on_create_elem_id() {
let mut old = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut old, "50");
let mut new = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut new, "99");
let mut expected = VirtualNode::element("div");
set_on_create_elem_with_unique_id(&mut expected, "99");
DiffTestCase {
old,
new,
expected: vec![Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnCreateElemOnExistingNode(0, &expected),
)],
}
.test();
}
#[test]
fn on_remove_elem_for_replaced_elem() {
let mut old = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut old, "150");
let expected = {
let mut old = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut old, "150");
old
};
let new = VirtualNode::element("span");
DiffTestCase {
old,
new,
expected: vec![
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(0, &expected)),
Patch::Replace {
old_idx: 0,
new_node: &VirtualNode::element("span"),
},
],
}
.test();
}
#[test]
fn on_remove_elem_for_replaced_children_recursively() {
DiffTestCase {
old: html! {
<div>
<em key="key" on_remove_element=||{}>
<br />
<strong key="key" on_remove_element=||{}></strong>
</em>
<br />
<div key="key" on_remove_element=||{}></div>
</div>
},
new: html! {
<span></span>
},
expected: vec![
Patch::Replace {
old_idx: 0,
new_node: &VirtualNode::element("span"),
},
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
1,
&html! {
<em key="key" on_remove_element=||{}>
<br />
<strong key="key" on_remove_element=||{}></strong>
</em>
},
)),
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
3,
&html! {
<div key="key" on_remove_element=||{}></div>
},
)),
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
5,
&html! {
<strong key="key" on_remove_element=||{}></strong>
},
)),
],
}
.test();
}
#[test]
fn on_remove_elem_for_truncated_children_recursively() {
let mut grandchild = VirtualNode::element("strong");
set_on_remove_elem_with_unique_id(&mut grandchild, "key");
let mut child = VirtualNode::element("em");
set_on_remove_elem_with_unique_id(&mut child, "key");
child.as_velement_mut().unwrap().children.push(grandchild);
let old = html! {
<div>
<span>
<em></em>
</span>
{child}
</div>
};
let new = html! {
<div>
<span>
<em></em>
</span>
</div>
};
let expected_child = {
let mut grandchild = VirtualNode::element("strong");
set_on_remove_elem_with_unique_id(&mut grandchild, "key");
let mut child = VirtualNode::element("em");
set_on_remove_elem_with_unique_id(&mut child, "key");
child.as_velement_mut().unwrap().children.push(grandchild);
child
};
let expected_grandchild = {
let mut grandchild = VirtualNode::element("strong");
set_on_remove_elem_with_unique_id(&mut grandchild, "key");
grandchild
};
DiffTestCase {
old,
new,
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![2],
},
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
2,
&expected_child,
)),
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
4,
&expected_grandchild,
)),
],
}
.test();
}
#[test]
fn new_node_does_not_have_on_remove_elem() {
let old_child = on_remove_node_with_on_remove_child();
let mut old = html! {
<div>
{old_child}
</div>
};
set_on_remove_elem_with_unique_id(&mut old, "some-key");
let expected = {
let old_child = on_remove_node_with_on_remove_child();
let mut old = html! {
<div>
{old_child}
</div>
};
set_on_remove_elem_with_unique_id(&mut old, "some-key");
old
};
let new_child = on_remove_node_with_on_remove_child();
let new = html! {
<div>
{new_child}
</div>
};
DiffTestCase {
old,
new,
expected: vec![Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnRemoveElem(0, &expected),
)],
}
.test();
}
#[test]
fn different_on_remove_elem_id() {
let old_child = on_remove_node_with_on_remove_child();
let mut old = html! {
<div>
{old_child}
</div>
};
set_on_remove_elem_with_unique_id(&mut old, "start");
let expected = {
let old_child = on_remove_node_with_on_remove_child();
let mut old = html! {
<div>
{old_child}
</div>
};
set_on_remove_elem_with_unique_id(&mut old, "start");
old
};
let new_child = on_remove_node_with_on_remove_child();
let mut new = html! {
<div>
{new_child}
</div>
};
set_on_remove_elem_with_unique_id(&mut new, "end");
DiffTestCase {
old,
new,
expected: vec![Patch::SpecialAttribute(
PatchSpecialAttribute::CallOnRemoveElem(0, &expected),
)],
}
.test();
}
#[test]
fn same_on_remove_elem_id() {
let mut old = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut old, "same");
let mut new = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut new, "same");
DiffTestCase {
old,
new,
expected: vec![],
}
.test();
}
#[test]
fn same_dangerous_inner_html() {
let mut old = VirtualNode::element("div");
set_dangerous_inner_html(&mut old, "hi");
let mut new = VirtualNode::element("div");
set_dangerous_inner_html(&mut new, "hi");
DiffTestCase {
old,
new,
expected: vec![],
}
.test();
}
#[test]
fn different_dangerous_inner_html() {
let mut old = VirtualNode::element("div");
set_dangerous_inner_html(&mut old, "old");
let mut new = VirtualNode::element("div");
set_dangerous_inner_html(&mut new, "new");
let mut expected = VirtualNode::element("div");
set_dangerous_inner_html(&mut expected, "new");
DiffTestCase {
old,
new,
expected: vec![Patch::SpecialAttribute(
PatchSpecialAttribute::SetDangerousInnerHtml(0, &expected),
)],
}
.test();
}
#[test]
fn remove_dangerous_inner_html() {
let mut old = VirtualNode::element("div");
set_dangerous_inner_html(&mut old, "hi");
let new = html! { <div><em></em></div> };
DiffTestCase {
old,
new,
expected: vec![
Patch::SpecialAttribute(PatchSpecialAttribute::RemoveDangerousInnerHtml(0)),
Patch::AppendChildren {
parent_old_node_idx: 0,
new_nodes: vec![&VirtualNode::element("em")],
},
],
}
.test();
}
#[test]
fn does_not_set_events_id_if_already_had_events() {
let mut old = VElement::new("div");
old.events.insert(onclick_name(), mock_event_handler());
let mut new = VElement::new("div");
new.events.insert(onclick_name(), mock_event_handler());
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![],
}
.test();
}
#[test]
fn does_not_reset_events_id_if_earlier_node_replaced_by_same_number_of_nodes() {
let old = html! {
<div>
<span>
<em>
<area />
</em>
</span>
<strong onclick=|| {}>
<div></div>
<a onclick=|| {}></a>
</strong>
</div>
};
let new = html! {
<div>
<div>
<ul>
<li> </li>
</ul>
</div>
<strong onclick=|| {}>
<div></div>
<a onclick=|| {}></a>
</strong>
</div>
};
DiffTestCase {
old,
new,
expected: vec![Patch::Replace {
old_idx: 1,
new_node: &html! {<div> <ul> <li> </li> </ul> </div>},
}],
}
.test();
}
#[test]
fn removes_events_if_no_more_events() {
let mut old = VElement::new("div");
old.events.insert(onclick_name(), mock_event_handler());
let new = VElement::new("div");
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![Patch::RemoveEvents(
0,
vec![(&EventName::ONCLICK, &mock_event_handler())]
.into_iter()
.collect(),
)],
}
.test();
}
#[test]
fn remove_event_patches_come_before_add_event_patches() {
let mut old = VElement::new("div");
old.events.insert(oninput_name(), mock_event_handler());
let mut new = VElement::new("div");
new.events.insert(onmousemove_name(), mock_event_handler());
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![
Patch::RemoveEvents(0, vec![(&oninput_name(), &mock_event_handler())]),
Patch::AddEvents(
0,
vec![(&onmousemove_name(), &mock_event_handler())]
.into_iter()
.collect(),
),
],
}
.test();
}
#[test]
fn remove_all_tracked_events_if_replaced() {
let mut old = VElement::new("div");
old.events.insert(oninput_name(), mock_event_handler());
let new = VElement::new("some-other-element");
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![
Patch::RemoveAllVirtualEventsWithNodeIdx(0),
Patch::Replace {
old_idx: 0,
new_node: &VirtualNode::Element(VElement::new("some-other-element")),
},
],
}
.test();
}
#[test]
fn removes_tracked_events_if_ancestor_replaced() {
let mut old = VElement::new("div");
old.children.push(VirtualNode::Element(VElement::new("a")));
old.children.push(VirtualNode::text("b"));
let mut child_of_old = VElement::new("div");
child_of_old
.events
.insert(oninput_name(), mock_event_handler());
old.children.push(VirtualNode::Element(child_of_old));
let new = VElement::new("some-other-element");
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![
Patch::Replace {
old_idx: 0,
new_node: &VirtualNode::Element(VElement::new("some-other-element")),
},
Patch::RemoveAllVirtualEventsWithNodeIdx(3),
],
}
.test();
}
#[test]
fn remove_tracked_events_if_truncated() {
let mut old = VElement::new("div");
let mut child_of_old = VElement::new("div");
child_of_old
.events
.insert(oninput_name(), mock_event_handler());
old.children.push(VirtualNode::Element(child_of_old));
let new = VElement::new("div");
DiffTestCase {
old: VirtualNode::Element(old),
new: VirtualNode::Element(new),
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![1],
},
Patch::RemoveAllVirtualEventsWithNodeIdx(1),
],
}
.test();
}
#[test]
fn prepend_new_element_to_keyed_list() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
</div>
},
new: html! {
<div>
<span></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![&VirtualNode::element("span")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<div>
<div></div>
</div>
<div>
<em key="a"></em>
</div>
</div>
},
new: html! {
<div>
<div>
<div></div>
</div>
<div>
<span key="new"></span>
<em key="a"></em>
</div>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 4,
new_nodes: vec![&node_with_key("span", "new")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
</div>
},
new: html! {
<div>
<footer></footer>
<span></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![
&VirtualNode::element("footer"),
&VirtualNode::element("span"),
],
}],
}
.test();
}
#[test]
fn move_keyed_node_before_and_append_children_to_that_nodes_child() {
DiffTestCase {
old: html! {
<div>
<span key="a">
<img />
</span>
<em key="b">
<div>
</div>
</em>
</div>
},
new: html! {
<div>
<em key="b">
<div>
<br />
</div>
</em>
<span key="a">
<img />
</span>
</div>
},
expected: vec![
Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
},
Patch::AppendChildren {
parent_old_node_idx: 4,
new_nodes: vec![&VirtualNode::element("br")],
},
],
}
.test();
}
#[test]
fn remove_unkeyed_node_in_between_keyed_nodes() {
DiffTestCase {
old: html! {
<div>
<div key="a"></div>
<span></span>
<strong></strong>
<em key="b"></em>
</div>
},
new: html! {
<div>
<div key="a"></div>
<em key="b"></em>
</div>
},
expected: vec![Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![2, 3],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<div key="a"></div>
<span>
<strong key="123" on_remove_element=||{}></strong>
</span>
<em key="b"></em>
</div>
},
new: html! {
<div>
<div key="a"></div>
<em key="b"></em>
</div>
},
expected: vec![
Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![2],
},
Patch::SpecialAttribute(PatchSpecialAttribute::CallOnRemoveElem(
4,
&node_with_on_remove_element("strong", "123"),
)),
],
}
.test();
}
#[test]
fn append_new_element_to_keyed_list() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span></span>
</div>
},
expected: vec![Patch::AppendChildren {
parent_old_node_idx: 0,
new_nodes: vec![&VirtualNode::element("span")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span></span>
<strong></strong>
</div>
},
expected: vec![Patch::AppendChildren {
parent_old_node_idx: 0,
new_nodes: vec![
&VirtualNode::element("span"),
&VirtualNode::element("strong"),
],
}],
}
.test();
}
#[test]
fn insert_new_element_into_middle_of_keyed_list() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<footer key="b"></footer>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span></span>
<footer key="b"></footer>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 2,
new_nodes: vec![&VirtualNode::element("span")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<footer key="b"></footer>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span key="new-key"></span>
<footer key="b"></footer>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 2,
new_nodes: vec![&node_with_key("span", "new-key")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<footer key="b"></footer>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span></span>
<div></div>
<footer key="b"></footer>
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 2,
new_nodes: vec![&VirtualNode::element("span"), &VirtualNode::element("div")],
}],
}
.test();
}
#[test]
fn reorder_keyed_elements() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="b"></span>
</div>
},
new: html! {
<div>
<span key="b"></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="b"></span>
<span key="c"></span>
<span key="d"></span>
<span key="e"></span>
</div>
},
new: html! {
<div>
<span key="e"></span>
<em key="a"></em>
<span key="b"></span>
<span key="c"></span>
<span key="d"></span>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![5],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="BBB"></span>
<span key="c"></span>
<span key="DDD"></span>
<span key="e"></span>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span key="DDD"></span>
<span key="c"></span>
<span key="BBB"></span>
<span key="e"></span>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 2,
to_move: vec![4, 3],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<footer key="b"></footer>
</div>
},
new: html! {
<div>
<footer key="b"></footer>
<span></span>
<div></div>
<em key="a"></em>
</div>
},
expected: vec![
Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
},
Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![&VirtualNode::element("span"), &VirtualNode::element("div")],
},
],
}
.test();
}
#[test]
fn swap_keyed_and_unkeyed_elements() {
DiffTestCase {
old: html! {
<div>
<em></em>
<span key="b"></span>
</div>
},
new: html! {
<div>
<span key="b"></span>
<em></em>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span></span>
</div>
},
new: html! {
<div>
<span></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span class="hello"></span>
</div>
},
new: html! {
<div>
<span></span>
<em key="a"></em>
</div>
},
expected: vec![
Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
},
Patch::RemoveAttributes(2, vec!["class"]),
],
}
.test();
}
#[test]
fn move_before_and_truncate_same_node() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="b">
<img />
</span>
</div>
},
new: html! {
<div>
<span key="b">
</span>
<em key="a"></em>
</div>
},
expected: vec![
Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
},
Patch::RemoveChildren {
parent_old_node_idx: 2,
to_remove: vec![3],
},
],
}
.test();
}
#[test]
fn move_and_insert_before() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<footer key="b"></footer>
<span key="c"></span>
<span key="d"></span>
<span key="e"></span>
</div>
},
new: html! {
<div>
<em key="a"></em>
<span key="c"></span>
<header></header>
<span key="d"></span>
<strong></strong>
<span key="e"></span>
<header></header>
<img />
<footer key="b"></footer>
</div>
},
expected: vec![
Patch::InsertBefore {
anchor_old_node_idx: 4,
new_nodes: vec![&VirtualNode::element("header")],
},
Patch::InsertBefore {
anchor_old_node_idx: 5,
new_nodes: vec![&VirtualNode::element("strong")],
},
Patch::AppendChildren {
parent_old_node_idx: 0,
new_nodes: vec![
&VirtualNode::element("header"),
&VirtualNode::element("img"),
],
},
Patch::MoveToEndOfSiblings {
parent_old_node_idx: 0,
siblings_to_move: vec![2],
},
],
}
.test();
}
#[test]
fn move_two_keyed_elements_before_another() {
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="b"></span>
<span key="c"></span>
</div>
},
new: html! {
<div>
<span key="c"></span>
<span key="b"></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![3, 2],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
<span key="b"></span>
<span key="c"></span>
</div>
},
new: html! {
<div>
<span key="b"></span>
<span key="c"></span>
<em key="a"></em>
</div>
},
expected: vec![Patch::MoveToEndOfSiblings {
parent_old_node_idx: 0,
siblings_to_move: vec![1],
}],
}
.test();
}
#[test]
fn same_tag_same_key() {
DiffTestCase {
old: html! {
<div>
<span key="a"></span>
</div>
},
new: html! {
<div>
<span key="a"></span>
</div>
},
expected: vec![],
}
.test();
DiffTestCase {
old: html! {
<div>
<span key="a"></span>
<em key="b"></em>
</div>
},
new: html! {
<div>
<span key="a"></span>
<em key="b"></em>
</div>
},
expected: vec![],
}
.test();
}
#[test]
fn same_tag_different_key() {
DiffTestCase {
old: html! {
<div>
<span key="a"></span>
</div>
},
new: html! {
<div>
<span key="b"></span>
</div>
},
expected: vec![],
}
.test();
DiffTestCase {
old: html! {
<div>
<span key="a" id="111"></span>
</div>
},
new: html! {
<div>
<span key="b" id="222"></span>
</div>
},
expected: vec![Patch::AddAttributes(
1,
vec![("id", &"222".into())].into_iter().collect(),
)],
}
.test();
}
#[test]
fn same_key_different_tag() {
let mut new = VirtualNode::element("span");
new.as_velement_mut()
.unwrap()
.attrs
.insert("key".to_string(), AttributeValue::String("a".to_string()));
DiffTestCase {
old: html! {
<div>
<em key="a"></em>
</div>
},
new: html! {
<div>
<span key="a"></span>
</div>
},
expected: vec![Patch::Replace {
old_idx: 1,
new_node: &new,
}],
}
.test();
}
#[test]
fn preserve_focusable_elements() {
DiffTestCase {
old: html! {
<div>
<input />
</div>
},
new: html! {
<div>
<br />
<input />
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![&VirtualNode::element("br")],
}],
}
.test();
DiffTestCase {
old: html! {
<div>
<textarea />
</div>
},
new: html! {
<div>
<br />
<textarea />
</div>
},
expected: vec![Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![&VirtualNode::element("br")],
}],
}
.test();
let combination = ("input", "textarea");
for (first, second) in [
(combination.0, combination.1),
(combination.1, combination.0),
] {
let old_a = VirtualNode::element(first);
let new_a = VirtualNode::element(first);
let old_b = VirtualNode::element(second);
let new_b = VirtualNode::element(second);
DiffTestCase {
old: html! {
<div>
{ old_a }
{ old_b }
</div>
},
new: html! {
<div>
<br />
{ new_b }
{ new_a }
</div>
},
expected: vec![
Patch::InsertBefore {
anchor_old_node_idx: 1,
new_nodes: vec![&VirtualNode::element("br")],
},
Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
},
],
}
.test();
}
DiffTestCase {
old: html! {
<div>
<input />
<textarea />
</div>
},
new: html! {
<div>
<textarea />
<input />
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
}
#[test]
fn implicit_keys() {
DiffTestCase {
old: html! {
<div>
<div>
<input />
</div>
<br />
</div>
},
new: html! {
<div>
<br />
<div>
<input />
</div>
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
}
#[test]
fn removed_keyed_node_between_unkeyed_nodes_all_same_tag() {
DiffTestCase {
old: html! {
<div>
<div>
<img />
</div>
<div key="2"></div>
<div>
<br />
</div>
</div>
},
new: html! {
<div>
<div>
<img />
</div>
<div>
<br />
</div>
</div>
},
expected: vec![Patch::RemoveChildren {
parent_old_node_idx: 0,
to_remove: vec![2],
}],
}
.test();
}
#[test]
fn prioritizes_explicit_key_over_implicit_key() {
DiffTestCase {
old: html! {
<div>
<input key="keyed" />
<input />
</div>
},
new: html! {
<div>
<input />
<input key="keyed" />
</div>
},
expected: vec![Patch::MoveNodesBefore {
anchor_old_node_idx: 1,
to_move: vec![2],
}],
}
.test();
}
fn node_with_key(tag: &'static str, key: &'static str) -> VirtualNode {
let mut node = VirtualNode::element(tag);
node.as_velement_mut()
.unwrap()
.attrs
.insert("key".to_string(), key.into());
node
}
fn node_with_on_remove_element(tag: &'static str, key: &'static str) -> VirtualNode {
let mut node = node_with_key(tag, key);
set_on_remove_elem_with_unique_id(&mut node, key);
node
}
fn set_on_create_elem_with_unique_id(node: &mut VirtualNode, on_create_elem_id: &'static str) {
node.as_velement_mut()
.unwrap()
.special_attributes
.set_on_create_element(on_create_elem_id, |_: web_sys::Element| {});
}
fn set_on_remove_elem_with_unique_id(node: &mut VirtualNode, on_remove_elem_id: &'static str) {
node.as_velement_mut()
.unwrap()
.special_attributes
.set_on_remove_element(on_remove_elem_id, |_: web_sys::Element| {});
}
fn set_dangerous_inner_html(node: &mut VirtualNode, html: &str) {
node.as_velement_mut()
.unwrap()
.special_attributes
.dangerous_inner_html = Some(html.to_string());
}
fn on_remove_node_with_on_remove_child() -> VirtualNode {
let mut child = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut child, "555");
let mut node = VirtualNode::element("div");
set_on_remove_elem_with_unique_id(&mut node, "666");
node.as_velement_mut().unwrap().children.push(child);
node
}
fn mock_event_handler() -> EventHandler {
let closure = EventAttribFn::new_noop();
EventHandler::UnsupportedSignature(closure)
}
fn onclick_name() -> EventName {
"onclick".into()
}
fn oninput_name() -> EventName {
"oninput".into()
}
fn onmousemove_name() -> EventName {
"onmousemove".into()
}
}