Skip to content

Commit 2aa5f9b

Browse files
alanbldclaude
andcommitted
test(ooxml): Sprint 15 - StyleMap validation + StyleSheet edge cases
- Add StyleMap::validate() tests (all present, missing styles, default map) - Add StyleMap::from_stylesheet() edge cases (localized headings, code/list/table detection, empty stylesheet, fallback by styleId) - Add StyleSheet character/numbering style parsing tests - Add StyleSheet iterator tests (all(), heading_styles()) - Add default paragraph/character detection tests - Add unknown type and missing name fallback tests - Add ElementType coverage and fallback style tests - 371 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e77149e commit 2aa5f9b

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

crates/utf8dok-ooxml/src/styles.rs

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,4 +935,387 @@ mod tests {
935935
assert_eq!(table_styles.len(), 1);
936936
assert_eq!(table_styles[0].id, "Table1");
937937
}
938+
939+
// ==================== Sprint 15: StyleMap Validation & Edge Cases ====================
940+
941+
#[test]
942+
fn test_style_map_validate_all_present() {
943+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
944+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
945+
<w:style w:type="paragraph" w:styleId="Heading1">
946+
<w:name w:val="Heading 1"/>
947+
</w:style>
948+
<w:style w:type="paragraph" w:styleId="Normal">
949+
<w:name w:val="Normal"/>
950+
</w:style>
951+
</w:styles>"#;
952+
953+
let styles = StyleSheet::parse(xml).unwrap();
954+
let mut map = StyleMap::new();
955+
map.set(ElementType::Heading(1), "Heading1");
956+
map.set(ElementType::Paragraph, "Normal");
957+
958+
let missing = map.validate(&styles);
959+
assert!(missing.is_empty(), "All mapped styles exist in stylesheet");
960+
}
961+
962+
#[test]
963+
fn test_style_map_validate_missing_styles() {
964+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
965+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
966+
<w:style w:type="paragraph" w:styleId="Normal">
967+
<w:name w:val="Normal"/>
968+
</w:style>
969+
</w:styles>"#;
970+
971+
let styles = StyleSheet::parse(xml).unwrap();
972+
let mut map = StyleMap::new();
973+
map.set(ElementType::Heading(1), "MissingH1");
974+
map.set(ElementType::Paragraph, "Normal");
975+
map.set(ElementType::CodeBlock, "MissingCode");
976+
977+
let missing = map.validate(&styles);
978+
assert_eq!(missing.len(), 2);
979+
assert!(missing.contains(&"MissingH1".to_string()));
980+
assert!(missing.contains(&"MissingCode".to_string()));
981+
}
982+
983+
#[test]
984+
fn test_style_map_validate_default_map() {
985+
// Validate default StyleMap against a minimal stylesheet
986+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
987+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
988+
<w:style w:type="paragraph" w:styleId="Normal">
989+
<w:name w:val="Normal"/>
990+
</w:style>
991+
</w:styles>"#;
992+
993+
let styles = StyleSheet::parse(xml).unwrap();
994+
let map = StyleMap::default();
995+
996+
let missing = map.validate(&styles);
997+
// Default map has many styles that won't exist in a minimal stylesheet
998+
assert!(!missing.is_empty());
999+
// But Normal should not be in missing list
1000+
assert!(!missing.contains(&"Normal".to_string()));
1001+
}
1002+
1003+
#[test]
1004+
fn test_style_map_from_stylesheet_localized_headings() {
1005+
// Italian localized template with "Titolo" instead of "Heading"
1006+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1007+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1008+
<w:style w:type="paragraph" w:styleId="Titolo1">
1009+
<w:name w:val="Titolo 1"/>
1010+
<w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1011+
</w:style>
1012+
<w:style w:type="paragraph" w:styleId="Titolo2">
1013+
<w:name w:val="Titolo 2"/>
1014+
<w:pPr><w:outlineLvl w:val="1"/></w:pPr>
1015+
</w:style>
1016+
<w:style w:type="paragraph" w:styleId="Normale" w:default="1">
1017+
<w:name w:val="Normale"/>
1018+
</w:style>
1019+
</w:styles>"#;
1020+
1021+
let styles = StyleSheet::parse(xml).unwrap();
1022+
let map = StyleMap::from_stylesheet(&styles);
1023+
1024+
// Should detect headings by outline level, not by name
1025+
assert_eq!(map.heading(1), "Titolo1");
1026+
assert_eq!(map.heading(2), "Titolo2");
1027+
// Default paragraph style detected
1028+
assert_eq!(map.paragraph(), "Normale");
1029+
}
1030+
1031+
#[test]
1032+
fn test_style_map_from_stylesheet_code_style_detection() {
1033+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1034+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1035+
<w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1036+
<w:name w:val="Normal"/>
1037+
</w:style>
1038+
<w:style w:type="paragraph" w:styleId="HTMLPreformatted">
1039+
<w:name w:val="HTML Preformatted"/>
1040+
</w:style>
1041+
</w:styles>"#;
1042+
1043+
let styles = StyleSheet::parse(xml).unwrap();
1044+
let map = StyleMap::from_stylesheet(&styles);
1045+
1046+
// Should detect HTML Preformatted as code style
1047+
assert_eq!(map.code_block(), "HTMLPreformatted");
1048+
}
1049+
1050+
#[test]
1051+
fn test_style_map_from_stylesheet_list_detection() {
1052+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1053+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1054+
<w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1055+
<w:name w:val="Normal"/>
1056+
</w:style>
1057+
<w:style w:type="paragraph" w:styleId="ListParagraph">
1058+
<w:name w:val="List Paragraph"/>
1059+
</w:style>
1060+
</w:styles>"#;
1061+
1062+
let styles = StyleSheet::parse(xml).unwrap();
1063+
let map = StyleMap::from_stylesheet(&styles);
1064+
1065+
// Should detect list style by name
1066+
assert_eq!(map.list(false), "ListParagraph");
1067+
}
1068+
1069+
#[test]
1070+
fn test_style_map_from_stylesheet_table_detection() {
1071+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1072+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1073+
<w:style w:type="paragraph" w:styleId="Normal" w:default="1">
1074+
<w:name w:val="Normal"/>
1075+
</w:style>
1076+
<w:style w:type="table" w:styleId="GridTable1Light">
1077+
<w:name w:val="Grid Table 1 Light"/>
1078+
</w:style>
1079+
</w:styles>"#;
1080+
1081+
let styles = StyleSheet::parse(xml).unwrap();
1082+
let map = StyleMap::from_stylesheet(&styles);
1083+
1084+
// Should detect table style by name
1085+
assert_eq!(map.table(), "GridTable1Light");
1086+
}
1087+
1088+
#[test]
1089+
fn test_style_map_from_stylesheet_empty() {
1090+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1091+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1092+
</w:styles>"#;
1093+
1094+
let styles = StyleSheet::parse(xml).unwrap();
1095+
let map = StyleMap::from_stylesheet(&styles);
1096+
1097+
// Should fallback to defaults for unmapped elements
1098+
assert_eq!(map.heading(1), "Heading1"); // Fallback
1099+
assert_eq!(map.paragraph(), "Normal"); // Fallback
1100+
}
1101+
1102+
#[test]
1103+
fn test_stylesheet_parse_character_style() {
1104+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1105+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1106+
<w:style w:type="character" w:styleId="Strong" w:default="1">
1107+
<w:name w:val="Strong"/>
1108+
<w:uiPriority w:val="22"/>
1109+
</w:style>
1110+
<w:style w:type="character" w:styleId="Emphasis">
1111+
<w:name w:val="Emphasis"/>
1112+
</w:style>
1113+
</w:styles>"#;
1114+
1115+
let styles = StyleSheet::parse(xml).unwrap();
1116+
1117+
// Check character style parsed correctly
1118+
let strong = styles.get("Strong").unwrap();
1119+
assert_eq!(strong.style_type, StyleType::Character);
1120+
assert_eq!(strong.ui_priority, Some(22));
1121+
1122+
// Default character style should be tracked
1123+
assert_eq!(styles.default_character, Some("Strong".to_string()));
1124+
}
1125+
1126+
#[test]
1127+
fn test_stylesheet_parse_numbering_style() {
1128+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1129+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1130+
<w:style w:type="numbering" w:styleId="ListBullet">
1131+
<w:name w:val="List Bullet"/>
1132+
</w:style>
1133+
</w:styles>"#;
1134+
1135+
let styles = StyleSheet::parse(xml).unwrap();
1136+
1137+
let list = styles.get("ListBullet").unwrap();
1138+
assert_eq!(list.style_type, StyleType::Numbering);
1139+
}
1140+
1141+
#[test]
1142+
fn test_stylesheet_all_iterator() {
1143+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1144+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1145+
<w:style w:type="paragraph" w:styleId="Normal">
1146+
<w:name w:val="Normal"/>
1147+
</w:style>
1148+
<w:style w:type="character" w:styleId="Strong">
1149+
<w:name w:val="Strong"/>
1150+
</w:style>
1151+
<w:style w:type="table" w:styleId="TableGrid">
1152+
<w:name w:val="Table Grid"/>
1153+
</w:style>
1154+
</w:styles>"#;
1155+
1156+
let styles = StyleSheet::parse(xml).unwrap();
1157+
1158+
// all() should return all styles
1159+
let all: Vec<_> = styles.all().collect();
1160+
assert_eq!(all.len(), 3);
1161+
1162+
// Verify different types exist
1163+
let types: Vec<StyleType> = all.iter().map(|s| s.style_type).collect();
1164+
assert!(types.contains(&StyleType::Paragraph));
1165+
assert!(types.contains(&StyleType::Character));
1166+
assert!(types.contains(&StyleType::Table));
1167+
}
1168+
1169+
#[test]
1170+
fn test_stylesheet_heading_styles_iterator() {
1171+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1172+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1173+
<w:style w:type="paragraph" w:styleId="Heading1">
1174+
<w:name w:val="Heading 1"/>
1175+
<w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1176+
</w:style>
1177+
<w:style w:type="paragraph" w:styleId="Heading2">
1178+
<w:name w:val="Heading 2"/>
1179+
<w:pPr><w:outlineLvl w:val="1"/></w:pPr>
1180+
</w:style>
1181+
<w:style w:type="paragraph" w:styleId="Normal">
1182+
<w:name w:val="Normal"/>
1183+
</w:style>
1184+
</w:styles>"#;
1185+
1186+
let styles = StyleSheet::parse(xml).unwrap();
1187+
1188+
let headings: Vec<_> = styles.heading_styles().collect();
1189+
assert_eq!(headings.len(), 2);
1190+
1191+
// Verify all have outline levels
1192+
for h in &headings {
1193+
assert!(h.outline_level.is_some());
1194+
}
1195+
}
1196+
1197+
#[test]
1198+
fn test_stylesheet_default_paragraph_detection() {
1199+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1200+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1201+
<w:style w:type="paragraph" w:styleId="BodyText" w:default="1">
1202+
<w:name w:val="Body Text"/>
1203+
</w:style>
1204+
<w:style w:type="paragraph" w:styleId="Heading1">
1205+
<w:name w:val="Heading 1"/>
1206+
</w:style>
1207+
</w:styles>"#;
1208+
1209+
let styles = StyleSheet::parse(xml).unwrap();
1210+
1211+
// Should detect non-Normal default paragraph style
1212+
assert_eq!(styles.default_paragraph, Some("BodyText".to_string()));
1213+
}
1214+
1215+
#[test]
1216+
fn test_style_next_attribute() {
1217+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1218+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1219+
<w:style w:type="paragraph" w:styleId="Heading1">
1220+
<w:name w:val="Heading 1"/>
1221+
<w:next w:val="Normal"/>
1222+
</w:style>
1223+
<w:style w:type="paragraph" w:styleId="Normal">
1224+
<w:name w:val="Normal"/>
1225+
</w:style>
1226+
</w:styles>"#;
1227+
1228+
let styles = StyleSheet::parse(xml).unwrap();
1229+
1230+
let h1 = styles.get("Heading1").unwrap();
1231+
assert_eq!(h1.next, Some("Normal".to_string()));
1232+
}
1233+
1234+
#[test]
1235+
fn test_style_map_element_type_coverage() {
1236+
let map = StyleMap::default();
1237+
1238+
// Test all admonition types have defaults
1239+
assert_eq!(map.get(ElementType::AdmonitionNote), "Note");
1240+
assert_eq!(map.get(ElementType::AdmonitionTip), "Tip");
1241+
assert_eq!(map.get(ElementType::AdmonitionImportant), "Important");
1242+
assert_eq!(map.get(ElementType::AdmonitionWarning), "Warning");
1243+
assert_eq!(map.get(ElementType::AdmonitionCaution), "Caution");
1244+
1245+
// Test list types
1246+
assert_eq!(map.get(ElementType::ListDescription), "ListParagraph");
1247+
assert_eq!(map.get(ElementType::TableHeader), "TableGrid");
1248+
}
1249+
1250+
#[test]
1251+
fn test_style_map_fallback_styles() {
1252+
let map = StyleMap::new(); // Empty map
1253+
1254+
// All element types should have fallback styles
1255+
assert_eq!(map.get(ElementType::Heading(5)), "Heading1");
1256+
assert_eq!(map.get(ElementType::Paragraph), "Normal");
1257+
assert_eq!(map.get(ElementType::CodeBlock), "Normal");
1258+
assert_eq!(map.get(ElementType::ListBullet), "ListBullet");
1259+
assert_eq!(map.get(ElementType::ListNumber), "ListNumber");
1260+
assert_eq!(map.get(ElementType::ListDescription), "Normal");
1261+
assert_eq!(map.get(ElementType::Table), "TableGrid");
1262+
assert_eq!(map.get(ElementType::TableHeader), "TableGrid");
1263+
assert_eq!(map.get(ElementType::AdmonitionNote), "Normal");
1264+
}
1265+
1266+
#[test]
1267+
fn test_stylesheet_unknown_type_defaults_to_paragraph() {
1268+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1269+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1270+
<w:style w:type="unknown" w:styleId="Mystery">
1271+
<w:name w:val="Mystery Style"/>
1272+
</w:style>
1273+
</w:styles>"#;
1274+
1275+
let styles = StyleSheet::parse(xml).unwrap();
1276+
1277+
let mystery = styles.get("Mystery").unwrap();
1278+
// Unknown types default to Paragraph
1279+
assert_eq!(mystery.style_type, StyleType::Paragraph);
1280+
}
1281+
1282+
#[test]
1283+
fn test_stylesheet_style_without_name() {
1284+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1285+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1286+
<w:style w:type="paragraph" w:styleId="NoName">
1287+
</w:style>
1288+
</w:styles>"#;
1289+
1290+
let styles = StyleSheet::parse(xml).unwrap();
1291+
1292+
let no_name = styles.get("NoName").unwrap();
1293+
// Name should default to ID when not specified
1294+
assert_eq!(no_name.name, "NoName");
1295+
}
1296+
1297+
#[test]
1298+
fn test_style_map_from_stylesheet_fallback_by_id() {
1299+
// Test that from_stylesheet falls back to checking by styleId
1300+
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1301+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1302+
<w:style w:type="paragraph" w:styleId="Heading1">
1303+
<w:name w:val="Custom Name"/>
1304+
</w:style>
1305+
<w:style w:type="paragraph" w:styleId="Normal">
1306+
<w:name w:val="Another Name"/>
1307+
</w:style>
1308+
<w:style w:type="paragraph" w:styleId="CodeBlock">
1309+
<w:name w:val="My Code"/>
1310+
</w:style>
1311+
</w:styles>"#;
1312+
1313+
let styles = StyleSheet::parse(xml).unwrap();
1314+
let map = StyleMap::from_stylesheet(&styles);
1315+
1316+
// Should find by styleId fallback
1317+
assert_eq!(map.heading(1), "Heading1");
1318+
assert_eq!(map.paragraph(), "Normal");
1319+
assert_eq!(map.code_block(), "CodeBlock");
1320+
}
9381321
}

0 commit comments

Comments
 (0)