返回 reveal.js
markdown.test.tsx
根目录 / react / src / components / markdown.test.tsx
1 import { render, screen } from '@testing-library/react';
2 import { afterEach, describe, expect, it, vi } from 'vitest';
3 import { Markdown } from './markdown';
4 import { RevealContext } from '../reveal-context';
5
6 describe('Markdown', () => {
7 afterEach(() => {
8 vi.restoreAllMocks();
9 });
10
11 it('renders markdown as slide HTML and normalizes common indentation', () => {
12 const { container } = render(
13 <Markdown>
14 {`
15 ## Hello
16
17 A paragraph with **markdown**.
18 `}
19 </Markdown>
20 );
21
22 const section = container.querySelector('section');
23 expect(section?.querySelector('h2')).toHaveTextContent('Hello');
24 expect(section?.querySelector('p')).toHaveTextContent('A paragraph with markdown.');
25 });
26
27 it('renders new slide HTML when the markdown source changes', () => {
28 const { container, rerender } = render(<Markdown markdown="## Before" />);
29
30 expect(container.querySelector('h2')).toHaveTextContent('Before');
31
32 rerender(<Markdown markdown={'### After\n\nUpdated paragraph'} />);
33
34 expect(container.querySelector('h2')).not.toBeInTheDocument();
35 expect(container.querySelector('h3')).toHaveTextContent('After');
36 expect(container.querySelector('p')).toHaveTextContent('Updated paragraph');
37 });
38
39 it('splits horizontal and vertical slides using custom separators', () => {
40 const { container } = render(
41 <Markdown separator={'^\\n---\\n$'} verticalSeparator={'^\\n--\\n$'}>
42 {`
43 ## Slide 1.1
44
45 --
46
47 ## Slide 1.2
48
49 ---
50
51 ## Slide 2
52 `}
53 </Markdown>
54 );
55
56 const topLevelSections = Array.from(container.children).filter(
57 (element): element is HTMLElement => element.tagName === 'SECTION'
58 );
59 expect(topLevelSections).toHaveLength(2);
60 expect(topLevelSections[0].querySelectorAll(':scope > section')).toHaveLength(2);
61 expect(topLevelSections[1]).toHaveTextContent('Slide 2');
62 });
63
64 it('creates speaker notes using the configured notes separator', () => {
65 const { container } = render(
66 <Markdown notesSeparator="^Note:">
67 {`
68 ## Slide
69
70 Visible content
71
72 Note:
73 Remember this detail
74 `}
75 </Markdown>
76 );
77
78 expect(container.querySelector('section')).toHaveTextContent('Visible content');
79 expect(container.querySelector('aside.notes')).toHaveTextContent('Remember this detail');
80 });
81
82 it('applies .slide attributes to the generated section', () => {
83 const { container } = render(
84 <Markdown>
85 {`
86 <!-- .slide: id="intro" data-background="#ff0000" -->
87 ## Intro
88 `}
89 </Markdown>
90 );
91
92 const section = container.querySelector('section');
93 expect(section).toHaveAttribute('id', 'intro');
94 expect(section).toHaveAttribute('data-background', '#ff0000');
95 });
96
97 it('applies .element attributes to the preceding markdown element', () => {
98 const { container } = render(
99 <Markdown>
100 {`
101 - Item 1 <!-- .element: class="fragment" data-fragment-index="2" -->
102 - Item 2
103 `}
104 </Markdown>
105 );
106
107 const items = container.querySelectorAll('li');
108 expect(items[0]).toHaveClass('fragment');
109 expect(items[0]).toHaveAttribute('data-fragment-index', '2');
110 expect(items[1]).not.toHaveClass('fragment');
111 });
112
113 it('supports custom slide and element attribute separators', () => {
114 const { container } = render(
115 <Markdown
116 slideAttributesSeparator={'--\\s(.*?)$'}
117 elementAttributesSeparator={'{_\\s*?([^}]+?)}'}
118 >
119 {`
120 <!-- -- data-background="#123456" -->
121 ## Title <!-- {_class="fragment highlight-red"} -->
122 `}
123 </Markdown>
124 );
125
126 expect(container.querySelector('section')).toHaveAttribute('data-background', '#123456');
127 expect(container.querySelector('h2')).toHaveClass('fragment', 'highlight-red');
128 });
129
130 it('matches the core plugin code block line highlight syntax', () => {
131 const { container } = render(
132 <Markdown>
133 {`
134 \`\`\`javascript [456: 3,4,5]
135 console.log('hello');
136 \`\`\`
137 `}
138 </Markdown>
139 );
140
141 const code = container.querySelector('code');
142 expect(code).toHaveClass('javascript');
143 expect(code).toHaveAttribute('data-line-numbers', '3,4,5');
144 expect(code).toHaveAttribute('data-ln-start-from', '456');
145 });
146
147 it('runs the Reveal highlight plugin when the deck becomes available later', () => {
148 const highlightBlock = vi.fn((block: HTMLElement) => {
149 block.setAttribute('data-highlighted', 'yes');
150 });
151 const deck = {
152 getPlugin: vi.fn().mockReturnValue({ highlightBlock }),
153 syncFragments: vi.fn(),
154 } as any;
155
156 const { container, rerender } = render(
157 <RevealContext.Provider value={null}>
158 <Markdown markdown={'```javascript\nconsole.log("hello");\n```'} />
159 </RevealContext.Provider>
160 );
161
162 expect(highlightBlock).not.toHaveBeenCalled();
163
164 rerender(
165 <RevealContext.Provider value={deck}>
166 <Markdown markdown={'```javascript\nconsole.log("hello");\n```'} />
167 </RevealContext.Provider>
168 );
169
170 const section = container.querySelector('section');
171 const code = container.querySelector('code');
172 expect(code).toBeInTheDocument();
173 expect(highlightBlock).toHaveBeenCalledWith(code);
174 expect(code).toHaveAttribute('data-highlighted', 'yes');
175 expect(deck.syncFragments).toHaveBeenCalledWith(section);
176 });
177
178 it('rehighlights markdown code blocks when the same slide rerenders', () => {
179 const highlightBlock = vi.fn((block: HTMLElement) => {
180 block.setAttribute('data-highlighted', 'yes');
181 });
182 const deck = {
183 getPlugin: vi.fn().mockReturnValue({ highlightBlock }),
184 syncFragments: vi.fn(),
185 } as any;
186
187 const { container, rerender } = render(
188 <RevealContext.Provider value={deck}>
189 <Markdown markdown={'```javascript\nconsole.log("hello");\n```'} />
190 </RevealContext.Provider>
191 );
192
193 const code = container.querySelector('code');
194 expect(code).toHaveAttribute('data-highlighted', 'yes');
195 expect(highlightBlock).toHaveBeenCalledTimes(1);
196
197 code?.removeAttribute('data-highlighted');
198
199 rerender(
200 <RevealContext.Provider value={deck}>
201 <Markdown markdown={'```javascript\nconsole.log("hello");\n```'} />
202 </RevealContext.Provider>
203 );
204
205 const rerenderedCode = container.querySelector('code');
206 expect(highlightBlock).toHaveBeenCalledTimes(2);
207 expect(rerenderedCode).toHaveAttribute('data-highlighted', 'yes');
208 });
209
210 it('can load markdown from an external source', async () => {
211 const fetchMock = vi.fn().mockResolvedValue({
212 ok: true,
213 text: vi.fn().mockResolvedValue('## Remote slide'),
214 });
215 vi.stubGlobal('fetch', fetchMock);
216
217 render(<Markdown src="/slides.md" />);
218
219 expect(await screen.findByText('Remote slide')).toBeInTheDocument();
220 expect(fetchMock).toHaveBeenCalledWith('/slides.md', expect.any(Object));
221 });
222 });
223
223 lines Plain Text