@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 +6 -0
- package/dist/shiki.spec.d.ts +1 -0
- package/package.json +1 -1
- package/src/lib/shiki.js +13 -0
- package/src/lib/shiki.spec.js +284 -0
- package/src/lib/stories.spec.js +107 -1
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
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
|
+
})
|
package/src/lib/stories.spec.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import {
|
|
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
|
})
|