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

1"""Tests for finding and preparing Office templates.""" 

2 

3from __future__ import annotations 

4 

5import pathlib 

6import shutil 

7import zipfile 

8 

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 

15 

16import coursebook 

17from coursebook import templates 

18 

19 

20def test_list_templates() -> None: 

21 """Tests listing the included templates.""" 

22 assert templates.list_templates('powerpoint') == ['chalkboard'] 

23 assert templates.list_templates('word') == [] 

24 

25 

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' 

30 

31 

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) 

40 

41 

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) 

48 

49 

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) 

58 

59 

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 

71 

72 

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) 

80 

81 

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 

98 

99 

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 

104 

105 

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 

123 

124 

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' 

133 

134 

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' 

148 

149 

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')