Files
RenewCMS/web/src/lib/__tests__/markdown.spec.ts
T

95 lines
3.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { renderMarkdown } from '@/lib/markdown'
import { applyMarkdownAction } from '@/lib/markdownEdit'
describe('renderMarkdown', () => {
it('renders bold text and a bulleted list', () => {
const html = renderMarkdown(
'Two days with **lime putty**.\n\n- Bring work clothes\n- Lunch',
)
expect(html).toContain('<strong>lime putty</strong>')
expect(html.match(/<li>/g)).toHaveLength(2)
})
it('shows raw HTML as text', () => {
const html = renderMarkdown('<script>alert(1)</script>')
expect(html).not.toContain('<script>')
expect(html).toContain('&lt;script&gt;')
})
it.each(['https://example.com', 'http://example.com', 'mailto:a@b.co'])(
'keeps the link %s active',
(url) => {
expect(renderMarkdown(`[x](${url})`)).toContain(`href="${url}"`)
},
)
it.each(['javascript:alert(1)', 'data:text/html,x', 'vbscript:x', '/relative', 'url'])(
'leaves the link %s inert',
(url) => {
expect(renderMarkdown(`[x](${url})`)).not.toContain('<a ')
},
)
it('opens links in a new tab without leaking the opener', () => {
const html = renderMarkdown('[x](https://example.com)')
expect(html).toContain('target="_blank"')
expect(html).toContain('rel="noopener noreferrer"')
})
it('does not load images', () => {
expect(renderMarkdown('![x](https://example.com/a.png)')).not.toContain('<img')
})
})
describe('applyMarkdownAction', () => {
it.each([
['bold', '**lime**'],
['italic', '_lime_'],
['heading', '## lime'],
['link', '[lime](url)'],
['list', '- lime'],
] as const)('%s turns the selection into %s', (action, result) => {
expect(applyMarkdownAction(action, 'lime', 0, 4).value).toBe(result)
})
it('selects the url placeholder of a link', () => {
const edit = applyMarkdownAction('link', 'see lime now', 4, 8)
expect(edit.value.slice(edit.start, edit.end)).toBe('url')
})
it('prefixes every selected line of a list', () => {
expect(applyMarkdownAction('list', 'a\nb', 0, 3).value).toBe('- a\n- b')
})
it('puts the heading mark at the start of the line', () => {
expect(applyMarkdownAction('heading', 'one\ntwo lime', 8, 12).value).toBe(
'one\n## two lime',
)
})
it('moves the selection past the list marker of the first line', () => {
const edit = applyMarkdownAction('list', 'a\nb', 0, 3)
expect([edit.start, edit.end]).toEqual([2, 7])
})
it('prefixes only the current line when the cursor sits right after a line break', () => {
expect(applyMarkdownAction('list', 'one\ntwo', 4, 4).value).toBe('one\n- two')
expect(applyMarkdownAction('heading', 'one\ntwo', 4, 4).value).toBe('one\n## two')
})
it('prefixes the line the cursor ends, not the next one', () => {
expect(applyMarkdownAction('heading', 'one\ntwo', 3, 3).value).toBe('## one\ntwo')
})
it('prefixes the first line when it is empty and the caret sits at the start', () => {
expect(applyMarkdownAction('heading', '\nfoo', 0, 0).value).toBe('## \nfoo')
expect(applyMarkdownAction('list', '\nfoo', 0, 0).value).toBe('- \nfoo')
})
})