1use sheetkit_xml::content_types::{mime_types, ContentTypeOverride, ContentTypes};
7use sheetkit_xml::relationships::{rel_types, Relationship, Relationships};
8use sheetkit_xml::workbook::{SheetEntry, WorkbookXml};
9use sheetkit_xml::worksheet::{
10 Pane, Selection, SheetFormatPr, SheetPr, SheetProtection, SheetView, SheetViews, TabColor,
11 WorksheetXml,
12};
13
14use crate::error::{Error, Result};
15use crate::protection::legacy_password_hash;
16use crate::utils::cell_ref::cell_name_to_coordinates;
17use crate::utils::constants::{
18 DEFAULT_ROW_HEIGHT, MAX_COLUMN_WIDTH, MAX_ROW_HEIGHT, MAX_SHEET_NAME_LENGTH,
19 SHEET_NAME_INVALID_CHARS,
20};
21
22pub fn validate_sheet_name(name: &str) -> Result<()> {
30 if name.is_empty() {
31 return Err(Error::InvalidSheetName("sheet name cannot be empty".into()));
32 }
33 if name.len() > MAX_SHEET_NAME_LENGTH {
34 return Err(Error::InvalidSheetName(format!(
35 "sheet name '{}' exceeds {} characters",
36 name, MAX_SHEET_NAME_LENGTH
37 )));
38 }
39 for ch in SHEET_NAME_INVALID_CHARS {
40 if name.contains(*ch) {
41 return Err(Error::InvalidSheetName(format!(
42 "sheet name '{}' contains invalid character '{}'",
43 name, ch
44 )));
45 }
46 }
47 if name.starts_with('\'') || name.ends_with('\'') {
48 return Err(Error::InvalidSheetName(format!(
49 "sheet name '{}' cannot start or end with a single quote",
50 name
51 )));
52 }
53 Ok(())
54}
55
56pub fn next_rid(existing_rels: &[Relationship]) -> String {
60 let max = existing_rels
61 .iter()
62 .filter_map(|r| r.id.strip_prefix("rId").and_then(|n| n.parse::<u32>().ok()))
63 .max()
64 .unwrap_or(0);
65 format!("rId{}", max + 1)
66}
67
68pub fn next_sheet_id(existing_sheets: &[SheetEntry]) -> u32 {
73 existing_sheets
74 .iter()
75 .map(|s| s.sheet_id)
76 .max()
77 .unwrap_or(0)
78 + 1
79}
80
81pub fn find_sheet_index(worksheets: &[(String, WorksheetXml)], name: &str) -> Option<usize> {
83 worksheets.iter().position(|(n, _)| n == name)
84}
85
86pub fn add_sheet(
91 workbook_xml: &mut WorkbookXml,
92 workbook_rels: &mut Relationships,
93 content_types: &mut ContentTypes,
94 worksheets: &mut Vec<(String, WorksheetXml)>,
95 name: &str,
96 worksheet_data: WorksheetXml,
97) -> Result<usize> {
98 validate_sheet_name(name)?;
99
100 if worksheets.iter().any(|(n, _)| n == name) {
101 return Err(Error::SheetAlreadyExists {
102 name: name.to_string(),
103 });
104 }
105
106 let rid = next_rid(&workbook_rels.relationships);
107 let sheet_id = next_sheet_id(&workbook_xml.sheets.sheets);
108 let sheet_number = worksheets.len() + 1;
109 let target = format!("worksheets/sheet{}.xml", sheet_number);
110
111 workbook_xml.sheets.sheets.push(SheetEntry {
112 name: name.to_string(),
113 sheet_id,
114 state: None,
115 r_id: rid.clone(),
116 });
117
118 workbook_rels.relationships.push(Relationship {
119 id: rid,
120 rel_type: rel_types::WORKSHEET.to_string(),
121 target: target.clone(),
122 target_mode: None,
123 });
124
125 content_types.overrides.push(ContentTypeOverride {
126 part_name: format!("/xl/{}", target),
127 content_type: mime_types::WORKSHEET.to_string(),
128 });
129
130 worksheets.push((name.to_string(), worksheet_data));
131
132 Ok(worksheets.len() - 1)
133}
134
135pub fn delete_sheet(
139 workbook_xml: &mut WorkbookXml,
140 workbook_rels: &mut Relationships,
141 content_types: &mut ContentTypes,
142 worksheets: &mut Vec<(String, WorksheetXml)>,
143 name: &str,
144) -> Result<()> {
145 let idx = find_sheet_index(worksheets, name).ok_or_else(|| Error::SheetNotFound {
146 name: name.to_string(),
147 })?;
148
149 if worksheets.len() <= 1 {
150 return Err(Error::InvalidSheetName(
151 "cannot delete the last sheet in a workbook".into(),
152 ));
153 }
154
155 let r_id = workbook_xml.sheets.sheets[idx].r_id.clone();
156
157 worksheets.remove(idx);
158 workbook_xml.sheets.sheets.remove(idx);
159 workbook_rels.relationships.retain(|r| r.id != r_id);
160
161 rebuild_content_type_overrides(content_types, worksheets.len());
162 rebuild_worksheet_relationships(workbook_xml, workbook_rels);
163
164 Ok(())
165}
166
167pub fn rename_sheet(
169 workbook_xml: &mut WorkbookXml,
170 worksheets: &mut [(String, WorksheetXml)],
171 old_name: &str,
172 new_name: &str,
173) -> Result<()> {
174 validate_sheet_name(new_name)?;
175
176 let idx = find_sheet_index(worksheets, old_name).ok_or_else(|| Error::SheetNotFound {
177 name: old_name.to_string(),
178 })?;
179
180 if worksheets.iter().any(|(n, _)| n == new_name) {
181 return Err(Error::SheetAlreadyExists {
182 name: new_name.to_string(),
183 });
184 }
185
186 worksheets[idx].0 = new_name.to_string();
187 workbook_xml.sheets.sheets[idx].name = new_name.to_string();
188
189 Ok(())
190}
191
192pub fn copy_sheet(
194 workbook_xml: &mut WorkbookXml,
195 workbook_rels: &mut Relationships,
196 content_types: &mut ContentTypes,
197 worksheets: &mut Vec<(String, WorksheetXml)>,
198 source_name: &str,
199 target_name: &str,
200) -> Result<usize> {
201 let source_idx =
202 find_sheet_index(worksheets, source_name).ok_or_else(|| Error::SheetNotFound {
203 name: source_name.to_string(),
204 })?;
205
206 let cloned_data = worksheets[source_idx].1.clone();
207
208 add_sheet(
209 workbook_xml,
210 workbook_rels,
211 content_types,
212 worksheets,
213 target_name,
214 cloned_data,
215 )
216}
217
218pub fn active_sheet_index(workbook_xml: &WorkbookXml) -> usize {
220 workbook_xml
221 .book_views
222 .as_ref()
223 .and_then(|bv| bv.workbook_views.first())
224 .and_then(|v| v.active_tab)
225 .unwrap_or(0) as usize
226}
227
228pub fn set_active_sheet_index(workbook_xml: &mut WorkbookXml, index: u32) {
230 use sheetkit_xml::workbook::{BookViews, WorkbookView};
231
232 let book_views = workbook_xml.book_views.get_or_insert_with(|| BookViews {
233 workbook_views: vec![WorkbookView {
234 x_window: None,
235 y_window: None,
236 window_width: None,
237 window_height: None,
238 active_tab: Some(0),
239 }],
240 });
241
242 if let Some(view) = book_views.workbook_views.first_mut() {
243 view.active_tab = Some(index);
244 }
245}
246
247#[derive(Debug, Clone, Default)]
253pub struct SheetProtectionConfig {
254 pub password: Option<String>,
256 pub select_locked_cells: bool,
258 pub select_unlocked_cells: bool,
260 pub format_cells: bool,
262 pub format_columns: bool,
264 pub format_rows: bool,
266 pub insert_columns: bool,
268 pub insert_rows: bool,
270 pub insert_hyperlinks: bool,
272 pub delete_columns: bool,
274 pub delete_rows: bool,
276 pub sort: bool,
278 pub auto_filter: bool,
280 pub pivot_tables: bool,
282}
283
284pub fn protect_sheet(ws: &mut WorksheetXml, config: &SheetProtectionConfig) -> Result<()> {
290 let hashed = config.password.as_ref().map(|p| {
291 let h = legacy_password_hash(p);
292 format!("{:04X}", h)
293 });
294
295 let to_opt = |v: bool| if v { Some(true) } else { None };
296
297 ws.sheet_protection = Some(SheetProtection {
298 password: hashed,
299 sheet: Some(true),
300 objects: Some(true),
301 scenarios: Some(true),
302 select_locked_cells: to_opt(config.select_locked_cells),
303 select_unlocked_cells: to_opt(config.select_unlocked_cells),
304 format_cells: to_opt(config.format_cells),
305 format_columns: to_opt(config.format_columns),
306 format_rows: to_opt(config.format_rows),
307 insert_columns: to_opt(config.insert_columns),
308 insert_rows: to_opt(config.insert_rows),
309 insert_hyperlinks: to_opt(config.insert_hyperlinks),
310 delete_columns: to_opt(config.delete_columns),
311 delete_rows: to_opt(config.delete_rows),
312 sort: to_opt(config.sort),
313 auto_filter: to_opt(config.auto_filter),
314 pivot_tables: to_opt(config.pivot_tables),
315 });
316
317 Ok(())
318}
319
320pub fn unprotect_sheet(ws: &mut WorksheetXml) -> Result<()> {
322 ws.sheet_protection = None;
323 Ok(())
324}
325
326pub fn is_sheet_protected(ws: &WorksheetXml) -> bool {
328 ws.sheet_protection
329 .as_ref()
330 .and_then(|p| p.sheet)
331 .unwrap_or(false)
332}
333
334pub fn set_tab_color(ws: &mut WorksheetXml, rgb: &str) -> Result<()> {
336 let sheet_pr = ws.sheet_pr.get_or_insert_with(SheetPr::default);
337 sheet_pr.tab_color = Some(TabColor {
338 rgb: Some(rgb.to_string()),
339 theme: None,
340 indexed: None,
341 });
342 Ok(())
343}
344
345pub fn get_tab_color(ws: &WorksheetXml) -> Option<String> {
347 ws.sheet_pr
348 .as_ref()
349 .and_then(|pr| pr.tab_color.as_ref())
350 .and_then(|tc| tc.rgb.clone())
351}
352
353pub fn set_default_row_height(ws: &mut WorksheetXml, height: f64) -> Result<()> {
357 if height > MAX_ROW_HEIGHT {
358 return Err(Error::RowHeightExceeded {
359 height,
360 max: MAX_ROW_HEIGHT,
361 });
362 }
363 let fmt = ws.sheet_format_pr.get_or_insert(SheetFormatPr {
364 default_row_height: DEFAULT_ROW_HEIGHT,
365 default_col_width: None,
366 custom_height: None,
367 outline_level_row: None,
368 outline_level_col: None,
369 });
370 fmt.default_row_height = height;
371 Ok(())
372}
373
374pub fn get_default_row_height(ws: &WorksheetXml) -> f64 {
378 ws.sheet_format_pr
379 .as_ref()
380 .map(|f| f.default_row_height)
381 .unwrap_or(DEFAULT_ROW_HEIGHT)
382}
383
384pub fn set_default_col_width(ws: &mut WorksheetXml, width: f64) -> Result<()> {
388 if width > MAX_COLUMN_WIDTH {
389 return Err(Error::ColumnWidthExceeded {
390 width,
391 max: MAX_COLUMN_WIDTH,
392 });
393 }
394 let fmt = ws.sheet_format_pr.get_or_insert(SheetFormatPr {
395 default_row_height: DEFAULT_ROW_HEIGHT,
396 default_col_width: None,
397 custom_height: None,
398 outline_level_row: None,
399 outline_level_col: None,
400 });
401 fmt.default_col_width = Some(width);
402 Ok(())
403}
404
405pub fn get_default_col_width(ws: &WorksheetXml) -> Option<f64> {
409 ws.sheet_format_pr
410 .as_ref()
411 .and_then(|f| f.default_col_width)
412}
413
414pub fn set_panes(ws: &mut WorksheetXml, cell: &str) -> Result<()> {
423 let (col, row) = cell_name_to_coordinates(cell)?;
424
425 if col == 1 && row == 1 {
426 return Err(Error::InvalidCellReference(
427 "freeze pane at A1 has no effect".to_string(),
428 ));
429 }
430
431 let x_split = col - 1;
432 let y_split = row - 1;
433
434 let active_pane = match (x_split > 0, y_split > 0) {
435 (true, true) => "bottomRight",
436 (true, false) => "topRight",
437 (false, true) => "bottomLeft",
438 (false, false) => unreachable!(),
439 };
440
441 let pane = Pane {
442 x_split: if x_split > 0 { Some(x_split) } else { None },
443 y_split: if y_split > 0 { Some(y_split) } else { None },
444 top_left_cell: Some(cell.to_string()),
445 active_pane: Some(active_pane.to_string()),
446 state: Some("frozen".to_string()),
447 };
448
449 let selection = Selection {
450 pane: Some(active_pane.to_string()),
451 active_cell: Some(cell.to_string()),
452 sqref: Some(cell.to_string()),
453 };
454
455 let sheet_views = ws.sheet_views.get_or_insert_with(|| SheetViews {
456 sheet_views: vec![SheetView {
457 tab_selected: None,
458 show_grid_lines: None,
459 show_formulas: None,
460 show_row_col_headers: None,
461 zoom_scale: None,
462 view: None,
463 top_left_cell: None,
464 workbook_view_id: 0,
465 pane: None,
466 selection: vec![],
467 }],
468 });
469
470 if let Some(view) = sheet_views.sheet_views.first_mut() {
471 view.pane = Some(pane);
472 view.selection = vec![selection];
473 }
474
475 Ok(())
476}
477
478pub fn unset_panes(ws: &mut WorksheetXml) {
480 if let Some(ref mut sheet_views) = ws.sheet_views {
481 for view in &mut sheet_views.sheet_views {
482 view.pane = None;
483 view.selection = vec![];
485 }
486 }
487}
488
489pub fn get_panes(ws: &WorksheetXml) -> Option<String> {
494 ws.sheet_views
495 .as_ref()
496 .and_then(|sv| sv.sheet_views.first())
497 .and_then(|view| view.pane.as_ref())
498 .and_then(|pane| pane.top_left_cell.clone())
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum ViewMode {
504 Normal,
506 PageBreak,
508 PageLayout,
510}
511
512impl ViewMode {
513 pub fn as_str(&self) -> &'static str {
515 match self {
516 ViewMode::Normal => "normal",
517 ViewMode::PageBreak => "pageBreakPreview",
518 ViewMode::PageLayout => "pageLayout",
519 }
520 }
521
522 pub fn from_xml_str(s: &str) -> Option<Self> {
524 match s {
525 "normal" => Some(ViewMode::Normal),
526 "pageBreakPreview" => Some(ViewMode::PageBreak),
527 "pageLayout" => Some(ViewMode::PageLayout),
528 _ => None,
529 }
530 }
531}
532
533#[derive(Debug, Clone, Default)]
535pub struct SheetViewOptions {
536 pub show_gridlines: Option<bool>,
538 pub show_formulas: Option<bool>,
540 pub show_row_col_headers: Option<bool>,
542 pub zoom_scale: Option<u32>,
544 pub view_mode: Option<ViewMode>,
546 pub top_left_cell: Option<String>,
548}
549
550pub fn set_sheet_view_options(ws: &mut WorksheetXml, opts: &SheetViewOptions) -> Result<()> {
555 if let Some(zoom) = opts.zoom_scale {
556 if !(10..=400).contains(&zoom) {
557 return Err(Error::InvalidArgument(format!(
558 "zoom scale {zoom} is outside the valid range 10-400"
559 )));
560 }
561 }
562
563 let sheet_views = ws.sheet_views.get_or_insert_with(|| SheetViews {
564 sheet_views: vec![SheetView {
565 tab_selected: None,
566 show_grid_lines: None,
567 show_formulas: None,
568 show_row_col_headers: None,
569 zoom_scale: None,
570 view: None,
571 top_left_cell: None,
572 workbook_view_id: 0,
573 pane: None,
574 selection: vec![],
575 }],
576 });
577
578 if let Some(view) = sheet_views.sheet_views.first_mut() {
579 if let Some(v) = opts.show_gridlines {
580 view.show_grid_lines = if v { None } else { Some(false) };
581 }
582 if let Some(v) = opts.show_formulas {
583 view.show_formulas = if v { Some(true) } else { None };
584 }
585 if let Some(v) = opts.show_row_col_headers {
586 view.show_row_col_headers = if v { None } else { Some(false) };
587 }
588 if let Some(zoom) = opts.zoom_scale {
589 view.zoom_scale = if zoom == 100 { None } else { Some(zoom) };
590 }
591 if let Some(ref mode) = opts.view_mode {
592 view.view = match mode {
593 ViewMode::Normal => None,
594 other => Some(other.as_str().to_string()),
595 };
596 }
597 if let Some(ref cell) = opts.top_left_cell {
598 view.top_left_cell = if cell.is_empty() {
599 None
600 } else {
601 Some(cell.clone())
602 };
603 }
604 }
605
606 Ok(())
607}
608
609pub fn get_sheet_view_options(ws: &WorksheetXml) -> SheetViewOptions {
611 let view = ws
612 .sheet_views
613 .as_ref()
614 .and_then(|sv| sv.sheet_views.first());
615
616 match view {
617 None => SheetViewOptions {
618 show_gridlines: Some(true),
619 show_formulas: Some(false),
620 show_row_col_headers: Some(true),
621 zoom_scale: Some(100),
622 view_mode: Some(ViewMode::Normal),
623 top_left_cell: None,
624 },
625 Some(v) => SheetViewOptions {
626 show_gridlines: Some(v.show_grid_lines.unwrap_or(true)),
627 show_formulas: Some(v.show_formulas.unwrap_or(false)),
628 show_row_col_headers: Some(v.show_row_col_headers.unwrap_or(true)),
629 zoom_scale: Some(v.zoom_scale.unwrap_or(100)),
630 view_mode: Some(
631 v.view
632 .as_deref()
633 .and_then(ViewMode::from_xml_str)
634 .unwrap_or(ViewMode::Normal),
635 ),
636 top_left_cell: v.top_left_cell.clone(),
637 },
638 }
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Eq)]
643pub enum SheetVisibility {
644 Visible,
646 Hidden,
648 VeryHidden,
650}
651
652impl SheetVisibility {
653 pub fn as_xml_str(&self) -> Option<&'static str> {
655 match self {
656 SheetVisibility::Visible => None,
657 SheetVisibility::Hidden => Some("hidden"),
658 SheetVisibility::VeryHidden => Some("veryHidden"),
659 }
660 }
661
662 pub fn from_xml_str(s: Option<&str>) -> Self {
664 match s {
665 Some("hidden") => SheetVisibility::Hidden,
666 Some("veryHidden") => SheetVisibility::VeryHidden,
667 _ => SheetVisibility::Visible,
668 }
669 }
670}
671
672fn rebuild_content_type_overrides(content_types: &mut ContentTypes, sheet_count: usize) {
675 content_types
676 .overrides
677 .retain(|o| o.content_type != mime_types::WORKSHEET);
678
679 for i in 1..=sheet_count {
680 content_types.overrides.push(ContentTypeOverride {
681 part_name: format!("/xl/worksheets/sheet{}.xml", i),
682 content_type: mime_types::WORKSHEET.to_string(),
683 });
684 }
685}
686
687fn rebuild_worksheet_relationships(
689 workbook_xml: &mut WorkbookXml,
690 workbook_rels: &mut Relationships,
691) {
692 let sheet_rids: Vec<String> = workbook_xml
693 .sheets
694 .sheets
695 .iter()
696 .map(|s| s.r_id.clone())
697 .collect();
698
699 for (i, rid) in sheet_rids.iter().enumerate() {
700 if let Some(rel) = workbook_rels
701 .relationships
702 .iter_mut()
703 .find(|r| r.id == *rid)
704 {
705 rel.target = format!("worksheets/sheet{}.xml", i + 1);
706 }
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use sheetkit_xml::content_types::ContentTypes;
714 use sheetkit_xml::relationships;
715 use sheetkit_xml::workbook::WorkbookXml;
716 use sheetkit_xml::worksheet::WorksheetXml;
717
718 #[test]
721 fn test_protect_sheet_no_password() {
722 let mut ws = WorksheetXml::default();
723 let config = SheetProtectionConfig::default();
724 protect_sheet(&mut ws, &config).unwrap();
725
726 assert!(ws.sheet_protection.is_some());
727 let prot = ws.sheet_protection.as_ref().unwrap();
728 assert_eq!(prot.sheet, Some(true));
729 assert_eq!(prot.objects, Some(true));
730 assert_eq!(prot.scenarios, Some(true));
731 assert!(prot.password.is_none());
732 }
733
734 #[test]
735 fn test_protect_sheet_with_password() {
736 let mut ws = WorksheetXml::default();
737 let config = SheetProtectionConfig {
738 password: Some("secret".to_string()),
739 ..SheetProtectionConfig::default()
740 };
741 protect_sheet(&mut ws, &config).unwrap();
742
743 let prot = ws.sheet_protection.as_ref().unwrap();
744 assert!(prot.password.is_some());
745 let pw = prot.password.as_ref().unwrap();
746 assert_eq!(pw.len(), 4);
748 assert!(pw.chars().all(|c| c.is_ascii_hexdigit()));
749 let expected = format!("{:04X}", legacy_password_hash("secret"));
751 assert_eq!(pw, &expected);
752 }
753
754 #[test]
755 fn test_unprotect_sheet() {
756 let mut ws = WorksheetXml::default();
757 let config = SheetProtectionConfig {
758 password: Some("test".to_string()),
759 ..SheetProtectionConfig::default()
760 };
761 protect_sheet(&mut ws, &config).unwrap();
762 assert!(ws.sheet_protection.is_some());
763
764 unprotect_sheet(&mut ws).unwrap();
765 assert!(ws.sheet_protection.is_none());
766 }
767
768 #[test]
769 fn test_is_sheet_protected() {
770 let mut ws = WorksheetXml::default();
771 assert!(!is_sheet_protected(&ws));
772
773 let config = SheetProtectionConfig::default();
774 protect_sheet(&mut ws, &config).unwrap();
775 assert!(is_sheet_protected(&ws));
776
777 unprotect_sheet(&mut ws).unwrap();
778 assert!(!is_sheet_protected(&ws));
779 }
780
781 #[test]
782 fn test_protect_sheet_with_permissions() {
783 let mut ws = WorksheetXml::default();
784 let config = SheetProtectionConfig {
785 password: None,
786 format_cells: true,
787 insert_rows: true,
788 delete_columns: true,
789 sort: true,
790 ..SheetProtectionConfig::default()
791 };
792 protect_sheet(&mut ws, &config).unwrap();
793
794 let prot = ws.sheet_protection.as_ref().unwrap();
795 assert_eq!(prot.format_cells, Some(true));
796 assert_eq!(prot.insert_rows, Some(true));
797 assert_eq!(prot.delete_columns, Some(true));
798 assert_eq!(prot.sort, Some(true));
799 assert!(prot.format_columns.is_none());
801 assert!(prot.format_rows.is_none());
802 assert!(prot.insert_columns.is_none());
803 assert!(prot.insert_hyperlinks.is_none());
804 assert!(prot.delete_rows.is_none());
805 assert!(prot.auto_filter.is_none());
806 assert!(prot.pivot_tables.is_none());
807 assert!(prot.select_locked_cells.is_none());
808 assert!(prot.select_unlocked_cells.is_none());
809 }
810
811 #[test]
814 fn test_set_tab_color() {
815 let mut ws = WorksheetXml::default();
816 set_tab_color(&mut ws, "FF0000").unwrap();
817
818 assert!(ws.sheet_pr.is_some());
819 let tab_color = ws.sheet_pr.as_ref().unwrap().tab_color.as_ref().unwrap();
820 assert_eq!(tab_color.rgb, Some("FF0000".to_string()));
821 }
822
823 #[test]
824 fn test_get_tab_color() {
825 let mut ws = WorksheetXml::default();
826 set_tab_color(&mut ws, "00FF00").unwrap();
827 assert_eq!(get_tab_color(&ws), Some("00FF00".to_string()));
828 }
829
830 #[test]
831 fn test_get_tab_color_none() {
832 let ws = WorksheetXml::default();
833 assert_eq!(get_tab_color(&ws), None);
834 }
835
836 #[test]
839 fn test_set_default_row_height() {
840 let mut ws = WorksheetXml::default();
841 set_default_row_height(&mut ws, 20.0).unwrap();
842
843 assert!(ws.sheet_format_pr.is_some());
844 assert_eq!(
845 ws.sheet_format_pr.as_ref().unwrap().default_row_height,
846 20.0
847 );
848 }
849
850 #[test]
851 fn test_get_default_row_height() {
852 let ws = WorksheetXml::default();
853 assert_eq!(get_default_row_height(&ws), DEFAULT_ROW_HEIGHT);
854
855 let mut ws2 = WorksheetXml::default();
856 set_default_row_height(&mut ws2, 25.0).unwrap();
857 assert_eq!(get_default_row_height(&ws2), 25.0);
858 }
859
860 #[test]
861 fn test_set_default_row_height_exceeds_max() {
862 let mut ws = WorksheetXml::default();
863 let result = set_default_row_height(&mut ws, 500.0);
864 assert!(result.is_err());
865 assert!(matches!(
866 result.unwrap_err(),
867 Error::RowHeightExceeded { .. }
868 ));
869 }
870
871 #[test]
874 fn test_set_default_col_width() {
875 let mut ws = WorksheetXml::default();
876 set_default_col_width(&mut ws, 12.0).unwrap();
877
878 assert!(ws.sheet_format_pr.is_some());
879 assert_eq!(
880 ws.sheet_format_pr.as_ref().unwrap().default_col_width,
881 Some(12.0)
882 );
883 }
884
885 #[test]
886 fn test_get_default_col_width() {
887 let ws = WorksheetXml::default();
888 assert_eq!(get_default_col_width(&ws), None);
889
890 let mut ws2 = WorksheetXml::default();
891 set_default_col_width(&mut ws2, 18.5).unwrap();
892 assert_eq!(get_default_col_width(&ws2), Some(18.5));
893 }
894
895 #[test]
896 fn test_set_default_col_width_exceeds_max() {
897 let mut ws = WorksheetXml::default();
898 let result = set_default_col_width(&mut ws, 300.0);
899 assert!(result.is_err());
900 assert!(matches!(
901 result.unwrap_err(),
902 Error::ColumnWidthExceeded { .. }
903 ));
904 }
905
906 #[test]
909 fn test_validate_empty_name() {
910 let result = validate_sheet_name("");
911 assert!(result.is_err());
912 let err_msg = result.unwrap_err().to_string();
913 assert!(
914 err_msg.contains("empty"),
915 "Error should mention empty: {err_msg}"
916 );
917 }
918
919 #[test]
920 fn test_validate_too_long_name() {
921 let long_name = "a".repeat(32);
922 let result = validate_sheet_name(&long_name);
923 assert!(result.is_err());
924 let err_msg = result.unwrap_err().to_string();
925 assert!(
926 err_msg.contains("exceeds"),
927 "Error should mention exceeds: {err_msg}"
928 );
929 }
930
931 #[test]
932 fn test_validate_exactly_max_length_is_ok() {
933 let name = "a".repeat(MAX_SHEET_NAME_LENGTH);
934 assert!(validate_sheet_name(&name).is_ok());
935 }
936
937 #[test]
938 fn test_validate_invalid_chars() {
939 for ch in SHEET_NAME_INVALID_CHARS {
940 let name = format!("Sheet{}", ch);
941 let result = validate_sheet_name(&name);
942 assert!(result.is_err(), "Name with '{}' should be invalid", ch);
943 }
944 }
945
946 #[test]
947 fn test_validate_single_quote_boundary() {
948 assert!(validate_sheet_name("'Sheet").is_err());
949 assert!(validate_sheet_name("Sheet'").is_err());
950 assert!(validate_sheet_name("'Sheet'").is_err());
951 assert!(validate_sheet_name("She'et").is_ok());
953 }
954
955 #[test]
956 fn test_validate_valid_name() {
957 assert!(validate_sheet_name("Sheet1").is_ok());
958 assert!(validate_sheet_name("My Data").is_ok());
959 assert!(validate_sheet_name("Q1-2024").is_ok());
960 assert!(validate_sheet_name("Sheet (2)").is_ok());
961 }
962
963 #[test]
964 fn test_next_rid() {
965 let rels = vec![
966 Relationship {
967 id: "rId1".to_string(),
968 rel_type: "".to_string(),
969 target: "".to_string(),
970 target_mode: None,
971 },
972 Relationship {
973 id: "rId3".to_string(),
974 rel_type: "".to_string(),
975 target: "".to_string(),
976 target_mode: None,
977 },
978 ];
979 assert_eq!(next_rid(&rels), "rId4");
980 }
981
982 #[test]
983 fn test_next_rid_empty() {
984 assert_eq!(next_rid(&[]), "rId1");
985 }
986
987 #[test]
988 fn test_next_sheet_id() {
989 let sheets = vec![
990 SheetEntry {
991 name: "Sheet1".to_string(),
992 sheet_id: 1,
993 state: None,
994 r_id: "rId1".to_string(),
995 },
996 SheetEntry {
997 name: "Sheet2".to_string(),
998 sheet_id: 5,
999 state: None,
1000 r_id: "rId2".to_string(),
1001 },
1002 ];
1003 assert_eq!(next_sheet_id(&sheets), 6);
1004 }
1005
1006 #[test]
1007 fn test_next_sheet_id_empty() {
1008 assert_eq!(next_sheet_id(&[]), 1);
1009 }
1010
1011 fn test_workbook_parts() -> (
1013 WorkbookXml,
1014 Relationships,
1015 ContentTypes,
1016 Vec<(String, WorksheetXml)>,
1017 ) {
1018 let workbook_xml = WorkbookXml::default();
1019 let workbook_rels = relationships::workbook_rels();
1020 let content_types = ContentTypes::default();
1021 let worksheets = vec![("Sheet1".to_string(), WorksheetXml::default())];
1022 (workbook_xml, workbook_rels, content_types, worksheets)
1023 }
1024
1025 #[test]
1026 fn test_add_sheet_basic() {
1027 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1028
1029 let idx = add_sheet(
1030 &mut wb_xml,
1031 &mut wb_rels,
1032 &mut ct,
1033 &mut ws,
1034 "Sheet2",
1035 WorksheetXml::default(),
1036 )
1037 .unwrap();
1038
1039 assert_eq!(idx, 1);
1040 assert_eq!(ws.len(), 2);
1041 assert_eq!(ws[1].0, "Sheet2");
1042 assert_eq!(wb_xml.sheets.sheets.len(), 2);
1043 assert_eq!(wb_xml.sheets.sheets[1].name, "Sheet2");
1044
1045 let ws_rels: Vec<_> = wb_rels
1046 .relationships
1047 .iter()
1048 .filter(|r| r.rel_type == rel_types::WORKSHEET)
1049 .collect();
1050 assert_eq!(ws_rels.len(), 2);
1051
1052 let ws_overrides: Vec<_> = ct
1053 .overrides
1054 .iter()
1055 .filter(|o| o.content_type == mime_types::WORKSHEET)
1056 .collect();
1057 assert_eq!(ws_overrides.len(), 2);
1058 }
1059
1060 #[test]
1061 fn test_add_sheet_duplicate_returns_error() {
1062 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1063
1064 let result = add_sheet(
1065 &mut wb_xml,
1066 &mut wb_rels,
1067 &mut ct,
1068 &mut ws,
1069 "Sheet1",
1070 WorksheetXml::default(),
1071 );
1072
1073 assert!(result.is_err());
1074 assert!(
1075 matches!(result.unwrap_err(), Error::SheetAlreadyExists { name } if name == "Sheet1")
1076 );
1077 }
1078
1079 #[test]
1080 fn test_add_sheet_invalid_name_returns_error() {
1081 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1082
1083 let result = add_sheet(
1084 &mut wb_xml,
1085 &mut wb_rels,
1086 &mut ct,
1087 &mut ws,
1088 "Bad[Name",
1089 WorksheetXml::default(),
1090 );
1091
1092 assert!(result.is_err());
1093 assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
1094 }
1095
1096 #[test]
1097 fn test_delete_sheet_basic() {
1098 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1099
1100 add_sheet(
1101 &mut wb_xml,
1102 &mut wb_rels,
1103 &mut ct,
1104 &mut ws,
1105 "Sheet2",
1106 WorksheetXml::default(),
1107 )
1108 .unwrap();
1109
1110 assert_eq!(ws.len(), 2);
1111
1112 delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Sheet1").unwrap();
1113
1114 assert_eq!(ws.len(), 1);
1115 assert_eq!(ws[0].0, "Sheet2");
1116 assert_eq!(wb_xml.sheets.sheets.len(), 1);
1117 assert_eq!(wb_xml.sheets.sheets[0].name, "Sheet2");
1118
1119 let ws_rels: Vec<_> = wb_rels
1120 .relationships
1121 .iter()
1122 .filter(|r| r.rel_type == rel_types::WORKSHEET)
1123 .collect();
1124 assert_eq!(ws_rels.len(), 1);
1125
1126 let ws_overrides: Vec<_> = ct
1127 .overrides
1128 .iter()
1129 .filter(|o| o.content_type == mime_types::WORKSHEET)
1130 .collect();
1131 assert_eq!(ws_overrides.len(), 1);
1132 }
1133
1134 #[test]
1135 fn test_delete_last_sheet_returns_error() {
1136 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1137
1138 let result = delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Sheet1");
1139 assert!(result.is_err());
1140 }
1141
1142 #[test]
1143 fn test_delete_nonexistent_sheet_returns_error() {
1144 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1145
1146 let result = delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "Nonexistent");
1147 assert!(result.is_err());
1148 assert!(
1149 matches!(result.unwrap_err(), Error::SheetNotFound { name } if name == "Nonexistent")
1150 );
1151 }
1152
1153 #[test]
1154 fn test_rename_sheet_basic() {
1155 let (mut wb_xml, _, _, mut ws) = test_workbook_parts();
1156
1157 rename_sheet(&mut wb_xml, &mut ws, "Sheet1", "MySheet").unwrap();
1158
1159 assert_eq!(ws[0].0, "MySheet");
1160 assert_eq!(wb_xml.sheets.sheets[0].name, "MySheet");
1161 }
1162
1163 #[test]
1164 fn test_rename_sheet_to_existing_returns_error() {
1165 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1166
1167 add_sheet(
1168 &mut wb_xml,
1169 &mut wb_rels,
1170 &mut ct,
1171 &mut ws,
1172 "Sheet2",
1173 WorksheetXml::default(),
1174 )
1175 .unwrap();
1176
1177 let result = rename_sheet(&mut wb_xml, &mut ws, "Sheet1", "Sheet2");
1178 assert!(result.is_err());
1179 assert!(
1180 matches!(result.unwrap_err(), Error::SheetAlreadyExists { name } if name == "Sheet2")
1181 );
1182 }
1183
1184 #[test]
1185 fn test_rename_nonexistent_sheet_returns_error() {
1186 let (mut wb_xml, _, _, mut ws) = test_workbook_parts();
1187
1188 let result = rename_sheet(&mut wb_xml, &mut ws, "Nope", "NewName");
1189 assert!(result.is_err());
1190 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { name } if name == "Nope"));
1191 }
1192
1193 #[test]
1194 fn test_copy_sheet_basic() {
1195 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1196
1197 let idx = copy_sheet(
1198 &mut wb_xml,
1199 &mut wb_rels,
1200 &mut ct,
1201 &mut ws,
1202 "Sheet1",
1203 "Sheet1 Copy",
1204 )
1205 .unwrap();
1206
1207 assert_eq!(idx, 1);
1208 assert_eq!(ws.len(), 2);
1209 assert_eq!(ws[1].0, "Sheet1 Copy");
1210 assert_eq!(ws[1].1, ws[0].1);
1212 }
1213
1214 #[test]
1215 fn test_copy_nonexistent_sheet_returns_error() {
1216 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1217
1218 let result = copy_sheet(
1219 &mut wb_xml,
1220 &mut wb_rels,
1221 &mut ct,
1222 &mut ws,
1223 "Nonexistent",
1224 "Copy",
1225 );
1226 assert!(result.is_err());
1227 }
1228
1229 #[test]
1230 fn test_copy_sheet_to_existing_name_returns_error() {
1231 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1232
1233 let result = copy_sheet(
1234 &mut wb_xml,
1235 &mut wb_rels,
1236 &mut ct,
1237 &mut ws,
1238 "Sheet1",
1239 "Sheet1",
1240 );
1241 assert!(result.is_err());
1242 }
1243
1244 #[test]
1245 fn test_find_sheet_index() {
1246 let ws: Vec<(String, WorksheetXml)> = vec![
1247 ("Sheet1".to_string(), WorksheetXml::default()),
1248 ("Sheet2".to_string(), WorksheetXml::default()),
1249 ];
1250
1251 assert_eq!(find_sheet_index(&ws, "Sheet1"), Some(0));
1252 assert_eq!(find_sheet_index(&ws, "Sheet2"), Some(1));
1253 assert_eq!(find_sheet_index(&ws, "Sheet3"), None);
1254 }
1255
1256 #[test]
1257 fn test_active_sheet_index_default() {
1258 let wb_xml = WorkbookXml::default();
1259 assert_eq!(active_sheet_index(&wb_xml), 0);
1260 }
1261
1262 #[test]
1263 fn test_set_active_sheet_index() {
1264 let mut wb_xml = WorkbookXml::default();
1265 set_active_sheet_index(&mut wb_xml, 2);
1266
1267 assert_eq!(active_sheet_index(&wb_xml), 2);
1268 }
1269
1270 #[test]
1271 fn test_multiple_add_delete_consistency() {
1272 let (mut wb_xml, mut wb_rels, mut ct, mut ws) = test_workbook_parts();
1273
1274 add_sheet(
1275 &mut wb_xml,
1276 &mut wb_rels,
1277 &mut ct,
1278 &mut ws,
1279 "A",
1280 WorksheetXml::default(),
1281 )
1282 .unwrap();
1283 add_sheet(
1284 &mut wb_xml,
1285 &mut wb_rels,
1286 &mut ct,
1287 &mut ws,
1288 "B",
1289 WorksheetXml::default(),
1290 )
1291 .unwrap();
1292 add_sheet(
1293 &mut wb_xml,
1294 &mut wb_rels,
1295 &mut ct,
1296 &mut ws,
1297 "C",
1298 WorksheetXml::default(),
1299 )
1300 .unwrap();
1301
1302 assert_eq!(ws.len(), 4);
1303
1304 delete_sheet(&mut wb_xml, &mut wb_rels, &mut ct, &mut ws, "B").unwrap();
1305
1306 assert_eq!(ws.len(), 3);
1307 let names: Vec<&str> = ws.iter().map(|(n, _)| n.as_str()).collect();
1308 assert_eq!(names, vec!["Sheet1", "A", "C"]);
1309
1310 assert_eq!(wb_xml.sheets.sheets.len(), 3);
1311 let ws_rels: Vec<_> = wb_rels
1312 .relationships
1313 .iter()
1314 .filter(|r| r.rel_type == rel_types::WORKSHEET)
1315 .collect();
1316 assert_eq!(ws_rels.len(), 3);
1317 let ws_overrides: Vec<_> = ct
1318 .overrides
1319 .iter()
1320 .filter(|o| o.content_type == mime_types::WORKSHEET)
1321 .collect();
1322 assert_eq!(ws_overrides.len(), 3);
1323 }
1324
1325 #[test]
1328 fn test_set_panes_freeze_row() {
1329 let mut ws = WorksheetXml::default();
1330 set_panes(&mut ws, "A2").unwrap();
1331
1332 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1333 .pane
1334 .as_ref()
1335 .unwrap();
1336 assert_eq!(pane.y_split, Some(1));
1337 assert!(pane.x_split.is_none());
1338 assert_eq!(pane.top_left_cell, Some("A2".to_string()));
1339 assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1340 assert_eq!(pane.state, Some("frozen".to_string()));
1341 }
1342
1343 #[test]
1344 fn test_set_panes_freeze_col() {
1345 let mut ws = WorksheetXml::default();
1346 set_panes(&mut ws, "B1").unwrap();
1347
1348 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1349 .pane
1350 .as_ref()
1351 .unwrap();
1352 assert_eq!(pane.x_split, Some(1));
1353 assert!(pane.y_split.is_none());
1354 assert_eq!(pane.top_left_cell, Some("B1".to_string()));
1355 assert_eq!(pane.active_pane, Some("topRight".to_string()));
1356 assert_eq!(pane.state, Some("frozen".to_string()));
1357 }
1358
1359 #[test]
1360 fn test_set_panes_freeze_both() {
1361 let mut ws = WorksheetXml::default();
1362 set_panes(&mut ws, "B2").unwrap();
1363
1364 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1365 .pane
1366 .as_ref()
1367 .unwrap();
1368 assert_eq!(pane.x_split, Some(1));
1369 assert_eq!(pane.y_split, Some(1));
1370 assert_eq!(pane.top_left_cell, Some("B2".to_string()));
1371 assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1372 assert_eq!(pane.state, Some("frozen".to_string()));
1373 }
1374
1375 #[test]
1376 fn test_set_panes_freeze_multiple_rows() {
1377 let mut ws = WorksheetXml::default();
1378 set_panes(&mut ws, "A4").unwrap();
1379
1380 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1381 .pane
1382 .as_ref()
1383 .unwrap();
1384 assert_eq!(pane.y_split, Some(3));
1385 assert!(pane.x_split.is_none());
1386 assert_eq!(pane.top_left_cell, Some("A4".to_string()));
1387 assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1388 }
1389
1390 #[test]
1391 fn test_set_panes_freeze_multiple_cols() {
1392 let mut ws = WorksheetXml::default();
1393 set_panes(&mut ws, "D1").unwrap();
1394
1395 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1396 .pane
1397 .as_ref()
1398 .unwrap();
1399 assert_eq!(pane.x_split, Some(3));
1400 assert!(pane.y_split.is_none());
1401 assert_eq!(pane.top_left_cell, Some("D1".to_string()));
1402 assert_eq!(pane.active_pane, Some("topRight".to_string()));
1403 }
1404
1405 #[test]
1406 fn test_set_panes_a1_error() {
1407 let mut ws = WorksheetXml::default();
1408 let result = set_panes(&mut ws, "A1");
1409 assert!(result.is_err());
1410 assert!(matches!(
1411 result.unwrap_err(),
1412 Error::InvalidCellReference(_)
1413 ));
1414 }
1415
1416 #[test]
1417 fn test_set_panes_invalid_cell_error() {
1418 let mut ws = WorksheetXml::default();
1419 let result = set_panes(&mut ws, "ZZZZ1");
1420 assert!(result.is_err());
1421 }
1422
1423 #[test]
1424 fn test_unset_panes() {
1425 let mut ws = WorksheetXml::default();
1426 set_panes(&mut ws, "B2").unwrap();
1427 assert!(get_panes(&ws).is_some());
1428
1429 unset_panes(&mut ws);
1430 assert!(get_panes(&ws).is_none());
1431 let view = &ws.sheet_views.as_ref().unwrap().sheet_views[0];
1433 assert!(view.pane.is_none());
1434 assert!(view.selection.is_empty());
1435 }
1436
1437 #[test]
1438 fn test_get_panes_none_when_not_set() {
1439 let ws = WorksheetXml::default();
1440 assert!(get_panes(&ws).is_none());
1441 }
1442
1443 #[test]
1444 fn test_get_panes_returns_value_after_set() {
1445 let mut ws = WorksheetXml::default();
1446 set_panes(&mut ws, "C5").unwrap();
1447 assert_eq!(get_panes(&ws), Some("C5".to_string()));
1448 }
1449
1450 #[test]
1451 fn test_set_panes_selection_has_pane_attribute() {
1452 let mut ws = WorksheetXml::default();
1453 set_panes(&mut ws, "B2").unwrap();
1454
1455 let selection = &ws.sheet_views.as_ref().unwrap().sheet_views[0].selection[0];
1456 assert_eq!(selection.pane, Some("bottomRight".to_string()));
1457 assert_eq!(selection.active_cell, Some("B2".to_string()));
1458 assert_eq!(selection.sqref, Some("B2".to_string()));
1459 }
1460
1461 #[test]
1462 fn test_set_panes_overwrites_previous() {
1463 let mut ws = WorksheetXml::default();
1464 set_panes(&mut ws, "A2").unwrap();
1465 assert_eq!(get_panes(&ws), Some("A2".to_string()));
1466
1467 set_panes(&mut ws, "C3").unwrap();
1468 assert_eq!(get_panes(&ws), Some("C3".to_string()));
1469
1470 let pane = ws.sheet_views.as_ref().unwrap().sheet_views[0]
1471 .pane
1472 .as_ref()
1473 .unwrap();
1474 assert_eq!(pane.x_split, Some(2));
1475 assert_eq!(pane.y_split, Some(2));
1476 assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1477 }
1478
1479 #[test]
1480 fn test_unset_panes_noop_when_no_views() {
1481 let mut ws = WorksheetXml::default();
1482 unset_panes(&mut ws);
1484 assert!(get_panes(&ws).is_none());
1485 }
1486
1487 #[test]
1488 fn test_view_mode_as_str() {
1489 assert_eq!(ViewMode::Normal.as_str(), "normal");
1490 assert_eq!(ViewMode::PageBreak.as_str(), "pageBreakPreview");
1491 assert_eq!(ViewMode::PageLayout.as_str(), "pageLayout");
1492 }
1493
1494 #[test]
1495 fn test_view_mode_from_str() {
1496 assert_eq!(ViewMode::from_xml_str("normal"), Some(ViewMode::Normal));
1497 assert_eq!(
1498 ViewMode::from_xml_str("pageBreakPreview"),
1499 Some(ViewMode::PageBreak)
1500 );
1501 assert_eq!(
1502 ViewMode::from_xml_str("pageLayout"),
1503 Some(ViewMode::PageLayout)
1504 );
1505 assert_eq!(ViewMode::from_xml_str("unknown"), None);
1506 }
1507
1508 #[test]
1509 fn test_sheet_visibility_as_xml_str() {
1510 assert_eq!(SheetVisibility::Visible.as_xml_str(), None);
1511 assert_eq!(SheetVisibility::Hidden.as_xml_str(), Some("hidden"));
1512 assert_eq!(SheetVisibility::VeryHidden.as_xml_str(), Some("veryHidden"));
1513 }
1514
1515 #[test]
1516 fn test_sheet_visibility_from_xml_str() {
1517 assert_eq!(
1518 SheetVisibility::from_xml_str(None),
1519 SheetVisibility::Visible
1520 );
1521 assert_eq!(
1522 SheetVisibility::from_xml_str(Some("hidden")),
1523 SheetVisibility::Hidden
1524 );
1525 assert_eq!(
1526 SheetVisibility::from_xml_str(Some("veryHidden")),
1527 SheetVisibility::VeryHidden
1528 );
1529 assert_eq!(
1530 SheetVisibility::from_xml_str(Some("unknown")),
1531 SheetVisibility::Visible
1532 );
1533 }
1534
1535 #[test]
1536 fn test_set_sheet_view_options_basic() {
1537 let mut ws = WorksheetXml::default();
1538 let opts = SheetViewOptions {
1539 show_gridlines: Some(false),
1540 show_formulas: Some(true),
1541 zoom_scale: Some(200),
1542 view_mode: Some(ViewMode::PageBreak),
1543 top_left_cell: Some("B5".to_string()),
1544 show_row_col_headers: Some(false),
1545 };
1546 set_sheet_view_options(&mut ws, &opts).unwrap();
1547
1548 let result = get_sheet_view_options(&ws);
1549 assert_eq!(result.show_gridlines, Some(false));
1550 assert_eq!(result.show_formulas, Some(true));
1551 assert_eq!(result.zoom_scale, Some(200));
1552 assert_eq!(result.view_mode, Some(ViewMode::PageBreak));
1553 assert_eq!(result.top_left_cell, Some("B5".to_string()));
1554 assert_eq!(result.show_row_col_headers, Some(false));
1555 }
1556
1557 #[test]
1558 fn test_set_sheet_view_options_zoom_out_of_range() {
1559 let mut ws = WorksheetXml::default();
1560 let result = set_sheet_view_options(
1561 &mut ws,
1562 &SheetViewOptions {
1563 zoom_scale: Some(9),
1564 ..Default::default()
1565 },
1566 );
1567 assert!(result.is_err());
1568
1569 let result2 = set_sheet_view_options(
1570 &mut ws,
1571 &SheetViewOptions {
1572 zoom_scale: Some(401),
1573 ..Default::default()
1574 },
1575 );
1576 assert!(result2.is_err());
1577 }
1578
1579 #[test]
1580 fn test_set_sheet_view_options_preserves_existing() {
1581 let mut ws = WorksheetXml::default();
1582 set_sheet_view_options(
1583 &mut ws,
1584 &SheetViewOptions {
1585 zoom_scale: Some(150),
1586 ..Default::default()
1587 },
1588 )
1589 .unwrap();
1590 set_sheet_view_options(
1591 &mut ws,
1592 &SheetViewOptions {
1593 show_gridlines: Some(false),
1594 ..Default::default()
1595 },
1596 )
1597 .unwrap();
1598
1599 let result = get_sheet_view_options(&ws);
1600 assert_eq!(result.zoom_scale, Some(150));
1601 assert_eq!(result.show_gridlines, Some(false));
1602 }
1603
1604 #[test]
1605 fn test_get_sheet_view_options_defaults() {
1606 let ws = WorksheetXml::default();
1607 let opts = get_sheet_view_options(&ws);
1608 assert_eq!(opts.show_gridlines, Some(true));
1609 assert_eq!(opts.show_formulas, Some(false));
1610 assert_eq!(opts.show_row_col_headers, Some(true));
1611 assert_eq!(opts.zoom_scale, Some(100));
1612 assert_eq!(opts.view_mode, Some(ViewMode::Normal));
1613 assert!(opts.top_left_cell.is_none());
1614 }
1615
1616 #[test]
1617 fn test_set_sheet_view_options_does_not_break_panes() {
1618 let mut ws = WorksheetXml::default();
1619 set_panes(&mut ws, "B2").unwrap();
1620 set_sheet_view_options(
1621 &mut ws,
1622 &SheetViewOptions {
1623 zoom_scale: Some(120),
1624 ..Default::default()
1625 },
1626 )
1627 .unwrap();
1628
1629 assert_eq!(get_panes(&ws), Some("B2".to_string()));
1630 assert_eq!(get_sheet_view_options(&ws).zoom_scale, Some(120));
1631 }
1632}