Skip to content

Commit 29a4421

Browse files
alanbldclaude
andcommitted
test(ooxml): Sprint 12 - Style contract resolution + core properties tests
Added 15 new tests (317 total): writer.rs - Style contract resolution: - resolve_heading_style with/without contract - resolve_paragraph_style with/without contract - next_bookmark_id increments writer.rs - Cover metadata extraction: - title only, authors vec, author attribute - revision number/date, empty metadata writer.rs - Core properties (docProps/core.xml): - no metadata (early return) - creates new core.xml - updates existing title/author - inserts title into existing without title - revdate ISO format conversion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b2c92f5 commit 29a4421

File tree

1 file changed

+347
-0
lines changed

1 file changed

+347
-0
lines changed

crates/utf8dok-ooxml/src/writer.rs

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2823,4 +2823,351 @@ paragraph = "Normal"
28232823
"Should have bookmark or anchor"
28242824
);
28252825
}
2826+
2827+
// ==================== Sprint 12: Style Contract Resolution Tests ====================
2828+
2829+
#[test]
2830+
fn test_resolve_heading_style_without_contract() {
2831+
let writer = DocxWriter::new();
2832+
2833+
// Without contract, should fall back to style_map defaults
2834+
assert_eq!(writer.resolve_heading_style(1), "Heading1");
2835+
assert_eq!(writer.resolve_heading_style(2), "Heading2");
2836+
assert_eq!(writer.resolve_heading_style(9), "Heading9");
2837+
}
2838+
2839+
#[test]
2840+
fn test_resolve_heading_style_with_contract() {
2841+
use crate::style_map::{ParagraphStyleMapping, StyleContract};
2842+
2843+
let mut contract = StyleContract::default();
2844+
// Headings are stored in paragraph_styles with heading_level set
2845+
contract.paragraph_styles.insert(
2846+
"CustomH1".to_string(),
2847+
ParagraphStyleMapping {
2848+
role: "heading".to_string(),
2849+
heading_level: Some(1),
2850+
..Default::default()
2851+
},
2852+
);
2853+
contract.paragraph_styles.insert(
2854+
"CustomH2".to_string(),
2855+
ParagraphStyleMapping {
2856+
role: "heading".to_string(),
2857+
heading_level: Some(2),
2858+
..Default::default()
2859+
},
2860+
);
2861+
2862+
let mut writer = DocxWriter::new();
2863+
writer.set_style_contract(contract);
2864+
2865+
// Should use contract mappings
2866+
assert_eq!(writer.resolve_heading_style(1), "CustomH1");
2867+
assert_eq!(writer.resolve_heading_style(2), "CustomH2");
2868+
2869+
// Level 3 not in contract - should fall back to style_map
2870+
assert_eq!(writer.resolve_heading_style(3), "Heading3");
2871+
}
2872+
2873+
#[test]
2874+
fn test_resolve_paragraph_style_without_contract() {
2875+
let writer = DocxWriter::new();
2876+
2877+
// Without contract, should fall back to style_map.paragraph()
2878+
assert_eq!(writer.resolve_paragraph_style("body"), "Normal");
2879+
assert_eq!(writer.resolve_paragraph_style("intro"), "Normal");
2880+
}
2881+
2882+
#[test]
2883+
fn test_resolve_paragraph_style_with_contract() {
2884+
use crate::style_map::{ParagraphStyleMapping, StyleContract};
2885+
2886+
let mut contract = StyleContract::default();
2887+
// Key is Word style ID, value contains the semantic role
2888+
contract.paragraph_styles.insert(
2889+
"AbstractStyle".to_string(),
2890+
ParagraphStyleMapping {
2891+
role: "abstract".to_string(),
2892+
..Default::default()
2893+
},
2894+
);
2895+
contract.paragraph_styles.insert(
2896+
"NoteStyle".to_string(),
2897+
ParagraphStyleMapping {
2898+
role: "note".to_string(),
2899+
..Default::default()
2900+
},
2901+
);
2902+
2903+
let mut writer = DocxWriter::new();
2904+
writer.set_style_contract(contract);
2905+
2906+
// Should use contract mappings (looks up by role, returns Word style ID)
2907+
assert_eq!(writer.resolve_paragraph_style("abstract"), "AbstractStyle");
2908+
assert_eq!(writer.resolve_paragraph_style("note"), "NoteStyle");
2909+
2910+
// Unknown role should fall back
2911+
assert_eq!(writer.resolve_paragraph_style("unknown"), "Normal");
2912+
}
2913+
2914+
#[test]
2915+
fn test_next_bookmark_id_increments() {
2916+
let mut writer = DocxWriter::new();
2917+
2918+
assert_eq!(writer.next_bookmark_id(), 0);
2919+
assert_eq!(writer.next_bookmark_id(), 1);
2920+
assert_eq!(writer.next_bookmark_id(), 2);
2921+
}
2922+
2923+
#[test]
2924+
fn test_extract_cover_metadata_with_title_only() {
2925+
let mut meta = utf8dok_ast::DocumentMeta::default();
2926+
meta.title = Some("My Document Title".to_string());
2927+
2928+
let doc = Document {
2929+
metadata: meta,
2930+
intent: None,
2931+
blocks: vec![],
2932+
};
2933+
2934+
let writer = DocxWriter::new();
2935+
let cover_meta = writer.extract_cover_metadata(&doc);
2936+
2937+
assert_eq!(cover_meta.title, "My Document Title");
2938+
assert!(cover_meta.author.is_empty());
2939+
assert!(cover_meta.revnumber.is_empty());
2940+
}
2941+
2942+
#[test]
2943+
fn test_extract_cover_metadata_with_authors_vec() {
2944+
let mut meta = utf8dok_ast::DocumentMeta::default();
2945+
meta.title = Some("Title".to_string());
2946+
meta.authors = vec!["John Doe".to_string(), "Jane Smith".to_string()];
2947+
2948+
let doc = Document {
2949+
metadata: meta,
2950+
intent: None,
2951+
blocks: vec![],
2952+
};
2953+
2954+
let writer = DocxWriter::new();
2955+
let cover_meta = writer.extract_cover_metadata(&doc);
2956+
2957+
assert_eq!(cover_meta.author, "John Doe, Jane Smith");
2958+
}
2959+
2960+
#[test]
2961+
fn test_extract_cover_metadata_with_author_attribute() {
2962+
let mut meta = utf8dok_ast::DocumentMeta::default();
2963+
meta.attributes
2964+
.insert("author".to_string(), "Attribute Author".to_string());
2965+
2966+
let doc = Document {
2967+
metadata: meta,
2968+
intent: None,
2969+
blocks: vec![],
2970+
};
2971+
2972+
let writer = DocxWriter::new();
2973+
let cover_meta = writer.extract_cover_metadata(&doc);
2974+
2975+
// When authors vec is empty, falls back to attribute
2976+
assert_eq!(cover_meta.author, "Attribute Author");
2977+
}
2978+
2979+
#[test]
2980+
fn test_extract_cover_metadata_with_revision() {
2981+
let mut meta = utf8dok_ast::DocumentMeta::default();
2982+
meta.revision = Some("1.0".to_string());
2983+
meta.attributes
2984+
.insert("revdate".to_string(), "2025-01-01".to_string());
2985+
2986+
let doc = Document {
2987+
metadata: meta,
2988+
intent: None,
2989+
blocks: vec![],
2990+
};
2991+
2992+
let writer = DocxWriter::new();
2993+
let cover_meta = writer.extract_cover_metadata(&doc);
2994+
2995+
assert_eq!(cover_meta.revnumber, "1.0");
2996+
assert_eq!(cover_meta.revdate, "2025-01-01");
2997+
}
2998+
2999+
#[test]
3000+
fn test_extract_cover_metadata_empty() {
3001+
let doc = Document {
3002+
metadata: utf8dok_ast::DocumentMeta::default(),
3003+
intent: None,
3004+
blocks: vec![],
3005+
};
3006+
3007+
let writer = DocxWriter::new();
3008+
let cover_meta = writer.extract_cover_metadata(&doc);
3009+
3010+
assert!(cover_meta.title.is_empty());
3011+
assert!(cover_meta.author.is_empty());
3012+
assert!(cover_meta.revnumber.is_empty());
3013+
assert!(cover_meta.revdate.is_empty());
3014+
}
3015+
3016+
#[test]
3017+
fn test_update_core_properties_with_no_metadata() {
3018+
use crate::archive::OoxmlArchive;
3019+
use crate::test_utils::create_minimal_template;
3020+
3021+
let template = create_minimal_template();
3022+
let cursor = Cursor::new(&template);
3023+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3024+
3025+
let doc = Document {
3026+
metadata: utf8dok_ast::DocumentMeta::default(),
3027+
intent: None,
3028+
blocks: vec![],
3029+
};
3030+
3031+
let writer = DocxWriter::new();
3032+
let result = writer.update_core_properties(&mut archive, &doc);
3033+
assert!(result.is_ok());
3034+
3035+
// Should not create core.xml when no metadata
3036+
assert!(archive.get("docProps/core.xml").is_none());
3037+
}
3038+
3039+
#[test]
3040+
fn test_update_core_properties_creates_new() {
3041+
use crate::archive::OoxmlArchive;
3042+
use crate::test_utils::create_minimal_template;
3043+
3044+
let template = create_minimal_template();
3045+
let cursor = Cursor::new(&template);
3046+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3047+
3048+
let mut meta = utf8dok_ast::DocumentMeta::default();
3049+
meta.title = Some("New Title".to_string());
3050+
meta.authors = vec!["Author Name".to_string()];
3051+
3052+
let doc = Document {
3053+
metadata: meta,
3054+
intent: None,
3055+
blocks: vec![],
3056+
};
3057+
3058+
let writer = DocxWriter::new();
3059+
let result = writer.update_core_properties(&mut archive, &doc);
3060+
assert!(result.is_ok());
3061+
3062+
// Should create core.xml
3063+
let core_xml = archive.get_string("docProps/core.xml").unwrap();
3064+
assert!(core_xml.is_some());
3065+
3066+
let content = core_xml.unwrap();
3067+
assert!(content.contains("<dc:title>New Title</dc:title>"));
3068+
assert!(content.contains("<dc:creator>Author Name</dc:creator>"));
3069+
}
3070+
3071+
#[test]
3072+
fn test_update_core_properties_updates_existing() {
3073+
use crate::archive::OoxmlArchive;
3074+
use crate::test_utils::create_minimal_template;
3075+
3076+
let template = create_minimal_template();
3077+
let cursor = Cursor::new(&template);
3078+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3079+
3080+
// Pre-populate with existing core.xml
3081+
let existing = r#"<?xml version="1.0"?>
3082+
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
3083+
xmlns:dc="http://purl.org/dc/elements/1.1/">
3084+
<dc:title>Old Title</dc:title>
3085+
<dc:creator>Old Author</dc:creator>
3086+
</cp:coreProperties>"#;
3087+
archive.set_string("docProps/core.xml", existing.to_string());
3088+
3089+
let mut meta = utf8dok_ast::DocumentMeta::default();
3090+
meta.title = Some("Updated Title".to_string());
3091+
meta.authors = vec!["Updated Author".to_string()];
3092+
3093+
let doc = Document {
3094+
metadata: meta,
3095+
intent: None,
3096+
blocks: vec![],
3097+
};
3098+
3099+
let writer = DocxWriter::new();
3100+
let result = writer.update_core_properties(&mut archive, &doc);
3101+
assert!(result.is_ok());
3102+
3103+
let core_xml = archive.get_string("docProps/core.xml").unwrap().unwrap();
3104+
assert!(core_xml.contains("<dc:title>Updated Title</dc:title>"));
3105+
assert!(core_xml.contains("<dc:creator>Updated Author</dc:creator>"));
3106+
assert!(!core_xml.contains("Old Title"));
3107+
assert!(!core_xml.contains("Old Author"));
3108+
}
3109+
3110+
#[test]
3111+
fn test_update_core_properties_with_revdate() {
3112+
use crate::archive::OoxmlArchive;
3113+
use crate::test_utils::create_minimal_template;
3114+
3115+
let template = create_minimal_template();
3116+
let cursor = Cursor::new(&template);
3117+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3118+
3119+
let mut meta = utf8dok_ast::DocumentMeta::default();
3120+
meta.attributes
3121+
.insert("revdate".to_string(), "2025-06-15".to_string());
3122+
3123+
let doc = Document {
3124+
metadata: meta,
3125+
intent: None,
3126+
blocks: vec![],
3127+
};
3128+
3129+
let writer = DocxWriter::new();
3130+
let result = writer.update_core_properties(&mut archive, &doc);
3131+
assert!(result.is_ok());
3132+
3133+
let core_xml = archive.get_string("docProps/core.xml").unwrap().unwrap();
3134+
// Date should have ISO format with time
3135+
assert!(core_xml.contains("2025-06-15T00:00:00Z"));
3136+
}
3137+
3138+
#[test]
3139+
fn test_update_core_properties_insert_into_existing_without_title() {
3140+
use crate::archive::OoxmlArchive;
3141+
use crate::test_utils::create_minimal_template;
3142+
3143+
let template = create_minimal_template();
3144+
let cursor = Cursor::new(&template);
3145+
let mut archive = OoxmlArchive::from_reader(cursor).unwrap();
3146+
3147+
// Existing core.xml without title element
3148+
let existing = r#"<?xml version="1.0"?>
3149+
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
3150+
xmlns:dc="http://purl.org/dc/elements/1.1/">
3151+
<dc:creator>Existing Author</dc:creator>
3152+
</cp:coreProperties>"#;
3153+
archive.set_string("docProps/core.xml", existing.to_string());
3154+
3155+
let mut meta = utf8dok_ast::DocumentMeta::default();
3156+
meta.title = Some("Inserted Title".to_string());
3157+
3158+
let doc = Document {
3159+
metadata: meta,
3160+
intent: None,
3161+
blocks: vec![],
3162+
};
3163+
3164+
let writer = DocxWriter::new();
3165+
let result = writer.update_core_properties(&mut archive, &doc);
3166+
assert!(result.is_ok());
3167+
3168+
let core_xml = archive.get_string("docProps/core.xml").unwrap().unwrap();
3169+
assert!(core_xml.contains("<dc:title>Inserted Title</dc:title>"));
3170+
// Existing author should remain
3171+
assert!(core_xml.contains("<dc:creator>Existing Author</dc:creator>"));
3172+
}
28263173
}

0 commit comments

Comments
 (0)