Coverage for tests/test_templates.py: 100%
100 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-31 22:06 +0000
1"""Tests for finding and preparing Office templates."""
3from __future__ import annotations
5import pathlib
6import shutil
7import zipfile
9import docx
10import pptx
11import pytest
12from docx.enum.text import WD_ALIGN_PARAGRAPH
13from docx.oxml.ns import qn
14from docx.shared import Pt
16import coursebook
17from coursebook import templates
20def test_list_templates() -> None:
21 """Tests listing the included templates."""
22 assert templates.list_templates('powerpoint') == ['chalkboard']
23 assert templates.list_templates('word') == []
26def test_find_included_template(tmp_path: pathlib.Path) -> None:
27 """Tests finding an included template by name."""
28 path = templates.find_template('chalkboard', 'powerpoint', tmp_path)
29 assert path.name == 'chalkboard.thmx'
32def test_project_template_comes_first(tmp_path: pathlib.Path) -> None:
33 """Tests that a template in the project folder is found first."""
34 included = templates.find_template('chalkboard', 'powerpoint', tmp_path)
35 copy = shutil.copy(included, tmp_path / 'chalkboard.thmx')
36 assert templates.find_template('chalkboard', 'powerpoint', tmp_path) == (
37 pathlib.Path(copy))
38 assert templates.find_template(
39 'chalkboard.thmx', 'powerpoint', tmp_path) == pathlib.Path(copy)
42def test_missing_template(tmp_path: pathlib.Path) -> None:
43 """Tests that a missing template lists the included templates."""
44 with pytest.raises(FileNotFoundError, match = 'chalkboard'):
45 templates.find_template('whiteboard', 'powerpoint', tmp_path)
46 with pytest.raises(FileNotFoundError, match = 'not found'):
47 templates.find_template('house.docx', 'word', tmp_path)
50def test_wrong_template_type(tmp_path: pathlib.Path) -> None:
51 """Tests a template with the wrong extension or application."""
52 with pytest.raises(ValueError, match = 'not a word template'):
53 templates.find_template('deck.pptx', 'word', tmp_path)
54 with pytest.raises(ValueError, match = 'unknown application'):
55 templates.find_template('deck', 'excel', tmp_path)
56 with pytest.raises(ValueError, match = 'not a Word or PowerPoint'):
57 templates.prepare_template(tmp_path / 'sheet.xltx', tmp_path)
60def test_convert_theme(tmp_path: pathlib.Path) -> None:
61 """Tests converting the chalkboard Office theme to a presentation."""
62 theme = templates.find_template('chalkboard', 'powerpoint', tmp_path)
63 path = templates.prepare_template(theme, tmp_path)
64 assert path.suffix == '.pptx'
65 presentation = pptx.Presentation(str(path))
66 names = {layout.name for layout in presentation.slide_layouts}
67 # Pandoc needs these layouts.
68 assert {
69 'Title Slide', 'Title and Content', 'Section Header', 'Two Content',
70 'Comparison', 'Content with Caption', 'Blank'} <= names
73def test_theme_without_presentation(tmp_path: pathlib.Path) -> None:
74 """Tests that a theme without a slide master raises an error."""
75 path = tmp_path / 'colors.thmx'
76 with zipfile.ZipFile(path, 'w') as theme:
77 theme.writestr('theme/theme/theme1.xml', '<a:theme/>')
78 with pytest.raises(ValueError, match = 'slide master'):
79 templates.prepare_template(path, tmp_path)
82def test_convert_office_templates(tmp_path: pathlib.Path) -> None:
83 """Tests converting '.dotx' and '.potx' templates."""
84 pairs = (('docx', 'dotx', 'wordprocessingml'),
85 ('pptx', 'potx', 'presentationml'))
86 for document, template, kind in pairs:
87 source = tmp_path / f'house.{template}'
88 main = f'application/vnd.openxmlformats-officedocument.{kind}'
89 with zipfile.ZipFile(source, 'w') as package:
90 package.writestr(
91 '[Content_Types].xml',
92 f'<Types>{main}.template.main+xml</Types>')
93 converted = templates.prepare_template(source, tmp_path / 'out')
94 assert converted.suffix == f'.{document}'
95 with zipfile.ZipFile(converted) as package:
96 types = package.read('[Content_Types].xml').decode()
97 assert 'template.main' not in types
100def test_documents_are_used_as_is(tmp_path: pathlib.Path) -> None:
101 """Tests that '.docx' and '.pptx' files are not converted."""
102 path = tmp_path / 'house.docx'
103 assert templates.prepare_template(path, tmp_path / 'out') == path
106def test_create_word_template(tmp_path: pathlib.Path) -> None:
107 """Tests applying the law review formatting to a Word template."""
108 path = templates.create_word_template(
109 coursebook.LawReview().formatting, tmp_path / 'reference.docx')
110 document = docx.Document(str(path))
111 normal = document.styles['Normal']
112 assert normal.font.name == 'Times New Roman'
113 assert normal.font.size == Pt(12)
114 assert document.styles['Body Text'].paragraph_format.line_spacing == 2.0
115 assert document.styles['Footnote Text'].font.size == Pt(10)
116 heading = document.styles['Heading 1']
117 assert heading.font.name == 'Times New Roman'
118 assert heading.font.all_caps is True
119 assert heading.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER
120 assert str(heading.font.color.rgb) == '000000'
121 fonts = heading.element.rPr.find(qn('w:rFonts'))
122 assert fonts.get(qn('w:asciiTheme')) is None
125def test_empty_formatting_keeps_pandoc_template(tmp_path: pathlib.Path) -> None:
126 """Tests that empty formatting keeps pandoc's fonts."""
127 path = templates.create_word_template(
128 coursebook.Formatting(), tmp_path / 'reference.docx')
129 document = docx.Document(str(path))
130 assert document.styles['Normal'].font.name is None
131 fonts = document.styles['Heading 1'].element.rPr.find(qn('w:rFonts'))
132 assert fonts.get(qn('w:asciiTheme')) == 'majorHAnsi'
135def test_word_template_page_layout(tmp_path: pathlib.Path) -> None:
136 """Tests margins, page numbers, and continuous footnote numbers."""
137 path = templates.create_word_template(
138 coursebook.LawReview().formatting, tmp_path / 'reference.docx')
139 document = docx.Document(str(path))
140 section = document.sections[0]
141 assert section.left_margin.inches == 1.0
142 assert section.top_margin.inches == 1.0
143 footer = section.footer.paragraphs[0]._p.xml
144 assert 'PAGE' in footer
145 properties = section._sectPr.find(qn('w:footnotePr'))
146 restart = properties.find(qn('w:numRestart'))
147 assert restart.get(qn('w:val')) == 'continuous'
150def test_find_included_csl(tmp_path: pathlib.Path) -> None:
151 """Tests finding the included Bluebook citation style."""
152 assert templates.list_templates('csl') == ['bluebook-law-review']
153 path = templates.find_template('bluebook-law-review', 'csl', tmp_path)
154 assert path.suffix == '.csl'
155 assert 'class="note"' in path.read_text(encoding = 'utf-8')