@rokkit/stories 1.0.0-next.123 → 1.0.0-next.124

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.
package/dist/shiki.d.ts CHANGED
@@ -12,3 +12,9 @@ export function highlightCode(code: string, options?: {
12
12
  theme: string;
13
13
  }): Promise<string>;
14
14
  export function preloadHighlighter(): Promise<void>;
15
+ /**
16
+ * Reset highlighter state - for testing only
17
+ * @internal
18
+ * @throws {Error} If called outside of test environment
19
+ */
20
+ export function resetForTesting(): void;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/stories",
3
- "version": "1.0.0-next.123",
3
+ "version": "1.0.0-next.124",
4
4
  "description": "Utilities for building tutorials.",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/lib/shiki.js CHANGED
@@ -73,3 +73,16 @@ export async function preloadHighlighter() {
73
73
  console.warn('Failed to preload syntax highlighter:', error.message)
74
74
  }
75
75
  }
76
+
77
+ /**
78
+ * Reset highlighter state - for testing only
79
+ * @internal
80
+ * @throws {Error} If called outside of test environment
81
+ */
82
+ export function resetForTesting() {
83
+ if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test') {
84
+ throw new Error('resetForTesting should only be called in test environment')
85
+ }
86
+ highlighter = null
87
+ isInitializing = false
88
+ }
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ // Mock the shiki module completely
4
+ vi.mock('shiki', () => ({
5
+ createHighlighter: vi.fn()
6
+ }))
7
+
8
+ // Import functions once at the top
9
+ import { highlightCode, preloadHighlighter, resetForTesting } from './shiki.js'
10
+
11
+ describe('shiki.js', () => {
12
+ let mockHighlighter
13
+ let mockCreateHighlighter
14
+
15
+ beforeEach(async () => {
16
+ vi.clearAllMocks()
17
+
18
+ // Create mock highlighter instance
19
+ mockHighlighter = {
20
+ codeToHtml: vi.fn()
21
+ }
22
+
23
+ // Get the mocked createHighlighter
24
+ const { createHighlighter } = await import('shiki')
25
+ mockCreateHighlighter = createHighlighter
26
+ mockCreateHighlighter.mockResolvedValue(mockHighlighter)
27
+
28
+ // Reset module state before each test
29
+ resetForTesting()
30
+ })
31
+
32
+ describe('highlightCode', () => {
33
+ it('should highlight code successfully with default options', async () => {
34
+ const code = 'console.log("hello world")'
35
+ const expectedHtml = '<pre class="shiki"><code>console.log("hello world")</code></pre>'
36
+
37
+ mockHighlighter.codeToHtml.mockReturnValue(expectedHtml)
38
+
39
+ const result = await highlightCode(code)
40
+
41
+ expect(mockCreateHighlighter).toHaveBeenCalledWith({
42
+ themes: ['github-light', 'github-dark'],
43
+ langs: ['svelte', 'javascript', 'typescript', 'css', 'html', 'json', 'bash', 'shell']
44
+ })
45
+ expect(mockHighlighter.codeToHtml).toHaveBeenCalledWith(code, {
46
+ lang: undefined,
47
+ theme: undefined
48
+ })
49
+ expect(result).toBe(expectedHtml)
50
+ })
51
+
52
+ it('should highlight code with specified language and theme', async () => {
53
+ const code = 'const x = 42;'
54
+ const options = { lang: 'javascript', theme: 'github-light' }
55
+ const expectedHtml = '<pre class="shiki"><code>const x = 42;</code></pre>'
56
+
57
+ mockHighlighter.codeToHtml.mockReturnValue(expectedHtml)
58
+
59
+ const result = await highlightCode(code, options)
60
+
61
+ expect(mockHighlighter.codeToHtml).toHaveBeenCalledWith(code, {
62
+ lang: 'javascript',
63
+ theme: 'github-light'
64
+ })
65
+ expect(result).toBe(expectedHtml)
66
+ })
67
+
68
+ it('should remove conflicting background color styles', async () => {
69
+ const code = 'test code'
70
+ const htmlWithStyle =
71
+ '<pre class="shiki" style="background-color: #fff; color: #000;">code</pre>'
72
+ const expectedHtml = '<pre class="shiki">code</pre>'
73
+
74
+ mockHighlighter.codeToHtml.mockReturnValue(htmlWithStyle)
75
+
76
+ const result = await highlightCode(code)
77
+
78
+ expect(result).toBe(expectedHtml)
79
+ })
80
+
81
+ it('should throw error for invalid code input - null', async () => {
82
+ await expect(highlightCode(null)).rejects.toThrow('Invalid code provided for highlighting')
83
+ })
84
+
85
+ it('should throw error for invalid code input - undefined', async () => {
86
+ await expect(highlightCode(undefined)).rejects.toThrow(
87
+ 'Invalid code provided for highlighting'
88
+ )
89
+ })
90
+
91
+ it('should throw error for invalid code input - number', async () => {
92
+ await expect(highlightCode(123)).rejects.toThrow('Invalid code provided for highlighting')
93
+ })
94
+
95
+ it('should throw error for invalid code input - object', async () => {
96
+ await expect(highlightCode({})).rejects.toThrow('Invalid code provided for highlighting')
97
+ })
98
+
99
+ it('should handle empty string code', async () => {
100
+ const code = ''
101
+
102
+ // Empty string should throw error based on the current validation
103
+ await expect(highlightCode(code)).rejects.toThrow('Invalid code provided for highlighting')
104
+ })
105
+
106
+ it('should throw error when highlighter creation fails', async () => {
107
+ const code = 'test code'
108
+ const error = new Error('Failed to create highlighter')
109
+
110
+ mockCreateHighlighter.mockRejectedValue(error)
111
+
112
+ await expect(highlightCode(code)).rejects.toThrow(
113
+ 'Failed to highlight code: Failed to create highlighter'
114
+ )
115
+ })
116
+
117
+ it('should throw error when codeToHtml fails', async () => {
118
+ const code = 'test code'
119
+ const error = new Error('Highlighting failed')
120
+
121
+ mockHighlighter.codeToHtml.mockImplementation(() => {
122
+ throw error
123
+ })
124
+
125
+ await expect(highlightCode(code)).rejects.toThrow(
126
+ 'Failed to highlight code: Highlighting failed'
127
+ )
128
+ })
129
+ })
130
+
131
+ describe('preloadHighlighter', () => {
132
+ it('should preload highlighter successfully', async () => {
133
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
134
+
135
+ await preloadHighlighter()
136
+
137
+ expect(mockCreateHighlighter).toHaveBeenCalledWith({
138
+ themes: ['github-light', 'github-dark'],
139
+ langs: ['svelte', 'javascript', 'typescript', 'css', 'html', 'json', 'bash', 'shell']
140
+ })
141
+ expect(consoleSpy).not.toHaveBeenCalled()
142
+
143
+ consoleSpy.mockRestore()
144
+ })
145
+
146
+ it('should handle preload failure gracefully', async () => {
147
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
148
+ const error = new Error('Preload failed')
149
+
150
+ mockCreateHighlighter.mockRejectedValue(error)
151
+
152
+ await preloadHighlighter()
153
+
154
+ expect(consoleSpy).toHaveBeenCalledWith(
155
+ 'Failed to preload syntax highlighter:',
156
+ 'Preload failed'
157
+ )
158
+
159
+ consoleSpy.mockRestore()
160
+ })
161
+
162
+ it('should not throw error even if initialization fails', async () => {
163
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
164
+ mockCreateHighlighter.mockRejectedValue(new Error('Init failed'))
165
+
166
+ // Should not throw
167
+ await expect(preloadHighlighter()).resolves.toBeUndefined()
168
+
169
+ consoleSpy.mockRestore()
170
+ })
171
+ })
172
+
173
+ describe('code validation', () => {
174
+ it('should accept valid string code', async () => {
175
+ const code = 'console.log("test")'
176
+ const expectedHtml = '<pre class="shiki"><code>test</code></pre>'
177
+
178
+ mockHighlighter.codeToHtml.mockReturnValue(expectedHtml)
179
+
180
+ const result = await highlightCode(code)
181
+ expect(result).toBe(expectedHtml)
182
+ })
183
+
184
+ it('should reject falsy values except empty string', async () => {
185
+ await expect(highlightCode(false)).rejects.toThrow('Invalid code provided for highlighting')
186
+ await expect(highlightCode(0)).rejects.toThrow('Invalid code provided for highlighting')
187
+ await expect(highlightCode('')).rejects.toThrow('Invalid code provided for highlighting')
188
+ })
189
+ })
190
+
191
+ describe('error handling', () => {
192
+ it('should wrap all errors with descriptive message', async () => {
193
+ const code = 'test'
194
+
195
+ mockHighlighter.codeToHtml.mockImplementation(() => {
196
+ throw new Error('Random error')
197
+ })
198
+
199
+ await expect(highlightCode(code)).rejects.toThrow('Failed to highlight code: Random error')
200
+ })
201
+ })
202
+
203
+ describe('style removal', () => {
204
+ it('should remove various style patterns', async () => {
205
+ const code = 'test'
206
+
207
+ const testCases = [
208
+ {
209
+ input: '<pre class="shiki" style="background: red;">code</pre>',
210
+ expected: '<pre class="shiki">code</pre>'
211
+ },
212
+ {
213
+ input: '<pre id="test" style="color: blue; background: white;">code</pre>',
214
+ expected: '<pre id="test">code</pre>'
215
+ },
216
+ {
217
+ input: '<pre>code</pre>', // No style attribute
218
+ expected: '<pre>code</pre>'
219
+ }
220
+ ]
221
+
222
+ for (const testCase of testCases) {
223
+ mockHighlighter.codeToHtml.mockReturnValue(testCase.input)
224
+ const result = await highlightCode(code)
225
+ expect(result).toBe(testCase.expected)
226
+ }
227
+ })
228
+ })
229
+
230
+ describe('concurrent initialization', () => {
231
+ it('should handle concurrent calls to highlightCode', async () => {
232
+ // This test takes ~50ms due to the setTimeout delay in the waiting loop
233
+ const code1 = 'console.log("test1")'
234
+ const code2 = 'console.log("test2")'
235
+ const expectedHtml1 = '<pre class="shiki"><code>test1</code></pre>'
236
+ const expectedHtml2 = '<pre class="shiki"><code>test2</code></pre>'
237
+
238
+ // Make createHighlighter slow to ensure concurrent calls hit the waiting logic
239
+ let resolveHighlighter
240
+ const highlighterPromise = new Promise((resolve) => {
241
+ resolveHighlighter = resolve
242
+ })
243
+ mockCreateHighlighter.mockReturnValue(highlighterPromise)
244
+
245
+ // Set up the mock to return different values based on input
246
+ mockHighlighter.codeToHtml.mockImplementation((code) => {
247
+ if (code === code1) return expectedHtml1
248
+ if (code === code2) return expectedHtml2
249
+ return '<pre class="shiki"><code>default</code></pre>'
250
+ })
251
+
252
+ // Start concurrent calls - they will wait for highlighter creation
253
+ const promise1 = highlightCode(code1)
254
+ const promise2 = highlightCode(code2)
255
+
256
+ // Resolve the highlighter after a small delay to ensure both calls are waiting
257
+ setTimeout(() => resolveHighlighter(mockHighlighter), 10)
258
+
259
+ const [result1, result2] = await Promise.all([promise1, promise2])
260
+
261
+ expect(result1).toBe(expectedHtml1)
262
+ expect(result2).toBe(expectedHtml2)
263
+ // Should only create highlighter once despite concurrent calls
264
+ expect(mockCreateHighlighter).toHaveBeenCalledTimes(1)
265
+ })
266
+ })
267
+
268
+ describe('resetForTesting environment check', () => {
269
+ it('should throw error when called outside test environment', () => {
270
+ // Mock process.env to simulate non-test environment
271
+ const originalEnv = process.env.NODE_ENV
272
+ process.env.NODE_ENV = 'production'
273
+
274
+ try {
275
+ expect(() => resetForTesting()).toThrow(
276
+ 'resetForTesting should only be called in test environment'
277
+ )
278
+ } finally {
279
+ // Restore original environment
280
+ process.env.NODE_ENV = originalEnv
281
+ }
282
+ })
283
+ })
284
+ })
@@ -1,5 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { getSections, getSlug, fetchImports, fetchStories, groupFiles } from './stories.js'
2
+ import {
3
+ getSections,
4
+ getSlug,
5
+ fetchImports,
6
+ fetchStories,
7
+ groupFiles,
8
+ getAllSections,
9
+ findSection,
10
+ findGroupForSection
11
+ } from './stories.js'
3
12
 
