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 set_auto_filter(&mut self, sheet: &str, range: &str) -> Result<()> {
94 let ws = self.worksheet_mut(sheet)?;
95 crate::table::set_auto_filter(ws, range)
96 }
97
98 pub fn remove_auto_filter(&mut self, sheet: &str) -> Result<()> {
100 let ws = self.worksheet_mut(sheet)?;
101 crate::table::remove_auto_filter(ws);
102 Ok(())
103 }
104
105 pub fn set_panes(&mut self, sheet: &str, cell: &str) -> Result<()> {
111 let ws = self.worksheet_mut(sheet)?;
112 crate::sheet::set_panes(ws, cell)
113 }
114
115 pub fn unset_panes(&mut self, sheet: &str) -> Result<()> {
117 let ws = self.worksheet_mut(sheet)?;
118 crate::sheet::unset_panes(ws);
119 Ok(())
120 }
121
122 pub fn get_panes(&self, sheet: &str) -> Result<Option<String>> {
127 let ws = self.worksheet_ref(sheet)?;
128 Ok(crate::sheet::get_panes(ws))
129 }
130
131 pub fn set_page_margins(
133 &mut self,
134 sheet: &str,
135 margins: &crate::page_layout::PageMarginsConfig,
136 ) -> Result<()> {
137 let ws = self.worksheet_mut(sheet)?;
138 crate::page_layout::set_page_margins(ws, margins)
139 }
140
141 pub fn get_page_margins(&self, sheet: &str) -> Result<crate::page_layout::PageMarginsConfig> {
143 let ws = self.worksheet_ref(sheet)?;
144 Ok(crate::page_layout::get_page_margins(ws))
145 }
146
147 pub fn set_page_setup(
152 &mut self,
153 sheet: &str,
154 orientation: Option<crate::page_layout::Orientation>,
155 paper_size: Option<crate::page_layout::PaperSize>,
156 scale: Option<u32>,
157 fit_to_width: Option<u32>,
158 fit_to_height: Option<u32>,
159 ) -> Result<()> {
160 let ws = self.worksheet_mut(sheet)?;
161 crate::page_layout::set_page_setup(
162 ws,
163 orientation,
164 paper_size,
165 scale,
166 fit_to_width,
167 fit_to_height,
168 )
169 }
170
171 pub fn get_orientation(&self, sheet: &str) -> Result<Option<crate::page_layout::Orientation>> {
173 let ws = self.worksheet_ref(sheet)?;
174 Ok(crate::page_layout::get_orientation(ws))
175 }
176
177 pub fn get_paper_size(&self, sheet: &str) -> Result<Option<crate::page_layout::PaperSize>> {
179 let ws = self.worksheet_ref(sheet)?;
180 Ok(crate::page_layout::get_paper_size(ws))
181 }
182
183 pub fn get_page_setup_details(
187 &self,
188 sheet: &str,
189 ) -> Result<(Option<u32>, Option<u32>, Option<u32>)> {
190 let ws = self.worksheet_ref(sheet)?;
191 Ok((
192 crate::page_layout::get_scale(ws),
193 crate::page_layout::get_fit_to_width(ws),
194 crate::page_layout::get_fit_to_height(ws),
195 ))
196 }
197
198 pub fn set_header_footer(
200 &mut self,
201 sheet: &str,
202 header: Option<&str>,
203 footer: Option<&str>,
204 ) -> Result<()> {
205 let ws = self.worksheet_mut(sheet)?;
206 crate::page_layout::set_header_footer(ws, header, footer)
207 }
208
209 pub fn get_header_footer(&self, sheet: &str) -> Result<(Option<String>, Option<String>)> {
211 let ws = self.worksheet_ref(sheet)?;
212 Ok(crate::page_layout::get_header_footer(ws))
213 }
214
215 pub fn set_print_options(
217 &mut self,
218 sheet: &str,
219 grid_lines: Option<bool>,
220 headings: Option<bool>,
221 h_centered: Option<bool>,
222 v_centered: Option<bool>,
223 ) -> Result<()> {
224 let ws = self.worksheet_mut(sheet)?;
225 crate::page_layout::set_print_options(ws, grid_lines, headings, h_centered, v_centered)
226 }
227
228 #[allow(clippy::type_complexity)]
232 pub fn get_print_options(
233 &self,
234 sheet: &str,
235 ) -> Result<(Option<bool>, Option<bool>, Option<bool>, Option<bool>)> {
236 let ws = self.worksheet_ref(sheet)?;
237 Ok(crate::page_layout::get_print_options(ws))
238 }
239
240 pub fn insert_page_break(&mut self, sheet: &str, row: u32) -> Result<()> {
242 let ws = self.worksheet_mut(sheet)?;
243 crate::page_layout::insert_page_break(ws, row)
244 }
245
246 pub fn remove_page_break(&mut self, sheet: &str, row: u32) -> Result<()> {
248 let ws = self.worksheet_mut(sheet)?;
249 crate::page_layout::remove_page_break(ws, row)
250 }
251
252 pub fn get_page_breaks(&self, sheet: &str) -> Result<Vec<u32>> {
254 let ws = self.worksheet_ref(sheet)?;
255 Ok(crate::page_layout::get_page_breaks(ws))
256 }
257
258 pub fn set_cell_hyperlink(
264 &mut self,
265 sheet: &str,
266 cell: &str,
267 link: crate::hyperlink::HyperlinkType,
268 display: Option<&str>,
269 tooltip: Option<&str>,
270 ) -> Result<()> {
271 let sheet_idx = self.sheet_index(sheet)?;
272 let ws = &mut self.worksheets[sheet_idx].1;
273 let rels = self
274 .worksheet_rels
275 .entry(sheet_idx)
276 .or_insert_with(|| Relationships {
277 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
278 relationships: vec![],
279 });
280 crate::hyperlink::set_cell_hyperlink(ws, rels, cell, &link, display, tooltip)
281 }
282
283 pub fn get_cell_hyperlink(
287 &self,
288 sheet: &str,
289 cell: &str,
290 ) -> Result<Option<crate::hyperlink::HyperlinkInfo>> {
291 let sheet_idx = self.sheet_index(sheet)?;
292 let ws = &self.worksheets[sheet_idx].1;
293 let empty_rels = Relationships {
294 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
295 relationships: vec![],
296 };
297 let rels = self.worksheet_rels.get(&sheet_idx).unwrap_or(&empty_rels);
298 crate::hyperlink::get_cell_hyperlink(ws, rels, cell)
299 }
300
301 pub fn delete_cell_hyperlink(&mut self, sheet: &str, cell: &str) -> Result<()> {
306 let sheet_idx = self.sheet_index(sheet)?;
307 let ws = &mut self.worksheets[sheet_idx].1;
308 let rels = self
309 .worksheet_rels
310 .entry(sheet_idx)
311 .or_insert_with(|| Relationships {
312 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
313 relationships: vec![],
314 });
315 crate::hyperlink::delete_cell_hyperlink(ws, rels, cell)
316 }
317
318 pub fn protect_workbook(&mut self, config: WorkbookProtectionConfig) {
320 let password_hash = config.password.as_ref().map(|p| {
321 let hash = crate::protection::legacy_password_hash(p);
322 format!("{:04X}", hash)
323 });
324 self.workbook_xml.workbook_protection = Some(WorkbookProtection {
325 workbook_password: password_hash,
326 lock_structure: if config.lock_structure {
327 Some(true)
328 } else {
329 None
330 },
331 lock_windows: if config.lock_windows {
332 Some(true)
333 } else {
334 None
335 },
336 revisions_password: None,
337 lock_revision: if config.lock_revision {
338 Some(true)
339 } else {
340 None
341 },
342 });
343 }
344
345 pub fn unprotect_workbook(&mut self) {
347 self.workbook_xml.workbook_protection = None;
348 }
349
350 pub fn is_workbook_protected(&self) -> bool {
352 self.workbook_xml.workbook_protection.is_some()
353 }
354
355 pub fn get_theme_color(&self, index: u32, tint: Option<f64>) -> Option<String> {
358 crate::theme::resolve_theme_color(&self.theme_colors, index, tint)
359 }
360
361 pub fn set_defined_name(
367 &mut self,
368 name: &str,
369 value: &str,
370 scope: Option<&str>,
371 comment: Option<&str>,
372 ) -> Result<()> {
373 let dn_scope = self.resolve_defined_name_scope(scope)?;
374 crate::defined_names::set_defined_name(
375 &mut self.workbook_xml,
376 name,
377 value,
378 dn_scope,
379 comment,
380 )
381 }
382
383 pub fn get_defined_name(
389 &self,
390 name: &str,
391 scope: Option<&str>,
392 ) -> Result<Option<crate::defined_names::DefinedNameInfo>> {
393 let dn_scope = self.resolve_defined_name_scope(scope)?;
394 Ok(crate::defined_names::get_defined_name(
395 &self.workbook_xml,
396 name,
397 dn_scope,
398 ))
399 }
400
401 pub fn get_all_defined_names(&self) -> Vec<crate::defined_names::DefinedNameInfo> {
403 crate::defined_names::get_all_defined_names(&self.workbook_xml)
404 }
405
406 pub fn delete_defined_name(&mut self, name: &str, scope: Option<&str>) -> Result<()> {
410 let dn_scope = self.resolve_defined_name_scope(scope)?;
411 crate::defined_names::delete_defined_name(&mut self.workbook_xml, name, dn_scope)
412 }
413
414 pub fn protect_sheet(
418 &mut self,
419 sheet: &str,
420 config: &crate::sheet::SheetProtectionConfig,
421 ) -> Result<()> {
422 let ws = self.worksheet_mut(sheet)?;
423 crate::sheet::protect_sheet(ws, config)
424 }
425
426 pub fn unprotect_sheet(&mut self, sheet: &str) -> Result<()> {
428 let ws = self.worksheet_mut(sheet)?;
429 crate::sheet::unprotect_sheet(ws)
430 }
431
432 pub fn is_sheet_protected(&self, sheet: &str) -> Result<bool> {
434 let ws = self.worksheet_ref(sheet)?;
435 Ok(crate::sheet::is_sheet_protected(ws))
436 }
437
438 fn resolve_defined_name_scope(
440 &self,
441 scope: Option<&str>,
442 ) -> Result<crate::defined_names::DefinedNameScope> {
443 match scope {
444 None => Ok(crate::defined_names::DefinedNameScope::Workbook),
445 Some(sheet_name) => {
446 let idx = self.sheet_index(sheet_name)?;
447 Ok(crate::defined_names::DefinedNameScope::Sheet(idx as u32))
448 }
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use tempfile::TempDir;
457
458 #[test]
459 fn test_workbook_add_data_validation() {
460 let mut wb = Workbook::new();
461 let config =
462 crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No", "Maybe"]);
463 wb.add_data_validation("Sheet1", &config).unwrap();
464
465 let validations = wb.get_data_validations("Sheet1").unwrap();
466 assert_eq!(validations.len(), 1);
467 assert_eq!(validations[0].sqref, "A1:A100");
468 }
469
470 #[test]
471 fn test_workbook_remove_data_validation() {
472 let mut wb = Workbook::new();
473 let config1 = crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
474 let config2 = crate::validation::DataValidationConfig::whole_number("B1:B100", 1, 100);
475 wb.add_data_validation("Sheet1", &config1).unwrap();
476 wb.add_data_validation("Sheet1", &config2).unwrap();
477
478 wb.remove_data_validation("Sheet1", "A1:A100").unwrap();
479
480 let validations = wb.get_data_validations("Sheet1").unwrap();
481 assert_eq!(validations.len(), 1);
482 assert_eq!(validations[0].sqref, "B1:B100");
483 }
484
485 #[test]
486 fn test_workbook_data_validation_sheet_not_found() {
487 let mut wb = Workbook::new();
488 let config = crate::validation::DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
489 let result = wb.add_data_validation("NoSheet", &config);
490 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
491 }
492
493 #[test]
494 fn test_workbook_data_validation_roundtrip() {
495 let dir = TempDir::new().unwrap();
496 let path = dir.path().join("validation_roundtrip.xlsx");
497
498 let mut wb = Workbook::new();
499 let config =
500 crate::validation::DataValidationConfig::dropdown("A1:A50", &["Red", "Blue", "Green"]);
501 wb.add_data_validation("Sheet1", &config).unwrap();
502 wb.save(&path).unwrap();
503
504 let wb2 = Workbook::open(&path).unwrap();
505 let validations = wb2.get_data_validations("Sheet1").unwrap();
506 assert_eq!(validations.len(), 1);
507 assert_eq!(validations[0].sqref, "A1:A50");
508 assert_eq!(
509 validations[0].validation_type,
510 crate::validation::ValidationType::List
511 );
512 }
513
514 #[test]
515 fn test_workbook_add_comment() {
516 let mut wb = Workbook::new();
517 let config = crate::comment::CommentConfig {
518 cell: "A1".to_string(),
519 author: "Alice".to_string(),
520 text: "Test comment".to_string(),
521 };
522 wb.add_comment("Sheet1", &config).unwrap();
523
524 let comments = wb.get_comments("Sheet1").unwrap();
525 assert_eq!(comments.len(), 1);
526 assert_eq!(comments[0].cell, "A1");
527 assert_eq!(comments[0].author, "Alice");
528 assert_eq!(comments[0].text, "Test comment");
529 }
530
531 #[test]
532 fn test_workbook_remove_comment() {
533 let mut wb = Workbook::new();
534 let config = crate::comment::CommentConfig {
535 cell: "A1".to_string(),
536 author: "Alice".to_string(),
537 text: "Test comment".to_string(),
538 };
539 wb.add_comment("Sheet1", &config).unwrap();
540 wb.remove_comment("Sheet1", "A1").unwrap();
541
542 let comments = wb.get_comments("Sheet1").unwrap();
543 assert!(comments.is_empty());
544 }
545
546 #[test]
547 fn test_workbook_multiple_comments() {
548 let mut wb = Workbook::new();
549 wb.add_comment(
550 "Sheet1",
551 &crate::comment::CommentConfig {
552 cell: "A1".to_string(),
553 author: "Alice".to_string(),
554 text: "First".to_string(),
555 },
556 )
557 .unwrap();
558 wb.add_comment(
559 "Sheet1",
560 &crate::comment::CommentConfig {
561 cell: "B2".to_string(),
562 author: "Bob".to_string(),
563 text: "Second".to_string(),
564 },
565 )
566 .unwrap();
567
568 let comments = wb.get_comments("Sheet1").unwrap();
569 assert_eq!(comments.len(), 2);
570 }
571
572 #[test]
573 fn test_workbook_comment_sheet_not_found() {
574 let mut wb = Workbook::new();
575 let config = crate::comment::CommentConfig {
576 cell: "A1".to_string(),
577 author: "Alice".to_string(),
578 text: "Test".to_string(),
579 };
580 let result = wb.add_comment("NoSheet", &config);
581 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
582 }
583
584 #[test]
585 fn test_workbook_comment_roundtrip() {
586 let dir = TempDir::new().unwrap();
587 let path = dir.path().join("comment_roundtrip.xlsx");
588
589 let mut wb = Workbook::new();
590 wb.add_comment(
591 "Sheet1",
592 &crate::comment::CommentConfig {
593 cell: "A1".to_string(),
594 author: "Author".to_string(),
595 text: "A saved comment".to_string(),
596 },
597 )
598 .unwrap();
599 wb.save(&path).unwrap();
600
601 let file = std::fs::File::open(&path).unwrap();
603 let mut archive = zip::ZipArchive::new(file).unwrap();
604 assert!(
605 archive.by_name("xl/comments1.xml").is_ok(),
606 "comments1.xml should be present in the ZIP"
607 );
608 }
609
610 #[test]
611 fn test_workbook_comment_roundtrip_open() {
612 let dir = TempDir::new().unwrap();
613 let path = dir.path().join("comment_roundtrip_open.xlsx");
614
615 let mut wb = Workbook::new();
616 wb.add_comment(
617 "Sheet1",
618 &crate::comment::CommentConfig {
619 cell: "A1".to_string(),
620 author: "Author".to_string(),
621 text: "Persist me".to_string(),
622 },
623 )
624 .unwrap();
625 wb.save(&path).unwrap();
626
627 let wb2 = Workbook::open(&path).unwrap();
628 let comments = wb2.get_comments("Sheet1").unwrap();
629 assert_eq!(comments.len(), 1);
630 assert_eq!(comments[0].cell, "A1");
631 assert_eq!(comments[0].author, "Author");
632 assert_eq!(comments[0].text, "Persist me");
633 }
634
635 #[test]
636 fn test_workbook_comment_produces_vml_part() {
637 let dir = TempDir::new().unwrap();
638 let path = dir.path().join("comment_vml.xlsx");
639
640 let mut wb = Workbook::new();
641 wb.add_comment(
642 "Sheet1",
643 &crate::comment::CommentConfig {
644 cell: "B3".to_string(),
645 author: "Tester".to_string(),
646 text: "VML check".to_string(),
647 },
648 )
649 .unwrap();
650 wb.save(&path).unwrap();
651
652 let file = std::fs::File::open(&path).unwrap();
653 let mut archive = zip::ZipArchive::new(file).unwrap();
654 assert!(
655 archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok(),
656 "vmlDrawing1.vml should be present in the ZIP"
657 );
658
659 let mut vml_data = Vec::new();
661 archive
662 .by_name("xl/drawings/vmlDrawing1.vml")
663 .unwrap()
664 .read_to_end(&mut vml_data)
665 .unwrap();
666 let vml_str = String::from_utf8(vml_data).unwrap();
667 assert!(vml_str.contains("<x:Row>2</x:Row>"));
668 assert!(vml_str.contains("<x:Column>1</x:Column>"));
669 assert!(vml_str.contains("ObjectType=\"Note\""));
670 }
671
672 #[test]
673 fn test_workbook_comment_vml_roundtrip_open() {
674 let dir = TempDir::new().unwrap();
675 let path = dir.path().join("comment_vml_roundtrip.xlsx");
676
677 let mut wb = Workbook::new();
678 wb.add_comment(
679 "Sheet1",
680 &crate::comment::CommentConfig {
681 cell: "A1".to_string(),
682 author: "Author".to_string(),
683 text: "Roundtrip VML".to_string(),
684 },
685 )
686 .unwrap();
687 wb.save(&path).unwrap();
688
689 let wb2 = Workbook::open(&path).unwrap();
691 let path2 = dir.path().join("comment_vml_roundtrip2.xlsx");
692 wb2.save(&path2).unwrap();
693
694 let file = std::fs::File::open(&path2).unwrap();
696 let mut archive = zip::ZipArchive::new(file).unwrap();
697 assert!(archive.by_name("xl/drawings/vmlDrawing1.vml").is_ok());
698
699 let wb3 = Workbook::open(&path2).unwrap();
701 let comments = wb3.get_comments("Sheet1").unwrap();
702 assert_eq!(comments.len(), 1);
703 assert_eq!(comments[0].text, "Roundtrip VML");
704 }
705
706 #[test]
707 fn test_workbook_comment_vml_legacy_drawing_ref() {
708 let dir = TempDir::new().unwrap();
709 let path = dir.path().join("comment_vml_legacy_ref.xlsx");
710
711 let mut wb = Workbook::new();
712 wb.add_comment(
713 "Sheet1",
714 &crate::comment::CommentConfig {
715 cell: "C5".to_string(),
716 author: "Author".to_string(),
717 text: "Legacy drawing test".to_string(),
718 },
719 )
720 .unwrap();
721 wb.save(&path).unwrap();
722
723 let file = std::fs::File::open(&path).unwrap();
725 let mut archive = zip::ZipArchive::new(file).unwrap();
726 let mut ws_data = Vec::new();
727 archive
728 .by_name("xl/worksheets/sheet1.xml")
729 .unwrap()
730 .read_to_end(&mut ws_data)
731 .unwrap();
732 let ws_str = String::from_utf8(ws_data).unwrap();
733 assert!(
734 ws_str.contains("legacyDrawing"),
735 "worksheet should contain legacyDrawing element"
736 );
737 }
738
739 #[test]
740 fn test_workbook_comment_vml_cleanup_on_last_remove() {
741 let dir = TempDir::new().unwrap();
742 let path = dir.path().join("comment_vml_cleanup.xlsx");
743
744 let mut wb = Workbook::new();
745 wb.add_comment(
746 "Sheet1",
747 &crate::comment::CommentConfig {
748 cell: "A1".to_string(),
749 author: "Author".to_string(),
750 text: "Will be removed".to_string(),
751 },
752 )
753 .unwrap();
754 wb.remove_comment("Sheet1", "A1").unwrap();
755 wb.save(&path).unwrap();
756
757 let file = std::fs::File::open(&path).unwrap();
759 let mut archive = zip::ZipArchive::new(file).unwrap();
760 assert!(
761 archive.by_name("xl/drawings/vmlDrawing1.vml").is_err(),
762 "vmlDrawing1.vml should not be present when there are no comments"
763 );
764 }
765
766 #[test]
767 fn test_workbook_multiple_comments_vml() {
768 let dir = TempDir::new().unwrap();
769 let path = dir.path().join("multi_comment_vml.xlsx");
770
771 let mut wb = Workbook::new();
772 wb.add_comment(
773 "Sheet1",
774 &crate::comment::CommentConfig {
775 cell: "A1".to_string(),
776 author: "Alice".to_string(),
777 text: "First".to_string(),
778 },
779 )
780 .unwrap();
781 wb.add_comment(
782 "Sheet1",
783 &crate::comment::CommentConfig {
784 cell: "D10".to_string(),
785 author: "Bob".to_string(),
786 text: "Second".to_string(),
787 },
788 )
789 .unwrap();
790 wb.save(&path).unwrap();
791
792 let file = std::fs::File::open(&path).unwrap();
793 let mut archive = zip::ZipArchive::new(file).unwrap();
794 let mut vml_data = Vec::new();
795 archive
796 .by_name("xl/drawings/vmlDrawing1.vml")
797 .unwrap()
798 .read_to_end(&mut vml_data)
799 .unwrap();
800 let vml_str = String::from_utf8(vml_data).unwrap();
801 assert!(vml_str.contains("_x0000_s1025"));
803 assert!(vml_str.contains("_x0000_s1026"));
804 }
805
806 #[test]
807 fn test_workbook_set_auto_filter() {
808 let mut wb = Workbook::new();
809 wb.set_auto_filter("Sheet1", "A1:D10").unwrap();
810
811 let ws = wb.worksheet_ref("Sheet1").unwrap();
812 assert!(ws.auto_filter.is_some());
813 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:D10");
814 }
815
816 #[test]
817 fn test_workbook_remove_auto_filter() {
818 let mut wb = Workbook::new();
819 wb.set_auto_filter("Sheet1", "A1:D10").unwrap();
820 wb.remove_auto_filter("Sheet1").unwrap();
821
822 let ws = wb.worksheet_ref("Sheet1").unwrap();
823 assert!(ws.auto_filter.is_none());
824 }
825
826 #[test]
827 fn test_workbook_auto_filter_sheet_not_found() {
828 let mut wb = Workbook::new();
829 let result = wb.set_auto_filter("NoSheet", "A1:D10");
830 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
831 }
832
833 #[test]
834 fn test_workbook_auto_filter_roundtrip() {
835 let dir = TempDir::new().unwrap();
836 let path = dir.path().join("autofilter_roundtrip.xlsx");
837
838 let mut wb = Workbook::new();
839 wb.set_auto_filter("Sheet1", "A1:C50").unwrap();
840 wb.save(&path).unwrap();
841
842 let wb2 = Workbook::open(&path).unwrap();
843 let ws = wb2.worksheet_ref("Sheet1").unwrap();
844 assert!(ws.auto_filter.is_some());
845 assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:C50");
846 }
847
848 #[test]
849 fn test_protect_unprotect_workbook() {
850 let mut wb = Workbook::new();
851 assert!(!wb.is_workbook_protected());
852
853 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
854 password: None,
855 lock_structure: true,
856 lock_windows: false,
857 lock_revision: false,
858 });
859 assert!(wb.is_workbook_protected());
860
861 wb.unprotect_workbook();
862 assert!(!wb.is_workbook_protected());
863 }
864
865 #[test]
866 fn test_protect_workbook_with_password() {
867 let mut wb = Workbook::new();
868 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
869 password: Some("secret".to_string()),
870 lock_structure: true,
871 lock_windows: false,
872 lock_revision: false,
873 });
874
875 let prot = wb.workbook_xml.workbook_protection.as_ref().unwrap();
876 assert!(prot.workbook_password.is_some());
877 let hash_str = prot.workbook_password.as_ref().unwrap();
878 assert_eq!(hash_str.len(), 4);
880 assert!(hash_str.chars().all(|c| c.is_ascii_hexdigit()));
881 assert_eq!(prot.lock_structure, Some(true));
882 }
883
884 #[test]
885 fn test_protect_workbook_structure_only() {
886 let mut wb = Workbook::new();
887 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
888 password: None,
889 lock_structure: true,
890 lock_windows: false,
891 lock_revision: false,
892 });
893
894 let prot = wb.workbook_xml.workbook_protection.as_ref().unwrap();
895 assert!(prot.workbook_password.is_none());
896 assert_eq!(prot.lock_structure, Some(true));
897 assert!(prot.lock_windows.is_none());
898 assert!(prot.lock_revision.is_none());
899 }
900
901 #[test]
902 fn test_protect_workbook_save_roundtrip() {
903 let dir = TempDir::new().unwrap();
904 let path = dir.path().join("protected.xlsx");
905
906 let mut wb = Workbook::new();
907 wb.protect_workbook(crate::protection::WorkbookProtectionConfig {
908 password: Some("hello".to_string()),
909 lock_structure: true,
910 lock_windows: true,
911 lock_revision: false,
912 });
913 wb.save(&path).unwrap();
914
915 let wb2 = Workbook::open(&path).unwrap();
916 assert!(wb2.is_workbook_protected());
917 let prot = wb2.workbook_xml.workbook_protection.as_ref().unwrap();
918 assert!(prot.workbook_password.is_some());
919 assert_eq!(prot.lock_structure, Some(true));
920 assert_eq!(prot.lock_windows, Some(true));
921 }
922
923 #[test]
924 fn test_is_workbook_protected() {
925 let wb = Workbook::new();
926 assert!(!wb.is_workbook_protected());
927
928 let mut wb2 = Workbook::new();
929 wb2.protect_workbook(crate::protection::WorkbookProtectionConfig {
930 password: None,
931 lock_structure: false,
932 lock_windows: false,
933 lock_revision: false,
934 });
935 assert!(wb2.is_workbook_protected());
937 }
938
939 #[test]
940 fn test_unprotect_already_unprotected() {
941 let mut wb = Workbook::new();
942 assert!(!wb.is_workbook_protected());
943 wb.unprotect_workbook();
945 assert!(!wb.is_workbook_protected());
946 }
947
948 #[test]
949 fn test_set_and_get_external_hyperlink() {
950 use crate::hyperlink::HyperlinkType;
951
952 let mut wb = Workbook::new();
953 wb.set_cell_hyperlink(
954 "Sheet1",
955 "A1",
956 HyperlinkType::External("https://example.com".to_string()),
957 Some("Example"),
958 Some("Visit Example"),
959 )
960 .unwrap();
961
962 let info = wb.get_cell_hyperlink("Sheet1", "A1").unwrap().unwrap();
963 assert_eq!(
964 info.link_type,
965 HyperlinkType::External("https://example.com".to_string())
966 );
967 assert_eq!(info.display, Some("Example".to_string()));
968 assert_eq!(info.tooltip, Some("Visit Example".to_string()));
969 }
970
971 #[test]
972 fn test_set_and_get_internal_hyperlink() {
973 use crate::hyperlink::HyperlinkType;
974
975 let mut wb = Workbook::new();
976 wb.new_sheet("Data").unwrap();
977 wb.set_cell_hyperlink(
978 "Sheet1",
979 "B2",
980 HyperlinkType::Internal("Data!A1".to_string()),
981 Some("Go to Data"),
982 None,
983 )
984 .unwrap();
985
986 let info = wb.get_cell_hyperlink("Sheet1", "B2").unwrap().unwrap();
987 assert_eq!(
988 info.link_type,
989 HyperlinkType::Internal("Data!A1".to_string())
990 );
991 assert_eq!(info.display, Some("Go to Data".to_string()));
992 }
993
994 #[test]
995 fn test_set_and_get_email_hyperlink() {
996 use crate::hyperlink::HyperlinkType;
997
998 let mut wb = Workbook::new();
999 wb.set_cell_hyperlink(
1000 "Sheet1",
1001 "C3",
1002 HyperlinkType::Email("mailto:user@example.com".to_string()),
1003 None,
1004 None,
1005 )
1006 .unwrap();
1007
1008 let info = wb.get_cell_hyperlink("Sheet1", "C3").unwrap().unwrap();
1009 assert_eq!(
1010 info.link_type,
1011 HyperlinkType::Email("mailto:user@example.com".to_string())
1012 );
1013 }
1014
1015 #[test]
1016 fn test_delete_hyperlink_via_workbook() {
1017 use crate::hyperlink::HyperlinkType;
1018
1019 let mut wb = Workbook::new();
1020 wb.set_cell_hyperlink(
1021 "Sheet1",
1022 "A1",
1023 HyperlinkType::External("https://example.com".to_string()),
1024 None,
1025 None,
1026 )
1027 .unwrap();
1028
1029 wb.delete_cell_hyperlink("Sheet1", "A1").unwrap();
1030
1031 let info = wb.get_cell_hyperlink("Sheet1", "A1").unwrap();
1032 assert!(info.is_none());
1033 }
1034
1035 #[test]
1036 fn test_hyperlink_roundtrip_save_open() {
1037 use crate::hyperlink::HyperlinkType;
1038
1039 let dir = TempDir::new().unwrap();
1040 let path = dir.path().join("hyperlink.xlsx");
1041
1042 let mut wb = Workbook::new();
1043 wb.set_cell_hyperlink(
1044 "Sheet1",
1045 "A1",
1046 HyperlinkType::External("https://rust-lang.org".to_string()),
1047 Some("Rust"),
1048 Some("Rust Homepage"),
1049 )
1050 .unwrap();
1051 wb.set_cell_hyperlink(
1052 "Sheet1",
1053 "B1",
1054 HyperlinkType::Internal("Sheet1!C1".to_string()),
1055 Some("Go to C1"),
1056 None,
1057 )
1058 .unwrap();
1059 wb.set_cell_hyperlink(
1060 "Sheet1",
1061 "C1",
1062 HyperlinkType::Email("mailto:hello@example.com".to_string()),
1063 Some("Email"),
1064 None,
1065 )
1066 .unwrap();
1067 wb.save(&path).unwrap();
1068
1069 let wb2 = Workbook::open(&path).unwrap();
1070
1071 let a1 = wb2.get_cell_hyperlink("Sheet1", "A1").unwrap().unwrap();
1073 assert_eq!(
1074 a1.link_type,
1075 HyperlinkType::External("https://rust-lang.org".to_string())
1076 );
1077 assert_eq!(a1.display, Some("Rust".to_string()));
1078 assert_eq!(a1.tooltip, Some("Rust Homepage".to_string()));
1079
1080 let b1 = wb2.get_cell_hyperlink("Sheet1", "B1").unwrap().unwrap();
1082 assert_eq!(
1083 b1.link_type,
1084 HyperlinkType::Internal("Sheet1!C1".to_string())
1085 );
1086 assert_eq!(b1.display, Some("Go to C1".to_string()));
1087
1088 let c1 = wb2.get_cell_hyperlink("Sheet1", "C1").unwrap().unwrap();
1090 assert_eq!(
1091 c1.link_type,
1092 HyperlinkType::Email("mailto:hello@example.com".to_string())
1093 );
1094 assert_eq!(c1.display, Some("Email".to_string()));
1095 }
1096
1097 #[test]
1098 fn test_hyperlink_on_nonexistent_sheet() {
1099 use crate::hyperlink::HyperlinkType;
1100
1101 let mut wb = Workbook::new();
1102 let result = wb.set_cell_hyperlink(
1103 "NoSheet",
1104 "A1",
1105 HyperlinkType::External("https://example.com".to_string()),
1106 None,
1107 None,
1108 );
1109 assert!(result.is_err());
1110 }
1111
1112 #[test]
1113 fn test_set_defined_name_workbook_scope() {
1114 let mut wb = Workbook::new();
1115 wb.set_defined_name("SalesData", "Sheet1!$A$1:$D$10", None, None)
1116 .unwrap();
1117
1118 let info = wb.get_defined_name("SalesData", None).unwrap().unwrap();
1119 assert_eq!(info.name, "SalesData");
1120 assert_eq!(info.value, "Sheet1!$A$1:$D$10");
1121 assert_eq!(info.scope, crate::defined_names::DefinedNameScope::Workbook);
1122 assert!(info.comment.is_none());
1123 }
1124
1125 #[test]
1126 fn test_set_defined_name_sheet_scope() {
1127 let mut wb = Workbook::new();
1128 wb.set_defined_name("LocalRange", "Sheet1!$B$2:$C$5", Some("Sheet1"), None)
1129 .unwrap();
1130
1131 let info = wb
1132 .get_defined_name("LocalRange", Some("Sheet1"))
1133 .unwrap()
1134 .unwrap();
1135 assert_eq!(info.name, "LocalRange");
1136 assert_eq!(info.value, "Sheet1!$B$2:$C$5");
1137 assert_eq!(info.scope, crate::defined_names::DefinedNameScope::Sheet(0));
1138 }
1139
1140 #[test]
1141 fn test_update_existing_defined_name() {
1142 let mut wb = Workbook::new();
1143 wb.set_defined_name("DataRange", "Sheet1!$A$1:$A$10", None, None)
1144 .unwrap();
1145
1146 wb.set_defined_name("DataRange", "Sheet1!$A$1:$A$50", None, Some("Updated"))
1147 .unwrap();
1148
1149 let all = wb.get_all_defined_names();
1150 assert_eq!(all.len(), 1, "should not duplicate the entry");
1151 assert_eq!(all[0].value, "Sheet1!$A$1:$A$50");
1152 assert_eq!(all[0].comment, Some("Updated".to_string()));
1153 }
1154
1155 #[test]
1156 fn test_get_all_defined_names() {
1157 let mut wb = Workbook::new();
1158 wb.new_sheet("Sheet2").unwrap();
1159
1160 wb.set_defined_name("Alpha", "Sheet1!$A$1", None, None)
1161 .unwrap();
1162 wb.set_defined_name("Beta", "Sheet1!$B$1", Some("Sheet1"), None)
1163 .unwrap();
1164 wb.set_defined_name("Gamma", "Sheet2!$C$1", Some("Sheet2"), None)
1165 .unwrap();
1166
1167 let all = wb.get_all_defined_names();
1168 assert_eq!(all.len(), 3);
1169 assert_eq!(all[0].name, "Alpha");
1170 assert_eq!(all[1].name, "Beta");
1171 assert_eq!(all[2].name, "Gamma");
1172 }
1173
1174 #[test]
1175 fn test_delete_defined_name() {
1176 let mut wb = Workbook::new();
1177 wb.set_defined_name("ToDelete", "Sheet1!$A$1", None, None)
1178 .unwrap();
1179 assert!(wb.get_defined_name("ToDelete", None).unwrap().is_some());
1180
1181 wb.delete_defined_name("ToDelete", None).unwrap();
1182 assert!(wb.get_defined_name("ToDelete", None).unwrap().is_none());
1183 }
1184
1185 #[test]
1186 fn test_delete_nonexistent_defined_name_returns_error() {
1187 let mut wb = Workbook::new();
1188 let result = wb.delete_defined_name("Ghost", None);
1189 assert!(result.is_err());
1190 assert!(result.unwrap_err().to_string().contains("Ghost"));
1191 }
1192
1193 #[test]
1194 fn test_defined_name_sheet_scope_requires_existing_sheet() {
1195 let mut wb = Workbook::new();
1196 let result = wb.set_defined_name("TestName", "Sheet1!$A$1", Some("NonExistent"), None);
1197 assert!(result.is_err());
1198 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1199 }
1200
1201 #[test]
1202 fn test_defined_name_roundtrip() {
1203 let dir = TempDir::new().unwrap();
1204 let path = dir.path().join("defined_names.xlsx");
1205
1206 let mut wb = Workbook::new();
1207 wb.set_defined_name("Revenue", "Sheet1!$E$1:$E$100", None, Some("Total revenue"))
1208 .unwrap();
1209 wb.set_defined_name("LocalName", "Sheet1!$A$1", Some("Sheet1"), None)
1210 .unwrap();
1211 wb.save(&path).unwrap();
1212
1213 let wb2 = Workbook::open(&path).unwrap();
1214 let all = wb2.get_all_defined_names();
1215 assert_eq!(all.len(), 2);
1216 assert_eq!(all[0].name, "Revenue");
1217 assert_eq!(all[0].value, "Sheet1!$E$1:$E$100");
1218 assert_eq!(all[0].comment, Some("Total revenue".to_string()));
1219 assert_eq!(all[1].name, "LocalName");
1220 assert_eq!(all[1].value, "Sheet1!$A$1");
1221 assert_eq!(
1222 all[1].scope,
1223 crate::defined_names::DefinedNameScope::Sheet(0)
1224 );
1225 }
1226
1227 #[test]
1228 fn test_protect_sheet_via_workbook() {
1229 let mut wb = Workbook::new();
1230 let config = crate::sheet::SheetProtectionConfig::default();
1231 wb.protect_sheet("Sheet1", &config).unwrap();
1232
1233 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1234 }
1235
1236 #[test]
1237 fn test_unprotect_sheet_via_workbook() {
1238 let mut wb = Workbook::new();
1239 let config = crate::sheet::SheetProtectionConfig::default();
1240 wb.protect_sheet("Sheet1", &config).unwrap();
1241 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1242
1243 wb.unprotect_sheet("Sheet1").unwrap();
1244 assert!(!wb.is_sheet_protected("Sheet1").unwrap());
1245 }
1246
1247 #[test]
1248 fn test_protect_sheet_nonexistent_returns_error() {
1249 let mut wb = Workbook::new();
1250 let config = crate::sheet::SheetProtectionConfig::default();
1251 let result = wb.protect_sheet("NoSuchSheet", &config);
1252 assert!(result.is_err());
1253 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1254 }
1255
1256 #[test]
1257 fn test_is_sheet_protected_nonexistent_returns_error() {
1258 let wb = Workbook::new();
1259 let result = wb.is_sheet_protected("NoSuchSheet");
1260 assert!(result.is_err());
1261 }
1262
1263 #[test]
1264 fn test_protect_sheet_with_password_and_permissions() {
1265 let mut wb = Workbook::new();
1266 let config = crate::sheet::SheetProtectionConfig {
1267 password: Some("secret".to_string()),
1268 format_cells: true,
1269 insert_rows: true,
1270 sort: true,
1271 ..crate::sheet::SheetProtectionConfig::default()
1272 };
1273 wb.protect_sheet("Sheet1", &config).unwrap();
1274 assert!(wb.is_sheet_protected("Sheet1").unwrap());
1275 }
1276
1277 #[test]
1278 fn test_sheet_protection_roundtrip() {
1279 let dir = TempDir::new().unwrap();
1280 let path = dir.path().join("sheet_protection.xlsx");
1281
1282 let mut wb = Workbook::new();
1283 let config = crate::sheet::SheetProtectionConfig {
1284 password: Some("pass".to_string()),
1285 format_cells: true,
1286 ..crate::sheet::SheetProtectionConfig::default()
1287 };
1288 wb.protect_sheet("Sheet1", &config).unwrap();
1289 wb.save(&path).unwrap();
1290
1291 let wb2 = Workbook::open(&path).unwrap();
1292 assert!(wb2.is_sheet_protected("Sheet1").unwrap());
1293 }
1294}