Skip to content

Commit cdee04e

Browse files
alanbldclaude
andcommitted
test(ooxml): Sprint 17 - Image and conversion edge cases
Image tests: - Add is_inline/is_anchor method tests - Add with_dimensions_emu direct EMU dimension tests - Add ImagePosition::default() and WrapType::default() tests - Add clone/copy/eq trait tests for Image, ImagePosition, WrapType - Add EMU constant verification and fractional inch conversions - Add anchor with all wrap types test - Add empty target filename edge case - Add case-insensitive content type tests - Add Debug format tests for Image, WrapType, ImagePosition Conversion tests: - Add Block::SectionBreak conversion test - Add document with section breaks integration test - Add table row header flag preservation test - Add table cell multiple paragraphs test - Add Run with only monospace formatting test - Add Run with nested bold/italic/monospace test - Add internal anchor hyperlink test - Add ParagraphChild::Image conversion test - Add ParagraphChild::Bookmark ignored test - Add convert_document_with_styles heading detection test - Add ConversionContext unknown style test 416 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0935a0c commit cdee04e

File tree

2 files changed

+508
-0
lines changed

2 files changed

+508
-0
lines changed

crates/utf8dok-ooxml/src/conversion.rs

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,4 +1161,328 @@ mod tests {
11611161
panic!("Expected Table block");
11621162
}
11631163
}
1164+
1165+
// ==================== Sprint 17: Conversion Edge Cases ====================
1166+
1167+
#[test]
1168+
fn test_block_section_break_conversion() {
1169+
let ctx = ConversionContext::default();
1170+
1171+
let section_break = Block::SectionBreak;
1172+
let ast_block = section_break.to_ast(&ctx);
1173+
1174+
// Section break should convert to a Break block
1175+
assert!(matches!(ast_block, AstBlock::Break(_)));
1176+
}
1177+
1178+
#[test]
1179+
fn test_document_with_section_breaks() {
1180+
let doc = Document {
1181+
blocks: vec![
1182+
Block::Paragraph(Paragraph {
1183+
style_id: None,
1184+
children: vec![ParagraphChild::Run(Run {
1185+
text: "Before".to_string(),
1186+
bold: false,
1187+
italic: false,
1188+
monospace: false,
1189+
})],
1190+
numbering: None,
1191+
}),
1192+
Block::SectionBreak,
1193+
Block::Paragraph(Paragraph {
1194+
style_id: None,
1195+
children: vec![ParagraphChild::Run(Run {
1196+
text: "After".to_string(),
1197+
bold: false,
1198+
italic: false,
1199+
monospace: false,
1200+
})],
1201+
numbering: None,
1202+
}),
1203+
],
1204+
};
1205+
1206+
let ast_doc = convert_document(&doc);
1207+
assert_eq!(ast_doc.blocks.len(), 3);
1208+
assert!(matches!(&ast_doc.blocks[0], AstBlock::Paragraph(_)));
1209+
assert!(matches!(&ast_doc.blocks[1], AstBlock::Break(_)));
1210+
assert!(matches!(&ast_doc.blocks[2], AstBlock::Paragraph(_)));
1211+
}
1212+
1213+
#[test]
1214+
fn test_table_row_header_flag() {
1215+
let ctx = ConversionContext::default();
1216+
1217+
let table = Table {
1218+
style_id: None,
1219+
rows: vec![
1220+
TableRow {
1221+
is_header: true,
1222+
cells: vec![TableCell {
1223+
paragraphs: vec![Paragraph {
1224+
style_id: None,
1225+
children: vec![ParagraphChild::Run(Run {
1226+
text: "Header".to_string(),
1227+
bold: false,
1228+
italic: false,
1229+
monospace: false,
1230+
})],
1231+
numbering: None,
1232+
}],
1233+
}],
1234+
},
1235+
TableRow {
1236+
is_header: false,
1237+
cells: vec![TableCell {
1238+
paragraphs: vec![Paragraph {
1239+
style_id: None,
1240+
children: vec![ParagraphChild::Run(Run {
1241+
text: "Data".to_string(),
1242+
bold: false,
1243+
italic: false,
1244+
monospace: false,
1245+
})],
1246+
numbering: None,
1247+
}],
1248+
}],
1249+
},
1250+
],
1251+
};
1252+
1253+
let block = table.to_ast(&ctx);
1254+
1255+
if let AstBlock::Table(ast_table) = block {
1256+
assert_eq!(ast_table.rows.len(), 2);
1257+
// Header flag should be preserved
1258+
assert!(ast_table.rows[0].is_header);
1259+
assert!(!ast_table.rows[1].is_header);
1260+
} else {
1261+
panic!("Expected Table block");
1262+
}
1263+
}
1264+
1265+
#[test]
1266+
fn test_table_cell_multiple_paragraphs() {
1267+
let ctx = ConversionContext::default();
1268+
1269+
let table = Table {
1270+
style_id: None,
1271+
rows: vec![TableRow {
1272+
is_header: false,
1273+
cells: vec![TableCell {
1274+
paragraphs: vec![
1275+
Paragraph {
1276+
style_id: None,
1277+
children: vec![ParagraphChild::Run(Run {
1278+
text: "Line 1".to_string(),
1279+
bold: false,
1280+
italic: false,
1281+
monospace: false,
1282+
})],
1283+
numbering: None,
1284+
},
1285+
Paragraph {
1286+
style_id: None,
1287+
children: vec![ParagraphChild::Run(Run {
1288+
text: "Line 2".to_string(),
1289+
bold: false,
1290+
italic: false,
1291+
monospace: false,
1292+
})],
1293+
numbering: None,
1294+
},
1295+
],
1296+
}],
1297+
}],
1298+
};
1299+
1300+
let block = table.to_ast(&ctx);
1301+
1302+
if let AstBlock::Table(ast_table) = block {
1303+
// Cell should have content for each paragraph
1304+
assert!(!ast_table.rows[0].cells[0].content.is_empty());
1305+
} else {
1306+
panic!("Expected Table block");
1307+
}
1308+
}
1309+
1310+
#[test]
1311+
fn test_run_with_only_monospace() {
1312+
let ctx = ConversionContext::default();
1313+
1314+
let run = Run {
1315+
text: "code".to_string(),
1316+
bold: false,
1317+
italic: false,
1318+
monospace: true,
1319+
};
1320+
1321+
let inline = run.to_ast(&ctx);
1322+
1323+
// Should be wrapped in Monospace format
1324+
if let Inline::Format(FormatType::Monospace, inner) = inline {
1325+
if let Inline::Text(text) = *inner {
1326+
assert_eq!(text, "code");
1327+
} else {
1328+
panic!("Expected inner Text");
1329+
}
1330+
} else {
1331+
panic!("Expected Monospace format");
1332+
}
1333+
}
1334+
1335+
#[test]
1336+
fn test_run_with_all_formatting_nested() {
1337+
let ctx = ConversionContext::default();
1338+
1339+
let run = Run {
1340+
text: "formatted".to_string(),
1341+
bold: true,
1342+
italic: true,
1343+
monospace: true,
1344+
};
1345+
1346+
let inline = run.to_ast(&ctx);
1347+
1348+
// Should have nested formatting (order: bold > italic > monospace)
1349+
fn count_nesting(inline: &Inline) -> u32 {
1350+
match inline {
1351+
Inline::Format(_, inner) => 1 + count_nesting(inner),
1352+
_ => 0,
1353+
}
1354+
}
1355+
1356+
assert_eq!(count_nesting(&inline), 3);
1357+
}
1358+
1359+
#[test]
1360+
fn test_hyperlink_internal_anchor() {
1361+
let ctx = ConversionContext::default();
1362+
1363+
let hyperlink = Hyperlink {
1364+
id: None,
1365+
anchor: Some("section-1".to_string()),
1366+
runs: vec![Run {
1367+
text: "Go to section".to_string(),
1368+
bold: false,
1369+
italic: false,
1370+
monospace: false,
1371+
}],
1372+
};
1373+
1374+
let inline = hyperlink.to_ast(&ctx);
1375+
1376+
if let Inline::Link(link) = inline {
1377+
assert!(link.url.starts_with('#'));
1378+
assert!(link.url.contains("section-1"));
1379+
} else {
1380+
panic!("Expected Link inline");
1381+
}
1382+
}
1383+
1384+
#[test]
1385+
fn test_paragraph_child_image_conversion() {
1386+
let ctx = ConversionContext::default();
1387+
1388+
let para = Paragraph {
1389+
style_id: None,
1390+
children: vec![ParagraphChild::Image(crate::image::Image::new_inline(
1391+
1,
1392+
"rId1".to_string(),
1393+
"media/image.png".to_string(),
1394+
))],
1395+
numbering: None,
1396+
};
1397+
1398+
let block = para.to_ast(&ctx);
1399+
1400+
// Image in paragraph should create some inline content
1401+
if let AstBlock::Paragraph(ast_para) = block {
1402+
assert!(!ast_para.inlines.is_empty());
1403+
} else {
1404+
panic!("Expected Paragraph block");
1405+
}
1406+
}
1407+
1408+
#[test]
1409+
fn test_paragraph_child_bookmark_ignored() {
1410+
let ctx = ConversionContext::default();
1411+
1412+
let para = Paragraph {
1413+
style_id: None,
1414+
children: vec![
1415+
ParagraphChild::Bookmark(crate::document::Bookmark {
1416+
name: "_Toc123".to_string(),
1417+
}),
1418+
ParagraphChild::Run(Run {
1419+
text: "Visible".to_string(),
1420+
bold: false,
1421+
italic: false,
1422+
monospace: false,
1423+
}),
1424+
],
1425+
numbering: None,
1426+
};
1427+
1428+
let block = para.to_ast(&ctx);
1429+
1430+
if let AstBlock::Paragraph(ast_para) = block {
1431+
// Bookmark should not create visible inline, only the run text
1432+
let text_count = ast_para
1433+
.inlines
1434+
.iter()
1435+
.filter(|i| matches!(i, Inline::Text(_)))
1436+
.count();
1437+
assert_eq!(text_count, 1);
1438+
} else {
1439+
panic!("Expected Paragraph block");
1440+
}
1441+
}
1442+
1443+
#[test]
1444+
fn test_convert_document_with_styles_preserves_headings() {
1445+
use crate::styles::StyleSheet;
1446+
1447+
let style_xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1448+
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1449+
<w:style w:type="paragraph" w:styleId="Heading1">
1450+
<w:name w:val="Heading 1"/>
1451+
<w:pPr><w:outlineLvl w:val="0"/></w:pPr>
1452+
</w:style>
1453+
</w:styles>"#;
1454+
1455+
let styles = StyleSheet::parse(style_xml).unwrap();
1456+
1457+
let doc = Document {
1458+
blocks: vec![Block::Paragraph(Paragraph {
1459+
style_id: Some("Heading1".to_string()),
1460+
children: vec![ParagraphChild::Run(Run {
1461+
text: "Title".to_string(),
1462+
bold: false,
1463+
italic: false,
1464+
monospace: false,
1465+
})],
1466+
numbering: None,
1467+
})],
1468+
};
1469+
1470+
let ast_doc = convert_document_with_styles(&doc, &styles);
1471+
assert_eq!(ast_doc.blocks.len(), 1);
1472+
1473+
// Should be converted to a heading
1474+
if let AstBlock::Heading(heading) = &ast_doc.blocks[0] {
1475+
assert_eq!(heading.level, 1);
1476+
} else {
1477+
panic!("Expected Heading block for heading style");
1478+
}
1479+
}
1480+
1481+
#[test]
1482+
fn test_context_heading_level_unknown_style() {
1483+
let ctx = ConversionContext::default();
1484+
1485+
// Unknown style should return None
1486+
assert!(ctx.heading_level("NonExistentStyle").is_none());
1487+
}
11641488
}

0 commit comments

Comments
 (0)