Files
vectorcraft/crates/ui-egui/src/dialogs/tests.rs
T

570 lines
28 KiB
Rust
Raw 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.
//! The dialog registry and the shared frame, drawn headlessly.
use serde_json::json;
use vectorcraft_engine::Session;
use super::*;
use crate::theme;
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app
}
/// One headless frame of the dialog layer.
fn frame(app: &mut VectorcraftApp, input: egui::RawInput) {
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let mut out = ctx.run_ui(input, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
}
fn enter() -> egui::RawInput {
let key = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
egui::RawInput { events: vec![key], ..Default::default() }
}
fn open(app: &mut VectorcraftApp, kind: &str, fields: serde_json::Value) {
app.ui.dialog = Some(Dialog::new(kind, fields));
}
fn objects(app: &VectorcraftApp) -> usize {
app.session.doc().unwrap().doc.layers.iter().map(|l| l.children().map_or(0, |c| c.len())).sum()
}
#[test]
fn kinds_map_to_their_dialogs() {
for (kind, want) in [
("newDocument", DialogKind::NewDocument),
("roundedRectangle", DialogKind::Shape),
("lineSegment", DialogKind::Shape),
("rectangularGrid", DialogKind::Shape),
("shear", DialogKind::Transform),
("splitIntoGrid", DialogKind::PathOp),
("artboardOptions", DialogKind::ArtboardOptions),
("command", DialogKind::Command),
("effect", DialogKind::Effect),
(crate::unsaved::KIND, DialogKind::SaveChanges),
("manageWorkspaces", DialogKind::Workspaces),
("findFont", DialogKind::FindFont),
] {
assert_eq!(DialogKind::of(kind), Some(want), "{kind}");
}
assert_eq!(DialogKind::of("nope"), None);
assert!(spec("effect").preview && spec("recolor").preview && !spec("move").preview);
assert!(spec("preferences").window.is_some() && spec("rectangle").window.is_none());
}
#[test]
fn every_dialog_draws() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
let openers: [(&str, serde_json::Value); 8] = [
("file.newDialog", json!({})),
("file.documentSetup", json!({})),
("file.exportForScreens", json!({})),
("ui.recolorDialog", json!({})),
("ui.paramDialog", json!({"command": "object.path.simplify", "label": "Simplify", "params": {"tolerance": 2, "csv": "a\nb"}})),
("effect.dialog", json!({"effect": "distort.roughen"})),
("edit.preferences", json!({})),
("type.findFont", json!({})),
];
for (id, p) in openers {
app.run(id, p).unwrap();
assert!(app.ui.dialog.is_some(), "{id} opened nothing");
frame(&mut app, Default::default());
app.ui.dialog = None;
let _ = app.session.cancel_interaction();
}
for kind in [
"rectangle",
"roundedRectangle",
"ellipse",
"polygon",
"star",
"lineSegment",
"rectangularGrid",
"rotate",
"reflect",
"scale",
"shear",
"artboardOptions",
] {
open_tool_dialog(&mut app, kind, json!({"x": 5, "y": 5, "index": 0}));
assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == kind), "{kind}");
frame(&mut app, Default::default());
}
for (kind, fields) in [
("move", json!({"dx": "0 pt", "dy": "0 pt"})),
("average", json!({"axis": "both"})),
(crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"})),
("newWorkspace", json!({"name": "Mine"})),
("shortcuts", json!({})),
("unregistered", json!({"value": "1"})),
] {
open(&mut app, kind, fields);
frame(&mut app, Default::default());
}
}
#[test]
fn enter_confirms_through_the_dialog_spec() {
let mut app = app();
open_tool_dialog(&mut app, "rectangle", json!({"x": 10, "y": 20}));
frame(&mut app, enter());
assert!(app.ui.dialog.is_none());
assert_eq!(objects(&app), 1);
// An unregistered kind still closes on OK without running anything.
open(&mut app, "unregistered", json!({}));
assert_eq!(confirm(&mut app), Ok(serde_json::Value::Null));
assert!(app.ui.dialog.is_none());
assert_eq!(confirm(&mut app), Err("no dialog open".into()));
}
#[test]
fn confirm_runs_each_dialogs_command() {
let mut app = app();
open(&mut app, "star", json!({"x": 50, "y": 50, "radius1": "40 pt", "radius2": "20 pt", "points": 6}));
confirm(&mut app).unwrap();
open(&mut app, "lineSegment", json!({"x": 0, "y": 0, "length": "30 pt", "angle": 90}));
confirm(&mut app).unwrap();
assert_eq!(objects(&app), 2);
app.run("select.all", json!({})).unwrap();
// Scale with Uniform uses the horizontal value for both axes; Copy keeps the original.
open(&mut app, "scale", json!({"sx": 200, "sy": 50, "uniform": true, "copy": true}));
confirm(&mut app).unwrap();
assert_eq!(objects(&app), 4);
// The parameter dialog closes before its command runs, then runs it with the edited values.
app.run("ui.paramDialog", json!({"command": "view.saved.new", "label": "New View", "params": {"name": "Close-up"}})).unwrap();
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.doc().unwrap().doc.views.last().map(|v| v.name.as_str()), Some("Close-up"));
// New Document creates the document and closes.
let docs = app.session.documents().len();
open(
&mut app,
"newDocument",
json!({"width": "300 pt", "height": "200 pt", "units": "Points", "name": "Card", "artboards": 1, "colorMode": "RGB"}),
);
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.documents().len(), docs + 1);
}
#[test]
fn effect_preview_is_kept_as_one_undo_step() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
let before = app.session.doc().unwrap().doc.clone();
app.run("effect.dialog", json!({"effect": "distort.roughen"})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true));
frame(&mut app, Default::default());
assert!(app.session.in_interaction(), "the preview runs as an interaction");
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
assert_eq!(app.last_effect.as_ref().map(|e| e.0.as_str()), Some("distort.roughen"));
assert_ne!(app.session.doc().unwrap().doc, before);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, before);
}
#[test]
fn raster_filter_dialogs_preview_and_apply() {
for (effect, key, value) in [
("blur.radial", "method", "zoom"),
("blur.smart", "quality", "high"),
("sharpen.unsharpMask", "amount", "120"),
("pixelate.colorHalftone", "channel1", "30"),
("pixelate.crystallize", "cellSize", "20"),
("pixelate.mezzotint", "type", "longLines"),
("pixelate.pointillize", "cellSize", "12"),
("texture.craquelure", "crackDepth", "9"),
("texture.grain", "grainType", "stippled"),
("texture.mosaicTiles", "tileSize", "20"),
("texture.patchwork", "relief", "20"),
("texture.stainedGlass", "borderThickness", "2"),
("texture.texturizer", "lightDirection", "bottomLeft"),
] {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": effect})).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
let v: serde_json::Value = value.parse::<f64>().map_or_else(|_| json!(value), |n| json!(n));
d.fields.insert(key.into(), v.clone());
frame(&mut app, Default::default());
assert!(app.session.in_interaction(), "{effect}: previews");
confirm(&mut app).unwrap();
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
assert_eq!(n.appearance.effects.len(), 1, "{effect}");
assert_eq!(n.appearance.effects[0].params[key], v, "{effect}");
}
// Fields follow the effect's documented order (its dialog's), not the alphabet.
let doc = vectorcraft_effects::effect_info("blur.smart").unwrap().params;
let ranks: Vec<usize> = ["radius", "threshold", "quality"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
assert!(ranks.windows(2).all(|w| w[0] < w[1]), "{ranks:?}");
assert_eq!(super::effect::doc_rank(doc, "nope"), usize::MAX);
let doc = vectorcraft_effects::effect_info("pixelate.colorHalftone").unwrap().params;
let ranks: Vec<usize> = ["maxRadius", "channel1", "channel2", "channel3", "channel4"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[4] < usize::MAX, "{ranks:?}");
// Labels as the dialogs show them.
assert_eq!(super::form::humanize("maxRadius"), "Max. Radius:");
assert_eq!(super::form::humanize("channel3"), "Channel 3:");
assert_eq!(super::form::humanize("cellSize"), "Cell Size:");
assert_eq!(super::form::humanize("lightDirection"), "Light Direction:");
let doc = vectorcraft_effects::effect_info("texture.texturizer").unwrap().params;
let ranks: Vec<usize> = ["texture", "scaling", "relief", "lightDirection", "invert"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[4] < usize::MAX, "{ranks:?}");
}
#[test]
fn effect_dialog_applies_to_its_appearance_item() {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": "distort.roughen", "item": 1})).unwrap();
frame(&mut app, Default::default());
confirm(&mut app).unwrap();
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
assert_eq!(n.appearance.items[1].effects().len(), 1, "the effect goes to the stroke");
assert!(n.appearance.effects.is_empty() && n.appearance.items[0].effects().is_empty());
}
#[test]
fn revolve_dialog_preview_cancel_confirm_and_undo() {
let mut app = app();
app.run("path.create", json!({"d":"M150 40 L180 60 L180 140 L150 160"})).unwrap();
app.run("select.all", json!({})).unwrap();
let before = app.session.doc().unwrap().doc.clone();
app.run("effect.dialog", json!({"effect":"threeD.revolve","item":null})).unwrap();
frame(&mut app, Default::default());
assert!(app.session.in_interaction());
cancel(&mut app);
assert_eq!(app.session.doc().unwrap().doc, before);
app.run("effect.dialog", json!({"effect":"threeD.revolve","item":null})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("angle".into(), json!(180));
frame(&mut app, Default::default());
confirm(&mut app).unwrap();
assert_ne!(app.session.doc().unwrap().doc, before);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, before);
}
#[test]
fn dialogs_are_as_tall_as_their_content() {
// The save prompt, shown after a taller dialog, is as tall as its text and buttons: no empty
// band around the buttons, nothing inherited from the other dialog.
let mut app = app();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let height = |app: &mut VectorcraftApp, kind: &str, fields: serde_json::Value| {
open(app, kind, fields);
for _ in 0..30 {
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
}
ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind)))).unwrap().height()
};
let tall = height(&mut app, "command", json!({"__command": "object.move", "a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8}));
let prompt = height(&mut app, crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"}));
assert!(prompt < 180.0 && prompt < tall, "save prompt {prompt} pt tall (the dialog before it: {tall} pt)");
}
#[test]
fn dialogs_do_not_stretch_to_the_screen() {
// A dialog is as tall as its content: the same on a short and a tall screen.
let openers: &[(&str, serde_json::Value)] = &[
("ui.newSwatch", json!({})),
("ui.newColorGroup", json!({})),
("ui.swatchOptions", json!({"name": "White"})),
("ui.colorPicker", json!({})),
("ui.graphicStyleOptions", json!({})),
("ui.brushOptions", json!({"name": "3 pt. Round"})),
("ui.brushOptions", json!({"name": "Bristle Round"})),
("ui.colorGuideOptions", json!({})),
("ui.colorBalanceDialog", json!({})),
("ui.saturateDialog", json!({})),
("ui.saveSwatchLibrary", json!({})),
("ui.flattenTransparencyDialog", json!({})),
("ui.expandDialog", json!({})),
("ui.spotColors", json!({})),
("ui.paramDialog", json!({"command": "object.path.simplify", "label": "Simplify", "params": {"tolerance": 2}})),
("effect.dialog", json!({"effect": "distort.roughen"})),
("ui.recolorDialog", json!({})),
];
let height = |id: &str, p: &serde_json::Value, screen_h: f32| -> Option<(String, f32)> {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
app.run(id, p.clone()).ok()?;
let kind = app.ui.dialog.as_ref()?.kind.clone();
spec(&kind).window.is_none().then_some(())?;
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, screen_h));
for _ in 0..4 {
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
ctx.run_ui(input, |ui| show(&mut app, ui.ctx())).textures_delta.clear();
}
let h = ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind.as_str()))))?.height();
Some((kind, h))
};
let mut checked = 0;
for (id, p) in openers {
let (Some((kind, short)), Some((_, tall))) = (height(id, p, 1000.0), height(id, p, 2000.0)) else { continue };
assert!((short - tall).abs() < 1.0, "{kind} is {short} pt tall on a 1000 pt screen but {tall} pt on a 2000 pt one");
checked += 1;
}
assert!(checked >= 10, "only {checked} dialogs drew in the shared frame");
}
/// The dialog window of `app`'s open dialog after a few frames in a `w` × 900 window.
fn dialog_rect(app: &mut VectorcraftApp, w: f32) -> egui::Rect {
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, 900.0));
let kind = app.ui.dialog.as_ref().unwrap().kind.clone();
for _ in 0..3 {
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
}
ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind.as_str())))).unwrap()
}
#[test]
fn a_form_dialog_is_as_wide_as_its_fields_not_the_window() {
let mut app = app();
// Clean Up: three check boxes in the generic parameter dialog.
let params = json!({"emptyTextPaths": true, "strayPoints": true, "unpaintedObjects": true});
app.run("ui.paramDialog", json!({"command": "object.path.cleanUp", "label": "Clean Up", "params": params})).unwrap();
let wide = dialog_rect(&mut app, 1600.0);
assert!(wide.width() < 500.0, "Clean Up is {:.0} wide in a 1600-point window", wide.width());
// The same dialog in a narrow window still fits it.
let narrow = dialog_rect(&mut app, 360.0);
assert!(narrow.width() <= 360.0, "{:.0} wide in a 360-point window", narrow.width());
}
#[test]
fn menu_parameter_dialogs_are_compact() {
// #292: Object › Path › Simplify (and its neighbours) spanned the whole window.
for id in ["object.path.simplify", "object.path.offsetPath", "object.move", "object.rotate", "object.path.splitIntoGrid", "path.average"] {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("select.all", json!({})).unwrap();
crate::menus::invoke(&mut app, id, json!({}));
assert!(app.ui.dialog.is_some(), "{id} opened no dialog");
let wide = dialog_rect(&mut app, 1600.0);
eprintln!("{id}: {:.0} × {:.0} in a 1600-point window", wide.width(), wide.height());
assert!(wide.width() < 420.0, "{id} is {:.0} wide in a 1600-point window", wide.width());
}
}
/// A list mixing built-in labels with names translates only the built-in entries: names that
/// happen to be catalog keys ("Black", "Regular") are shown as they are.
#[test]
fn mixed_lists_translate_only_their_built_in_entries() {
use crate::i18n::{Lang, tr};
let zh = Lang::from_code("zh-hant").unwrap();
for key in ["None", "Black", "Regular", "Default"] {
assert_ne!(tr(zh, key), key, "{key} must be a catalog key for this test");
}
let names = ["None", "Black", "Regular", "Default"];
assert_eq!(shown_names(zh, &names, |k| k == 0 || k == 3), [tr(zh, "None"), "Black", "Regular", tr(zh, "Default")]);
assert_eq!(shown_names(zh, &names, |_| false), names);
assert_eq!(shown_names(Lang::EN, &names, |_| true), names);
}
/// Confirmations and plug-in dialogs are headed by the text they are given (callers translate
/// their own templates; a plug-in's name is its own).
#[test]
fn given_headings_are_shown_as_they_are() {
let d = Dialog::new(confirm::KIND, json!({"message": "Delete “Black”?", "detail": "Regular"}));
assert_eq!((spec(&d.kind).heading)(&d), "Delete “Black”?");
let d = Dialog::new(plugin::KIND, json!({"__label": "Black", "__plugin": "x"}));
assert_eq!((spec(&d.kind).heading)(&d), "Black");
}
#[test]
fn a_dialog_opens_with_its_first_field_focused_so_typing_and_enter_apply() {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
};
let enter = || egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
let bounds = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap();
// Object › Transform › Rotate…: type 90 over the angle, press Enter.
crate::menus::invoke(&mut app, "object.rotate", json!({}));
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("90".into())]);
frame(&mut app, vec![enter()]);
assert!(app.ui.dialog.is_none());
let b = bounds(&app);
assert!((b.width() - 20.0).abs() < 1e-6 && (b.height() - 40.0).abs() < 1e-6, "{b:?}");
// Move…: its first field is a distance (with a unit): typing replaces it as well.
let before = bounds(&app);
crate::menus::invoke(&mut app, "object.move", json!({}));
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("15".into())]);
frame(&mut app, vec![enter()]);
assert!(app.ui.dialog.is_none());
let after = bounds(&app);
assert!((after.x0 - before.x0 - 15.0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} → {after:?}");
// Transform Each, then the Transform effect: Scale › Horizontal, a plain number, comes first.
let before = bounds(&app);
crate::menus::invoke(&mut app, "object.transformEach", json!({}));
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("50".into())]);
frame(&mut app, vec![enter()]);
assert!(app.ui.dialog.is_none());
let after = bounds(&app);
assert!((after.width() - before.width() / 2.0).abs() < 1e-6 && (after.height() - before.height()).abs() < 1e-6, "{before:?} → {after:?}");
app.run("effect.dialog", json!({"effect": "distort.transform"})).unwrap();
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("50".into())]);
frame(&mut app, vec![enter()]);
assert!(app.ui.dialog.is_none());
let node = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
assert_eq!(node.appearance.effects.last().map(|e| e.params["scaleH"].clone()), Some(json!(50.0)));
}
/// #1000: a count typed in Object › Create Object Mosaic… reaches the command (a whole number typed
/// in a command's dialog is an integer), so the mosaic has that many columns, not the 10 × 10 grid.
#[test]
fn object_mosaic_makes_the_grid_typed_in_its_dialog() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 60, "height": 40})).unwrap();
app.run("object.rasterize", json!({"ppi": 72})).unwrap();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
};
let entry = crate::menus::menu_entries(&app).into_iter().find(|e| e.label == "Create Object Mosaic…").expect("menu item");
let (cmd, params) = crate::menus::click_target(&entry.label, entry.command.as_deref().unwrap(), &entry.params);
crate::menus::invoke(&mut app, &cmd, params);
// Its first field, Columns, is focused: type 3 over it and press Enter.
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("3".into())]);
let enter = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
frame(&mut app, vec![enter]);
assert!(app.ui.dialog.is_none());
let mut tiles = None;
app.session.doc().unwrap().doc.walk(|n| {
if n.name.as_deref() == Some("Object Mosaic") {
tiles = n.children().map(|c| c.len());
}
});
assert_eq!(tiles, Some(3 * 10));
}
/// #793: Artboard Options lists its fields as Name, Width, Height, not by key.
#[test]
fn artboard_options_lists_name_then_width_and_height() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 612, "height": 792})).unwrap();
open(&mut app, "artboardOptions", json!({"index": 0, "name": "Artboard 1", "width": 612, "height": 792, "x": 0, "y": 0}));
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
let at = |s: &str| text.find(s).unwrap_or_else(|| panic!("{s} in {text}"));
assert!(at("Name") < at("Width") && at("Width") < at("Height"), "{text}");
}
#[test]
fn the_transform_effect_dialog_has_its_controls_previews_them_and_shows_them_again() {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": "distort.transform"})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
for s in [
"Transform",
"Scale",
"Move",
"Rotate",
"Horizontal:",
"Vertical:",
"Angle:",
"Copies:",
"Reflect X",
"Reflect Y",
"Reference Point",
"Random",
"Preview",
] {
assert!(text.lines().any(|l| l == s), "{s} in {text}");
}
assert!(app.session.in_interaction(), "the preview runs");
let effect = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].clone();
// The reference point and Random update the preview.
let d = app.ui.dialog.as_mut().unwrap();
for (k, v) in [("moveH", json!(30)), ("copies", json!(3)), ("reference", json!(8))] {
d.fields.insert(k.into(), v);
}
frame(&mut app, Default::default());
assert_eq!((effect(&app).params["reference"].clone(), effect(&app).params["copies"].clone()), (json!(8), json!(3)));
app.ui.dialog.as_mut().unwrap().fields.insert("random".into(), json!(true));
frame(&mut app, Default::default());
assert_eq!(effect(&app).params["random"], json!(true));
confirm(&mut app).unwrap();
assert!(!app.session.in_interaction());
assert_eq!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects.len(), 1);
// Editing it from the Appearance panel shows the same values.
app.run("effect.dialog", json!({"effect": "distort.transform", "index": 0})).unwrap();
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.f64("reference", 4.0), d.f64("copies", 0.0), d.f64("moveH", 0.0), d.bool("random")), (8.0, 3.0, 30.0, true));
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
assert!(text.lines().any(|l| l == "Reference Point"), "{text}");
}
#[test]
fn artboard_options_position_moves_art_only_with_the_tool_option_on() {
for move_art in [false, true] {
let mut app = app();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 40})).unwrap()["id"].as_u64().unwrap();
app.select_tool("artboard");
app.session.set_tool_option("moveArt", &json!(move_art));
crate::dialogs::artboard_options::open(&mut app).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("x".into(), json!(50));
// Draw the dialog once and confirm through its registry, as the OK button does.
frame(&mut app, Default::default());
let before = app.session.doc().unwrap().doc.clone();
super::confirm(&mut app).unwrap();
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect.x0, 50.0);
assert_eq!(
app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap().x0,
if move_art { 60.0 } else { 10.0 }
);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(*app.session.doc().unwrap().doc, *before);
}
}
/// Zig Zag's dialog has Size and Ridges per segment sliders, and Relative / Absolute and Smooth /
/// Corner as radio buttons, not dropdowns (#1133).
#[test]
fn zig_zag_has_sliders_and_radio_buttons() {
let mut app = app();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
app.run("effect.dialog", json!({"effect": "distort.zigZag"})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
let lines: Vec<&str> = text.lines().map(str::trim).collect();
for want in ["Size:", "Relative", "Absolute", "Ridges per segment:", "Points", "Smooth", "Corner"] {
assert!(lines.contains(&want), "no {want:?} in {lines:?}");
}
assert!(!lines.contains(&"Ridges:"), "{lines:?}");
}