test fixtures

This commit is contained in:
Joe 2026-02-14 11:04:28 -06:00
parent bf65d8bfa8
commit bbd9c0e0fa
22 changed files with 2107 additions and 0 deletions

View file

@ -50,3 +50,64 @@ impl fmt::Display for RojoRef {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rbx_dom_weak::InstanceBuilder;
// -----------------------------------------------------------------------
// ref_attribute_name tests
// -----------------------------------------------------------------------
#[test]
fn ref_attr_name_primary_part() {
assert_eq!(ref_attribute_name("PrimaryPart"), "Rojo_Ref_PrimaryPart");
}
#[test]
fn ref_attr_name_value() {
assert_eq!(ref_attribute_name("Value"), "Rojo_Ref_Value");
}
#[test]
fn ref_attr_name_part0() {
assert_eq!(ref_attribute_name("Part0"), "Rojo_Ref_Part0");
}
#[test]
fn ref_attr_name_attachment0() {
assert_eq!(ref_attribute_name("Attachment0"), "Rojo_Ref_Attachment0");
}
#[test]
fn ref_attr_name_adornee() {
assert_eq!(ref_attribute_name("Adornee"), "Rojo_Ref_Adornee");
}
// -----------------------------------------------------------------------
// ref_target_path tests
// -----------------------------------------------------------------------
#[test]
fn ref_target_path_root_child() {
let mut dom = WeakDom::new(InstanceBuilder::new("DataModel"));
let child = dom.insert(dom.root_ref(), InstanceBuilder::new("Workspace"));
assert_eq!(ref_target_path(&dom, child), "Workspace");
}
#[test]
fn ref_target_path_deeply_nested() {
let mut dom = WeakDom::new(InstanceBuilder::new("DataModel"));
let a = dom.insert(dom.root_ref(), InstanceBuilder::new("A"));
let b = dom.insert(a, InstanceBuilder::new("B"));
let c = dom.insert(b, InstanceBuilder::new("C"));
assert_eq!(ref_target_path(&dom, c), "A/B/C");
}
#[test]
fn ref_target_path_root_is_empty() {
let dom = WeakDom::new(InstanceBuilder::new("DataModel"));
assert_eq!(ref_target_path(&dom, dom.root_ref()), "");
}
}

View file

