@pyreon/styler 0.24.5 → 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,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
|
-
})
|