refactor(ui-egui): move dialogs_refs tests to dialogs_refs/tests.rs

This commit is contained in:
aiongg
2026-10-26 20:54:02 +00:00
parent eae2919ea3
commit aedf03549f
2 changed files with 379 additions and 380 deletions
+1 -380
View File
@@ -1284,383 +1284,4 @@ fn sources_ui(app: &mut WordApp, ui: &mut Ui, selected: &mut Option<usize>) -> N
}
#[cfg(test)]
mod tests {
use serde_json::json;
use wordcraft_doc::{InlineObject, StoryRef};
use wordcraft_engine::Session;
use super::*;
use crate::Services;
fn app(text: &str) -> WordApp {
WordApp::new(Session::new(wordcraft_doc::Document::from_text(text)), Services::default())
}
fn form(a: &WordApp) -> RefsDialog {
match &a.dialog {
Some(Dialog::Refs(d)) => (**d).clone(),
d => panic!("no references dialog: {d:?}"),
}
}
fn instrs(a: &WordApp) -> Vec<String> {
let doc = &a.session.doc;
doc.para_paths(StoryRef::Body)
.iter()
.filter_map(|p| doc.para(StoryRef::Body, p))
.flat_map(|p| p.objects.iter())
.filter_map(|o| match o {
InlineObject::Field { instr, .. } => Some(instr.clone()),
_ => None,
})
.collect()
}
/// #402: Insert Caption opens the dialog; a new label is kept with the interface settings,
/// and Numbering's chapter options become `STYLEREF` and `SEQ … \s` fields.
#[test]
fn caption_dialog_numbers_with_chapters() {
let mut a = app("Results\nA chart");
a.run("select.text", json!({"text": "Results"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "A chart"})).unwrap();
assert_eq!(a.run("references.caption", json!({})).unwrap(), json!({"pending": "caption"}));
let RefsDialog::Caption(mut f) = form(&a) else { panic!() };
assert_eq!((f.label.as_str(), f.labels.len()), ("Figure", 3));
// New Label…, then Numbering… with a chapter number.
f.new_label = "Plate".into();
a.dialog = None;
a.ui.caption_labels.push(f.new_label.clone());
f.label = "Plate".into();
f.format = "ALPHABETIC".into();
f.chapter = true;
f.separator = ".".into();
f.text = "Growth".into();
assert_eq!(f.preview(), "Plate 1.A");
a.run("references.caption", f.params()).unwrap();
assert!(instrs(&a).contains(&"SEQ Plate \\* ALPHABETIC \\s 1".to_string()), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).contains("Plate 1.A: Growth"));
// The label is offered next time and saved with the preferences.
assert!(CaptionForm::read(&a).labels.contains(&"Plate".to_string()));
assert_eq!(a.prefs().caption_labels, vec!["Plate".to_string()]);
}
/// #3120: Insert Caption picks the label used last, and saves it with the preferences.
#[test]
fn caption_dialog_remembers_the_last_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
let mut a = app("A chart\nA photo");
a.ui.caption_labels.push("Diagram".into());
a.run("select.text", json!({"text": "A chart"})).unwrap();
a.run("references.caption", json!({})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_value("Figure").click();
h.run_steps(3);
h.get_by_label("Diagram").click();
h.run_steps(3);
h.get_by_role_and_label(Role::Button, "OK").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "OK closes the dialog");
assert!(h.state().session.doc.plain_text(StoryRef::Body).contains("Diagram 1"));
h.state_mut().run("select.text", json!({"text": "A photo"})).unwrap();
h.state_mut().run("references.caption", json!({})).unwrap();
h.run_steps(4);
h.get_by_value("Diagram");
assert_eq!(h.state().prefs().last_caption_label, "Diagram");
// In a table it is still Table; a deleted label falls back to Figure.
let a = h.state_mut();
a.run("insert.table", json!({"rows": 1, "cols": 2})).unwrap();
assert_eq!(CaptionForm::read(a).label, "Table");
a.run("select.text", json!({"text": "A chart"})).unwrap();
a.ui.caption_labels.clear();
assert_eq!(CaptionForm::read(a).label, "Figure");
}
/// #916: Cross-reference from the ribbon opens a dialog listing the targets of each type
/// (equation captions too) and inserts a `REF` field to the one picked.
#[test]
fn cross_reference_dialog_lists_targets_and_inserts_a_ref_field() {
let mut a = app("Kilns\nE=mc2\nSee ");
a.run("select.text", json!({"text": "Kilns"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "E=mc2"})).unwrap();
a.run("references.caption", json!({"label": "Equation", "text": "energy"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("insert.crossReference", json!({})).unwrap(), json!({"pending": "crossReference"}));
let RefsDialog::CrossRef(f) = form(&a) else { panic!() };
assert_eq!((f.targets.clone(), f.selected), (vec!["Kilns".to_string()], Some(0)));
let mut f = CrossRefForm::read(&a, "equation");
assert_eq!(f.targets, vec!["Equation 1: energy".to_string()]);
f.show = "number".into();
a.dialog = None;
a.run("insert.crossReference", f.params().unwrap()).unwrap();
assert!(instrs(&a).iter().any(|i| i.starts_with("REF _Ref")), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).ends_with("See Equation 1"));
// Nothing to pick: nothing to insert.
assert_eq!(CrossRefForm::read(&a, "table").params(), None);
}
/// #2940: a label made with Insert Caption › New Label is a Reference type of the
/// Cross-reference dialog, whether the document has a caption with it yet or came with one.
#[test]
fn cross_reference_dialog_offers_custom_caption_labels() {
let mut a = app("Rock\nSee ");
let types = |a: &WordApp| ref_types(a).into_iter().map(|(v, _)| v).collect::<Vec<_>>();
assert_eq!(types(&a), ["heading", "bookmark", "equation", "figure", "table"]);
// Made in the Caption dialog but not used yet: listed, with nothing to pick.
a.ui.caption_labels.push("Plate".into());
assert!(types(&a).contains(&"label:Plate".to_string()));
assert_eq!(CrossRefForm::read(&a, "label:Plate").params(), None);
// A document's own captions bring theirs, once, and Figure stays the built-in one.
a.ui.caption_labels.clear();
a.run("select.text", json!({"text": "Rock"})).unwrap();
a.run("references.caption", json!({"label": "Specimen", "text": "A rock"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
a.run("references.caption", json!({"label": "Figure", "text": "Another"})).unwrap();
let t = types(&a);
assert_eq!(t.iter().filter(|v| v.as_str() == "label:Specimen").count(), 1, "{t:?}");
assert!(!t.iter().any(|v| v == "label:Figure"), "{t:?}");
let mut f = CrossRefForm::read(&a, "label:Specimen");
assert_eq!(f.targets, vec!["Specimen 1: A rock".to_string()]);
f.show = "number".into();
a.dialog = None;
a.run("caret.docEnd", json!({})).unwrap();
a.run("insert.crossReference", f.params().unwrap()).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).ends_with("Specimen 1"), "{}", a.session.doc.plain_text(StoryRef::Body));
}
/// #2940: through the window: the dialog open on a custom label lists its caption, and the
/// Insert button puts a reference to it in the text. (The Reference type box and its items
/// are not in the accessibility tree the test harness clicks, so the type is set on the form.)
#[test]
fn cross_reference_dialog_inserts_a_reference_to_a_custom_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
let mut a = app("Rock\nSee ");
a.run("select.text", json!({"text": "Rock"})).unwrap();
a.run("references.caption", json!({"label": "Specimen", "text": "A rock", "format": "ROMAN"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("insert.crossReference", json!({})).unwrap(), json!({"pending": "crossReference"}));
let form = CrossRefForm::read(&a, "label:Specimen");
assert_eq!(form.targets, vec!["Specimen I: A rock".to_string()]);
a.dialog = Some(Dialog::Refs(Box::new(RefsDialog::CrossRef(form))));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_role_and_label(Role::Window, "Cross-reference");
h.get_all_by_role_and_label(Role::Button, "Insert").last().expect("the dialog's Insert button").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "Insert closes the dialog");
let text = h.state().session.doc.plain_text(StoryRef::Body);
assert!(text.ends_with("See Specimen I: A rock"), "{text}");
assert!(instrs(h.state()).iter().any(|i| i.starts_with("REF _Ref")), "{:?}", instrs(h.state()));
}
/// #402: Mark Entry opens with the selection; Insert Index opens with Word's defaults and
/// reads the index back; both write their switches.
#[test]
fn index_and_mark_entry_dialogs_write_switches() {
let mut a = app("Kilns are hot.\nMore text.");
a.run("select.text", json!({"text": "Kilns"})).unwrap();
assert_eq!(a.run("references.markEntry", json!({})).unwrap(), json!({"pending": "markEntry"}));
let RefsDialog::MarkEntry(mut m) = form(&a) else { panic!() };
assert_eq!(m.entry, "Kilns");
m.subentry = "firing".into();
m.italic = true;
a.run("references.markEntry", m.params(false)).unwrap();
assert!(instrs(&a).contains(&"XE \"Kilns:firing\" \\i".to_string()), "{:?}", instrs(&a));
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.index", json!({})).unwrap(), json!({"pending": "index"}));
let RefsDialog::Index(mut f) = form(&a) else { panic!() };
assert_eq!((f.columns, f.kind.as_str(), f.headings), (2, "indented", true));
f.kind = "runIn".into();
f.right_align = true;
a.run("references.index", f.params()).unwrap();
assert!(instrs(&a).contains(&"INDEX \\h \"A\" \\c \"2\" \\e \"\t\" \\r".to_string()), "{:?}", instrs(&a));
assert_eq!(IndexForm::read(&a), f, "the dialog shows the index's settings");
}
/// #402: Custom Table of Contents and its Options make the TOC switches, and the dialog
/// opens again with them.
#[test]
fn custom_table_of_contents_dialog() {
let mut a = app("Intro\nNote");
a.run("select.text", json!({"text": "Intro"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "Note"})).unwrap();
a.run("styles.apply", json!({"style": "Quote"})).unwrap();
a.run("caret.docStart", json!({})).unwrap();
a.run("ui.dialog", json!({"name": "toc"})).unwrap();
let RefsDialog::Toc(mut f) = form(&a) else { panic!() };
assert_eq!((f.levels, f.hyperlinks, f.right_align, f.heading_styles, f.outline_levels), (3, true, true, true, true));
f.levels = 2;
f.hyperlinks = false;
f.leader = "hyphen".into();
// Options…: the Quote style at level 2.
f.style_levels.iter_mut().find(|(n, _)| n == "Quote").unwrap().1 = 2;
a.run("references.toc", f.params()).unwrap();
assert_eq!(instrs(&a)[0], "TOC \\o \"1-2\" \\z \\u \\t \"Quote,2\"");
let text = a.session.doc.plain_text(StoryRef::Body);
assert!(text.contains("Intro\t1") && text.contains("Note\t1"), "{text}");
assert_eq!(TocForm::read(&a), f);
}
/// #3013: Insert Table of Figures opens its dialog, which offers the document's own caption
/// labels, so a list of tables (or of a custom label) can be built next to a list of figures.
#[test]
fn table_of_figures_dialog_picks_the_caption_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::{NodeT, Queryable};
let mut a = app("Kiln\nPress\nRock\n");
for (text, label) in [("Kiln", "Figure"), ("Press", "Table"), ("Rock", "Specimen")] {
a.run("select.text", json!({"text": text})).unwrap();
a.run("references.caption", json!({"label": label, "text": text})).unwrap();
}
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.tableOfFigures", json!({})).unwrap(), json!({"pending": "tableOfFigures"}));
let RefsDialog::Figures(mut f) = form(&a) else { panic!() };
assert_eq!(f.label, "Figure");
assert_eq!(f.labels, ["Equation", "Figure", "Table", "Specimen"]);
f.label = "Table".into();
f.leader = "underscore".into();
a.dialog = Some(Dialog::Refs(Box::new(RefsDialog::Figures(f))));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_role_and_label(Role::Window, "Table of Figures");
// Without page numbers there is nothing to align: the margin choice is off until they are back.
let margin = |h: &egui_kittest::Harness<'_, WordApp>| {
h.get_by_role_and_label(Role::CheckBox, "Page numbers at the right margin").accesskit_node().is_disabled()
};
assert!(!margin(&h));
h.get_by_role_and_label(Role::CheckBox, "Include page numbers").click();
h.run_steps(2);
assert!(margin(&h), "no page numbers: no right margin choice");
h.get_by_role_and_label(Role::CheckBox, "Include page numbers").click();
h.run_steps(2);
assert!(!margin(&h));
h.get_all_by_role_and_label(Role::Button, "OK").last().expect("the dialog's OK button").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "OK closes the dialog");
let mut a = h.into_state();
assert!(instrs(&a).contains(&r#"TOC \c "Table""#.to_string()), "{:?}", instrs(&a));
let entries: Vec<(String, Option<wordcraft_doc::props::TabLeader>)> = a
.session
.doc
.body
.iter()
.filter_map(|b| b.as_para())
.filter(|p| p.props.style.as_deref() == Some("ListEntry"))
.map(|p| (p.plain_text(), p.props.tabs.as_ref().and_then(|t| t.last()).map(|t| t.leader)))
.collect();
assert_eq!(entries, [("Table 1: Press\t1".to_string(), Some(wordcraft_doc::props::TabLeader::Underscore))]);
a.run("caret.docEnd", json!({})).unwrap();
a.run("ui.dialog", json!({"name": "tableOfFigures"})).unwrap();
let RefsDialog::Figures(mut f) = form(&a) else { panic!() };
f.label = "Specimen".into();
f.include_label = false;
f.right_align = false;
a.run("references.tableOfFigures", f.params()).unwrap();
assert!(instrs(&a).contains(&r#"TOC \a "Specimen" \p " ""#.to_string()), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).contains("Rock 1"));
}
/// #402: Insert Citation without a source opens Create Source; the type picks the fields,
/// "show all" adds the rest, and Edit Source from the Source Manager updates the citation.
#[test]
fn create_and_edit_source() {
let mut a = app("Studios matter ");
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.citation", json!({})).unwrap(), json!({"pending": "createSource"}));
let RefsDialog::Source(mut f) = form(&a) else { panic!() };
let f = &mut *f;
f.source.kind = "website".into();
let shown = f.fields().len();
assert!(f.fields().iter().any(|(k, l)| *k == "url" && *l == "URL"));
f.show_all = true;
assert!(f.fields().len() > shown && f.fields().iter().any(|(k, _)| *k == "doi"));
for (k, v) in [("author", "Rivera, Alex"), ("title", "Kilns Online"), ("year", "2024"), ("url", "https://example.org"), ("doi", "10.1/x")] {
*field_mut(&mut f.source, k).unwrap() = v.into();
}
a.run("references.citation", json!({"source": serde_json::to_value(&f.source).unwrap()})).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).contains("(Rivera, 2024)"));
assert_eq!(a.session.doc.sources[0].doi, "10.1/x");
// Manage Sources › Edit…: a new year shows in the citation.
assert_eq!(a.run("references.sources", json!({})).unwrap(), json!({"pending": "sourceManager"}));
let mut src = a.session.doc.sources[0].clone();
src.year = "2025".into();
a.run("references.sources", json!({"add": src})).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).contains("(Rivera, 2025)"), "{}", a.session.doc.plain_text(StoryRef::Body));
assert_eq!(a.session.doc.sources.len(), 1);
}
/// #2640: the Source Manager's Import… asks the host's file dialog for a Word bibliography
/// file (`b:Sources`, Word's own `Sources.xml`) and adds its sources to the document, shown
/// in the list at once.
#[test]
fn the_source_manager_imports_a_word_sources_file() {
use egui_kittest::kittest::Queryable;
let dir = std::env::temp_dir().join(format!("wc-refs-import-sources-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("Sources.xml");
std::fs::write(
&path,
r#"<b:Sources xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography">
<b:Source><b:Tag>Riv21</b:Tag><b:SourceType>Book</b:SourceType><b:Title>Shared Spaces</b:Title><b:Year>2021</b:Year></b:Source>
<b:Source><b:Tag>Oka19</b:Tag><b:SourceType>Book</b:SourceType><b:Title>Kilns and Clay</b:Title><b:Year>2019</b:Year></b:Source>
</b:Sources>"#,
)
.unwrap();
let mut a = app("Studios matter ");
let asked = std::rc::Rc::new(std::cell::RefCell::new(String::new()));
let (seen, picked) = (asked.clone(), path.to_string_lossy().to_string());
a.services.pick_open = Some(Box::new(move |purpose| {
*seen.borrow_mut() = purpose.to_string();
Some(picked.clone())
}));
assert_eq!(a.run("references.sources", json!({})).unwrap(), json!({"pending": "sourceManager"}));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
for _ in 0..4 {
h.step();
}
h.get_by_label("Import…").click();
for _ in 0..4 {
h.step();
}
assert_eq!(*asked.borrow(), "sources", "the host can offer Word sources files");
assert_eq!(h.state().session.doc.sources.len(), 2);
assert!(h.state().status_msg.as_ref().is_some_and(|(m, _)| m.contains("Imported 2 sources")), "{:?}", h.state().status_msg);
let _ = std::fs::remove_dir_all(&dir);
}
}
mod tests;
+378
View File
@@ -0,0 +1,378 @@
use serde_json::json;
use wordcraft_doc::{InlineObject, StoryRef};
use wordcraft_engine::Session;
use super::*;
use crate::Services;
fn app(text: &str) -> WordApp {
WordApp::new(Session::new(wordcraft_doc::Document::from_text(text)), Services::default())
}
fn form(a: &WordApp) -> RefsDialog {
match &a.dialog {
Some(Dialog::Refs(d)) => (**d).clone(),
d => panic!("no references dialog: {d:?}"),
}
}
fn instrs(a: &WordApp) -> Vec<String> {
let doc = &a.session.doc;
doc.para_paths(StoryRef::Body)
.iter()
.filter_map(|p| doc.para(StoryRef::Body, p))
.flat_map(|p| p.objects.iter())
.filter_map(|o| match o {
InlineObject::Field { instr, .. } => Some(instr.clone()),
_ => None,
})
.collect()
}
/// #402: Insert Caption opens the dialog; a new label is kept with the interface settings,
/// and Numbering's chapter options become `STYLEREF` and `SEQ … \s` fields.
#[test]
fn caption_dialog_numbers_with_chapters() {
let mut a = app("Results\nA chart");
a.run("select.text", json!({"text": "Results"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "A chart"})).unwrap();
assert_eq!(a.run("references.caption", json!({})).unwrap(), json!({"pending": "caption"}));
let RefsDialog::Caption(mut f) = form(&a) else { panic!() };
assert_eq!((f.label.as_str(), f.labels.len()), ("Figure", 3));
// New Label…, then Numbering… with a chapter number.
f.new_label = "Plate".into();
a.dialog = None;
a.ui.caption_labels.push(f.new_label.clone());
f.label = "Plate".into();
f.format = "ALPHABETIC".into();
f.chapter = true;
f.separator = ".".into();
f.text = "Growth".into();
assert_eq!(f.preview(), "Plate 1.A");
a.run("references.caption", f.params()).unwrap();
assert!(instrs(&a).contains(&"SEQ Plate \\* ALPHABETIC \\s 1".to_string()), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).contains("Plate 1.A: Growth"));
// The label is offered next time and saved with the preferences.
assert!(CaptionForm::read(&a).labels.contains(&"Plate".to_string()));
assert_eq!(a.prefs().caption_labels, vec!["Plate".to_string()]);
}
/// #3120: Insert Caption picks the label used last, and saves it with the preferences.
#[test]
fn caption_dialog_remembers_the_last_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
let mut a = app("A chart\nA photo");
a.ui.caption_labels.push("Diagram".into());
a.run("select.text", json!({"text": "A chart"})).unwrap();
a.run("references.caption", json!({})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_value("Figure").click();
h.run_steps(3);
h.get_by_label("Diagram").click();
h.run_steps(3);
h.get_by_role_and_label(Role::Button, "OK").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "OK closes the dialog");
assert!(h.state().session.doc.plain_text(StoryRef::Body).contains("Diagram 1"));
h.state_mut().run("select.text", json!({"text": "A photo"})).unwrap();
h.state_mut().run("references.caption", json!({})).unwrap();
h.run_steps(4);
h.get_by_value("Diagram");
assert_eq!(h.state().prefs().last_caption_label, "Diagram");
// In a table it is still Table; a deleted label falls back to Figure.
let a = h.state_mut();
a.run("insert.table", json!({"rows": 1, "cols": 2})).unwrap();
assert_eq!(CaptionForm::read(a).label, "Table");
a.run("select.text", json!({"text": "A chart"})).unwrap();
a.ui.caption_labels.clear();
assert_eq!(CaptionForm::read(a).label, "Figure");
}
/// #916: Cross-reference from the ribbon opens a dialog listing the targets of each type
/// (equation captions too) and inserts a `REF` field to the one picked.
#[test]
fn cross_reference_dialog_lists_targets_and_inserts_a_ref_field() {
let mut a = app("Kilns\nE=mc2\nSee ");
a.run("select.text", json!({"text": "Kilns"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "E=mc2"})).unwrap();
a.run("references.caption", json!({"label": "Equation", "text": "energy"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("insert.crossReference", json!({})).unwrap(), json!({"pending": "crossReference"}));
let RefsDialog::CrossRef(f) = form(&a) else { panic!() };
assert_eq!((f.targets.clone(), f.selected), (vec!["Kilns".to_string()], Some(0)));
let mut f = CrossRefForm::read(&a, "equation");
assert_eq!(f.targets, vec!["Equation 1: energy".to_string()]);
f.show = "number".into();
a.dialog = None;
a.run("insert.crossReference", f.params().unwrap()).unwrap();
assert!(instrs(&a).iter().any(|i| i.starts_with("REF _Ref")), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).ends_with("See Equation 1"));
// Nothing to pick: nothing to insert.
assert_eq!(CrossRefForm::read(&a, "table").params(), None);
}
/// #2940: a label made with Insert Caption › New Label is a Reference type of the
/// Cross-reference dialog, whether the document has a caption with it yet or came with one.
#[test]
fn cross_reference_dialog_offers_custom_caption_labels() {
let mut a = app("Rock\nSee ");
let types = |a: &WordApp| ref_types(a).into_iter().map(|(v, _)| v).collect::<Vec<_>>();
assert_eq!(types(&a), ["heading", "bookmark", "equation", "figure", "table"]);
// Made in the Caption dialog but not used yet: listed, with nothing to pick.
a.ui.caption_labels.push("Plate".into());
assert!(types(&a).contains(&"label:Plate".to_string()));
assert_eq!(CrossRefForm::read(&a, "label:Plate").params(), None);
// A document's own captions bring theirs, once, and Figure stays the built-in one.
a.ui.caption_labels.clear();
a.run("select.text", json!({"text": "Rock"})).unwrap();
a.run("references.caption", json!({"label": "Specimen", "text": "A rock"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
a.run("references.caption", json!({"label": "Figure", "text": "Another"})).unwrap();
let t = types(&a);
assert_eq!(t.iter().filter(|v| v.as_str() == "label:Specimen").count(), 1, "{t:?}");
assert!(!t.iter().any(|v| v == "label:Figure"), "{t:?}");
let mut f = CrossRefForm::read(&a, "label:Specimen");
assert_eq!(f.targets, vec!["Specimen 1: A rock".to_string()]);
f.show = "number".into();
a.dialog = None;
a.run("caret.docEnd", json!({})).unwrap();
a.run("insert.crossReference", f.params().unwrap()).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).ends_with("Specimen 1"), "{}", a.session.doc.plain_text(StoryRef::Body));
}
/// #2940: through the window: the dialog open on a custom label lists its caption, and the
/// Insert button puts a reference to it in the text. (The Reference type box and its items
/// are not in the accessibility tree the test harness clicks, so the type is set on the form.)
#[test]
fn cross_reference_dialog_inserts_a_reference_to_a_custom_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
let mut a = app("Rock\nSee ");
a.run("select.text", json!({"text": "Rock"})).unwrap();
a.run("references.caption", json!({"label": "Specimen", "text": "A rock", "format": "ROMAN"})).unwrap();
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("insert.crossReference", json!({})).unwrap(), json!({"pending": "crossReference"}));
let form = CrossRefForm::read(&a, "label:Specimen");
assert_eq!(form.targets, vec!["Specimen I: A rock".to_string()]);
a.dialog = Some(Dialog::Refs(Box::new(RefsDialog::CrossRef(form))));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_role_and_label(Role::Window, "Cross-reference");
h.get_all_by_role_and_label(Role::Button, "Insert").last().expect("the dialog's Insert button").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "Insert closes the dialog");
let text = h.state().session.doc.plain_text(StoryRef::Body);
assert!(text.ends_with("See Specimen I: A rock"), "{text}");
assert!(instrs(h.state()).iter().any(|i| i.starts_with("REF _Ref")), "{:?}", instrs(h.state()));
}
/// #402: Mark Entry opens with the selection; Insert Index opens with Word's defaults and
/// reads the index back; both write their switches.
#[test]
fn index_and_mark_entry_dialogs_write_switches() {
let mut a = app("Kilns are hot.\nMore text.");
a.run("select.text", json!({"text": "Kilns"})).unwrap();
assert_eq!(a.run("references.markEntry", json!({})).unwrap(), json!({"pending": "markEntry"}));
let RefsDialog::MarkEntry(mut m) = form(&a) else { panic!() };
assert_eq!(m.entry, "Kilns");
m.subentry = "firing".into();
m.italic = true;
a.run("references.markEntry", m.params(false)).unwrap();
assert!(instrs(&a).contains(&"XE \"Kilns:firing\" \\i".to_string()), "{:?}", instrs(&a));
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.index", json!({})).unwrap(), json!({"pending": "index"}));
let RefsDialog::Index(mut f) = form(&a) else { panic!() };
assert_eq!((f.columns, f.kind.as_str(), f.headings), (2, "indented", true));
f.kind = "runIn".into();
f.right_align = true;
a.run("references.index", f.params()).unwrap();
assert!(instrs(&a).contains(&"INDEX \\h \"A\" \\c \"2\" \\e \"\t\" \\r".to_string()), "{:?}", instrs(&a));
assert_eq!(IndexForm::read(&a), f, "the dialog shows the index's settings");
}
/// #402: Custom Table of Contents and its Options make the TOC switches, and the dialog
/// opens again with them.
#[test]
fn custom_table_of_contents_dialog() {
let mut a = app("Intro\nNote");
a.run("select.text", json!({"text": "Intro"})).unwrap();
a.run("styles.apply", json!({"style": "Heading 1"})).unwrap();
a.run("select.text", json!({"text": "Note"})).unwrap();
a.run("styles.apply", json!({"style": "Quote"})).unwrap();
a.run("caret.docStart", json!({})).unwrap();
a.run("ui.dialog", json!({"name": "toc"})).unwrap();
let RefsDialog::Toc(mut f) = form(&a) else { panic!() };
assert_eq!((f.levels, f.hyperlinks, f.right_align, f.heading_styles, f.outline_levels), (3, true, true, true, true));
f.levels = 2;
f.hyperlinks = false;
f.leader = "hyphen".into();
// Options…: the Quote style at level 2.
f.style_levels.iter_mut().find(|(n, _)| n == "Quote").unwrap().1 = 2;
a.run("references.toc", f.params()).unwrap();
assert_eq!(instrs(&a)[0], "TOC \\o \"1-2\" \\z \\u \\t \"Quote,2\"");
let text = a.session.doc.plain_text(StoryRef::Body);
assert!(text.contains("Intro\t1") && text.contains("Note\t1"), "{text}");
assert_eq!(TocForm::read(&a), f);
}
/// #3013: Insert Table of Figures opens its dialog, which offers the document's own caption
/// labels, so a list of tables (or of a custom label) can be built next to a list of figures.
#[test]
fn table_of_figures_dialog_picks_the_caption_label() {
use egui::accesskit::Role;
use egui_kittest::kittest::{NodeT, Queryable};
let mut a = app("Kiln\nPress\nRock\n");
for (text, label) in [("Kiln", "Figure"), ("Press", "Table"), ("Rock", "Specimen")] {
a.run("select.text", json!({"text": text})).unwrap();
a.run("references.caption", json!({"label": label, "text": text})).unwrap();
}
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.tableOfFigures", json!({})).unwrap(), json!({"pending": "tableOfFigures"}));
let RefsDialog::Figures(mut f) = form(&a) else { panic!() };
assert_eq!(f.label, "Figure");
assert_eq!(f.labels, ["Equation", "Figure", "Table", "Specimen"]);
f.label = "Table".into();
f.leader = "underscore".into();
a.dialog = Some(Dialog::Refs(Box::new(RefsDialog::Figures(f))));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_role_and_label(Role::Window, "Table of Figures");
// Without page numbers there is nothing to align: the margin choice is off until they are back.
let margin = |h: &egui_kittest::Harness<'_, WordApp>| {
h.get_by_role_and_label(Role::CheckBox, "Page numbers at the right margin").accesskit_node().is_disabled()
};
assert!(!margin(&h));
h.get_by_role_and_label(Role::CheckBox, "Include page numbers").click();
h.run_steps(2);
assert!(margin(&h), "no page numbers: no right margin choice");
h.get_by_role_and_label(Role::CheckBox, "Include page numbers").click();
h.run_steps(2);
assert!(!margin(&h));
h.get_all_by_role_and_label(Role::Button, "OK").last().expect("the dialog's OK button").click();
h.run_steps(3);
assert!(h.state().dialog.is_none(), "OK closes the dialog");
let mut a = h.into_state();
assert!(instrs(&a).contains(&r#"TOC \c "Table""#.to_string()), "{:?}", instrs(&a));
let entries: Vec<(String, Option<wordcraft_doc::props::TabLeader>)> = a
.session
.doc
.body
.iter()
.filter_map(|b| b.as_para())
.filter(|p| p.props.style.as_deref() == Some("ListEntry"))
.map(|p| (p.plain_text(), p.props.tabs.as_ref().and_then(|t| t.last()).map(|t| t.leader)))
.collect();
assert_eq!(entries, [("Table 1: Press\t1".to_string(), Some(wordcraft_doc::props::TabLeader::Underscore))]);
a.run("caret.docEnd", json!({})).unwrap();
a.run("ui.dialog", json!({"name": "tableOfFigures"})).unwrap();
let RefsDialog::Figures(mut f) = form(&a) else { panic!() };
f.label = "Specimen".into();
f.include_label = false;
f.right_align = false;
a.run("references.tableOfFigures", f.params()).unwrap();
assert!(instrs(&a).contains(&r#"TOC \a "Specimen" \p " ""#.to_string()), "{:?}", instrs(&a));
assert!(a.session.doc.plain_text(StoryRef::Body).contains("Rock 1"));
}
/// #402: Insert Citation without a source opens Create Source; the type picks the fields,
/// "show all" adds the rest, and Edit Source from the Source Manager updates the citation.
#[test]
fn create_and_edit_source() {
let mut a = app("Studios matter ");
a.run("caret.docEnd", json!({})).unwrap();
assert_eq!(a.run("references.citation", json!({})).unwrap(), json!({"pending": "createSource"}));
let RefsDialog::Source(mut f) = form(&a) else { panic!() };
let f = &mut *f;
f.source.kind = "website".into();
let shown = f.fields().len();
assert!(f.fields().iter().any(|(k, l)| *k == "url" && *l == "URL"));
f.show_all = true;
assert!(f.fields().len() > shown && f.fields().iter().any(|(k, _)| *k == "doi"));
for (k, v) in [("author", "Rivera, Alex"), ("title", "Kilns Online"), ("year", "2024"), ("url", "https://example.org"), ("doi", "10.1/x")] {
*field_mut(&mut f.source, k).unwrap() = v.into();
}
a.run("references.citation", json!({"source": serde_json::to_value(&f.source).unwrap()})).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).contains("(Rivera, 2024)"));
assert_eq!(a.session.doc.sources[0].doi, "10.1/x");
// Manage Sources › Edit…: a new year shows in the citation.
assert_eq!(a.run("references.sources", json!({})).unwrap(), json!({"pending": "sourceManager"}));
let mut src = a.session.doc.sources[0].clone();
src.year = "2025".into();
a.run("references.sources", json!({"add": src})).unwrap();
assert!(a.session.doc.plain_text(StoryRef::Body).contains("(Rivera, 2025)"), "{}", a.session.doc.plain_text(StoryRef::Body));
assert_eq!(a.session.doc.sources.len(), 1);
}
/// #2640: the Source Manager's Import… asks the host's file dialog for a Word bibliography
/// file (`b:Sources`, Word's own `Sources.xml`) and adds its sources to the document, shown
/// in the list at once.
#[test]
fn the_source_manager_imports_a_word_sources_file() {
use egui_kittest::kittest::Queryable;
let dir = std::env::temp_dir().join(format!("wc-refs-import-sources-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("Sources.xml");
std::fs::write(
&path,
r#"<b:Sources xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography">
<b:Source><b:Tag>Riv21</b:Tag><b:SourceType>Book</b:SourceType><b:Title>Shared Spaces</b:Title><b:Year>2021</b:Year></b:Source>
<b:Source><b:Tag>Oka19</b:Tag><b:SourceType>Book</b:SourceType><b:Title>Kilns and Clay</b:Title><b:Year>2019</b:Year></b:Source>
</b:Sources>"#,
)
.unwrap();
let mut a = app("Studios matter ");
let asked = std::rc::Rc::new(std::cell::RefCell::new(String::new()));
let (seen, picked) = (asked.clone(), path.to_string_lossy().to_string());
a.services.pick_open = Some(Box::new(move |purpose| {
*seen.borrow_mut() = purpose.to_string();
Some(picked.clone())
}));
assert_eq!(a.run("references.sources", json!({})).unwrap(), json!({"pending": "sourceManager"}));
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
for _ in 0..4 {
h.step();
}
h.get_by_label("Import…").click();
for _ in 0..4 {
h.step();
}
assert_eq!(*asked.borrow(), "sources", "the host can offer Word sources files");
assert_eq!(h.state().session.doc.sources.len(), 2);
assert!(h.state().status_msg.as_ref().is_some_and(|(m, _)| m.contains("Imported 2 sources")), "{:?}", h.state().status_msg);
let _ = std::fs::remove_dir_all(&dir);
}