Files
figcraft/crates/doc/src/client_id_tests.rs
T
Filip StrajnarandClaude Opus 5.5 652bf35b19 M1: page ruler guides survive save and reopen (#1476)
A page kept its ruler guides in `NodeKind::Page`, and since frame guides
(#70) every layer also has its own `guides` field. Both are written as
`guides`, and on reading the layer's own field takes the key, so:

- a reopened page's guides landed in the layer field and were not drawn;
- saving that page wrote `guides` twice (the page kind's list, always
  written, then the layer's), and the file no longer opened ("duplicate
  field `guides`").

Every layer, pages included, now keeps its guides in the one layer field;
the key in the file is unchanged, so existing files open with their guides.
The field reads every `guides` list a layer has, so files already saved
with the key twice open again with both lists, in file order. This also
covers collab `Create` payloads from older peers.

Regressions: JSON round trips of page and frame guides, a page with two
lists, a bad list still refused (doc); save, reopen, save, reopen through
`.figcraft` and a document.json with the key twice (format). Checked in the
app over the control channel: guides drawn after two save/reopen cycles, and
a file with the key twice opens with all its guides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6Dyzof21BCrUsByhx2w
2026-10-28 22:47:24 +01:00

130 lines
5.3 KiB
Rust

//! Client-scoped ids (collaboration), raw structure edits and page-less documents.
use crate::{DocError, Document, Node, NodeId, NodeKind};
use figcraft_geom::Affine;
fn rect(doc: &mut Document, parent: NodeId) -> NodeId {
doc.create(parent, NodeKind::Rectangle, "R", 0.0, 0.0, 10.0, 10.0).unwrap()
}
const C3: u64 = 3 << 32;
const C5: u64 = 5 << 32;
#[test]
fn single_user_allocation_is_unchanged() {
let mut d = Document::new("a");
let page = d.first_page().unwrap();
assert_eq!(page, NodeId(1));
assert_eq!(d.client_id(), 0);
assert_eq!(rect(&mut d, page), NodeId(2));
assert_eq!(d.next_id(), NodeId(3));
// Inserting any id above the counter moves it, as before.
d.insert(Node::new(NodeId(C5 | 9), NodeKind::Rectangle, "x", 1.0, 1.0), page, None).unwrap();
assert_eq!(d.next_id(), NodeId((C5 | 9) + 1));
}
#[test]
fn clients_allocate_in_their_own_space_and_never_collide() {
let mut base = Document::new("a");
let page = base.first_page().unwrap();
rect(&mut base, page);
let (mut a, mut b) = (base.clone(), base.clone());
a.set_client_id(3);
b.set_client_id(5);
let ia: Vec<NodeId> = (0..3).map(|_| rect(&mut a, page)).collect();
let ib: Vec<NodeId> = (0..3).map(|_| rect(&mut b, page)).collect();
assert_eq!(ia, vec![NodeId(C3 | 1), NodeId(C3 | 2), NodeId(C3 | 3)]);
assert_eq!(ib, vec![NodeId(C5 | 1), NodeId(C5 | 2), NodeId(C5 | 3)]);
// Someone else's ids arriving don't move this client's counter; its own higher ids do.
a.insert(Node::new(NodeId(C5 | 77), NodeKind::Rectangle, "theirs", 1.0, 1.0), page, None).unwrap();
assert_eq!(a.next_id(), NodeId(C3 | 4));
a.insert(Node::new(NodeId(C3 | 40), NodeKind::Rectangle, "mine", 1.0, 1.0), page, None).unwrap();
assert_eq!(a.alloc_id(), NodeId(C3 | 41));
// Old ids (client 0) don't count either.
a.reserve_ids(NodeId(1_000));
assert_eq!(a.next_id(), NodeId(C3 | 42));
}
#[test]
fn setting_a_client_continues_after_its_existing_ids() {
let mut d = Document::new("a");
let page = d.first_page().unwrap();
d.insert(Node::new(NodeId(C3 | 7), NodeKind::Rectangle, "earlier session", 1.0, 1.0), page, None).unwrap();
d.set_client_id(3);
assert_eq!(d.client_id(), 3);
assert_eq!(d.alloc_id(), NodeId(C3 | 8));
// Setting the same client again keeps the counter (handed-out ids stay handed out).
d.set_client_id(3);
assert_eq!(d.alloc_id(), NodeId(C3 | 9));
// Back to single-user: after the highest id of any space.
d.set_client_id(0);
assert_eq!(d.alloc_id(), NodeId((C3 | 7) + 1));
// repair() keeps the client.
d.set_client_id(5);
d.repair();
assert_eq!(d.client_id(), 5);
assert_eq!(d.alloc_id(), NodeId(C5 | 1));
}
#[test]
fn the_counter_is_saved_but_never_compared_and_the_client_never_saved() {
let mut a = Document::new("a");
let mut b = a.clone();
b.set_client_id(9);
b.alloc_id();
assert_eq!(a, b, "same content, different counters");
let json = serde_json::to_value(&b).unwrap();
assert_eq!(json["next_id"], serde_json::json!((9u64 << 32) | 2));
let back: Document = serde_json::from_value(json).unwrap();
assert_eq!(back.client_id(), 0);
assert_eq!(back.next_id(), NodeId((9u64 << 32) | 2));
a.set_client_id(9);
assert_eq!(a.next_id(), NodeId((9u64 << 32) | 1));
}
#[test]
fn place_keeps_the_local_transform_and_refuses_bad_moves() {
let mut d = Document::new("a");
let page = d.first_page().unwrap();
let f = d.create(page, NodeKind::Frame, "F", 100.0, 100.0, 50.0, 50.0).unwrap();
let g = d.create(f, NodeKind::Frame, "G", 10.0, 10.0, 20.0, 20.0).unwrap();
let r = rect(&mut d, page);
d.get_mut(r).unwrap().transform = Affine::translate((5.0, 6.0));
d.place(r, f, 0).unwrap();
assert_eq!(d.node(r).unwrap().transform, Affine::translate((5.0, 6.0)));
assert_eq!(d.node(r).unwrap().parent, Some(f));
assert_eq!(d.node(f).unwrap().children, vec![r, g]);
d.place(r, f, 99).unwrap();
assert_eq!(d.node(f).unwrap().children, vec![g, r], "same parent, index clamped");
assert_eq!(d.place(f, g, 0), Err(DocError::Cycle(f, g)));
assert_eq!(d.place(f, f, 0), Err(DocError::Cycle(f, f)));
assert_eq!(d.place(g, r, 0), Err(DocError::NotContainer(r)));
assert!(d.place(page, f, 0).is_err());
assert!(d.place(NodeId(999), f, 0).is_err());
assert!(d.validate().is_empty());
}
/// A page-less document takes pages inserted with their own ids, refuses a duplicate or a layer
/// that isn't a page, and repairs into a file with a page.
#[test]
fn pages_can_be_inserted_onto_a_page_less_base() {
let mut d = Document::without_pages("snapshot");
assert!(d.pages.is_empty() && d.node_count() == 0);
let p = Node::new(
NodeId(C3 | 1),
NodeKind::Page { background: figcraft_color::Color::DEFAULT_PAGE, flows: vec![], prototype: Default::default() },
"P",
0.0,
0.0,
);
d.insert_page(p.clone()).unwrap();
assert_eq!(d.pages, vec![NodeId(C3 | 1)]);
assert!(d.insert_page(p).is_err(), "duplicate");
assert!(d.insert_page(Node::new(NodeId(4), NodeKind::Frame, "F", 1.0, 1.0)).is_err(), "not a page");
assert!(d.validate().is_empty());
// The page-less base repairs into a file with a page.
let mut e = Document::without_pages("x");
e.repair();
assert_eq!(e.pages.len(), 1);
}