Files
ElyOffice/apps/ely-table/src/tests/names.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

365 lines
12 KiB
Rust

use super::*;
/// The active workbook's names, from the kernel.
fn names(app: &gpui::App, book: &Entity<Book>) -> Vec<ely_lok::sheet::Name> {
let book = book.read(app);
let tab = book.active_tab().expect("a tab");
let mut worker = book.core.worker.lock().expect("the worker lock");
let json = worker
.values(&tab.doc, ely_lok::sheet::NAMES)
.expect("names");
ely_lok::sheet::names(&json).expect("the names parse")
}
/// Opens the define dialog in the test window.
fn ask_define(cx: &mut TestAppContext, book: &Entity<Book>) {
cx.update(|app| book.update(app, |book, cx| book.ask_define(cx)));
}
/// The define dialog's range text, once it is open.
fn asked_range(cx: &TestAppContext, book: &Entity<Book>) -> String {
settle(cx, |app| {
matches!(book.read(app).dialog, Some(Dialog::Define { .. }))
});
cx.update(|app| match &book.read(app).dialog {
Some(Dialog::Define { range, .. }) => range.read(app).text().to_string(),
_ => unreachable!("checked above"),
})
}
/// Waits until every queued input of every tab landed.
fn quiet(cx: &TestAppContext, book: &Entity<Book>) {
settle(cx, |app| {
book.read(app)
.core
.tabs
.iter()
.all(|tab| tab.view.read(app).settled())
});
}
#[test]
fn a_name_is_checked_then_defined_on_the_selection() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((_window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
blank(&mut cx, &book);
let sheet = cx.update(|app| sheet_names(app, &book)[0].clone());
update(&mut cx, &book, |book, cx| {
book.run_active("cell.goto", &["B2:C3"], cx)
});
settle(&cx, |app| {
book.read(app)
.active_tab()
.is_some_and(|tab| tab.view.read(app).cell().address == "B2:C3")
});
update(&mut cx, &book, |book, cx| book.ask_define(cx));
settle(&cx, |app| {
matches!(book.read(app).dialog, Some(Dialog::Define { .. }))
});
let define = |cx: &mut TestAppContext, name: &str| {
cx.update(|app| {
book.update(app, |book, cx| {
if let Some(Dialog::Define { name: field, .. }) = &book.dialog {
let name = name.to_string();
field.update(cx, |field, cx| field.set_text(name, cx));
}
book.define(cx);
})
});
};
// Checks answer in order: the cell name first.
define(&mut cx, "C4");
define(&mut cx, "Area");
settle(&cx, |app| book.read(app).dialog.is_none());
settle(&cx, |app| {
book.read(app)
.active_tab()
.is_some_and(|tab| tab.view.read(app).settled())
});
let defined = cx.update(|app| names(app, &book));
assert_eq!(defined.len(), 1);
assert_eq!(
(defined[0].name.as_str(), defined[0].formula.as_str()),
("Area", format!("{sheet}!$B$2:$C$3").as_str()),
"the selection, sheet included"
);
}
#[test]
fn define_waits_for_queued_input_and_reads_named_selections() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((_window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
blank(&mut cx, &book);
let sheet = cx.update(|app| sheet_names(app, &book)[0].clone());
update(&mut cx, &book, |book, cx| {
book.run_active("cell.goto", &["B2:C3"], cx)
});
ask_define(&mut cx, &book);
assert_eq!(
asked_range(&cx, &book),
format!("{sheet}!$B$2:$C$3"),
"the selection after the queued move"
);
update(&mut cx, &book, |book, cx| {
book.dialog = None;
let range = format!("'{sheet}'!$D$4:$E$5");
book.run_active("names.define", &["Area", &range, ""], cx);
book.run_active("cell.goto", &["A1"], cx);
book.run_active("cell.goto", &["Area"], cx);
});
quiet(&cx, &book);
let range = cx.update(|app| {
let names = names(app, &book);
names[0].range.clone()
});
assert!(range.ends_with("!$D$4:$E$5"), "{range}");
ask_define(&mut cx, &book);
assert_eq!(
asked_range(&cx, &book),
range,
"a named selection keeps its range"
);
}
#[test]
fn the_names_list_stays_with_its_workbook_and_scope() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((_window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
for _ in 0..2 {
update(&mut cx, &book, |book, cx| book.new_blank(cx));
}
settle(&cx, |app| {
let tabs = &book.read(app).core.tabs;
tabs.len() == 2 && tabs.iter().all(|tab| !tab.extra.sheets.list.is_empty())
});
let first = cx.update(|app| book.read(app).core.tabs[0].id);
let sheet = cx.update(|app| {
book.read(app).core.tabs[0].extra.sheets.list[0]
.name
.clone()
});
update(&mut cx, &book, |book, cx| {
book.core.active = Some(first);
book.run_active("sheet.insert", &["Other", "2"], cx);
book.run_active(
"names.define",
&["Spot", &format!("'{sheet}'!$A$5"), ""],
cx,
);
book.run_active("names.define", &["Spot", "'Other'!$C$7", "Other"], cx);
book.run_active("names.define", &["Local", "'Other'!$B$2", "Other"], cx);
book.show_sheet(0, cx);
});
quiet(&cx, &book);
cx.update(|app| {
book.update(app, |book, cx| {
book.run_active("cell.enter", &["7"], cx);
book.names_in(first, cx);
})
});
settle(&cx, |app| {
matches!(book.read(app).dialog, Some(Dialog::Names { .. }))
});
update(&mut cx, &book, |book, _| {
book.core.active = Some(book.core.tabs[1].id)
});
let pick = |cx: &mut TestAppContext, name: &str, scope: &str| {
update(cx, &book, |book, cx| {
let Some(Dialog::Names { tab, names, .. }) = &book.dialog else {
panic!("the names list is open");
};
assert_eq!(*tab, first, "the list stays with the asking workbook");
let at = names
.iter()
.position(|known| known.name == name && known.scope == scope)
.expect("the name is listed");
book.choose_name(at, cx);
book.act_on_name(crate::formulas::NameAct::Go, cx);
});
};
let shown = |cx: &mut TestAppContext| {
quiet(cx, &book);
let active = cx.update(|app| book.read(app).core.active);
assert_eq!(active, Some(first), "the jump shows its workbook");
update(cx, &book, |book, cx| book.refill(cx));
let named = cx.update(|app| book.read(app).bars.name.read(app).text().to_string());
let address = cx.update(|app| {
book.read(app).core.tabs[0]
.view
.read(app)
.cell()
.address
.clone()
});
assert_eq!(named, address, "the name box shows the target");
cx.update(|app| {
let tab = &book.read(app).core.tabs[0];
(
tab.extra.sheets.current,
tab.view.read(app).cell().address.clone(),
)
})
};
pick(&mut cx, "Local", "Other");
settle(&cx, |app| {
book.read(app).core.tabs[0].extra.sheets.current == 1
});
let (part, address) = shown(&mut cx);
assert!(
part == 1 && (address == "B2" || address.starts_with("Local")),
"{part} {address}"
);
cx.update(|app| {
book.update(app, |book, cx| {
book.names_in(first, cx);
})
});
settle(&cx, |app| {
matches!(book.read(app).dialog, Some(Dialog::Names { .. }))
});
update(&mut cx, &book, |book, _| {
book.core.active = Some(book.core.tabs[1].id)
});
pick(&mut cx, "Spot", "");
settle(&cx, |app| {
book.read(app).core.tabs[0].extra.sheets.current == 0
});
let (part, address) = shown(&mut cx);
assert!(
part == 0 && (address == "A5" || address == "Spot"),
"{part} {address}"
);
}
#[test]
fn a_waiting_definition_keeps_its_workbook_sheets() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((_window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
for _ in 0..2 {
update(&mut cx, &book, |book, cx| book.new_blank(cx));
}
settle(&cx, |app| {
let tabs = &book.read(app).core.tabs;
tabs.len() == 2 && tabs.iter().all(|tab| !tab.extra.sheets.list.is_empty())
});
let (first, second) = cx.update(|app| {
let tabs = &book.read(app).core.tabs;
(tabs[0].id, tabs[1].id)
});
for (id, name) in [(first, "Origin"), (second, "Destination")] {
update(&mut cx, &book, |book, cx| {
book.core.active = Some(id);
book.run_active("sheet.rename", &[name, "1"], cx);
});
}
settle(&cx, |app| {
book.read(app)
.core
.tabs
.iter()
.zip(["Origin", "Destination"])
.all(|(tab, name)| {
tab.extra
.sheets
.list
.first()
.is_some_and(|sheet| sheet.name == name)
})
});
cx.update(|app| {
book.update(app, |book, cx| {
book.core.active = Some(first);
book.run_active("cell.goto", &["B2:C3"], cx);
book.ask_define(cx);
})
});
settle(&cx, |app| {
matches!(book.read(app).dialog, Some(Dialog::Define { .. }))
});
update(&mut cx, &book, |book, _| book.core.active = Some(second));
let scopes = cx.update(|app| book.read(app).define_scopes());
assert_eq!(
scopes,
Some(vec!["Origin".to_string()]),
"the asking workbook's sheets"
);
update(&mut cx, &book, |book, cx| {
if let Some(Dialog::Define { name, scope, .. }) = &mut book.dialog {
*scope = "Origin".into();
name.update(cx, |name, cx| name.set_text("Area", cx));
}
book.define(cx);
book.core.active = Some(first);
});
settle(&cx, |app| book.read(app).dialog.is_none());
quiet(&cx, &book);
let defined = cx.update(|app| names(app, &book));
assert!(
defined
.iter()
.any(|name| name.name == "Area" && name.scope == "Origin"),
"{defined:?}"
);
}
#[test]
fn a_rename_stays_with_its_workbook() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let Some((window, book)) = book(&mut cx, dir.path()) else {
eprintln!("SKIP: no LOK build");
return;
};
for _ in 0..2 {
update(&mut cx, &book, |book, cx| book.new_blank(cx));
}
settle(&cx, |app| {
let tabs = &book.read(app).core.tabs;
tabs.len() == 2 && tabs.iter().all(|tab| !tab.extra.sheets.list.is_empty())
});
let (first, second) = cx.update(|app| {
let tabs = &book.read(app).core.tabs;
(tabs[0].id, tabs[1].id)
});
let before = cx.update(|app| {
book.read(app).core.tabs[1].extra.sheets.list[0]
.name
.clone()
});
cx.update_window(window.into(), |_, window, app| {
book.update(app, |book, cx| {
book.core.active = Some(first);
book.ask_rename(window, cx);
book.core.active = Some(second);
book.rename_asked("Kept", cx);
})
})
.expect("the window lives");
settle(&cx, |app| {
book.read(app).core.tabs[0].extra.sheets.list[0].name == "Kept"
});
assert_eq!(
cx.update(|app| book.read(app).core.tabs[1].extra.sheets.list[0]
.name
.clone()),
before,
"the other workbook keeps its sheet name"
);
}