Skip to content

Commit 1a736db

Browse files
committed
WIP: improving tests for the backend markdown support
1 parent 4c3ad65 commit 1a736db

File tree

7 files changed

+1114
-968
lines changed

7 files changed

+1114
-968
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ python manage.py migrate # Run twice for askbot and django_authopenid apps
2424
### Development Environment
2525
- Test project available at: `testproject/` for development and testing
2626
- Working site example at: `askbot_site/`
27-
- Virtual environment typically in: `env/`
27+
- Virtual environment typically in: `env/`, but the specific environment to use might be passed to you in the task file; pay attention to the task at hand.
2828

2929
### Pre-commit Hooks
3030
```bash

askbot/tests/test_markdown_integration.py

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Integration tests for complete markdown-it-py setup with all plugins.
33
"""
4+
from bs4 import BeautifulSoup
45
from django.test import TestCase
56
from askbot.tests.utils import with_settings
67
from askbot.utils.markup import get_md_converter, reset_md_converter
@@ -38,26 +39,88 @@ def test_footnotes(self):
3839
md = get_md_converter()
3940
text = "Text with footnote[^1]\n\n[^1]: Footnote content"
4041
html = md.render(text)
41-
self.assertIn('footnote', html.lower())
42+
43+
soup = BeautifulSoup(html, 'html5lib')
44+
45+
# Check for footnote reference (superscript with link)
46+
footnote_refs = soup.find_all('sup', class_='footnote-ref')
47+
self.assertEqual(len(footnote_refs), 1)
48+
49+
# Check for footnote section at bottom
50+
footnote_section = soup.find('section', class_='footnotes')
51+
self.assertIsNotNone(footnote_section)
52+
53+
# Check footnote content
54+
footnote_list = footnote_section.find('ol')
55+
self.assertIsNotNone(footnote_list)
56+
footnote_items = footnote_list.find_all('li')
57+
self.assertEqual(len(footnote_items), 1)
58+
self.assertIn('Footnote content', footnote_items[0].text)
4259

4360
def test_task_lists(self):
4461
md = get_md_converter()
4562
text = "- [ ] Unchecked\n- [x] Checked"
4663
html = md.render(text)
47-
self.assertTrue('checkbox' in html.lower() or 'task' in html.lower())
64+
65+
soup = BeautifulSoup(html, 'html5lib')
66+
67+
# Find all checkbox inputs
68+
checkboxes = soup.find_all('input', type='checkbox')
69+
self.assertEqual(len(checkboxes), 2)
70+
71+
# Verify unchecked box
72+
self.assertFalse(checkboxes[0].has_attr('checked'))
73+
74+
# Verify checked box
75+
self.assertTrue(checkboxes[1].has_attr('checked'))
76+
77+
# Verify task list classes
78+
task_list = soup.find('ul', class_='contains-task-list')
79+
self.assertIsNotNone(task_list)
80+
81+
task_items = soup.find_all('li', class_='task-list-item')
82+
self.assertEqual(len(task_items), 2)
4883

4984
def test_syntax_highlighting(self):
5085
md = get_md_converter()
5186
text = "```python\ndef hello():\n pass\n```"
5287
html = md.render(text)
53-
self.assertIn('highlight', html)
88+
89+
soup = BeautifulSoup(html, 'html5lib')
90+
91+
# Check for pre > code structure
92+
pre_tag = soup.find('pre')
93+
self.assertIsNotNone(pre_tag)
94+
95+
code_tag = pre_tag.find('code')
96+
self.assertIsNotNone(code_tag)
97+
98+
# Verify language class
99+
self.assertTrue(
100+
'language-python' in code_tag.get('class', []) or
101+
'highlight' in code_tag.get('class', [])
102+
)
103+
104+
# Verify code content is present
105+
self.assertIn('def hello():', code_tag.text)
106+
self.assertIn('pass', code_tag.text)
54107

55108
def test_video_embedding(self):
56109
md = get_md_converter()
57110
text = "Check this: @[youtube](dQw4w9WgXcQ)"
58111
html = md.render(text)
59-
self.assertIn('youtube.com/embed/dQw4w9WgXcQ', html)
60-
self.assertIn('iframe', html)
112+
113+
soup = BeautifulSoup(html, 'html5lib')
114+
115+
# Find iframe element
116+
iframe = soup.find('iframe')
117+
self.assertIsNotNone(iframe)
118+
119+
# Verify src attribute
120+
self.assertIn('youtube.com/embed/dQw4w9WgXcQ', iframe['src'])
121+
122+
# Verify surrounding text
123+
self.assertIn('Check this:', html)
61124

62125
@with_settings(ENABLE_AUTO_LINKING=True,
63126
AUTO_LINK_PATTERNS=r'#bug(\d+)',
@@ -69,7 +132,19 @@ def test_link_patterns_enabled(self):
69132
text = "Fixed #bug123"
70133
html = md.render(text)
71134

72-
self.assertIn('bugs.example.com/123', html)
135+
soup = BeautifulSoup(html, 'html5lib')
136+
137+
# Find the link
138+
links = soup.find_all('a')
139+
self.assertEqual(len(links), 1)
140+
141+
link = links[0]
142+
self.assertEqual(link['href'], 'https://bugs.example.com/123')
143+
self.assertEqual(link.text.strip(), '#bug123')
144+
145+
# Verify surrounding text preserved
146+
paragraph = soup.find('p')
147+
self.assertIn('Fixed', paragraph.text)
73148

74149
@with_settings(MARKUP_CODE_FRIENDLY=True)
75150
def test_code_friendly_mode(self):
@@ -93,13 +168,28 @@ def test_mathjax_math_delimiters_preserved(self):
93168
# Inline math
94169
text = "The equation $E = mc^2$ is famous"
95170
html = md.render(text)
96-
self.assertTrue('$E = mc^2$' in html or '$E = mc^2$' in html.replace(' ', ' '))
171+
172+
soup = BeautifulSoup(html, 'html5lib')
173+
paragraph = soup.find('p')
174+
self.assertIsNotNone(paragraph)
175+
176+
# Verify math delimiters are preserved (not converted to HTML)
177+
para_html = str(paragraph)
178+
self.assertIn('$E = mc^2$', para_html)
179+
self.assertNotIn('<em>', para_html) # No emphasis tags in math
180+
self.assertIn('famous', paragraph.text)
97181

98182
# Display math
99183
text = "$$\\int_0^1 x dx = \\frac{1}{2}$$"
100184
html = md.render(text)
185+
186+
soup = BeautifulSoup(html, 'html5lib')
187+
188+
# Display math should be in its own block
101189
self.assertIn('$$', html)
102-
self.assertTrue('\\int_0^1' in html or r'\int_0^1' in html)
190+
# Verify LaTeX commands preserved
191+
self.assertIn('\\int_0^1', html)
192+
self.assertIn('\\frac{1}{2}', html)
103193

104194
@with_settings(ENABLE_MATHJAX=True)
105195
def test_mathjax_underscores_not_emphasis(self):
@@ -110,10 +200,19 @@ def test_mathjax_underscores_not_emphasis(self):
110200
text = "$a_b$ and $x_{123}$"
111201
html = md.render(text)
112202

113-
# Should NOT have <em> or <sub> tags inside math
114-
# Math content should be preserved verbatim
115-
self.assertTrue('$a_b$' in html or '$a_b$' in html.replace('&nbsp;', ' '))
116-
self.assertTrue('<em>' not in html or html.count('<em>') == 0)
203+
soup = BeautifulSoup(html, 'html5lib')
204+
205+
# Verify no em or sub tags created
206+
em_tags = soup.find_all('em')
207+
sub_tags = soup.find_all('sub')
208+
self.assertEqual(len(em_tags), 0, "Found emphasis tags in math content")
209+
self.assertEqual(len(sub_tags), 0, "Found subscript tags in math content")
210+
211+
# Verify math delimiters preserved
212+
paragraph = soup.find('p')
213+
para_html = str(paragraph)
214+
self.assertIn('$a_b$', para_html)
215+
self.assertIn('$x_{123}$', para_html)
117216

118217
def test_combined_features(self):
119218
"""Test document using multiple features"""
@@ -141,9 +240,42 @@ def example():
141240
"""
142241
html = md.render(text)
143242

144-
# Check all features rendered
145-
self.assertIn('<h1>Title</h1>', html)
146-
self.assertIn('<strong>bold text</strong>', html)
147-
self.assertIn('youtube.com/embed/abc123', html)
148-
self.assertTrue('highlight' in html or 'class="language-python"' in html)
149-
self.assertIn('<table>', html)
243+
soup = BeautifulSoup(html, 'html5lib')
244+
245+
# Verify heading
246+
h1 = soup.find('h1')
247+
self.assertIsNotNone(h1)
248+
self.assertEqual(h1.text.strip(), 'Title')
249+
250+
# Verify bold text
251+
strong = soup.find('strong')
252+
self.assertIsNotNone(strong)
253+
self.assertEqual(strong.text, 'bold text')
254+
255+
# Verify video iframe
256+
iframe = soup.find('iframe')
257+
self.assertIsNotNone(iframe)
258+
self.assertIn('abc123', iframe['src'])
259+
260+
# Verify code block with language class
261+
pre = soup.find('pre')
262+
self.assertIsNotNone(pre)
263+
code = pre.find('code')
264+
self.assertIsNotNone(code)
265+
self.assertTrue(
266+
'language-python' in code.get('class', []) or
267+
'highlight' in str(pre)
268+
)
269+
self.assertIn('def example():', code.text)
270+
271+
# Verify table structure
272+
table = soup.find('table')
273+
self.assertIsNotNone(table)
274+
th_cells = table.find_all('th')
275+
self.assertEqual(len(th_cells), 2)
276+
277+
# Verify task list
278+
checkboxes = soup.find_all('input', type='checkbox')
279+
self.assertEqual(len(checkboxes), 2)
280+
self.assertTrue(checkboxes[0].has_attr('checked')) # First is checked
281+
self.assertFalse(checkboxes[1].has_attr('checked')) # Second unchecked

askbot/tests/test_markdown_link_patterns_plugin.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tests for the custom link patterns plugin."""
2+
from bs4 import BeautifulSoup
23
from django.test import TestCase
34
from markdown_it import MarkdownIt
45
from askbot.utils.markdown_plugins.link_patterns import link_patterns_plugin
@@ -30,8 +31,26 @@ def test_multiple_patterns(self):
3031
text = "Fixed #bug456 by @alice"
3132
html = md.render(text)
3233

33-
self.assertIn('bugs.example.com/456', html)
34-
self.assertIn('github.com/alice', html)
34+
soup = BeautifulSoup(html, 'html5lib')
35+
36+
# Should have exactly 2 links
37+
links = soup.find_all('a')
38+
self.assertEqual(len(links), 2)
39+
40+
# Verify bug link
41+
bug_link = [l for l in links if 'bugs.example.com' in l['href']][0]
42+
self.assertEqual(bug_link['href'], 'https://bugs.example.com/456')
43+
self.assertEqual(bug_link.text, '#bug456')
44+
45+
# Verify mention link
46+
mention_link = [l for l in links if 'github.com' in l['href']][0]
47+
self.assertEqual(mention_link['href'], 'https://github.com/alice')
48+
self.assertEqual(mention_link.text, '@alice')
49+
50+
# Verify surrounding text preserved
51+
paragraph = soup.find('p')
52+
self.assertIn('Fixed', paragraph.text)
53+
self.assertIn('by', paragraph.text)
3554

3655
def test_disabled_plugin(self):
3756
md = MarkdownIt().use(link_patterns_plugin, {
@@ -72,11 +91,21 @@ def test_pattern_in_code_block_not_linkified(self):
7291
text = "Text #bug123 and `code #bug456` here"
7392
html = md.render(text)
7493