@ -104,3 +104,225 @@ fn remove_property() {
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
}
// ---------------------------------------------------------------------------
// Ref property patch application tests
// ---------------------------------------------------------------------------
use rbx_dom_weak::types::{Ref, Variant};
#[test]
fn apply_ref_property_update() {
let mut redactions = RedactionMap::default();
// Build a tree: ROOT > ChildA, ChildB
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![
InstanceSnapshot::new().name("ChildA").class_name("Part"),
InstanceSnapshot::new().name("ChildB").class_name("Model"),
]),
);
intern_tree(&tree, &mut redactions);
let root_id = tree.get_root_id();
let children: Vec<Ref> = tree
.get_instance(root_id)
.unwrap()
.children()
.to_vec();
let child_a_id = children[0];
let child_b_id = children[1];
// Apply a patch that sets a Ref property on ChildB pointing to ChildA
let patch_set = PatchSet {
updated_instances: vec![PatchUpdate {
id: child_b_id,
changed_name: None,
changed_class_name: None,
changed_properties: UstrMap::from_iter([(
ustr("PrimaryPart"),
Some(Variant::Ref(child_a_id)),
)]),
changed_metadata: None,
}],
..Default::default()
};
let applied = apply_patch_set(&mut tree, patch_set);
assert_eq!(applied.updated.len(), 1);
// Verify the Ref property was set
let child_b = tree.get_instance(child_b_id).unwrap();
let pp = child_b.properties().get(&ustr("PrimaryPart"));
assert!(
matches!(pp, Some(Variant::Ref(r)) if *r == child_a_id),
"PrimaryPart should point to ChildA"
);
}
#[test]
fn apply_nil_ref_property_update() {
let mut redactions = RedactionMap::default();
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Model")
.class_name("Model")]),
);
intern_tree(&tree, &mut redactions);
let root_id = tree.get_root_id();
let model_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// First set PrimaryPart to something
{
let mut model = tree.get_instance_mut(model_id).unwrap();
model
.properties_mut()
.insert(ustr("PrimaryPart"), Variant::Ref(root_id));
}
// Now apply a patch that sets PrimaryPart to nil (Ref::none())
let patch_set = PatchSet {
updated_instances: vec![PatchUpdate {
id: model_id,
changed_name: None,
changed_class_name: None,
changed_properties: UstrMap::from_iter([(
ustr("PrimaryPart"),
Some(Variant::Ref(Ref::none())),
)]),
changed_metadata: None,
}],
..Default::default()
};
let applied = apply_patch_set(&mut tree, patch_set);
assert_eq!(applied.updated.len(), 1);
let model = tree.get_instance(model_id).unwrap();
let pp = model.properties().get(&ustr("PrimaryPart"));
assert!(
matches!(pp, Some(Variant::Ref(r)) if r.is_none()),
"PrimaryPart should be nil after applying null Ref"
);
}
#[test]
fn apply_multiple_ref_property_updates() {
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![
InstanceSnapshot::new().name("PartA").class_name("Part"),
InstanceSnapshot::new().name("PartB").class_name("Part"),
InstanceSnapshot::new().name("Model").class_name("Model"),
]),
);
let root_id = tree.get_root_id();
let children: Vec<Ref> = tree
.get_instance(root_id)
.unwrap()
.children()
.to_vec();
let part_a = children[0];
let part_b = children[1];
let model_id = children[2];
// Apply multiple Ref property updates in the same patch
let patch_set = PatchSet {
updated_instances: vec![
PatchUpdate {
id: model_id,
changed_name: None,
changed_class_name: None,
changed_properties: UstrMap::from_iter([(
ustr("PrimaryPart"),
Some(Variant::Ref(part_a)),
)]),
changed_metadata: None,
},
PatchUpdate {
id: model_id,
changed_name: None,
changed_class_name: None,
changed_properties: UstrMap::from_iter([(
ustr("CustomRef"),
Some(Variant::Ref(part_b)),
)]),
changed_metadata: None,
},
],
..Default::default()
};
let applied = apply_patch_set(&mut tree, patch_set);
assert_eq!(applied.updated.len(), 2);
let model = tree.get_instance(model_id).unwrap();
assert!(matches!(
model.properties().get(&ustr("PrimaryPart")),
Some(Variant::Ref(r)) if *r == part_a
));
assert!(matches!(
model.properties().get(&ustr("CustomRef")),
Some(Variant::Ref(r)) if *r == part_b
));
}
#[test]
fn apply_ref_to_nonexistent_instance() {
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Model")
.class_name("Model")]),
);
let root_id = tree.get_root_id();
let model_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Create a Ref to an ID that doesn't exist in the tree
let fake_ref = Ref::new();
let patch_set = PatchSet {
updated_instances: vec![PatchUpdate {
id: model_id,
changed_name: None,
changed_class_name: None,
changed_properties: UstrMap::from_iter([(
ustr("PrimaryPart"),
Some(Variant::Ref(fake_ref)),
)]),
changed_metadata: None,
}],
..Default::default()
};
// Should not crash -- the Ref is set on the tree instance even if the
// target doesn't exist (dangling refs are allowed in WeakDom)
let applied = apply_patch_set(&mut tree, patch_set);
assert_eq!(applied.updated.len(), 1);
}

View file

