1use super::*;
2
3impl Workbook {
4 pub fn add_data_validation(
6 &mut self,
7 sheet: &str,
8 config: &DataValidationConfig,
9 ) -> Result<()> {
10 let ws = self.worksheet_mut(sheet)?;
11 crate::validation::add_validation(ws, config)
12 }
13
14 pub fn get_data_validations(&self, sheet: &str) -> Result<Vec<DataValidationConfig>> {
16 let ws = self.worksheet_ref(sheet)?;
17 Ok(crate::validation::get_validations(ws))
18 }
19
20 pub fn remove_data_validation(&mut self, sheet: &str, sqref: &str) -> Result<()> {
22 let ws = self.worksheet_mut(sheet)?;
23 crate::validation::remove_validation(ws, sqref)
24 }
25
26 pub fn set_conditional_format(
28 &mut self,
29 sheet: &str,
30 sqref: &str,
31 rules: &[ConditionalFormatRule],
32 ) -> Result<()> {
33 let idx = self.sheet_index(sheet)?;
34 let ws = &mut self.worksheets[idx].1;
35 crate::conditional::set_conditional_format(ws, &mut self.stylesheet, sqref, rules)
36 }
37
38 pub fn get_conditional_formats(
42 &self,
43 sheet: &str,
44 ) -> Result<Vec<(String, Vec<ConditionalFormatRule>)>> {
45 let ws = self.worksheet_ref(sheet)?;
46 Ok(crate::conditional::get_conditional_formats(
47 ws,
48 &self.stylesheet,
49 ))
50 }
51
52 pub fn delete_conditional_format(&mut self, sheet: &str, sqref: &str) -> Result<()> {
54 let ws = self.worksheet_mut(sheet)?;
55 crate::conditional::delete_conditional_format(ws, sqref)
56 }
57
58 pub fn add_comment(&mut self, sheet: &str, config: &CommentConfig) -> Result<()> {
63 let idx = self.sheet_index(sheet)?;
64 crate::comment::add_comment(&mut self.sheet_comments[idx], config);
65 if idx < self.sheet_vml.len() {
67 self.sheet_vml[idx] = None;
68 }
69 Ok(())
70 }
71
72 pub fn get_comments(&self, sheet: &str) -> Result<Vec<CommentConfig>> {
74 let idx = self.sheet_index(sheet)?;
75 Ok(crate::comment::get_all_comments(&self.sheet_comments[idx]))
76 }
77
78 pub fn remove_comment(&mut self, sheet: &str, cell: &str) -> Result<()> {
83 let idx = self.sheet_index(sheet)?;
84 crate::comment::remove_comment(&mut self.sheet_comments[idx], cell);
85 if idx < self.sheet_vml.len() {
87 self.sheet_vml[idx] = None;
88 }
89 Ok(())
90 }
91
92 pub fn add_threaded_comment(
97 &mut self,
98 sheet: &str,
99 cell: &str,
100 input: &ThreadedCommentInput,
101 ) -> Result<String> {
102 let idx = self.sheet_index(sheet)?;
103 crate::threaded_comment::add_threaded_comment(
104 &mut self.sheet_threaded_comments[idx],
105 &mut self.person_list,
106 cell,
107 input,
108 )
109 }
110
111 pub fn get_threaded_comments(&self, sheet: &str) -> Result<Vec<ThreadedCommentData>> {
113 let idx = self.sheet_index(sheet)?;
114 Ok(crate::threaded_comment::get_threaded_comments(
115 &self.sheet_threaded_comments[idx],
116 &self.person_list,
117 ))
118 }
119
120 pub fn get_threaded_comments_by_cell(
122 &self,
123 sheet: &str,
124 cell: &str,
125 ) -> Result<Vec<ThreadedCommentData>> {
126 let idx = self.sheet_index(sheet)?;
127 Ok(crate::threaded_comment::get_threaded_comments_by_cell(
128 &self.sheet_threaded_comments[idx],
129 &self.person_list,
130 cell,
131 ))
132 }
133
134 pub fn delete_threaded_comment(&mut self, sheet: &str, comment_id: &str) -> Result<()> {
138 let idx = self.sheet_index(sheet)?;
139 crate::threaded_comment::delete_threaded_comment(
140 &mut self.sheet_threaded_comments[idx],
141 comment_id,
142 )
143 }
144
145 pub fn resolve_threaded_comment(
149 &mut self,
150 sheet: &str,
151 comment_id: &str,
152 done: bool,
153 ) -> Result<()> {
154 let idx = self.sheet_index(sheet)?;
155 crate::threaded_comment::resolve_threaded_comment(
156 &mut self.sheet_threaded_comments[idx],
157 comment_id,
158 done,
159 )
160 }
161
162 pub fn add_person(&mut self, input: &PersonInput) -> String {
165 crate::threaded_comment::add_person(&mut self.person_list, input)
166 }
167
168 pub fn get_persons(&self) -> Vec<PersonData> {
170 crate::threaded_comment::get_persons(&self.person_list)
171 }
172
173 pub fn set_auto_filter(&mut self, sheet: &str, range: &str) -> Result<()> {
175 let ws = self.worksheet_mut(sheet)?;
176 crate::table::set_auto_filter(ws, range)
177 }
178
179 pub fn remove_auto_filter(&mut self, sheet: &str) -> Result<()> {
181 let ws = self.worksheet_mut(sheet)?;
182 crate::table::remove_auto_filter(ws);
183 Ok(())
184 }
185
186 pub fn add_table(&mut self, sheet: &str, config: &crate::table::TableConfig) -> Result<()> {
191 crate::table::validate_table_config(config)?;
192 let sheet_idx = self.sheet_index(sheet)?;
193
194 if self.tables.iter().any(|(_, t, _)| t.name == config.name) {
196 return Err(Error::TableAlreadyExists {
197 name: config.name.clone(),
198 });
199 }
200
201 let table_id = self.tables.iter().map(|(_, t, _)| t.id).max().unwrap_or(0) + 1;
203
204 let max_existing = self
205 .tables
206 .iter()
207 .filter_map(|(path, _, _)| {
208 path.trim_start_matches("xl/tables/table")
209 .trim_end_matches(".xml")
210 .parse::<u32>()
211 .ok()
212 })
213 .max()
214 .unwrap_or(0);
215 let table_num = max_existing + 1;
216 let table_path = format!("xl/tables/table{}.xml", table_num);
217 let table_xml = crate::table::build_table_xml(config, table_id);
218
219 self.tables.push((table_path, table_xml, sheet_idx));
220 Ok(())
221 }
222
223 pub fn get_tables(&self, sheet: &str) -> Result<Vec<crate::table::TableInfo>> {
227 let sheet_idx = self.sheet_index(sheet)?;
228 let infos = self
229 .tables
230 .iter()
231 .filter(|(_, _, idx)| *idx == sheet_idx)
232 .map(|(_, table_xml, _)| crate::table::table_xml_to_info(table_xml))
233 .collect();
234 Ok(infos)
235 }
236
237 pub fn delete_table(&mut self, sheet: &str, table_name: &str) -> Result<()> {
241 let sheet_idx = self.sheet_index(sheet)?;
242
243 let pos = self
244 .tables
245 .iter()
246 .position(|(_, t, idx)| t.name == table_name && *idx == sheet_idx);
247 match pos {
248 Some(i) => {
249 self.tables.remove(i);
250 Ok(())
251 }
252 None => Err(Error::TableNotFound {
253 name: table_name.to_string(),
254 }),
255 }
256 }
257
258 pub fn set_panes(&mut self, sheet: &str, cell: &str) -> Result<()> {
264 let ws = self.worksheet_mut(sheet)?;
265 crate::sheet::set_panes(ws, cell)
266 }
267
268 pub fn unset_panes(&mut self, sheet: &str) -> Result<()> {
270 let ws = self.worksheet_mut(sheet)?;
271 crate::sheet::unset_panes(ws);
272 Ok(())
273 }
274
275 pub fn get_panes(&self, sheet: &str) -> Result<Option<String>> {
280 let ws = self.worksheet_ref(sheet)?;
281 Ok(crate::sheet::get_panes(ws))
282 }
283
284 pub fn set_page_margins(
286 &mut self,
287 sheet: &str,
288 margins: &crate::page_layout::PageMarginsConfig,
289 ) -> Result<()> {
290 let ws = self.worksheet_mut(sheet)?;
291 crate::page_layout::set_page_margins(ws, margins)
292 }
293
294 pub fn get_page_margins(&self, sheet: &str) -> Result<crate::page_layout::PageMarginsConfig> {
296 let ws = self.worksheet_ref(sheet)?;
297 Ok(crate::page_layout::get_page_margins(ws))
298 }
299
300 pub fn set_page_setup(
305 &mut self,
306 sheet: &str,
307 orientation: Option<crate::page_layout::Orientation>,
308 paper_size: Option<crate::page_layout::PaperSize>,
309 scale: Option<u32>,
310 fit_to_width: Option<u32>,
311 fit_to_height: Option<u32>,
312 ) -> Result<()> {
313 let ws = self.worksheet_mut(sheet)?;
314 crate::page_layout::set_page_setup(
315 ws,
316 orientation,
317 paper_size,
318 scale,
319 fit_to_width,
320 fit_to_height,
321 )
322 }
323
324 pub fn get_orientation(&self, sheet: &str) -> Result<Option<crate::page_layout::Orientation>> {
326 let ws = self.worksheet_ref(sheet)?;
327 Ok(crate::page_layout::get_orientation(ws))
328 }
329
330 pub fn get_paper_size(&self, sheet: &str) -> Result<Option<crate::page_layout::PaperSize>> {
332 let ws = self.worksheet_ref(sheet)?;
333 Ok(crate::page_layout::get_paper_size(ws))
334 }
335
336 pub fn get_page_setup_details(
340 &self,
341 sheet: &str,
342 ) -> Result<(Option<u32>, Option<u32>, Option<u32>)> {
343 let ws = self.worksheet_ref(sheet)?;
344 Ok((
345 crate::page_layout::get_scale(ws),
346 crate::page_layout::get_fit_to_width(ws),
347 crate::page_layout::get_fit_to_height(ws),
348 ))
349 }
350
351 pub fn set_header_footer(
353 &mut self,
354 sheet: &str,
355 header: Option<&str>,
356 footer: Option<&str>,
357 ) -> Result<()> {
358 let ws = self.worksheet_mut(sheet)?;
359 crate::page_layout::set_header_footer(ws, header, footer)
360 }
361
362 pub fn get_header_footer(&self, sheet: &str) -> Result<(Option<String>, Option<String>)> {
364 let ws = self.worksheet_ref(sheet)?;
365 Ok(crate::page_layout::get_header_footer(ws))
366 }
367
368 pub fn set_print_options(
370 &mut self,
371 sheet: &str,
372 grid_lines: Option<bool>,
373 headings: Option<bool>,
374 h_centered: Option<bool>,
375 v_centered: Option<bool>,
376 ) -> Result<()> {
377 let ws = self.worksheet_mut(sheet)?;
378 crate::page_layout::set_print_options(ws, grid_lines, headings, h_centered, v_centered)
379 }
380
381 #[allow(clippy::type_complexity)]
385 pub fn get_print_options(
386 &self,
387 sheet: &str,
388 ) -> Result<(Option<bool>, Option<bool>, Option<bool>, Option<bool>)> {
389 let ws = self.worksheet_ref(sheet)?;
390 Ok(crate::page_layout::get_print_options(ws))
391 }
392
393 pub fn insert_page_break(&mut self, sheet: &str, row: u32) -> Result<()> {
395 let ws = self.worksheet_mut(sheet)?;
396 crate::page_layout::insert_page_break(ws, row)
397 }
398
399 pub fn remove_page_break(&mut self, sheet: &str, row: u32) -> Result<()> {
401 let ws = self.worksheet_mut(sheet)?;
402 crate::page_layout::remove_page_break(ws, row)
403 }
404
405 pub fn get_page_breaks(&self, sheet: &str) -> Result<Vec<u32>> {
407 let ws = self.worksheet_ref(sheet)?;
408 Ok(crate::page_layout::get_page_breaks(ws))
409 }
410
411 pub fn set_cell_hyperlink(
417 &mut self,
418 sheet: &str,
419 cell: &str,
420 link: crate::hyperlink::HyperlinkType,
421 display: Option<&str>,
422 tooltip: Option<&str>,
423 ) -> Result<()> {
424 let sheet_idx = self.sheet_index(sheet)?;
425 let ws = &mut self.worksheets[sheet_idx].1;
426 let rels = self
427 .worksheet_rels
428 .entry(sheet_idx)
429 .or_insert_with(|| Relationships {
430 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
431 relationships: vec![],
432 });
433 crate::hyperlink::set_cell_hyperlink(ws, rels, cell, &link, display, tooltip)
434 }
435
436 pub fn get_cell_hyperlink(
440 &self,
441 sheet: &str,
442 cell: &str,
443 ) -> Result<Option<crate::hyperlink::HyperlinkInfo>> {
444 let sheet_idx = self.sheet_index(sheet)?;
445 let ws = &self.worksheets[sheet_idx].1;
446 let empty_rels = Relationships {
447 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
448 relationships: vec![],
449 };
450 let rels = self.worksheet_rels.get(&sheet_idx).unwrap_or(&empty_rels);
451 crate::hyperlink::get_cell_hyperlink(ws, rels, cell)
452 }
453
454 pub fn delete_cell_hyperlink(&mut self, sheet: &str, cell: &str) -> Result<()> {
459 let sheet_idx = self.sheet_index(sheet)?;
460 let ws = &mut self.worksheets[sheet_idx].1;
461 let rels = self
462 .worksheet_rels
463 .entry(sheet_idx)
464 .or_insert_with(|| Relationships {
465 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
466 relationships: vec![],
467 });
468 crate::hyperlink::delete_cell_hyperlink(ws, rels, cell)
469 }
470
471 pub fn protect_workbook(&mut self, config: WorkbookProtectionConfig) {
473 let password_hash = config.password.as_ref().map(|p| {
474 let hash = crate::protection::legacy_password_hash(p);
475 format!("{:04X}", hash)
476 });
477 self.workbook_xml.workbook_protection = Some(WorkbookProtection {
478 workbook_password: password_hash,
479 lock_structure: if config.lock_structure {
480 Some(true)
481 } else {
482 None
483 },
484 lock_windows: if config.lock_windows {
485 Some(true)
486 } else {
487 None
488 },
489 revisions_password: None,
490 lock_revision: if config.lock_revision {
491 Some(true)
492 } else {
493 None
494 },
495 });
496 }
497
498 pub fn unprotect_workbook(&mut self) {
500 self.workbook_xml.workbook_protection = None;
501 }
502
503 pub fn is_workbook_protected(&self) -> bool {
505 self.workbook_xml.workbook_protection.is_some()
506 }
507
508 pub fn get_theme_color(&self, index: u32, tint: Option<f64>) -> Option<String> {
511 crate::theme::resolve_theme_color(&self.theme_colors, index, tint)
512 }
513
514 pub fn set_defined_name(
520 &mut self,
521 name: &str,
522 value: &str,
523 scope: Option<&str>,
524 comment: Option<&str>,
525 ) -> Result<()> {
526 let dn_scope = self.resolve_defined_name_scope(scope)?;
527 crate::defined_names::set_defined_name(
528 &mut self.workbook_xml,
529 name,
530 value,
531 dn_scope,
532 comment,
533 )
534 }
535
536 pub fn get_defined_name(
542 &self,
543 name: &str,
544 scope: Option<&str>,
545 ) -> Result<Option<crate::defined_names::DefinedNameInfo>> {
546 let dn_scope = self.resolve_defined_name_scope(scope)?;
547 Ok(crate::defined_names::get_defined_name(
548 &self.workbook_xml,
549 name,
550 dn_scope,
551 ))
552 }
553
554 pub fn get_all_defined_names(&self) -> Vec<crate::defined_names::DefinedNameInfo> {
556 crate::defined_names::get_all_defined_names(&self.workbook_xml)
557 }
558
559 pub fn delete_defined_name(&mut self, name: &str, scope: Option<&str>) -> Result<()> {
563 let dn_scope = self.resolve_defined_name_scope(scope)?;
564 crate::defined_names::delete_defined_name(&mut self.workbook_xml, name, dn_scope)
565 }
566
567 pub fn protect_sheet(
571 &mut self,
572 sheet: &str,
573 config: &crate::sheet::SheetProtectionConfig,
574 ) -> Result<()> {
575 let ws = self.worksheet_mut(sheet)?;
576 crate::sheet::protect_sheet(ws, config)
577 }
578
579 pub fn unprotect_sheet(&mut self, sheet: &str) -> Result<()> {
581 let ws = self.worksheet_mut(sheet)?;
582 crate::sheet::unprotect_sheet(ws)
583 }
584
585 pub fn is_sheet_protected(&self, sheet: &str) -> Result<bool> {
587 let ws = self.worksheet_ref(sheet)?;
588 Ok(crate::sheet::is_sheet_protected(ws))
589 }
590
591 pub fn set_sheet_view_options(
595 &mut self,
596 sheet: &str,
597 opts: &crate::sheet::SheetViewOptions,
598 ) -> Result<()> {
599 let ws = self.worksheet_mut(sheet)?;
600 crate::sheet::set_sheet_view_options(ws, opts)
601 }
602
603 pub fn get_sheet_view_options(&self, sheet: &str) -> Result<crate::sheet::SheetViewOptions> {
605 let ws = self.worksheet_ref(sheet)?;
606 Ok(crate::sheet::get_sheet_view_options(ws))
607 }
608
609 pub fn set_sheet_visibility(
614 &mut self,
615 sheet: &str,
616 visibility: crate::sheet::SheetVisibility,
617 ) -> Result<()> {
618 let idx = self.sheet_index(sheet)?;
619
620 if visibility != crate::sheet::SheetVisibility::Visible {
621 let visible_count = self
622 .workbook_xml
623 .sheets
624 .sheets
625 .iter()
626 .enumerate()
627 .filter(|(i, entry)| {
628 if *i == idx {
629 return false;
630 }
631 crate::sheet::SheetVisibility::from_xml_str(entry.state.as_deref())
632 == crate::sheet::SheetVisibility::Visible
633 })
634 .count();
635 if visible_count == 0 {
636 return Err(Error::InvalidArgument(
637 "cannot hide the last visible sheet".to_string(),
638 ));
639 }
640 }
641
642 self.workbook_xml.sheets.sheets[idx].state = visibility.as_xml_str().map(|s| s.to_string());
643 Ok(())
644 }
645
646 pub fn get_sheet_visibility(&self, sheet: &str) -> Result<crate::sheet::SheetVisibility> {
648 let idx = self.sheet_index(sheet)?;
649 let entry = &self.workbook_xml.sheets.sheets[idx];
650 Ok(crate::sheet::SheetVisibility::from_xml_str(
651 entry.state.as_deref(),
652 ))
653 }
654
655 pub fn get_vba_project(&self) -> Option<&[u8]> {
660 self.vba_blob.as_deref()
661 }
662
663 pub fn get_vba_modules(&self) -> Result<Option<crate::vba::VbaProject>> {
673 match &self.vba_blob {
674 None => Ok(None),
675 Some(bin) => {
676 let project = crate::vba::extract_vba_modules(bin)?;
677 Ok(Some(project))
678 }
679 }
680 }
681
682 fn hydrate_form_controls(&mut self, idx: usize) {
692 while self.sheet_form_controls.len() <= idx {
693 self.sheet_form_controls.push(vec![]);
694 }
695 if !self.sheet_form_controls[idx].is_empty() {
696 return;
697 }
698 if let Some(Some(vml_bytes)) = self.sheet_vml.get(idx) {
699 let vml_str = String::from_utf8_lossy(vml_bytes);
700 let parsed = crate::control::parse_form_controls(&vml_str);
701 if !parsed.is_empty() {
702 self.sheet_form_controls[idx] =
703 parsed.iter().map(|info| info.to_config()).collect();
704 let cleaned = crate::control::strip_form_control_shapes_from_vml(vml_bytes);
708 self.sheet_vml[idx] = cleaned;
709 }
710 }
711 }
712
713 pub fn add_form_control(
719 &mut self,
720 sheet: &str,
721 config: crate::control::FormControlConfig,
722 ) -> Result<()> {
723 let idx = self.sheet_index(sheet)?;
724 config.validate()?;
725 self.hydrate_form_controls(idx);
726 self.sheet_form_controls[idx].push(config);
727 if idx < self.sheet_vml.len() {
729 self.sheet_vml[idx] = None;
730 }
731 Ok(())
732 }
733
734 pub fn get_form_controls(
739 &mut self,
740 sheet: &str,
741 ) -> Result<Vec<crate::control::FormControlInfo>> {
742 let idx = self.sheet_index(sheet)?;
743 self.hydrate_form_controls(idx);
744
745 let controls = &self.sheet_form_controls[idx];
746 if !controls.is_empty() {
747 let vml = crate::control::build_form_control_vml(controls, 1025);
748 return Ok(crate::control::parse_form_controls(&vml));
749 }
750
751 Ok(vec![])
752 }
753
754 pub fn delete_form_control(&mut self, sheet: &str, index: usize) -> Result<()> {
756 let idx = self.sheet_index(sheet)?;
757 self.hydrate_form_controls(idx);
758 let controls = &mut self.sheet_form_controls[idx];
759 if index >= controls.len() {
760 return Err(Error::InvalidArgument(format!(
761 "form control index {index} out of bounds (sheet has {} controls)",
762 controls.len()
763 )));
764 }
765 controls.remove(index);
766 if idx < self.sheet_vml.len() {
768 self.sheet_vml[idx] = None;
769 }
770 Ok(())
771 }
772
773 fn resolve_defined_name_scope(
775 &self,
776 scope: Option<&str>,
777 ) -> Result<crate::defined_names::DefinedNameScope> {
778 match scope {
779 None => Ok(crate::defined_names::DefinedNameScope::Workbook),
780 Some(sheet_name) => {
781 let idx = self.sheet_index(sheet_name)?;
782 Ok(crate::defined_names::DefinedNameScope::Sheet(idx as u32))
783 }
784 }
785 }
786
787 pub fn render_to_svg(&self, options: &crate::render::RenderOptions) -> Result<String> {
793 let ws = self.worksheet_ref(&options.sheet_name)?;
794 crate::render::render_to_svg(ws, &self.sst_runtime, &self.stylesheet, options)
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use tempfile::TempDir;
802
803 #[test]
804 fn test_workbook_add_data_validation() {
805 let mut wb = Workbook::new();
806 let config =
807 crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No", "Maybe"]);
808 wb.add_data_validation("Sheet1", &config).unwrap();
809
810 let validations = wb.get_data_validations("Sheet1").unwrap();
811 assert_eq!(validations.len(), 1);
812 assert_eq!(validations[0].sqref, "A1:A100");
813 }
814
815 #[test]
816 fn test_workbook_remove_data_validation() {
817 let mut wb = Workbook::new();
818 let config1 = crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
819 let config2 = crate::validation::DataValidationConfig::whole_number("B1:B100", 1, 100);
820 wb.add_data_validation("Sheet1", &config1).unwrap();
821 wb.add_data_validation("Sheet1", &config2).unwrap();
822
823 wb.remove_data_validation("Sheet1", "A1:A100").unwrap();
824
825 let validations = wb.get_data_validations("Sheet1").unwrap();
826 assert_eq!(validations.len(), 1);
827 assert_eq!(validations[0].sqref, "B1:B100");
828 }
829
830 #[test]
831 fn test_workbook_data_validation_sheet_not_found() {
832 let mut wb = Workbook::new();
833 let config = crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
834 let result = wb.add_data_validation("NoSheet", &config);
835 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
836 }
837
838 #[test]
839 fn test_workbook_data_validation_roundtrip() {
840 let dir = TempDir::new().unwrap();
841 let path = dir.path().join("validation_roundtrip.xlsx");
842
843 let mut wb = Workbook::new();
844 let config =
845 crate::validation::DataValidationConfig::dropdown("A1:A50", &["Red", "Blue", "Green"]);
846 wb.add_data_validation("Sheet1", &config).unwrap();
847 wb.save(&path).unwrap();
848
849 let wb2 = Workbook::open(&path).unwrap();
850 let validations = wb2.get_data_validations("Sheet1").unwrap();
851 assert_eq!(validations.len(), 1);
852 assert_eq!(validations[0].sqref, "A1:A50");
853 assert_eq!(
854 validations[0].validation_type,
855 crate::validation::ValidationType::List
856 );
857 }
858
859 #[test]
860 fn test_workbook_add_comment() {
861 let mut wb = Workbook::new();
862 let config = crate::comment::CommentConfig {
863 cell: "A1".to_string(),
864 author: "Alice".to_string(),
865 text: "Test comment".to_string(),
866 };
867 wb.add_comment("Sheet1", &config).unwrap();
868
869 let comments = wb.get_comments("Sheet1").unwrap();
870 assert_eq!(comments.len(), 1);
871 assert_eq!(comments[0].cell, "A1");
872 assert_eq!(comments[0].author, "Alice");
873 assert_eq!(comments[0].text, "Test comment");
874 }
875
876 #[test]
877 fn test_workbook_remove_comment() {
878 let mut wb = Workbook::new();
879 let config = crate::comment::CommentConfig {
880 cell: "A1".to_string(),
881 author: "Alice".to_string(),
882 text: "Test comment".to_string(),
883 };
884 wb.add_comment("Sheet1", &config).unwrap();
885 wb.remove_comment("Sheet1", "A1").unwrap();
886
887 let comments = wb.get_comments("Sheet1").unwrap();
888 assert!(comments.is_empty());
889 }
890
891 #[test]
892 fn test_workbook_multiple_comments() {
893 let mut wb = Workbook::new();
894 wb.add_comment(
895 "Sheet1",
896 &crate::comment::CommentConfig {
897 cell: "A1".to_string(),
898 author: "Alice".to_string(),
899 text: "First".to_string(),
900 },
901 )
902 .unwrap();
903 wb.add_comment(
904 "Sheet1",
905 &crate::comment::CommentConfig {
906 cell: "B2".to_string(),
907 author: "Bob".to_string(),
908 text: "Second".to_string(),
909 },
910 )
911 .unwrap();
912
913 let comments = wb.get_comments("Sheet1").unwrap();
914 assert_eq!(comments.len(), 2);
915 }
916
917 #[test]
918 fn test_workbook_comment_sheet_not_found() {
919 let mut wb = Workbook::new();
920 let config = crate::comment::CommentConfig {
921 cell: "A1".to_string(),
922 author: "Alice".to_string(),
923 text: "Test".to_string(),
924 };
925 let result = wb.add_comment("NoSheet", &config);
926 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
927 }
928
929 #[test]
930 fn test_workbook_comment_roundtrip() {
931 let dir = TempDir::new().unwrap();
932 let path = dir.path().join("comment_roundtrip.xlsx");
933
934 let mut wb = Workbook::new();
935 wb.add_comment(
936 "Sheet1",
937 &crate::comment::CommentConfig {
938 cell: "A1".to_string(),
939 author: "Author".to_string(),
940 text: "A saved comment".to_string(),
941 },
942 )
943 .unwrap();
944 wb.save(&path).unwrap();
945
946 let file = std::fs::File::open(&path).unwrap();
948 let mut archive = zip::ZipArchive::new(file).unwrap();
949 assert!(
950 archive.by_name("xl/comments1.xml").is_ok(),
951 "comments1.xml should be present in the ZIP"
952 );
953 }
954
955 #[test]
956 fn test_workbook_comment_roundtrip_open() {
957 let dir = TempDir::new().unwrap();
958 let path = dir.path().join("comment_roundtrip_open.xlsx");
959
960 let mut wb = Workbook::new();
961 wb.add_comment(
962 "Sheet1",
963 &crate::comment::CommentConfig {
964 cell: "A1".to_string(),
965 author: "Author".to_string(),
966 text: "Persist me".to_string(),
967 },
968 )
969 .unwrap();
970 wb.save(&path).unwrap();
971
972 let wb2 = Workbook::open(&path).unwrap();
973 let comments = wb2.get_comments("Sheet1").unwrap();
974 assert_eq!(comments.len(), 1);
975 assert_eq!(comments[0].cell, "A1");
976 assert_eq!(comments[0].author, "Author");
977 assert_eq!(comments[0].text, "Persist me");
978 }
979
980 #[test]
981 fn test_workbook_comment_produces_vml_part() {
982 let dir = TempDir::new().unwrap();
983 let path = dir.path().join("comment_vml.xlsx");
984
985 let mut wb = Workbook::new();
986 wb.add_comment(
987 "Sheet1",
988 &crate::comment::CommentConfig {
989 cell: "B3".to_string(),
990 author: "Tester".to_string(),
991 text: "VML check".to_string(),
992 },
993 )
994 .unwrap();
995 wb.save(&path).unwrap();
996
997 let file = std::fs::File::open(&path).unwrap();
998 let mut archive = zip::ZipArchive::new(file).unwrap();
999 assert!(
1000 archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok(),
1001 "vmlDrawing1.vml should be present in the ZIP"
1002 );
1003
1004 let mut vml_data = Vec::new();
1006 archive
1007 .by_name("xl/drawings/vmlDrawing1.vml")
1008 .unwrap()
1009 .read_to_end(&mut vml_data)
1010 .unwrap();
1011 let vml_str = String::from_utf8(vml_data).unwrap();
1012 assert!(vml_str.contains("<x:Row>2</x:Row>"));
1013 assert!(vml_str.contains("<x:Column>1</x:Column>"));
1014 assert!(vml_str.contains("ObjectType=\"Note\""));
1015 }
1016
1017 #[test]
1018 fn test_workbook_comment_vml_roundtrip_open() {
1019 let dir = TempDir::new().unwrap();
1020 let path = dir.path().join("comment_vml_roundtrip.xlsx");
1021
1022 let mut wb = Workbook::new();
1023 wb.add_comment(
1024 "Sheet1",
1025 &crate::comment::CommentConfig {
1026 cell: "A1".to_string(),
1027 author: "Author".to_string(),
1028 text: "Roundtrip VML".to_string(),
1029 },
1030 )
1031 .unwrap();
1032 wb.save(&path).unwrap();
1033
1034 let wb2 = Workbook::open(&path).unwrap();
1036 let path2 = dir.path().join("comment_vml_roundtrip2.xlsx");
1037 wb2.save(&path2).unwrap();
1038
1039 let file = std::fs::File::open(&path2).unwrap();
1041 let mut archive = zip::ZipArchive::new(file).unwrap();
1042 assert!(archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok());
1043
1044 let wb3 = Workbook::open(&path2).unwrap();
1046 let comments = wb3.get_comments("Sheet1").unwrap();
1047 assert_eq!(comments.len(), 1);
1048 assert_eq!(comments[0].text, "Roundtrip VML");
1049 }
1050
1051 #[test]
1052 fn test_workbook_comment_vml_legacy_drawing_ref() {
1053 let dir = TempDir::new().unwrap();
1054 let path = dir.path().join("comment_vml_legacy_ref.xlsx");
1055
1056 let mut wb = Workbook::new();
1057 wb.add_comment(
1058 "Sheet1",
1059 &crate::comment::CommentConfig {
1060 cell: "C5".to_string(),
1061 author: "Author".to_string(),
1062 text: "Legacy drawing test".to_string(),
1063 },
1064 )
1065 .unwrap();
1066 wb.save(&path).unwrap();
1067
1068 let file = std::fs::File::open(&path).unwrap();
1070 let mut archive = zip::ZipArchive::new(file).unwrap();
1071 let mut ws_data = Vec::new();
1072 archive
1073 .by_name("xl/worksheets/sheet1.xml")
1074 .unwrap()
1075 .read_to_end(&mut ws_data)
1076 .unwrap();
1077 let ws_str = String::from_utf8(ws_data).unwrap();
1078 assert!(
1079 ws_str.contains("legacyDrawing"),
1080 "worksheet should contain legacyDrawing element"
1081 );
1082 }
1083
1084 #[test]
1085 fn test_workbook_comment_vml_cleanup_on_last_remove() {
1086 let dir = TempDir::new().unwrap();
1087 let path = dir.path().join("comment_vml_cleanup.xlsx");
1088
1089 let mut wb = Workbook::new();
1090 wb.add_comment(
1091 "Sheet1",
1092 &crate::comment::CommentConfig {
1093 cell: "A1".to_string(),
1094 author: "Author".to_string(),
1095 text: "Will be removed".to_string(),
1096 },
1097 )
1098 .unwrap();
1099 wb.remove_comment("Sheet1", "A1").unwrap();
1100 wb.save(&path).unwrap();
1101
1102 let file = std::fs::File::open(&path).unwrap();
1104 let mut archive = zip::ZipArchive::new(file).unwrap();
1105 assert!(
1106 archive.by_name("xl/drawings/vmlDrawing1.vml").is_err(),
1107 "vmlDrawing1.vml should not be present when there are no comments"
1108 );
1109 }
1110
1111 #[test]
1112 fn test_workbook_multiple_comments_vml() {
1113 let dir = TempDir::new().unwrap();
1114 let path = dir.path().join("multi_comment_vml.xlsx");
1115
1116 let mut wb = Workbook::new();
1117 wb.add_comment(
1118 "Sheet1",
1119 &crate::comment::CommentConfig {
1120 cell: "A1".to_string(),
1121 author: "Alice".to_string(),
1122 text: "First".to_string(),
1123 },
1124 )
1125 .unwrap();
1126 wb.add_comment(
1127 "Sheet1",
1128 &crate::comment::CommentConfig {
1129 cell: "D10".to_string(),
1130 author: "Bob".to_string(),
1131 text: "Second".to_string(),
1132 },
1133 )
1134 .unwrap();
1135 wb.save(&path).unwrap();
1136
1137 let file = std::fs::File::open(&path).unwrap();
1138 let mut archive = zip::ZipArchive::new(file).unwrap();
1139 let mut vml_data = Vec::new();
1140 archive
1141 .by_name("xl/drawings/vmlDrawing1.vml")
1142 .unwrap()
1143 .read_to_end(&mut vml_data)
1144 .unwrap();
1145 let vml_str = String::from_utf8(vml_data).unwrap();
1146 assert!(vml_str.contains("_x0000_s1025"));
1148 assert!(vml_str.contains("_x0000_s1026"));
1149 }
1150
1151 #[test]
1152 fn test_workbook_set_auto_filter() {
1153 let mut wb = Workbook::new();
1154 wb.set_auto_filter("Sheet1", "A1:D10").unwrap();
1155
1156 let ws = wb.worksheet_ref("Sheet1").unwrap();
1157 assert!(ws.auto_filter.is_some());
1158 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:D10");
1159 }
1160
1161 #[test]
1162 fn test_workbook_remove_auto_filter() {
1163 let mut wb = Workbook::new();
1164 wb.set_auto_filter("Sheet1", "A1:D10").unwrap();
1165 wb.remove_auto_filter("Sheet1").unwrap();
1166
1167 let ws = wb.worksheet_ref("Sheet1").unwrap();
1168 assert!(ws.auto_filter.is_none());
1169 }
1170
1171 #[test]
1172 fn test_workbook_auto_filter_sheet_not_found() {
1173 let mut wb = Workbook::new();
1174 let result = wb.set_auto_filter("NoSheet", "A1:D10");
1175 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1176 }
1177
1178 #[test]
1179 fn test_workbook_auto_filter_roundtrip() {
1180 let dir = TempDir::new().unwrap();
1181 let path = dir.path().join("autofilter_roundtrip.xlsx");
1182
1183 let mut wb = Workbook::new();
1184 wb.set_auto_filter("Sheet1", "A1:C50").unwrap();
1185 wb.save(&path).unwrap();
1186
1187 let wb2 = Workbook::open(&path).unwrap();
1188 let ws = wb2.worksheet_ref("Sheet1").unwrap();
1189 assert!(ws.auto_filter.is_some());
1190 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:C50");
1191 }
1192
1193 #[test]
1194 fn test_protect_unprotect_workbook() {
1195 let mut wb = Workbook::new();
1196 assert!(!wb.is_workbook_protected());
1197
1198 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
1199 password: None,
1200 lock_structure: true,
1201 lock_windows: false,
1202 lock_revision: false,
1203 });
1204 assert!(wb.is_workbook_protected());
1205
1206 wb.unprotect_workbook();
1207 assert!(!wb.is_workbook_protected());
1208 }
1209
1210 #[test]
1211 fn test_protect_workbook_with_password() {
1212 let mut wb = Workbook::new();
1213 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
1214 password: Some("secret".to_string()),
1215 lock_structure: true,
1216 lock_windows: false,
1217 lock_revision: false,
1218 });
1219
1220 let prot = wb.workbook_xml.workbook_protection.as_ref().unwrap();
1221 assert!(prot.workbook_password.is_some());
1222 let hash_str = prot.workbook_password.as_ref().unwrap();
1223 assert_eq!(hash_str.len(), 4);
1225 assert!(hash_str.chars().all(|c| c.is_ascii_hexdigit()));
1226 assert_eq!(prot.lock_structure, Some(true));
1227 }
1228
1229 #[test]
1230 fn test_protect_workbook_structure_only() {
1231 let mut wb = Workbook::new();
1232 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
1233 password: None,
1234 lock_structure: true,
1235 lock_windows: false,
1236 lock_revision: false,
1237 });
1238
1239 let prot = wb.workbook_xml.workbook_protection.as_ref().unwrap();
1240 assert!(prot.workbook_password.is_none());
1241 assert_eq!(prot.lock_structure, Some(true));
1242 assert!(prot.lock_windows.is_none());
1243 assert!(prot.lock_revision.is_none());
1244 }
1245
1246 #[test]
1247 fn test_protect_workbook_save_roundtrip() {
1248 let dir = TempDir::new().unwrap();
1249 let path = dir.path().join("protected.xlsx");
1250
1251 let mut wb = Workbook::new();
1252 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
1253 password: Some("hello".to_string()),
1254 lock_structure: true,
1255 lock_windows: true,
1256 lock_revision: false,
1257 });
1258 wb.save(&path).unwrap();
1259
1260 let wb2 = Workbook::open(&path).unwrap();
1261 assert!(wb2.is_workbook_protected());
1262 let prot = wb2.workbook_xml.workbook_protection.as_ref().unwrap();
1263 assert!(prot.workbook_password.is_some());
1264 assert_eq!(prot.lock_structure, Some(true));
1265 assert_eq!(prot.lock_windows, Some(true));
1266 }
1267
1268 #[test]
1269 fn test_is_workbook_protected() {
1270 let wb = Workbook::new();
1271 assert!(!wb.is_workbook_protected());
1272
1273 let mut wb2 = Workbook::new();
1274 wb2.protect_workbook(crate::protection::WorkbookProtectionConfig {
1275 password: None,
1276 lock_structure: false,
1277 lock_windows: false,
1278 lock_revision: false,
1279 });
1280 assert!(wb2.is_workbook_protected());
1282 }
1283
1284 #[test]
1285 fn test_unprotect_already_unprotected() {
1286 let mut wb = Workbook::new();
1287 assert!(!wb.is_workbook_protected());
1288 wb.unprotect_workbook();
1290 assert!(!wb.is_workbook_protected());
1291 }
1292
1293 #[test]
1294 fn test_set_and_get_external_hyperlink() {
1295 use crate::hyperlink::HyperlinkType;
1296
1297 let mut wb = Workbook::new();
1298 wb.set_cell_hyperlink(
1299 "Sheet1",
1300 "A1",
1301 HyperlinkType::External("https://example.com".to_string()),
1302 Some("Example"),
1303 Some("Visit Example"),
1304 )
1305 .unwrap();
1306
1307 let info = wb.get_cell_hyperlink("Sheet1", "A1").unwrap().unwrap();
1308 assert_eq!(
1309 info.link_type,
1310 HyperlinkType::External("https://example.com".to_string())
1311 );
1312 assert_eq!(info.display, Some("Example".to_string()));
1313 assert_eq!(info.tooltip, Some("Visit Example".to_string()));
1314 }
1315
1316 #[test]
1317 fn test_set_and_get_internal_hyperlink() {
1318 use crate::hyperlink::HyperlinkType;
1319
1320 let mut wb = Workbook::new();
1321 wb.new_sheet("Data").unwrap();
1322 wb.set_cell_hyperlink(
1323 "Sheet1",
1324 "B2",
1325 HyperlinkType::Internal("Data!A1".to_string()),
1326 Some("Go to Data"),
1327 None,
1328 )
1329 .unwrap();
1330
1331 let info = wb.get_cell_hyperlink("Sheet1", "B2").unwrap().unwrap();
1332 assert_eq!(
1333 info.link_type,
1334 HyperlinkType::Internal("Data!A1".to_string())
1335 );
1336 assert_eq!(info.display, Some("Go to Data".to_string()));
1337 }
1338
1339 #[test]
1340 fn test_set_and_get_email_hyperlink() {
1341 use crate::hyperlink::HyperlinkType;
1342
1343 let mut wb = Workbook::new();
1344 wb.set_cell_hyperlink(
1345 "Sheet1",
1346 "C3",
1347 HyperlinkType::Email("mailto:user@example.com".to_string()),
1348 None,
1349 None,
1350 )
1351 .unwrap();
1352
1353 let info = wb.get_cell_hyperlink("Sheet1", "C3").unwrap().unwrap();
1354 assert_eq!(
1355 info.link_type,
1356 HyperlinkType::Email("mailto:user@example.com".to_string())
1357 );
1358 }
1359
1360 #[test]
1361 fn test_delete_hyperlink_via_workbook() {
1362 use crate::hyperlink::HyperlinkType;
1363
1364 let mut wb = Workbook::new();
1365 wb.set_cell_hyperlink(
1366 "Sheet1",
1367 "A1",
1368 HyperlinkType::External("https://example.com".to_string()),
1369 None,
1370 None,
1371 )
1372 .unwrap();
1373
1374 wb.delete_cell_hyperlink("Sheet1", "A1").unwrap();
1375
1376 let info = wb.get_cell_hyperlink("Sheet1", "A1").unwrap();
1377 assert!(info.is_none());
1378 }
1379
1380 #[test]
1381 fn test_hyperlink_roundtrip_save_open() {
1382 use crate::hyperlink::HyperlinkType;
1383
1384 let dir = TempDir::new().unwrap();
1385 let path = dir.path().join("hyperlink.xlsx");
1386
1387 let mut wb = Workbook::new();
1388 wb.set_cell_hyperlink(
1389 "Sheet1",
1390 "A1",
1391 HyperlinkType::External("https://rust-lang.org".to_string()),
1392 Some("Rust"),
1393 Some("Rust Homepage"),
1394 )
1395 .unwrap();
1396 wb.set_cell_hyperlink(
1397 "Sheet1",
1398 "B1",
1399 HyperlinkType::Internal("Sheet1!C1".to_string()),
1400 Some("Go to C1"),
1401 None,
1402 )
1403 .unwrap();
1404 wb.set_cell_hyperlink(
1405 "Sheet1",
1406 "C1",
1407 HyperlinkType::Email("mailto:hello@example.com".to_string()),
1408 Some("Email"),
1409 None,
1410 )
1411 .unwrap();
1412 wb.save(&path).unwrap();
1413
1414 let wb2 = Workbook::open(&path).unwrap();
1415
1416 let a1 = wb2.get_cell_hyperlink("Sheet1", "A1").unwrap().unwrap();
1418 assert_eq!(
1419 a1.link_type,
1420 HyperlinkType::External("https://rust-lang.org".to_string())
1421 );
1422 assert_eq!(a1.display, Some("Rust".to_string()));
1423 assert_eq!(a1.tooltip, Some("Rust Homepage".to_string()));
1424
1425 let b1 = wb2.get_cell_hyperlink("Sheet1", "B1").unwrap().unwrap();
1427 assert_eq!(
1428 b1.link_type,
1429 HyperlinkType::Internal("Sheet1!C1".to_string())
1430 );
1431 assert_eq!(b1.display, Some("Go to C1".to_string()));
1432
1433 let c1 = wb2.get_cell_hyperlink("Sheet1", "C1").unwrap().unwrap();
1435 assert_eq!(
1436 c1.link_type,
1437 HyperlinkType::Email("mailto:hello@example.com".to_string())
1438 );
1439 assert_eq!(c1.display, Some("Email".to_string()));
1440 }
1441
1442 #[test]
1443 fn test_hyperlink_on_nonexistent_sheet() {
1444 use crate::hyperlink::HyperlinkType;
1445
1446 let mut wb = Workbook::new();
1447 let result = wb.set_cell_hyperlink(
1448 "NoSheet",
1449 "A1",
1450 HyperlinkType::External("https://example.com".to_string()),
1451 None,
1452 None,
1453 );
1454 assert!(result.is_err());
1455 }
1456
1457 #[test]
1458 fn test_set_defined_name_workbook_scope() {
1459 let mut wb = Workbook::new();
1460 wb.set_defined_name("SalesData", "Sheet1!$A$1:$D$10", None, None)
1461 .unwrap();
1462
1463 let info = wb.get_defined_name("SalesData", None).unwrap().unwrap();
1464 assert_eq!(info.name, "SalesData");
1465 assert_eq!(info.value, "Sheet1!$A$1:$D$10");
1466 assert_eq!(info.scope, crate::defined_names::DefinedNameScope::Workbook);
1467 assert!(info.comment.is_none());
1468 }
1469
1470 #[test]
1471 fn test_set_defined_name_sheet_scope() {
1472 let mut wb = Workbook::new();
1473 wb.set_defined_name("LocalRange", "Sheet1!$B$2:$C$5", Some("Sheet1"), None)
1474 .unwrap();
1475
1476 let info = wb
1477 .get_defined_name("LocalRange", Some("Sheet1"))
1478 .unwrap()
1479 .unwrap();
1480 assert_eq!(info.name, "LocalRange");
1481 assert_eq!(info.value, "Sheet1!$B$2:$C$5");
1482 assert_eq!(info.scope, crate::defined_names::DefinedNameScope::Sheet(0));
1483 }
1484
1485 #[test]
1486 fn test_update_existing_defined_name() {
1487 let mut wb = Workbook::new();
1488 wb.set_defined_name("DataRange", "Sheet1!$A$1:$A$10", None, None)
1489 .unwrap();
1490
1491 wb.set_defined_name("DataRange", "Sheet1!$A$1:$A$50", None, Some("Updated"))
1492 .unwrap();
1493
1494 let all = wb.get_all_defined_names();
1495 assert_eq!(all.len(), 1, "should not duplicate the entry");
1496 assert_eq!(all[0].value, "Sheet1!$A$1:$A$50");
1497 assert_eq!(all[0].comment, Some("Updated".to_string()));
1498 }
1499
1500 #[test]
1501 fn test_get_all_defined_names() {
1502 let mut wb = Workbook::new();
1503 wb.new_sheet("Sheet2").unwrap();
1504
1505 wb.set_defined_name("Alpha", "Sheet1!$A$1", None, None)
1506 .unwrap();
1507 wb.set_defined_name("Beta", "Sheet1!$B$1", Some("Sheet1"), None)
1508 .unwrap();
1509 wb.set_defined_name("Gamma", "Sheet2!$C$1", Some("Sheet2"), None)
1510 .unwrap();
1511
1512 let all = wb.get_all_defined_names();
1513 assert_eq!(all.len(), 3);
1514 assert_eq!(all[0].name, "Alpha");
1515 assert_eq!(all[1].name, "Beta");
1516 assert_eq!(all[2].name, "Gamma");
1517 }
1518
1519 #[test]
1520 fn test_delete_defined_name() {
1521 let mut wb = Workbook::new();
1522 wb.set_defined_name("ToDelete", "Sheet1!$A$1", None, None)
1523 .unwrap();
1524 assert!(wb.get_defined_name("ToDelete", None).unwrap().is_some());
1525
1526 wb.delete_defined_name("ToDelete", None).unwrap();
1527 assert!(wb.get_defined_name("ToDelete", None).unwrap().is_none());
1528 }
1529
1530 #[test]
1531 fn test_delete_nonexistent_defined_name_returns_error() {
1532 let mut wb = Workbook::new();
1533 let result = wb.delete_defined_name("Ghost", None);
1534 assert!(result.is_err());
1535 assert!(result.unwrap_err().to_string().contains("Ghost"));
1536 }
1537
1538 #[test]
1539 fn test_defined_name_sheet_scope_requires_existing_sheet() {
1540 let mut wb = Workbook::new();
1541 let result = wb.set_defined_name("TestName", "Sheet1!$A$1", Some("NonExistent"), None);
1542 assert!(result.is_err());
1543 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1544 }
1545
1546 #[test]
1547 fn test_defined_name_roundtrip() {
1548 let dir = TempDir::new().unwrap();
1549 let path = dir.path().join("defined_names.xlsx");
1550
1551 let mut wb = Workbook::new();
1552 wb.set_defined_name("Revenue", "Sheet1!$E$1:$E$100", None, Some("Total revenue"))
1553 .unwrap();
1554 wb.set_defined_name("LocalName", "Sheet1!$A$1", Some("Sheet1"), None)
1555 .unwrap();
1556 wb.save(&path).unwrap();
1557
1558 let wb2 = Workbook::open(&path).unwrap();
1559 let all = wb2.get_all_defined_names();
1560 assert_eq!(all.len(), 2);
1561 assert_eq!(all[0].name, "Revenue");
1562 assert_eq!(all[0].value, "Sheet1!$E$1:$E$100");
1563 assert_eq!(all[0].comment, Some("Total revenue".to_string()));
1564 assert_eq!(all[1].name, "LocalName");
1565 assert_eq!(all[1].value, "Sheet1!$A$1");
1566 assert_eq!(
1567 all[1].scope,
1568 crate::defined_names::DefinedNameScope::Sheet(0)
1569 );
1570 }
1571
1572 #[test]
1573 fn test_protect_sheet_via_workbook() {
1574 let mut wb = Workbook::new();
1575 let config = crate::sheet::SheetProtectionConfig::default();
1576 wb.protect_sheet("Sheet1", &config).unwrap();
1577
1578 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1579 }
1580
1581 #[test]
1582 fn test_unprotect_sheet_via_workbook() {
1583 let mut wb = Workbook::new();
1584 let config = crate::sheet::SheetProtectionConfig::default();
1585 wb.protect_sheet("Sheet1", &config).unwrap();
1586 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1587
1588 wb.unprotect_sheet("Sheet1").unwrap();
1589 assert!(!wb.is_sheet_protected("Sheet1").unwrap());
1590 }
1591
1592 #[test]
1593 fn test_protect_sheet_nonexistent_returns_error() {
1594 let mut wb = Workbook::new();
1595 let config = crate::sheet::SheetProtectionConfig::default();
1596 let result = wb.protect_sheet("NoSuchSheet", &config);
1597 assert!(result.is_err());
1598 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1599 }
1600
1601 #[test]
1602 fn test_is_sheet_protected_nonexistent_returns_error() {
1603 let wb = Workbook::new();
1604 let result = wb.is_sheet_protected("NoSuchSheet");
1605 assert!(result.is_err());
1606 }
1607
1608 #[test]
1609 fn test_protect_sheet_with_password_and_permissions() {
1610 let mut wb = Workbook::new();
1611 let config = crate::sheet::SheetProtectionConfig {
1612 password: Some("secret".to_string()),
1613 format_cells: true,
1614 insert_rows: true,
1615 sort: true,
1616 ..crate::sheet::SheetProtectionConfig::default()
1617 };
1618 wb.protect_sheet("Sheet1", &config).unwrap();
1619 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1620 }
1621
1622 #[test]
1623 fn test_sheet_protection_roundtrip() {
1624 let dir = TempDir::new().unwrap();
1625 let path = dir.path().join("sheet_protection.xlsx");
1626
1627 let mut wb = Workbook::new();
1628 let config = crate::sheet::SheetProtectionConfig {
1629 password: Some("pass".to_string()),
1630 format_cells: true,
1631 ..crate::sheet::SheetProtectionConfig::default()
1632 };
1633 wb.protect_sheet("Sheet1", &config).unwrap();
1634 wb.save(&path).unwrap();
1635
1636 let wb2 = Workbook::open(&path).unwrap();
1637 assert!(wb2.is_sheet_protected("Sheet1").unwrap());
1638 }
1639
1640 #[test]
1641 fn test_add_table() {
1642 use crate::table::{TableColumn, TableConfig};
1643
1644 let mut wb = Workbook::new();
1645 let config = TableConfig {
1646 name: "Sales".to_string(),
1647 display_name: "Sales".to_string(),
1648 range: "A1:C5".to_string(),
1649 columns: vec![
1650 TableColumn {
1651 name: "Product".to_string(),
1652 totals_row_function: None,
1653 totals_row_label: None,
1654 },
1655 TableColumn {
1656 name: "Quantity".to_string(),
1657 totals_row_function: None,
1658 totals_row_label: None,
1659 },
1660 TableColumn {
1661 name: "Price".to_string(),
1662 totals_row_function: None,
1663 totals_row_label: None,
1664 },
1665 ],
1666 show_header_row: true,
1667 style_name: Some("TableStyleMedium2".to_string()),
1668 auto_filter: true,
1669 ..TableConfig::default()
1670 };
1671 wb.add_table("Sheet1", &config).unwrap();
1672
1673 let tables = wb.get_tables("Sheet1").unwrap();
1674 assert_eq!(tables.len(), 1);
1675 assert_eq!(tables[0].name, "Sales");
1676 assert_eq!(tables[0].display_name, "Sales");
1677 assert_eq!(tables[0].range, "A1:C5");
1678 assert_eq!(tables[0].columns, vec!["Product", "Quantity", "Price"]);
1679 assert!(tables[0].auto_filter);
1680 assert!(tables[0].show_header_row);
1681 assert_eq!(tables[0].style_name, Some("TableStyleMedium2".to_string()));
1682 }
1683
1684 #[test]
1685 fn test_add_table_duplicate_name_error() {
1686 use crate::table::{TableColumn, TableConfig};
1687
1688 let mut wb = Workbook::new();
1689 let config = TableConfig {
1690 name: "T1".to_string(),
1691 display_name: "T1".to_string(),
1692 range: "A1:B5".to_string(),
1693 columns: vec![TableColumn {
1694 name: "Col".to_string(),
1695 totals_row_function: None,
1696 totals_row_label: None,
1697 }],
1698 ..TableConfig::default()
1699 };
1700 wb.add_table("Sheet1", &config).unwrap();
1701 let result = wb.add_table("Sheet1", &config);
1702 assert!(matches!(
1703 result.unwrap_err(),
1704 Error::TableAlreadyExists { .. }
1705 ));
1706 }
1707
1708 #[test]
1709 fn test_add_table_invalid_config() {
1710 use crate::table::TableConfig;
1711
1712 let mut wb = Workbook::new();
1713 let config = TableConfig {
1714 name: String::new(),
1715 range: "A1:B5".to_string(),
1716 ..TableConfig::default()
1717 };
1718 assert!(wb.add_table("Sheet1", &config).is_err());
1719 }
1720
1721 #[test]
1722 fn test_add_table_sheet_not_found() {
1723 use crate::table::{TableColumn, TableConfig};
1724
1725 let mut wb = Workbook::new();
1726 let config = TableConfig {
1727 name: "T1".to_string(),
1728 display_name: "T1".to_string(),
1729 range: "A1:B5".to_string(),
1730 columns: vec![TableColumn {
1731 name: "Col".to_string(),
1732 totals_row_function: None,
1733 totals_row_label: None,
1734 }],
1735 ..TableConfig::default()
1736 };
1737 let result = wb.add_table("NoSheet", &config);
1738 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1739 }
1740
1741 #[test]
1742 fn test_get_tables_empty() {
1743 let wb = Workbook::new();
1744 let tables = wb.get_tables("Sheet1").unwrap();
1745 assert!(tables.is_empty());
1746 }
1747
1748 #[test]
1749 fn test_get_tables_sheet_not_found() {
1750 let wb = Workbook::new();
1751 let result = wb.get_tables("NoSheet");
1752 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1753 }
1754
1755 #[test]
1756 fn test_delete_table() {
1757 use crate::table::{TableColumn, TableConfig};
1758
1759 let mut wb = Workbook::new();
1760 let config = TableConfig {
1761 name: "T1".to_string(),
1762 display_name: "T1".to_string(),
1763 range: "A1:B5".to_string(),
1764 columns: vec![TableColumn {
1765 name: "Col".to_string(),
1766 totals_row_function: None,
1767 totals_row_label: None,
1768 }],
1769 ..TableConfig::default()
1770 };
1771 wb.add_table("Sheet1", &config).unwrap();
1772 assert_eq!(wb.get_tables("Sheet1").unwrap().len(), 1);
1773
1774 wb.delete_table("Sheet1", "T1").unwrap();
1775 assert!(wb.get_tables("Sheet1").unwrap().is_empty());
1776 }
1777
1778 #[test]
1779 fn test_delete_table_not_found() {
1780 let mut wb = Workbook::new();
1781 let result = wb.delete_table("Sheet1", "NoTable");
1782 assert!(matches!(result.unwrap_err(), Error::TableNotFound { .. }));
1783 }
1784
1785 #[test]
1786 fn test_delete_table_wrong_sheet() {
1787 use crate::table::{TableColumn, TableConfig};
1788
1789 let mut wb = Workbook::new();
1790 wb.new_sheet("Sheet2").unwrap();
1791 let config = TableConfig {
1792 name: "T1".to_string(),
1793 display_name: "T1".to_string(),
1794 range: "A1:B5".to_string(),
1795 columns: vec![TableColumn {
1796 name: "Col".to_string(),
1797 totals_row_function: None,
1798 totals_row_label: None,
1799 }],
1800 ..TableConfig::default()
1801 };
1802 wb.add_table("Sheet1", &config).unwrap();
1803
1804 let result = wb.delete_table("Sheet2", "T1");
1805 assert!(matches!(result.unwrap_err(), Error::TableNotFound { .. }));
1806 assert_eq!(wb.get_tables("Sheet1").unwrap().len(), 1);
1808 }
1809
1810 #[test]
1811 fn test_multiple_tables_on_sheet() {
1812 use crate::table::{TableColumn, TableConfig};
1813
1814 let mut wb = Workbook::new();
1815 let config1 = TableConfig {
1816 name: "T1".to_string(),
1817 display_name: "T1".to_string(),
1818 range: "A1:B5".to_string(),
1819 columns: vec![
1820 TableColumn {
1821 name: "Name".to_string(),
1822 totals_row_function: None,
1823 totals_row_label: None,
1824 },
1825 TableColumn {
1826 name: "Score".to_string(),
1827 totals_row_function: None,
1828 totals_row_label: None,
1829 },
1830 ],
1831 ..TableConfig::default()
1832 };
1833 let config2 = TableConfig {
1834 name: "T2".to_string(),
1835 display_name: "T2".to_string(),
1836 range: "D1:E5".to_string(),
1837 columns: vec![
1838 TableColumn {
1839 name: "City".to_string(),
1840 totals_row_function: None,
1841 totals_row_label: None,
1842 },
1843 TableColumn {
1844 name: "Population".to_string(),
1845 totals_row_function: None,
1846 totals_row_label: None,
1847 },
1848 ],
1849 ..TableConfig::default()
1850 };
1851 wb.add_table("Sheet1", &config1).unwrap();
1852 wb.add_table("Sheet1", &config2).unwrap();
1853
1854 let tables = wb.get_tables("Sheet1").unwrap();
1855 assert_eq!(tables.len(), 2);
1856 assert_eq!(tables[0].name, "T1");
1857 assert_eq!(tables[1].name, "T2");
1858 }
1859
1860 #[test]
1861 fn test_tables_on_different_sheets() {
1862 use crate::table::{TableColumn, TableConfig};
1863
1864 let mut wb = Workbook::new();
1865 wb.new_sheet("Sheet2").unwrap();
1866 let config1 = TableConfig {
1867 name: "T1".to_string(),
1868 display_name: "T1".to_string(),
1869 range: "A1:B5".to_string(),
1870 columns: vec![TableColumn {
1871 name: "Col1".to_string(),
1872 totals_row_function: None,
1873 totals_row_label: None,
1874 }],
1875 ..TableConfig::default()
1876 };
1877 let config2 = TableConfig {
1878 name: "T2".to_string(),
1879 display_name: "T2".to_string(),
1880 range: "A1:B5".to_string(),
1881 columns: vec![TableColumn {
1882 name: "Col2".to_string(),
1883 totals_row_function: None,
1884 totals_row_label: None,
1885 }],
1886 ..TableConfig::default()
1887 };
1888 wb.add_table("Sheet1", &config1).unwrap();
1889 wb.add_table("Sheet2", &config2).unwrap();
1890
1891 assert_eq!(wb.get_tables("Sheet1").unwrap().len(), 1);
1892 assert_eq!(wb.get_tables("Sheet2").unwrap().len(), 1);
1893 assert_eq!(wb.get_tables("Sheet1").unwrap()[0].name, "T1");
1894 assert_eq!(wb.get_tables("Sheet2").unwrap()[0].name, "T2");
1895 }
1896
1897 #[test]
1898 fn test_table_save_produces_zip_parts() {
1899 use crate::table::{TableColumn, TableConfig};
1900
1901 let dir = TempDir::new().unwrap();
1902 let path = dir.path().join("table_parts.xlsx");
1903
1904 let mut wb = Workbook::new();
1905 let config = TableConfig {
1906 name: "Sales".to_string(),
1907 display_name: "Sales".to_string(),
1908 range: "A1:C5".to_string(),
1909 columns: vec![
1910 TableColumn {
1911 name: "Product".to_string(),
1912 totals_row_function: None,
1913 totals_row_label: None,
1914 },
1915 TableColumn {
1916 name: "Qty".to_string(),
1917 totals_row_function: None,
1918 totals_row_label: None,
1919 },
1920 TableColumn {
1921 name: "Price".to_string(),
1922 totals_row_function: None,
1923 totals_row_label: None,
1924 },
1925 ],
1926 style_name: Some("TableStyleMedium2".to_string()),
1927 ..TableConfig::default()
1928 };
1929 wb.add_table("Sheet1", &config).unwrap();
1930 wb.save(&path).unwrap();
1931
1932 let file = std::fs::File::open(&path).unwrap();
1933 let mut archive = zip::ZipArchive::new(file).unwrap();
1934
1935 assert!(
1936 archive.by_name("xl/tables/table1.xml").is_ok(),
1937 "table1.xml should be present in the ZIP"
1938 );
1939 assert!(
1940 archive
1941 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
1942 .is_ok(),
1943 "worksheet rels should be present"
1944 );
1945
1946 let mut table_data = Vec::new();
1948 archive
1949 .by_name("xl/tables/table1.xml")
1950 .unwrap()
1951 .read_to_end(&mut table_data)
1952 .unwrap();
1953 let table_str = String::from_utf8(table_data).unwrap();
1954 assert!(table_str.contains("Sales"));
1955 assert!(table_str.contains("A1:C5"));
1956 assert!(table_str.contains("TableStyleMedium2"));
1957 assert!(table_str.contains("autoFilter"));
1958 assert!(table_str.contains("tableColumn"));
1959
1960 let mut ws_data = Vec::new();
1962 archive
1963 .by_name("xl/worksheets/sheet1.xml")
1964 .unwrap()
1965 .read_to_end(&mut ws_data)
1966 .unwrap();
1967 let ws_str = String::from_utf8(ws_data).unwrap();
1968 assert!(
1969 ws_str.contains("tableParts"),
1970 "worksheet should contain tableParts element"
1971 );
1972 assert!(
1973 ws_str.contains("tablePart"),
1974 "worksheet should contain tablePart reference"
1975 );
1976
1977 let mut ct_data = Vec::new();
1979 archive
1980 .by_name("[Content_Types].xml")
1981 .unwrap()
1982 .read_to_end(&mut ct_data)
1983 .unwrap();
1984 let ct_str = String::from_utf8(ct_data).unwrap();
1985 assert!(
1986 ct_str.contains("table+xml"),
1987 "content types should reference the table"
1988 );
1989
1990 let mut rels_data = Vec::new();
1992 archive
1993 .by_name("xl/worksheets/_rels/sheet1.xml.rels")
1994 .unwrap()
1995 .read_to_end(&mut rels_data)
1996 .unwrap();
1997 let rels_str = String::from_utf8(rels_data).unwrap();
1998 assert!(
1999 rels_str.contains("relationships/table"),
2000 "worksheet rels should reference the table"
2001 );
2002 }
2003
2004 #[test]
2005 fn test_table_roundtrip_save_open() {
2006 use crate::table::{TableColumn, TableConfig};
2007
2008 let dir = TempDir::new().unwrap();
2009 let path = dir.path().join("table_roundtrip.xlsx");
2010
2011 let mut wb = Workbook::new();
2012 let config = TableConfig {
2013 name: "Inventory".to_string(),
2014 display_name: "Inventory".to_string(),
2015 range: "A1:D10".to_string(),
2016 columns: vec![
2017 TableColumn {
2018 name: "Item".to_string(),
2019 totals_row_function: None,
2020 totals_row_label: None,
2021 },
2022 TableColumn {
2023 name: "Stock".to_string(),
2024 totals_row_function: None,
2025 totals_row_label: None,
2026 },
2027 TableColumn {
2028 name: "Price".to_string(),
2029 totals_row_function: None,
2030 totals_row_label: None,
2031 },
2032 TableColumn {
2033 name: "Supplier".to_string(),
2034 totals_row_function: None,
2035 totals_row_label: None,
2036 },
2037 ],
2038 show_header_row: true,
2039 style_name: Some("TableStyleLight1".to_string()),
2040 auto_filter: true,
2041 ..TableConfig::default()
2042 };
2043 wb.add_table("Sheet1", &config).unwrap();
2044 wb.save(&path).unwrap();
2045
2046 let wb2 = Workbook::open(&path).unwrap();
2047 let tables = wb2.get_tables("Sheet1").unwrap();
2048 assert_eq!(tables.len(), 1);
2049 assert_eq!(tables[0].name, "Inventory");
2050 assert_eq!(tables[0].display_name, "Inventory");
2051 assert_eq!(tables[0].range, "A1:D10");
2052 assert_eq!(
2053 tables[0].columns,
2054 vec!["Item", "Stock", "Price", "Supplier"]
2055 );
2056 assert!(tables[0].auto_filter);
2057 assert!(tables[0].show_header_row);
2058 assert_eq!(tables[0].style_name, Some("TableStyleLight1".to_string()));
2059 }
2060
2061 #[test]
2062 fn test_table_roundtrip_multiple_tables() {
2063 use crate::table::{TableColumn, TableConfig};
2064
2065 let dir = TempDir::new().unwrap();
2066 let path = dir.path().join("multi_table_roundtrip.xlsx");
2067
2068 let mut wb = Workbook::new();
2069 wb.new_sheet("Sheet2").unwrap();
2070 wb.add_table(
2071 "Sheet1",
2072 &TableConfig {
2073 name: "T1".to_string(),
2074 display_name: "T1".to_string(),
2075 range: "A1:B5".to_string(),
2076 columns: vec![
2077 TableColumn {
2078 name: "Name".to_string(),
2079 totals_row_function: None,
2080 totals_row_label: None,
2081 },
2082 TableColumn {
2083 name: "Value".to_string(),
2084 totals_row_function: None,
2085 totals_row_label: None,
2086 },
2087 ],
2088 ..TableConfig::default()
2089 },
2090 )
2091 .unwrap();
2092 wb.add_table(
2093 "Sheet2",
2094 &TableConfig {
2095 name: "T2".to_string(),
2096 display_name: "T2".to_string(),
2097 range: "C1:D8".to_string(),
2098 columns: vec![
2099 TableColumn {
2100 name: "Category".to_string(),
2101 totals_row_function: None,
2102 totals_row_label: None,
2103 },
2104 TableColumn {
2105 name: "Count".to_string(),
2106 totals_row_function: None,
2107 totals_row_label: None,
2108 },
2109 ],
2110 auto_filter: false,
2111 ..TableConfig::default()
2112 },
2113 )
2114 .unwrap();
2115 wb.save(&path).unwrap();
2116
2117 let wb2 = Workbook::open(&path).unwrap();
2118 let t1 = wb2.get_tables("Sheet1").unwrap();
2119 assert_eq!(t1.len(), 1);
2120 assert_eq!(t1[0].name, "T1");
2121 assert_eq!(t1[0].range, "A1:B5");
2122
2123 let t2 = wb2.get_tables("Sheet2").unwrap();
2124 assert_eq!(t2.len(), 1);
2125 assert_eq!(t2[0].name, "T2");
2126 assert_eq!(t2[0].range, "C1:D8");
2127 assert!(!t2[0].auto_filter);
2128 }
2129
2130 #[test]
2131 fn test_table_roundtrip_resave() {
2132 use crate::table::{TableColumn, TableConfig};
2133
2134 let dir = TempDir::new().unwrap();
2135 let path1 = dir.path().join("table_resave1.xlsx");
2136 let path2 = dir.path().join("table_resave2.xlsx");
2137
2138 let mut wb = Workbook::new();
2139 wb.add_table(
2140 "Sheet1",
2141 &TableConfig {
2142 name: "T1".to_string(),
2143 display_name: "T1".to_string(),
2144 range: "A1:B3".to_string(),
2145 columns: vec![
2146 TableColumn {
2147 name: "X".to_string(),
2148 totals_row_function: None,
2149 totals_row_label: None,
2150 },
2151 TableColumn {
2152 name: "Y".to_string(),
2153 totals_row_function: None,
2154 totals_row_label: None,
2155 },
2156 ],
2157 ..TableConfig::default()
2158 },
2159 )
2160 .unwrap();
2161 wb.save(&path1).unwrap();
2162
2163 let wb2 = Workbook::open(&path1).unwrap();
2164 wb2.save(&path2).unwrap();
2165
2166 let wb3 = Workbook::open(&path2).unwrap();
2167 let tables = wb3.get_tables("Sheet1").unwrap();
2168 assert_eq!(tables.len(), 1);
2169 assert_eq!(tables[0].name, "T1");
2170 assert_eq!(tables[0].columns, vec!["X", "Y"]);
2171 }
2172
2173 #[test]
2174 fn test_auto_filter_not_regressed_by_tables() {
2175 let dir = TempDir::new().unwrap();
2176 let path = dir.path().join("autofilter_with_table.xlsx");
2177
2178 let mut wb = Workbook::new();
2179 wb.set_auto_filter("Sheet1", "A1:C50").unwrap();
2180 wb.save(&path).unwrap();
2181
2182 let wb2 = Workbook::open(&path).unwrap();
2183 let ws = wb2.worksheet_ref("Sheet1").unwrap();
2184 assert!(ws.auto_filter.is_some());
2185 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:C50");
2186 }
2187
2188 #[test]
2189 fn test_delete_sheet_removes_tables() {
2190 use crate::table::{TableColumn, TableConfig};
2191
2192 let mut wb = Workbook::new();
2193 wb.new_sheet("Sheet2").unwrap();
2194 wb.add_table(
2195 "Sheet1",
2196 &TableConfig {
2197 name: "T1".to_string(),
2198 display_name: "T1".to_string(),
2199 range: "A1:B5".to_string(),
2200 columns: vec![TableColumn {
2201 name: "Col".to_string(),
2202 totals_row_function: None,
2203 totals_row_label: None,
2204 }],
2205 ..TableConfig::default()
2206 },
2207 )
2208 .unwrap();
2209 wb.add_table(
2210 "Sheet2",
2211 &TableConfig {
2212 name: "T2".to_string(),
2213 display_name: "T2".to_string(),
2214 range: "A1:B5".to_string(),
2215 columns: vec![TableColumn {
2216 name: "Col".to_string(),
2217 totals_row_function: None,
2218 totals_row_label: None,
2219 }],
2220 ..TableConfig::default()
2221 },
2222 )
2223 .unwrap();
2224
2225 wb.delete_sheet("Sheet1").unwrap();
2226 let tables = wb.get_tables("Sheet2").unwrap();
2228 assert_eq!(tables.len(), 1);
2229 assert_eq!(tables[0].name, "T2");
2230 }
2231
2232 #[test]
2233 fn test_table_with_no_auto_filter() {
2234 use crate::table::{TableColumn, TableConfig};
2235
2236 let dir = TempDir::new().unwrap();
2237 let path = dir.path().join("table_no_filter.xlsx");
2238
2239 let mut wb = Workbook::new();
2240 wb.add_table(
2241 "Sheet1",
2242 &TableConfig {
2243 name: "Plain".to_string(),
2244 display_name: "Plain".to_string(),
2245 range: "A1:B3".to_string(),
2246 columns: vec![
2247 TableColumn {
2248 name: "A".to_string(),
2249 totals_row_function: None,
2250 totals_row_label: None,
2251 },
2252 TableColumn {
2253 name: "B".to_string(),
2254 totals_row_function: None,
2255 totals_row_label: None,
2256 },
2257 ],
2258 auto_filter: false,
2259 ..TableConfig::default()
2260 },
2261 )
2262 .unwrap();
2263 wb.save(&path).unwrap();
2264
2265 let wb2 = Workbook::open(&path).unwrap();
2266 let tables = wb2.get_tables("Sheet1").unwrap();
2267 assert_eq!(tables.len(), 1);
2268 assert!(!tables[0].auto_filter);
2269 }
2270
2271 #[test]
2272 fn test_set_and_get_sheet_view_options_defaults() {
2273 let wb = Workbook::new();
2274 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2275 assert_eq!(opts.show_gridlines, Some(true));
2276 assert_eq!(opts.show_formulas, Some(false));
2277 assert_eq!(opts.show_row_col_headers, Some(true));
2278 assert_eq!(opts.zoom_scale, Some(100));
2279 assert_eq!(opts.view_mode, Some(crate::sheet::ViewMode::Normal));
2280 assert!(opts.top_left_cell.is_none());
2281 }
2282
2283 #[test]
2284 fn test_set_sheet_view_options_gridlines_off() {
2285 let mut wb = Workbook::new();
2286 wb.set_sheet_view_options(
2287 "Sheet1",
2288 &crate::sheet::SheetViewOptions {
2289 show_gridlines: Some(false),
2290 ..Default::default()
2291 },
2292 )
2293 .unwrap();
2294
2295 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2296 assert_eq!(opts.show_gridlines, Some(false));
2297 }
2298
2299 #[test]
2300 fn test_set_sheet_view_options_zoom() {
2301 let mut wb = Workbook::new();
2302 wb.set_sheet_view_options(
2303 "Sheet1",
2304 &crate::sheet::SheetViewOptions {
2305 zoom_scale: Some(150),
2306 ..Default::default()
2307 },
2308 )
2309 .unwrap();
2310
2311 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2312 assert_eq!(opts.zoom_scale, Some(150));
2313 }
2314
2315 #[test]
2316 fn test_set_sheet_view_options_zoom_invalid_low() {
2317 let mut wb = Workbook::new();
2318 let result = wb.set_sheet_view_options(
2319 "Sheet1",
2320 &crate::sheet::SheetViewOptions {
2321 zoom_scale: Some(5),
2322 ..Default::default()
2323 },
2324 );
2325 assert!(result.is_err());
2326 }
2327
2328 #[test]
2329 fn test_set_sheet_view_options_zoom_invalid_high() {
2330 let mut wb = Workbook::new();
2331 let result = wb.set_sheet_view_options(
2332 "Sheet1",
2333 &crate::sheet::SheetViewOptions {
2334 zoom_scale: Some(500),
2335 ..Default::default()
2336 },
2337 );
2338 assert!(result.is_err());
2339 }
2340
2341 #[test]
2342 fn test_set_sheet_view_options_view_mode() {
2343 let mut wb = Workbook::new();
2344 wb.set_sheet_view_options(
2345 "Sheet1",
2346 &crate::sheet::SheetViewOptions {
2347 view_mode: Some(crate::sheet::ViewMode::PageBreak),
2348 ..Default::default()
2349 },
2350 )
2351 .unwrap();
2352
2353 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2354 assert_eq!(opts.view_mode, Some(crate::sheet::ViewMode::PageBreak));
2355 }
2356
2357 #[test]
2358 fn test_set_sheet_view_options_page_layout() {
2359 let mut wb = Workbook::new();
2360 wb.set_sheet_view_options(
2361 "Sheet1",
2362 &crate::sheet::SheetViewOptions {
2363 view_mode: Some(crate::sheet::ViewMode::PageLayout),
2364 ..Default::default()
2365 },
2366 )
2367 .unwrap();
2368
2369 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2370 assert_eq!(opts.view_mode, Some(crate::sheet::ViewMode::PageLayout));
2371 }
2372
2373 #[test]
2374 fn test_set_sheet_view_options_show_formulas() {
2375 let mut wb = Workbook::new();
2376 wb.set_sheet_view_options(
2377 "Sheet1",
2378 &crate::sheet::SheetViewOptions {
2379 show_formulas: Some(true),
2380 ..Default::default()
2381 },
2382 )
2383 .unwrap();
2384
2385 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2386 assert_eq!(opts.show_formulas, Some(true));
2387 }
2388
2389 #[test]
2390 fn test_set_sheet_view_options_top_left_cell() {
2391 let mut wb = Workbook::new();
2392 wb.set_sheet_view_options(
2393 "Sheet1",
2394 &crate::sheet::SheetViewOptions {
2395 top_left_cell: Some("C10".to_string()),
2396 ..Default::default()
2397 },
2398 )
2399 .unwrap();
2400
2401 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2402 assert_eq!(opts.top_left_cell, Some("C10".to_string()));
2403 }
2404
2405 #[test]
2406 fn test_sheet_view_options_roundtrip() {
2407 let dir = TempDir::new().unwrap();
2408 let path = dir.path().join("view_opts.xlsx");
2409
2410 let mut wb = Workbook::new();
2411 wb.set_sheet_view_options(
2412 "Sheet1",
2413 &crate::sheet::SheetViewOptions {
2414 show_gridlines: Some(false),
2415 show_formulas: Some(true),
2416 zoom_scale: Some(200),
2417 view_mode: Some(crate::sheet::ViewMode::PageBreak),
2418 top_left_cell: Some("B5".to_string()),
2419 ..Default::default()
2420 },
2421 )
2422 .unwrap();
2423 wb.save(&path).unwrap();
2424
2425 let wb2 = Workbook::open(&path).unwrap();
2426 let opts = wb2.get_sheet_view_options("Sheet1").unwrap();
2427 assert_eq!(opts.show_gridlines, Some(false));
2428 assert_eq!(opts.show_formulas, Some(true));
2429 assert_eq!(opts.zoom_scale, Some(200));
2430 assert_eq!(opts.view_mode, Some(crate::sheet::ViewMode::PageBreak));
2431 assert_eq!(opts.top_left_cell, Some("B5".to_string()));
2432 }
2433
2434 #[test]
2435 fn test_sheet_view_options_nonexistent_sheet() {
2436 let wb = Workbook::new();
2437 let result = wb.get_sheet_view_options("NoSheet");
2438 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
2439 }
2440
2441 #[test]
2442 fn test_set_sheet_view_options_preserves_panes() {
2443 let mut wb = Workbook::new();
2444 wb.set_panes("Sheet1", "B2").unwrap();
2445 wb.set_sheet_view_options(
2446 "Sheet1",
2447 &crate::sheet::SheetViewOptions {
2448 zoom_scale: Some(150),
2449 ..Default::default()
2450 },
2451 )
2452 .unwrap();
2453
2454 assert_eq!(wb.get_panes("Sheet1").unwrap(), Some("B2".to_string()));
2455 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2456 assert_eq!(opts.zoom_scale, Some(150));
2457 }
2458
2459 #[test]
2460 fn test_get_sheet_visibility_default() {
2461 let wb = Workbook::new();
2462 let vis = wb.get_sheet_visibility("Sheet1").unwrap();
2463 assert_eq!(vis, crate::sheet::SheetVisibility::Visible);
2464 }
2465
2466 #[test]
2467 fn test_set_sheet_visibility_hidden() {
2468 let mut wb = Workbook::new();
2469 wb.new_sheet("Sheet2").unwrap();
2470 wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::Hidden)
2471 .unwrap();
2472
2473 let vis = wb.get_sheet_visibility("Sheet1").unwrap();
2474 assert_eq!(vis, crate::sheet::SheetVisibility::Hidden);
2475 let vis2 = wb.get_sheet_visibility("Sheet2").unwrap();
2476 assert_eq!(vis2, crate::sheet::SheetVisibility::Visible);
2477 }
2478
2479 #[test]
2480 fn test_set_sheet_visibility_very_hidden() {
2481 let mut wb = Workbook::new();
2482 wb.new_sheet("Sheet2").unwrap();
2483 wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::VeryHidden)
2484 .unwrap();
2485
2486 let vis = wb.get_sheet_visibility("Sheet1").unwrap();
2487 assert_eq!(vis, crate::sheet::SheetVisibility::VeryHidden);
2488 }
2489
2490 #[test]
2491 fn test_set_sheet_visibility_back_to_visible() {
2492 let mut wb = Workbook::new();
2493 wb.new_sheet("Sheet2").unwrap();
2494 wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::Hidden)
2495 .unwrap();
2496 wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::Visible)
2497 .unwrap();
2498
2499 let vis = wb.get_sheet_visibility("Sheet1").unwrap();
2500 assert_eq!(vis, crate::sheet::SheetVisibility::Visible);
2501 }
2502
2503 #[test]
2504 fn test_set_sheet_visibility_cannot_hide_last_visible() {
2505 let mut wb = Workbook::new();
2506 let result = wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::Hidden);
2507 assert!(result.is_err());
2508 assert!(result.unwrap_err().to_string().contains("last visible"));
2509 }
2510
2511 #[test]
2512 fn test_set_sheet_visibility_cannot_hide_all() {
2513 let mut wb = Workbook::new();
2514 wb.new_sheet("Sheet2").unwrap();
2515 wb.set_sheet_visibility("Sheet1", crate::sheet::SheetVisibility::Hidden)
2516 .unwrap();
2517
2518 let result = wb.set_sheet_visibility("Sheet2", crate::sheet::SheetVisibility::Hidden);
2519 assert!(result.is_err());
2520 }
2521
2522 #[test]
2523 fn test_sheet_visibility_nonexistent_sheet() {
2524 let wb = Workbook::new();
2525 let result = wb.get_sheet_visibility("NoSheet");
2526 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
2527 }
2528
2529 #[test]
2530 fn test_sheet_visibility_roundtrip() {
2531 let dir = TempDir::new().unwrap();
2532 let path = dir.path().join("visibility.xlsx");
2533
2534 let mut wb = Workbook::new();
2535 wb.new_sheet("Sheet2").unwrap();
2536 wb.new_sheet("Sheet3").unwrap();
2537 wb.set_sheet_visibility("Sheet2", crate::sheet::SheetVisibility::Hidden)
2538 .unwrap();
2539 wb.set_sheet_visibility("Sheet3", crate::sheet::SheetVisibility::VeryHidden)
2540 .unwrap();
2541 wb.save(&path).unwrap();
2542
2543 let wb2 = Workbook::open(&path).unwrap();
2544 assert_eq!(
2545 wb2.get_sheet_visibility("Sheet1").unwrap(),
2546 crate::sheet::SheetVisibility::Visible
2547 );
2548 assert_eq!(
2549 wb2.get_sheet_visibility("Sheet2").unwrap(),
2550 crate::sheet::SheetVisibility::Hidden
2551 );
2552 assert_eq!(
2553 wb2.get_sheet_visibility("Sheet3").unwrap(),
2554 crate::sheet::SheetVisibility::VeryHidden
2555 );
2556 }
2557
2558 #[test]
2559 fn test_sheet_view_options_show_row_col_headers_off() {
2560 let mut wb = Workbook::new();
2561 wb.set_sheet_view_options(
2562 "Sheet1",
2563 &crate::sheet::SheetViewOptions {
2564 show_row_col_headers: Some(false),
2565 ..Default::default()
2566 },
2567 )
2568 .unwrap();
2569
2570 let opts = wb.get_sheet_view_options("Sheet1").unwrap();
2571 assert_eq!(opts.show_row_col_headers, Some(false));
2572 }
2573
2574 #[test]
2575 fn test_table_path_no_collision_after_delete() {
2576 use crate::table::{TableColumn, TableConfig};
2577
2578 let dir = TempDir::new().unwrap();
2579 let path = dir.path().join("table_path_collision.xlsx");
2580
2581 let mut wb = Workbook::new();
2582 let make_config = |name: &str, range: &str| TableConfig {
2583 name: name.to_string(),
2584 display_name: name.to_string(),
2585 range: range.to_string(),
2586 columns: vec![
2587 TableColumn {
2588 name: "A".to_string(),
2589 totals_row_function: None,
2590 totals_row_label: None,
2591 },
2592 TableColumn {
2593 name: "B".to_string(),
2594 totals_row_function: None,
2595 totals_row_label: None,
2596 },
2597 ],
2598 ..TableConfig::default()
2599 };
2600
2601 wb.add_table("Sheet1", &make_config("T1", "A1:B5")).unwrap();
2602 wb.add_table("Sheet1", &make_config("T2", "D1:E5")).unwrap();
2603 wb.delete_table("Sheet1", "T1").unwrap();
2604 wb.add_table("Sheet1", &make_config("T3", "G1:H5")).unwrap();
2605
2606 let paths: Vec<&str> = wb.tables.iter().map(|(p, _, _)| p.as_str()).collect();
2607 let mut unique_paths = paths.clone();
2608 unique_paths.sort();
2609 unique_paths.dedup();
2610 assert_eq!(
2611 paths.len(),
2612 unique_paths.len(),
2613 "table paths must be unique: {:?}",
2614 paths
2615 );
2616
2617 wb.save(&path).unwrap();
2618 let wb2 = Workbook::open(&path).unwrap();
2619 let tables = wb2.get_tables("Sheet1").unwrap();
2620 assert_eq!(tables.len(), 2);
2621 let names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect();
2622 assert!(names.contains(&"T2"));
2623 assert!(names.contains(&"T3"));
2624 }
2625
2626 #[test]
2627 fn test_dangling_table_parts_after_reopen_delete_save() {
2628 use crate::table::{TableColumn, TableConfig};
2629
2630 let dir = TempDir::new().unwrap();
2631 let path1 = dir.path().join("dangling_tp_step1.xlsx");
2632 let path2 = dir.path().join("dangling_tp_step2.xlsx");
2633
2634 let mut wb = Workbook::new();
2635 wb.add_table(
2636 "Sheet1",
2637 &TableConfig {
2638 name: "T1".to_string(),
2639 display_name: "T1".to_string(),
2640 range: "A1:B5".to_string(),
2641 columns: vec![
2642 TableColumn {
2643 name: "X".to_string(),
2644 totals_row_function: None,
2645 totals_row_label: None,
2646 },
2647 TableColumn {
2648 name: "Y".to_string(),
2649 totals_row_function: None,
2650 totals_row_label: None,
2651 },
2652 ],
2653 ..TableConfig::default()
2654 },
2655 )
2656 .unwrap();
2657 wb.save(&path1).unwrap();
2658
2659 let mut wb2 = Workbook::open(&path1).unwrap();
2660 assert_eq!(wb2.get_tables("Sheet1").unwrap().len(), 1);
2661 wb2.delete_table("Sheet1", "T1").unwrap();
2662 wb2.save(&path2).unwrap();
2663
2664 let file = std::fs::File::open(&path2).unwrap();
2665 let mut archive = zip::ZipArchive::new(file).unwrap();
2666 let mut ws_data = Vec::new();
2667 archive
2668 .by_name("xl/worksheets/sheet1.xml")
2669 .unwrap()
2670 .read_to_end(&mut ws_data)
2671 .unwrap();
2672 let ws_str = String::from_utf8(ws_data).unwrap();
2673 assert!(
2674 !ws_str.contains("tableParts"),
2675 "worksheet XML must not contain tableParts after all tables are deleted"
2676 );
2677
2678 assert!(
2679 archive.by_name("xl/tables/table1.xml").is_err(),
2680 "table1.xml must not be present after deletion"
2681 );
2682
2683 let wb3 = Workbook::open(&path2).unwrap();
2684 assert!(wb3.get_tables("Sheet1").unwrap().is_empty());
2685 }
2686
2687 #[test]
2688 fn test_workbook_add_threaded_comment() {
2689 let mut wb = Workbook::new();
2690 let input = crate::threaded_comment::ThreadedCommentInput {
2691 author: "Alice".to_string(),
2692 text: "Hello thread".to_string(),
2693 parent_id: None,
2694 };
2695 let id = wb.add_threaded_comment("Sheet1", "A1", &input).unwrap();
2696 assert!(!id.is_empty());
2697
2698 let comments = wb.get_threaded_comments("Sheet1").unwrap();
2699 assert_eq!(comments.len(), 1);
2700 assert_eq!(comments[0].cell_ref, "A1");
2701 assert_eq!(comments[0].text, "Hello thread");
2702 assert_eq!(comments[0].author, "Alice");
2703 }
2704
2705 #[test]
2706 fn test_workbook_threaded_comment_reply() {
2707 let mut wb = Workbook::new();
2708 let parent_id = wb
2709 .add_threaded_comment(
2710 "Sheet1",
2711 "A1",
2712 &crate::threaded_comment::ThreadedCommentInput {
2713 author: "Alice".to_string(),
2714 text: "Initial".to_string(),
2715 parent_id: None,
2716 },
2717 )
2718 .unwrap();
2719
2720 wb.add_threaded_comment(
2721 "Sheet1",
2722 "A1",
2723 &crate::threaded_comment::ThreadedCommentInput {
2724 author: "Bob".to_string(),
2725 text: "Reply".to_string(),
2726 parent_id: Some(parent_id.clone()),
2727 },
2728 )
2729 .unwrap();
2730
2731 let comments = wb.get_threaded_comments("Sheet1").unwrap();
2732 assert_eq!(comments.len(), 2);
2733 assert_eq!(comments[1].parent_id, Some(parent_id));
2734 }
2735
2736 #[test]
2737 fn test_workbook_threaded_comments_by_cell() {
2738 let mut wb = Workbook::new();
2739 wb.add_threaded_comment(
2740 "Sheet1",
2741 "A1",
2742 &crate::threaded_comment::ThreadedCommentInput {
2743 author: "Alice".to_string(),
2744 text: "On A1".to_string(),
2745 parent_id: None,
2746 },
2747 )
2748 .unwrap();
2749 wb.add_threaded_comment(
2750 "Sheet1",
2751 "B2",
2752 &crate::threaded_comment::ThreadedCommentInput {
2753 author: "Bob".to_string(),
2754 text: "On B2".to_string(),
2755 parent_id: None,
2756 },
2757 )
2758 .unwrap();
2759
2760 let a1 = wb.get_threaded_comments_by_cell("Sheet1", "A1").unwrap();
2761 assert_eq!(a1.len(), 1);
2762 assert_eq!(a1[0].text, "On A1");
2763
2764 let b2 = wb.get_threaded_comments_by_cell("Sheet1", "B2").unwrap();
2765 assert_eq!(b2.len(), 1);
2766 assert_eq!(b2[0].text, "On B2");
2767 }
2768
2769 #[test]
2770 fn test_workbook_delete_threaded_comment() {
2771 let mut wb = Workbook::new();
2772 let id = wb
2773 .add_threaded_comment(
2774 "Sheet1",
2775 "A1",
2776 &crate::threaded_comment::ThreadedCommentInput {
2777 author: "Alice".to_string(),
2778 text: "Delete me".to_string(),
2779 parent_id: None,
2780 },
2781 )
2782 .unwrap();
2783
2784 wb.delete_threaded_comment("Sheet1", &id).unwrap();
2785 let comments = wb.get_threaded_comments("Sheet1").unwrap();
2786 assert!(comments.is_empty());
2787 }
2788
2789 #[test]
2790 fn test_workbook_resolve_threaded_comment() {
2791 let mut wb = Workbook::new();
2792 let id = wb
2793 .add_threaded_comment(
2794 "Sheet1",
2795 "A1",
2796 &crate::threaded_comment::ThreadedCommentInput {
2797 author: "Alice".to_string(),
2798 text: "Resolve me".to_string(),
2799 parent_id: None,
2800 },
2801 )
2802 .unwrap();
2803
2804 wb.resolve_threaded_comment("Sheet1", &id, true).unwrap();
2805 let comments = wb.get_threaded_comments("Sheet1").unwrap();
2806 assert!(comments[0].done);
2807
2808 wb.resolve_threaded_comment("Sheet1", &id, false).unwrap();
2809 let comments = wb.get_threaded_comments("Sheet1").unwrap();
2810 assert!(!comments[0].done);
2811 }
2812
2813 #[test]
2814 fn test_workbook_add_person() {
2815 let mut wb = Workbook::new();
2816 let id = wb.add_person(&crate::threaded_comment::PersonInput {
2817 display_name: "Alice".to_string(),
2818 user_id: Some("alice@example.com".to_string()),
2819 provider_id: Some("ADAL".to_string()),
2820 });
2821 assert!(!id.is_empty());
2822
2823 let persons = wb.get_persons();
2824 assert_eq!(persons.len(), 1);
2825 assert_eq!(persons[0].display_name, "Alice");
2826 }
2827
2828 #[test]
2829 fn test_workbook_threaded_comment_roundtrip() {
2830 let dir = TempDir::new().unwrap();
2831 let path = dir.path().join("threaded_comment_roundtrip.xlsx");
2832
2833 let mut wb = Workbook::new();
2834 let id = wb
2835 .add_threaded_comment(
2836 "Sheet1",
2837 "A1",
2838 &crate::threaded_comment::ThreadedCommentInput {
2839 author: "Alice".to_string(),
2840 text: "Persisted comment".to_string(),
2841 parent_id: None,
2842 },
2843 )
2844 .unwrap();
2845 wb.resolve_threaded_comment("Sheet1", &id, true).unwrap();
2846 wb.save(&path).unwrap();
2847
2848 let wb2 = Workbook::open(&path).unwrap();
2849 let comments = wb2.get_threaded_comments("Sheet1").unwrap();
2850 assert_eq!(comments.len(), 1);
2851 assert_eq!(comments[0].cell_ref, "A1");
2852 assert_eq!(comments[0].text, "Persisted comment");
2853 assert_eq!(comments[0].author, "Alice");
2854 assert!(comments[0].done);
2855
2856 let persons = wb2.get_persons();
2857 assert_eq!(persons.len(), 1);
2858 assert_eq!(persons[0].display_name, "Alice");
2859 }
2860
2861 #[test]
2862 fn test_workbook_threaded_comment_buffer_roundtrip() {
2863 let mut wb = Workbook::new();
2864 let parent_id = wb
2865 .add_threaded_comment(
2866 "Sheet1",
2867 "B2",
2868 &crate::threaded_comment::ThreadedCommentInput {
2869 author: "Bob".to_string(),
2870 text: "Buffer test".to_string(),
2871 parent_id: None,
2872 },
2873 )
2874 .unwrap();
2875 wb.add_threaded_comment(
2876 "Sheet1",
2877 "B2",
2878 &crate::threaded_comment::ThreadedCommentInput {
2879 author: "Alice".to_string(),
2880 text: "Buffer reply".to_string(),
2881 parent_id: Some(parent_id.clone()),
2882 },
2883 )
2884 .unwrap();
2885
2886 let buf = wb.save_to_buffer().unwrap();
2887 let wb2 = Workbook::open_from_buffer(&buf).unwrap();
2888
2889 let comments = wb2.get_threaded_comments("Sheet1").unwrap();
2890 assert_eq!(comments.len(), 2);
2891 assert_eq!(comments[0].text, "Buffer test");
2892 assert_eq!(comments[1].text, "Buffer reply");
2893 assert_eq!(comments[1].parent_id, Some(parent_id));
2894 }
2895
2896 #[test]
2897 fn test_workbook_threaded_comment_multiple_sheets() {
2898 let dir = TempDir::new().unwrap();
2899 let path = dir.path().join("tc_multi_sheet.xlsx");
2900
2901 let mut wb = Workbook::new();
2902 wb.new_sheet("Sheet2").unwrap();
2903
2904 wb.add_threaded_comment(
2905 "Sheet1",
2906 "A1",
2907 &crate::threaded_comment::ThreadedCommentInput {
2908 author: "Alice".to_string(),
2909 text: "Sheet1 comment".to_string(),
2910 parent_id: None,
2911 },
2912 )
2913 .unwrap();
2914 wb.add_threaded_comment(
2915 "Sheet2",
2916 "C3",
2917 &crate::threaded_comment::ThreadedCommentInput {
2918 author: "Bob".to_string(),
2919 text: "Sheet2 comment".to_string(),
2920 parent_id: None,
2921 },
2922 )
2923 .unwrap();
2924 wb.save(&path).unwrap();
2925
2926 let wb2 = Workbook::open(&path).unwrap();
2927 let s1 = wb2.get_threaded_comments("Sheet1").unwrap();
2928 assert_eq!(s1.len(), 1);
2929 assert_eq!(s1[0].text, "Sheet1 comment");
2930
2931 let s2 = wb2.get_threaded_comments("Sheet2").unwrap();
2932 assert_eq!(s2.len(), 1);
2933 assert_eq!(s2[0].text, "Sheet2 comment");
2934
2935 let persons = wb2.get_persons();
2936 assert_eq!(persons.len(), 2);
2937 }
2938
2939 #[test]
2940 fn test_workbook_threaded_comment_sheet_not_found() {
2941 let wb = Workbook::new();
2942 let result = wb.get_threaded_comments("NoSheet");
2943 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
2944 }
2945}