Files
figcraft/crates/engine/src/devmode_behaviour_tests.rs
T

226 lines
10 KiB
Rust

//! Dev Mode behaviour suite, written from the public documentation of handoff tools (in our own
//! words): the canvas inspects and never edits, what the menus and shortcuts may run there, the
//! list of what's ready for development, statuses (ready, changed, completed) and the focus view.
use crate::Session;
use crate::components::tests::{ex, idv};
use crate::devinspect::dev_mode_allows;
use crate::tools::{PointerEvent, PointerKind, ToolId};
use figcraft_doc::{DevStatus, NodeId};
use serde_json::{Value, json};
fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
PointerEvent::at(kind, x, y, 1.0)
}
fn drag(s: &mut Session, from: (f64, f64), to: (f64, f64)) {
s.pointer(&ev(PointerKind::Down, from.0, from.1)).unwrap();
for k in 1..=4 {
let t = f64::from(k) / 4.0;
s.pointer(&ev(PointerKind::Move, from.0 + (to.0 - from.0) * t, from.1 + (to.1 - from.1) * t)).unwrap();
}
s.pointer(&ev(PointerKind::Up, to.0, to.1)).unwrap();
}
fn frame(s: &mut Session, x: f64, name: &str) -> NodeId {
idv(&ex(s, "frame.create", json!({"x": x, "y": 0, "width": 200, "height": 200, "name": name})))
}
fn rect(s: &mut Session, parent: NodeId, x: f64) -> NodeId {
idv(&ex(s, "shape.rectangle", json!({"x": x, "y": 20, "width": 40, "height": 40, "parent": parent})))
}
fn names(v: &Value) -> Vec<String> {
v["items"].as_array().map(|a| a.iter().filter_map(|i| i["name"].as_str().map(String::from)).collect()).unwrap_or_default()
}
fn status(s: &mut Session, ids: &[NodeId], st: &str) {
ex(s, "node.set", json!({"ids": ids, "props": {"devStatus": st}}));
}
#[test]
fn the_dev_mode_canvas_selects_and_pans_but_never_edits() {
let mut s = Session::new();
let f = frame(&mut s, 0.0, "Screen");
let r = rect(&mut s, f, 20.0);
let t = idv(&ex(&mut s, "text.create", json!({"text": "Hello", "x": 100, "y": 120, "parent": f})));
s.tool.dev_mode = true;
let before = s.doc().unwrap().clone();
let steps = s.undo_labels(100).len();
// A press selects; dragging it moves nothing.
drag(&mut s, (40.0, 40.0), (140.0, 90.0));
assert_eq!(s.selection(), vec![r]);
// Nor do its resize handles.
drag(&mut s, (60.0, 60.0), (120.0, 120.0));
// Drawing tools only select there.
for tool in [ToolId::Rectangle, ToolId::Frame, ToolId::Text, ToolId::Pen, ToolId::Pencil, ToolId::Scale] {
s.set_tool(tool);
drag(&mut s, (300.0, 300.0), (360.0, 360.0));
}
s.set_tool(ToolId::Move);
// A double-click on text doesn't start typing; on a frame's layer it selects into it.
ex(&mut s, "select.set", json!({"ids": [t]}));
s.pointer(&ev(PointerKind::DoubleClick, 110.0, 128.0)).unwrap();
assert!(s.ds().unwrap().text_edit.is_none(), "no text editing");
ex(&mut s, "select.set", json!({"ids": [r]}));
s.pointer(&ev(PointerKind::DoubleClick, 40.0, 40.0)).unwrap();
assert!(s.ds().unwrap().vector_edit.is_none(), "no vector editing");
assert!(*s.doc().unwrap() == before, "the design is untouched");
assert_eq!(s.undo_labels(100).len(), steps, "nothing to undo");
// Keys that edit with a tool (a selected guide's Delete) go to the commands instead.
assert!(!s.tool_key("Delete", false, false, false).unwrap());
// The hand still pans; comments still start.
s.set_tool(ToolId::Hand);
let cx = s.ds().unwrap().view.cx;
s.pointer(&ev(PointerKind::Down, 500.0, 500.0)).unwrap();
s.pointer(&PointerEvent { sx: 550.0, ..ev(PointerKind::Move, 500.0, 500.0) }).unwrap();
s.pointer(&ev(PointerKind::Up, 500.0, 500.0)).unwrap();
assert_ne!(s.ds().unwrap().view.cx, cx, "panned");
s.set_tool(ToolId::Comment);
s.pointer(&ev(PointerKind::Down, 500.0, 500.0)).unwrap();
assert!(s.ui_requests.iter().any(|u| u.kind == "comment.new"));
// Back in the design: the same drag moves.
s.tool.dev_mode = false;
s.set_tool(ToolId::Move);
drag(&mut s, (40.0, 40.0), (140.0, 90.0));
assert!(s.doc().unwrap().node(r).is_some_and(|n| n.transform.as_coeffs()[4] > 100.0), "moved");
}
#[test]
fn dev_mode_runs_its_own_edits_and_refuses_design_edits_from_menus_and_shortcuts() {
for id in [
"annotation.add",
"annotation.edit",
"dev.resolveChange",
"dev.focus",
"devResource.add",
"codeConnect.set",
"comment.add",
"edit.undo",
"edit.redo",
"edit.copy",
"edit.copyAsCss",
"select.all",
"view.zoomIn",
"view.devMode",
"file.export",
"file.save",
"tool.move",
"tool.hand",
"tool.comment",
"inspect.code",
] {
assert!(dev_mode_allows(id), "{id} runs in Dev Mode");
}
for id in [
"edit.delete",
"edit.paste",
"edit.cut",
"edit.duplicate",
"transform.nudge",
"object.group",
"tool.rectangle",
"tool.text",
"tool.place",
"file.import",
"file.revert",
"data.fill",
] {
assert!(!dev_mode_allows(id), "{id} edits the design");
}
}
#[test]
fn the_ready_list_shows_frames_and_sections_with_a_status_across_pages() {
let mut s = Session::new();
let a = frame(&mut s, 0.0, "Login");
let b = frame(&mut s, 300.0, "Signup");
let nested = idv(&ex(&mut s, "frame.create", json!({"x": 10, "y": 10, "width": 50, "height": 50, "parent": a, "name": "Card"})));
status(&mut s, &[a], "readyForDev");
status(&mut s, &[b], "completed");
// A layer inside a frame takes no status of its own (its frame's covers it).
assert!(s.execute("node.set", &json!({"id": nested, "props": {"devStatus": "readyForDev"}})).is_err());
assert!(s.execute("node.set", &json!({"ids": [a, nested], "props": {"devStatus": "completed"}})).is_err(), "all or nothing");
assert_eq!(s.doc().unwrap().node(a).map(|n| n.dev_status), Some(DevStatus::ReadyForDev));
// Clearing one is always fine.
ex(&mut s, "node.set", json!({"id": nested, "props": {"devStatus": null}}));
let pg2 = idv(&ex(&mut s, "page.add", json!({"name": "Settings"})));
ex(&mut s, "page.select", json!({"id": pg2}));
let c = frame(&mut s, 0.0, "Profile");
status(&mut s, &[c], "readyForDev");
assert_eq!(names(&ex(&mut s, "inspect.readyForDev", json!({}))), vec!["Login", "Profile"]);
assert_eq!(names(&ex(&mut s, "inspect.readyForDev", json!({"status": "completed"}))), vec!["Signup"]);
assert_eq!(names(&ex(&mut s, "inspect.readyForDev", json!({"status": "any"}))), vec!["Login", "Signup", "Profile"]);
// Statuses are undoable.
s.undo().unwrap();
assert_eq!(names(&ex(&mut s, "inspect.readyForDev", json!({}))), vec!["Login"]);
}
#[test]
fn ready_frames_edited_after_marking_show_as_changed_until_resolved_or_completed() {
let mut s = Session::new();
let a = frame(&mut s, 0.0, "Login");
let b = frame(&mut s, 300.0, "Signup");
let r = rect(&mut s, a, 20.0);
let outside = idv(&ex(&mut s, "shape.rectangle", json!({"x": 900, "y": 0, "width": 10, "height": 10})));
status(&mut s, &[a, b], "readyForDev");
let changed = |s: &mut Session| names(&ex(s, "inspect.readyForDev", json!({"status": "changed"})));
assert!(changed(&mut s).is_empty(), "marking isn't a change");
// Edits elsewhere don't count; an edit inside does.
ex(&mut s, "node.set", json!({"id": outside, "props": {"width": 30}}));
assert!(changed(&mut s).is_empty());
ex(&mut s, "node.set", json!({"id": r, "props": {"width": 60}}));
assert_eq!(changed(&mut s), vec!["Login"]);
let items = ex(&mut s, "inspect.readyForDev", json!({}));
assert_eq!(items["items"][0]["changed"], true, "the list says which changed: {items}");
assert_eq!(items["items"][1]["changed"], false);
// Still ready for dev (the status itself stays).
assert_eq!(names(&items), vec!["Login", "Signup"]);
// Resolving goes back to plain ready; so does marking it again, or completing it.
ex(&mut s, "dev.resolveChange", json!({"ids": [a]}));
assert!(changed(&mut s).is_empty());
ex(&mut s, "node.set", json!({"id": r, "props": {"width": 70}}));
status(&mut s, &[a], "completed");
assert!(changed(&mut s).is_empty());
let all = ex(&mut s, "inspect.readyForDev", json!({"status": "any"}));
assert_eq!(all["items"][0]["changed"], false, "a completed frame isn't changed: {all}");
// Completed frames don't turn changed when edited.
ex(&mut s, "node.set", json!({"id": r, "props": {"width": 80}}));
assert!(!s.doc().unwrap().node(a).unwrap().dev_changed);
}
#[test]
fn the_focus_view_steps_through_what_is_ready_and_skips_completed_frames() {
let mut s = Session::new();
let a = frame(&mut s, 0.0, "Login");
let b = frame(&mut s, 300.0, "Signup");
let c = frame(&mut s, 600.0, "Profile");
let r = rect(&mut s, c, 620.0);
status(&mut s, &[a, c], "readyForDev");
status(&mut s, &[b], "completed");
ex(&mut s, "node.set", json!({"id": r, "props": {"width": 60}}));
let step = |s: &mut Session, d: &str| ex(s, "dev.focus", json!({"direction": d}));
let v = ex(&mut s, "dev.focus", json!({"id": a}));
assert_eq!((v["index"].clone(), v["count"].clone()), (json!(0), json!(2)));
assert_eq!(step(&mut s, "next")["id"], json!(c), "completed ones are skipped, changed ones aren't");
assert_eq!(step(&mut s, "previous")["id"], json!(a));
// Any top-level frame opens in it, even one that's completed.
assert_eq!(ex(&mut s, "dev.focus", json!({"id": b}))["index"], json!(null));
}
#[test]
fn a_drag_under_way_when_dev_mode_starts_moves_nothing() {
let mut s = Session::new();
let f = frame(&mut s, 0.0, "Screen");
let r = rect(&mut s, f, 20.0);
ex(&mut s, "select.set", json!({"ids": [r]}));
let before = s.doc().unwrap().world_bounds(r);
s.pointer(&ev(PointerKind::Down, 40.0, 40.0)).unwrap();
s.pointer(&ev(PointerKind::Move, 90.0, 40.0)).unwrap();
s.drop_gesture().unwrap();
s.tool.dev_mode = true;
s.pointer(&ev(PointerKind::Move, 120.0, 40.0)).unwrap();
s.pointer(&ev(PointerKind::Up, 120.0, 40.0)).unwrap();
assert_eq!(s.doc().unwrap().world_bounds(r), before, "the drag was dropped, not committed");
}