4
13
  describe('stories.js', () => {
5
14
  beforeEach(() => {
@@ -295,4 +304,101 @@ describe('stories.js', () => {
295
304
  expect(result).toEqual({})
296
305
  })
297
306
  })
307
+
308
+ describe('getAllSections', () => {
309
+ it('should throw error when sections variable is undefined', () => {
310
+ // getAllSections references a global 'sections' variable that doesn't exist
311
+ // This function would throw a ReferenceError in practice
312
+ expect(() => getAllSections()).toThrow()
313
+ })
314
+ })
315
+
316
+ describe('findSection', () => {
317
+ const mockSections = [
318
+ {
319
+ title: 'Welcome',
320
+ children: [
321
+ { title: 'Introduction', slug: '/welcome/introduction' },
322
+ { title: 'Getting Started', slug: '/welcome/get' }
323
+ ]
324
+ },
325
+ {
326
+ title: 'Elements',
327
+ children: [
328
+ { title: 'List', slug: '/elements/list' },
329
+ { title: 'Button', slug: '/elements/button' }
330
+ ]
331
+ }
332
+ ]
333
+
334
+ it('should find a section by slug', () => {
335
+ const result = findSection(mockSections, '/elements/list')
336
+ expect(result).toEqual({ title: 'List', slug: '/elements/list' })
337
+ })
338
+
339
+ it('should return empty object when section is not found', () => {
340
+ const result = findSection(mockSections, '/nonexistent/section')
341
+ expect(result).toEqual({})
342
+ })
343
+
344
+ it('should find section from first group', () => {
345
+ const result = findSection(mockSections, '/welcome/introduction')
346
+ expect(result).toEqual({ title: 'Introduction', slug: '/welcome/introduction' })
347
+ })
348
+
349
+ it('should find section from second group', () => {
350
+ const result = findSection(mockSections, '/elements/button')
351
+ expect(result).toEqual({ title: 'Button', slug: '/elements/button' })
352
+ })
353
+ })
354
+
355
+ describe('findGroupForSection', () => {
356
+ const mockSections = [
357
+ {
358
+ title: 'Welcome',
359
+ id: 'welcome',
360
+ children: [
361
+ { title: 'Introduction', id: 'intro' },
362
+ { title: 'Getting Started', id: 'get-started' }
363
+ ]
364
+ },
365
+ {
366
+ title: 'Elements',
367
+ id: 'elements',
368
+ children: [
369
+ { title: 'List', id: 'list' },
370
+ { title: 'Button', id: 'button' }
371
+ ]
372
+ }
373
+ ]
374
+
375
+ it('should find the group containing a section', () => {
376
+ const result = findGroupForSection(mockSections, 'list')
377
+ expect(result).toEqual({
378
+ title: 'Elements',
379
+ id: 'elements',
380
+ children: [
381
+ { title: 'List', id: 'list' },
382
+ { title: 'Button', id: 'button' }
383
+ ]
384
+ })
385
+ })
386
+
387
+ it('should return null when section is not found', () => {
388
+ const result = findGroupForSection(mockSections, 'nonexistent')
389
+ expect(result).toBeNull()
390
+ })
391
+
392
+ it('should find group for section in first group', () => {
393
+ const result = findGroupForSection(mockSections, 'intro')
394
+ expect(result).toEqual({
395
+ title: 'Welcome',
396
+ id: 'welcome',
397
+ children: [
398
+ { title: 'Introduction', id: 'intro' },
399
+ { title: 'Getting Started', id: 'get-started' }
400
+ ]
401
+ })
402
+ })
403
+ })
298
404
  })