@ -145,3 +145,361 @@ fn remove_child() {
fn empty_tree() -> RojoTree {
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
}
// ---------------------------------------------------------------------------
// Rojo_Ref_* attribute resolution tests
// ---------------------------------------------------------------------------
use rbx_dom_weak::types::{Attributes, Variant};
/// Helper: build a tree with ROOT > Workspace > Part (to support path resolution).
fn tree_with_workspace_part() -> RojoTree {
let snapshot = InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Workspace")
.class_name("Workspace")
.children(vec![InstanceSnapshot::new()
.name("Part")
.class_name("Part")])]);
RojoTree::new(snapshot)
}
/// Helper: build a tree with ROOT > Workspace > Model > Target + OtherPart.
fn tree_with_model_and_parts() -> RojoTree {
let snapshot = InstanceSnapshot::new()
.name("ROOT")
.class_name("DataModel")
.children(vec![InstanceSnapshot::new()
.name("Workspace")
.class_name("Workspace")
.children(vec![InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.children(vec![
InstanceSnapshot::new()
.name("Target")
.class_name("Part"),
InstanceSnapshot::new()
.name("OtherPart")
.class_name("Part"),
])])]);
RojoTree::new(snapshot)
}
#[test]
fn ref_attr_resolves_primary_part() {
let tree = tree_with_model_and_parts();
// Find the Model instance in the tree
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Create a snapshot of Model with Rojo_Ref_PrimaryPart attribute
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new()
.name("OtherPart")
.class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// The patch should contain an update with PrimaryPart as a Variant::Ref
let has_primary_part_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties.get(&ustr("PrimaryPart")).map_or(false, |v| {
matches!(v, Some(Variant::Ref(r)) if r.is_some())
})
});
assert!(
has_primary_part_ref,
"Rojo_Ref_PrimaryPart should resolve to a Variant::Ref in the patch"
);
}
#[test]
fn ref_attr_resolves_value() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Add an ObjectValue child to Workspace in the tree
let mut tree = tree;
let objval_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new()
.name("MyRef")
.class_name("ObjectValue"),
);
// Create snapshot with Rojo_Ref_Value pointing to Workspace/Part
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_Value".into(),
Variant::String("Workspace/Part".into()),
);
let snapshot = InstanceSnapshot::new()
.name("MyRef")
.class_name("ObjectValue")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, objval_id);
let has_value_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("Value"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()))
});
assert!(
has_value_ref,
"Rojo_Ref_Value should resolve to a Variant::Ref in the patch"
);
}
#[test]
fn ref_attr_multiple_on_same_instance() {
let tree = tree_with_model_and_parts();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Multiple Rojo_Ref_* attributes
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
attrs.insert(
"Rojo_Ref_CustomRef".into(),
Variant::String("Workspace/Model/OtherPart".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new()
.name("OtherPart")
.class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
let update = patch_set
.updated_instances
.iter()
.find(|u| u.id == model_id);
assert!(update.is_some(), "Should have an update for the Model");
let update = update.unwrap();
let has_pp = update
.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()));
let has_custom = update
.changed_properties
.get(&ustr("CustomRef"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_some()));
assert!(has_pp, "PrimaryPart ref should resolve");
assert!(has_custom, "CustomRef ref should resolve");
}
#[test]
fn ref_attr_nonexistent_path_resolves_to_none() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let mut tree = tree;
let model_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new().name("Model").class_name("Model"),
);
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/DoesNotExist".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// Non-existent path should resolve to None (nil Ref)
let has_nil_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if r.is_none()) || v.is_none())
});
assert!(
has_nil_ref || patch_set.updated_instances.is_empty(),
"Non-existent path should not produce a valid Ref"
);
}
#[test]
fn ref_attr_with_regular_attributes_mixed() {
let tree = tree_with_model_and_parts();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let model_id = tree
.get_instance(workspace_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
// Mix of Rojo_Ref_* and regular attributes
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("Workspace/Model/Target".into()),
);
attrs.insert("CustomAttribute".into(), Variant::String("hello".into()));
attrs.insert("NumberAttr".into(), Variant::Float64(42.0));
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]))
.children(vec![
InstanceSnapshot::new().name("Target").class_name("Part"),
InstanceSnapshot::new()
.name("OtherPart")
.class_name("Part"),
]);
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// Should have PrimaryPart as Ref AND the regular attributes should
// remain in the Attributes property (not be extracted as Ref properties).
let has_pp = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(_))))
});
assert!(has_pp, "PrimaryPart should be resolved from Rojo_Ref_*");
}
#[test]
fn ref_attr_empty_path_resolves_to_root() {
let tree = tree_with_workspace_part();
let root_id = tree.get_root_id();
let workspace_id = tree
.get_instance(root_id)
.unwrap()
.children()
.first()
.copied()
.unwrap();
let mut tree = tree;
let model_id = tree.insert_instance(
workspace_id,
InstanceSnapshot::new().name("Model").class_name("Model"),
);
let mut attrs = Attributes::new();
attrs.insert(
"Rojo_Ref_PrimaryPart".into(),
Variant::String("".into()),
);
let snapshot = InstanceSnapshot::new()
.name("Model")
.class_name("Model")
.properties(UstrMap::from_iter([(
ustr("Attributes"),
Variant::Attributes(attrs),
)]));
let patch_set = compute_patch_set(Some(snapshot), &tree, model_id);
// Empty path should resolve to root (RojoTree.get_instance_by_path("") returns root)
let has_ref = patch_set.updated_instances.iter().any(|u| {
u.changed_properties
.get(&ustr("PrimaryPart"))
.map_or(false, |v| matches!(v, Some(Variant::Ref(r)) if *r == root_id))
});
assert!(has_ref, "Empty path should resolve to root instance");
}