Skip to content

Commit 29b3d18

Browse files
committed
test: add some component tests for <MarkdownText>
1 parent a268db2 commit 29b3d18

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { mountSuspended } from '@nuxt/test-utils/runtime'
3+
import MarkdownText from '~/components/MarkdownText.vue'
4+
5+
describe('MarkdownText', () => {
6+
describe('plain text', () => {
7+
it('renders plain text unchanged', async () => {
8+
const component = await mountSuspended(MarkdownText, {
9+
props: { text: 'Hello world' },
10+
})
11+
expect(component.text()).toBe('Hello world')
12+
})
13+
14+
it('returns empty for empty text', async () => {
15+
const component = await mountSuspended(MarkdownText, {
16+
props: { text: '' },
17+
})
18+
expect(component.text()).toBe('')
19+
})
20+
})
21+
22+
describe('HTML escaping', () => {
23+
it('escapes HTML tags to prevent XSS', async () => {
24+
const component = await mountSuspended(MarkdownText, {
25+
props: { text: '<script>alert("xss")</script>' },
26+
})
27+
expect(component.html()).not.toContain('<script>')
28+
expect(component.text()).toContain('<script>')
29+
})
30+
31+
it('escapes special characters', async () => {
32+
const component = await mountSuspended(MarkdownText, {
33+
props: { text: 'a < b && c > d' },
34+
})
35+
expect(component.text()).toBe('a < b && c > d')
36+
})
37+
})
38+
39+
describe('bold formatting', () => {
40+
it('renders **text** as bold', async () => {
41+
const component = await mountSuspended(MarkdownText, {
42+
props: { text: 'This is **bold** text' },
43+
})
44+
const strong = component.find('strong')
45+
expect(strong.exists()).toBe(true)
46+
expect(strong.text()).toBe('bold')
47+
})
48+
49+
it('renders __text__ as bold', async () => {
50+
const component = await mountSuspended(MarkdownText, {
51+
props: { text: 'This is __bold__ text' },
52+
})
53+
const strong = component.find('strong')
54+
expect(strong.exists()).toBe(true)
55+
expect(strong.text()).toBe('bold')
56+
})
57+
})
58+
59+
describe('italic formatting', () => {
60+
it('renders *text* as italic', async () => {
61+
const component = await mountSuspended(MarkdownText, {
62+
props: { text: 'This is *italic* text' },
63+
})
64+
const em = component.find('em')
65+
expect(em.exists()).toBe(true)
66+
expect(em.text()).toBe('italic')
67+
})
68+
69+
it('renders _text_ as italic', async () => {
70+
const component = await mountSuspended(MarkdownText, {
71+
props: { text: 'This is _italic_ text' },
72+
})
73+
const em = component.find('em')
74+
expect(em.exists()).toBe(true)
75+
expect(em.text()).toBe('italic')
76+
})
77+
})
78+
79+
describe('inline code', () => {
80+
it('renders `code` in code tags', async () => {
81+
const component = await mountSuspended(MarkdownText, {
82+
props: { text: 'Run `npm install` to start' },
83+
})
84+
const code = component.find('code')
85+
expect(code.exists()).toBe(true)
86+
expect(code.text()).toBe('npm install')
87+
})
88+
})
89+
90+
describe('strikethrough', () => {
91+
it('renders ~~text~~ as strikethrough', async () => {
92+
const component = await mountSuspended(MarkdownText, {
93+
props: { text: 'This is ~~deleted~~ text' },
94+
})
95+
const del = component.find('del')
96+
expect(del.exists()).toBe(true)
97+
expect(del.text()).toBe('deleted')
98+
})
99+
})
100+
101+
describe('links', () => {
102+
it('renders [text](https://url) as a link', async () => {
103+
const component = await mountSuspended(MarkdownText, {
104+
props: { text: 'Visit [our site](https://example.com) for more' },
105+
})
106+
const link = component.find('a')
107+
expect(link.exists()).toBe(true)
108+
expect(link.attributes('href')).toBe('https://example.com/')
109+
expect(link.text()).toBe('our site')
110+
})
111+
112+
it('adds security attributes to links', async () => {
113+
const component = await mountSuspended(MarkdownText, {
114+
props: { text: '[link](https://example.com)' },
115+
})
116+
const link = component.find('a')
117+
expect(link.attributes('rel')).toBe('nofollow noreferrer noopener')
118+
expect(link.attributes('target')).toBe('_blank')
119+
})
120+
121+
it('allows mailto: links', async () => {
122+
const component = await mountSuspended(MarkdownText, {
123+
props: { text: 'Contact [us](mailto:test@example.com)' },
124+
})
125+
const link = component.find('a')
126+
expect(link.exists()).toBe(true)
127+
expect(link.attributes('href')).toBe('mailto:test@example.com')
128+
})
129+
130+
it('blocks javascript: protocol links', async () => {
131+
const component = await mountSuspended(MarkdownText, {
132+
props: { text: '[click me](javascript:alert("xss"))' },
133+
})
134+
const link = component.find('a')
135+
expect(link.exists()).toBe(false)
136+
expect(component.text()).toContain('click me')
137+
})
138+
139+
it('blocks http: links (only https allowed)', async () => {
140+
const component = await mountSuspended(MarkdownText, {
141+
props: { text: '[site](http://example.com)' },
142+
})
143+
const link = component.find('a')
144+
expect(link.exists()).toBe(false)
145+
expect(component.text()).toContain('site')
146+
})
147+
148+
it('handles invalid URLs gracefully', async () => {
149+
const component = await mountSuspended(MarkdownText, {
150+
props: { text: '[link](not a valid url)' },
151+
})
152+
const link = component.find('a')
153+
expect(link.exists()).toBe(false)
154+
expect(component.text()).toContain('link')
155+
})
156+
157+
it('handles URLs with ampersands', async () => {
158+
const component = await mountSuspended(MarkdownText, {
159+
props: { text: '[search](https://example.com?a=1&b=2)' },
160+
})
161+
const link = component.find('a')
162+
expect(link.exists()).toBe(true)
163+
expect(link.attributes('href')).toBe('https://example.com/?a=1&b=2')
164+
})
165+
})
166+
167+
describe('plain prop', () => {
168+
it('renders link text without anchor tag when plain=true', async () => {
169+
const component = await mountSuspended(MarkdownText, {
170+
props: {
171+
text: 'Visit [our site](https://example.com) for more',
172+
plain: true,
173+
},
174+
})
175+
const link = component.find('a')
176+
expect(link.exists()).toBe(false)
177+
expect(component.text()).toBe('Visit our site for more')
178+
})
179+
180+
it('still renders other formatting when plain=true', async () => {
181+
const component = await mountSuspended(MarkdownText, {
182+
props: {
183+
text: '**bold** and [link](https://example.com)',
184+
plain: true,
185+
},
186+
})
187+
const strong = component.find('strong')
188+
const link = component.find('a')
189+
expect(strong.exists()).toBe(true)
190+
expect(link.exists()).toBe(false)
191+
expect(component.text()).toBe('bold and link')
192+
})
193+
})
194+
195+
describe('combined formatting', () => {
196+
it('handles multiple formatting in one string', async () => {
197+
const component = await mountSuspended(MarkdownText, {
198+
props: { text: '**bold** and *italic* and `code`' },
199+
})
200+
expect(component.find('strong').exists()).toBe(true)
201+
expect(component.find('em').exists()).toBe(true)
202+
expect(component.find('code').exists()).toBe(true)
203+
})
204+
})
205+
})

0 commit comments

Comments
 (0)