Skip to content

Commit fc9b8f3

Browse files
alanbldclaude
andcommitted
test(ooxml): Sprint 21 - Comments, content types, and cover page coverage
Add 19 new tests for writer.rs covering: - Comments generation: empty, single, multiple, XML escaping - write_comments: archive updates, relationship and content type updates - update_content_types: PNG extension handling, deduplication - Literal block language comments integration - Cover page: full metadata, page break generation - Internal state: comment/image/drawing IDs, collections Total writer.rs tests: 53 → 72 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b1a22f7 commit fc9b8f3

File tree

1 file changed

+393
-0
lines changed

1 file changed

+393
-0
lines changed

crates/utf8dok-ooxml/src/writer.rs

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3716,4 +3716,397 @@ paragraph = "Normal"
37163716
assert!(archive.contains("utf8dok/source.adoc"));
37173717
assert!(archive.contains("utf8dok/utf8dok.toml"));
37183718
}
3719+
3720+
// ==================== Sprint 21: Comments and Content Types Tests ====================
3721+
3722+
#[test]
3723+
fn test_generate_comments_xml_empty() {
3724+
let writer = DocxWriter::new();
3725+
assert!(writer.generate_comments_xml().is_none());
3726+
}
3727+
3728+
#[test]
3729+
fn test_generate_comments_xml_with_comments() {
3730+
let mut writer = DocxWriter::new();
3731+
writer.comments.push(Comment {
3732+
id: 1,
3733+
text: "rust".to_string(),
3734+
author: "utf8dok".to_string(),
3735+
});
3736+
3737+
let xml = writer.generate_comments_xml();
3738+
assert!(xml.is_some());
3739+
3740+
let content = xml.unwrap();
3741+
assert!(content.contains("w:comments"));
3742+
assert!(content.contains("w:id=\"1\""));
3743+
assert!(content.contains("w:author=\"utf8dok\""));
3744+
assert!(content.contains("rust"));
3745+
}
3746+
3747+
#[test]
3748+
fn test_generate_comments_xml_escapes_special_chars() {
3749+
let mut writer = DocxWriter::new();
3750+
writer.comments.push(Comment {
3751+
id: 1,
3752+
text: "code with <tags> & \"quotes\"".to_string(),
3753+
author: "Test <Author>".to_string(),
3754+
});
3755+
3756+
let xml = writer.generate_comments_xml().unwrap();
3757+
assert!(xml.contains("&lt;tags&gt;"));
3758+
assert!(xml.contains("&amp;"));
3759+
assert!(xml.contains("&quot;quotes&quot;"));
3760+
assert!(xml.contains("Test &lt;Author&gt;"));
3761+
}
3762+
3763+
#[test]
3764+
fn test_generate_comments_xml_multiple_comments() {
3765+
let mut writer = DocxWriter::new();
3766+
writer.comments.push(Comment {
3767+
id: 1,
3768+
text: "First".to_string(),
3769+
author: "Author1".to_string(),
3770+
});
3771+
writer.comments.push(Comment {
3772+
id: 2,
3773+
text: "Second".to_string(),
3774+
author: "Author2".to_string(),
3775+
});
3776+
3777+
let xml = writer.generate_comments_xml().unwrap();
3778+
assert!(xml.contains("w:id=\"1\""));
3779+
assert!(xml.contains("w:id=\"2\""));
3780+
assert!(xml.contains("First"));
3781+
assert!(xml.contains("Second"));
3782+
}
3783+
3784+
#[test]
3785+
fn test_write_comments_adds_to_archive() {
3786+
use crate::archive::OoxmlArchive;
3787+
use crate::test_utils::create_minimal_template;
3788+
3789+
let template = create_minimal_template();
3790+
let cursor = Cursor::new(&template);
3791+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3792+
3793+
let mut writer = DocxWriter::new();
3794+
writer.comments.push(Comment {
3795+
id: 1,
3796+
text: "test language".to_string(),
3797+
author: "utf8dok".to_string(),
3798+
});
3799+
3800+
let result = writer.write_comments(&mut archive);
3801+
assert!(result.is_ok());
3802+
3803+
// Check comments.xml was created
3804+
let comments = archive.get_string("word/comments.xml").unwrap();
3805+
assert!(comments.is_some());
3806+
assert!(comments.unwrap().contains("test language"));
3807+
}
3808+
3809+
#[test]
3810+
fn test_write_comments_updates_relationships() {
3811+
use crate::archive::OoxmlArchive;
3812+
use crate::test_utils::create_minimal_template;
3813+
3814+
let template = create_minimal_template();
3815+
let cursor = Cursor::new(&template);
3816+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3817+
3818+
let mut writer = DocxWriter::new();
3819+
writer.comments.push(Comment {
3820+
id: 1,
3821+
text: "test".to_string(),
3822+
author: "utf8dok".to_string(),
3823+
});
3824+
3825+
writer.write_comments(&mut archive).unwrap();
3826+
3827+
// Check relationships were updated
3828+
let rels = archive
3829+
.get_string("word/_rels/document.xml.rels")
3830+
.unwrap()
3831+
.unwrap();
3832+
assert!(rels.contains("comments.xml"));
3833+
assert!(rels.contains("relationships/comments"));
3834+
}
3835+
3836+
#[test]
3837+
fn test_write_comments_updates_content_types() {
3838+
use crate::archive::OoxmlArchive;
3839+
use crate::test_utils::create_minimal_template;
3840+
3841+
let template = create_minimal_template();
3842+
let cursor = Cursor::new(&template);
3843+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3844+
3845+
let mut writer = DocxWriter::new();
3846+
writer.comments.push(Comment {
3847+
id: 1,
3848+
text: "test".to_string(),
3849+
author: "utf8dok".to_string(),
3850+
});
3851+
3852+
writer.write_comments(&mut archive).unwrap();
3853+
3854+
// Check content types were updated
3855+
let content_types = archive.get_string("[Content_Types].xml").unwrap().unwrap();
3856+
assert!(content_types.contains("/word/comments.xml"));
3857+
}
3858+
3859+
#[test]
3860+
fn test_write_comments_empty_does_nothing() {
3861+
use crate::archive::OoxmlArchive;
3862+
use crate::test_utils::create_minimal_template;
3863+
3864+
let template = create_minimal_template();
3865+
let cursor = Cursor::new(&template);
3866+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3867+
3868+
let writer = DocxWriter::new();
3869+
let result = writer.write_comments(&mut archive);
3870+
assert!(result.is_ok());
3871+
3872+
// Should not have created comments.xml
3873+
assert!(archive.get("word/comments.xml").is_none());
3874+
}
3875+
3876+
#[test]
3877+
fn test_update_content_types_adds_png() {
3878+
use crate::archive::OoxmlArchive;
3879+
use crate::test_utils::create_minimal_template;
3880+
3881+
let template = create_minimal_template();
3882+
let cursor = Cursor::new(&template);
3883+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3884+
3885+
// Simulate having media files
3886+
let mut writer = DocxWriter::new();
3887+
writer.media_files.push(("word/media/image1.png".to_string(), vec![0x89, 0x50]));
3888+
3889+
let result = writer.update_content_types(&mut archive);
3890+
assert!(result.is_ok());
3891+
3892+
let content_types = archive.get_string("[Content_Types].xml").unwrap().unwrap();
3893+
assert!(content_types.contains("Extension=\"png\""));
3894+
assert!(content_types.contains("image/png"));
3895+
}
3896+
3897+
#[test]
3898+
fn test_update_content_types_does_not_duplicate() {
3899+
use crate::archive::OoxmlArchive;
3900+
use crate::test_utils::create_minimal_template;
3901+
3902+
let template = create_minimal_template();
3903+
let cursor = Cursor::new(&template);
3904+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3905+
3906+
// Pre-add PNG extension
3907+
let existing = archive.get_string("[Content_Types].xml").unwrap().unwrap();
3908+
let with_png = existing.replace(
3909+
"</Types>",
3910+
"<Default Extension=\"png\" ContentType=\"image/png\"/></Types>",
3911+
);
3912+
archive.set_string("[Content_Types].xml", with_png);
3913+
3914+
let mut writer = DocxWriter::new();
3915+
writer.media_files.push(("word/media/image1.png".to_string(), vec![]));
3916+
3917+
writer.update_content_types(&mut archive).unwrap();
3918+
3919+
// Should not have duplicated
3920+
let content = archive.get_string("[Content_Types].xml").unwrap().unwrap();
3921+
let count = content.matches("Extension=\"png\"").count();
3922+
assert_eq!(count, 1);
3923+
}
3924+
3925+
#[test]
3926+
fn test_literal_block_with_language_creates_comment() {
3927+
use crate::test_utils::create_minimal_template;
3928+
use utf8dok_ast::LiteralBlock;
3929+
3930+
let doc = Document {
3931+
metadata: Default::default(),
3932+
intent: None,
3933+
blocks: vec![Block::Literal(LiteralBlock {
3934+
content: "fn main() {}".to_string(),
3935+
language: Some("rust".to_string()),
3936+
title: None,
3937+
style_id: None,
3938+
})],
3939+
};
3940+
3941+
let template = create_minimal_template();
3942+
let result = DocxWriter::generate(&doc, &template).unwrap();
3943+
3944+
// Extract and check comments
3945+
let cursor = Cursor::new(&result);
3946+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
3947+
3948+
let comments = archive.get_string("word/comments.xml").unwrap();
3949+
assert!(comments.is_some());
3950+
assert!(comments.unwrap().contains("rust"));
3951+
}
3952+
3953+
#[test]
3954+
fn test_literal_block_without_language_no_comment() {
3955+
use crate::test_utils::create_minimal_template;
3956+
use utf8dok_ast::LiteralBlock;
3957+
3958+
let doc = Document {
3959+
metadata: Default::default(),
3960+
intent: None,
3961+
blocks: vec![Block::Literal(LiteralBlock {
3962+
content: "plain text".to_string(),
3963+
language: None,
3964+
title: None,
3965+
style_id: None,
3966+
})],
3967+
};
3968+
3969+
let template = create_minimal_template();
3970+
let result = DocxWriter::generate(&doc, &template).unwrap();
3971+
3972+
let cursor = Cursor::new(&result);
3973+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
3974+
3975+
// Should not have comments.xml
3976+
assert!(archive.get("word/comments.xml").is_none());
3977+
}
3978+
3979+
#[test]
3980+
fn test_cover_page_with_full_metadata() {
3981+
use crate::test_utils::create_template_with_styles;
3982+
3983+
let mut meta = utf8dok_ast::DocumentMeta::default();
3984+
meta.title = Some("Corporate Report".to_string());
3985+
meta.authors = vec!["John Doe".to_string()];
3986+
meta.revision = Some("1.0".to_string());
3987+
meta.attributes
3988+
.insert("revdate".to_string(), "2025-01-15".to_string());
3989+
3990+
let doc = Document {
3991+
metadata: meta,
3992+
intent: None,
3993+
blocks: vec![Block::Paragraph(Paragraph {
3994+
inlines: vec![Inline::Text("Content".to_string())],
3995+
..Default::default()
3996+
})],
3997+
};
3998+
3999+
let mut writer = DocxWriter::new();
4000+
// Create a minimal cover image (1x1 PNG)
4001+
let cover_png = vec![
4002+
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
4003+
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
4004+
0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08,
4005+
0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0x3F, 0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59,
4006+
0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
4007+
];
4008+
writer.set_cover_image("cover.png", cover_png);
4009+
4010+
let template = create_template_with_styles();
4011+
let template_obj = Template::from_bytes(&template).unwrap();
4012+
let result = writer.generate_with_template(&doc, template_obj).unwrap();
4013+
4014+
let cursor = Cursor::new(&result);
4015+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
4016+
4017+
// Should have cover image in media
4018+
assert!(archive.contains("word/media/cover_cover.png"));
4019+
4020+
// Document should contain the metadata text
4021+
let doc_xml = crate::test_utils::extract_document_xml(&result);
4022+
assert!(doc_xml.contains("Corporate Report"));
4023+
assert!(doc_xml.contains("John Doe"));
4024+
}
4025+
4026+
#[test]
4027+
fn test_cover_page_generates_page_break() {
4028+
use crate::test_utils::create_template_with_styles;
4029+
4030+
let mut meta = utf8dok_ast::DocumentMeta::default();
4031+
meta.title = Some("Title".to_string());
4032+
4033+
let doc = Document {
4034+
metadata: meta,
4035+
intent: None,
4036+
blocks: vec![Block::Paragraph(Paragraph {
4037+
inlines: vec![Inline::Text("Body content".to_string())],
4038+
..Default::default()
4039+
})],
4040+
};
4041+
4042+
let mut writer = DocxWriter::new();
4043+
writer.set_cover_image("cover.png", vec![0x89, 0x50, 0x4E, 0x47]);
4044+
4045+
let template = create_template_with_styles();
4046+
let template_obj = Template::from_bytes(&template).unwrap();
4047+
let result = writer.generate_with_template(&doc, template_obj).unwrap();
4048+
4049+
let doc_xml = crate::test_utils::extract_document_xml(&result);
4050+
// After cover page, should have page break before content
4051+
assert!(doc_xml.contains("<w:br w:type=\"page\"/>"));
4052+
}
4053+
4054+
#[test]
4055+
fn test_next_comment_id_increments() {
4056+
let mut writer = DocxWriter::new();
4057+
4058+
assert_eq!(writer.next_comment_id, 1);
4059+
writer.next_comment_id += 1;
4060+
assert_eq!(writer.next_comment_id, 2);
4061+
writer.next_comment_id += 1;
4062+
assert_eq!(writer.next_comment_id, 3);
4063+
}
4064+
4065+
#[test]
4066+
fn test_next_image_id_increments() {
4067+
let mut writer = DocxWriter::new();
4068+
4069+
assert_eq!(writer.next_image_id, 1);
4070+
writer.next_image_id += 1;
4071+
assert_eq!(writer.next_image_id, 2);
4072+
}
4073+
4074+
#[test]
4075+
fn test_next_drawing_id_increments() {
4076+
let mut writer = DocxWriter::new();
4077+
4078+
assert_eq!(writer.next_drawing_id, 1);
4079+
writer.next_drawing_id += 1;
4080+
assert_eq!(writer.next_drawing_id, 2);
4081+
}
4082+
4083+
#[test]
4084+
fn test_media_files_collection() {
4085+
let mut writer = DocxWriter::new();
4086+
assert!(writer.media_files.is_empty());
4087+
4088+
writer
4089+
.media_files
4090+
.push(("word/media/image1.png".to_string(), vec![1, 2, 3]));
4091+
writer
4092+
.media_files
4093+
.push(("word/media/image2.png".to_string(), vec![4, 5, 6]));
4094+
4095+
assert_eq!(writer.media_files.len(), 2);
4096+
assert_eq!(writer.media_files[0].0, "word/media/image1.png");
4097+
assert_eq!(writer.media_files[1].0, "word/media/image2.png");
4098+
}
4099+
4100+
#[test]
4101+
fn test_diagram_sources_collection() {
4102+
let mut writer = DocxWriter::new();
4103+
assert!(writer.diagram_sources.is_empty());
4104+
4105+
writer
4106+
.diagram_sources
4107+
.push(("utf8dok/diagrams/d1.mmd".to_string(), "graph TD".to_string()));
4108+
4109+
assert_eq!(writer.diagram_sources.len(), 1);
4110+
assert!(writer.diagram_sources[0].1.contains("graph TD"));
4111+
}
37194112
}

0 commit comments

Comments
 (0)