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
« 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."""
3from __future__ import annotations
5import pathlib
7import pytest
9from coursebook import markdown, settings, sources
11FENCE: str = '```'
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'
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'
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)
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')
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')
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.'
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 == {}
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'}
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'
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')
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'
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]
111def test_extract_images() -> None:
112 """Tests finding images and their attributes."""
113 text = (
114 'See {width=50%} and .\n'
115 f'{FENCE}\n\n{FENCE}')
116 assert markdown.extract_images(text) == [
117 '{width=50%}', '']
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
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') == ''
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)
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)