139 lines
4.8 KiB
Python
139 lines
4.8 KiB
Python
"""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 <w:num> 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 <w:abstractNum> 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"
|