mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-02 16:30:50 -06:00
Merge g3 into g4
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/edit.rs # crates/engine/src/cmd/formulas.rs # crates/engine/src/cmd/inspect.rs # crates/engine/src/cmd/pivot.rs # crates/ui-egui/src/lib.rs # crates/ui-egui/src/tabs.rs # docs/gaps.md # docs/ui-parity.md
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
//! The 100% stacked area chart: `insert.chart` and `chart.set` take type `area` with subtype
|
||||
//! `stacked100`.
|
||||
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_model::ChartKind;
|
||||
use serde_json::json;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", "10", "30"], ["Q2", "30", "10"]]})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn kinds(s: &Session) -> Vec<ChartKind> {
|
||||
s.doc().unwrap().wb.sheet(0).unwrap().charts.iter().map(|c| c.kind).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_chart_makes_a_hundred_percent_stacked_area() {
|
||||
let mut s = session();
|
||||
s.execute("insert.chart", json!({"range": "A1:C3", "type": "area", "subtype": "stacked100"})).unwrap();
|
||||
assert_eq!(kinds(&s), [ChartKind::AreaStacked100]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chart_set_changes_a_chart_to_a_hundred_percent_stacked_area_and_back() {
|
||||
let mut s = session();
|
||||
s.execute("insert.chart", json!({"range": "A1:C3", "type": "area", "subtype": "stacked"})).unwrap();
|
||||
assert_eq!(kinds(&s), [ChartKind::AreaStacked]);
|
||||
s.execute("chart.set", json!({"type": "area", "subtype": "stacked100"})).unwrap();
|
||||
assert_eq!(kinds(&s), [ChartKind::AreaStacked100]);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(kinds(&s), [ChartKind::AreaStacked]);
|
||||
}
|
||||
+3
-3
@@ -23,7 +23,7 @@
|
||||
//!
|
||||
//! Knobs (environment variables):
|
||||
//! - `GRIDCRAFT_FUZZ_SEEDS` (default 6) and `GRIDCRAFT_FUZZ_STEPS` (default 40): the budget. Soak with
|
||||
//! `GRIDCRAFT_FUZZ_SEEDS=200 GRIDCRAFT_FUZZ_STEPS=200 cargo test --release -p gridcraft-engine --test command_sequence_fuzz`.
|
||||
//! `GRIDCRAFT_FUZZ_SEEDS=200 GRIDCRAFT_FUZZ_STEPS=200 cargo test --release -p gridcraft-engine --test it command_sequence_fuzz::`.
|
||||
//! - `GRIDCRAFT_FUZZ_SEED=17`: replay that one seed (the failure message prints the command line).
|
||||
//! - `GRIDCRAFT_FUZZ_SHRINK=1`: on failure, shrink the sequence to a minimal list of commands that
|
||||
//! still fails the same way, printed as `execute` calls ready for a regression test.
|
||||
@@ -139,7 +139,7 @@ fn report(seed: u64, steps: usize, sample: &str, sequence: &[Step], f: &Failure)
|
||||
None => format!("end-of-run checks after {} steps", sequence.len()),
|
||||
};
|
||||
let mut out = format!(
|
||||
"command-sequence fuzz failure: seed {seed} (sample `{sample}`), {at}\n invariant [{}]: {}\n replay: GRIDCRAFT_FUZZ_SEED={seed} GRIDCRAFT_FUZZ_STEPS={steps} cargo test -p gridcraft-engine --test command_sequence_fuzz\n",
|
||||
"command-sequence fuzz failure: seed {seed} (sample `{sample}`), {at}\n invariant [{}]: {}\n replay: GRIDCRAFT_FUZZ_SEED={seed} GRIDCRAFT_FUZZ_STEPS={steps} cargo test -p gridcraft-engine --test it command_sequence_fuzz::\n",
|
||||
f.kind, f.what
|
||||
);
|
||||
let end = (f.step + 1).min(sequence.len());
|
||||
@@ -233,7 +233,7 @@ fn guarded<T>(step: usize, kind: &'static str, f: impl FnOnce() -> Result<T, Str
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(e)) => Err(fail(step, kind, e)),
|
||||
Err(p) => {
|
||||
let msg = p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||
let msg = p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||
Err(fail(step, kind, format!("panicked: {msg}")))
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ fn opened() -> Session {
|
||||
let bytes = io::save_bytes(&wb, "dynamic.xlsx").unwrap();
|
||||
let (back, _) = io::open_bytes("dynamic.xlsx", &bytes).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.add_document(DocState::new(back, None, "dynamic".into()));
|
||||
s.add_document(DocState::new(back, None, "dynamic".into(), gridcraft_engine::Trust::FromFile));
|
||||
s
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//! A font a formatting command derives from the theme's body font keeps following it (#2517).
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use gridcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
fn styles_xml(session: &Session) -> String {
|
||||
let bytes = gridcraft_xlsx::write_xlsx(&session.doc().unwrap().wb).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name("xl/styles.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derived_body_font_keeps_minor_scheme() {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
session.execute("home.bold", json!({"range": "A1"})).unwrap();
|
||||
session.execute("home.fontSize", json!({"range": "B1", "size": 14})).unwrap();
|
||||
let xml = styles_xml(&session);
|
||||
let fonts = xml.split("<fonts").nth(1).unwrap().split("</fonts>").next().unwrap();
|
||||
let fonts: Vec<&str> = fonts.split("<font>").skip(1).collect();
|
||||
assert!(fonts.len() >= 3, "{xml}");
|
||||
for f in &fonts {
|
||||
assert!(f.contains("<scheme val=\"minor\"/>"), "every body-font font follows the theme: {f}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use gridcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
fn table() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("range.setValues", json!({"range":"A1", "values":[["Item", "Qty"], ["x", "7"], ["y", "8"]]})).unwrap();
|
||||
s.execute("insert.table", json!({"range":"A1:B3", "header":true, "name":"T"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn styles(s: &Session) -> Vec<String> {
|
||||
s.doc().unwrap().wb.active().unwrap().tables.iter().map(|t| t.style.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_inside_a_table_restyles_it() {
|
||||
let mut s = table();
|
||||
s.execute("selection.set", json!({"range":"B2"})).unwrap();
|
||||
s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap();
|
||||
assert_eq!(styles(&s), ["TableStyleLight9"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_on_a_range_inside_a_table_restyles_it() {
|
||||
let mut s = table();
|
||||
s.execute("home.formatAsTable", json!({"range":"A2:B3", "style":"TableStyleDark3"})).unwrap();
|
||||
assert_eq!(styles(&s), ["TableStyleDark3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_across_a_table_edge_is_rejected() {
|
||||
let mut s = table();
|
||||
let e = s.execute("home.formatAsTable", json!({"range":"A1:C3", "style":"TableStyleLight9"})).unwrap_err();
|
||||
assert!(e.to_string().contains("overlap"), "{e}");
|
||||
assert_eq!(styles(&s), ["TableStyleMedium2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_inside_a_table_returns_that_table() {
|
||||
let mut s = table();
|
||||
let r = s.execute("home.formatAsTable", json!({"range":"B2", "style":"TableStyleLight9"})).unwrap();
|
||||
assert_eq!(r, json!({"table":"T", "range":"A1:B3"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_uses_the_active_area_of_a_multi_area_selection() {
|
||||
let mut s = table();
|
||||
s.execute("selection.set", json!({"range":"D10:E11,B2"})).unwrap();
|
||||
s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap();
|
||||
assert_eq!(styles(&s), ["TableStyleLight9"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_as_table_inside_a_table_with_data_beside_it_restyles_it() {
|
||||
let mut s = table();
|
||||
s.execute("range.setValues", json!({"range":"C1", "values":[["Note"], ["a"], ["b"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"range":"B2"})).unwrap();
|
||||
s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap();
|
||||
assert_eq!(styles(&s), ["TableStyleLight9"]);
|
||||
}
|
||||
+1
-1
@@ -78,7 +78,7 @@ fn longest_flat_chains_survive_editing_saving_and_reopening() {
|
||||
let bytes = io::save_bytes(wb, "chains.xlsx").expect("save");
|
||||
let (reopened, _) = io::open_bytes("chains.xlsx", &bytes).expect("open");
|
||||
let mut session = Session::new();
|
||||
session.add_document(DocState::new(reopened, None, "chains".into()));
|
||||
session.add_document(DocState::new(reopened, None, "chains".into(), gridcraft_engine::Trust::FromFile));
|
||||
session.execute("formulas.calculateNow", json!({})).expect("recalc");
|
||||
assert_eq!(value(&session, "C2"), Value::Number(4096.0));
|
||||
assert_eq!(value(&session, "C3"), Value::Number(-4094.0));
|
||||
@@ -0,0 +1,31 @@
|
||||
//! The crate's integration tests, one module per file, built as one test binary.
|
||||
|
||||
mod autofit_filter_buttons;
|
||||
mod cf_formula_validate;
|
||||
mod chart_area_stacked100;
|
||||
mod chart_workbook_names;
|
||||
mod command_sequence_fuzz;
|
||||
mod cross_sheet_cut;
|
||||
mod csv_export_budget;
|
||||
mod custom_filter_blanks;
|
||||
mod custom_view_freeze;
|
||||
mod dynamicmetadata;
|
||||
mod extreme_selection_moves;
|
||||
mod font_scheme_minor;
|
||||
mod format_as_table_restyle;
|
||||
mod hidden_sheet_navigation;
|
||||
mod ink_roundtrip;
|
||||
mod line_count_overflow;
|
||||
mod long_operator_chains;
|
||||
mod offset_overflow;
|
||||
mod percent_entry;
|
||||
mod pivot_unknown_func;
|
||||
mod pivotchart_link;
|
||||
mod range_fill_formats;
|
||||
mod row_autofit;
|
||||
mod sheet_name_quoting;
|
||||
mod sparse_clear_invalidation;
|
||||
mod stored_list_shape;
|
||||
mod table_resize_overlap;
|
||||
mod table_total_row_last_row;
|
||||
mod wrap_row_height;
|
||||
+2
-1
@@ -6,7 +6,8 @@ const NOT_OBJECTS: [&str; 4] = ["\"[1,2]\"", "\"7\"", "\"true\"", "\"\"\"hi\"\"\
|
||||
fn with_name(name: &str, refers_to: &str) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
s.execute("formulas.defineName", json!({"name": name, "refersTo": refers_to})).unwrap();
|
||||
// `force`: the name commands refuse GridCraft's own data names otherwise.
|
||||
s.execute("formulas.defineName", json!({"name": name, "refersTo": refers_to, "force": true})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user