use ely_gpui_component::tables::{FilterRule, SortKey, Test}; use ely_lok::{Request, sheet}; use super::*; /// Name, Qty, City; one repeated row. const DATA: &str = r#" NameQtyCity a3x b1y a3x c2y "#; pub(super) fn open(cx: &mut TestAppContext, book: &Entity) { update(cx, book, |book, cx| { book.new_from_text("data.fods", DATA, cx) }); settle(cx, |app| sheet_names(app, book) == ["S"]); } /// Waits for the active tab's queued input. pub(super) fn quiet(cx: &TestAppContext, book: &Entity) { settle(cx, |app| { book.read(app) .active_tab() .is_some_and(|tab| tab.view.read(app).settled()) }); } /// The cells of `range`, read through the worker. pub(super) fn cells(cx: &mut TestAppContext, book: &Entity, range: &str) -> String { quiet(cx, book); cx.update(|app| { let book = book.read(app); let tab = book.active_tab().expect("a tab"); let mut worker = book.core.worker.lock().expect("the worker lock"); worker .run(&tab.doc, "cell.goto", &[range]) .expect("the range"); tab.doc .selection_text(&mut worker) .expect("the text") .trim() .to_string() }) } pub(super) fn run(cx: &mut TestAppContext, book: &Entity, id: &str, values: &[&str]) { update(cx, book, |book, cx| book.run_active(id, values, cx)); quiet(cx, book); } /// The column's entries the filter drops. fn dropped(cx: &mut TestAppContext, book: &Entity, column: u32) -> Vec { quiet(cx, book); cx.update(|app| { 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, &sheet::cell_query(sheet::FILTER, column, 0)) .expect("the entries"); sheet::filter_values(&json) .expect("parses") .entries .into_iter() .filter(|entry| !entry.selected) .map(|entry| entry.text) .collect() }) } pub(super) fn asked(cx: &TestAppContext, book: &Entity, is: fn(&Dialog) -> bool) { settle(cx, |app| book.read(app).dialog.as_ref().is_some_and(is)); } #[test] fn sort_filter_duplicates_and_columns_run_from_their_dialogs() { 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; }; open(&mut cx, &book); run(&mut cx, &book, "cell.goto", &["B3"]); update(&mut cx, &book, |book, cx| { book.ask_data(crate::data::Asked::Sort, cx) }); asked(&cx, &book, |dialog| matches!(dialog, Dialog::Sort { .. })); update(&mut cx, &book, |book, cx| { let Some(Dialog::Sort { picked, keys, header, .. }) = &mut book.dialog else { unreachable!("checked above"); }; assert_eq!(picked.top, ["Name", "Qty", "City"]); assert!(*header, "the kernel sees headers"); assert_eq!(crate::data::title(picked, 1, *header), "Qty"); *keys = vec![SortKey { column: "1".into(), rising: true, }]; book.data_asked(cx); }); assert_eq!(cells(&mut cx, &book, "A2:A5"), "b\nc\na\na"); run(&mut cx, &book, "cell.goto", &["A1"]); update(&mut cx, &book, |book, cx| { book.ask_data(crate::data::Asked::Filter, cx) }); asked(&cx, &book, |dialog| matches!(dialog, Dialog::Filter { .. })); update(&mut cx, &book, |book, cx| { if let Some(Dialog::Filter { rules, .. }) = &mut book.dialog { *rules = vec![FilterRule { column: "2".into(), test: Test::Equals, value: "x".into(), }]; } book.data_asked(cx); }); assert_eq!(dropped(&mut cx, &book, 2), ["y"]); run(&mut cx, &book, "data.unfilter", &[]); run(&mut cx, &book, "cell.goto", &["A2"]); update(&mut cx, &book, |book, cx| { book.ask_data(crate::data::Asked::Dedupe, cx) }); asked(&cx, &book, |dialog| matches!(dialog, Dialog::Dedupe { .. })); update(&mut cx, &book, |book, cx| book.data_asked(cx)); assert_eq!(cells(&mut cx, &book, "A1:A5"), "Name\nb\nc\na"); run(&mut cx, &book, "cell.goto", &["A7"]); run(&mut cx, &book, "cell.enter", &["p;q;r"]); run(&mut cx, &book, "cell.goto", &["A7"]); update(&mut cx, &book, |book, cx| book.ask_columns(cx)); asked(&cx, &book, |dialog| { matches!(dialog, Dialog::Columns { .. }) }); update(&mut cx, &book, |book, cx| { if let Some(Dialog::Columns { separators, .. }) = &mut book.dialog { *separators = vec!["59".into()]; } book.columns_asked(cx); }); assert_eq!(cells(&mut cx, &book, "A7:C7"), "p\tq\tr"); } #[test] fn a_column_button_lists_validation_offers_and_formats_list() { 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; }; open(&mut cx, &book); run(&mut cx, &book, "cell.goto", &["A1"]); run(&mut cx, &book, "data.autofilter", &[]); let id = cx.update(|app| book.read(app).core.active.expect("a tab")); update(&mut cx, &book, |book, cx| { book.ask_column(id, &Request::Filter { column: 0, row: 0 }, cx) }); asked(&cx, &book, |dialog| matches!(dialog, Dialog::Column { .. })); update(&mut cx, &book, |book, cx| { let Some(Dialog::Column { entries, picked, .. }) = &mut book.dialog else { unreachable!("checked above"); }; assert_eq!(picked.len(), entries.len(), "nothing filtered yet"); *picked = vec!["c".into()]; book.column_asked(None, cx); }); assert_eq!(dropped(&mut cx, &book, 0), ["a", "b"]); run(&mut cx, &book, "data.unfilter", &[]); run(&mut cx, &book, "cell.goto", &["E2"]); update(&mut cx, &book, |book, cx| book.ask_rule(cx)); asked(&cx, &book, |dialog| { matches!(dialog, Dialog::Validation { .. }) }); update(&mut cx, &book, |book, cx| { let Some(Dialog::Validation { rule, fields, .. }) = &mut book.dialog else { unreachable!("checked above"); }; rule.kind = "list".into(); rule.show_input = true; fields .list .update(cx, |list, cx| list.set_text("Red\nGreen", cx)); fields .input_message .update(cx, |note, cx| note.set_text("Pick a color", cx)); book.rule_asked(cx); }); run(&mut cx, &book, "cell.goto", &["E3"]); run(&mut cx, &book, "cell.goto", &["E2"]); settle(&cx, |app| { book.read(app) .active_tab() .and_then(|tab| tab.extra.rule.as_ref()) .is_some_and(|(at, rule)| at.1 == "E2" && rule.input_message == "Pick a color") }); update(&mut cx, &book, |book, cx| { book.offer_choices(id, (4, 1), cx) }); settle(&cx, |app| book.read(app).choices.is_some()); let values = cx.update(|app| { let choices = book.read(app).choices.as_ref().expect("the menu"); choices.values.clone() }); assert_eq!(values, ["Red", "Green"]); run(&mut cx, &book, "cell.goto", &["B2:B5"]); cx.update_window(window.into(), |_, window, app| { book.update(app, |book, cx| book.ask_conditional(window, cx)) }) .expect("the window lives"); update(&mut cx, &book, |book, cx| { if let Some(Dialog::Conditional { value1, .. }) = &book.dialog { value1.update(cx, |value, cx| value.set_text("2", cx)); } book.conditional_asked(cx); }); quiet(&cx, &book); update(&mut cx, &book, |book, cx| book.ask_formats(cx)); asked(&cx, &book, |dialog| { matches!(dialog, Dialog::Formats { .. }) }); let style = cx.update(|app| match &book.read(app).dialog { Some(Dialog::Formats { formats, .. }) => formats[0].style.clone(), _ => unreachable!("checked above"), }); assert_eq!(style, "Good"); update(&mut cx, &book, |book, cx| book.format_removed(cx)); quiet(&cx, &book); update(&mut cx, &book, |book, cx| book.ask_formats(cx)); assert!( cx.update(|app| book.read(app).dialog.is_none()), "none left" ); } #[test] fn the_panel_reads_number_formats_and_find_counts_cells() { 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; }; open(&mut cx, &book); run(&mut cx, &book, "cell.goto", &["B2"]); run(&mut cx, &book, "number.percent", &[]); update(&mut cx, &book, |book, cx| { let tab = book.active_tab().expect("a tab"); let shown = (tab.view.clone(), tab.doc.clone()); book.panel .update(cx, |panel, cx| panel.show(Some((&shown.0, &shown.1)), cx)); }); let kind = |app: &gpui::App| book.read(app).panel.read(app).number.clone(); settle(&cx, |app| { kind(app).is_some_and(|(_, kind)| kind == "percent") }); let panel = cx.update(|app| book.read(app).panel.clone()); cx.update(|app| panel.update(app, |panel, cx| panel.apply_code("[ZZZ]0", cx))); quiet(&cx, &book); cx.update(|app| panel.update(app, |panel, cx| panel.apply_code("0.0", cx))); settle(&cx, |app| kind(app).is_some_and(|(code, _)| code == "0.0")); cx.update_window(window.into(), |_, window, app| { book.update(app, |book, cx| { book.on_find(&ely_office::FindReplace, window, cx) }) }) .expect("the window lives"); let query = cx.update(|app| { let finder = book.read(app).core.finder.as_ref().expect("the finder"); finder.query.clone() }); cx.update(|app| query.update(app, |query, cx| query.set_text("a", cx))); settle(&cx, |app| { book.read(app) .core .finder .as_ref() .is_some_and(|finder| finder.all.len() == 3) }); } /// Two blocks: keys in A, other numbers in C. const BLOCKS: &str = r#" KL 39 17 28 "#; #[test] fn a_column_button_sorts_its_own_block_not_the_cursors() { 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; }; update(&mut cx, &book, |book, cx| { book.new_from_text("blocks.fods", BLOCKS, cx) }); settle(&cx, |app| sheet_names(app, &book) == ["S"]); run(&mut cx, &book, "cell.goto", &["A1"]); run(&mut cx, &book, "data.autofilter", &[]); run(&mut cx, &book, "cell.goto", &["C3"]); let id = cx.update(|app| book.read(app).core.active.expect("a tab")); update(&mut cx, &book, |book, cx| { book.ask_column(id, &Request::Filter { column: 0, row: 0 }, cx) }); asked(&cx, &book, |dialog| matches!(dialog, Dialog::Column { .. })); update(&mut cx, &book, |book, cx| book.column_asked(Some(true), cx)); assert_eq!(cells(&mut cx, &book, "A2:A4"), "1\n2\n3"); assert_eq!(cells(&mut cx, &book, "C2:C4"), "9\n7\n8"); }