use super::*; /// Sheet1 and Sheet2 with a workbook range name, a name scoped to each sheet, a constant, a /// formula and a hidden name. fn go_to_workbook() -> Session { let mut s = Session::new(); s.new_workbook(); s.execute("home.insertSheet", json!({"name": "Sheet2"})).unwrap(); s.execute("selection.set", json!({"sheet": 0})).unwrap(); let define = |s: &mut Session, name: &str, refers: &str, scope: &str| { s.execute("formulas.defineName", json!({"name": name, "refersTo": refers, "scope": scope})).unwrap(); }; define(&mut s, "Price", "=Sheet1!$B$2:$B$5", "Workbook"); define(&mut s, "type", "=Sheet1!$D$2:$D$5", "Workbook"); define(&mut s, "Areas", "=Sheet1!$A$1,Sheet1!$C$3:$C$4", "Workbook"); define(&mut s, "Local", "=Sheet2!$C$3", "Sheet2"); define(&mut s, "Own", "=Sheet1!$E$1", "Sheet1"); define(&mut s, "Rate", "=0.07", "Workbook"); define(&mut s, "Total", "=SUM(Sheet1!$B$2:$B$5)", "Workbook"); let wb = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb); wb.names.push(gridcraft_model::DefinedName { name: "Secret".into(), scope: None, formula: "Sheet1!$Z$9".into(), comment: String::new(), hidden: true, }); s } /// "The Go to popup window shows named ranges on every worksheet in your workbook." Names /// that are not ranges, and hidden names, are not listed. #[test] fn go_to_lists_the_named_ranges_of_every_sheet() { let mut s = go_to_workbook(); let list = s.execute("edit.goTo", json!({"list": true})).unwrap(); assert_eq!(list["names"], json!(["Areas", "Own", "Price", "Sheet2!Local", "type"]), "{list}"); assert_eq!(list["recent"], json!([]), "{list}"); // Seen from Sheet2, its own name is bare and Sheet1's is qualified. s.execute("selection.set", json!({"sheet": 1})).unwrap(); let list = s.execute("edit.goTo", json!({"list": true})).unwrap(); assert_eq!(list["names"], json!(["Areas", "Local", "Price", "Sheet1!Own", "type"]), "{list}"); } #[test] fn go_to_a_name_scoped_to_another_sheet_goes_to_that_sheet() { let mut s = go_to_workbook(); let out = s.execute("edit.goTo", json!({"reference": "Sheet2!Local"})).unwrap(); assert_eq!(out["selection"], "C3", "{out}"); assert_eq!(s.doc().unwrap().wb.active_sheet(), 1); // A name of several areas selects all of them. let out = s.execute("edit.goTo", json!({"reference": "Areas"})).unwrap(); assert_eq!(out["selection"], "A1,C3:C4", "{out}"); assert_eq!(s.doc().unwrap().wb.active_sheet(), 0); } /// A name matches in any case and any script, as a formula matches it. #[test] fn go_to_matches_a_sheet_name_in_any_case() { let mut s = go_to_workbook(); s.execute("formulas.defineName", json!({"name": "Größe", "refersTo": "=Sheet2!$D$4", "scope": "Sheet2"})).unwrap(); s.execute("formulas.defineName", json!({"name": "GRÖSSE", "refersTo": "=Sheet1!$F$6", "scope": "Workbook"})).unwrap(); let out = s.execute("edit.goTo", json!({"reference": "Sheet2!GRÖSSE"})).unwrap(); assert_eq!(out["selection"], "D4", "{out}"); // Seen from Sheet2, its own name hides the workbook name it shares. let list = s.execute("edit.goTo", json!({"list": true})).unwrap(); assert_eq!(list["names"], json!(["Areas", "Größe", "Local", "Price", "Sheet1!Own", "type"]), "{list}"); } /// "You can enter multiple named ranges or cell references in the Reference box. Separating /// each with a comma ... Excel will highlight all the ranges." #[test] fn go_to_selects_every_reference_and_name_in_a_list() { let mut s = go_to_workbook(); let out = s.execute("edit.goTo", json!({"reference": "B14:C22,F19:G30,H21:H29"})).unwrap(); assert_eq!(out["selection"], "B14:C22,F19:G30,H21:H29", "{out}"); let out = s.execute("edit.goTo", json!({"reference": "Price, Type"})).unwrap(); assert_eq!(out["selection"], "B2:B5,D2:D5", "{out}"); let out = s.execute("edit.goTo", json!({"reference": "Sheet2!$A$1,Sheet2!B2"})).unwrap(); assert_eq!(out["selection"], "A1,B2", "{out}"); assert_eq!(s.doc().unwrap().wb.active_sheet(), 1); // Areas on two sheets, and a part that is not a reference, leave the selection alone. for bad in ["A1,Sheet1!B2", "A1,Rate", "A1,", "Total"] { assert!(s.execute("edit.goTo", json!({"reference": bad})).is_err(), "{bad}"); assert_eq!(s.doc().unwrap().selection.a1(), "A1,B2", "{bad}"); } } #[test] fn go_to_reads_the_parts_of_a_long_reference_one_at_a_time() { let mut parts = reference_parts("A1, 'Q1, Q2'!B2,SUM(C3,D4)"); assert_eq!(parts.next(), Some("A1")); assert_eq!(parts.next(), Some("'Q1, Q2'!B2")); assert_eq!(parts.next(), Some("SUM(C3,D4)")); assert_eq!(parts.next(), None); assert_eq!(reference_parts("").collect::>(), vec![""]); let mut s = go_to_workbook(); let err = s.execute("edit.goTo", json!({"reference": "A1,".repeat(GO_TO_MAX_AREAS + 1)})).unwrap_err(); assert!(err.to_string().contains("at most"), "{err}"); let bad = format!("Total{}", ",".repeat(1_000_000)); assert!(s.execute("edit.goTo", json!({"reference": bad})).is_err()); } /// "The Go to box keeps track of ranges as you enter them, and you can return to any of them." #[test] fn go_to_keeps_the_references_entered_newest_first() { let mut s = go_to_workbook(); s.execute("edit.goTo", json!({"reference": "B14:C22,F19:G30", "record": true})).unwrap(); s.execute("edit.goTo", json!({"reference": "Sheet2!A5", "record": true})).unwrap(); // Names are listed already; a call without `record` (the Name Box) is not kept. s.execute("edit.goTo", json!({"reference": "Price", "record": true})).unwrap(); s.execute("edit.goTo", json!({"reference": "Z9"})).unwrap(); s.execute("edit.goTo", json!({"reference": "b14:c22,f19:g30", "record": true})).unwrap(); let list = s.execute("edit.goTo", json!({"list": true})).unwrap(); assert_eq!(list["recent"], json!(["Sheet1!B14:C22,Sheet1!F19:G30", "Sheet2!A5"]), "{list}"); // Going back from Sheet2 to a recent reference returns to its sheet. s.execute("selection.set", json!({"sheet": 1})).unwrap(); let out = s.execute("edit.goTo", json!({"reference": "Sheet1!B14:C22,Sheet1!F19:G30", "record": true})).unwrap(); assert_eq!(out["selection"], "B14:C22,F19:G30", "{out}"); assert_eq!(s.doc().unwrap().wb.active_sheet(), 0); for i in 0..GO_TO_RECENT + 2 { s.execute("edit.goTo", json!({"reference": format!("A{}", i + 1), "record": true})).unwrap(); } let list = s.execute("edit.goTo", json!({"list": true})).unwrap(); assert_eq!(list["recent"].as_array().map(Vec::len), Some(GO_TO_RECENT), "{list}"); assert_eq!(list["recent"][0], format!("Sheet1!A{}", GO_TO_RECENT + 2), "{list}"); } #[test] fn cell_set_takes_numbers_for_input_and_value() { // #697: a JSON number for `input` used to clear the cell instead of entering it. let mut s = Session::new(); s.new_workbook(); s.execute("cell.set", json!({"cell": "A1", "input": 123.5})).unwrap(); s.execute("cell.set", json!({"cell": "A2", "value": 77})).unwrap(); s.execute("cell.set", json!({"cell": "A3", "input": true})).unwrap(); s.execute("cell.set", json!({"cell": "A4", "input": "=A1+A2"})).unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); assert_eq!(get(&mut s, "A1")["value"], json!(123.5)); assert_eq!(get(&mut s, "A2")["value"], json!(77.0)); assert_eq!(get(&mut s, "A3")["value"], json!(true)); assert_eq!(get(&mut s, "A4")["value"], json!(200.5)); assert!(s.execute("cell.set", json!({"cell": "A5"})).is_err()); } /// Every string in `values` went through typed input, read with en-US rules, in silence: a /// caller sending "05.04.2026" from a dd.mm.yyyy export stored 4 May, "2.500" became 2.5 and /// "4.25 %" became a percentage (#2255). `parse: false` stores a string as the text it is. #[test] fn set_values_can_store_strings_as_the_text_they_are() { let mut s = Session::new(); s.new_workbook(); let strings = ["05.04.2026", "02/11/2026", "2.500", "4.25 %", "=1+1", "'keep"]; let row: Vec = strings.iter().map(|v| json!(v)).collect(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); // The default is unchanged: the dates swap, the thousands separator is a decimal point. s.execute("range.setValues", json!({"range": "A1", "values": [row.clone()]})).unwrap(); assert_eq!(get(&mut s, "A1")["text"], "5/4/2026", "dd.mm.yyyy read as mm.dd.yyyy"); assert_eq!(get(&mut s, "C1")["value"], json!(2.5), "\"2.500\" read as a decimal"); assert_eq!(get(&mut s, "D1")["value"], json!(0.0425), "\"4.25 %\" read as a percentage"); // With `parse: false` each string is the text it was given, and a formula is still a // formula. s.execute("range.setValues", json!({"range": "A3", "parse": false, "values": [row.clone()]})).unwrap(); for (i, want) in strings.iter().enumerate() { let cell = format!("{}3", (b'A' + i as u8) as char); let got = get(&mut s, &cell); if *want == "=1+1" { assert_eq!(got["value"], json!(2.0), "{cell} is still a formula: {got}"); assert_eq!(got["formula"], "=1+1", "{got}"); } else { assert_eq!(got["value"], json!(*want), "{cell} is the text it was given: {got}"); assert!(got["formula"].is_null(), "{cell} is not a formula: {got}"); } } // Numbers and booleans are untouched either way. s.execute("range.setValues", json!({"range": "A5", "parse": false, "values": [[12.5, true, null]]})).unwrap(); assert_eq!(get(&mut s, "A5")["value"], json!(12.5)); assert_eq!(get(&mut s, "B5")["value"], json!(true)); assert_eq!(get(&mut s, "C5")["text"], ""); // `parse` is a flag, and anything else is a usage error rather than a quiet default. s.execute("range.setValues", json!({"range": "A7", "parse": true, "values": [["2.500"]]})).unwrap(); assert_eq!(get(&mut s, "A7")["value"], json!(2.5)); assert!(s.execute("range.setValues", json!({"range": "A8", "parse": "no", "values": [["x"]]})).is_err()); assert!(s.execute("range.setValues", json!({"range": "A8", "parse": 0, "values": [["x"]]})).is_err()); // A line break still turns on Wrap Text. s.execute("range.setValues", json!({"range": "A9", "parse": false, "values": [["one\ntwo"]]})).unwrap(); assert_eq!(get(&mut s, "A9")["value"], json!("one\ntwo")); let d = s.doc().unwrap(); let sh = d.wb.active().unwrap(); assert!(d.wb.styles.get(sh.style_id(CellRef::parse("A9").unwrap())).align.wrap); } /// The result names the strings typed input read as something else, so a script notices without /// reading the file back (#2255). A formula and a leading apostrophe are not conversions. #[test] fn set_values_reports_the_strings_typed_input_read() { let mut s = Session::new(); s.new_workbook(); let row = json!(["05.04.2026", "02/11/2026", "2.500", "4.25 %", "=1+1", "'keep"]); let r = s.execute("range.setValues", json!({"range": "A1", "values": [row.clone()]})).unwrap(); assert_eq!(r["cells"], 6, "{r}"); assert_eq!(r["convertedCount"], 4, "{r}"); assert_eq!(r["converted"][0], json!({"cell": "A1", "input": "05.04.2026", "stored": "5/4/2026"}), "{r}"); assert_eq!(r["converted"][1], json!({"cell": "B1", "input": "02/11/2026", "stored": "2/11/2026"}), "{r}"); assert_eq!(r["converted"][2], json!({"cell": "C1", "input": "2.500", "stored": "2.5"}), "{r}"); assert_eq!(r["converted"][3], json!({"cell": "D1", "input": "4.25 %", "stored": "4.25%"}), "{r}"); // The report is a diagnostic, not a second copy of the data: a long input is cut. let long = s.execute("range.setValues", json!({"range": "J1", "values": [["9".repeat(100)]]})).unwrap(); assert_eq!(long["convertedCount"], 1, "{long}"); let input = long["converted"][0]["input"].as_str().unwrap_or_default(); assert_eq!(input.chars().count(), 33, "32 characters and the ellipsis: {input}"); assert!(input.ends_with('…'), "{input}"); // Nothing is read with `parse: false`, so nothing is reported. let verbatim = s.execute("range.setValues", json!({"range": "A3", "parse": false, "values": [row]})).unwrap(); assert_eq!(verbatim["convertedCount"], 0, "{verbatim}"); assert_eq!(verbatim["converted"], json!([]), "{verbatim}"); } /// An empty string clears the cell with `parse: false` too, as `null` and the default path do: a /// script clearing cells with "" must not leave cells `COUNTA` counts. #[test] fn set_values_parse_false_clears_an_empty_string() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1:B1", "values": [["keep", "keep"]]})).unwrap(); s.execute("home.bold", json!({"range": "A1"})).unwrap(); s.execute("range.setValues", json!({"range": "A1", "parse": false, "values": [["", ""]]})).unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); assert_eq!(get(&mut s, "A1")["type"], "empty"); assert_eq!(get(&mut s, "B1")["type"], "empty"); let d = s.doc().unwrap(); let sh = d.wb.sheet(0).unwrap(); assert!(d.wb.styles.get(sh.style_id(CellRef::parse("A1").unwrap())).font.bold, "clearing with \"\" keeps the formatting"); assert_eq!(s.execute("formulas.evaluate", json!({"formula": "=COUNTA(A1:B1)"})).unwrap(), json!(0.0)); } /// The text `parse: false` stores is still text after the workbook is saved and reopened, and the /// default's reading of a date survives as a date (#2255). #[test] fn set_values_text_survives_an_xlsx_roundtrip() { let mut s = Session::new(); s.new_workbook(); let values = json!([["05.04.2026", "2.500", "4.25 %", "=1+1", 3, true]]); s.execute("range.setValues", json!({"range": "A1", "parse": false, "values": values})).unwrap(); s.execute("range.setValues", json!({"range": "A3", "values": [["05.04.2026"]]})).unwrap(); let bytes = crate::io::save_bytes(&s.doc().unwrap().wb, "book.xlsx").unwrap(); let (wb, _) = crate::io::open_bytes("book.xlsx", &bytes).unwrap(); let sh = wb.sheet(0).unwrap(); for (col, want) in [(0, "05.04.2026"), (1, "2.500"), (2, "4.25 %")] { assert_eq!(sh.value(CellRef::new(0, col)), Value::text(want), "column {col} is the text it was given"); } assert_eq!(sh.value(CellRef::new(0, 3)), Value::number(2.0), "=1+1 is still a formula"); assert_eq!(sh.value(CellRef::new(0, 4)), Value::number(3.0)); assert_eq!(sh.value(CellRef::new(0, 5)), Value::Bool(true)); // The default read the date, and the file keeps it as one. let at = CellRef::new(2, 0); assert_eq!(crate::display::cell_text(&wb, sh, at), "5/4/2026"); assert_eq!(wb.styles.get(sh.style_id(at)).num_fmt.as_str(), "m/d/yyyy"); } #[test] fn legacy_array_entry() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "B2", "values": [[1], [2]]})).unwrap(); s.execute("cell.set", json!({"cell": "C4", "input": "old"})).unwrap(); s.execute("selection.set", json!({"range": "C2:C4"})).unwrap(); s.execute("cell.set", json!({"cell": "C2", "input": "=B2:B3*2", "array": true})).unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); assert_eq!(get(&mut s, "C3")["value"], json!(4.0)); // Past the result Excel shows #N/A; the old content of the range is replaced. assert_eq!(get(&mut s, "C4")["text"], "#N/A"); let bar = s.doc().unwrap().wb.active().unwrap().cell(CellRef::parse("C2").unwrap()).map(|c| c.bar_text()); assert_eq!(bar.as_deref(), Some("{=B2:B3*2}")); } #[test] fn an_array_entered_outside_the_selection_covers_only_its_cell() { // Found by the command-sequence fuzzer: the array took the selection's range even with // `cell` elsewhere (or on another sheet), so its formula sat outside its own range and the // selected cells were wiped. let mut s = Session::new(); s.new_workbook(); s.execute("cell.set", json!({"cell": "K20", "input": "keep"})).unwrap(); s.execute("selection.set", json!({"range": "K20:K22"})).unwrap(); s.execute("cell.set", json!({"cell": "H5", "input": "=1+1", "array": true})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); let f = sh.cell(CellRef::parse("H5").unwrap()).and_then(|c| c.formula.clone()).unwrap(); assert_eq!(f.array, Some(RangeRef::parse("H5").unwrap())); assert_eq!(sh.value(CellRef::parse("K20").unwrap()), Value::text("keep")); } #[test] fn wildcards() { assert!(wildcard("hello", "h*o")); assert!(wildcard("hello", "h?llo")); assert!(!wildcard("hello", "h?lo")); assert!(wildcard("a*b", "a~*b")); assert!(!wildcard("axb", "a~*b")); assert!(wildcard("", "*")); } #[test] fn tsv() { assert_eq!(parse_tsv("a\tb\n1\t2\n"), vec![vec!["a", "b"], vec!["1", "2"]]); assert_eq!(parse_tsv("\"x\ty\"\tz"), vec![vec!["x\ty", "z"]]); assert_eq!(parse_tsv("\"he said \"\"hi\"\"\""), vec![vec!["he said \"hi\""]]); } #[test] fn paste_special_operations_keep_text_errors_and_formulas() { let mut s = Session::new(); s.new_workbook(); s.execute( "range.setValues", json!({"range": "A1", "values": [["'123", "Total", "=NA()", "=B3*2", 5], [1, null, null, null, null], [null, 4, null, null, null]]}), ) .unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); assert_eq!(get(&mut s, "A1")["value"], "123"); s.execute("edit.copy", json!({"range": "A2"})).unwrap(); s.execute("selection.set", json!({"range": "A1:E1"})).unwrap(); s.execute("edit.pasteSpecial", json!({"what": "values", "operation": "multiply"})).unwrap(); assert_eq!(get(&mut s, "A1")["value"], json!(123.0)); assert_eq!(get(&mut s, "B1")["value"], "Total"); assert_eq!(get(&mut s, "C1")["value"]["error"], "#N/A"); assert_eq!(get(&mut s, "D1")["formula"], "=(B3*2)*1"); assert_eq!(get(&mut s, "D1")["value"], json!(8.0)); assert_eq!(get(&mut s, "E1")["value"], json!(5.0)); } #[test] fn cut_paste_that_overflows_the_grid_moves_nothing() { // #997: the source was emptied first and the cells that fell off the grid were skipped. let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2]]})).unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); s.execute("edit.cut", json!({"range": "A1:A2"})).unwrap(); // Transposed, the 2 rows become 2 columns and the second one is past XFD. assert!(s.execute("edit.pasteSpecial", json!({"what": "all", "transpose": true, "at": "XFD1"})).is_err()); assert_eq!(get(&mut s, "A1")["value"], json!(1.0)); assert_eq!(get(&mut s, "A2")["value"], json!(2.0)); // Not transposed, the second row would be past the last row. assert!(s.execute("edit.pasteSpecial", json!({"what": "all", "at": "A1048576"})).is_err()); assert_eq!(get(&mut s, "A1")["value"], json!(1.0)); assert_eq!(get(&mut s, "A2")["value"], json!(2.0)); // Not transposed it still fits in column XFD. s.execute("edit.pasteSpecial", json!({"what": "all", "at": "XFD1"})).unwrap(); assert_eq!(get(&mut s, "XFD1")["value"], json!(1.0)); assert_eq!(get(&mut s, "XFD2")["value"], json!(2.0)); } #[test] fn replace_case_insensitive() { assert_eq!(replace_ci("Apple apple APPLE", "apple", "pear"), "pear pear pear"); } #[test] fn replace_case_insensitive_keeps_text_when_folding_moves_boundaries() { // U+212A KELVIN SIGN folds to `k` (3→1 bytes) while U+0130 folds to `i` plus a // combining dot (2→3 bytes): the folded text is the same length but its byte positions // no longer line up with the original, and the old slicing dropped the tail. let mut s = Session::new(); s.new_workbook(); let text = "\u{212A}\u{0130}\u{0130}"; s.execute("cell.set", json!({"cell": "A1", "input": text})).unwrap(); s.execute("edit.replace", json!({"what": "k", "with": "x"})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(0, 0)), Value::text(text)); }