Skip to content

Commit 75f4d86

Browse files
alanbldclaude
andcommitted
feat(ooxml): Sprint 4 - Shared test fixtures module
Add test_utils module with shared DOCX template fixtures: - create_minimal_template(): Basic DOCX structure for testing - create_template_with_styles(): Template with Heading1/Heading2 styles - extract_document_xml(): Extract document.xml from DOCX bytes - extract_file(): Extract any file from DOCX archive Updated writer.rs and writer_coverage.rs to use shared fixtures, eliminating code duplication across test files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 87d8770 commit 75f4d86

File tree

4 files changed

+229
-120
lines changed

4 files changed

+229
-120
lines changed

crates/utf8dok-ooxml/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ pub mod styles;
3636
pub mod template;
3737
pub mod writer;
3838

39+
/// Test utilities for creating DOCX fixtures. Available unconditionally for integration tests.
40+
#[doc(hidden)]
41+
pub mod test_utils;
42+
3943
pub use archive::OoxmlArchive;
4044
pub use conversion::{convert_document, convert_document_with_styles, ConversionContext, ToAst};
4145
pub use document::{
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//! Shared test utilities for utf8dok-ooxml
2+
//!
3+
//! This module provides common fixtures and helpers used across tests.
4+
5+
use std::io::{Cursor, Write};
6+
use zip::write::SimpleFileOptions;
7+
use zip::CompressionMethod;
8+
use zip::ZipWriter;
9+
10+
use crate::archive::OoxmlArchive;
11+
12+
/// Create a minimal valid DOCX template for testing
13+
///
14+
/// This creates a valid DOCX ZIP structure with:
15+
/// - [Content_Types].xml
16+
/// - _rels/.rels
17+
/// - word/_rels/document.xml.rels
18+
/// - word/document.xml (placeholder content)
19+
///
20+
/// # Example
21+
/// ```ignore
22+
/// use utf8dok_ooxml::test_utils::create_minimal_template;
23+
/// let template = create_minimal_template();
24+
/// ```
25+
pub fn create_minimal_template() -> Vec<u8> {
26+
let mut buffer = Cursor::new(Vec::new());
27+
let mut zip = ZipWriter::new(&mut buffer);
28+
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
29+
30+
// [Content_Types].xml
31+
zip.start_file("[Content_Types].xml", options).unwrap();
32+
zip.write_all(
33+
br#"<?xml version="1.0" encoding="UTF-8"?>
34+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
35+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
36+
<Default Extension="xml" ContentType="application/xml"/>
37+
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
38+
</Types>"#,
39+
)
40+
.unwrap();
41+
42+
// _rels/.rels
43+
zip.start_file("_rels/.rels", options).unwrap();
44+
zip.write_all(
45+
br#"<?xml version="1.0" encoding="UTF-8"?>
46+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
47+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
48+
</Relationships>"#,
49+
)
50+
.unwrap();
51+
52+
// word/_rels/document.xml.rels
53+
zip.start_file("word/_rels/document.xml.rels", options)
54+
.unwrap();
55+
zip.write_all(
56+
br#"<?xml version="1.0" encoding="UTF-8"?>
57+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
58+
</Relationships>"#,
59+
)
60+
.unwrap();
61+
62+
// word/document.xml (placeholder, will be replaced)
63+
zip.start_file("word/document.xml", options).unwrap();
64+
zip.write_all(
65+
br#"<?xml version="1.0" encoding="UTF-8"?>
66+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
67+
<w:body>
68+
<w:p><w:r><w:t>Template</w:t></w:r></w:p>
69+
</w:body>
70+
</w:document>"#,
71+
)
72+
.unwrap();
73+
74+
zip.finish().unwrap();
75+
buffer.into_inner()
76+
}
77+
78+
/// Create a minimal DOCX template with styles.xml
79+
///
80+
/// Includes basic heading styles for testing style-aware features.
81+
pub fn create_template_with_styles() -> Vec<u8> {
82+
let mut buffer = Cursor::new(Vec::new());
83+
let mut zip = ZipWriter::new(&mut buffer);
84+
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
85+
86+
// [Content_Types].xml
87+
zip.start_file("[Content_Types].xml", options).unwrap();
88+
zip.write_all(
89+
br#"<?xml version="1.0" encoding="UTF-8"?>
90+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
91+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
92+
<Default Extension="xml" ContentType="application/xml"/>
93+
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
94+
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
95+
</Types>"#,
96+
)
97+
.unwrap();
98+
99+
// _rels/.rels
100+
zip.start_file("_rels/.rels", options).unwrap();
101+
zip.write_all(
102+
br#"<?xml version="1.0" encoding="UTF-8"?>
103+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
104+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
105+
</Relationships>"#,
106+
)
107+
.unwrap();
108+
109+
// word/_rels/document.xml.rels
110+
zip.start_file("word/_rels/document.xml.rels", options)
111+
.unwrap();
112+
zip.write_all(
113+
br#"<?xml version="1.0" encoding="UTF-8"?>
114+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
115+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
116+
</Relationships>"#,
117+
)
118+
.unwrap();
119+
120+
// word/styles.xml
121+
zip.start_file("word/styles.xml", options).unwrap();
122+
zip.write_all(
123+
br#"<?xml version="1.0" encoding="UTF-8"?>
124+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
125+
<w:style w:type="paragraph" w:styleId="Heading1">
126+
<w:name w:val="heading 1"/>
127+
<w:pPr><w:outlineLvl w:val="0"/></w:pPr>
128+
</w:style>
129+
<w:style w:type="paragraph" w:styleId="Heading2">
130+
<w:name w:val="heading 2"/>
131+
<w:pPr><w:outlineLvl w:val="1"/></w:pPr>
132+
</w:style>
133+
<w:style w:type="paragraph" w:styleId="Normal">
134+
<w:name w:val="Normal"/>
135+
</w:style>
136+
</w:styles>"#,
137+
)
138+
.unwrap();
139+
140+
// word/document.xml
141+
zip.start_file("word/document.xml", options).unwrap();
142+
zip.write_all(
143+
br#"<?xml version="1.0" encoding="UTF-8"?>
144+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
145+
<w:body>
146+
<w:p><w:r><w:t>Template with styles</w:t></w:r></w:p>
147+
</w:body>
148+
</w:document>"#,
149+
)
150+
.unwrap();
151+
152+
zip.finish().unwrap();
153+
buffer.into_inner()
154+
}
155+
156+
/// Extract document.xml content from a DOCX byte array
157+
pub fn extract_document_xml(docx: &[u8]) -> String {
158+
let cursor = Cursor::new(docx);
159+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
160+
archive.get_string("word/document.xml").unwrap().unwrap()
161+
}
162+
163+
/// Extract any file content from a DOCX byte array
164+
pub fn extract_file(docx: &[u8], path: &str) -> Option<String> {
165+
let cursor = Cursor::new(docx);
166+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
167+
archive.get_string(path).unwrap()
168+
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use super::*;
173+
174+
#[test]
175+
fn test_create_minimal_template() {
176+
let template = create_minimal_template();
177+
assert!(!template.is_empty());
178+
179+
// Verify it's a valid ZIP
180+
let cursor = Cursor::new(&template);
181+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
182+
183+
assert!(archive.contains("[Content_Types].xml"));
184+
assert!(archive.contains("word/document.xml"));
185+
assert!(archive.contains("_rels/.rels"));
186+
}
187+
188+
#[test]
189+
fn test_create_template_with_styles() {
190+
let template = create_template_with_styles();
191+
assert!(!template.is_empty());
192+
193+
let cursor = Cursor::new(&template);
194+
let archive = OoxmlArchive::from_reader(cursor).unwrap();
195+
196+
assert!(archive.contains("word/styles.xml"));
197+
198+
let styles = archive.get_string("word/styles.xml").unwrap().unwrap();
199+
assert!(styles.contains("Heading1"));
200+
assert!(styles.contains("Heading2"));
201+
}
202+
203+
#[test]
204+
fn test_extract_document_xml() {
205+
let template = create_minimal_template();
206+
let doc_xml = extract_document_xml(&template);
207+
208+
assert!(doc_xml.contains("w:document"));
209+
assert!(doc_xml.contains("Template"));
210+
}
211+
212+
#[test]
213+
fn test_extract_file() {
214+
let template = create_minimal_template();
215+
216+
let content_types = extract_file(&template, "[Content_Types].xml");
217+
assert!(content_types.is_some());
218+
assert!(content_types.unwrap().contains("Types"));
219+
220+
let missing = extract_file(&template, "nonexistent.xml");
221+
assert!(missing.is_none());
222+
}
223+
}

crates/utf8dok-ooxml/src/writer.rs

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,61 +1915,10 @@ fn escape_xml(text: &str) -> String {
19151915
#[cfg(test)]
19161916
mod tests {
19171917
use super::*;
1918+
use crate::test_utils::create_minimal_template;
19181919
use std::collections::HashMap;
19191920
use std::io::{Cursor, Write};
19201921

1921-
/// Create a minimal valid DOCX template for testing
1922-
fn create_minimal_template() -> Vec<u8> {
1923-
use zip::write::SimpleFileOptions;
1924-
use zip::CompressionMethod;
1925-
use zip::ZipWriter;
1926-
1927-
let mut buffer = Cursor::new(Vec::new());
1928-
let mut zip = ZipWriter::new(&mut buffer);
1929-
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
1930-
1931-
// [Content_Types].xml
1932-
zip.start_file("[Content_Types].xml", options).unwrap();
1933-
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
1934-
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
1935-
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
1936-
<Default Extension="xml" ContentType="application/xml"/>
1937-
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
1938-
</Types>"#).unwrap();
1939-
1940-
// _rels/.rels
1941-
zip.start_file("_rels/.rels", options).unwrap();
1942-
zip.write_all(br#"<?xml version="1.0" encoding="UTF-8"?>
1943-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1944-
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
1945-
</Relationships>"#).unwrap();
1946-
1947-
// word/_rels/document.xml.rels
1948-
zip.start_file("word/_rels/document.xml.rels", options)
1949-
.unwrap();
1950-
zip.write_all(
1951-
br#"<?xml version="1.0" encoding="UTF-8"?>
1952-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1953-
</Relationships>"#,
1954-
)
1955-
.unwrap();
1956-
1957-
// word/document.xml (placeholder, will be replaced)
1958-
zip.start_file("word/document.xml", options).unwrap();
1959-
zip.write_all(
1960-
br#"<?xml version="1.0" encoding="UTF-8"?>
1961-
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1962-
<w:body>
1963-
<w:p><w:r><w:t>Template</w:t></w:r></w:p>
1964-
</w:body>
1965-
</w:document>"#,
1966-
)
1967-
.unwrap();
1968-
1969-
zip.finish().unwrap();
1970-
buffer.into_inner()
1971-
}
1972-
19731922
#[test]
19741923
fn test_write_basic_doc() {
19751924
let template = create_minimal_template();

crates/utf8dok-ooxml/tests/writer_coverage.rs

Lines changed: 1 addition & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,15 @@
33
//! Tests for `DocxWriter` to increase code coverage beyond the inline tests.
44
55
use std::collections::HashMap;
6-
use std::io::Cursor;
76

87
use utf8dok_ast::{
98
Admonition, AdmonitionType, Block, BreakType, ColumnSpec, Document, DocumentMeta, FormatType,
109
Heading, Inline, Link, List, ListItem, ListType, LiteralBlock, Paragraph, Table, TableCell,
1110
TableRow,
1211
};
13-
use utf8dok_ooxml::archive::OoxmlArchive;
12+
use utf8dok_ooxml::test_utils::{create_minimal_template, extract_document_xml};
1413
use utf8dok_ooxml::writer::DocxWriter;
1514

16-
/// Create a minimal valid DOCX template for testing
17-
fn create_minimal_template() -> Vec<u8> {
18-
use std::io::Write;
19-
use zip::write::SimpleFileOptions;
20-
use zip::CompressionMethod;
21-
use zip::ZipWriter;
22-
23-
let mut buffer = Cursor::new(Vec::new());
24-
let mut zip = ZipWriter::new(&mut buffer);
25-
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
26-
27-
// [Content_Types].xml
28-
zip.start_file("[Content_Types].xml", options).unwrap();
29-
zip.write_all(
30-
br#"<?xml version="1.0" encoding="UTF-8"?>
31-
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
32-
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
33-
<Default Extension="xml" ContentType="application/xml"/>
34-
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
35-
</Types>"#,
36-
)
37-
.unwrap();
38-
39-
// _rels/.rels
40-
zip.start_file("_rels/.rels", options).unwrap();
41-
zip.write_all(
42-
br#"<?xml version="1.0" encoding="UTF-8"?>
43-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
44-
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
45-
</Relationships>"#,
46-
)
47-
.unwrap();
48-
49-
// word/_rels/document.xml.rels
50-
zip.start_file("word/_rels/document.xml.rels", options)
51-
.unwrap();
52-
zip.write_all(
53-
br#"<?xml version="1.0" encoding="UTF-8"?>
54-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
55-
</Relationships>"#,
56-
)
57-
.unwrap();
58-
59-
// word/document.xml (placeholder)
60-
zip.start_file("word/document.xml", options).unwrap();
61-
zip.write_all(
62-
br#"<?xml version="1.0" encoding="UTF-8"?>
63-
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
64-
<w:body>
65-
<w:p><w:r><w:t>Template</w:t></w:r></w:p>
66-
</w:body>
67-
</w:document>"#,
68-
)
69-
.unwrap();
70-
71-
zip.finish().unwrap();
72-
buffer.into_inner()
73-
}
74-
75-
/// Helper to extract document.xml from a generated DOCX
76-
fn extract_document_xml(docx: &[u8]) -> String {
77-
let cursor = Cursor::new(docx);
78-
let archive = OoxmlArchive::from_reader(cursor).unwrap();
79-
archive.get_string("word/document.xml").unwrap().unwrap()
80-
}
81-
8215
// =============================================================================
8316
// Task 1: Complex Table Tests
8417
// =============================================================================

0 commit comments

Comments
 (0)