"""Tests for XML helper functions — borders, shading, numbering. These prove the EFFECT (XML structure lands in the right place), not the call. Each test unzips the docx and inspects word/document.xml directly. """ import zipfile import xml.etree.ElementTree as ET from pathlib import Path import pytest from docx import Document from docx.oxml import OxmlElement from md2doc import ( add_paragraph_bottom_border, add_paragraph_shading, add_cell_shading, attach_list_numbering, register_multilevel_numbering, ) W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" def get_doc_xml(doc): tmp = Path(__file__).parent / "_tmp_helper_test.docx" doc.save(tmp) try: with zipfile.ZipFile(tmp) as z: xml_bytes = z.read("word/document.xml") return ET.fromstring(xml_bytes) finally: tmp.unlink() def find_all(root, tag): return root.findall(f".//{{{W_NS}}}{tag}") def test_add_paragraph_bottom_border_inserts_pBdr(): """H1: paragraph bottom border lands inside w:pPr as w:pBdr/w:bottom.""" doc = Document() p = doc.add_paragraph() add_paragraph_bottom_border(p, color="000000", size="12") root = get_doc_xml(doc) pBdr = find_all(root, "pBdr") bottom = find_all(root, "bottom") assert len(pBdr) >= 1, "w:pBdr missing" assert len(bottom) >= 1, "w:bottom missing" b = bottom[0] assert b.get(f"{{{W_NS}}}color") == "000000" assert b.get(f"{{{W_NS}}}sz") == "12" assert b.get(f"{{{W_NS}}}val") == "single" def test_add_paragraph_shading_inserts_shd(): """H2: paragraph shading w:shd lands inside w:pPr with correct fill.""" doc = Document() p = doc.add_paragraph() add_paragraph_shading(p, fill="D9D9D9") root = get_doc_xml(doc) shd = find_all(root, "shd") assert len(shd) >= 1, "w:shd missing" assert shd[0].get(f"{{{W_NS}}}fill") == "D9D9D9" assert shd[0].get(f"{{{W_NS}}}val") == "clear" def test_add_cell_shading_inserts_shd_in_tcPr(): """H4: cell shading w:shd lands inside w:tcPr.""" doc = Document() table = doc.add_table(rows=2, cols=2) cell = table.rows[0].cells[0] add_cell_shading(cell, fill="4472C4") root = get_doc_xml(doc) tcPr_list = find_all(root, "tcPr") shd_in_tc = [tc.find(f"{{{W_NS}}}shd") for tc in tcPr_list if tc.find(f"{{{W_NS}}}shd") is not None] assert len(shd_in_tc) >= 1, "w:shd missing from w:tcPr" assert shd_in_tc[0].get(f"{{{W_NS}}}fill") == "4472C4" def test_register_multilevel_numbering_creates_abstractNum_with_3_levels(): """H3 Route B: register creates ONE abstractNum with 3 w:lvl elements (ilvl 0/1/2). BEHAVIORAL: traces num_id → w:num → w:abstractNumId → w:abstractNum, then asserts THAT abstractNum (not all 9 default abstractNums in the template) has exactly the levels we registered. A `return 1` no-op mutation must FAIL. """ doc = Document() num_id = register_multilevel_numbering(doc, levels=3, kind="bullet") assert isinstance(num_id, int) tmp = Path(__file__).parent / "_tmp_num.docx" doc.save(tmp) try: with zipfile.ZipFile(tmp) as z: num_xml = z.read("word/numbering.xml") root = ET.fromstring(num_xml) our_num = None for num_el in root.findall(f"{{{W_NS}}}num"): if num_el.get(f"{{{W_NS}}}numId") == str(num_id): our_num = num_el break assert our_num is not None, f"No with numId={num_id} (function did nothing)" abstract_ref = our_num.find(f"{{{W_NS}}}abstractNumId") assert abstract_ref is not None, "missing w:abstractNumId in our w:num" abstract_id = abstract_ref.get(f"{{{W_NS}}}val") our_abstract = None for an in root.findall(f"{{{W_NS}}}abstractNum"): if an.get(f"{{{W_NS}}}abstractNumId") == abstract_id: our_abstract = an break assert our_abstract is not None, f"No with id={abstract_id}" lvls = our_abstract.findall(f"{{{W_NS}}}lvl") ilvls = sorted(int(l.get(f"{{{W_NS}}}ilvl")) for l in lvls) assert ilvls == [0, 1, 2], f"expected ilvl=[0,1,2] in OUR abstractNum, got {ilvls}" finally: tmp.unlink() def test_attach_list_numbering_sets_numPr_with_ilvl(): """H3 Route B: paragraph gets w:numPr with w:ilvl and w:numId.""" doc = Document() num_id = register_multilevel_numbering(doc, levels=3, kind="bullet") p = doc.add_paragraph("nested item") attach_list_numbering(p, num_id=num_id, ilvl=1) root = get_doc_xml(doc) numPr = find_all(root, "numPr") ilvl = find_all(root, "ilvl") numId_xml = find_all(root, "numId") assert len(numPr) >= 1, "w:numPr missing" assert len(ilvl) >= 1, "w:ilvl missing" assert ilvl[0].get(f"{{{W_NS}}}val") == "1" assert len(numId_xml) >= 1, "w:numId missing"