use super::*; #[test] fn filter_blanks_include_empty_text_and_formula_results() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["id", "f"], ["e", null], ["ef", "=\"\""], ["sp", " "], ["z", 0]]})).unwrap(); s.execute("data.filter", json!({"range": "A1:B5"})).unwrap(); let r = s.execute("data.filterBy", json!({"column": "B", "values": [], "blanks": true})).unwrap(); assert_eq!(r["hiddenRows"], json!(2)); let vals = filter_values(&s.doc().unwrap().wb, 0, 1); // The `=""` result joins (Blanks) instead of showing as its own empty entry. assert!(!vals.iter().any(|(t, _)| t.is_empty()), "{vals:?}"); let blanks: Vec<_> = vals.into_iter().filter(|(t, _)| t == "(Blanks)").collect(); assert_eq!(blanks, vec![("(Blanks)".to_string(), true)]); } #[test] fn subtotal_honours_header_false() { // #131: with header:false the first data row was treated as a header and left out of its // group's subtotal and the grand total. let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["East", 10], ["East", 20], ["West", 30], ["West", 40]]})).unwrap(); s.execute("data.subtotal", json!({"range": "A1:B4", "groupBy": "A", "function": "sum", "columns": ["B"], "header": false})).unwrap(); let get = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap(); assert_eq!(get(&mut s, "A1")["value"], json!("East")); assert_eq!(get(&mut s, "A3")["text"], json!("East Total")); assert_eq!(get(&mut s, "B3")["value"], json!(30.0), "East's subtotal counts both East rows"); assert_eq!(get(&mut s, "A7")["text"], json!("Grand Total")); assert_eq!(get(&mut s, "B7")["value"], json!(100.0), "the grand total counts every data row"); } #[test] fn subtotal_puts_summary_rows_below_their_detail() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["Region", "Sales"], ["East", 10], ["East", 20], ["West", 30]]})).unwrap(); s.execute("data.outlineSettings", json!({"summaryBelow": false})).unwrap(); s.execute("data.subtotal", json!({"range": "A1:B4", "groupBy": "A", "columns": ["B"]})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap().clone(); assert!(sh.summary_below, "the totals sit below their groups"); // East's detail is rows 2–3 and its total row 4: collapsing level 1 keeps the total showing. s.execute("data.showLevels", json!({"rows": 1})).unwrap(); let hidden = |s: &Session, row: u32| s.doc().unwrap().wb.active().unwrap().is_row_hidden(row); assert!(hidden(&s, 1) && hidden(&s, 2), "East's detail rows are hidden"); assert!(!hidden(&s, 3), "East Total stays visible"); // One undo takes the subtotals and the setting back. s.execute("edit.undo", json!({})).unwrap(); s.execute("edit.undo", json!({})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert!(!sh.summary_below); assert_eq!(sh.value(CellRef::new(3, 0)).display(), "West"); } #[test] fn filter_by_huge_numeric_column_is_rejected_not_overflowed() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "B1", "values": [["Name", "Qty"], ["a", 1], ["b", 2]]})).unwrap(); s.execute("data.filter", json!({"range": "B1:C3"})).unwrap(); for col in [json!(2), json!(u64::from(u32::MAX)), json!(u64::MAX)] { let err = s.execute("data.filterBy", json!({"column": col, "values": ["a"]})).unwrap_err(); assert!(err.to_string().contains("column outside the filter range"), "{col}: {err}"); } let ok = s.execute("data.filterBy", json!({"column": 0, "values": ["a"]})).unwrap(); assert_eq!(ok["hiddenRows"], json!(1)); } #[test] fn filter_by_inside_a_table_installs_the_autofilter_and_keeps_the_button() { for sel in [json!({"cell": "A1"}), json!({"cell": "A2"}), json!({"range": "A1:A3"})] { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["H"], ["a"], ["b"]]})).unwrap(); s.execute("insert.table", json!({"range": "A1:A3", "header": true})).unwrap(); s.execute("selection.set", sel.clone()).unwrap(); s.execute("data.filterBy", json!({"column": "A", "values": ["a"]})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.autofilter.as_ref().map(|a| a.range.a1()), Some("A1:A3".to_string()), "{sel}"); assert!(sh.tables[0].filter_button, "{sel}"); assert!(!sh.is_row_hidden(1), "{sel}"); assert!(sh.is_row_hidden(2), "{sel}"); assert_eq!(sh.value(CellRef::new(2, 0)).display(), "b", "{sel}"); } } #[test] fn autofilter_range_grows_when_a_value_is_entered_in_the_row_below() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["Name", "Qty"], ["d", 4], ["b", 2]]})).unwrap(); s.execute("data.filter", json!({"range": "A1:B3"})).unwrap(); let range = |s: &Session| s.doc().unwrap().wb.active().unwrap().autofilter.as_ref().map(|a| a.range.a1()); s.execute("cell.set", json!({"cell": "A4", "input": "e"})).unwrap(); assert_eq!(range(&s), Some("A1:B4".to_string())); s.execute("range.setValues", json!({"range": "A5", "values": [["f", 6]]})).unwrap(); assert_eq!(range(&s), Some("A1:B5".to_string())); // A row typed after a blank row does not grow it. s.execute("cell.set", json!({"cell": "A7", "input": "x"})).unwrap(); assert_eq!(range(&s), Some("A1:B5".to_string())); // A cell outside the filter's columns does not either. s.execute("cell.set", json!({"cell": "C6", "input": "x"})).unwrap(); assert_eq!(range(&s), Some("A1:B5".to_string())); // Empty text is a blank entry, not a value. s.execute("range.setValues", json!({"range": "A6", "parse": false, "values": [[""]]})).unwrap(); assert_eq!(range(&s), Some("A1:B5".to_string())); // A formula and Ctrl+Enter (range.fill) are entries too. s.execute("cell.set", json!({"cell": "B6", "input": "=1+1"})).unwrap(); assert_eq!(range(&s), Some("A1:B6".to_string())); s.execute("range.fill", json!({"range": "A7:B7", "input": "g"})).unwrap(); assert_eq!(range(&s), Some("A1:B7".to_string())); } #[test] fn list_validation_without_formula1_is_an_error() { let mut s = Session::new(); s.new_workbook(); assert!(s.execute("data.validation", json!({"range": "A1:A3", "type": "list", "source": "\"a,b,c\""})).is_err()); assert!(s.execute("data.validation", json!({"range": "A1:A3", "type": "list"})).is_err()); assert!(s.doc().unwrap().wb.active().unwrap().validations.is_empty()); s.execute("data.validation", json!({"range": "A1:A3", "type": "list", "formula1": "\"a,b,c\""})).unwrap(); assert_eq!(s.execute("data.validate", json!({"cell": "A1", "input": "a"})).unwrap()["ok"], json!(true)); } #[test] fn partial_validation_edits_keep_the_rest_of_the_old_rule() { let mut s = Session::new(); s.new_workbook(); let rule = |range: &str, lo: &str, hi: &str| json!({"range": range, "type": "whole", "operator": "between", "formula1": lo, "formula2": hi}); let ok = |s: &mut Session, cell: &str, input: &str| s.execute("data.validate", json!({"cell": cell, "input": input})).unwrap()["ok"] == json!(true); s.execute("data.validation", rule("B1:B3", "1", "10")).unwrap(); s.execute("data.validation", json!({"range": "B2", "clear": true})).unwrap(); assert!(ok(&mut s, "B2", "999")); assert!(!ok(&mut s, "B1", "999")); assert!(!ok(&mut s, "B3", "999")); s.execute("data.validation", rule("B2:B4", "100", "200")).unwrap(); for cell in ["B2", "B3", "B4"] { assert!(ok(&mut s, cell, "150"), "{cell}"); assert!(!ok(&mut s, cell, "5"), "{cell}"); } assert!(ok(&mut s, "B1", "5")); assert!(!ok(&mut s, "B1", "150")); } #[test] fn showing_detail_gives_zero_width_columns_their_width_back() { // #1714: templates hide grouped columns as `width="0" hidden="1"`; + must show them. let mut s = Session::new(); s.new_workbook(); let hidden = LineInfo { size: Some(0.0), hidden: true, outline: 1, ..LineInfo::default() }; let sh = Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap(); for c in 2..=4 { sh.cols.insert(c, hidden); } sh.cols.insert(5, LineInfo { collapsed: true, ..LineInfo::default() }); s.execute("data.showDetail", json!({"cols": "C:E", "level": 1})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); for c in 2..=4 { assert_eq!(sh.col_width(c), sh.default_col_width, "column {c}"); } // Hiding again hides them; the level buttons show zero-width columns too. s.execute("data.hideDetail", json!({"cols": "C:E", "level": 1})).unwrap(); assert_eq!(s.doc().unwrap().wb.active().unwrap().col_width(2), 0.0); Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap().cols.insert(3, hidden); s.execute("data.showLevels", json!({"cols": 2})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!((sh.col_width(2), sh.col_width(3)), (sh.default_col_width, sh.default_col_width)); } #[test] fn removing_an_autofilter_keeps_manually_hidden_and_collapsed_rows() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["H"], ["a"], ["b"], ["c"]]})).unwrap(); s.execute("range.setValues", json!({"range": "A10", "values": [["z"]]})).unwrap(); s.execute("data.group", json!({"rows": "2:4"})).unwrap(); s.execute("data.hideDetail", json!({"rows": "2:4"})).unwrap(); s.execute("home.hideRows", json!({"rows": "10:10"})).unwrap(); let hidden = |s: &Session| -> Vec { let sh = s.doc().unwrap().wb.active().unwrap(); sh.rows.iter().filter(|(_, l)| l.hidden).map(|(r, _)| *r).collect() }; let before = hidden(&s); assert_eq!(before, [1, 2, 3, 9]); s.execute("data.filter", json!({"range": "A1:A4"})).unwrap(); s.execute("data.filter", json!({"range": "A1:A4"})).unwrap(); assert!(s.doc().unwrap().wb.active().unwrap().autofilter.is_none()); assert_eq!(hidden(&s), before); assert_eq!(row_state(&s, 4..=4), [(false, true)]); } #[test] fn table_dropdown_setup_outside_the_table_keeps_manually_hidden_rows() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["H"], ["a"], ["b"], ["c"]]})).unwrap(); s.execute("range.setValues", json!({"range": "A10", "values": [["z"]]})).unwrap(); s.execute("insert.table", json!({"range": "A1:A4", "header": true})).unwrap(); s.execute("home.hideRows", json!({"rows": "3:3"})).unwrap(); s.execute("home.hideRows", json!({"rows": "10:10"})).unwrap(); s.execute("selection.set", json!({"range": "E1"})).unwrap(); // The table filter dropdown installs an AutoFilter on demand by toggling `data.filter` twice. s.execute("data.filter", json!({"range": "A1:A4"})).unwrap(); s.execute("data.filter", json!({"range": "A1:A4"})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.rows.iter().filter(|(_, l)| l.hidden).map(|(r, _)| *r).collect::>(), [2, 9]); } /// Rows 5:8 grouped, with 6:7 grouped again inside (0-based 4..=7 and 5..=6). fn nested_rows() -> Session { let mut s = Session::new(); s.new_workbook(); s.execute("data.group", json!({"rows": "5:8"})).unwrap(); s.execute("data.group", json!({"rows": "6:7"})).unwrap(); s } /// (hidden, collapsed) of 0-based rows. fn row_state(s: &Session, rows: std::ops::RangeInclusive) -> Vec<(bool, bool)> { let sh = s.doc().unwrap().wb.active().unwrap(); rows.map(|r| sh.rows.get(&r).map(|l| (l.hidden, l.collapsed)).unwrap_or_default()).collect() } #[test] fn hide_detail_marks_the_summary_collapsed_and_show_detail_keeps_inner_groups_collapsed() { let mut s = nested_rows(); s.execute("data.hideDetail", json!({"rows": "6:7", "level": 2})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(false, false), (true, false), (true, false), (false, true), (false, false)]); s.execute("data.hideDetail", json!({"rows": "5:8", "level": 1})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(true, false), (true, false), (true, false), (true, true), (false, true)]); s.execute("data.showDetail", json!({"rows": "5:8", "level": 1})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(false, false), (true, false), (true, false), (false, true), (false, false)]); s.execute("data.showDetail", json!({"rows": "6:7", "level": 2})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(false, false); 5]); // Each step is one undoable command. s.execute("edit.undo", json!({})).unwrap(); assert_eq!(row_state(&s, 5..=7), [(true, false), (true, false), (false, true)]); } #[test] fn hide_detail_on_the_summary_row_collapses_the_group_above_it() { let mut s = nested_rows(); s.execute("data.hideDetail", json!({"rows": "9:9"})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(true, false), (true, false), (true, false), (true, false), (false, true)]); } #[test] fn show_levels_collapses_and_expands_to_a_level() { let mut s = nested_rows(); s.execute("data.showLevels", json!({"rows": 1})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(true, false), (true, false), (true, false), (true, true), (false, true)]); s.execute("data.showLevels", json!({"rows": 2})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(false, false), (true, false), (true, false), (false, true), (false, false)]); s.execute("data.showLevels", json!({"rows": 3})).unwrap(); assert_eq!(row_state(&s, 4..=8), [(false, false); 5]); // Columns are separate; no level at all is an error rather than a guess. s.execute("data.group", json!({"cols": "C:E"})).unwrap(); s.execute("data.showLevels", json!({"cols": 1})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert!((2..=4).all(|c| sh.cols.get(&c).is_some_and(|l| l.hidden))); assert!(sh.cols.get(&5).is_some_and(|l| l.collapsed)); assert_eq!(row_state(&s, 4..=4), [(false, false)]); assert!(s.execute("data.showLevels", json!({})).is_err()); } #[test] fn group_by_columns_for_a_block_of_cells() { let mut s = Session::new(); s.new_workbook(); s.execute("selection.set", json!({"range": "B2:C5"})).unwrap(); s.execute("data.group", json!({"by": "cols"})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert!((1..=2).all(|c| sh.cols.get(&c).is_some_and(|l| l.outline == 1))); assert!(sh.rows.values().all(|l| l.outline == 0)); } #[test] fn group_shortcuts_are_the_windows_ones() { let sc = |id: &str| specs().into_iter().find(|c| c.id == id).and_then(|c| c.shortcut); assert_eq!(sc("data.group"), Some("Alt+Shift+Right")); assert_eq!(sc("data.ungroup"), Some("Alt+Shift+Left")); } #[test] fn summary_rows_above_the_detail_take_the_collapsed_mark() { let mut s = nested_rows(); let r = s.execute("data.outlineSettings", json!({"summaryBelow": false})).unwrap(); assert_eq!(r, json!({"summaryBelow": false, "summaryRight": true})); // Row 4 (index 3) is the outer group's summary, row 5 the inner one's. s.execute("data.hideDetail", json!({"rows": "5:8", "level": 1})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, true), (true, false), (true, false), (true, false), (true, false), (false, false)]); s.execute("data.showDetail", json!({"rows": "5:8", "level": 1})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, false); 6]); // Hide Detail on the summary row collapses the group below it. s.execute("data.hideDetail", json!({"rows": "4:4"})).unwrap(); assert_eq!(row_state(&s, 3..=4), [(false, true), (true, false)]); s.execute("data.showLevels", json!({"rows": 1})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, true), (true, true), (true, false), (true, false), (true, false), (false, false)]); // Ungrouping the outer group clears its mark on the row above. s.execute("data.showLevels", json!({"rows": 3})).unwrap(); s.execute("data.hideDetail", json!({"rows": "5:8", "level": 1})).unwrap(); s.execute("data.ungroup", json!({"rows": "5:8"})).unwrap(); assert_eq!(row_state(&s, 3..=3), [(false, false)]); } #[test] fn changing_the_summary_direction_moves_collapsed_marks() { let mut s = nested_rows(); s.execute("data.group", json!({"cols": "C:E"})).unwrap(); s.execute("data.hideDetail", json!({"rows": "5:8", "level": 1})).unwrap(); s.execute("data.hideDetail", json!({"cols": "C:E"})).unwrap(); s.execute("data.outlineSettings", json!({"summaryBelow": false, "summaryRight": false})).unwrap(); let col_marked = |s: &Session, c: u32| s.doc().unwrap().wb.active().unwrap().cols.get(&c).is_some_and(|l| l.collapsed); assert_eq!(row_state(&s, 3..=8).iter().map(|r| r.1).collect::>(), [true, false, false, false, false, false]); assert!(col_marked(&s, 1) && !col_marked(&s, 5)); s.execute("data.outlineSettings", json!({"summaryBelow": true})).unwrap(); assert_eq!(row_state(&s, 3..=8).iter().map(|r| r.1).collect::>(), [false, false, false, false, false, true]); assert!(col_marked(&s, 1), "columns keep their direction"); // One undo step per change; no params leaves the settings as they are. s.execute("edit.undo", json!({})).unwrap(); assert!(row_state(&s, 3..=3)[0].1); assert_eq!(s.execute("data.outlineSettings", json!({})).unwrap(), json!({"summaryBelow": false, "summaryRight": false})); // Nested groups that end on one row share its mark: it moves to the outer group only. let mut s = Session::new(); s.new_workbook(); s.execute("data.group", json!({"rows": "5:8"})).unwrap(); s.execute("data.group", json!({"rows": "6:8"})).unwrap(); s.execute("data.showLevels", json!({"rows": 1})).unwrap(); s.execute("data.outlineSettings", json!({"summaryBelow": false})).unwrap(); assert_eq!(row_state(&s, 3..=8).iter().map(|r| r.1).collect::>(), [true, false, false, false, false, false]); s.execute("data.showDetail", json!({"rows": "4:4"})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, false); 6]); // With only the inner group collapsed, its mark goes to row 5, and the outer group's // Hide and Show Detail keep it collapsed. s.execute("data.outlineSettings", json!({"summaryBelow": true})).unwrap(); s.execute("data.hideDetail", json!({"rows": "6:8", "level": 2})).unwrap(); s.execute("data.outlineSettings", json!({"summaryBelow": false})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, false), (false, true), (true, false), (true, false), (true, false), (false, false)]); s.execute("data.hideDetail", json!({"rows": "5:8", "level": 1})).unwrap(); s.execute("data.showDetail", json!({"rows": "5:8", "level": 1})).unwrap(); assert_eq!(row_state(&s, 3..=8), [(false, false), (false, true), (true, false), (true, false), (true, false), (false, false)]); // A group on the first row has no row above it to mark. let mut s = Session::new(); s.new_workbook(); s.execute("data.group", json!({"rows": "1:3"})).unwrap(); s.execute("data.outlineSettings", json!({"summaryBelow": false})).unwrap(); s.execute("data.hideDetail", json!({"rows": "1:3"})).unwrap(); assert_eq!(row_state(&s, 0..=3), [(true, false), (true, false), (true, false), (false, false)]); } #[test] fn column_sort_rejects_row_key_zero_without_panicking() { let mut s = Session::new(); s.new_workbook(); s.execute("cell.set", json!({"cell": "A1", "input": "2"})).unwrap(); for row in [0u64, 1 << 32] { let params = json!({"range": "A1:B2", "header": false, "orientation": "columns", "keys": [{"row": row}]}); let err = s.execute("data.sort", params).unwrap_err(); assert!(matches!(err, EngineError::BadParams { .. }), "row {row}: {err:?}"); } } #[test] fn single_row_and_column_parameters() { let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [[1, 2], [3, 4]]})).unwrap(); s.execute("home.insertRows", json!({"rows": "2"})).unwrap(); s.execute("home.insertColumns", json!({"cols": "B"})).unwrap(); let v = |s: &mut Session, a: &str| s.execute("cell.get", json!({"cell": a})).unwrap()["value"].clone(); assert_eq!(v(&mut s, "C3"), json!(4.0)); s.execute("home.hideRows", json!({"rows": "1"})).unwrap(); s.execute("data.group", json!({"cols": "C"})).unwrap(); assert!(s.execute("home.deleteRows", json!({"rows": "0"})).is_err()); s.execute("home.deleteRows", json!({"rows": "2"})).unwrap(); s.execute("home.deleteColumns", json!({"cols": "B"})).unwrap(); assert_eq!(v(&mut s, "B2"), json!(4.0)); } #[test] fn t2c_fixed_widths_are_positions_in_any_order_without_duplicates() { let cases: [(Json, [&str; 4]); 5] = [ (json!([6, 3]), ["abc", "def", "ghij", ""]), (json!([3, 3]), ["abc", "defghij", "", ""]), (json!([0]), ["abcdefghij", "", "", ""]), (json!([20]), ["abcdefghij", "", "", ""]), (json!([7, 0, 3, 7, 10]), ["abc", "defg", "hij", ""]), ]; for (widths, want) in cases { let mut s = Session::new(); s.new_workbook(); s.execute("cell.set", json!({"cell": "A1", "input": "abcdefghij"})).unwrap(); s.execute("data.textToColumns", json!({"range": "A1:A1", "fixedWidths": widths})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); let got: Vec = (0..4).map(|c| sh.value(CellRef::new(0, c)).display()).collect(); assert_eq!(got, want, "{widths}"); } } #[test] fn split() { assert_eq!(split_delimited("a,b,,c", &[','], false, '"'), ["a", "b", "", "c"]); assert_eq!(split_delimited("a,b,,c", &[','], true, '"'), ["a", "b", "c"]); assert_eq!(split_delimited("\"x,y\",z", &[','], false, '"'), ["x,y", "z"]); } #[test] fn remove_duplicates_without_a_valid_column_is_refused() { // `columns: []` (or only unusable names) used to compare nothing and delete every row // but the first. let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "A1", "values": [["a"], ["b"], ["c"]]})).unwrap(); for columns in [json!([]), json!(["ZZZ"])] { let err = s.execute("data.removeDuplicates", json!({"range": "A1:A3", "columns": columns})).unwrap_err(); assert!(err.to_string().contains("columns"), "{err}"); } let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(0, 0)), Value::text("a")); assert_eq!(sh.value(CellRef::new(2, 0)), Value::text("c")); } #[test] fn a_list_typed_without_quotes_is_saved_as_a_quoted_list() { // In a file an inline list is a string literal (`"a,b"`); unquoted it reads as a formula. // References, names and formulas stay unquoted, and every rule survives a save and reopen. let mut s = Session::new(); s.new_workbook(); s.execute("range.setValues", json!({"range": "K1:K3", "values": [["Red"], ["Green"], ["Blue"]]})).unwrap(); s.execute("formulas.defineName", json!({"name": "Colors", "refersTo": "=Sheet1!$K$1:$K$3"})).unwrap(); let rules = [ ("A1", "Open,In progress,Closed", r#""Open,In progress,Closed""#), ("B1", r#""Open,In progress,Closed""#, r#""Open,In progress,Closed""#), ("C1", " 1,2,3 ", r#""1,2,3""#), ("D1", r#"Say "hi",Bye"#, r#""Say ""hi"",Bye""#), ("E1", "=$K$1:$K$3", "$K$1:$K$3"), ("F1", "$K$1:$K$3", "$K$1:$K$3"), ("G1", "=Colors", "Colors"), ("H1", "Sheet1!$K$1:$K$3", "Sheet1!$K$1:$K$3"), ("I1", "=OFFSET($K$1,0,0,3,1)", "OFFSET($K$1,0,0,3,1)"), ]; for (cell, source, _) in rules { s.execute("data.validation", json!({"range": cell, "type": "list", "formula1": source})).unwrap(); } let accepted = [ ("A1", "In progress"), ("B1", "Closed"), ("C1", "2"), ("D1", r#"Say "hi""#), ("E1", "Green"), ("F1", "Blue"), ("G1", "Red"), ("H1", "Green"), ]; let check = |wb: &Workbook| { for (cell, input) in accepted { let at = CellRef::parse(cell).unwrap(); assert!(check_validation(wb, 0, at, input).is_none(), "{cell} refuses {input}"); assert!(check_validation(wb, 0, at, "Pending").is_some(), "{cell} accepts Pending"); } }; let wb = s.doc().unwrap().wb.clone(); check(&wb); let sheet_xml = |wb: &Workbook| { let bytes = gridcraft_xlsx::write_xlsx(wb).unwrap(); let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap(); let mut xml = String::new(); std::io::Read::read_to_string(&mut zip.by_name("xl/worksheets/sheet1.xml").unwrap(), &mut xml).unwrap(); xml }; let xml = sheet_xml(&wb); for (cell, _, saved) in rules { assert!(xml.contains(&format!(r#"sqref="{cell}">{saved}"#)), "{cell}: {saved} missing from {xml}"); } let (reopened, _) = gridcraft_xlsx::read_xlsx(&gridcraft_xlsx::write_xlsx(&wb).unwrap()).unwrap(); check(&reopened); assert_eq!(sheet_xml(&reopened), xml, "a second save writes the same rules"); } #[test] fn a_rule_with_an_unknown_type_operator_or_error_style_is_refused() { // A typo used to give a rule that validates nothing (`type="none"`) or the wrong check, // with no error. let mut s = Session::new(); s.new_workbook(); for params in [ json!({"range": "A1", "type": "lst", "formula1": "\"a,b\""}), json!({"range": "A1", "type": "whole", "operator": "greaterthan", "formula1": "1"}), json!({"range": "A1", "type": "whole", "operator": ">", "formula1": "1"}), json!({"range": "A1", "type": "list", "formula1": "\"a,b\"", "errorStyle": "warn"}), ] { let e = s.execute("data.validation", params.clone()).unwrap_err().to_string(); assert!(e.contains("unknown"), "{params}: {e}"); } assert!(s.doc().unwrap().wb.active().unwrap().validations.is_empty()); for params in [ json!({"range": "A1", "type": "any"}), json!({"range": "A2", "type": "whole", "operator": "greaterThan", "formula1": "1"}), json!({"range": "A3", "type": "list", "formula1": "\"a,b\"", "errorStyle": "warning"}), ] { s.execute("data.validation", params).unwrap(); } let sh = s.doc().unwrap().wb.active().unwrap().clone(); assert_eq!(sh.validations.len(), 3); assert!(check_validation(&s.doc().unwrap().wb, 0, CellRef::parse("A2").unwrap(), "0").is_some()); }