@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,511 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
-
import { sheet } from '../sheet'
|
|
4
|
-
import { styled } from '../styled'
|
|
5
|
-
|
|
6
|
-
describe('styled', () => {
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
sheet.reset()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('basic creation', () => {
|
|
12
|
-
it('returns a tagged template function', () => {
|
|
13
|
-
const tagFn = styled('div')
|
|
14
|
-
expect(typeof tagFn).toBe('function')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('tagged template returns a ComponentFn', () => {
|
|
18
|
-
const Comp = styled('div')`
|
|
19
|
-
display: flex;
|
|
20
|
-
`
|
|
21
|
-
expect(typeof Comp).toBe('function')
|
|
22
|
-
})
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
describe('static CSS (no function interpolations)', () => {
|
|
26
|
-
it('produces a VNode with the correct tag', () => {
|
|
27
|
-
const Comp = styled('div')`
|
|
28
|
-
display: flex;
|
|
29
|
-
`
|
|
30
|
-
const vnode = Comp({}) as VNode
|
|
31
|
-
expect(vnode.type).toBe('div')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('produces a VNode with the correct tag for span', () => {
|
|
35
|
-
const Comp = styled('span')`
|
|
36
|
-
color: red;
|
|
37
|
-
`
|
|
38
|
-
const vnode = Comp({}) as VNode
|
|
39
|
-
expect(vnode.type).toBe('span')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('applies a generated className', () => {
|
|
43
|
-
const Comp = styled('div')`
|
|
44
|
-
display: flex;
|
|
45
|
-
`
|
|
46
|
-
const vnode = Comp({}) as VNode
|
|
47
|
-
expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('same component produces same className across calls', () => {
|
|
51
|
-
const Comp = styled('div')`
|
|
52
|
-
display: flex;
|
|
53
|
-
color: red;
|
|
54
|
-
`
|
|
55
|
-
const vnode1 = Comp({}) as VNode
|
|
56
|
-
const vnode2 = Comp({}) as VNode
|
|
57
|
-
expect(vnode1.props.class).toBe(vnode2.props.class)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('different CSS produces different classNames', () => {
|
|
61
|
-
const Comp1 = styled('div')`
|
|
62
|
-
color: red;
|
|
63
|
-
`
|
|
64
|
-
const Comp2 = styled('div')`
|
|
65
|
-
color: blue;
|
|
66
|
-
`
|
|
67
|
-
const vnode1 = Comp1({}) as VNode
|
|
68
|
-
const vnode2 = Comp2({}) as VNode
|
|
69
|
-
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('empty CSS', () => {
|
|
74
|
-
it('renders element without className for empty template', () => {
|
|
75
|
-
const Comp = styled('div')``
|
|
76
|
-
const vnode = Comp({}) as VNode
|
|
77
|
-
expect(vnode.props.class).toBeFalsy()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('renders element without className for whitespace-only template', () => {
|
|
81
|
-
const Comp = styled('div')``
|
|
82
|
-
const vnode = Comp({}) as VNode
|
|
83
|
-
expect(vnode.props.class).toBeFalsy()
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('static interpolations (non-function)', () => {
|
|
88
|
-
it('treats string interpolations as static', () => {
|
|
89
|
-
const color = 'red'
|
|
90
|
-
const Comp = styled('div')`
|
|
91
|
-
color: ${color};
|
|
92
|
-
`
|
|
93
|
-
const vnode = Comp({}) as VNode
|
|
94
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('treats number interpolations as static', () => {
|
|
98
|
-
const size = 16
|
|
99
|
-
const Comp = styled('div')`
|
|
100
|
-
font-size: ${size}px;
|
|
101
|
-
`
|
|
102
|
-
const vnode = Comp({}) as VNode
|
|
103
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe('generic typed props', () => {
|
|
108
|
-
it('accepts a type parameter for typed interpolation props', () => {
|
|
109
|
-
// The generic provides type-safe access to consumer props inside interpolations
|
|
110
|
-
const Comp = styled('div')<{ $color: string; $size: number }>`
|
|
111
|
-
color: ${(props) => props.$color};
|
|
112
|
-
font-size: ${(props) => props.$size}px;
|
|
113
|
-
`
|
|
114
|
-
const vnode = Comp({ $color: 'red', $size: 14 }) as VNode
|
|
115
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('different typed prop values produce different classes', () => {
|
|
119
|
-
const Comp = styled('div')<{ $variant: 'primary' | 'danger' }>`
|
|
120
|
-
background: ${(props) => (props.$variant === 'primary' ? 'blue' : 'red')};
|
|
121
|
-
`
|
|
122
|
-
const a = Comp({ $variant: 'primary' }) as VNode
|
|
123
|
-
const b = Comp({ $variant: 'danger' }) as VNode
|
|
124
|
-
expect(a.props.class).not.toBe(b.props.class)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('default generic still allows untyped interpolations (back-compat)', () => {
|
|
128
|
-
const Comp = styled('div')`
|
|
129
|
-
color: ${(props: { color?: string }) => props.color || 'black'};
|
|
130
|
-
`
|
|
131
|
-
const vnode = Comp({ color: 'red' }) as VNode
|
|
132
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('dynamic CSS (function interpolations)', () => {
|
|
137
|
-
it('resolves function interpolations with props', () => {
|
|
138
|
-
const Comp = styled('div')`
|
|
139
|
-
color: ${(props: any) => props.color};
|
|
140
|
-
`
|
|
141
|
-
const vnode = Comp({ color: 'red' }) as VNode
|
|
142
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('different prop values produce different classNames', () => {
|
|
146
|
-
const Comp = styled('div')`
|
|
147
|
-
color: ${(props: any) => props.color};
|
|
148
|
-
`
|
|
149
|
-
const vnode1 = Comp({ color: 'red' }) as VNode
|
|
150
|
-
const vnode2 = Comp({ color: 'blue' }) as VNode
|
|
151
|
-
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('same prop values produce same className (dedup)', () => {
|
|
155
|
-
const Comp = styled('div')`
|
|
156
|
-
color: ${(props: any) => props.color};
|
|
157
|
-
`
|
|
158
|
-
const vnode1 = Comp({ color: 'red' }) as VNode
|
|
159
|
-
const vnode2 = Comp({ color: 'red' }) as VNode
|
|
160
|
-
expect(vnode1.props.class).toBe(vnode2.props.class)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('handles functions returning empty string', () => {
|
|
164
|
-
const Comp = styled('div')`
|
|
165
|
-
${() => ''}
|
|
166
|
-
`
|
|
167
|
-
const vnode = Comp({}) as VNode
|
|
168
|
-
expect(vnode.props.class).toBeFalsy()
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('handles functions returning false', () => {
|
|
172
|
-
const Comp = styled('div')`
|
|
173
|
-
${(props: any) => (props.active ? 'color: red;' : false)}
|
|
174
|
-
`
|
|
175
|
-
const vnode = Comp({ active: false }) as VNode
|
|
176
|
-
expect(vnode.props.class).toBeFalsy()
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
describe('className merging', () => {
|
|
181
|
-
it('merges user class with generated className', () => {
|
|
182
|
-
const Comp = styled('div')`
|
|
183
|
-
display: flex;
|
|
184
|
-
`
|
|
185
|
-
const vnode = Comp({ class: 'custom' }) as VNode
|
|
186
|
-
expect(vnode.props.class).toContain('pyr-')
|
|
187
|
-
expect(vnode.props.class).toContain('custom')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('merges user className with generated className', () => {
|
|
191
|
-
const Comp = styled('div')`
|
|
192
|
-
display: flex;
|
|
193
|
-
`
|
|
194
|
-
const vnode = Comp({ className: 'custom' }) as VNode
|
|
195
|
-
expect(vnode.props.class).toContain('pyr-')
|
|
196
|
-
expect(vnode.props.class).toContain('custom')
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('handles class without generated className (empty CSS)', () => {
|
|
200
|
-
const Comp = styled('div')``
|
|
201
|
-
const vnode = Comp({ class: 'custom' }) as VNode
|
|
202
|
-
expect(vnode.props.class).toBe('custom')
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
describe('as prop (polymorphic rendering)', () => {
|
|
207
|
-
it('changes the rendered element type', () => {
|
|
208
|
-
const Comp = styled('div')`
|
|
209
|
-
display: flex;
|
|
210
|
-
`
|
|
211
|
-
const vnode = Comp({ as: 'section' }) as VNode
|
|
212
|
-
expect(vnode.type).toBe('section')
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('renders as button', () => {
|
|
216
|
-
const Comp = styled('div')`
|
|
217
|
-
cursor: pointer;
|
|
218
|
-
`
|
|
219
|
-
const vnode = Comp({ as: 'button' }) as VNode
|
|
220
|
-
expect(vnode.type).toBe('button')
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('defaults to original tag when as is not provided', () => {
|
|
224
|
-
const Comp = styled('span')`
|
|
225
|
-
color: red;
|
|
226
|
-
`
|
|
227
|
-
const vnode = Comp({}) as VNode
|
|
228
|
-
expect(vnode.type).toBe('span')
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
describe('prop filtering (HTML elements)', () => {
|
|
233
|
-
it('forwards valid HTML attributes', () => {
|
|
234
|
-
const Comp = styled('input')`
|
|
235
|
-
display: block;
|
|
236
|
-
`
|
|
237
|
-
const vnode = Comp({ type: 'text', placeholder: 'test' }) as VNode
|
|
238
|
-
expect(vnode.props.type).toBe('text')
|
|
239
|
-
expect(vnode.props.placeholder).toBe('test')
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('forwards data-* attributes', () => {
|
|
243
|
-
const Comp = styled('div')`
|
|
244
|
-
display: flex;
|
|
245
|
-
`
|
|
246
|
-
const vnode = Comp({ 'data-testid': 'hello' }) as VNode
|
|
247
|
-
expect(vnode.props['data-testid']).toBe('hello')
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
it('forwards aria-* attributes', () => {
|
|
251
|
-
const Comp = styled('div')`
|
|
252
|
-
display: flex;
|
|
253
|
-
`
|
|
254
|
-
const vnode = Comp({ 'aria-label': 'world' }) as VNode
|
|
255
|
-
expect(vnode.props['aria-label']).toBe('world')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('forwards event handlers (on* props)', () => {
|
|
259
|
-
const handler = () => {
|
|
260
|
-
/* no-op */
|
|
261
|
-
}
|
|
262
|
-
const Comp = styled('button')`
|
|
263
|
-
cursor: pointer;
|
|
264
|
-
`
|
|
265
|
-
const vnode = Comp({ onClick: handler }) as VNode
|
|
266
|
-
expect(vnode.props.onClick).toBe(handler)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('filters unknown props for HTML elements', () => {
|
|
270
|
-
const Comp = styled('div')`
|
|
271
|
-
display: flex;
|
|
272
|
-
`
|
|
273
|
-
const vnode = Comp({ unknownProp: 'test' }) as VNode
|
|
274
|
-
expect(vnode.props.unknownProp).toBeUndefined()
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('filters $-prefixed transient props', () => {
|
|
278
|
-
const Comp = styled('div')`
|
|
279
|
-
display: flex;
|
|
280
|
-
`
|
|
281
|
-
const vnode = Comp({ $variant: 'primary' }) as VNode
|
|
282
|
-
expect(vnode.props.$variant).toBeUndefined()
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('does not forward class/className as separate props', () => {
|
|
286
|
-
const Comp = styled('div')`
|
|
287
|
-
display: flex;
|
|
288
|
-
`
|
|
289
|
-
const vnode = Comp({ class: 'extra', className: 'another' }) as VNode
|
|
290
|
-
expect(vnode.props.className).toBeUndefined()
|
|
291
|
-
})
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
describe('shouldForwardProp option', () => {
|
|
295
|
-
it('uses custom filter when provided', () => {
|
|
296
|
-
const Comp = styled('div', {
|
|
297
|
-
shouldForwardProp: (prop) => prop !== 'color',
|
|
298
|
-
})`
|
|
299
|
-
display: flex;
|
|
300
|
-
`
|
|
301
|
-
const vnode = Comp({ color: 'red', 'data-testid': 'test' }) as VNode
|
|
302
|
-
expect(vnode.props.color).toBeUndefined()
|
|
303
|
-
expect(vnode.props['data-testid']).toBe('test')
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
it('custom filter controls all prop forwarding', () => {
|
|
307
|
-
const Comp = styled('div', {
|
|
308
|
-
shouldForwardProp: () => false,
|
|
309
|
-
})`
|
|
310
|
-
display: flex;
|
|
311
|
-
`
|
|
312
|
-
const vnode = Comp({ id: 'test', role: 'button' }) as VNode
|
|
313
|
-
expect(vnode.props.id).toBeUndefined()
|
|
314
|
-
expect(vnode.props.role).toBeUndefined()
|
|
315
|
-
})
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
describe('layer option', () => {
|
|
319
|
-
it('accepts layer option without error', () => {
|
|
320
|
-
const Comp = styled('div', { layer: 'rocketstyle' })`
|
|
321
|
-
color: red;
|
|
322
|
-
`
|
|
323
|
-
const vnode = Comp({}) as VNode
|
|
324
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe('children forwarding', () => {
|
|
329
|
-
it('passes single child through', () => {
|
|
330
|
-
const Comp = styled('div')`
|
|
331
|
-
display: flex;
|
|
332
|
-
`
|
|
333
|
-
const vnode = Comp({ children: 'hello' }) as VNode
|
|
334
|
-
expect(vnode.children).toEqual(['hello'])
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
it('passes array children through', () => {
|
|
338
|
-
const Comp = styled('div')`
|
|
339
|
-
display: flex;
|
|
340
|
-
`
|
|
341
|
-
const children = ['a', 'b', 'c']
|
|
342
|
-
const vnode = Comp({ children }) as VNode
|
|
343
|
-
expect(vnode.children).toEqual(['a', 'b', 'c'])
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
it('handles null children', () => {
|
|
347
|
-
const Comp = styled('div')`
|
|
348
|
-
display: flex;
|
|
349
|
-
`
|
|
350
|
-
const vnode = Comp({ children: null }) as VNode
|
|
351
|
-
expect(vnode.children).toEqual([])
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
it('handles undefined children', () => {
|
|
355
|
-
const Comp = styled('div')`
|
|
356
|
-
display: flex;
|
|
357
|
-
`
|
|
358
|
-
const vnode = Comp({}) as VNode
|
|
359
|
-
expect(vnode.children).toEqual([])
|
|
360
|
-
})
|
|
361
|
-
})
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
describe('styled.tag (Proxy)', () => {
|
|
365
|
-
afterEach(() => {
|
|
366
|
-
sheet.reset()
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('styled.div creates a div component', () => {
|
|
370
|
-
const Comp = styled.div`
|
|
371
|
-
color: red;
|
|
372
|
-
`
|
|
373
|
-
const vnode = Comp({}) as VNode
|
|
374
|
-
expect(vnode.type).toBe('div')
|
|
375
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
it('styled.span creates a span component', () => {
|
|
379
|
-
const Comp = styled.span`
|
|
380
|
-
font-size: 16px;
|
|
381
|
-
`
|
|
382
|
-
const vnode = Comp({}) as VNode
|
|
383
|
-
expect(vnode.type).toBe('span')
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it('styled.button creates a button component', () => {
|
|
387
|
-
const Comp = styled.button`
|
|
388
|
-
cursor: pointer;
|
|
389
|
-
`
|
|
390
|
-
const vnode = Comp({}) as VNode
|
|
391
|
-
expect(vnode.type).toBe('button')
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('styled.section creates a section component', () => {
|
|
395
|
-
const Comp = styled.section`
|
|
396
|
-
padding: 20px;
|
|
397
|
-
`
|
|
398
|
-
const vnode = Comp({}) as VNode
|
|
399
|
-
expect(vnode.type).toBe('section')
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
describe('empty-rawProps static VNode cache', () => {
|
|
403
|
-
// Hot path for `<MyStyled />` with no props: pre-built VNode returned
|
|
404
|
-
// from the StaticStyled closure verbatim. Skips `buildProps` + `h()` +
|
|
405
|
-
// children-array construction per render. Mirrors vitus-labs PR #228.
|
|
406
|
-
it('returns the SAME VNode identity across renders when rawProps is empty', () => {
|
|
407
|
-
const Comp = styled('div')`
|
|
408
|
-
color: red;
|
|
409
|
-
`
|
|
410
|
-
const v1 = Comp({}) as VNode
|
|
411
|
-
const v2 = Comp({}) as VNode
|
|
412
|
-
// Same VNode object — proves the pre-built cache fires.
|
|
413
|
-
expect(v1).toBe(v2)
|
|
414
|
-
expect(v1.type).toBe('div')
|
|
415
|
-
expect((v1.props as Record<string, string>).class).toMatch(/^pyr-/)
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('falls through to the full path when ANY prop is provided', () => {
|
|
419
|
-
const Comp = styled('div')`
|
|
420
|
-
color: red;
|
|
421
|
-
`
|
|
422
|
-
const v1 = Comp({}) as VNode
|
|
423
|
-
const v2 = Comp({ 'data-x': '1' }) as VNode
|
|
424
|
-
// Different identity — the second call bypassed the cache because
|
|
425
|
-
// `for (const _k in rawProps) hasExtraProps = true` fires.
|
|
426
|
-
expect(v1).not.toBe(v2)
|
|
427
|
-
// Both still produce the correct className.
|
|
428
|
-
expect((v1.props as Record<string, unknown>).class).toMatch(/^pyr-/)
|
|
429
|
-
expect((v2.props as Record<string, unknown>).class).toMatch(/^pyr-/)
|
|
430
|
-
// Second VNode carries the extra prop forwarded through buildProps.
|
|
431
|
-
expect((v2.props as Record<string, unknown>)['data-x']).toBe('1')
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('falls through to the full path when `as` overrides the tag', () => {
|
|
435
|
-
const Comp = styled('div')`
|
|
436
|
-
color: red;
|
|
437
|
-
`
|
|
438
|
-
const v1 = Comp({}) as VNode
|
|
439
|
-
const v2 = Comp({ as: 'span' }) as VNode
|
|
440
|
-
// `as` is enumerable → `hasExtraProps = true` → bypasses cache.
|
|
441
|
-
// Output tag is the override.
|
|
442
|
-
expect(v2.type).toBe('span')
|
|
443
|
-
expect(v1).not.toBe(v2)
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
it('falls through to the full path when a ref is provided', () => {
|
|
447
|
-
const Comp = styled('div')`
|
|
448
|
-
color: red;
|
|
449
|
-
`
|
|
450
|
-
const refCb = () => {}
|
|
451
|
-
const v1 = Comp({}) as VNode
|
|
452
|
-
const v2 = Comp({ ref: refCb }) as VNode
|
|
453
|
-
// `ref` is enumerable in JS, so `hasExtraProps = true` already fires.
|
|
454
|
-
// The explicit `rawProps.ref == null` guard is defense-in-depth for
|
|
455
|
-
// any future call site that uses Object.defineProperty(rawProps, 'ref',
|
|
456
|
-
// { enumerable: false, ... }) — that shape would otherwise return the
|
|
457
|
-
// cached no-ref VNode and silently drop the user's callback.
|
|
458
|
-
expect(v1).not.toBe(v2)
|
|
459
|
-
})
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
describe('clearAll resets static-component cache', () => {
|
|
463
|
-
// Regression: pre-fix, `staticComponentCache` (WeakMap) and the
|
|
464
|
-
// single-entry hot cache (`_hotStrings` / `_hotTag` / `_hotComponent`)
|
|
465
|
-
// survived `sheet.clearAll()`. After HMR / dev reload, the same
|
|
466
|
-
// template-literal call site re-invoked `styled('div')\`...\`` and got
|
|
467
|
-
// back the SAME ComponentFn instance — but the class name that
|
|
468
|
-
// component returns was deleted from the DOM by `clearAll`. End-user
|
|
469
|
-
// symptom: every hot reload silently broke styles for any static
|
|
470
|
-
// styled component until full page refresh.
|
|
471
|
-
//
|
|
472
|
-
// Fix wires `onSheetClear` so styled.tsx subscribes at module load
|
|
473
|
-
// and resets both caches alongside the sheet.
|
|
474
|
-
it('producing a new component after clearAll, with a fresh class name', () => {
|
|
475
|
-
// First mount: get baseline component + className.
|
|
476
|
-
const tag = 'div'
|
|
477
|
-
const literal: TemplateStringsArray = Object.assign(
|
|
478
|
-
['color: red;'] as unknown as TemplateStringsArray,
|
|
479
|
-
{ raw: ['color: red;'] },
|
|
480
|
-
)
|
|
481
|
-
const Comp1 = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
|
|
482
|
-
const vnode1 = Comp1({}) as VNode
|
|
483
|
-
const class1 = (vnode1.props as Record<string, string>).class
|
|
484
|
-
|
|
485
|
-
// Same call, no clear: hot cache returns the SAME function identity.
|
|
486
|
-
const Comp1Again = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
|
|
487
|
-
expect(Comp1Again).toBe(Comp1)
|
|
488
|
-
|
|
489
|
-
// Clear the sheet (HMR simulation).
|
|
490
|
-
sheet.clearAll()
|
|
491
|
-
|
|
492
|
-
// After clear: same template-literal identity should produce a NEW
|
|
493
|
-
// component (caches were dropped). Its className resolves against
|
|
494
|
-
// the now-empty sheet, so the new className IS re-inserted into
|
|
495
|
-
// the DOM and the class is observable.
|
|
496
|
-
const Comp2 = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
|
|
497
|
-
expect(Comp2).not.toBe(Comp1)
|
|
498
|
-
const vnode2 = Comp2({}) as VNode
|
|
499
|
-
const class2 = (vnode2.props as Record<string, string>).class
|
|
500
|
-
// FNV-1a hashing is content-deterministic, so class names are
|
|
501
|
-
// structurally equal — but the sheet has freshly re-inserted the
|
|
502
|
-
// rule. Asserting non-empty + same format is the load-bearing
|
|
503
|
-
// observation: pre-fix, Comp2 === Comp1 and class2 would also have
|
|
504
|
-
// been `''` if the user had run `clearAll` between insertions
|
|
505
|
-
// (cache stale, sheet empty).
|
|
506
|
-
expect(class1).toMatch(/^pyr-/)
|
|
507
|
-
expect(class2).toMatch(/^pyr-/)
|
|
508
|
-
expect(sheet.has(class2!)).toBe(true)
|
|
509
|
-
})
|
|
510
|
-
})
|
|
511
|
-
})
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @pyreon/core */
|
|
2
|
-
import { h, provide } from '@pyreon/core'
|
|
3
|
-
import { mountInBrowser } from '@pyreon/test-utils/browser'
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
-
import { css } from '../css'
|
|
6
|
-
import { keyframes } from '../keyframes'
|
|
7
|
-
import { sheet } from '../sheet'
|
|
8
|
-
import { styled } from '../styled'
|
|
9
|
-
import { ThemeContext, ThemeProvider } from '../ThemeProvider'
|
|
10
|
-
|
|
11
|
-
// Real-Chromium smoke for @pyreon/styler.
|
|
12
|
-
//
|
|
13
|
-
// happy-dom approximates a stylesheet but does NOT compute actual
|
|
14
|
-
// styles — `getComputedStyle(el).color` returns empty in happy-dom for
|
|
15
|
-
// rules in the injected stylesheet. These tests assert real cascade
|
|
16
|
-
// behavior in Chromium: the generated class is applied AND the browser
|
|
17
|
-
// resolves the rule to the expected computed style.
|
|
18
|
-
//
|
|
19
|
-
// What the suite locks in:
|
|
20
|
-
// 1. `styled('div')` produces a VNode that mounts to a real div with
|
|
21
|
-
// the generated class applied.
|
|
22
|
-
// 2. The CSS rule reaches `document.head` and Chromium computes the
|
|
23
|
-
// style as authored.
|
|
24
|
-
// 3. Reactive interpolations update computed styles when the signal
|
|
25
|
-
// changes (via the same reactive cascade as static text).
|
|
26
|
-
// 4. Different tags (div, span, button) produce distinct elements
|
|
27
|
-
// with the same class infrastructure.
|
|
28
|
-
// 5. `keyframes` template returns an animation name and the rule is
|
|
29
|
-
// registered so the animation can fire.
|
|
30
|
-
|
|
31
|
-
describe('@pyreon/styler in real browser', () => {
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
// Don't remove the styler <style> element — sheet is a singleton
|
|
34
|
-
// with `this.sheet` bound to that element; removing detaches the
|
|
35
|
-
// sheet object and breaks subsequent insertRule calls. Cache is
|
|
36
|
-
// independent and safe to clear.
|
|
37
|
-
sheet.clearCache()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('mounts a styled div with the generated class applied to the DOM', () => {
|
|
41
|
-
const Box = styled('div')`
|
|
42
|
-
display: flex;
|
|
43
|
-
color: rgb(255, 0, 0);
|
|
44
|
-
`
|
|
45
|
-
const { container, unmount } = mountInBrowser(h(Box, { id: 'box' }))
|
|
46
|
-
const el = container.querySelector<HTMLDivElement>('#box')
|
|
47
|
-
expect(el).not.toBeNull()
|
|
48
|
-
expect(el?.tagName.toLowerCase()).toBe('div')
|
|
49
|
-
expect(el?.className).toMatch(/^pyr-/)
|
|
50
|
-
unmount()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('Chromium computes the authored style — real cascade, not just class application', () => {
|
|
54
|
-
const Red = styled('div')`
|
|
55
|
-
color: rgb(255, 0, 0);
|
|
56
|
-
padding: 12px;
|
|
57
|
-
`
|
|
58
|
-
const { container, unmount } = mountInBrowser(h(Red, { id: 'r' }))
|
|
59
|
-
const el = container.querySelector<HTMLElement>('#r')!
|
|
60
|
-
const cs = getComputedStyle(el)
|
|
61
|
-
expect(cs.color).toBe('rgb(255, 0, 0)')
|
|
62
|
-
expect(cs.padding).toBe('12px')
|
|
63
|
-
unmount()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('function interpolation resolves per-render against props (dynamic path)', () => {
|
|
67
|
-
const Dynamic = styled('div')<{ $tone: string }>`
|
|
68
|
-
color: ${(p) => p.$tone};
|
|
69
|
-
`
|
|
70
|
-
const a = mountInBrowser(h(Dynamic, { id: 'a', $tone: 'rgb(0, 128, 0)' }))
|
|
71
|
-
const b = mountInBrowser(h(Dynamic, { id: 'b', $tone: 'rgb(0, 0, 255)' }))
|
|
72
|
-
expect(getComputedStyle(a.container.querySelector<HTMLElement>('#a')!).color).toBe(
|
|
73
|
-
'rgb(0, 128, 0)',
|
|
74
|
-
)
|
|
75
|
-
expect(getComputedStyle(b.container.querySelector<HTMLElement>('#b')!).color).toBe(
|
|
76
|
-
'rgb(0, 0, 255)',
|
|
77
|
-
)
|
|
78
|
-
a.unmount()
|
|
79
|
-
b.unmount()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('different tags produce distinct elements (div, span, button)', () => {
|
|
83
|
-
const D = styled('div')`color: red;`
|
|
84
|
-
const S = styled('span')`color: green;`
|
|
85
|
-
const B = styled('button')`color: blue;`
|
|
86
|
-
const { container, unmount } = mountInBrowser(
|
|
87
|
-
h('div', null, h(D, { id: 'd' }), h(S, { id: 's' }), h(B, { id: 'b' })),
|
|
88
|
-
)
|
|
89
|
-
expect(container.querySelector('#d')?.tagName.toLowerCase()).toBe('div')
|
|
90
|
-
expect(container.querySelector('#s')?.tagName.toLowerCase()).toBe('span')
|
|
91
|
-
expect(container.querySelector('#b')?.tagName.toLowerCase()).toBe('button')
|
|
92
|
-
unmount()
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('ThemeProvider injects a theme readable by themed components', () => {
|
|
96
|
-
const Themed = styled('div')`
|
|
97
|
-
color: ${(p: { theme: { color: string } }) => p.theme.color};
|
|
98
|
-
`
|
|
99
|
-
const { container, unmount } = mountInBrowser(
|
|
100
|
-
h(ThemeProvider, { theme: { color: 'rgb(128, 0, 128)' } }, h(Themed, { id: 't' })),
|
|
101
|
-
)
|
|
102
|
-
const el = container.querySelector<HTMLElement>('#t')!
|
|
103
|
-
expect(getComputedStyle(el).color).toBe('rgb(128, 0, 128)')
|
|
104
|
-
unmount()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('keyframes registers an animation name usable in styled rules', () => {
|
|
108
|
-
const fadeIn = keyframes`
|
|
109
|
-
from { opacity: 0; }
|
|
110
|
-
to { opacity: 1; }
|
|
111
|
-
`
|
|
112
|
-
const name = String(fadeIn)
|
|
113
|
-
expect(name).toMatch(/^pyr-kf-/)
|
|
114
|
-
|
|
115
|
-
const Animated = styled('div')`
|
|
116
|
-
opacity: 1;
|
|
117
|
-
animation: ${name} 50ms forwards;
|
|
118
|
-
`
|
|
119
|
-
const { container, unmount } = mountInBrowser(h(Animated, { id: 'a' }))
|
|
120
|
-
const el = container.querySelector<HTMLElement>('#a')!
|
|
121
|
-
// animation-name is the resolved animation token (Chromium normalizes).
|
|
122
|
-
const cs = getComputedStyle(el)
|
|
123
|
-
expect(cs.animationName).toContain(name)
|
|
124
|
-
unmount()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('css`...` lazy CSSResult interpolates into styled() and Chromium computes the rule', () => {
|
|
128
|
-
// CSSResult is lazy — toString() / nested interpolation triggers
|
|
129
|
-
// resolution. This test asserts a standalone css`` block flows
|
|
130
|
-
// through styled() and reaches the cascade in real Chromium.
|
|
131
|
-
const accent = css`
|
|
132
|
-
color: rgb(123, 200, 50);
|
|
133
|
-
font-weight: 700;
|
|
134
|
-
`
|
|
135
|
-
const Styled = styled('div')`
|
|
136
|
-
${accent}
|
|
137
|
-
padding: 4px;
|
|
138
|
-
`
|
|
139
|
-
const { container, unmount } = mountInBrowser(h(Styled, { id: 'c' }))
|
|
140
|
-
const el = container.querySelector<HTMLElement>('#c')!
|
|
141
|
-
const cs = getComputedStyle(el)
|
|
142
|
-
expect(cs.color).toBe('rgb(123, 200, 50)')
|
|
143
|
-
expect(cs.fontWeight).toBe('700')
|
|
144
|
-
expect(cs.padding).toBe('4px')
|
|
145
|
-
unmount()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('dynamic styled component resolves theme at mount time (static context)', () => {
|
|
149
|
-
// ThemeContext is static — components read the theme once at setup.
|
|
150
|
-
// Theme switching works via mode signal re-rendering the component tree,
|
|
151
|
-
// not via reactive context + per-component effects. This test verifies
|
|
152
|
-
// the static-read contract: component gets the theme provided at mount.
|
|
153
|
-
const theme = { color: 'rgb(200, 0, 0)' }
|
|
154
|
-
|
|
155
|
-
const Themed = styled('div')`
|
|
156
|
-
color: ${(p: Record<string, any>) => p.theme?.color};
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
const Provider = (props: { children?: unknown }) => {
|
|
160
|
-
provide(ThemeContext, () => theme)
|
|
161
|
-
return props.children as never
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const { container, unmount } = mountInBrowser(
|
|
165
|
-
h(Provider, null, h(Themed, { id: 't' })),
|
|
166
|
-
)
|
|
167
|
-
const el = container.querySelector<HTMLElement>('#t')!
|
|
168
|
-
expect(getComputedStyle(el).color).toBe('rgb(200, 0, 0)')
|
|
169
|
-
unmount()
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('writes its CSS rules to a real <style> element under document.head', () => {
|
|
173
|
-
const X = styled('div')`background-color: rgb(10, 20, 30);`
|
|
174
|
-
const { unmount } = mountInBrowser(h(X, { id: 'x' }))
|
|
175
|
-
// Chromium accepts inserted CSS rules — the injected sheet exists
|
|
176
|
-
// and the rule is queryable via document.styleSheets.
|
|
177
|
-
let found = false
|
|
178
|
-
for (const s of Array.from(document.styleSheets)) {
|
|
179
|
-
try {
|
|
180
|
-
for (const rule of Array.from(s.cssRules ?? [])) {
|
|
181
|
-
if (rule.cssText.includes('rgb(10, 20, 30)')) {
|
|
182
|
-
found = true
|
|
183
|
-
break
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
// cross-origin sheets throw — ignore
|
|
188
|
-
}
|
|
189
|
-
if (found) break
|
|
190
|
-
}
|
|
191
|
-
expect(found).toBe(true)
|
|
192
|
-
unmount()
|
|
193
|
-
})
|
|
194
|
-
})
|