@pyreon/styler 0.11.4 → 0.11.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/README.md +27 -23
- package/lib/index.d.ts +9 -2
- package/lib/index.js +47 -4
- package/package.json +22 -22
- package/src/ThemeProvider.ts +10 -3
- package/src/__tests__/ThemeProvider.test.ts +21 -21
- package/src/__tests__/benchmark.bench.ts +56 -45
- package/src/__tests__/composition-chain.test.ts +200 -151
- package/src/__tests__/forward.test.ts +122 -122
- package/src/__tests__/globalStyle.test.ts +18 -18
- package/src/__tests__/hash.test.ts +27 -27
- package/src/__tests__/hybrid-injection.test.ts +83 -59
- package/src/__tests__/index.ts +10 -10
- package/src/__tests__/insertion-effect.test.ts +45 -32
- package/src/__tests__/integration.test.ts +81 -51
- package/src/__tests__/keyframes.test.ts +13 -13
- package/src/__tests__/memory-growth.test.ts +21 -21
- package/src/__tests__/p3-features.test.ts +162 -104
- package/src/__tests__/shared.test.ts +51 -33
- package/src/__tests__/sheet-advanced.test.ts +227 -227
- package/src/__tests__/sheet-split-atrules.test.ts +85 -85
- package/src/__tests__/sheet.test.ts +69 -69
- package/src/__tests__/styled-ssr.test.ts +36 -28
- package/src/__tests__/styled.test.ts +214 -145
- package/src/__tests__/theme.test.ts +11 -11
- package/src/__tests__/useCSS.test.ts +89 -59
- package/src/css.ts +1 -1
- package/src/forward.ts +187 -187
- package/src/globalStyle.ts +5 -5
- package/src/index.ts +15 -15
- package/src/keyframes.ts +3 -3
- package/src/resolve.ts +14 -14
- package/src/shared.ts +2 -2
- package/src/sheet.ts +26 -26
- package/src/styled.tsx +145 -100
- package/src/useCSS.ts +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
2
|
-
import { hash } from
|
|
3
|
-
import { createSheet, StyleSheet } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { hash } from '../hash'
|
|
3
|
+
import { createSheet, StyleSheet } from '../sheet'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Tests for the @media/@supports/@container splitting behavior.
|
|
@@ -10,8 +10,8 @@ import { createSheet, StyleSheet } from "../sheet"
|
|
|
10
10
|
* This matches the approach used by styled-components and Emotion.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
describe(
|
|
14
|
-
describe(
|
|
13
|
+
describe('StyleSheet -- at-rule splitting', () => {
|
|
14
|
+
describe('SSR mode (splitAtRules internals via SSR output)', () => {
|
|
15
15
|
let originalDocument: typeof document
|
|
16
16
|
|
|
17
17
|
beforeEach(() => {
|
|
@@ -24,18 +24,18 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
24
24
|
globalThis.document = originalDocument
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
it(
|
|
27
|
+
it('CSS without @media produces a single rule', () => {
|
|
28
28
|
const s = createSheet()
|
|
29
|
-
s.insert(
|
|
29
|
+
s.insert('color: red; font-size: 16px;')
|
|
30
30
|
const styles = s.getStyles()
|
|
31
31
|
|
|
32
32
|
// Should have exactly one rule: .pyr-xxx{color: red; font-size: 16px;}
|
|
33
33
|
expect(styles).toMatch(/^\.pyr-[0-9a-z]+\{color: red; font-size: 16px;\}$/)
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
it(
|
|
36
|
+
it('CSS with @media splits into base + media rules', () => {
|
|
37
37
|
const s = createSheet()
|
|
38
|
-
s.insert(
|
|
38
|
+
s.insert('color: red; @media (min-width: 768px){color: blue;}')
|
|
39
39
|
const styles = s.getStyles()
|
|
40
40
|
|
|
41
41
|
// Base rule: .pyr-xxx{color: red;}
|
|
@@ -46,15 +46,15 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
46
46
|
expect(styles).not.toMatch(/\.pyr-[0-9a-z]+\{[^}]*@media/)
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it(
|
|
49
|
+
it('CSS with multiple @media produces multiple separate rules', () => {
|
|
50
50
|
const s = createSheet()
|
|
51
51
|
s.insert(
|
|
52
|
-
|
|
52
|
+
'position: absolute; bottom: -4.375rem; @media (min-width: 36em){right: -11.25rem;} @media (min-width: 48em){bottom: 0; height: 40rem;}',
|
|
53
53
|
)
|
|
54
54
|
const styles = s.getStyles()
|
|
55
55
|
|
|
56
56
|
// Base
|
|
57
|
-
expect(styles).toContain(
|
|
57
|
+
expect(styles).toContain('position: absolute; bottom: -4.375rem;')
|
|
58
58
|
// Two separate media rules
|
|
59
59
|
expect(styles).toMatch(/@media \(min-width: 36em\)\{\.pyr-[0-9a-z]+\{right: -11.25rem;\}\}/)
|
|
60
60
|
expect(styles).toMatch(
|
|
@@ -62,9 +62,9 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
62
62
|
)
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
it(
|
|
65
|
+
it('CSS with only @media (no base declarations) works correctly', () => {
|
|
66
66
|
const s = createSheet()
|
|
67
|
-
s.insert(
|
|
67
|
+
s.insert('@media (min-width: 768px){color: blue;} @media (min-width: 1024px){color: green;}')
|
|
68
68
|
const styles = s.getStyles()
|
|
69
69
|
|
|
70
70
|
// No base rule (or empty base)
|
|
@@ -74,9 +74,9 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
74
74
|
expect(styles).toMatch(/@media \(min-width: 1024px\)\{\.pyr-[0-9a-z]+\{color: green;\}\}/)
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
it(
|
|
77
|
+
it('boost doubles selector in both base and media rules', () => {
|
|
78
78
|
const s = createSheet()
|
|
79
|
-
s.insert(
|
|
79
|
+
s.insert('color: red; @media (min-width: 768px){color: blue;}', true)
|
|
80
80
|
const styles = s.getStyles()
|
|
81
81
|
|
|
82
82
|
// Base: .pyr-xxx.pyr-xxx{color: red;}
|
|
@@ -87,18 +87,18 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
87
87
|
)
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
it(
|
|
90
|
+
it('@supports blocks are also split out', () => {
|
|
91
91
|
const s = createSheet()
|
|
92
|
-
s.insert(
|
|
92
|
+
s.insert('display: flex; @supports (display: grid){display: grid;}')
|
|
93
93
|
const styles = s.getStyles()
|
|
94
94
|
|
|
95
95
|
expect(styles).toMatch(/\.pyr-[0-9a-z]+\{display: flex;\}/)
|
|
96
96
|
expect(styles).toMatch(/@supports \(display: grid\)\{\.pyr-[0-9a-z]+\{display: grid;\}\}/)
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
-
it(
|
|
99
|
+
it('@container blocks are also split out', () => {
|
|
100
100
|
const s = createSheet()
|
|
101
|
-
s.insert(
|
|
101
|
+
s.insert('font-size: 1rem; @container (min-width: 400px){font-size: 1.25rem;}')
|
|
102
102
|
const styles = s.getStyles()
|
|
103
103
|
|
|
104
104
|
expect(styles).toMatch(/\.pyr-[0-9a-z]+\{font-size: 1rem;\}/)
|
|
@@ -107,9 +107,9 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
107
107
|
)
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it(
|
|
111
|
-
const s = createSheet({ layer:
|
|
112
|
-
s.insert(
|
|
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
113
|
const styles = s.getStyles()
|
|
114
114
|
|
|
115
115
|
// Base wrapped in layer
|
|
@@ -120,9 +120,9 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
120
120
|
)
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
it(
|
|
123
|
+
it('deduplicates CSS with @media (same CSS -> same className -> single insert)', () => {
|
|
124
124
|
const s = createSheet()
|
|
125
|
-
const cssStr =
|
|
125
|
+
const cssStr = 'color: red; @media (min-width: 768px){color: blue;}'
|
|
126
126
|
s.insert(cssStr)
|
|
127
127
|
s.insert(cssStr)
|
|
128
128
|
|
|
@@ -131,22 +131,22 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
131
131
|
expect(baseMatches).toHaveLength(1)
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
it(
|
|
134
|
+
it('real-world example: position + responsive inset/height', () => {
|
|
135
135
|
const s = createSheet()
|
|
136
136
|
const cssStr =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
142
|
s.insert(cssStr, true)
|
|
143
143
|
const styles = s.getStyles()
|
|
144
144
|
|
|
145
145
|
// Base rule has position, bottom, right, height
|
|
146
|
-
expect(styles).toContain(
|
|
147
|
-
expect(styles).toContain(
|
|
148
|
-
expect(styles).toContain(
|
|
149
|
-
expect(styles).toContain(
|
|
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
150
|
|
|
151
151
|
// Each media query is a separate top-level rule
|
|
152
152
|
expect(styles).toMatch(/@media only screen and \(min-width: 36em\)\{/)
|
|
@@ -158,42 +158,42 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
158
158
|
expect(styles).not.toMatch(/\.pyr-[0-9a-z]+\{[^}]*@media/)
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
it(
|
|
161
|
+
it('getStyleTag contains all split rules', () => {
|
|
162
162
|
const s = createSheet()
|
|
163
|
-
s.insert(
|
|
163
|
+
s.insert('color: red; @media (min-width: 768px){color: blue;}')
|
|
164
164
|
const tag = s.getStyleTag()
|
|
165
165
|
|
|
166
166
|
expect(tag).toMatch(/^<style data-pyreon-styler="">.*<\/style>$/)
|
|
167
|
-
expect(tag).toContain(
|
|
168
|
-
expect(tag).toContain(
|
|
167
|
+
expect(tag).toContain('color: red;')
|
|
168
|
+
expect(tag).toContain('@media (min-width: 768px)')
|
|
169
169
|
})
|
|
170
170
|
|
|
171
|
-
it(
|
|
171
|
+
it('reset clears all split rules from SSR buffer and cache', () => {
|
|
172
172
|
const s = createSheet()
|
|
173
|
-
s.insert(
|
|
174
|
-
expect(s.getStyles()).not.toBe(
|
|
173
|
+
s.insert('color: red; @media (min-width: 768px){color: blue;}')
|
|
174
|
+
expect(s.getStyles()).not.toBe('')
|
|
175
175
|
|
|
176
176
|
s.reset()
|
|
177
|
-
expect(s.getStyles()).toBe(
|
|
177
|
+
expect(s.getStyles()).toBe('')
|
|
178
178
|
expect(s.cacheSize).toBe(0) // cache also cleared for SSR correctness
|
|
179
179
|
})
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
-
describe(
|
|
182
|
+
describe('DOM mode (insertRule verification)', () => {
|
|
183
183
|
beforeEach(() => {
|
|
184
|
-
for (const el of Array.from(document.querySelectorAll(
|
|
184
|
+
for (const el of Array.from(document.querySelectorAll('style[data-pyreon-styler]')))
|
|
185
185
|
el.remove()
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
-
it(
|
|
188
|
+
it('inserts base + media as separate CSSRules', () => {
|
|
189
189
|
const s = createSheet()
|
|
190
|
-
s.insert(
|
|
190
|
+
s.insert('color: red; @media (min-width: 768px){color: blue;}')
|
|
191
191
|
|
|
192
192
|
// Find the style element
|
|
193
|
-
const styleEl = document.querySelector(
|
|
193
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
194
194
|
expect(styleEl).not.toBeNull()
|
|
195
195
|
const sheet = styleEl.sheet
|
|
196
|
-
if (!sheet) throw new Error(
|
|
196
|
+
if (!sheet) throw new Error('expected sheet')
|
|
197
197
|
|
|
198
198
|
// Should have at least 2 rules: one CSSStyleRule + one CSSMediaRule
|
|
199
199
|
let hasStyleRule = false
|
|
@@ -201,7 +201,7 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
201
201
|
|
|
202
202
|
for (let i = 0; i < sheet.cssRules.length; i++) {
|
|
203
203
|
const rule = sheet.cssRules[i]
|
|
204
|
-
if (rule instanceof CSSStyleRule && rule.selectorText.startsWith(
|
|
204
|
+
if (rule instanceof CSSStyleRule && rule.selectorText.startsWith('.pyr-')) {
|
|
205
205
|
hasStyleRule = true
|
|
206
206
|
}
|
|
207
207
|
if (rule instanceof CSSMediaRule) {
|
|
@@ -214,13 +214,13 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complex logic is inherent to this function
|
|
217
|
-
it(
|
|
217
|
+
it('boosted selector appears in both base and media rules', () => {
|
|
218
218
|
const s = createSheet()
|
|
219
|
-
const className = s.insert(
|
|
219
|
+
const className = s.insert('color: red; @media (min-width: 768px){color: blue;}', true)
|
|
220
220
|
|
|
221
|
-
const styleEl = document.querySelector(
|
|
221
|
+
const styleEl = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
222
222
|
const sheet = styleEl.sheet
|
|
223
|
-
if (!sheet) throw new Error(
|
|
223
|
+
if (!sheet) throw new Error('expected sheet')
|
|
224
224
|
const boostedSelector = `.${className}.${className}`
|
|
225
225
|
|
|
226
226
|
let baseFound = false
|
|
@@ -246,19 +246,19 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
246
246
|
})
|
|
247
247
|
})
|
|
248
248
|
|
|
249
|
-
describe(
|
|
249
|
+
describe('hydration with split rules', () => {
|
|
250
250
|
beforeEach(() => {
|
|
251
|
-
for (const el of Array.from(document.querySelectorAll(
|
|
251
|
+
for (const el of Array.from(document.querySelectorAll('style[data-pyreon-styler]')))
|
|
252
252
|
el.remove()
|
|
253
253
|
})
|
|
254
254
|
|
|
255
|
-
it(
|
|
255
|
+
it('hydrates className from CSSMediaRule inner selectors', () => {
|
|
256
256
|
// Simulate SSR: create a style tag with split rules
|
|
257
|
-
const el = document.createElement(
|
|
258
|
-
el.setAttribute(
|
|
257
|
+
const el = document.createElement('style')
|
|
258
|
+
el.setAttribute('data-pyreon-styler', '')
|
|
259
259
|
document.head.appendChild(el)
|
|
260
260
|
|
|
261
|
-
const className = `pyr-${hash(
|
|
261
|
+
const className = `pyr-${hash('color: red;')}`
|
|
262
262
|
|
|
263
263
|
// Insert rules that simulate what SSR produces
|
|
264
264
|
el.sheet?.insertRule(`.${className}{color: red;}`, 0)
|
|
@@ -272,12 +272,12 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
272
272
|
expect(s.cacheSize).toBeGreaterThanOrEqual(1)
|
|
273
273
|
})
|
|
274
274
|
|
|
275
|
-
it(
|
|
276
|
-
const el = document.createElement(
|
|
277
|
-
el.setAttribute(
|
|
275
|
+
it('hydrates className from boosted selectors in media rules', () => {
|
|
276
|
+
const el = document.createElement('style')
|
|
277
|
+
el.setAttribute('data-pyreon-styler', '')
|
|
278
278
|
document.head.appendChild(el)
|
|
279
279
|
|
|
280
|
-
const className = `pyr-${hash(
|
|
280
|
+
const className = `pyr-${hash('font-size: 1rem;')}`
|
|
281
281
|
|
|
282
282
|
el.sheet?.insertRule(`.${className}.${className}{font-size: 1rem;}`, 0)
|
|
283
283
|
el.sheet?.insertRule(
|
|
@@ -289,12 +289,12 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
289
289
|
expect(s.has(className)).toBe(true)
|
|
290
290
|
})
|
|
291
291
|
|
|
292
|
-
it(
|
|
293
|
-
const el = document.createElement(
|
|
294
|
-
el.setAttribute(
|
|
292
|
+
it('hydrates from media-only rules (no base style rule)', () => {
|
|
293
|
+
const el = document.createElement('style')
|
|
294
|
+
el.setAttribute('data-pyreon-styler', '')
|
|
295
295
|
document.head.appendChild(el)
|
|
296
296
|
|
|
297
|
-
const className = `pyr-${hash(
|
|
297
|
+
const className = `pyr-${hash('responsive-only')}`
|
|
298
298
|
|
|
299
299
|
// Only a media rule, no base rule
|
|
300
300
|
el.sheet?.insertRule(`@media (min-width: 768px){.${className}{color: blue;}}`, 0)
|
|
@@ -304,7 +304,7 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
304
304
|
})
|
|
305
305
|
})
|
|
306
306
|
|
|
307
|
-
describe(
|
|
307
|
+
describe('edge cases', () => {
|
|
308
308
|
let originalDocument: typeof document
|
|
309
309
|
|
|
310
310
|
beforeEach(() => {
|
|
@@ -317,39 +317,39 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
317
317
|
globalThis.document = originalDocument
|
|
318
318
|
})
|
|
319
319
|
|
|
320
|
-
it(
|
|
320
|
+
it('handles empty CSS text', () => {
|
|
321
321
|
const s = createSheet()
|
|
322
|
-
const cls = s.insert(
|
|
322
|
+
const cls = s.insert('')
|
|
323
323
|
expect(cls).toMatch(/^pyr-/)
|
|
324
|
-
expect(s.getStyles()).toBe(
|
|
324
|
+
expect(s.getStyles()).toBe('')
|
|
325
325
|
})
|
|
326
326
|
|
|
327
|
-
it(
|
|
327
|
+
it('handles CSS with @ in a value (not an at-rule)', () => {
|
|
328
328
|
const s = createSheet()
|
|
329
329
|
s.insert('content: "@media";')
|
|
330
330
|
// Should not be confused by @ in a string value
|
|
331
331
|
expect(s.getStyles()).toContain('content: "@media";')
|
|
332
332
|
})
|
|
333
333
|
|
|
334
|
-
it(
|
|
334
|
+
it('handles @keyframes reference in the CSS without splitting it', () => {
|
|
335
335
|
const s = createSheet()
|
|
336
|
-
s.insert(
|
|
336
|
+
s.insert('animation: fadeIn 0.3s;')
|
|
337
337
|
const styles = s.getStyles()
|
|
338
|
-
expect(styles).toContain(
|
|
338
|
+
expect(styles).toContain('animation: fadeIn 0.3s;')
|
|
339
339
|
})
|
|
340
340
|
|
|
341
|
-
it(
|
|
341
|
+
it('preserves &:hover nesting in base CSS', () => {
|
|
342
342
|
const s = createSheet()
|
|
343
|
-
s.insert(
|
|
343
|
+
s.insert('color: red; &:hover{color: blue;}')
|
|
344
344
|
const styles = s.getStyles()
|
|
345
345
|
|
|
346
346
|
// The &:hover block should stay inside the base rule
|
|
347
347
|
expect(styles).toMatch(/\.pyr-[0-9a-z]+\{color: red; &:hover\{color: blue;\}\}/)
|
|
348
348
|
})
|
|
349
349
|
|
|
350
|
-
it(
|
|
350
|
+
it('preserves &:hover nesting alongside @media splitting', () => {
|
|
351
351
|
const s = createSheet()
|
|
352
|
-
s.insert(
|
|
352
|
+
s.insert('color: red; &:hover{color: blue;} @media (min-width: 768px){font-size: 2rem;}')
|
|
353
353
|
const styles = s.getStyles()
|
|
354
354
|
|
|
355
355
|
// Base rule has color + &:hover
|
|
@@ -358,9 +358,9 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
358
358
|
expect(styles).toMatch(/@media \(min-width: 768px\)\{\.pyr-[0-9a-z]+\{font-size: 2rem;\}\}/)
|
|
359
359
|
})
|
|
360
360
|
|
|
361
|
-
it(
|
|
361
|
+
it('handles consecutive @media blocks with no base CSS between them', () => {
|
|
362
362
|
const s = createSheet()
|
|
363
|
-
s.insert(
|
|
363
|
+
s.insert('@media (min-width: 768px){color: blue;} @media (min-width: 1024px){color: green;}')
|
|
364
364
|
const styles = s.getStyles()
|
|
365
365
|
|
|
366
366
|
expect(styles).toMatch(/@media \(min-width: 768px\)/)
|
|
@@ -368,7 +368,7 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
368
368
|
})
|
|
369
369
|
})
|
|
370
370
|
|
|
371
|
-
describe(
|
|
371
|
+
describe('performance characteristics', () => {
|
|
372
372
|
let originalDocument: typeof document
|
|
373
373
|
|
|
374
374
|
beforeEach(() => {
|
|
@@ -381,7 +381,7 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
381
381
|
globalThis.document = originalDocument
|
|
382
382
|
})
|
|
383
383
|
|
|
384
|
-
it(
|
|
384
|
+
it('fast path: no scanning when CSS has no @ character', () => {
|
|
385
385
|
const s = createSheet()
|
|
386
386
|
// Insert 1000 simple rules -- should not trigger any splitting logic
|
|
387
387
|
const start = performance.now()
|
|
@@ -395,7 +395,7 @@ describe("StyleSheet -- at-rule splitting", () => {
|
|
|
395
395
|
expect(elapsed).toBeLessThan(500)
|
|
396
396
|
})
|
|
397
397
|
|
|
398
|
-
it(
|
|
398
|
+
it('splitting adds minimal overhead for CSS with @media', () => {
|
|
399
399
|
const s = createSheet()
|
|
400
400
|
const start = performance.now()
|
|
401
401
|
for (let i = 0; i < 500; i++) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from
|
|
2
|
-
import { hash } from
|
|
3
|
-
import { sheet } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { hash } from '../hash'
|
|
3
|
+
import { sheet } from '../sheet'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
5
|
+
describe('StyleSheet', () => {
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
sheet.reset()
|
|
8
8
|
})
|
|
@@ -11,154 +11,154 @@ describe("StyleSheet", () => {
|
|
|
11
11
|
sheet.reset()
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
describe(
|
|
15
|
-
it(
|
|
16
|
-
const className = sheet.insert(
|
|
14
|
+
describe('insert', () => {
|
|
15
|
+
it('returns a class name with pyr- prefix', () => {
|
|
16
|
+
const className = sheet.insert('display: flex;')
|
|
17
17
|
expect(className).toMatch(/^pyr-[0-9a-z]+$/)
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it(
|
|
21
|
-
const cls1 = sheet.insert(
|
|
22
|
-
const cls2 = sheet.insert(
|
|
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
23
|
expect(cls1).toBe(cls2)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it(
|
|
27
|
-
const cls1 = sheet.insert(
|
|
28
|
-
const cls2 = sheet.insert(
|
|
26
|
+
it('different CSS text returns different class names', () => {
|
|
27
|
+
const cls1 = sheet.insert('color: red;')
|
|
28
|
+
const cls2 = sheet.insert('color: blue;')
|
|
29
29
|
expect(cls1).not.toBe(cls2)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it(
|
|
33
|
-
const cssText =
|
|
32
|
+
it('class name matches hash of CSS text', () => {
|
|
33
|
+
const cssText = 'display: flex;'
|
|
34
34
|
const className = sheet.insert(cssText)
|
|
35
35
|
expect(className).toBe(`pyr-${hash(cssText)}`)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
39
|
-
const className = sheet.insert(
|
|
38
|
+
it('handles empty string CSS', () => {
|
|
39
|
+
const className = sheet.insert('')
|
|
40
40
|
expect(className).toMatch(/^pyr-[0-9a-z]+$/)
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it(
|
|
44
|
-
const className = sheet.insert(
|
|
43
|
+
it('supports boost mode (doubled selector)', () => {
|
|
44
|
+
const className = sheet.insert('color: red;', true)
|
|
45
45
|
expect(className).toMatch(/^pyr-[0-9a-z]+$/)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
describe(
|
|
50
|
-
it(
|
|
49
|
+
describe('cache eviction', () => {
|
|
50
|
+
it('evicts oldest entries when cache exceeds MAX_CACHE', () => {
|
|
51
51
|
for (let i = 0; i < 100; i++) {
|
|
52
52
|
sheet.insert(`unique-prop-${i}: value-${i};`)
|
|
53
53
|
}
|
|
54
|
-
const result = sheet.insert(
|
|
54
|
+
const result = sheet.insert('color: red;')
|
|
55
55
|
expect(result).toMatch(/^pyr-/)
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
describe(
|
|
60
|
-
it(
|
|
61
|
-
expect(() => sheet.insertKeyframes(
|
|
59
|
+
describe('insertKeyframes', () => {
|
|
60
|
+
it('does not throw', () => {
|
|
61
|
+
expect(() => sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')).not.toThrow()
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it(
|
|
65
|
-
sheet.insertKeyframes(
|
|
64
|
+
it('deduplicates by name', () => {
|
|
65
|
+
sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')
|
|
66
66
|
// Second call with same name should not throw
|
|
67
|
-
expect(() => sheet.insertKeyframes(
|
|
67
|
+
expect(() => sheet.insertKeyframes('test-anim', 'from{opacity:0}to{opacity:1}')).not.toThrow()
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
describe(
|
|
72
|
-
it(
|
|
73
|
-
expect(() => sheet.insertGlobal(
|
|
71
|
+
describe('insertGlobal', () => {
|
|
72
|
+
it('does not throw for valid CSS', () => {
|
|
73
|
+
expect(() => sheet.insertGlobal('body { margin: 0; }')).not.toThrow()
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
it(
|
|
77
|
-
sheet.insertGlobal(
|
|
78
|
-
sheet.insertGlobal(
|
|
76
|
+
it('handles multiple calls without error', () => {
|
|
77
|
+
sheet.insertGlobal('body { margin: 0; }')
|
|
78
|
+
sheet.insertGlobal('html { box-sizing: border-box; }')
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
-
it(
|
|
82
|
-
sheet.insertGlobal(
|
|
83
|
-
sheet.insertGlobal(
|
|
81
|
+
it('deduplicates same CSS', () => {
|
|
82
|
+
sheet.insertGlobal('body { margin: 0; }')
|
|
83
|
+
sheet.insertGlobal('body { margin: 0; }')
|
|
84
84
|
// No error, second is deduped
|
|
85
85
|
})
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
describe(
|
|
89
|
-
it(
|
|
90
|
-
const className = sheet.getClassName(
|
|
88
|
+
describe('getClassName', () => {
|
|
89
|
+
it('returns a className without injecting', () => {
|
|
90
|
+
const className = sheet.getClassName('color: red;')
|
|
91
91
|
expect(className).toMatch(/^pyr-[0-9a-z]+$/)
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
it(
|
|
95
|
-
const cssText =
|
|
94
|
+
it('returns same className as insert for same CSS', () => {
|
|
95
|
+
const cssText = 'display: flex;'
|
|
96
96
|
const getResult = sheet.getClassName(cssText)
|
|
97
97
|
const insertResult = sheet.insert(cssText)
|
|
98
98
|
expect(getResult).toBe(insertResult)
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
describe(
|
|
103
|
-
it(
|
|
104
|
-
const { className, rules } = sheet.prepare(
|
|
102
|
+
describe('prepare', () => {
|
|
103
|
+
it('returns className and rules', () => {
|
|
104
|
+
const { className, rules } = sheet.prepare('color: red;')
|
|
105
105
|
expect(className).toMatch(/^pyr-[0-9a-z]+$/)
|
|
106
106
|
expect(rules).toContain(className)
|
|
107
|
-
expect(rules).toContain(
|
|
107
|
+
expect(rules).toContain('color: red;')
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it(
|
|
111
|
-
const { className, rules } = sheet.prepare(
|
|
110
|
+
it('supports boost mode', () => {
|
|
111
|
+
const { className, rules } = sheet.prepare('color: red;', true)
|
|
112
112
|
// Boosted selector should have doubled class
|
|
113
113
|
expect(rules).toContain(`.${className}.${className}`)
|
|
114
114
|
})
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
describe(
|
|
118
|
-
it(
|
|
117
|
+
describe('SSR support', () => {
|
|
118
|
+
it('getStyleTag returns a string', () => {
|
|
119
119
|
const result = sheet.getStyleTag()
|
|
120
|
-
expect(typeof result).toBe(
|
|
121
|
-
expect(result).toContain(
|
|
120
|
+
expect(typeof result).toBe('string')
|
|
121
|
+
expect(result).toContain('data-pyreon-styler')
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
-
it(
|
|
124
|
+
it('getStyles returns empty string when no rules inserted', () => {
|
|
125
125
|
const result = sheet.getStyles()
|
|
126
|
-
expect(result).toBe(
|
|
126
|
+
expect(result).toBe('')
|
|
127
127
|
})
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
describe(
|
|
131
|
-
it(
|
|
132
|
-
const cls1 = sheet.insert(
|
|
130
|
+
describe('reset', () => {
|
|
131
|
+
it('clears cache so new inserts re-generate class names', () => {
|
|
132
|
+
const cls1 = sheet.insert('color: green;')
|
|
133
133
|
sheet.reset()
|
|
134
|
-
const cls2 = sheet.insert(
|
|
134
|
+
const cls2 = sheet.insert('color: green;')
|
|
135
135
|
expect(cls1).toBe(cls2)
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
describe(
|
|
140
|
-
it(
|
|
141
|
-
sheet.insert(
|
|
139
|
+
describe('clearCache and clearAll', () => {
|
|
140
|
+
it('clearCache clears the cache', () => {
|
|
141
|
+
sheet.insert('color: red;')
|
|
142
142
|
sheet.clearCache()
|
|
143
143
|
expect(sheet.cacheSize).toBe(0)
|
|
144
144
|
})
|
|
145
145
|
|
|
146
|
-
it(
|
|
147
|
-
sheet.insert(
|
|
146
|
+
it('clearAll clears cache and SSR buffer', () => {
|
|
147
|
+
sheet.insert('color: red;')
|
|
148
148
|
sheet.clearAll()
|
|
149
149
|
expect(sheet.cacheSize).toBe(0)
|
|
150
|
-
expect(sheet.getStyles()).toBe(
|
|
150
|
+
expect(sheet.getStyles()).toBe('')
|
|
151
151
|
})
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
describe(
|
|
155
|
-
it(
|
|
156
|
-
const className = sheet.insert(
|
|
154
|
+
describe('has', () => {
|
|
155
|
+
it('returns true for cached classNames', () => {
|
|
156
|
+
const className = sheet.insert('color: red;')
|
|
157
157
|
expect(sheet.has(className)).toBe(true)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
-
it(
|
|
161
|
-
expect(sheet.has(
|
|
160
|
+
it('returns false for unknown classNames', () => {
|
|
161
|
+
expect(sheet.has('pyr-unknown')).toBe(false)
|
|
162
162
|
})
|
|
163
163
|
})
|
|
164
164
|
})
|