Files
photocraft/crates/engine/tests/rasterize_selected_layers.rs
Valentin KleinandClaude Opus 5.5 706638b284 Tests: bad-layer-id checks use an id that can't exist (#3343) (#3351)
* Tests: bad-layer-id checks use an id that can't exist (#3343)

Layer ids come from one process-wide counter (shared with document and
smart-object ids, and bumped by ensure_ids_above on file loads). A full
`cargo test` run passes ids like 9999, so a layer a test just created can
get the "bad" id, the command succeeds and the assertion fails at random
(seen on CI in type_extra_cmds::tests::vertical_orientation_renders_columns_inside_the_canvas).

Use u64::MAX, which the counter never reaches, for every "no such layer"
sentinel in the engine and ui-egui tests. Test-only change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: use u64::MAX for bad-layer-id sentinels added on main since (#3343)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:59:43 -04:00

146 lines
6.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Layer › Rasterize acts on every selected layer, as in Photoshop ("select the layer or layers
//! you'd like to rasterize"; the Layers panel's right-click item reads "Rasterize Layers"), not
//! only on the one that was clicked. An explicit `layer` param still targets exactly that layer.
use photocraft_doc::{LayerContent, LayerId};
use photocraft_engine::Session;
use serde_json::{Value, json};
fn kind(s: &Session, id: LayerId) -> &'static str {
match &s.active().unwrap().doc.layer(id).unwrap().content {
LayerContent::Text(_) => "type",
LayerContent::Shape(_) => "shape",
LayerContent::Fill(_) => "fill",
LayerContent::Smart(_) => "smart",
LayerContent::Raster(_) => "raster",
_ => "other",
}
}
fn kinds(s: &Session, ids: &[LayerId]) -> Vec<&'static str> {
ids.iter().map(|id| kind(s, *id)).collect()
}
fn id_of(r: Value) -> LayerId {
LayerId(r["layer"].as_u64().unwrap())
}
struct Scene {
s: Session,
text: LayerId,
shape: LayerId,
smart: LayerId,
fill: LayerId,
/// A shape layer that is never selected.
bystander: LayerId,
}
fn scene() -> Scene {
let mut s = Session::new();
s.execute("file.new", json!({"width": 64, "height": 64})).unwrap();
let text = id_of(s.execute("type.create", json!({"text": "Hi", "size": 24, "x": 4, "y": 30})).unwrap());
let shape = id_of(s.execute("shape.create", json!({"kind": "ellipse", "rect": [4, 4, 40, 30]})).unwrap());
s.execute("layer.new.layer", json!({"name": "Source"})).unwrap();
s.execute("paint.stroke", json!({"points": [[20, 20]], "size": 12})).unwrap();
let smart = id_of(s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap());
let fill = id_of(s.execute("layer.newFillLayer.solidColor", json!({"color": "#00ff00"})).unwrap());
let bystander = id_of(s.execute("shape.create", json!({"kind": "rect", "rect": [30, 30, 60, 60]})).unwrap());
let sc = Scene { s, text, shape, smart, fill, bystander };
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart, sc.fill, sc.bystander]), ["type", "shape", "smart", "fill", "shape"]);
sc
}
impl Scene {
/// Select `ids` with the last one active (like ⌘-clicking rows, then right-clicking the last).
fn select(&mut self, ids: &[LayerId]) {
self.s.execute("layer.select", json!({"layer": ids[0].0})).unwrap();
for id in &ids[1..] {
self.s.execute("layer.select", json!({"layer": id.0, "mode": "add"})).unwrap();
}
assert_eq!(self.s.active().unwrap().selected_layers().len(), ids.len());
}
}
#[test]
fn rasterize_layer_converts_every_selected_layer_in_one_step() {
let mut sc = scene();
let all = [sc.text, sc.shape, sc.smart, sc.fill];
sc.select(&all);
let before = sc.s.active().unwrap().selected_layers();
let r = sc.s.execute("layer.rasterize.layer", json!({})).unwrap();
assert_eq!(r["layers"].as_array().unwrap().len(), 4, "{r}");
assert_eq!(kinds(&sc.s, &all), ["raster"; 4]);
assert_eq!(kind(&sc.s, sc.bystander), "shape", "an unselected layer is left alone");
assert_eq!(sc.s.active().unwrap().selected_layers(), before, "the selection survives");
// One history step brings all four back.
assert!(sc.s.undo());
assert_eq!(kinds(&sc.s, &all), ["type", "shape", "smart", "fill"]);
assert!(sc.s.redo());
assert_eq!(kinds(&sc.s, &all), ["raster"; 4]);
}
#[test]
fn a_kind_command_touches_only_the_selected_layers_of_that_kind() {
let mut sc = scene();
sc.select(&[sc.text, sc.shape, sc.smart]);
sc.s.execute("layer.rasterize.type", json!({})).unwrap();
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart]), ["raster", "shape", "smart"]);
// Nothing selected is a type layer any more.
assert!(sc.s.execute("layer.rasterize.type", json!({})).is_err());
sc.s.execute("layer.rasterize.shape", json!({})).unwrap();
assert_eq!(kinds(&sc.s, &[sc.shape, sc.smart, sc.bystander]), ["raster", "smart", "shape"]);
sc.s.execute("layer.rasterize.smartObject", json!({})).unwrap();
assert_eq!(kind(&sc.s, sc.smart), "raster");
assert_eq!(kind(&sc.s, sc.bystander), "shape");
}
#[test]
fn an_explicit_layer_param_stays_a_single_layer() {
// The "Rasterize?" prompt and scripts pass `layer`: the rest of the selection is not touched.
let mut sc = scene();
sc.select(&[sc.text, sc.shape, sc.smart]);
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.shape.0})).unwrap();
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape, sc.smart]), ["type", "raster", "smart"]);
sc.s.execute("layer.rasterize.shape", json!({"layer": sc.bystander.0})).unwrap();
assert_eq!(kind(&sc.s, sc.bystander), "raster");
assert_eq!(kinds(&sc.s, &[sc.text, sc.smart]), ["type", "smart"]);
}
#[test]
fn a_single_selected_layer_behaves_as_before() {
let mut sc = scene();
sc.select(&[sc.text]);
let r = sc.s.execute("layer.rasterize.layer", json!({})).unwrap();
assert_eq!(r["layer"].as_u64(), Some(sc.text.0));
assert_eq!(kinds(&sc.s, &[sc.text, sc.shape]), ["raster", "shape"]);
// A layer with nothing to rasterize reports it, and no history step is added.
let err = sc.s.execute("layer.rasterize.layer", json!({})).unwrap_err().to_string();
assert!(err.contains("nothing to rasterize"), "{err}");
let err = sc.s.execute("layer.rasterize.type", json!({})).unwrap_err().to_string();
assert!(err.contains("not a type layer"), "{err}");
}
#[test]
fn a_selection_with_nothing_to_rasterize_is_an_error_not_a_step() {
let mut sc = scene();
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.text.0})).unwrap();
sc.s.execute("layer.rasterize.layer", json!({"layer": sc.shape.0})).unwrap();
sc.select(&[sc.text, sc.shape]);
let revision = sc.s.active().unwrap().revision;
let err = sc.s.execute("layer.rasterize.layer", json!({})).unwrap_err().to_string();
assert!(err.contains("none of the selected layers"), "{err}");
assert_eq!(sc.s.active().unwrap().revision, revision, "a refused command changes nothing");
}
#[test]
fn bad_targets_fail_gracefully() {
let mut s = Session::new();
for id in ["layer.rasterize.layer", "layer.rasterize.type", "layer.rasterize.shape", "layer.rasterize.fillContent", "layer.rasterize.smartObject"] {
assert!(s.execute(id, json!({})).is_err(), "{id}: no document");
}
let mut sc = scene();
for id in ["layer.rasterize.layer", "layer.rasterize.shape", "layer.rasterize.smartObject"] {
assert!(sc.s.execute(id, json!({"layer": u64::MAX})).is_err(), "{id}: no such layer");
}
}