Files
vectorcraft/crates/engine/src/tests/document_move.rs
T
ruchirguitarandClaude Opus 5.5 e0645341a4 Document tabs: drag a tab along the strip to reorder the open documents (#5663)
A document tab dragged sideways along the tab strip moves its document to the
place an insertion line marks; the current document stays current. Dragged
above or below the strip, the tab still tears off into a floating window. The
views Window > New Window opened move with their document, and each document
keeps its own zoom, centre and artboard.

The move is the engine command `document.move {from?, to}`, so the control
channel, MCP and recordings reach it, and a replay moves the document before
the steps recorded after it. `sync_views` keeps the views with their documents
by uid when the documents change order.

Behaviour: https://helpx.adobe.com/illustrator/using/customizing-workspace.html

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:02:46 +07:00

134 lines
5.2 KiB
Rust

//! `document.move` (#5663): a document tab dragged along the tab strip moves its document to
//! another place among the open documents, and the current document stays the current one.
use serde_json::{Value, json};
use crate::*;
/// A session with documents A, B, C and D, in that order, D current.
fn four() -> Session {
let mut s = Session::new();
for name in ["A", "B", "C", "D"] {
s.execute("file.new", &json!({"name": name, "created": null})).unwrap();
}
s
}
fn order(s: &Session) -> String {
s.documents().iter().map(DocState::title).collect::<Vec<_>>().join("")
}
fn current(s: &Session) -> String {
s.active().map(DocState::title).unwrap_or_default()
}
fn moved(s: &mut Session, p: Value) -> Value {
s.execute("document.move", &p).unwrap_or_else(|e| panic!("document.move {p}: {e}"))
}
#[test]
fn a_document_moves_to_its_new_tab_and_the_others_shift() {
let mut s = four();
assert_eq!(order(&s), "ABCD");
assert_eq!(moved(&mut s, json!({"from": 0, "to": 2})), json!({"index": 2}));
assert_eq!(order(&s), "BCAD", "forward: the documents between shift left");
assert_eq!(moved(&mut s, json!({"from": 3, "to": 0})), json!({"index": 0}));
assert_eq!(order(&s), "DBCA", "backward: the documents between shift right");
assert_eq!(moved(&mut s, json!({"from": 1, "to": 1})), json!({"index": 1}));
assert_eq!(order(&s), "DBCA", "onto itself: nothing changes");
}
#[test]
fn the_current_document_stays_current_wherever_the_move_puts_it() {
let mut s = four();
assert!(s.set_active(1));
// The current document itself moves.
moved(&mut s, json!({"from": 1, "to": 3}));
assert_eq!((order(&s), current(&s)), ("ACDB".into(), "B".into()));
assert_eq!(s.active_index(), Some(3));
// A document moves past the current one, which shifts by one.
moved(&mut s, json!({"from": 3, "to": 0}));
moved(&mut s, json!({"from": 3, "to": 0}));
assert_eq!((order(&s), current(&s)), ("DBAC".into(), "B".into()));
assert_eq!(s.active_index(), Some(1));
// A move on one side of the current document leaves it where it is.
moved(&mut s, json!({"from": 2, "to": 3}));
assert_eq!((order(&s), current(&s)), ("DBCA".into(), "B".into()));
assert_eq!(s.active_index(), Some(1));
}
#[test]
fn without_from_the_current_document_moves() {
let mut s = four();
assert!(s.set_active(2));
moved(&mut s, json!({"to": 0}));
assert_eq!((order(&s), current(&s)), ("CABD".into(), "C".into()));
}
#[test]
fn a_move_past_the_last_tab_ends_there_and_bad_indices_change_nothing() {
let mut s = four();
assert_eq!(moved(&mut s, json!({"from": 0, "to": 99})), json!({"index": 3}));
assert_eq!(order(&s), "BCDA");
assert_eq!(moved(&mut s, json!({"from": 0, "to": u64::MAX})), json!({"index": 3}));
assert_eq!(order(&s), "CDAB");
for p in [
json!({"from": 4, "to": 0}),
json!({"from": u64::MAX, "to": 0}),
json!({"from": 0}),
json!({"from": -1, "to": 0}),
json!({"from": 0, "to": "1"}),
] {
assert!(s.execute("document.move", &p).is_err(), "{p} is refused");
assert_eq!(order(&s), "CDAB", "{p} changes nothing");
}
assert!(!s.move_document(0, 4));
assert!(!s.move_document(4, 0));
assert_eq!(order(&s), "CDAB");
}
#[test]
fn moving_needs_an_open_document_and_one_document_stays_put() {
let mut s = Session::new();
assert!(s.execute("document.move", &json!({"to": 0})).is_err(), "no document open");
s.execute("file.new", &json!({"name": "A", "created": null})).unwrap();
assert_eq!(moved(&mut s, json!({"to": 5})), json!({"index": 0}));
assert_eq!((order(&s), current(&s)), ("A".into(), "A".into()));
}
#[test]
fn a_move_keeps_the_tool_and_the_documents_edits() {
let mut s = four();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
let resets = s.tool_resets;
let nodes = s.doc().unwrap().doc.node_count();
let history = super::history(&s);
moved(&mut s, json!({"from": 3, "to": 0}));
assert_eq!(super::history(&s), history, "a move is no undo step, and it keeps Redo");
assert_eq!(s.tool_resets, resets, "no document was switched to, so the tool is not reset");
assert_eq!(s.doc().unwrap().doc.node_count(), nodes);
assert!(s.doc().unwrap().is_dirty(), "the document keeps its unsaved edit");
}
/// A recording replays the move before the steps after it, so an `index` they name is the same
/// document as when they were recorded.
#[test]
fn a_journaled_move_replays_in_order() {
let mut s = four();
for (id, p) in [
("document.move", json!({"from": 0, "to": 3})),
("document.activate", json!({"index": 3})),
("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})),
] {
s.execute(id, &p).unwrap();
}
assert_eq!((order(&s), current(&s)), ("BCDA".into(), "A".into()));
let mut r = Session::new();
for (id, p) in &s.journal {
r.execute(id, p).unwrap();
}
assert_eq!((order(&r), current(&r)), ("BCDA".into(), "A".into()));
let counts = |s: &Session| s.documents().iter().map(|d| d.doc.node_count()).collect::<Vec<_>>();
assert_eq!(counts(&r), counts(&s), "the rectangle is in the same document after the replay");
}