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