@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.
Files changed (47) hide show
  1. package/package.json +5 -7
  2. package/src/ThemeProvider.ts +0 -65
  3. package/src/__tests__/ThemeProvider.test.ts +0 -67
  4. package/src/__tests__/benchmark.bench.ts +0 -200
  5. package/src/__tests__/composition-chain.test.ts +0 -537
  6. package/src/__tests__/css.test.ts +0 -70
  7. package/src/__tests__/dev-gate-treeshake.test.ts +0 -85
  8. package/src/__tests__/forward.test.ts +0 -282
  9. package/src/__tests__/globalStyle.test.ts +0 -72
  10. package/src/__tests__/hash.test.ts +0 -70
  11. package/src/__tests__/hybrid-injection.test.ts +0 -225
  12. package/src/__tests__/index.ts +0 -14
  13. package/src/__tests__/inject-rules.browser.test.ts +0 -40
  14. package/src/__tests__/insertion-effect.test.ts +0 -119
  15. package/src/__tests__/integration-dom.test.ts +0 -58
  16. package/src/__tests__/integration.test.ts +0 -179
  17. package/src/__tests__/keyframes.test.ts +0 -68
  18. package/src/__tests__/memory-growth.test.ts +0 -220
  19. package/src/__tests__/native-marker.test.ts +0 -9
  20. package/src/__tests__/p3-features.test.ts +0 -316
  21. package/src/__tests__/resolve-cache.test.ts +0 -94
  22. package/src/__tests__/resolve.test.ts +0 -308
  23. package/src/__tests__/shared.test.ts +0 -133
  24. package/src/__tests__/sheet-advanced.test.ts +0 -659
  25. package/src/__tests__/sheet-split-atrules.test.ts +0 -410
  26. package/src/__tests__/sheet.test.ts +0 -250
  27. package/src/__tests__/static-styler-resolve-cost.test.ts +0 -160
  28. package/src/__tests__/styled-reactive.test.ts +0 -74
  29. package/src/__tests__/styled-ssr.test.ts +0 -75
  30. package/src/__tests__/styled.test.ts +0 -511
  31. package/src/__tests__/styler.browser.test.tsx +0 -194
  32. package/src/__tests__/theme.test.ts +0 -33
  33. package/src/__tests__/useCSS.test.ts +0 -172
  34. package/src/css.ts +0 -13
  35. package/src/env.d.ts +0 -6
  36. package/src/forward.ts +0 -308
  37. package/src/globalStyle.ts +0 -53
  38. package/src/hash.ts +0 -28
  39. package/src/index.ts +0 -15
  40. package/src/keyframes.ts +0 -36
  41. package/src/manifest.ts +0 -332
  42. package/src/resolve.ts +0 -225
  43. package/src/shared.ts +0 -22
  44. package/src/sheet.ts +0 -635
  45. package/src/styled.tsx +0 -503
  46. package/src/tests/manifest-snapshot.test.ts +0 -51
  47. package/src/useCSS.ts +0 -20
