Text: tests for the layer-level wrap style, the Type settings control, old files and inspect (#2094 review)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 committed 2026-11-07 23:47:15 -05:00
1 parent 071cc86bdf
commit d14c637de8
3 files changed
+162 -1

No files matched your search

+43
View File
@@ -1168,6 +1168,49 @@ mod case_tests {
assert!(t.paragraphs.is_empty());
assert_eq!((t.para_wrap_style(0), t.para_wrap_style(1)), (Pretty, Pretty));
}
#[test]
fn wrap_style_loads_from_old_paragraph_styles_and_texts() {
use WrapStyle::Auto;
// A paragraph style saved before it carried a wrap style.
let old = r#"{"paragraph_spacing":4.0,"list":"bulleted"}"#;
let ps: ParagraphStyle = serde_json::from_str(old).unwrap();
assert_eq!((ps.wrap_style, ps.paragraph_spacing, ps.list), (Auto, 4.0, ListStyle::Bulleted));
assert!(!serde_json::to_string(&ParagraphStyle::default()).unwrap().contains("wrap_style"), "Auto is not saved");
// A text saved before its paragraphs carried a wrap style.
let mut t = TextData::new("a\nb");
t.set_para_lists(0, 0, |p| p.list = ListStyle::Bulleted);
let mut v = serde_json::to_value(&t).unwrap();
for p in v["paragraphs"].as_array_mut().unwrap() {
assert!(p.as_object_mut().unwrap().remove("wrap").is_none(), "Auto is not saved");
}
v.as_object_mut().unwrap().remove("wrap_style");
let old: TextData = serde_json::from_value(v).unwrap();
assert_eq!(old.wrap_style, Auto);
assert_eq!(old.paragraphs.len(), 2);
assert!(old.paragraphs.iter().all(|p| p.wrap.is_none()));
assert_eq!((old.para_wrap_style(0), old.para_wrap_style(1)), (Auto, Auto));
assert_eq!(old, t);
}
#[test]
fn set_wrap_style_clears_an_override_on_the_empty_last_paragraph() {
use WrapStyle::{Auto, Balance, Pretty};
let mut t = TextData::new("ab\n");
assert_eq!(t.paragraph_count(), 2);
// The empty paragraph after the final newline: a caret there reaches it.
t.set_para_wrap_styles(3, 3, Balance);
assert_eq!((t.para_wrap_style(0), t.para_wrap_style(1)), (Auto, Balance));
t.set_wrap_style(Pretty);
assert!(t.paragraphs.is_empty(), "{:?}", t.paragraphs);
assert_eq!((t.para_wrap_style(0), t.para_wrap_style(1)), (Pretty, Pretty));
// Back to the layer's own style: no override is left on either paragraph.
t.set_para_wrap_styles(3, 3, Auto);
assert_eq!(t.para_wrap_style(1), Auto);
t.set_wrap_style(Auto);
assert!(t.paragraphs.is_empty());
assert_eq!(t.common_wrap_style(0, t.char_count()), Some(Auto));
}
}
#[cfg(test)]
+40
View File
@@ -541,6 +541,46 @@ fn wrap_style_per_paragraph_overrides_the_layers() {
assert!(s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "zigzag", "start": 0, "end": 0}})).is_err());
}
#[test]
fn text_wrap_style_without_editing_sets_the_layer_and_folds_paragraph_overrides() {
let mut s = Session::new();
let t = text(&mut s, "one\ntwo\nthree", json!({}));
let inspect = |s: &Session| crate::inspect::node_json(s.doc().unwrap(), t, 0);
let td = |s: &Session| s.doc().unwrap().node(t).unwrap().kind.text().unwrap().clone();
s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "balance", "start": 5, "end": 5}})).unwrap();
s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "pretty", "start": 9, "end": 9}})).unwrap();
assert_eq!(inspect(&s)["lineWrapStyles"], json!(["auto", "balance", "pretty"]));
assert!(s.ds().unwrap().text_edit.is_none(), "no edit session");
s.execute("text.wrapStyle", &json!({"ids": [t], "style": "balance"})).unwrap();
let after = td(&s);
assert!(after.paragraphs.iter().all(|p| p.wrap.is_none()), "{:?}", after.paragraphs);
assert_eq!(after.wrap_style, figcraft_doc::WrapStyle::Balance);
assert!((0..3).all(|p| after.para_wrap_style(p) == figcraft_doc::WrapStyle::Balance));
assert_eq!(inspect(&s)["wrapStyle"], json!("balance"));
assert!(inspect(&s).get("lineWrapStyles").is_none());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(inspect(&s)["lineWrapStyles"], json!(["auto", "balance", "pretty"]), "undo restores the overrides");
}
#[test]
fn inspect_reports_wrap_style_only_when_not_auto_and_counts_a_trailing_empty_paragraph() {
let mut s = Session::new();
let t = text(&mut s, "one\ntwo\n", json!({}));
let inspect = |s: &Session| crate::inspect::node_json(s.doc().unwrap(), t, 0);
let quiet = |s: &Session| inspect(s).get("wrapStyle").is_none() && inspect(s).get("lineWrapStyles").is_none();
assert!(quiet(&s), "uniform Auto says nothing");
// An override on the empty last paragraph (a caret after the final newline) is listed with the others.
s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "balance", "start": 8, "end": 8}})).unwrap();
assert_eq!(inspect(&s)["lineWrapStyles"], json!(["auto", "auto", "balance"]));
assert!(inspect(&s).get("wrapStyle").is_none());
// Every paragraph the same non-Auto style: the one key; back to Auto: neither.
s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "balance"}})).unwrap();
assert_eq!(inspect(&s)["wrapStyle"], json!("balance"));
assert!(inspect(&s).get("lineWrapStyles").is_none());
s.execute("node.set", &json!({"id": t, "props": {"wrapStyle": "auto"}})).unwrap();
assert!(quiet(&s));
}
#[test]
fn max_lines_on_auto_width_text_keeps_auto_width_and_hugs_that_many_lines() {
// "Max lines is available only if truncate text is enabled, and: Text resizing is set to
+79 -1
View File
@@ -210,6 +210,7 @@ pub(crate) fn field_row<R>(ui: &mut Ui, label: &str, add: impl FnOnce(&mut Ui, f
#[cfg(test)]
mod variable_tests {
use egui::{Event, Modifiers, PointerButton, Pos2, RawInput};
use figcraft_doc::WrapStyle;
use figcraft_engine::Session;
use super::*;
@@ -223,6 +224,8 @@ mod variable_tests {
ctx: egui::Context,
time: f64,
text: NodeId,
/// The AccessKit nodes of the last frame.
nodes: Vec<egui::accesskit::Node>,
}
impl Panel {
@@ -236,7 +239,8 @@ mod variable_tests {
s.execute("variable.create", &json!({"collection": "Type", "name": name, "type": ty, "values": {"Base": value}})).unwrap();
}
s.execute("select.set", &json!({ "ids": [text] })).unwrap();
let mut p = Self { app: FigApp::new(s, Box::new(NoServices)), ctx: egui::Context::default(), time: 0.0, text };
let mut p = Self { app: FigApp::new(s, Box::new(NoServices)), ctx: egui::Context::default(), time: 0.0, text, nodes: Vec::new() };
p.ctx.enable_accesskit();
crate::theme::install_fonts(&p.ctx, false, 0);
crate::theme::apply(&p.ctx, Tokens::light());
for _ in 0..3 {
@@ -269,6 +273,9 @@ mod variable_tests {
crate::variable_ui::picker(&mut self.app, ui.ctx());
});
out.textures_delta.clear();
if let Some(update) = out.platform_output.accesskit_update.take() {
self.nodes = update.nodes.into_iter().map(|(_, n)| n).collect();
}
let mut texts = Vec::new();
for c in out.shapes {
walk(&c.shape, &mut texts);
@@ -351,4 +358,75 @@ mod variable_tests {
assert!(shows(&texts, "Inter") && shows(&texts, "Bold"), "the dropdowns show the last values");
assert!(p.app.ui.last_error.is_none(), "{:?}", p.app.ui.last_error);
}
/// The centre of the AccessKit node labelled `label` (a button, say).
fn node_center(p: &Panel, label: &str) -> Pos2 {
let n = p.nodes.iter().find(|n| n.label() == Some(label)).unwrap_or_else(|| panic!("no node {label:?}"));
let r = n.bounds().expect("bounds");
Pos2::new(((r.x0 + r.x1) / 2.0) as f32, ((r.y0 + r.y1) / 2.0) as f32)
}
const WRAP: [&str; 3] = [
"Auto: as many words as fit on each line",
"Balance: every line close to the same length",
"Pretty: full lines, no one word alone on the last",
];
/// The Wrap style buttons that are on, by short name.
fn wrap_on(p: &Panel) -> Vec<&'static str> {
let on = |label: &str| p.nodes.iter().find(|n| n.label() == Some(label)).unwrap_or_else(|| panic!("no node {label:?}")).toggled();
["Auto", "Balance", "Pretty"].into_iter().zip(WRAP).filter(|(_, l)| on(l) == Some(egui::accesskit::Toggled::True)).map(|(n, _)| n).collect()
}
/// The panel with the Type settings popover open, over two paragraphs.
fn settings_panel() -> Panel {
let mut p = Panel::new();
p.app.session.execute("node.set", &json!({"id": p.text, "props": {"characters": "one\ntwo"}})).unwrap();
p.frame(vec![]);
let at = node_center(&p, "Type settings");
p.click(at);
p.frame(vec![]);
p
}
#[test]
fn wrap_style_control_follows_the_caret_paragraph() {
// #1827: with the caret in one paragraph the control shows and sets that paragraph's style.
let mut p = settings_panel();
assert_eq!(wrap_on(&p), ["Auto"], "the layer's style when not editing");
figcraft_engine::textedit::begin(&mut p.app.session, p.text, Some(5), false).unwrap();
p.frame(vec![]);
assert_eq!(wrap_on(&p), ["Auto"]);
let at = node_center(&p, WRAP[1]);
p.click(at);
let td = |p: &Panel| p.app.session.doc().unwrap().node(p.text).unwrap().kind.text().unwrap().clone();
assert_eq!((td(&p).para_wrap_style(0), td(&p).para_wrap_style(1)), (WrapStyle::Auto, WrapStyle::Balance), "only the caret's paragraph");
assert_eq!(td(&p).wrap_style, WrapStyle::Auto);
assert_eq!(wrap_on(&p), ["Balance"], "the control shows the paragraph's value");
figcraft_engine::textedit::set_caret(&mut p.app.session, 1, false).unwrap();
p.frame(vec![]);
assert_eq!(wrap_on(&p), ["Auto"], "another paragraph shows its own");
assert!(p.app.ui.last_error.is_none(), "{:?}", p.app.ui.last_error);
}
#[test]
fn wrap_style_control_has_nothing_active_for_mixed_paragraphs() {
let mut p = settings_panel();
figcraft_engine::textedit::begin(&mut p.app.session, p.text, Some(5), false).unwrap();
p.frame(vec![]);
let at = node_center(&p, WRAP[2]);
p.click(at);
assert_eq!(wrap_on(&p), ["Pretty"]);
// A selection across both paragraphs, which differ: none is on.
figcraft_engine::textedit::set_caret(&mut p.app.session, 1, false).unwrap();
figcraft_engine::textedit::set_caret(&mut p.app.session, 6, true).unwrap();
p.frame(vec![]);
assert!(wrap_on(&p).is_empty(), "{:?}", wrap_on(&p));
// Choosing one sets both paragraphs.
let at = node_center(&p, WRAP[1]);
p.click(at);
assert_eq!(wrap_on(&p), ["Balance"]);
let t = p.app.session.doc().unwrap().node(p.text).unwrap().kind.text().unwrap().clone();
assert_eq!((t.para_wrap_style(0), t.para_wrap_style(1)), (WrapStyle::Balance, WrapStyle::Balance));
}
}