75-
# #bug123 should be linked (in text)
76-
self.assertIn('bugs.example.com/123', html)
94+
soup = BeautifulSoup(html, 'html5lib')
95+
96+
# Should have exactly 1 link (the one in text, not in code)
97+
links = soup.find_all('a')
98+
self.assertEqual(len(links), 1)
99+
self.assertEqual(links[0]['href'], 'https://bugs.example.com/123')
77100

78-
# #bug456 should NOT be linked (in code)
79-
self.assertNotIn('bugs.example.com/456', html)
101+
# Verify code element exists and contains unlinked pattern
102+
code = soup.find('code')
103+
self.assertIsNotNone(code)
104+
self.assertIn('#bug456', code.text)
105+
106+
# Ensure no link inside code element
107+
code_links = code.find_all('a')
108+
self.assertEqual(len(code_links), 0)
80109

81110
def test_invalid_regex_ignored(self):
82111
# Plugin should not crash on invalid regex
@@ -88,7 +117,17 @@ def test_invalid_regex_ignored(self):
88117

89118
text = "Some text"
90119
html = md.render(text)
91-
self.assertIn('Some text', html) # Should still render
120+
121+
soup = BeautifulSoup(html, 'html5lib')
122+
123+
# Verify text rendered properly in paragraph
124+
paragraph = soup.find('p')
125+
self.assertIsNotNone(paragraph)
126+
self.assertEqual(paragraph.text.strip(), 'Some text')
127+
128+
# Verify no links created due to invalid regex
129+
links = soup.find_all('a')
130+
self.assertEqual(len(links), 0)
92131

93132
def test_mismatched_pattern_url_count(self):
94133
# Should disable auto-linking if counts don't match

0 commit comments

Comments
 (0)