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