@slidejs/runner-revealjs 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * @slidejs/runner-revealjs - RevealJsAdapter 单元测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { RevealJsAdapter } from '../adapter';
7
+ import type { SlideDefinition } from '@slidejs/core';
8
+ import type { RevealJsOptions } from '../types';
9
+
10
+ // Mock reveal.js
11
+ const mockReveal = {
12
+ initialize: vi.fn().mockResolvedValue(undefined),
13
+ sync: vi.fn(),
14
+ destroy: vi.fn(),
15
+ slide: vi.fn(),
16
+ getIndices: vi.fn().mockReturnValue({ h: 0, v: 0 }),
17
+ getTotalSlides: vi.fn().mockReturnValue(1),
18
+ on: vi.fn(),
19
+ };
20
+
21
+ vi.mock('reveal.js', () => {
22
+ return {
23
+ default: vi.fn().mockImplementation(() => mockReveal),
24
+ };
25
+ });
26
+
27
+ // 创建测试用的幻灯片定义
28
+ function createTestSlide(id: string, content: string[]): SlideDefinition {
29
+ return {
30
+ id,
31
+ content: {
32
+ type: 'text',
33
+ lines: content,
34
+ },
35
+ };
36
+ }
37
+
38
+ describe('RevealJsAdapter', () => {
39
+ let adapter: RevealJsAdapter;
40
+ let container: HTMLElement;
41
+
42
+ beforeEach(() => {
43
+ adapter = new RevealJsAdapter();
44
+ container = document.createElement('div');
45
+ document.body.appendChild(container);
46
+
47
+ // 重置 mock
48
+ vi.clearAllMocks();
49
+ });
50
+
51
+ afterEach(async () => {
52
+ if (adapter) {
53
+ await adapter.destroy();
54
+ }
55
+ if (container.parentNode) {
56
+ container.parentNode.removeChild(container);
57
+ }
58
+ });
59
+
60
+ describe('构造函数', () => {
61
+ it('应该创建 RevealJsAdapter 实例', () => {
62
+ expect(adapter).toBeInstanceOf(RevealJsAdapter);
63
+ expect(adapter.name).toBe('revealjs');
64
+ });
65
+ });
66
+
67
+ describe('initialize()', () => {
68
+ it('应该成功初始化适配器', async () => {
69
+ await adapter.initialize(container);
70
+
71
+ expect(mockReveal.initialize).toHaveBeenCalled();
72
+ });
73
+
74
+ it('应该创建 reveal.js DOM 结构', async () => {
75
+ await adapter.initialize(container);
76
+
77
+ const viewport = container.querySelector('.reveal-viewport');
78
+ const reveal = container.querySelector('.reveal');
79
+ const slides = container.querySelector('.slides');
80
+
81
+ expect(viewport).toBeTruthy();
82
+ expect(reveal).toBeTruthy();
83
+ expect(slides).toBeTruthy();
84
+ });
85
+
86
+ it('应该使用默认配置', async () => {
87
+ await adapter.initialize(container);
88
+
89
+ const Reveal = (await import('reveal.js')).default;
90
+ expect(Reveal).toHaveBeenCalledWith(
91
+ expect.any(HTMLElement),
92
+ expect.objectContaining({
93
+ controls: true,
94
+ progress: true,
95
+ center: true,
96
+ hash: false,
97
+ transition: 'slide',
98
+ })
99
+ );
100
+ });
101
+
102
+ it('应该接受自定义配置', async () => {
103
+ const options: RevealJsOptions = {
104
+ revealConfig: {
105
+ controls: false,
106
+ progress: false,
107
+ transition: 'fade',
108
+ },
109
+ };
110
+
111
+ await adapter.initialize(container, options);
112
+
113
+ const Reveal = (await import('reveal.js')).default;
114
+ expect(Reveal).toHaveBeenCalledWith(
115
+ expect.any(HTMLElement),
116
+ expect.objectContaining({
117
+ controls: false,
118
+ progress: false,
119
+ transition: 'fade',
120
+ })
121
+ );
122
+ });
123
+
124
+ it('应该在初始化失败时抛出错误', async () => {
125
+ mockReveal.initialize.mockRejectedValueOnce(new Error('Init failed'));
126
+
127
+ await expect(adapter.initialize(container)).rejects.toThrow(
128
+ 'Failed to initialize RevealJsAdapter'
129
+ );
130
+ });
131
+ });
132
+
133
+ describe('render()', () => {
134
+ it('应该在未初始化时抛出错误', async () => {
135
+ const slides = [createTestSlide('slide-1', ['Test'])];
136
+
137
+ await expect(adapter.render(slides)).rejects.toThrow('RevealJsAdapter not initialized');
138
+ });
139
+
140
+ it('应该成功渲染幻灯片', async () => {
141
+ await adapter.initialize(container);
142
+
143
+ const slides = [
144
+ createTestSlide('slide-1', ['# Slide 1']),
145
+ createTestSlide('slide-2', ['# Slide 2']),
146
+ ];
147
+
148
+ await adapter.render(slides);
149
+
150
+ const sections = container.querySelectorAll('.slides section');
151
+ expect(sections.length).toBe(2);
152
+ expect(mockReveal.sync).toHaveBeenCalled();
153
+ });
154
+
155
+ it('应该清空现有幻灯片后渲染新幻灯片', async () => {
156
+ await adapter.initialize(container);
157
+
158
+ const slides1 = [createTestSlide('slide-1', ['# Slide 1'])];
159
+ await adapter.render(slides1);
160
+
161
+ const slides2 = [createTestSlide('slide-2', ['# Slide 2'])];
162
+ await adapter.render(slides2);
163
+
164
+ const sections = container.querySelectorAll('.slides section');
165
+ expect(sections.length).toBe(1);
166
+ });
167
+ });
168
+
169
+ describe('navigateTo()', () => {
170
+ it('应该在未初始化时抛出错误', () => {
171
+ expect(() => adapter.navigateTo(0)).toThrow('RevealJsAdapter not initialized');
172
+ });
173
+
174
+ it('应该成功导航到指定幻灯片', async () => {
175
+ await adapter.initialize(container);
176
+ await adapter.render([createTestSlide('slide-1', ['Test'])]);
177
+
178
+ adapter.navigateTo(0);
179
+
180
+ expect(mockReveal.slide).toHaveBeenCalledWith(0, 0);
181
+ });
182
+ });
183
+
184
+ describe('getCurrentIndex()', () => {
185
+ it('应该在未初始化时返回 0', () => {
186
+ expect(adapter.getCurrentIndex()).toBe(0);
187
+ });
188
+
189
+ it('应该返回当前幻灯片索引', async () => {
190
+ await adapter.initialize(container);
191
+ await adapter.render([createTestSlide('slide-1', ['Test'])]);
192
+
193
+ mockReveal.getIndices.mockReturnValue({ h: 2, v: 0 });
194
+ expect(adapter.getCurrentIndex()).toBe(2);
195
+ });
196
+ });
197
+
198
+ describe('getTotalSlides()', () => {
199
+ it('应该在未初始化时返回 0', () => {
200
+ expect(adapter.getTotalSlides()).toBe(0);
201
+ });
202
+
203
+ it('应该返回幻灯片总数', async () => {
204
+ await adapter.initialize(container);
205
+ await adapter.render([
206
+ createTestSlide('slide-1', ['Test']),
207
+ createTestSlide('slide-2', ['Test']),
208
+ ]);
209
+
210
+ mockReveal.getTotalSlides.mockReturnValue(2);
211
+ expect(adapter.getTotalSlides()).toBe(2);
212
+ });
213
+ });
214
+
215
+ describe('destroy()', () => {
216
+ it('应该成功销毁适配器', async () => {
217
+ await adapter.initialize(container);
218
+ await adapter.destroy();
219
+
220
+ expect(mockReveal.destroy).toHaveBeenCalled();
221
+ });
222
+
223
+ it('应该清理 DOM 结构', async () => {
224
+ await adapter.initialize(container);
225
+
226
+ // 验证 DOM 结构存在
227
+ const reveal = container.querySelector('.reveal');
228
+ expect(reveal).toBeTruthy();
229
+
230
+ await adapter.destroy();
231
+
232
+ // 验证 reveal 实例被销毁
233
+ expect(mockReveal.destroy).toHaveBeenCalled();
234
+ });
235
+ });
236
+
237
+ describe('事件处理', () => {
238
+ it('应该注册事件监听器', async () => {
239
+ await adapter.initialize(container);
240
+
241
+ const handler = vi.fn();
242
+ adapter.on('ready', handler);
243
+
244
+ // 触发 ready 事件(通过 emit)
245
+ // 注意:这里我们测试的是适配器内部的事件系统
246
+ expect(handler).toBeDefined();
247
+ });
248
+
249
+ it('应该移除事件监听器', async () => {
250
+ await adapter.initialize(container);
251
+
252
+ const handler = vi.fn();
253
+ adapter.on('ready', handler);
254
+ adapter.off('ready', handler);
255
+
256
+ // 验证监听器已移除
257
+ expect(handler).toBeDefined();
258
+ });
259
+ });
260
+
261
+ describe('renderSlide()', () => {
262
+ it('应该渲染文本内容幻灯片', async () => {
263
+ await adapter.initialize(container);
264
+
265
+ const slide = createTestSlide('slide-1', ['# Title', '## Subtitle', 'Content']);
266
+ await adapter.render([slide]);
267
+
268
+ const section = container.querySelector('.slides section');
269
+ expect(section).toBeTruthy();
270
+
271
+ const slideContent = section?.querySelector('.slide-content');
272
+ expect(slideContent).toBeTruthy();
273
+ });
274
+
275
+ it('应该渲染动态内容幻灯片', async () => {
276
+ await adapter.initialize(container);
277
+
278
+ const slide: SlideDefinition = {
279
+ id: 'slide-1',
280
+ content: {
281
+ type: 'dynamic',
282
+ component: 'my-component',
283
+ props: {
284
+ title: 'Test',
285
+ },
286
+ },
287
+ };
288
+
289
+ await adapter.render([slide]);
290
+
291
+ const section = container.querySelector('.slides section');
292
+ expect(section).toBeTruthy();
293
+
294
+ const component = section?.querySelector('my-component');
295
+ expect(component).toBeTruthy();
296
+ expect(component?.getAttribute('title')).toBe('Test');
297
+ });
298
+ });
299
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @slidejs/runner-revealjs - createSlideRunner 单元测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { createSlideRunner } from '../runner';
7
+ import type { SlideContext } from '@slidejs/context';
8
+
9
+ // Mock reveal.js
10
+ vi.mock('reveal.js', () => {
11
+ return {
12
+ default: vi.fn().mockImplementation(() => {
13
+ return {
14
+ initialize: vi.fn().mockResolvedValue(undefined),
15
+ sync: vi.fn(),
16
+ destroy: vi.fn(),
17
+ slide: vi.fn(),
18
+ getIndices: vi.fn().mockReturnValue({ h: 0, v: 0 }),
19
+ getTotalSlides: vi.fn().mockReturnValue(1),
20
+ on: vi.fn(),
21
+ };
22
+ }),
23
+ };
24
+ });
25
+
26
+ // Mock CSS imports
27
+ vi.mock('reveal.js/dist/reveal.css?inline', () => ({
28
+ default: '/* reveal.js CSS */',
29
+ }));
30
+
31
+ vi.mock('../style.css?inline', () => ({
32
+ default: '/* custom CSS */',
33
+ }));
34
+
35
+ // 创建测试用的 SlideContext
36
+ function createTestContext(): SlideContext {
37
+ return {
38
+ sourceType: 'quiz',
39
+ sourceId: 'test-id',
40
+ metadata: {
41
+ title: 'Test Presentation',
42
+ },
43
+ items: [],
44
+ };
45
+ }
46
+
47
+ // 创建测试用的 DSL 源代码
48
+ function createTestDSLSource(): string {
49
+ return `
50
+ present quiz "test-id" {
51
+ rules {
52
+ rule start "intro" {
53
+ slide {
54
+ content text {
55
+ "# Welcome"
56
+ "## Test Slide"
57
+ }
58
+ }
59
+ }
60
+ rule end "end" {
61
+ slide {
62
+ content text {
63
+ "# End"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ `;
70
+ }
71
+
72
+ describe('createSlideRunner', () => {
73
+ let container: HTMLElement;
74
+
75
+ beforeEach(() => {
76
+ // 创建 DOM 容器
77
+ container = document.createElement('div');
78
+ container.id = 'test-container';
79
+ document.body.appendChild(container);
80
+
81
+ // 清空 document.head 中的样式
82
+ const existingStyles = document.head.querySelectorAll('style');
83
+ existingStyles.forEach(style => style.remove());
84
+ });
85
+
86
+ afterEach(() => {
87
+ // 清理 DOM
88
+ if (container.parentNode) {
89
+ container.parentNode.removeChild(container);
90
+ }
91
+ const existingStyles = document.head.querySelectorAll('style');
92
+ existingStyles.forEach(style => style.remove());
93
+ });
94
+
95
+ describe('基本功能', () => {
96
+ it('应该成功创建 SlideRunner 实例', async () => {
97
+ const dslSource = createTestDSLSource();
98
+ const context = createTestContext();
99
+
100
+ const runner = await createSlideRunner(dslSource, context, {
101
+ container: '#test-container',
102
+ });
103
+
104
+ expect(runner).toBeDefined();
105
+ expect(runner.getTotalSlides()).toBeGreaterThan(0);
106
+ });
107
+
108
+ it('应该接受 HTMLElement 作为容器', async () => {
109
+ const dslSource = createTestDSLSource();
110
+ const context = createTestContext();
111
+
112
+ const runner = await createSlideRunner(dslSource, context, {
113
+ container,
114
+ });
115
+
116
+ expect(runner).toBeDefined();
117
+ });
118
+
119
+ it('应该在容器不存在时抛出错误', async () => {
120
+ const dslSource = createTestDSLSource();
121
+ const context = createTestContext();
122
+
123
+ await expect(
124
+ createSlideRunner(dslSource, context, {
125
+ container: '#non-existent',
126
+ })
127
+ ).rejects.toThrow('Container not found');
128
+ });
129
+ });
130
+
131
+ describe('CSS 注入', () => {
132
+ it('应该注入 Reveal.js CSS 到 document.head', async () => {
133
+ const dslSource = createTestDSLSource();
134
+ const context = createTestContext();
135
+
136
+ await createSlideRunner(dslSource, context, {
137
+ container: '#test-container',
138
+ });
139
+
140
+ const globalStyle = document.head.querySelector('#reveal-styles');
141
+ expect(globalStyle).toBeTruthy();
142
+ expect(globalStyle?.textContent).toContain('reveal.js CSS');
143
+ });
144
+
145
+ it('应该只注入一次 Reveal.js CSS(避免重复)', async () => {
146
+ const dslSource = createTestDSLSource();
147
+ const context = createTestContext();
148
+
149
+ // 创建第一个 runner
150
+ await createSlideRunner(dslSource, context, {
151
+ container: '#test-container',
152
+ });
153
+
154
+ // 创建第二个容器和 runner
155
+ const container2 = document.createElement('div');
156
+ container2.id = 'test-container-2';
157
+ document.body.appendChild(container2);
158
+
159
+ await createSlideRunner(dslSource, context, {
160
+ container: '#test-container-2',
161
+ });
162
+
163
+ // 应该只有一个全局样式
164
+ const globalStyles = document.head.querySelectorAll('#reveal-styles');
165
+ expect(globalStyles.length).toBe(1);
166
+
167
+ container2.remove();
168
+ });
169
+
170
+ it('应该注入自定义 CSS 到容器', async () => {
171
+ const dslSource = createTestDSLSource();
172
+ const context = createTestContext();
173
+
174
+ await createSlideRunner(dslSource, context, {
175
+ container: '#test-container',
176
+ });
177
+
178
+ const customStyle = container.querySelector('#slidejs-runner-revealjs-styles');
179
+ expect(customStyle).toBeTruthy();
180
+ expect(customStyle?.textContent).toContain('custom CSS');
181
+ });
182
+
183
+ it('应该为每个容器创建独立的 revealContainer', async () => {
184
+ const dslSource = createTestDSLSource();
185
+ const context = createTestContext();
186
+
187
+ await createSlideRunner(dslSource, context, {
188
+ container: '#test-container',
189
+ });
190
+
191
+ // 应该有一个 revealContainer div
192
+ const revealContainers = container.querySelectorAll('div');
193
+ expect(revealContainers.length).toBeGreaterThan(0);
194
+
195
+ // revealContainer 应该有正确的样式
196
+ const revealContainer = Array.from(revealContainers).find(
197
+ div => div.style.width === '100%' && div.style.height === '100%'
198
+ );
199
+ expect(revealContainer).toBeTruthy();
200
+ });
201
+ });
202
+
203
+ describe('reveal.js 配置', () => {
204
+ it('应该传递 reveal.js 配置选项', async () => {
205
+ const dslSource = createTestDSLSource();
206
+ const context = createTestContext();
207
+
208
+ const revealOptions = {
209
+ controls: false,
210
+ progress: false,
211
+ center: false,
212
+ transition: 'fade' as const,
213
+ };
214
+
215
+ await createSlideRunner(dslSource, context, {
216
+ container: '#test-container',
217
+ revealOptions,
218
+ });
219
+
220
+ // 验证 Reveal 构造函数被调用(通过 mock)
221
+ const Reveal = (await import('reveal.js')).default;
222
+ expect(Reveal).toHaveBeenCalled();
223
+ });
224
+ });
225
+
226
+ describe('错误处理', () => {
227
+ it('应该在 DSL 解析失败时抛出错误', async () => {
228
+ const invalidDSL = 'invalid dsl syntax';
229
+ const context = createTestContext();
230
+
231
+ await expect(
232
+ createSlideRunner(invalidDSL, context, {
233
+ container: '#test-container',
234
+ })
235
+ ).rejects.toThrow();
236
+ });
237
+ });
238
+ });
package/src/env.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ /**
4
+ * 支持 ?inline 导入 CSS 文件作为字符串
5
+ */
6
+ declare module '*.css?inline' {
7
+ const content: string;
8
+ export default content;
9
+ }
10
+
11
+ /**
12
+ * 支持 ?raw 导入 CSS 文件作为字符串(备用)
13
+ */
14
+ declare module '*.css?raw' {
15
+ const content: string;
16
+ export default content;
17
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * 导出 reveal.js 适配器和 SlideRunner 工厂函数
5
5
  *
6
6
  * 样式会自动加载(包含 reveal.js 核心 CSS)。
7
+ * CSS 通过 runner.ts 动态注入,无需手动导入。
7
8
  * 主题样式需要单独导入(可选):
8
9
  * ```typescript
9
10
  * import 'reveal.js/dist/theme/black.css';
@@ -11,9 +12,6 @@
11
12
  * ```
12
13
  */
13
14
 
14
- // 导入核心样式(自动加载)
15
- import './style.css';
16
-
17
15
  // 适配器(低级 API)
18
16
  export { RevealJsAdapter } from './adapter';
19
17
  export type { RevealJsOptions } from './types';
package/src/runner.ts CHANGED
@@ -9,6 +9,9 @@ import { SlideRunner } from '@slidejs/runner';
9
9
  import type { SlideContext } from '@slidejs/context';
10
10
  import { RevealJsAdapter } from './adapter';
11
11
  import type { RevealJsOptions } from './types';
12
+ // 导入 CSS 内容用于注入
13
+ import revealCSS from 'reveal.js/dist/reveal.css?inline';
14
+ import customCSS from './style.css?inline';
12
15
 
13
16
  /**
14
17
  * SlideRunner 配置选项
@@ -65,10 +68,48 @@ export async function createSlideRunner<TContext extends SlideContext = SlideCon
65
68
  // 2. 编译为 SlideDSL
66
69
  const slideDSL = compile<TContext>(ast);
67
70
 
68
- // 3. 创建适配器和 Runner
71
+ // 2.1 注入 Reveal.js CSS 到 document.head(全局,如果尚未注入)
72
+ const globalStyleId = 'reveal-styles';
73
+ const globalStyles = document.head.querySelector(`#${globalStyleId}`);
74
+ if (!globalStyles) {
75
+ const style = document.createElement('style');
76
+ style.id = globalStyleId;
77
+ style.textContent = revealCSS;
78
+ document.head.appendChild(style);
79
+ }
80
+
81
+ // 2.2 获取用户提供的容器元素
82
+ let userContainer: HTMLElement;
83
+ if (typeof config.container === 'string') {
84
+ const element = document.querySelector(config.container);
85
+ if (!element) {
86
+ throw new Error(`Container not found: ${config.container}`);
87
+ }
88
+ userContainer = element as HTMLElement;
89
+ } else {
90
+ userContainer = config.container;
91
+ }
92
+
93
+ // 2.3 注入自定义 CSS 样式到容器
94
+ const styleId = 'slidejs-runner-revealjs-styles';
95
+ if (!userContainer.querySelector(`#${styleId}`)) {
96
+ const style = document.createElement('style');
97
+ style.id = styleId;
98
+ style.textContent = customCSS;
99
+ userContainer.appendChild(style);
100
+ }
101
+
102
+ // 2.4 创建一个新的 div 节点用于 Reveal.js(Reveal.js 会接管这个 div)
103
+ const revealContainer = document.createElement('div');
104
+ // 确保容器占满父元素的高度和宽度
105
+ revealContainer.style.width = '100%';
106
+ revealContainer.style.height = '100%';
107
+ userContainer.appendChild(revealContainer);
108
+
109
+ // 3. 创建适配器和 Runner(将 revealContainer 传给 Runner,而不是 userContainer)
69
110
  const adapter = new RevealJsAdapter();
70
111
  const runner = new SlideRunner<TContext>({
71
- container: config.container,
112
+ container: revealContainer,
72
113
  adapter,
73
114
  adapterOptions: {
74
115
  revealConfig: config.revealOptions,