@pyreon/styler 0.24.4 → 0.24.6
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/package.json +5 -7
- package/src/ThemeProvider.ts +0 -65
- package/src/__tests__/ThemeProvider.test.ts +0 -67
- package/src/__tests__/benchmark.bench.ts +0 -200
- package/src/__tests__/composition-chain.test.ts +0 -537
- package/src/__tests__/css.test.ts +0 -70
- package/src/__tests__/dev-gate-treeshake.test.ts +0 -85
- package/src/__tests__/forward.test.ts +0 -282
- package/src/__tests__/globalStyle.test.ts +0 -72
- package/src/__tests__/hash.test.ts +0 -70
- package/src/__tests__/hybrid-injection.test.ts +0 -225
- package/src/__tests__/index.ts +0 -14
- package/src/__tests__/inject-rules.browser.test.ts +0 -40
- package/src/__tests__/insertion-effect.test.ts +0 -119
- package/src/__tests__/integration-dom.test.ts +0 -58
- package/src/__tests__/integration.test.ts +0 -179
- package/src/__tests__/keyframes.test.ts +0 -68
- package/src/__tests__/memory-growth.test.ts +0 -220
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/p3-features.test.ts +0 -316
- package/src/__tests__/resolve-cache.test.ts +0 -94
- package/src/__tests__/resolve.test.ts +0 -308
- package/src/__tests__/shared.test.ts +0 -133
- package/src/__tests__/sheet-advanced.test.ts +0 -659
- package/src/__tests__/sheet-split-atrules.test.ts +0 -410
- package/src/__tests__/sheet.test.ts +0 -250
- package/src/__tests__/static-styler-resolve-cost.test.ts +0 -160
- package/src/__tests__/styled-reactive.test.ts +0 -74
- package/src/__tests__/styled-ssr.test.ts +0 -75
- package/src/__tests__/styled.test.ts +0 -511
- package/src/__tests__/styler.browser.test.tsx +0 -194
- package/src/__tests__/theme.test.ts +0 -33
- package/src/__tests__/useCSS.test.ts +0 -172
- package/src/css.ts +0 -13
- package/src/env.d.ts +0 -6
- package/src/forward.ts +0 -308
- package/src/globalStyle.ts +0 -53
- package/src/hash.ts +0 -28
- package/src/index.ts +0 -15
- package/src/keyframes.ts +0 -36
- package/src/manifest.ts +0 -332
- package/src/resolve.ts +0 -225
- package/src/shared.ts +0 -22
- package/src/sheet.ts +0 -635
- package/src/styled.tsx +0 -503
- package/src/tests/manifest-snapshot.test.ts +0 -51
- package/src/useCSS.ts +0 -20
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { hash } from '../hash'
|
|
3
|
-
import { createSheet, StyleSheet } from '../sheet'
|
|
4
|
-
|
|
5
|
-
describe('StyleSheet -- advanced features', () => {
|
|
6
|
-
describe('getClassName (pure hash computation)', () => {
|
|
7
|
-
it('returns className without inserting a rule', () => {
|
|
8
|
-
const s = createSheet()
|
|
9
|
-
const cls = s.getClassName('display: flex;')
|
|
10
|
-
expect(cls).toMatch(/^pyr-[0-9a-z]+$/)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('returns same className as insert for same CSS', () => {
|
|
14
|
-
const s = createSheet()
|
|
15
|
-
const clsPure = s.getClassName('display: flex;')
|
|
16
|
-
const clsInsert = s.insert('display: flex;')
|
|
17
|
-
expect(clsPure).toBe(clsInsert)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('matches hash-based className', () => {
|
|
21
|
-
const s = createSheet()
|
|
22
|
-
const cssText = 'display: flex;'
|
|
23
|
-
expect(s.getClassName(cssText)).toBe(`pyr-${hash(cssText)}`)
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe('bounded cache (eviction) -- DOM mode', () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
30
|
-
el.remove()
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('still deduplicates within cache bounds', () => {
|
|
35
|
-
const s = createSheet({ maxCacheSize: 100 })
|
|
36
|
-
const cls1 = s.insert('color: red;')
|
|
37
|
-
const cls2 = s.insert('color: red;')
|
|
38
|
-
expect(cls1).toBe(cls2)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('cacheSize property reflects current cache size', () => {
|
|
42
|
-
const s = createSheet()
|
|
43
|
-
expect(s.cacheSize).toBe(0)
|
|
44
|
-
|
|
45
|
-
s.insert('color: red;')
|
|
46
|
-
expect(s.cacheSize).toBe(1)
|
|
47
|
-
|
|
48
|
-
s.insert('color: blue;')
|
|
49
|
-
expect(s.cacheSize).toBe(2)
|
|
50
|
-
|
|
51
|
-
// Duplicate doesn't increase size
|
|
52
|
-
s.insert('color: red;')
|
|
53
|
-
expect(s.cacheSize).toBe(2)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('evicts oldest entries when max cache size is exceeded', () => {
|
|
57
|
-
const s = createSheet({ maxCacheSize: 10 })
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < 15; i++) {
|
|
60
|
-
s.insert(`prop-${i}: val-${i};`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
expect(s.cacheSize).toBeLessThanOrEqual(15)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('accepts custom maxCacheSize option', () => {
|
|
67
|
-
const s = createSheet({ maxCacheSize: 5 })
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < 20; i++) {
|
|
70
|
-
s.insert(`prop${i}: val${i};`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
expect(s.cacheSize).toBeLessThanOrEqual(20)
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('dev-mode warnings', () => {
|
|
78
|
-
it('warns on invalid CSS in dev mode', () => {
|
|
79
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
80
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
81
|
-
el.remove()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const s = new StyleSheet()
|
|
85
|
-
s.insert('invalid{{{css')
|
|
86
|
-
|
|
87
|
-
warnSpy.mockRestore()
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
describe('insertGlobal -- multi-rule splitting', () => {
|
|
92
|
-
beforeEach(() => {
|
|
93
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
94
|
-
el.remove()
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('injects multiple top-level rules from a single insertGlobal call', () => {
|
|
99
|
-
const s = createSheet()
|
|
100
|
-
s.insertGlobal('html { font-size: 16px; } body { margin: 0; }')
|
|
101
|
-
// Should not throw -- both rules are injected individually
|
|
102
|
-
expect(s.cacheSize).toBe(1) // single cache entry for the whole CSS text
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('injects nested @media rules correctly', () => {
|
|
106
|
-
const s = createSheet()
|
|
107
|
-
s.insertGlobal('body { margin: 0; } @media (min-width: 768px) { body { font-size: 18px; } }')
|
|
108
|
-
expect(s.cacheSize).toBe(1)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('handles three or more rules', () => {
|
|
112
|
-
const s = createSheet()
|
|
113
|
-
s.insertGlobal(
|
|
114
|
-
'html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } body { margin: 0; font-family: sans-serif; }',
|
|
115
|
-
)
|
|
116
|
-
expect(s.cacheSize).toBe(1)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('deduplicates identical multi-rule CSS', () => {
|
|
120
|
-
const s = createSheet()
|
|
121
|
-
const cssStr = 'html { font-size: 16px; } body { margin: 0; }'
|
|
122
|
-
s.insertGlobal(cssStr)
|
|
123
|
-
s.insertGlobal(cssStr)
|
|
124
|
-
expect(s.cacheSize).toBe(1)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('inserts multiple rules into the CSSOM via splitRules and insertRule', () => {
|
|
128
|
-
const s = createSheet()
|
|
129
|
-
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
130
|
-
|
|
131
|
-
// Verify rules were actually inserted into the CSSOM
|
|
132
|
-
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
133
|
-
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
134
|
-
const ruleTexts = Array.from(cssSheet.cssRules).map((r) => r.cssText)
|
|
135
|
-
expect(ruleTexts.some((r) => r.includes('margin'))).toBe(true)
|
|
136
|
-
expect(ruleTexts.some((r) => r.includes('padding'))).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('deduplicates insertGlobal -- second call with same CSS is a no-op', () => {
|
|
140
|
-
const s = createSheet()
|
|
141
|
-
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
142
|
-
|
|
143
|
-
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
144
|
-
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
145
|
-
const countBefore = cssSheet.cssRules.length
|
|
146
|
-
|
|
147
|
-
// Second call should be deduped via cache
|
|
148
|
-
s.insertGlobal('body{margin:0}div{padding:0}')
|
|
149
|
-
expect(cssSheet.cssRules.length).toBe(countBefore)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('inserts global CSS with @media into CSSOM', () => {
|
|
153
|
-
const s = createSheet()
|
|
154
|
-
s.insertGlobal('body{margin:0}@media (min-width:768px){body{font-size:18px}}')
|
|
155
|
-
|
|
156
|
-
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
157
|
-
const cssSheet = styleEl.sheet as CSSStyleSheet
|
|
158
|
-
const ruleTexts = Array.from(cssSheet.cssRules).map((r) => r.cssText)
|
|
159
|
-
expect(ruleTexts.some((r) => r.includes('margin'))).toBe(true)
|
|
160
|
-
expect(ruleTexts.some((r) => r.includes('media'))).toBe(true)
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('@layer on client-side insert', () => {
|
|
165
|
-
beforeEach(() => {
|
|
166
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
167
|
-
el.remove()
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('wraps inserted rules in @layer on client side', () => {
|
|
172
|
-
const s = createSheet({ layer: 'components' })
|
|
173
|
-
const className = s.insert('color: red;')
|
|
174
|
-
expect(className).toMatch(/^pyr-/)
|
|
175
|
-
// The sheet should have injected a @layer declaration + the wrapped rule
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('injects @layer declaration rule on mount', () => {
|
|
179
|
-
const s = createSheet({ layer: 'myLayer' })
|
|
180
|
-
s.insert('display: flex;')
|
|
181
|
-
// No crash -- @layer declaration was injected at mount time
|
|
182
|
-
expect(s.cacheSize).toBe(1)
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
describe('hydration from existing SSR style tag', () => {
|
|
187
|
-
beforeEach(() => {
|
|
188
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
189
|
-
el.remove()
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('reuses existing <style data-pyreon-styler> tag and hydrates cache', () => {
|
|
194
|
-
// Create a style tag that simulates SSR output
|
|
195
|
-
const el = document.createElement('style')
|
|
196
|
-
el.setAttribute('data-pyreon-styler', '')
|
|
197
|
-
document.head.appendChild(el)
|
|
198
|
-
|
|
199
|
-
// Insert a rule directly so hydrateFromTag can discover it
|
|
200
|
-
const sheetRef = el.sheet
|
|
201
|
-
if (sheetRef) sheetRef.insertRule('.pyr-abc { color: red; }', 0)
|
|
202
|
-
|
|
203
|
-
// Create a new sheet -- it should find and reuse the existing tag
|
|
204
|
-
const s = createSheet()
|
|
205
|
-
// The hydration should have populated the cache with 'pyr-abc'
|
|
206
|
-
expect(s.has('pyr-abc')).toBe(true)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('hydrates @media-wrapped rules from SSR style tag', () => {
|
|
210
|
-
const el = document.createElement('style')
|
|
211
|
-
el.setAttribute('data-pyreon-styler', '')
|
|
212
|
-
document.head.appendChild(el)
|
|
213
|
-
|
|
214
|
-
const sheetRef = el.sheet
|
|
215
|
-
if (sheetRef) {
|
|
216
|
-
sheetRef.insertRule('@media (min-width: 768px) { .pyr-xyz { font-size: 2rem; } }', 0)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const s = createSheet()
|
|
220
|
-
expect(s.has('pyr-xyz')).toBe(true)
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
describe('prepare()', () => {
|
|
225
|
-
it('produces single selector', () => {
|
|
226
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
227
|
-
el.remove()
|
|
228
|
-
})
|
|
229
|
-
const s = createSheet()
|
|
230
|
-
const result = s.prepare('color: red;')
|
|
231
|
-
expect(result.className).toMatch(/^pyr-/)
|
|
232
|
-
// Single selector, no doubling
|
|
233
|
-
expect(result.rules).toContain(`.${result.className}{`)
|
|
234
|
-
expect(result.rules).not.toContain(`.${result.className}.${result.className}`)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('prepare with empty base -- all CSS is @rules', () => {
|
|
238
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
239
|
-
el.remove()
|
|
240
|
-
})
|
|
241
|
-
const s = createSheet()
|
|
242
|
-
// CSS that is entirely an @media rule, so base is empty after splitting
|
|
243
|
-
const result = s.prepare('@media(min-width:0){color:red}')
|
|
244
|
-
expect(result.className).toMatch(/^pyr-/)
|
|
245
|
-
// Should contain the @media rule but NOT a bare selector rule (no base)
|
|
246
|
-
expect(result.rules).toContain('@media')
|
|
247
|
-
expect(result.rules).toContain('color:red')
|
|
248
|
-
// The rules should NOT start with a plain selector -- no base rule emitted
|
|
249
|
-
expect(result.rules).not.toMatch(new RegExp(`^\\.${result.className}\\{`))
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('prepare with only @supports produces no base rule', () => {
|
|
253
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
254
|
-
el.remove()
|
|
255
|
-
})
|
|
256
|
-
const s = createSheet()
|
|
257
|
-
const result = s.prepare(
|
|
258
|
-
'@supports(display:grid){display:grid}@media(min-width:0){color:red}',
|
|
259
|
-
)
|
|
260
|
-
expect(result.className).toMatch(/^pyr-/)
|
|
261
|
-
expect(result.rules).toContain('@supports')
|
|
262
|
-
expect(result.rules).toContain('@media')
|
|
263
|
-
// No base selector rule
|
|
264
|
-
expect(result.rules).not.toMatch(new RegExp(`^\\.${result.className}\\{`))
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
describe('layer on insert()', () => {
|
|
269
|
-
beforeEach(() => {
|
|
270
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
271
|
-
el.remove()
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('inserts rule with layer on client side', () => {
|
|
276
|
-
const s = createSheet()
|
|
277
|
-
const cls = s.insert('color: red;', false, 'rocketstyle')
|
|
278
|
-
expect(cls).toMatch(/^pyr-/)
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('deduplicates layered and non-layered separately via insertCache key', () => {
|
|
282
|
-
const s = createSheet()
|
|
283
|
-
const cls1 = s.insert('color: green;', false)
|
|
284
|
-
const cls2 = s.insert('color: green;', false, 'rocketstyle')
|
|
285
|
-
// Same className (same hash) but both should work without error
|
|
286
|
-
expect(cls1).toBe(cls2)
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
describe('clearAll() with rules', () => {
|
|
291
|
-
beforeEach(() => {
|
|
292
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
293
|
-
el.remove()
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('removes all CSS rules from the DOM sheet', () => {
|
|
298
|
-
const s = createSheet()
|
|
299
|
-
s.insert('color: red;')
|
|
300
|
-
s.insert('color: blue;')
|
|
301
|
-
expect(s.cacheSize).toBe(2)
|
|
302
|
-
|
|
303
|
-
s.clearAll()
|
|
304
|
-
expect(s.cacheSize).toBe(0)
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('allows re-insertion after clearAll', () => {
|
|
308
|
-
const s = createSheet()
|
|
309
|
-
s.insert('color: red;')
|
|
310
|
-
s.clearAll()
|
|
311
|
-
// Re-insert the same CSS -- should work since cache was cleared
|
|
312
|
-
const cls = s.insert('color: red;')
|
|
313
|
-
expect(cls).toMatch(/^pyr-/)
|
|
314
|
-
expect(s.cacheSize).toBe(1)
|
|
315
|
-
})
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
describe('clearCache()', () => {
|
|
319
|
-
beforeEach(() => {
|
|
320
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
321
|
-
el.remove()
|
|
322
|
-
})
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('clears the dedup cache but leaves DOM rules in place', () => {
|
|
326
|
-
const s = createSheet()
|
|
327
|
-
s.insert('color: red;')
|
|
328
|
-
expect(s.cacheSize).toBe(1)
|
|
329
|
-
|
|
330
|
-
s.clearCache()
|
|
331
|
-
expect(s.cacheSize).toBe(0)
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
describe('insertRule failure warning', () => {
|
|
336
|
-
beforeEach(() => {
|
|
337
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
338
|
-
el.remove()
|
|
339
|
-
})
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
it('warns in dev mode when insertRule fails for insert()', () => {
|
|
343
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
344
|
-
const s = createSheet()
|
|
345
|
-
// Insert invalid CSS that will cause insertRule to throw
|
|
346
|
-
s.insert('color: red; @INVALID_RULE {{{')
|
|
347
|
-
warnSpy.mockRestore()
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
it('warns in dev mode when insertGlobal insertRule fails', () => {
|
|
351
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
352
|
-
const s = createSheet()
|
|
353
|
-
// Insert intentionally malformed global CSS
|
|
354
|
-
s.insertGlobal('@INVALID {{{')
|
|
355
|
-
warnSpy.mockRestore()
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
it('warns in dev mode when insertKeyframes insertRule fails', () => {
|
|
359
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
360
|
-
const s = createSheet()
|
|
361
|
-
s.insertKeyframes('bad', '{{{invalid')
|
|
362
|
-
warnSpy.mockRestore()
|
|
363
|
-
})
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
describe('insert() insertRule failure via mock', () => {
|
|
367
|
-
beforeEach(() => {
|
|
368
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
369
|
-
el.remove()
|
|
370
|
-
})
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('warns when CSSStyleSheet.insertRule throws during insert()', () => {
|
|
374
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
375
|
-
const s = createSheet()
|
|
376
|
-
|
|
377
|
-
// Access the internal sheet and make insertRule throw
|
|
378
|
-
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
379
|
-
const realSheet = styleEl.sheet
|
|
380
|
-
if (!realSheet) throw new Error('Expected sheet to exist')
|
|
381
|
-
const origInsertRule = realSheet.insertRule.bind(realSheet)
|
|
382
|
-
realSheet.insertRule = (rule: string, index?: number) => {
|
|
383
|
-
// Let @layer declaration through (if any), fail on component rules
|
|
384
|
-
if (rule.startsWith('.pyr-')) {
|
|
385
|
-
throw new Error('Mock insertRule failure')
|
|
386
|
-
}
|
|
387
|
-
return origInsertRule(rule, index)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
s.insert('color: magenta;')
|
|
391
|
-
|
|
392
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
393
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain('[styler] Failed to insert CSS rule')
|
|
394
|
-
|
|
395
|
-
realSheet.insertRule = origInsertRule
|
|
396
|
-
warnSpy.mockRestore()
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
describe('insertGlobal insertRule failure via mock', () => {
|
|
401
|
-
beforeEach(() => {
|
|
402
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
403
|
-
el.remove()
|
|
404
|
-
})
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
it('warns when CSSStyleSheet.insertRule throws during insertGlobal()', () => {
|
|
408
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
409
|
-
const s = createSheet()
|
|
410
|
-
|
|
411
|
-
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
412
|
-
const realSheet = styleEl.sheet
|
|
413
|
-
if (!realSheet) throw new Error('Expected sheet to exist')
|
|
414
|
-
const origInsertRule = realSheet.insertRule.bind(realSheet)
|
|
415
|
-
realSheet.insertRule = (_rule: string, _index?: number) => {
|
|
416
|
-
throw new Error('Mock insertGlobal failure')
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
s.insertGlobal('body { margin: 0; }')
|
|
420
|
-
|
|
421
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
422
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain('[styler] Failed to insert global CSS rule')
|
|
423
|
-
|
|
424
|
-
realSheet.insertRule = origInsertRule
|
|
425
|
-
warnSpy.mockRestore()
|
|
426
|
-
})
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
describe('getClassName with insertCache populated', () => {
|
|
430
|
-
beforeEach(() => {
|
|
431
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
432
|
-
el.remove()
|
|
433
|
-
})
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
it('returns cached className from insertCache after insert()', () => {
|
|
437
|
-
const s = createSheet()
|
|
438
|
-
const cls1 = s.insert('color: purple;')
|
|
439
|
-
// getClassName should hit the insertCache
|
|
440
|
-
const cls2 = s.getClassName('color: purple;')
|
|
441
|
-
expect(cls1).toBe(cls2)
|
|
442
|
-
})
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
// SSR-specific tests: mock document as undefined to simulate server
|
|
446
|
-
describe('SSR mode (mocked)', () => {
|
|
447
|
-
let originalDocument: typeof document
|
|
448
|
-
|
|
449
|
-
beforeEach(() => {
|
|
450
|
-
originalDocument = globalThis.document
|
|
451
|
-
// Temporarily remove document to simulate SSR
|
|
452
|
-
// @ts-expect-error - intentionally deleting for SSR simulation
|
|
453
|
-
delete globalThis.document
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
afterEach(() => {
|
|
457
|
-
globalThis.document = originalDocument
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('creates independent StyleSheet instances in SSR', () => {
|
|
461
|
-
const sheet1 = createSheet()
|
|
462
|
-
const sheet2 = createSheet()
|
|
463
|
-
|
|
464
|
-
sheet1.insert('color: red;')
|
|
465
|
-
expect(sheet1.getStyles()).toContain('color: red;')
|
|
466
|
-
expect(sheet2.getStyles()).toBe('')
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('each instance has its own SSR buffer', () => {
|
|
470
|
-
const sheet1 = createSheet()
|
|
471
|
-
const sheet2 = createSheet()
|
|
472
|
-
|
|
473
|
-
sheet1.insert('color: red;')
|
|
474
|
-
sheet2.insert('color: blue;')
|
|
475
|
-
|
|
476
|
-
expect(sheet1.getStyles()).toContain('color: red;')
|
|
477
|
-
expect(sheet1.getStyles()).not.toContain('color: blue;')
|
|
478
|
-
expect(sheet2.getStyles()).toContain('color: blue;')
|
|
479
|
-
expect(sheet2.getStyles()).not.toContain('color: red;')
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
it('reset only clears the buffer of that instance', () => {
|
|
483
|
-
const sheet1 = createSheet()
|
|
484
|
-
const sheet2 = createSheet()
|
|
485
|
-
|
|
486
|
-
sheet1.insert('color: red;')
|
|
487
|
-
sheet2.insert('color: blue;')
|
|
488
|
-
|
|
489
|
-
sheet1.reset()
|
|
490
|
-
expect(sheet1.getStyles()).toBe('')
|
|
491
|
-
expect(sheet2.getStyles()).toContain('color: blue;')
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
it('each instance has its own dedup cache', () => {
|
|
495
|
-
const sheet1 = createSheet()
|
|
496
|
-
const sheet2 = createSheet()
|
|
497
|
-
|
|
498
|
-
const cls1 = sheet1.insert('display: flex;')
|
|
499
|
-
const cls2 = sheet2.insert('display: flex;')
|
|
500
|
-
|
|
501
|
-
// Same CSS -> same class name (deterministic hashing)
|
|
502
|
-
expect(cls1).toBe(cls2)
|
|
503
|
-
|
|
504
|
-
// Both inject independently
|
|
505
|
-
expect(sheet1.getStyles()).toContain('display: flex;')
|
|
506
|
-
expect(sheet2.getStyles()).toContain('display: flex;')
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
describe('concurrent SSR simulation', () => {
|
|
510
|
-
it('handles concurrent requests without cross-contamination', () => {
|
|
511
|
-
const request1 = createSheet()
|
|
512
|
-
const request2 = createSheet()
|
|
513
|
-
|
|
514
|
-
request1.insert('color: red;')
|
|
515
|
-
request1.insertGlobal('body { margin: 0; }')
|
|
516
|
-
|
|
517
|
-
request2.insert('color: blue;')
|
|
518
|
-
request2.insertGlobal('body { padding: 0; }')
|
|
519
|
-
|
|
520
|
-
const html1 = request1.getStyleTag()
|
|
521
|
-
const html2 = request2.getStyleTag()
|
|
522
|
-
|
|
523
|
-
expect(html1).toContain('color: red;')
|
|
524
|
-
expect(html1).not.toContain('color: blue;')
|
|
525
|
-
expect(html1).toContain('body { margin: 0; }')
|
|
526
|
-
expect(html1).not.toContain('body { padding: 0; }')
|
|
527
|
-
|
|
528
|
-
expect(html2).toContain('color: blue;')
|
|
529
|
-
expect(html2).not.toContain('color: red;')
|
|
530
|
-
expect(html2).toContain('body { padding: 0; }')
|
|
531
|
-
expect(html2).not.toContain('body { margin: 0; }')
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
it('handles many concurrent requests', () => {
|
|
535
|
-
const requests = Array.from({ length: 50 }, (_, i) => {
|
|
536
|
-
const s = createSheet()
|
|
537
|
-
s.insert(`color: color-${i};`)
|
|
538
|
-
return s
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
for (let i = 0; i < requests.length; i++) {
|
|
542
|
-
const styles = requests[i]?.getStyles()
|
|
543
|
-
expect(styles).toContain(`color: color-${i};`)
|
|
544
|
-
const otherIdx = (i + 1) % requests.length
|
|
545
|
-
expect(styles).not.toContain(`color: color-${otherIdx};`)
|
|
546
|
-
}
|
|
547
|
-
})
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
describe('@layer support', () => {
|
|
551
|
-
it('wraps inserted rules in @layer when configured', () => {
|
|
552
|
-
const s = createSheet({ layer: 'components' })
|
|
553
|
-
|
|
554
|
-
s.insert('color: red;')
|
|
555
|
-
const styles = s.getStyles()
|
|
556
|
-
|
|
557
|
-
expect(styles).toContain('@layer components')
|
|
558
|
-
expect(styles).toContain('color: red;')
|
|
559
|
-
})
|
|
560
|
-
|
|
561
|
-
it('does not wrap rules without layer option', () => {
|
|
562
|
-
const s = createSheet()
|
|
563
|
-
|
|
564
|
-
s.insert('color: red;')
|
|
565
|
-
const styles = s.getStyles()
|
|
566
|
-
|
|
567
|
-
expect(styles).not.toContain('@layer')
|
|
568
|
-
expect(styles).toContain('color: red;')
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
it('does not wrap @keyframes in @layer', () => {
|
|
572
|
-
const s = createSheet({ layer: 'components' })
|
|
573
|
-
|
|
574
|
-
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
575
|
-
const styles = s.getStyles()
|
|
576
|
-
|
|
577
|
-
expect(styles).toContain('@keyframes fadeIn')
|
|
578
|
-
// Keyframes are not wrapped in a layer block (layer declaration before them is fine)
|
|
579
|
-
expect(styles).not.toMatch(/@layer\s+\w+\s*\{[^}]*@keyframes/)
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
it('does not wrap global rules in @layer', () => {
|
|
583
|
-
const s = createSheet({ layer: 'components' })
|
|
584
|
-
|
|
585
|
-
s.insertGlobal('body { margin: 0; }')
|
|
586
|
-
const styles = s.getStyles()
|
|
587
|
-
|
|
588
|
-
expect(styles).toContain('body { margin: 0; }')
|
|
589
|
-
expect(styles).not.toMatch(/@layer components\{body/)
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
it('prepare() wraps rules in @layer when configured', () => {
|
|
593
|
-
const s = createSheet({ layer: 'components' })
|
|
594
|
-
const result = s.prepare('color: red;')
|
|
595
|
-
expect(result.rules).toContain('@layer components')
|
|
596
|
-
expect(result.rules).toContain('color: red;')
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
it('prepare() wraps in @layer with layer option', () => {
|
|
600
|
-
const s = createSheet({ layer: 'lib' })
|
|
601
|
-
const result = s.prepare('color: blue;')
|
|
602
|
-
expect(result.rules).toContain('@layer lib')
|
|
603
|
-
expect(result.rules).toContain(`.${result.className}`)
|
|
604
|
-
})
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
describe('SSR output', () => {
|
|
608
|
-
it('getStyleTag returns complete <style> tag', () => {
|
|
609
|
-
const s = createSheet()
|
|
610
|
-
s.insert('color: red;')
|
|
611
|
-
const tag = s.getStyleTag()
|
|
612
|
-
|
|
613
|
-
expect(tag).toMatch(/^<style data-pyreon-styler="">.*<\/style>$/)
|
|
614
|
-
expect(tag).toContain('color: red;')
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
it('getStyles returns raw CSS', () => {
|
|
618
|
-
const s = createSheet()
|
|
619
|
-
s.insert('color: red;')
|
|
620
|
-
s.insert('color: blue;')
|
|
621
|
-
const styles = s.getStyles()
|
|
622
|
-
|
|
623
|
-
expect(styles).not.toContain('<style')
|
|
624
|
-
expect(styles).toContain('color: red;')
|
|
625
|
-
expect(styles).toContain('color: blue;')
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
it('reset clears SSR buffer and cache', () => {
|
|
629
|
-
const s = createSheet()
|
|
630
|
-
s.insert('color: red;')
|
|
631
|
-
expect(s.getStyles()).toContain('color: red;')
|
|
632
|
-
|
|
633
|
-
s.reset()
|
|
634
|
-
expect(s.getStyles()).toBe('')
|
|
635
|
-
expect(s.cacheSize).toBe(0) // cache also cleared for SSR correctness
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
it('insertKeyframes deduplicates in SSR', () => {
|
|
639
|
-
const s = createSheet()
|
|
640
|
-
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
641
|
-
s.insertKeyframes('fadeIn', 'from { opacity: 0; } to { opacity: 1; }')
|
|
642
|
-
|
|
643
|
-
const styles = s.getStyles()
|
|
644
|
-
const matches = styles.match(/@keyframes fadeIn/g)
|
|
645
|
-
expect(matches).toHaveLength(1)
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
it('insertGlobal deduplicates in SSR', () => {
|
|
649
|
-
const s = createSheet()
|
|
650
|
-
s.insertGlobal('body { margin: 0; }')
|
|
651
|
-
s.insertGlobal('body { margin: 0; }')
|
|
652
|
-
|
|
653
|
-
const styles = s.getStyles()
|
|
654
|
-
const matches = styles.match(/body \{ margin: 0; \}/g)
|
|
655
|
-
expect(matches).toHaveLength(1)
|
|
656
|
-
})
|
|
657
|
-
})
|
|
658
|
-
})
|
|
659
|
-
})
|