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\""]]); // #2225: a quote that doesn't close the field leaves it as written, as Excel pastes it. assert_eq!(parse_tsv("\"hi\" there\tx\n"), vec![vec!["\"hi\" there", "x"]]); assert_eq!(parse_tsv("say \"hi\"\n"), vec![vec!["say \"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 drag_and_drop_asks_before_replacing_data() { // #3136: dragging column B's border onto column A replaced A without asking. let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1:B3", "values": [["Apple", "Kiwi"], ["Pear", "Plum"], ["Fig", "Lime"]]})).unwrap(); s.execute("edit.copy", json!({"range": "D9"})).unwrap(); let text = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["input"].clone(); let r = s.execute("range.move", json!({"range": "B:B", "to": "A1"})); let Err(EngineError::Confirm(q)) = r else { panic!("{r:?}") }; assert_eq!(q, "There's already data here. Do you want to replace it?"); // Nothing moved, and the copy made before is still on the clipboard. assert_eq!(text(&mut s, "A1"), json!("Apple")); assert_eq!(text(&mut s, "B1"), json!("Kiwi")); assert!(s.clipboard.as_ref().is_some_and(|c| !c.cut && c.range.a1() == "D9")); // Confirmed, column B replaces column A. s.execute("range.move", json!({"range": "B:B", "to": "A1", "overwrite": true})).unwrap(); assert_eq!(text(&mut s, "A1"), json!("Kiwi")); assert_eq!(text(&mut s, "A3"), json!("Lime")); assert_eq!(text(&mut s, "B1"), json!("")); // Onto empty cells, or overlapping only the moved cells themselves, nothing is asked. s.execute("range.move", json!({"range": "A1:A3", "to": "A2"})).unwrap(); assert_eq!(text(&mut s, "A4"), json!("Lime")); s.execute("range.move", json!({"range": "A2:A4", "to": "C2"})).unwrap(); assert_eq!(text(&mut s, "C2"), json!("Kiwi")); assert!(s.execute("range.move", json!({"range": "C2"})).is_err(), "`to` is required"); } #[test] fn drag_and_drop_asks_before_replacing_spilled_values() { // Cells showing a dynamic array's spilled values hold no cell of their own, but they are data. let mut s = Session::new(); s.new_workbook(); s.execute("cell.set", json!({"cell": "A1", "input": "=SEQUENCE(3)"})).unwrap(); s.execute("range.setValues", json!({"range": "C2:C3", "values": [["Kiwi"], ["Plum"]]})).unwrap(); let r = s.execute("range.move", json!({"range": "C2:C3", "to": "A2"})); let Err(EngineError::Confirm(q)) = r else { panic!("{r:?}") }; assert_eq!(q, "There's already data here. Do you want to replace it?"); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(2, 2)), Value::text("Plum")); s.execute("range.move", json!({"range": "C2:C3", "to": "A2", "overwrite": true})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(1, 0)), Value::text("Kiwi")); } #[test] fn drag_and_drop_on_another_sheet_stays_on_that_sheet() { // The move was checked on the range's sheet but pasted on the active one, replacing its data // without asking. let mut s = Session::new(); s.new_workbook(); s.execute("home.insertSheet", json!({"name": "Two"})).unwrap(); s.execute("range.setValues", json!({"range": "B1:B2", "values": [["Kiwi"], ["Plum"]]})).unwrap(); s.execute("sheet.activate", json!({"sheet": 0})).unwrap(); s.execute("cell.set", json!({"cell": "A1", "input": "Keep"})).unwrap(); s.execute("selection.set", json!({"range": "C5"})).unwrap(); s.execute("range.move", json!({"range": "Two!B1:B2", "to": "A1"})).unwrap(); // Sheet1 stays active with its own selection; the moved cells are on Two. assert_eq!(s.doc().unwrap().wb.active_sheet(), 0); assert_eq!(s.doc().unwrap().selection.a1(), "C5"); let wb = &s.doc().unwrap().wb; let two = wb.sheet_index("Two").unwrap(); assert_eq!(wb.sheet(0).unwrap().value(CellRef::new(0, 0)), Value::text("Keep")); assert_eq!(wb.sheet(two).unwrap().value(CellRef::new(0, 0)), Value::text("Kiwi")); assert_eq!(wb.sheet(two).unwrap().value(CellRef::new(1, 0)), Value::text("Plum")); assert_eq!(wb.sheet(two).unwrap().value(CellRef::new(0, 1)), Value::Empty); } #[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)); } #[test] fn next_corner_of_selection_goes_clockwise_and_anchors_there() { // #1442, as Excel does it: B2 → D2 → D4 → B4 → B2, the area stays selected, and Shift+arrows // then move the corner opposite the new active cell. let mut s = Session::new(); s.new_workbook(); s.execute("selection.set", json!({"range": "B2:D4", "active": "B2"})).unwrap(); let mut seen = Vec::new(); for _ in 0..4 { let r = s.execute("selection.nextCorner", json!({})).unwrap(); assert_eq!(r["selection"], "B2:D4"); seen.push(r["active"].as_str().unwrap().to_string()); } assert_eq!(seen, ["D2", "D4", "B4", "B2"]); s.execute("selection.nextCorner", json!({})).unwrap(); s.execute("selection.nextCorner", json!({})).unwrap(); s.execute("selection.move", json!({"dr": 0, "dc": 1, "extend": true})).unwrap(); assert_eq!(s.doc().unwrap().selection.current().a1(), "C2:D4"); // One row goes end to end; a single cell stays. s.execute("selection.set", json!({"range": "B2:D2", "active": "B2"})).unwrap(); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "D2"); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "B2"); s.execute("selection.set", json!({"range": "C5"})).unwrap(); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "C5"); } #[test] fn next_corner_skips_corners_the_sheet_protection_does_not_let_you_select() { // With locked cells unselectable, Ctrl+. goes on to the next corner it may select. let mut s = Session::new(); s.new_workbook(); s.execute("home.lockCell", json!({"range": "B2:C3", "on": false})).unwrap(); s.execute("selection.set", json!({"range": "B2:D3", "active": "B2"})).unwrap(); s.execute("review.protectSheet", json!({"selectLockedCells": false})).unwrap(); let r = s.execute("selection.nextCorner", json!({})).unwrap(); assert_eq!((r["active"].as_str(), r["selection"].as_str()), (Some("B3"), Some("B2:D3")), "D2 and D3 are locked"); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "B2"); } #[test] fn next_corner_stops_once_on_a_merged_corner_at_its_top_left_cell() { // A corner inside a merged cell is that merged cell, whose active cell is its top-left one. let mut s = Session::new(); s.new_workbook(); s.execute("home.mergeCells", json!({"range": "C2:D2"})).unwrap(); s.execute("selection.set", json!({"range": "B2:D3", "active": "B2"})).unwrap(); let seen: Vec = (0..4).map(|_| s.execute("selection.nextCorner", json!({})).unwrap()["active"].as_str().unwrap().to_string()).collect(); assert_eq!(seen, ["C2", "D3", "B3", "B2"]); // A selection that is one merged cell has no other corner. s.execute("selection.set", json!({"range": "C2:D2", "active": "C2"})).unwrap(); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "C2"); } #[test] fn next_corner_skips_hidden_corners_and_stays_inside_the_selected_area() { // A corner in a hidden column is passed over, as keyboard moves pass over hidden cells. let mut s = Session::new(); s.new_workbook(); s.execute("home.hideColumns", json!({"cols": "D:D"})).unwrap(); s.execute("selection.set", json!({"range": "B2:D3", "active": "B2"})).unwrap(); assert_eq!(s.execute("selection.nextCorner", json!({})).unwrap()["active"], "B3"); // Column C selected with B1:C1 merged: its top corner's merged cell starts in column B, // outside the selection, so the active cell doesn't go there. let mut s = Session::new(); s.new_workbook(); s.execute("home.mergeCells", json!({"range": "B1:C1"})).unwrap(); s.execute("selection.set", json!({"cell": "C5"})).unwrap(); s.execute("selection.column", json!({})).unwrap(); let r = s.execute("selection.nextCorner", json!({})).unwrap(); assert_eq!((r["active"].as_str(), r["selection"].as_str()), (Some("C1048576"), Some("C:C"))); } #[test] fn copy_quotes_only_cells_with_tabs_or_line_breaks() { // #2225: a cell holding a quote was wrapped in quotes on copy; Excel quotes only text with a // tab or line break, and pastes the result back as the same cells. let mut s = Session::new(); s.new_workbook(); for (cell, input) in [("A1", "say \"hi\""), ("A2", "plain"), ("A3", "\"hi\" there"), ("A4", "x \"q\"\ny"), ("A5", "tab\tinside")] { s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap(); } let text = s.execute("edit.copy", json!({"range": "A1:A5"})).unwrap()["text"].as_str().unwrap().to_string(); assert_eq!(text, "say \"hi\"\nplain\n\"hi\" there\n\"x \"\"q\"\"\ny\"\n\"tab\tinside\"\n"); let rows = parse_tsv(&text); let cells: Vec<&str> = rows.iter().map(|r| r[0].as_str()).collect(); assert_eq!(cells, ["say \"hi\"", "plain", "\"hi\" there", "x \"q\"\ny", "tab\tinside"]); } #[test] fn a_field_that_is_not_really_quoted_keeps_its_separators() { // A field opening with a quote is quoted only if a closing quote ends it; when it doesn't (or // never closes), the text is read as written, and the tabs and line breaks inside it still // separate cells, as they would have without the leading quote. assert_eq!(parse_tsv("\"a\tb\"x\tc"), vec![vec!["\"a", "b\"x", "c"]]); assert_eq!(parse_tsv("\"a\tb\nc"), vec![vec!["\"a", "b"], vec!["c"]]); assert_eq!(parse_tsv("\"a\tb\tc\n"), vec![vec!["\"a", "b", "c"]]); // Cells copied without quotes (they hold no tab or line break) read back as the same cells. let mut s = Session::new(); s.new_workbook(); for (cell, input) in [("A1", "'\"a"), ("B1", "b\"x"), ("C1", "c")] { s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap(); } let text = s.execute("edit.copy", json!({"range": "A1:C1"})).unwrap()["text"].as_str().unwrap().to_string(); assert_eq!(parse_tsv(&text), vec![vec!["\"a", "b\"x", "c"]], "{text:?}"); // Many fields that only look quoted: each is replayed at most once (the replay budget is the // input's length), and every one is still its own cell. let hostile = "\"x\t".repeat(50_000); assert_eq!(parse_tsv(&hostile).first().map(Vec::len), Some(50_001)); } /// `within: selection` searches only the selected cells, as Excel does with a range selected /// (#3224): Find All lists them, Find Next moves the active cell through them and keeps the range /// selected, and Replace changes only them. #[test] fn find_and_replace_within_the_selection() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1:B4", "values": [["pear", "pear"], ["plum", "pear"], ["pear", "fig"], ["pear", "pear"]]})) .unwrap(); s.execute("selection.set", json!({"range": "B1:B4"})).unwrap(); let all = s.execute("edit.find", json!({"what": "pear", "all": true, "within": "selection"})).unwrap(); let cells: Vec<&str> = all["results"].as_array().unwrap().iter().filter_map(|r| r["cell"].as_str()).collect(); assert_eq!(cells, ["B1", "B2", "B4"], "only column B"); assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B2"); assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B4"); assert_eq!(s.execute("edit.find", json!({"what": "pear", "within": "selection"})).unwrap()["cell"], "B1", "wraps inside the range"); let sel = &s.doc().unwrap().selection; assert_eq!((sel.a1(), sel.active.a1()), ("B1:B4".to_string(), "B1".to_string()), "the range stays selected"); assert!(s.execute("edit.find", json!({"what": "plum", "within": "selection"})).is_err(), "A2 is outside the range"); assert_eq!(s.execute("edit.replace", json!({"what": "pear", "with": "kiwi", "within": "selection"})).unwrap()["replaced"], 3); let sh = s.doc().unwrap().wb.active().unwrap(); let at = |a: &str| sh.value(CellRef::parse(a).unwrap()); assert_eq!((at("A1"), at("B1"), at("B4"), at("A4")), (Value::text("pear"), Value::text("kiwi"), Value::text("kiwi"), Value::text("pear"))); // The sheet scope is unchanged. let all = s.execute("edit.find", json!({"what": "pear", "all": true})).unwrap(); assert_eq!(all["count"], 3, "A1, A3 and A4 on the whole sheet"); }