Coverage for tests/test_markdown.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-31 22:06 +0000

1"""Tests for the markdown helpers and source files.""" 

2 

3from __future__ import annotations 

4 

5import pathlib 

6 

7import pytest 

8 

9from coursebook import markdown, settings, sources 

10 

11FENCE: str = '```' 

12 

13 

14def test_front_matter_with_dashes() -> None: 

15 """Tests front matter between '---' lines.""" 

16 metadata, body = markdown.split_front_matter( 

17 '\n---\ntitle: Facts\nvisuals_in_notes: yes\n---\nBody\n') 

18 assert metadata == {'title': 'Facts', 'visuals_in_notes': True} 

19 assert body.strip() == 'Body' 

20 

21 

22def test_front_matter_with_quotes() -> None: 

23 """Tests front matter between "'''" lines and empty front matter.""" 

24 metadata, body = markdown.split_front_matter("'''\n\n'''\n\nBody") 

25 assert metadata == {} 

26 assert body.strip() == 'Body' 

27 

28 

29def test_no_front_matter() -> None: 

30 """Tests that text without front matter is returned unchanged.""" 

31 text = 'Body\n---\nmore' 

32 assert markdown.split_front_matter(text) == ({}, text) 

33 

34 

35def test_unclosed_front_matter() -> None: 

36 """Tests that front matter must be closed.""" 

37 with pytest.raises(ValueError, match = 'never closed'): 

38 markdown.split_front_matter('---\ntitle: x\n') 

39 

40 

41def test_front_matter_must_be_mapping() -> None: 

42 """Tests that front matter must be a mapping.""" 

43 with pytest.raises(ValueError, match = 'mapping'): 

44 markdown.split_front_matter('---\n- a\n- b\n---\n') 

45 

46 

47def test_split_sections() -> None: 

48 """Tests dividers, the default section, and repeated sections.""" 

49 text = ( 

50 'Opening text.\n\n<slides>\n# Slide\n\n<NOTES>\nA note.\n\n' 

51 '</notes>\nMore text.\n\n< text >\nLast text.') 

52 sections = markdown.split_sections( 

53 text, names = settings.SECTION_ALIASES, default = 'text') 

54 assert sections['slides'] == '# Slide' 

55 assert sections['notes'] == 'A note.' 

56 assert sections['text'] == 'Opening text.\n\n\nMore text.\n\n\nLast text.' 

57 

58 

59def test_split_sections_skips_empty_sections() -> None: 

60 """Tests that sections with only blank lines are left out.""" 

61 sections = markdown.split_sections( 

62 '\n<text>\n\n\n<slides>\n\n', 

63 names = settings.SECTION_ALIASES, 

64 default = 'text') 

65 assert sections == {} 

66 

67 

68def test_split_sections_manuscript_alias() -> None: 

69 """Tests that '<manuscript>' marks the text.""" 

70 sections = markdown.split_sections( 

71 '<manuscript>\nWords', names = settings.SECTION_ALIASES, 

72 default = 'text') 

73 assert sections == {'text': 'Words'} 

74 

75 

76def test_dividers_in_code_are_ignored() -> None: 

77 """Tests that dividers inside fenced code blocks are not dividers.""" 

78 text = f'Text\n{FENCE}\n<slides>\n{FENCE}\n<slides>\nSlide' 

79 sections = markdown.split_sections( 

80 text, names = settings.SECTION_ALIASES, default = 'text') 

81 assert sections['text'] == f'Text\n{FENCE}\n<slides>\n{FENCE}' 

82 assert sections['slides'] == 'Slide' 

83 

84 

85def test_unknown_divider() -> None: 

86 """Tests that unknown dividers raise an error.""" 

87 with pytest.raises(ValueError, match = 'unknown divider'): 

88 markdown.split_sections( 

89 '<handouts>\n', names = settings.SECTION_ALIASES, 

90 default = 'text') 

91 

92 

93def test_shift_headings() -> None: 

94 """Tests shifting headings, including code blocks and limits.""" 

95 text = f'# One\n#hashtag\n{FENCE}\n# code\n{FENCE}\n##### Five\n#' 

96 shifted = markdown.shift_headings(text, levels = 2) 

97 assert shifted.split('\n') == [ 

98 '### One', '#hashtag', FENCE, '# code', FENCE, '###### Five', '###'] 

99 assert markdown.shift_headings('### Three', levels = -5) == '# Three' 

100 

101 

102def test_namespace_footnotes() -> None: 

103 """Tests that footnote labels get a prefix outside code.""" 

104 text = f'Claim.[^1] Inline.^[note]\n\n[^1]: Note.\n{FENCE}\n[^1]\n{FENCE}' 

105 namespaced = markdown.namespace_footnotes(text, prefix = 'intro') 

106 assert namespaced.split('\n') == [ 

107 'Claim.[^intro-1] Inline.^[note]', '', '[^intro-1]: Note.', FENCE, 

108 '[^1]', FENCE] 

109 

110 

111def test_extract_images() -> None: 

112 """Tests finding images and their attributes.""" 

113 text = ( 

114 'See ![A chart](chart.png){width=50%} and ![](b.jpg).\n' 

115 f'{FENCE}\n![code](c.png)\n{FENCE}') 

116 assert markdown.extract_images(text) == [ 

117 '![A chart](chart.png){width=50%}', '![](b.jpg)'] 

118 

119 

120@pytest.mark.parametrize(('name', 'expected'), [ 

121 ('Interesting Premise 1', 'interesting_premise_1'), 

122 ('Law & Economics', 'law_economics'), 

123 (' Law Review ', 'law_review'), 

124 ('law_review', 'law_review')]) 

125def test_normalize_name(name: str, expected: str) -> None: 

126 """Tests normalizing names for files and settings.""" 

127 assert markdown.normalize_name(name) == expected 

128 

129 

130def test_source_from_file(tmp_path: pathlib.Path) -> None: 

131 """Tests loading a source file.""" 

132 path = tmp_path / 'My Idea.md' 

133 path.write_text( 

134 '---\ntitle: Better Title\n---\nText\n<slides>\n# Slide\n', 

135 encoding = 'utf-8') 

136 source = sources.Source.from_file(path) 

137 assert source.name == 'my_idea' 

138 assert source.metadata == {'title': 'Better Title'} 

139 assert source.get('text') == 'Text' 

140 assert source.get('slides') == '# Slide' 

141 assert source.get('notes') == '' 

142 

143 

144def test_source_errors_name_the_file(tmp_path: pathlib.Path) -> None: 

145 """Tests that errors in a source file name the file.""" 

146 (tmp_path / 'broken.md').write_text('<handout>\n', encoding = 'utf-8') 

147 with pytest.raises(ValueError, match = 'broken.md'): 

148 sources.load_sources(tmp_path) 

149 

150 

151def test_duplicate_sources(tmp_path: pathlib.Path) -> None: 

152 """Tests that two files for the same entry raise an error.""" 

153 (tmp_path / 'My_Idea.md').write_text('a', encoding = 'utf-8') 

154 (tmp_path / 'my idea.md').write_text('b', encoding = 'utf-8') 

155 with pytest.raises(ValueError, match = 'same outline entry'): 

156 sources.load_sources(tmp_path)