use std::collections::BTreeMap;
use crate::{self as vertigo, dom};
use crate::{
DomNode, DomText, Value,
dev::{
command::DriverDomCommand,
inspect::{log_start, log_take},
},
render::render_list,
};
fn variant(command: &DriverDomCommand) -> &'static str {
match command {
DriverDomCommand::CreateNode { .. } => "CreateNode",
DriverDomCommand::CreateText { .. } => "CreateText",
DriverDomCommand::UpdateText { .. } => "UpdateText",
DriverDomCommand::SetAttr { .. } => "SetAttr",
DriverDomCommand::RemoveAttr { .. } => "RemoveAttr",
DriverDomCommand::RemoveNode { .. } => "RemoveNode",
DriverDomCommand::RemoveText { .. } => "RemoveText",
DriverDomCommand::InsertBefore { .. } => "InsertBefore",
DriverDomCommand::InsertCss { .. } => "InsertCss",
DriverDomCommand::CreateComment { .. } => "CreateComment",
DriverDomCommand::RemoveComment { .. } => "RemoveComment",
DriverDomCommand::CallbackAdd { .. } => "CallbackAdd",
DriverDomCommand::CallbackRemove { .. } => "CallbackRemove",
}
}
fn commands_for(root: impl FnOnce() -> DomNode, update: impl FnOnce()) -> BTreeMap<String, u32> {
let _root = root();
log_start();
update();
let commands = log_take();
let mut counts: BTreeMap<String, u32> = BTreeMap::new();
for command in &commands {
*counts.entry(variant(command).to_string()).or_default() += 1;
}
counts
}
fn counts(pairs: &[(&str, u32)]) -> BTreeMap<String, u32> {
pairs
.iter()
.map(|(name, count)| ((*name).to_string(), *count))
.collect()
}
#[test]
fn interpolated_text_is_patched_in_place() {
let text = Value::new("aaa".to_string());
let emitted = commands_for(
|| {
let text = text.clone();
dom! { <div>{text}</div> }
},
|| text.set("bbb".to_string()),
);
assert_eq!(emitted, counts(&[("UpdateText", 1)]));
}
#[test]
fn computed_text_node_is_patched_in_place() {
let text = Value::new("aaa".to_string());
let emitted = commands_for(
|| {
let node: DomNode = DomText::new_computed(text.clone()).into();
dom! { <div>{node}</div> }
},
|| text.set("bbb".to_string()),
);
assert_eq!(emitted, counts(&[("UpdateText", 1)]));
}
#[test]
fn interpolated_text_mounts_with_its_value_already_set() {
let text = Value::new("hello".to_string());
log_start();
let _root = {
let text = text.clone();
dom! { <div>{text}</div> }
};
let commands = log_take();
let created: Vec<&String> = commands
.iter()
.filter_map(|command| match command {
DriverDomCommand::CreateText { value, .. } => Some(value),
_ => None,
})
.collect();
assert_eq!(created, vec!["hello"], "{commands:?}");
assert!(
!commands
.iter()
.any(|command| matches!(command, DriverDomCommand::UpdateText { .. })),
"mounting must not need a follow-up patch: {commands:?}"
);
}
#[test]
fn attribute_change_is_one_command() {
let class = Value::new("row".to_string());
let emitted = commands_for(
|| {
let class = class.clone();
dom! { <div class={class} /> }
},
|| class.set("row sel".to_string()),
);
assert_eq!(emitted, counts(&[("SetAttr", 1)]));
}
#[test]
fn a_cut_off_write_emits_nothing() {
let caret = Value::new(0u32);
let active_block = caret.map(|offset| offset / 4_096);
let emitted = commands_for(
|| dom! { <div class={active_block.map(|index| index.to_string())} /> },
|| caret.set(37),
);
assert!(
emitted.is_empty(),
"a write absorbed by the equality cutoff must emit no DOM commands, got {emitted:?}"
);
}
fn keys(range: std::ops::Range<u32>) -> Vec<u32> {
range.collect()
}
fn mount_list(order: &Value<Vec<u32>>) -> DomNode {
let list = render_list(
order,
|key| *key,
|key| key.render_value(|key| dom! { <li>{key}</li> }),
);
dom! { <ul>{list}</ul> }
}
#[test]
fn append_cost_does_not_depend_on_list_length() {
let measure = |size: u32| {
let order = Value::new(keys(0..size));
commands_for(|| mount_list(&order), || order.set(keys(0..size + 1)))
};
let small = measure(10);
let large = measure(1_000);
assert_eq!(small, large, "append cost must not scale with list length");
assert_eq!(
small,
counts(&[
("CreateComment", 2),
("CreateNode", 1),
("CreateText", 1),
("InsertBefore", 4),
])
);
}
#[test]
fn removal_cost_does_not_depend_on_position() {
let from_end = {
let order = Value::new(keys(0..100));
commands_for(|| mount_list(&order), || order.set(keys(0..99)))
};
let from_middle = {
let order = Value::new(keys(0..100));
commands_for(
|| mount_list(&order),
|| {
let mut without = keys(0..100);
without.remove(50);
order.set(without);
},
)
};
assert_eq!(from_end, from_middle);
}
#[test]
fn reversing_moves_every_row_but_one_and_rebuilds_none() {
let size = 50u32;
let order = Value::new(keys(0..size));
let emitted = commands_for(
|| mount_list(&order),
|| {
let mut backward = keys(0..size);
backward.reverse();
order.set(backward);
},
);
assert_eq!(
emitted,
counts(&[("InsertBefore", 3 * (size - 1))]),
"reversing must only re-insert - no row may be recreated"
);
}
#[test]
fn swap_cost_does_not_depend_on_the_distance_between_the_rows() {
let measure = |size: u32| {
let order = Value::new(keys(0..size));
commands_for(
|| {
let list = render_list(
&order,
|key| *key,
|key| {
let key = crate::transaction(|ctx| key.get(ctx));
dom! { <li>{key}</li> }
},
);
dom! { <ul>{list}</ul> }
},
|| {
let mut swapped = keys(0..size);
swapped.swap(1, (size - 2) as usize);
order.set(swapped);
},
)
};
let near = measure(100);
let far = measure(1_000);
assert_eq!(near, far, "swap cost must not scale with list length");
assert_eq!(near, counts(&[("InsertBefore", 4)]));
}
#[test]
fn swapping_render_value_rows_moves_two_rows() {
let size = 1_000u32;
let order = Value::new(keys(0..size));
let emitted = commands_for(
|| mount_list(&order),
|| {
let mut swapped = keys(0..size);
swapped.swap(1, (size - 2) as usize);
order.set(swapped);
},
);
assert_eq!(emitted, counts(&[("InsertBefore", 6)]));
}
#[test]
fn reversing_plain_element_rows_moves_two_nodes_each_but_one_row() {
let size = 50u32;
let order = Value::new(keys(0..size));
let emitted = commands_for(
|| {
let list = render_list(
&order,
|key| *key,
|key| {
let key = crate::transaction(|ctx| key.get(ctx));
dom! { <li>{key}</li> }
},
);
dom! { <ul>{list}</ul> }
},
|| {
let mut backward = keys(0..size);
backward.reverse();
order.set(backward);
},
);
assert_eq!(emitted, counts(&[("InsertBefore", 2 * (size - 1))]));
}
#[test]
fn updating_one_row_does_not_disturb_the_list() {
let rows: Vec<Value<String>> = (0..20).map(|i| Value::new(format!("row {i}"))).collect();
let order = Value::new(keys(0..20));
let cells = rows.clone();
let emitted = commands_for(
|| {
let list = render_list(
&order,
|key| *key,
move |key| {
let key = crate::transaction(|ctx| key.get(ctx));
let cell = cells[key as usize].clone();
dom! { <li>{cell}</li> }
},
);
dom! { <ul>{list}</ul> }
},
|| rows[10].set("changed".to_string()),
);
assert_eq!(
emitted,
counts(&[("UpdateText", 1)]),
"one row's update must not touch the other rows"
);
}