diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 50865a2b7..5365d1403 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -482,6 +482,10 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) { } let mut need: f32 = default; for (c, cell) in sh.cells.row(row, 0, gridcraft_core::MAX_COLS - 1) { + // Merged cells never auto-fit their rows, like Excel (column autofit skips them too). + if sh.merge_at(CellRef::new(row, c)).is_some_and(|m| !m.is_single()) { + continue; + } let st = wb.styles.get(cell.style); let size = st.font.size; let line = (size * 1.8).round().max(default * size / gridcraft_model::DEFAULT_FONT_SIZE); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 07656aacb..2da46caed 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1166,6 +1166,33 @@ fn open_leaves_plain_workbooks_untouched() { assert!(sh.rows.values().all(|i| i.size.is_none()), "no row gains a height entry"); } +/// Merged cells never auto-fit their rows, like Excel (issue #1605): a wrapped merged +/// header keeps the default height instead of growing to its column-A wrap width, while an +/// unmerged wrapped cell in the same sheet still fits. +#[test] +fn autofit_skips_merged_cells() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "This is a long sentence that should wrap across the merged width of five columns"})) + .unwrap(); + s.execute("home.mergeCells", json!({"range": "A1:E1"})).unwrap(); + s.execute("home.wrapText", json!({"range": "A1:E1", "on": true})).unwrap(); + s.execute("cell.set", json!({"cell": "A3", "input": "This is a long sentence that should wrap across the merged width of five columns"})) + .unwrap(); + s.execute("home.wrapText", json!({"range": "A3", "on": true})).unwrap(); + let sh = s.doc().unwrap().wb.active().unwrap(); + assert!(sh.rows.get(&0).is_none_or(|i| i.size.is_none()), "merged wrap must not auto-fit row 1, got {:?}", sh.rows.get(&0)); + assert!(sh.rows.get(&2).is_some_and(|i| i.size.is_some_and(|h| h > 15.0)), "unmerged wrap still fits its row"); + // The save writes no height for the merged row. + let r = s.execute("file.saveBytes", json!({"format": "xlsx"})).unwrap(); + let bytes = crate::io::base64_decode(r["base64"].as_str().unwrap()).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(); + let row1 = xml.split("').next().unwrap().contains("ht="), "merged row 1 must be saved without ht, got {row1}"); + assert!(xml.contains(""), "the merge itself is saved"); +} + #[test] fn tables_and_structured_refs() { let mut s = s();