@@ -1,410 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
- import { hash } from '../hash'
3
- import { createSheet, StyleSheet } from '../sheet'
4
-
5
- /**
6
- * Tests for the @media/@supports/@container splitting behavior.
7
- *
8
- * When CSS text contains nested at-rules, sheet.insert() should split them
9
- * into separate top-level rules rather than relying on CSS Nesting.
10
- * This matches the approach used by styled-components and Emotion.
11
- */
12
-
13
- describe('StyleSheet -- at-rule splitting', () => {
14
- describe('SSR mode (splitAtRules internals via SSR output)', () => {
15
- let originalDocument: typeof document
16
-
17
- beforeEach(() => {
18
- originalDocument = globalThis.document
19
- // @ts-expect-error - intentionally deleting for SSR simulation
20
- delete globalThis.document
21
- })
22
-
23
- afterEach(() => {
24
- globalThis.document = originalDocument
25
- })
26
-
27
- it('CSS without @media produces a single rule', () => {
28
- const s = createSheet()
29
- s.insert('color: red; font-size: 16px;')
30
- const styles = s.getStyles()
31
-
32
- // Should have exactly one rule: .pyr-xxx{color: red; font-size: 16px;}
33
- expect(styles).toMatch(/^\.pyr-[0-9a-z]+\{color: red; font-size: 16px;\}$/)
34
- })
35
-
36
- it('CSS with @media splits into base + media rules', () => {
37
- const s = createSheet()
38
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
39
- const styles = s.getStyles()
40
-
41
- // Base rule: .pyr-xxx{color: red;}
42
- expect(styles).toMatch(/\.pyr-[0-9a-z]+\{color: red;\}/)
43
- // Media rule: @media (min-width: 768px){.pyr-xxx{color: blue;}}
44
- expect(styles).toMatch(/@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{color: blue;\}\}/)
45
- // The base rule should NOT contain @media inside its braces
46
- expect(styles).not.toMatch(/\.pyr-[0-9a-z]+\{[^}]*@media/)
47
- })
48
-
49
- it('CSS with multiple @media produces multiple separate rules', () => {
50
- const s = createSheet()
51
- s.insert(
52
- 'position: absolute; bottom: -4.375rem; @media (min-width: 36em){right: -11.25rem;} @media (min-width: 48em){bottom: 0; height: 40rem;}',
53
- )
54
- const styles = s.getStyles()
55
-
56
- // Base
57
- expect(styles).toContain('position: absolute; bottom: -4.375rem;')
58
- // Two separate media rules
59
- expect(styles).toMatch(/@media \(min-width: 36em\)\{\.pyr-[0-9a-z]+\{right: -11.25rem;\}\}/)
60
- expect(styles).toMatch(
61
- /@media \(min-width: 48em\)\{\.pyr-[0-9a-z]+\{bottom: 0; height: 40rem;\}\}/,
62
- )
63
- })
64
-
65
- it('CSS with only @media (no base declarations) works correctly', () => {
66
- const s = createSheet()
67
- s.insert('@media (min-width: 768px){color: blue;} @media (min-width: 1024px){color: green;}')
68
- const styles = s.getStyles()
69
-
70
- // No base rule (or empty base)
71
- expect(styles).not.toMatch(/\.pyr-[0-9a-z]+\{\}/)
72
- // Both media rules present
73
- expect(styles).toMatch(/@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{color: blue;\}\}/)
74
- expect(styles).toMatch(/@media \(min-width: 1024px\)\{\.pyr-[0-9a-z]+\{color: green;\}\}/)
75
- })
76
-
77
- it('layer wraps both base and media rules in @layer', () => {
78
- const s = createSheet({ layer: 'rocketstyle' })
79
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
80
- const styles = s.getStyles()
81
-
82
- // Base wrapped in @layer: @layer rocketstyle{.pyr-xxx{color: red;}}
83
- expect(styles).toMatch(/@layer rocketstyle\{\.pyr-[0-9a-z]+\{color: red;\}\}/)
84
- // Media wrapped in @layer: @layer rocketstyle{@media (...){.pyr-xxx{color: blue;}}}
85
- expect(styles).toMatch(
86
- /@layer rocketstyle\{@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{color: blue;\}\}\}/,
87
- )
88
- })
89
-
90
- it('@supports blocks are also split out', () => {
91
- const s = createSheet()
92
- s.insert('display: flex; @supports (display: grid){display: grid;}')
93
- const styles = s.getStyles()
94
-
95
- expect(styles).toMatch(/\.pyr-[0-9a-z]+\{display: flex;\}/)
96
- expect(styles).toMatch(/@supports \(display: grid\)\{\.pyr-[0-9a-z]+\{display: grid;\}\}/)
97
- })
98
-
99
- it('@container blocks are also split out', () => {
100
- const s = createSheet()
101
- s.insert('font-size: 1rem; @container (min-width: 400px){font-size: 1.25rem;}')
102
- const styles = s.getStyles()
103
-
104
- expect(styles).toMatch(/\.pyr-[0-9a-z]+\{font-size: 1rem;\}/)
105
- expect(styles).toMatch(
106
- /@container \(min-width: 400px\)\{\.pyr-[0-9a-z]+\{font-size: 1.25rem;\}\}/,
107
- )
108
- })
109
-
110
- it('@layer wraps each split rule individually', () => {
111
- const s = createSheet({ layer: 'components' })
112
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
113
- const styles = s.getStyles()
114
-
115
- // Base wrapped in layer
116
- expect(styles).toMatch(/@layer components\{\.pyr-[0-9a-z]+\{color: red;\}\}/)
117
- // Media wrapped in layer
118
- expect(styles).toMatch(
119
- /@layer components\{@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{color: blue;\}\}\}/,
120
- )
121
- })
122
-
123
- it('deduplicates CSS with @media (same CSS -> same className -> single insert)', () => {
124
- const s = createSheet()
125
- const cssStr = 'color: red; @media (min-width: 768px){color: blue;}'
126
- s.insert(cssStr)
127
- s.insert(cssStr)
128
-
129
- const styles = s.getStyles()
130
- const baseMatches = styles.match(/\.pyr-[0-9a-z]+\{color: red;\}/g)
131
- expect(baseMatches).toHaveLength(1)
132
- })
133
-
134
- it('real-world example: position + responsive inset/height', () => {
135
- const s = createSheet()
136
- const cssStr =
137
- 'position: absolute; bottom: -4.375rem; right: -12.5rem; height: 28.75rem; ' +
138
- '@media only screen and (min-width: 36em){right: -11.25rem;} ' +
139
- '@media only screen and (min-width: 48em){bottom: 0; height: 40rem;} ' +
140
- '@media only screen and (min-width: 62em){right: -6.25rem;} ' +
141
- '@media only screen and (min-width: 100em){right: initial; left: 55%;}'
142
- s.insert(cssStr)
143
- const styles = s.getStyles()
144
-
145
- // Base rule has position, bottom, right, height
146
- expect(styles).toContain('position: absolute;')
147
- expect(styles).toContain('bottom: -4.375rem;')
148
- expect(styles).toContain('right: -12.5rem;')
149
- expect(styles).toContain('height: 28.75rem;')
150
-
151
- // Each media query is a separate top-level rule
152
- expect(styles).toMatch(/@media only screen and \(min-width: 36em\)\{/)
153
- expect(styles).toMatch(/@media only screen and \(min-width: 48em\)\{/)
154
- expect(styles).toMatch(/@media only screen and \(min-width: 62em\)\{/)
155
- expect(styles).toMatch(/@media only screen and \(min-width: 100em\)\{/)
156
-
157
- // No nested @media inside a class selector
158
- expect(styles).not.toMatch(/\.pyr-[0-9a-z]+\{[^}]*@media/)
159
- })
160
-
161
- it('getStyleTag contains all split rules', () => {
162
- const s = createSheet()
163
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
164
- const tag = s.getStyleTag()
165
-
166
- expect(tag).toMatch(/^<style data-pyreon-styler="">.*<\/style>$/)
167
- expect(tag).toContain('color: red;')
168
- expect(tag).toContain('@media (min-width: 768px)')
169
- })
170
-
171
- it('reset clears all split rules from SSR buffer and cache', () => {
172
- const s = createSheet()
173
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
174
- expect(s.getStyles()).not.toBe('')
175
-
176
- s.reset()
177
- expect(s.getStyles()).toBe('')
178
- expect(s.cacheSize).toBe(0) // cache also cleared for SSR correctness
179
- })
180
- })
181
-
182
- describe('DOM mode (insertRule verification)', () => {
183
- beforeEach(() => {
184
- for (const el of Array.from(document.querySelectorAll('style[data-pyreon-styler]')))
185
- el.remove()
186
- })
187
-
188
- it('inserts base + media as separate CSSRules', () => {
189
- const s = createSheet()
190
- s.insert('color: red; @media (min-width: 768px){color: blue;}')
191
-
192
- // Find the style element
193
- const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
194
- expect(styleEl).not.toBeNull()
195
- const sheet = styleEl.sheet
196
- if (!sheet) throw new Error('expected sheet')
197
-
198
- // Should have at least 2 rules: one CSSStyleRule + one CSSMediaRule
199
- let hasStyleRule = false
200
- let hasMediaRule = false
201
-
202
- for (let i = 0; i < sheet.cssRules.length; i++) {
203
- const rule = sheet.cssRules[i]
204
- if (rule instanceof CSSStyleRule && rule.selectorText.startsWith('.pyr-')) {
205
- hasStyleRule = true
206
- }
207
- if (rule instanceof CSSMediaRule) {
208
- hasMediaRule = true
209
- }
210
- }
211
-
212
- expect(hasStyleRule).toBe(true)
213
- expect(hasMediaRule).toBe(true)
214
- })
215
-
216
- it('single selector appears in both base and media rules', () => {
217
- const s = createSheet()
218
- const className = s.insert('color: red; @media (min-width: 768px){color: blue;}')
219
-
220
- const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
221
- const sheet = styleEl.sheet
222
- if (!sheet) throw new Error('expected sheet')
223
- const singleSelector = `.${className}`
224
-
225
- let baseFound = false
226
- let mediaInnerFound = false
227
-
228
- for (let i = 0; i < sheet.cssRules.length; i++) {
229
- const rule = sheet.cssRules[i]
230
- if (rule instanceof CSSStyleRule && rule.selectorText === singleSelector) {
231
- baseFound = true
232
- }
233
- if (rule instanceof CSSMediaRule) {
234
- for (let j = 0; j < rule.cssRules.length; j++) {
235
- const inner = rule.cssRules[j]
236
- if (inner instanceof CSSStyleRule && inner.selectorText === singleSelector) {
237
- mediaInnerFound = true
238
- }
239
- }
240
- }
241
- }
242
-
243
- expect(baseFound).toBe(true)
244
- expect(mediaInnerFound).toBe(true)
245
- })
246
- })
247
-
248
- describe('hydration with split rules', () => {
249
- beforeEach(() => {
250
- for (const el of Array.from(document.querySelectorAll('style[data-pyreon-styler]')))
251
- el.remove()
252
- })
253
-
254
- it('hydrates className from CSSMediaRule inner selectors', () => {
255
- // Simulate SSR: create a style tag with split rules
256
- const el = document.createElement('style')
257
- el.setAttribute('data-pyreon-styler', '')
258
- document.head.appendChild(el)
259
-
260
- const className = `pyr-${hash('color: red;')}`
261
-
262
- // Insert rules that simulate what SSR produces
263
- el.sheet?.insertRule(`.${className}{color: red;}`, 0)
264
- el.sheet?.insertRule(`@media (min-width: 768px){.${className}{color: blue;}}`, 1)
265
-
266
- // Create a new sheet that will hydrate from the tag
267
- const s = new StyleSheet()
268
- // The sheet should have hydrated the className
269
- expect(s.has(className)).toBe(true)
270
- // Subsequent insert is a no-op (deduped)
271
- expect(s.cacheSize).toBeGreaterThanOrEqual(1)
272
- })
273
-
274
- it('hydrates className from @layer wrapped selectors in media rules', () => {
275
- const el = document.createElement('style')
276
- el.setAttribute('data-pyreon-styler', '')
277
- document.head.appendChild(el)
278
-
279
- const className = `pyr-${hash('font-size: 1rem;')}`
280
-
281
- el.sheet?.insertRule(`.${className}{font-size: 1rem;}`, 0)
282
- el.sheet?.insertRule(
283
- `@media (min-width: 768px){.${className}{font-size: 1.5rem;}}`,
284
- 1,
285
- )
286
-
287
- const s = new StyleSheet()
288
- expect(s.has(className)).toBe(true)
289
- })
290
-
291
- it('hydrates from media-only rules (no base style rule)', () => {
292
- const el = document.createElement('style')
293
- el.setAttribute('data-pyreon-styler', '')
294
- document.head.appendChild(el)
295
-
296
- const className = `pyr-${hash('responsive-only')}`
297
-
298
- // Only a media rule, no base rule
299
- el.sheet?.insertRule(`@media (min-width: 768px){.${className}{color: blue;}}`, 0)
300
-
301
- const s = new StyleSheet()
302
- expect(s.has(className)).toBe(true)
303
- })
304
- })
305
-
306
- describe('edge cases', () => {
307
- let originalDocument: typeof document
308
-
309
- beforeEach(() => {
310
- originalDocument = globalThis.document
311
- // @ts-expect-error - intentionally deleting for SSR simulation
312
- delete globalThis.document
313
- })
314
-
315
- afterEach(() => {
316
- globalThis.document = originalDocument
317
- })
318
-
319
- it('handles empty CSS text', () => {
320
- const s = createSheet()
321
- const cls = s.insert('')
322
- expect(cls).toMatch(/^pyr-/)
323
- expect(s.getStyles()).toBe('')
324
- })
325
-
326
- it('handles CSS with @ in a value (not an at-rule)', () => {
327
- const s = createSheet()
328
- s.insert('content: "@media";')
329
- // Should not be confused by @ in a string value
330
- expect(s.getStyles()).toContain('content: "@media";')
331
- })
332
-
333
- it('handles @keyframes reference in the CSS without splitting it', () => {
334
- const s = createSheet()
335
- s.insert('animation: fadeIn 0.3s;')
336
- const styles = s.getStyles()
337
- expect(styles).toContain('animation: fadeIn 0.3s;')
338
- })
339
-
340
- it('preserves &:hover nesting in base CSS', () => {
341
- const s = createSheet()
342
- s.insert('color: red; &:hover{color: blue;}')
343
- const styles = s.getStyles()
344
-
345
- // The &:hover block should stay inside the base rule
346
- expect(styles).toMatch(/\.pyr-[0-9a-z]+\{color: red; &:hover\{color: blue;\}\}/)
347
- })
348
-
349
- it('preserves &:hover nesting alongside @media splitting', () => {
350
- const s = createSheet()
351
- s.insert('color: red; &:hover{color: blue;} @media (min-width: 768px){font-size: 2rem;}')
352
- const styles = s.getStyles()
353
-
354
- // Base rule has color + &:hover
355
- expect(styles).toMatch(/\.pyr-[0-9a-z]+\{color: red; &:hover\{color: blue;\}\}/)
356
- // Media rule is separate
357
- expect(styles).toMatch(/@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{font-size: 2rem;\}\}/)
358
- })
359
-
360
- it('handles consecutive @media blocks with no base CSS between them', () => {
361
- const s = createSheet()
362
- s.insert('@media (min-width: 768px){color: blue;} @media (min-width: 1024px){color: green;}')
363
- const styles = s.getStyles()
364
-
365
- expect(styles).toMatch(/@media \(min-width: 768px\)/)
366
- expect(styles).toMatch(/@media \(min-width: 1024px\)/)
367
- })
368
- })
369
-
370
- describe('performance characteristics', () => {
371
- let originalDocument: typeof document
372
-
373
- beforeEach(() => {
374
- originalDocument = globalThis.document
375
- // @ts-expect-error - intentionally deleting for SSR simulation
376
- delete globalThis.document
377
- })
378
-
379
- afterEach(() => {
380
- globalThis.document = originalDocument
381
- })
382
-
383
- it('fast path: no scanning when CSS has no @ character', () => {
384
- const s = createSheet()
385
- // Insert 1000 simple rules -- should not trigger any splitting logic
386
- const start = performance.now()
387
- for (let i = 0; i < 1000; i++) {
388
- s.insert(`prop-${i}: val-${i};`)
389
- }
390
- const elapsed = performance.now() - start
391
-
392
- expect(s.cacheSize).toBe(1000)
393
- // Should complete quickly — 500ms is generous for CI runners
394
- expect(elapsed).toBeLessThan(500)
395
- })
396
-
397
- it('splitting adds minimal overhead for CSS with @media', () => {
398
- const s = createSheet()
399
- const start = performance.now()
400
- for (let i = 0; i < 500; i++) {
401
- s.insert(`color: color-${i}; @media (min-width: ${i}px){font-size: ${i}rem;}`)
402
- }
403
- const elapsed = performance.now() - start
404
-
405
- expect(s.cacheSize).toBe(500)
406
- // Should still be fast — 500ms is generous for CI runners
407
- expect(elapsed).toBeLessThan(500)
408
- })
409
- })
410
- })
@@ -1,250 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { hash } from '../hash'
3
- import { onSheetClear, sheet, StyleSheet } from '../sheet'
4
-
5
- describe('StyleSheet', () => {
6
- beforeEach(() => {
7
- sheet.reset()
8
- })
9
-
10
- afterEach(() => {
11
- sheet.reset()
12
- })
13
-
14
- describe('insert', () => {
15
- it('returns a class name with pyr- prefix', () => {
16
- const className = sheet.insert('display: flex;')
17
- expect(className).toMatch(/^pyr-[0-9a-z]+$/)
18
- })
19
-
20
- it('same CSS text always returns same class name (dedup)', () => {
21
- const cls1 = sheet.insert('color: red;')
22
- const cls2 = sheet.insert('color: red;')
23
- expect(cls1).toBe(cls2)
24
- })
25
-
26
- it('different CSS text returns different class names', () => {
27
- const cls1 = sheet.insert('color: red;')
28
- const cls2 = sheet.insert('color: blue;')
29
- expect(cls1).not.toBe(cls2)
30
- })
31
-
32
- it('class name matches hash of CSS text', () => {
33
- const cssText = 'display: flex;'
34
- const className = sheet.insert(cssText)
35
- expect(className).toBe(`pyr-${hash(cssText)}`)
36
- })
37
-
38
- it('handles empty string CSS', () => {
39
- const className = sheet.insert('')
40
- expect(className).toMatch(/^pyr-[0-9a-z]+$/)
41
- })
42
-
43
- it('supports layer mode (@layer wrapping)', () => {
44
- const className = sheet.insert('color: red;', false, 'rocketstyle')
45
- expect(className).toMatch(/^pyr-[0-9a-z]+$/)
46
- })
47
- })
48
-
49
- describe('cache eviction', () => {
50
- it('evicts oldest entries when cache exceeds MAX_CACHE', () => {
51
- for (let i = 0; i < 100; i++) {
52
- sheet.insert(`unique-prop-${i}: value-${i};`)
53
- }
54
- const result = sheet.insert('color: red;')
55
- expect(result).toMatch(/^pyr-/)
56
- })
57
- })
58
-
59
- describe('insertKeyframes', () => {
60
- it('does not throw', () => {
61
- expect(() => sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')).not.toThrow()
62
- })
63
-
64
- it('deduplicates by name', () => {
65
- sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')
66
- // Second call with same name should not throw
67
- expect(() => sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')).not.toThrow()
68
- })
69
- })
70
-
71
- describe('insertGlobal', () => {
72
- it('does not throw for valid CSS', () => {
73
- expect(() => sheet.insertGlobal('body { margin: 0; }')).not.toThrow()
74
- })
75
-
76
- it('handles multiple calls without error', () => {
77
- sheet.insertGlobal('body { margin: 0; }')
78
- sheet.insertGlobal('html { box-sizing: border-box; }')
79
- })
80
-
81
- it('deduplicates same CSS', () => {
82
- sheet.insertGlobal('body { margin: 0; }')
83
- sheet.insertGlobal('body { margin: 0; }')
84
- // No error, second is deduped
85
- })
86
- })
87
-
88
- describe('getClassName', () => {
89
- it('returns a className without injecting', () => {
90
- const className = sheet.getClassName('color: red;')
91
- expect(className).toMatch(/^pyr-[0-9a-z]+$/)
92
- })
93
-
94
- it('returns same className as insert for same CSS', () => {
95
- const cssText = 'display: flex;'
96
- const getResult = sheet.getClassName(cssText)
97
- const insertResult = sheet.insert(cssText)
98
- expect(getResult).toBe(insertResult)
99
- })
100
- })
101
-
102
- describe('prepare', () => {
103
- it('returns className and rules', () => {
104
- const { className, rules } = sheet.prepare('color: red;')
105
- expect(className).toMatch(/^pyr-[0-9a-z]+$/)
106
- expect(rules).toContain(className)
107
- expect(rules).toContain('color: red;')
108
- })
109
-
110
- it('produces single selector (no boost)', () => {
111
- const { className, rules } = sheet.prepare('color: red;')
112
- // Single selector, no doubling
113
- expect(rules).toContain(`.${className}{`)
114
- expect(rules).not.toContain(`.${className}.${className}`)
115
- })
116
- })
117
-
118
- describe('SSR support', () => {
119
- it('getStyleTag returns a string', () => {
120
- const result = sheet.getStyleTag()
121
- expect(typeof result).toBe('string')
122
- expect(result).toContain('data-pyreon-styler')
123
- })
124
-
125
- it('getStyles returns empty string when no rules inserted', () => {
126
- const result = sheet.getStyles()
127
- expect(result).toBe('')
128
- })
129
- })
130
-
131
- describe('reset', () => {
132
- it('clears cache so new inserts re-generate class names', () => {
133
- const cls1 = sheet.insert('color: green;')
134
- sheet.reset()
135
- const cls2 = sheet.insert('color: green;')
136
- expect(cls1).toBe(cls2)
137
- })
138
- })
139
-
140
- describe('clearCache and clearAll', () => {
141
- it('clearCache clears the cache', () => {
142
- sheet.insert('color: red;')
143
- sheet.clearCache()
144
- expect(sheet.cacheSize).toBe(0)
145
- })
146
-
147
- it('clearAll clears cache and SSR buffer', () => {
148
- sheet.insert('color: red;')
149
- sheet.clearAll()
150
- expect(sheet.cacheSize).toBe(0)
151
- expect(sheet.getStyles()).toBe('')
152
- })
153
- })
154
-
155
- describe('onSheetClear', () => {
156
- // Subscriber registry used by `styled.tsx` to drop its static-component
157
- // cache when the singleton sheet is cleared. Without this, stale
158
- // `StaticStyled` ComponentFns survive HMR and continue returning class
159
- // names the sheet just deleted from the DOM.
160
- it('fires subscribers after clearAll', () => {
161
- const cb = vi.fn()
162
- const dispose = onSheetClear(cb)
163
- sheet.clearAll()
164
- expect(cb).toHaveBeenCalledTimes(1)
165
- dispose()
166
- })
167
-
168
- it('does NOT fire subscribers on clearCache (partial cleanup)', () => {
169
- const cb = vi.fn()
170
- const dispose = onSheetClear(cb)
171
- sheet.insert('color: red;')
172
- sheet.clearCache()
173
- expect(cb).not.toHaveBeenCalled()
174
- dispose()
175
- })
176
-
177
- it('disposer removes the subscriber', () => {
178
- const cb = vi.fn()
179
- const dispose = onSheetClear(cb)
180
- dispose()
181
- sheet.clearAll()
182
- expect(cb).not.toHaveBeenCalled()
183
- })
184
-
185
- it('fires multiple subscribers in registration order', () => {
186
- const order: number[] = []
187
- const dispose1 = onSheetClear(() => order.push(1))
188
- const dispose2 = onSheetClear(() => order.push(2))
189
- sheet.clearAll()
190
- expect(order).toEqual([1, 2])
191
- dispose1()
192
- dispose2()
193
- })
194
- })
195
-
196
- describe('has', () => {
197
- it('returns true for cached classNames', () => {
198
- const className = sheet.insert('color: red;')
199
- expect(sheet.has(className)).toBe(true)
200
- })
201
-
202
- it('returns false for unknown classNames', () => {
203
- expect(sheet.has('pyr-unknown')).toBe(false)
204
- })
205
- })
206
-
207
- // Failed insertRule used to be silently swallowed in production because
208
- // `process.env.NODE_ENV !== 'production'` is dead code in real Vite browser
209
- // bundles (Vite does not polyfill `process`). The dev gate now uses
210
- // `import.meta.env.DEV` which fires the warn under vitest and tree-shakes
211
- // away in prod. This test asserts the warn fires for malformed CSS in dev.
212
- describe('insertRule failures fire console.warn in dev', () => {
213
- it('warns when StyleSheet.insertRule throws on malformed CSS', () => {
214
- const local = new StyleSheet()
215
- const realSheet = (local as unknown as { sheet: CSSStyleSheet | null }).sheet
216
- if (!realSheet) {
217
- // happy-dom may not expose a real sheet — skip; the prod-bundle
218
- // tree-shake test in dev-gate-treeshake.test.ts covers the build side.
219
- return
220
- }
221
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
222
- // Mock the prototype, not the instance — happy-dom's CSSStyleSheet may
223
- // expose `insertRule` as a non-configurable own property that vi.spyOn
224
- // can't intercept on an instance.
225
- const proto = Object.getPrototypeOf(realSheet) as { insertRule: () => number }
226
- const insertSpy = vi.spyOn(proto, 'insertRule').mockImplementation(() => {
227
- throw new SyntaxError('invalid rule')
228
- })
229
-
230
- // Use a unique CSS string to bypass cross-instance/global insert cache
231
- local.insert(`color: ${Math.random()};`)
232
-
233
- const styleWarnings = warnSpy.mock.calls.filter(
234
- (args) => typeof args[0] === 'string' && args[0].includes('[styler]'),
235
- )
236
- expect(styleWarnings.length).toBeGreaterThan(0)
237
-
238
- insertSpy.mockRestore()
239
- warnSpy.mockRestore()
240
- })
241
-
242
- it('uses bundler-agnostic process.env.NODE_ENV — vitest sets NODE_ENV !== "production"', () => {
243
- // Smoke test the gate itself: vitest must set process.env.NODE_ENV to
244
- // a non-production value for the regression test above to be meaningful.
245
- // Every modern bundler (incl. Vitest's Vite pipeline) auto-replaces
246
- // `process.env.NODE_ENV` at build time.
247
- expect(process.env.NODE_ENV).not.toBe('production')
248
- })
249
- })
250
- })