@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,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createSheet } from '../sheet'
|
|
3
|
-
|
|
4
|
-
// Layer 1 of the P0 rocketstyle-collapse slice: the styler injects
|
|
5
|
-
// pre-resolved rule text (captured at build time by the collapse
|
|
6
|
-
// resolver) into the LIVE sheet, idempotently, WITHOUT re-hashing — the
|
|
7
|
-
// class names are already baked into the rules AND into the collapsed
|
|
8
|
-
// _tpl() HTML, so a re-hash would break the contract.
|
|
9
|
-
|
|
10
|
-
describe('StyleSheet.injectRules (real browser)', () => {
|
|
11
|
-
it('inserts pre-resolved rules into the live sheet verbatim (no re-hash)', () => {
|
|
12
|
-
const s = createSheet()
|
|
13
|
-
const before = s.ruleCountForTest()
|
|
14
|
-
s.injectRules(['.pyr-abc123{color:rgb(1,2,3)}'], 'k1')
|
|
15
|
-
expect(s.ruleCountForTest()).toBe(before + 1)
|
|
16
|
-
// Class name is preserved exactly — proves no re-hash happened.
|
|
17
|
-
const probe = document.createElement('div')
|
|
18
|
-
probe.className = 'pyr-abc123'
|
|
19
|
-
document.body.appendChild(probe)
|
|
20
|
-
expect(getComputedStyle(probe).color).toBe('rgb(1, 2, 3)')
|
|
21
|
-
probe.remove()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('is idempotent by key — re-injecting the same bundle adds nothing', () => {
|
|
25
|
-
const s = createSheet()
|
|
26
|
-
s.injectRules(['.pyr-dup{color:red}', '.pyr-dup2{color:blue}'], 'dup')
|
|
27
|
-
const afterFirst = s.ruleCountForTest()
|
|
28
|
-
s.injectRules(['.pyr-dup{color:red}', '.pyr-dup2{color:blue}'], 'dup')
|
|
29
|
-
s.injectRules(['.pyr-dup{color:red}', '.pyr-dup2{color:blue}'], 'dup')
|
|
30
|
-
expect(s.ruleCountForTest()).toBe(afterFirst)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('distinct keys inject independently', () => {
|
|
34
|
-
const s = createSheet()
|
|
35
|
-
const before = s.ruleCountForTest()
|
|
36
|
-
s.injectRules(['.pyr-x{margin:1px}'], 'kx')
|
|
37
|
-
s.injectRules(['.pyr-y{margin:2px}'], 'ky')
|
|
38
|
-
expect(s.ruleCountForTest()).toBe(before + 2)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { styled } from '../styled'
|
|
4
|
-
|
|
5
|
-
describe('style injection (className generation)', () => {
|
|
6
|
-
describe('dynamic styled components', () => {
|
|
7
|
-
it('generates a proper className for dynamic CSS', () => {
|
|
8
|
-
const Comp = styled('div')`
|
|
9
|
-
color: ${(props: any) => props.$color};
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
const vnode = Comp({ $color: 'red' }) as VNode
|
|
13
|
-
|
|
14
|
-
// Class should be present and properly generated
|
|
15
|
-
expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('works with multiple dynamic components', () => {
|
|
19
|
-
const Comp1 = styled('div')`
|
|
20
|
-
color: ${(p: any) => p.$c};
|
|
21
|
-
`
|
|
22
|
-
const Comp2 = styled('span')`
|
|
23
|
-
font-size: ${(p: any) => p.$s};
|
|
24
|
-
`
|
|
25
|
-
|
|
26
|
-
const vnode1 = Comp1({ $c: 'red' }) as VNode
|
|
27
|
-
const vnode2 = Comp2({ $s: '16px' }) as VNode
|
|
28
|
-
|
|
29
|
-
expect(vnode1.props.class).toMatch(/^pyr-/)
|
|
30
|
-
expect(vnode2.props.class).toMatch(/^pyr-/)
|
|
31
|
-
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('handles different prop values producing different classNames', () => {
|
|
35
|
-
const Comp = styled('div')`
|
|
36
|
-
color: ${(p: any) => p.$color};
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
const colors = ['blue', 'green', 'yellow', 'purple', 'orange']
|
|
40
|
-
const classNames = new Set<string>()
|
|
41
|
-
|
|
42
|
-
for (const color of colors) {
|
|
43
|
-
const vnode = Comp({ $color: color }) as VNode
|
|
44
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
45
|
-
classNames.add(vnode.props.class as string)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Different colors should produce different classNames
|
|
49
|
-
expect(classNames.size).toBe(colors.length)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('same dynamic CSS produces same className', () => {
|
|
53
|
-
const Comp = styled('div')`
|
|
54
|
-
color: ${(p: any) => p.$color};
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
const vnode1 = Comp({ $color: 'red' }) as VNode
|
|
58
|
-
const cls1 = vnode1.props.class as string
|
|
59
|
-
|
|
60
|
-
const _vnode2 = Comp({ $color: 'blue' }) as VNode
|
|
61
|
-
|
|
62
|
-
const vnode3 = Comp({ $color: 'red' }) as VNode // back to red
|
|
63
|
-
const cls3 = vnode3.props.class as string
|
|
64
|
-
|
|
65
|
-
// Same resolved CSS -> same className
|
|
66
|
-
expect(cls1).toBe(cls3)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('static styled components', () => {
|
|
71
|
-
it('static components compute class at creation time', () => {
|
|
72
|
-
// Static components compute class at creation time
|
|
73
|
-
const Comp = styled('div')`
|
|
74
|
-
display: flex;
|
|
75
|
-
color: red;
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
const vnode = Comp({}) as VNode
|
|
79
|
-
|
|
80
|
-
expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('static className is stable across calls', () => {
|
|
84
|
-
const Comp = styled('div')`
|
|
85
|
-
display: flex;
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
const vnode1 = Comp({}) as VNode
|
|
89
|
-
const cls1 = vnode1.props.class as string
|
|
90
|
-
|
|
91
|
-
const _vnode2 = Comp({}) as VNode
|
|
92
|
-
const vnode3 = Comp({}) as VNode
|
|
93
|
-
const cls3 = vnode3.props.class as string
|
|
94
|
-
|
|
95
|
-
expect(cls1).toBe(cls3)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('theme-dependent components', () => {
|
|
100
|
-
it('produces different className for different resolved CSS', () => {
|
|
101
|
-
// In Pyreon, theme is accessed via useTheme() inside the component.
|
|
102
|
-
// Without ThemeProvider context, theme is {} (default).
|
|
103
|
-
// We test via direct prop-based dynamic interpolation instead.
|
|
104
|
-
const Comp = styled('div')`
|
|
105
|
-
background: ${(p: any) => p.$bg};
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
const vnode1 = Comp({ $bg: 'white' }) as VNode
|
|
109
|
-
const cls1 = vnode1.props.class as string
|
|
110
|
-
|
|
111
|
-
const vnode2 = Comp({ $bg: 'black' }) as VNode
|
|
112
|
-
const cls2 = vnode2.props.class as string
|
|
113
|
-
|
|
114
|
-
expect(cls1).not.toBe(cls2)
|
|
115
|
-
expect(cls1).toMatch(/^pyr-/)
|
|
116
|
-
expect(cls2).toMatch(/^pyr-/)
|
|
117
|
-
})
|
|
118
|
-
})
|
|
119
|
-
})
|
|
@@ -1,58 +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
|
-
// ─── Integration tests ───────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
describe('Styler integration — styled + sheet', () => {
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
sheet.reset()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('styled("div") with CSS applies a pyr- class to element', () => {
|
|
14
|
-
const Box = styled('div')`
|
|
15
|
-
color: red;
|
|
16
|
-
display: flex;
|
|
17
|
-
`
|
|
18
|
-
const vnode = Box({}) as VNode
|
|
19
|
-
expect(vnode.type).toBe('div')
|
|
20
|
-
expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('dynamic styled with props-based interpolation produces different classes', () => {
|
|
24
|
-
const Box = styled('div')`
|
|
25
|
-
color: ${((props: Record<string, unknown>) => (props.color as string) || 'black') as any};
|
|
26
|
-
`
|
|
27
|
-
const vnode1 = Box({ color: 'red' }) as VNode
|
|
28
|
-
const vnode2 = Box({ color: 'blue' }) as VNode
|
|
29
|
-
// Dynamic interpolations produce different CSS, different hashes
|
|
30
|
-
expect(vnode1.props.class).toMatch(/^pyr-/)
|
|
31
|
-
expect(vnode2.props.class).toMatch(/^pyr-/)
|
|
32
|
-
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('multiple styled components get different classes from same sheet', () => {
|
|
36
|
-
const Red = styled('span')`
|
|
37
|
-
color: red;
|
|
38
|
-
`
|
|
39
|
-
const Blue = styled('span')`
|
|
40
|
-
color: blue;
|
|
41
|
-
`
|
|
42
|
-
const vnodeRed = Red({}) as VNode
|
|
43
|
-
const vnodeBlue = Blue({}) as VNode
|
|
44
|
-
expect(vnodeRed.props.class).toMatch(/^pyr-/)
|
|
45
|
-
expect(vnodeBlue.props.class).toMatch(/^pyr-/)
|
|
46
|
-
expect(vnodeRed.props.class).not.toBe(vnodeBlue.props.class)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('@layer declarations exist in stylesheet (pyreon layer)', () => {
|
|
50
|
-
// The sheet mounts on construction and injects @layer pyreon;
|
|
51
|
-
// Verify the sheet has been mounted and can insert rules
|
|
52
|
-
const className = sheet.insert('display: grid;')
|
|
53
|
-
expect(className).toMatch(/^pyr-/)
|
|
54
|
-
// Same CSS returns same class (dedup works)
|
|
55
|
-
const className2 = sheet.insert('display: grid;')
|
|
56
|
-
expect(className2).toBe(className)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { css } from '../css'
|
|
4
|
-
import { styled } from '../styled'
|
|
5
|
-
|
|
6
|
-
describe('integration', () => {
|
|
7
|
-
describe('nested css results', () => {
|
|
8
|
-
it('resolves nested css tagged templates', () => {
|
|
9
|
-
const flexCSS = css`
|
|
10
|
-
display: flex;
|
|
11
|
-
`
|
|
12
|
-
const Comp = styled('div')`
|
|
13
|
-
${flexCSS}
|
|
14
|
-
color: red;
|
|
15
|
-
`
|
|
16
|
-
const vnode = Comp({}) as VNode
|
|
17
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('resolves conditional css (logical AND pattern)', () => {
|
|
21
|
-
const isWeb = true
|
|
22
|
-
const Comp = styled('div')`
|
|
23
|
-
${isWeb &&
|
|
24
|
-
css`
|
|
25
|
-
box-sizing: border-box;
|
|
26
|
-
`};
|
|
27
|
-
display: flex;
|
|
28
|
-
`
|
|
29
|
-
const vnode = Comp({}) as VNode
|
|
30
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('handles false conditional css', () => {
|
|
34
|
-
const isWeb = false
|
|
35
|
-
const Comp = styled('div')`
|
|
36
|
-
${isWeb &&
|
|
37
|
-
css`
|
|
38
|
-
box-sizing: border-box;
|
|
39
|
-
`};
|
|
40
|
-
display: flex;
|
|
41
|
-
`
|
|
42
|
-
const vnode = Comp({}) as VNode
|
|
43
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('array interpolations (makeItResponsive pattern)', () => {
|
|
48
|
-
it('resolves array of css results (simulating breakpoints)', () => {
|
|
49
|
-
// Simulates what makeItResponsive returns: array of css results per breakpoint
|
|
50
|
-
const breakpointStyles = [
|
|
51
|
-
css`
|
|
52
|
-
color: red;
|
|
53
|
-
`,
|
|
54
|
-
css`
|
|
55
|
-
@media (min-width: 48em) {
|
|
56
|
-
color: blue;
|
|
57
|
-
}
|
|
58
|
-
`,
|
|
59
|
-
css`
|
|
60
|
-
@media (min-width: 62em) {
|
|
61
|
-
color: green;
|
|
62
|
-
}
|
|
63
|
-
`,
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
const Comp = styled('div')`
|
|
67
|
-
display: flex;
|
|
68
|
-
${breakpointStyles};
|
|
69
|
-
`
|
|
70
|
-
const vnode = Comp({}) as VNode
|
|
71
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('resolves function returning array (makeItResponsive full pattern)', () => {
|
|
75
|
-
// makeItResponsive returns a function (props) => CSSResult[]
|
|
76
|
-
const responsiveFn = (props: any) => {
|
|
77
|
-
const theme = props.$element || {}
|
|
78
|
-
return [
|
|
79
|
-
css`
|
|
80
|
-
color: ${theme.color || 'black'};
|
|
81
|
-
`,
|
|
82
|
-
theme.breakpoint
|
|
83
|
-
? css`
|
|
84
|
-
@media (min-width: 48em) {
|
|
85
|
-
color: ${theme.breakpoint};
|
|
86
|
-
}
|
|
87
|
-
`
|
|
88
|
-
: '',
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const Comp = styled('div')`
|
|
93
|
-
display: flex;
|
|
94
|
-
${responsiveFn};
|
|
95
|
-
`
|
|
96
|
-
const vnode = Comp({ $element: { color: 'red', breakpoint: 'blue' } }) as VNode
|
|
97
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('createMediaQueries pattern', () => {
|
|
102
|
-
it('css called as function (css(...args)) wrapping in @media', () => {
|
|
103
|
-
// Simulates createMediaQueries: builds functions that call css(...args)
|
|
104
|
-
const createMedia = (breakpoint: number, rootSize: number) => {
|
|
105
|
-
const emSize = breakpoint / rootSize
|
|
106
|
-
return (...args: any[]) =>
|
|
107
|
-
css`
|
|
108
|
-
@media only screen and (min-width: ${emSize}em) {
|
|
109
|
-
${css(...(args as [TemplateStringsArray, ...any[]]))};
|
|
110
|
-
}
|
|
111
|
-
`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const md = createMedia(768, 16)
|
|
115
|
-
const result = md`
|
|
116
|
-
color: blue;
|
|
117
|
-
`
|
|
118
|
-
|
|
119
|
-
// Wrap in a styled component
|
|
120
|
-
const Comp = styled('div')`
|
|
121
|
-
color: red;
|
|
122
|
-
${result};
|
|
123
|
-
`
|
|
124
|
-
const vnode = Comp({}) as VNode
|
|
125
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('zero-breakpoint passthrough (css(...args) without @media)', () => {
|
|
129
|
-
// Breakpoint 0 means no @media wrapper
|
|
130
|
-
const passthrough = (...args: any[]) => css(...(args as [TemplateStringsArray, ...any[]]))
|
|
131
|
-
|
|
132
|
-
const result = passthrough`color: red;`
|
|
133
|
-
|
|
134
|
-
const Comp = styled('div')`
|
|
135
|
-
${result};
|
|
136
|
-
`
|
|
137
|
-
const vnode = Comp({}) as VNode
|
|
138
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('complex styled patterns', () => {
|
|
143
|
-
it('function interpolation with prop-based conditional', () => {
|
|
144
|
-
const Comp = styled('div')`
|
|
145
|
-
display: flex;
|
|
146
|
-
${({ $contentType }: any) => $contentType === 'content' && 'flex: 1;'};
|
|
147
|
-
`
|
|
148
|
-
const vnode = Comp({ $contentType: 'content' }) as VNode
|
|
149
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('platform-specific CSS (compile-time constant)', () => {
|
|
153
|
-
const __WEB__ = true
|
|
154
|
-
const platformCSS = __WEB__ ? 'box-sizing: border-box;' : ''
|
|
155
|
-
|
|
156
|
-
const Comp = styled('div')`
|
|
157
|
-
${platformCSS};
|
|
158
|
-
display: flex;
|
|
159
|
-
`
|
|
160
|
-
const vnode = Comp({}) as VNode
|
|
161
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('multiple function interpolations', () => {
|
|
165
|
-
const Comp = styled('div')`
|
|
166
|
-
color: ${(p: any) => p.$color || 'black'};
|
|
167
|
-
font-size: ${(p: any) => p.$size || '16px'};
|
|
168
|
-
`
|
|
169
|
-
const vnode = Comp({ $color: 'red', $size: '20px' }) as VNode
|
|
170
|
-
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('empty template produces no className', () => {
|
|
174
|
-
const Comp = styled('div')``
|
|
175
|
-
const vnode = Comp({}) as VNode
|
|
176
|
-
expect(vnode.props.class).toBeFalsy()
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
})
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import { keyframes } from '../keyframes'
|
|
3
|
-
import { sheet } from '../sheet'
|
|
4
|
-
|
|
5
|
-
describe('keyframes', () => {
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
sheet.reset()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns a KeyframesResult with a name property', () => {
|
|
11
|
-
const fadeIn = keyframes`
|
|
12
|
-
from { opacity: 0; }
|
|
13
|
-
to { opacity: 1; }
|
|
14
|
-
`
|
|
15
|
-
expect(fadeIn.name).toMatch(/^pyr-kf-/)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('returns pyr-kf- prefix', () => {
|
|
19
|
-
const fadeIn = keyframes`
|
|
20
|
-
from { opacity: 0; }
|
|
21
|
-
to { opacity: 1; }
|
|
22
|
-
`
|
|
23
|
-
expect(fadeIn.name).toMatch(/^pyr-kf-[0-9a-z]+$/)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('is deterministic — same input produces same name', () => {
|
|
27
|
-
const a = keyframes`from { opacity: 0; } to { opacity: 1; }`
|
|
28
|
-
const b = keyframes`from { opacity: 0; } to { opacity: 1; }`
|
|
29
|
-
expect(a.name).toBe(b.name)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('different input produces different names', () => {
|
|
33
|
-
const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
|
|
34
|
-
const slideIn = keyframes`from { transform: translateX(-100%); } to { transform: translateX(0); }`
|
|
35
|
-
expect(fadeIn.name).not.toBe(slideIn.name)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('supports interpolation values', () => {
|
|
39
|
-
const from = 0
|
|
40
|
-
const to = 1
|
|
41
|
-
const anim = keyframes`
|
|
42
|
-
from { opacity: ${from}; }
|
|
43
|
-
to { opacity: ${to}; }
|
|
44
|
-
`
|
|
45
|
-
expect(anim.name).toMatch(/^pyr-kf-/)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('toString returns the animation name', () => {
|
|
49
|
-
const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
|
|
50
|
-
expect(fadeIn.toString()).toBe(fadeIn.name)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('can be used in template literals for animation property', () => {
|
|
54
|
-
const fadeIn = keyframes`from { opacity: 0; } to { opacity: 1; }`
|
|
55
|
-
const animationValue = `${fadeIn} 0.3s ease-in`
|
|
56
|
-
expect(animationValue).toContain(fadeIn.name)
|
|
57
|
-
expect(animationValue).toContain('0.3s ease-in')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('handles complex keyframe definitions', () => {
|
|
61
|
-
const pulse = keyframes`
|
|
62
|
-
0% { transform: scale(1); }
|
|
63
|
-
50% { transform: scale(1.1); }
|
|
64
|
-
100% { transform: scale(1); }
|
|
65
|
-
`
|
|
66
|
-
expect(pulse.name).toMatch(/^pyr-kf-/)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import { createSheet } from '../sheet'
|
|
3
|
-
|
|
4
|
-
describe('memory growth', () => {
|
|
5
|
-
describe('bounded cache prevents unbounded growth (DOM mode)', () => {
|
|
6
|
-
it('cache stays bounded with maxCacheSize', () => {
|
|
7
|
-
const maxSize = 50
|
|
8
|
-
const s = createSheet({ maxCacheSize: maxSize })
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < maxSize * 3; i++) {
|
|
11
|
-
s.insert(`property-${i}: value-${i};`)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
expect(s.cacheSize).toBeLessThanOrEqual(maxSize * 1.5)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('cache eviction preserves recent entries', () => {
|
|
18
|
-
const maxSize = 20
|
|
19
|
-
const s = createSheet({ maxCacheSize: maxSize })
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < maxSize; i++) {
|
|
22
|
-
s.insert(`old-prop-${i}: old-val-${i};`)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const recentClasses: string[] = []
|
|
26
|
-
for (let i = 0; i < 5; i++) {
|
|
27
|
-
recentClasses.push(s.insert(`new-prop-${i}: new-val-${i};`))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (let i = 0; i < 5; i++) {
|
|
31
|
-
const cls = s.insert(`new-prop-${i}: new-val-${i};`)
|
|
32
|
-
expect(cls).toBe(recentClasses[i])
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('handles rapid insertions without memory issues', () => {
|
|
37
|
-
const s = createSheet({ maxCacheSize: 100 })
|
|
38
|
-
const iterations = 1000
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < iterations; i++) {
|
|
41
|
-
s.insert(`rapid-${i}: value;`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
expect(s.cacheSize).toBeLessThan(iterations)
|
|
45
|
-
expect(s.cacheSize).toBeGreaterThan(0)
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe('default cache (large limit, DOM mode)', () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
52
|
-
el.remove()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('default cache handles many unique rules', () => {
|
|
57
|
-
const s = createSheet()
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < 500; i++) {
|
|
60
|
-
s.insert(`default-prop-${i}: value-${i};`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
expect(s.cacheSize).toBe(500)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('deduplication prevents growth from repeated rules', () => {
|
|
67
|
-
const s = createSheet()
|
|
68
|
-
|
|
69
|
-
for (let cycle = 0; cycle < 100; cycle++) {
|
|
70
|
-
for (let i = 0; i < 10; i++) {
|
|
71
|
-
s.insert(`repeated-${i}: value;`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
expect(s.cacheSize).toBe(10)
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
// Regression: `evictIfNeeded()` historically trimmed ONLY `this.cache`.
|
|
80
|
-
// `insertCache` (keyed by full CSS text — the large keys) and the live
|
|
81
|
-
// `<style>` tag's `cssRules` were never evicted, so `maxCacheSize`
|
|
82
|
-
// bounded the smallest of the three layers while the two memory-heavy
|
|
83
|
-
// ones grew for the process lifetime. These tests inspect the two
|
|
84
|
-
// previously-unbounded layers directly. Bisect-verified.
|
|
85
|
-
describe('lockstep eviction bounds insertCache + DOM cssRules', () => {
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
document.querySelectorAll('style[data-pyreon-styler]').forEach((el) => {
|
|
88
|
-
el.remove()
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('insertCache stays bounded under N >> maxCacheSize unique inserts', () => {
|
|
93
|
-
const maxSize = 50
|
|
94
|
-
const s = createSheet({ maxCacheSize: maxSize })
|
|
95
|
-
const N = maxSize * 6
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < N; i++) s.insert(`prop-${i}: value-${i};`)
|
|
98
|
-
|
|
99
|
-
const ic = (s as unknown as { insertCache: Map<string, string> }).insertCache
|
|
100
|
-
// Pre-fix: ic.size === N (insertCache never evicted). Post-fix it
|
|
101
|
-
// tracks `cache`, which trims oldest 10% on each overflow.
|
|
102
|
-
expect(s.cacheSize).toBeLessThanOrEqual(maxSize * 1.5)
|
|
103
|
-
expect(ic.size).toBeLessThanOrEqual(maxSize * 1.5)
|
|
104
|
-
expect(ic.size).toBeLessThan(N)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('live DOM cssRules count does not grow unbounded', () => {
|
|
108
|
-
const maxSize = 30
|
|
109
|
-
const s = createSheet({ maxCacheSize: maxSize })
|
|
110
|
-
|
|
111
|
-
for (let i = 0; i < maxSize; i++) s.insert(`a-${i}: ${i};`)
|
|
112
|
-
const el = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
113
|
-
expect(el?.sheet).toBeTruthy()
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < maxSize * 5; i++) s.insert(`b-${i}: ${i};`)
|
|
116
|
-
|
|
117
|
-
const after = el.sheet!.cssRules.length
|
|
118
|
-
// Pre-fix: `after` ≈ maxSize*6 (+@layer decl) — every unique insert
|
|
119
|
-
// appended a rule, none ever deleted. Post-fix: eviction calls
|
|
120
|
-
// deleteRule in lockstep, so the live rule count stays within
|
|
121
|
-
// ~1.5× maxSize no matter how many uniques flowed through.
|
|
122
|
-
expect(after).toBeLessThanOrEqual(maxSize * 1.5 + 2)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('dedup still works after eviction cycles', () => {
|
|
126
|
-
const maxSize = 20
|
|
127
|
-
const s = createSheet({ maxCacheSize: maxSize })
|
|
128
|
-
|
|
129
|
-
const recent: string[] = []
|
|
130
|
-
for (let i = 0; i < 5; i++) recent.push(s.insert(`keep-${i}: v;`))
|
|
131
|
-
// Overflow to force eviction of older entries (not `recent`).
|
|
132
|
-
for (let i = 0; i < maxSize * 3; i++) s.insert(`churn-${i}: v;`)
|
|
133
|
-
// Recent entries: re-inserting yields the SAME deterministic
|
|
134
|
-
// className and exactly one live DOM rule each.
|
|
135
|
-
for (let i = 0; i < 5; i++) expect(s.insert(`keep-${i}: v;`)).toBe(recent[i])
|
|
136
|
-
|
|
137
|
-
const el = document.querySelector('style[data-pyreon-styler]') as HTMLStyleElement
|
|
138
|
-
let keepRules = 0
|
|
139
|
-
for (let i = 0; i < el.sheet!.cssRules.length; i++) {
|
|
140
|
-
const r = el.sheet!.cssRules[i]
|
|
141
|
-
if (r && r.cssText.includes('keep-0')) keepRules++
|
|
142
|
-
}
|
|
143
|
-
expect(keepRules).toBe(1)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('SSR mode memory', () => {
|
|
148
|
-
let originalDocument: typeof document
|
|
149
|
-
|
|
150
|
-
beforeEach(() => {
|
|
151
|
-
originalDocument = globalThis.document
|
|
152
|
-
// @ts-expect-error - intentionally deleting for SSR simulation
|
|
153
|
-
delete globalThis.document
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
afterEach(() => {
|
|
157
|
-
globalThis.document = originalDocument
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('reset prevents SSR buffer accumulation across requests', () => {
|
|
161
|
-
const s = createSheet()
|
|
162
|
-
|
|
163
|
-
for (let i = 0; i < 100; i++) {
|
|
164
|
-
s.insert(`req1-prop-${i}: value;`)
|
|
165
|
-
}
|
|
166
|
-
expect(s.getStyles().length).toBeGreaterThan(0)
|
|
167
|
-
|
|
168
|
-
s.reset()
|
|
169
|
-
expect(s.getStyles()).toBe('')
|
|
170
|
-
|
|
171
|
-
s.insert('req2-single: value;')
|
|
172
|
-
expect(s.getStyles()).not.toContain('req1-prop')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('keyframes cache does not grow unboundedly', () => {
|
|
176
|
-
const s = createSheet({ maxCacheSize: 20 })
|
|
177
|
-
|
|
178
|
-
for (let i = 0; i < 50; i++) {
|
|
179
|
-
s.insertKeyframes(`anim-${i}`, `from { opacity: ${i}; } to { opacity: 1; }`)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
expect(s.cacheSize).toBeLessThanOrEqual(50)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('global rules cache does not grow unboundedly', () => {
|
|
186
|
-
const s = createSheet({ maxCacheSize: 20 })
|
|
187
|
-
|
|
188
|
-
for (let i = 0; i < 50; i++) {
|
|
189
|
-
s.insertGlobal(`body { prop${i}: val${i}; }`)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
expect(s.cacheSize).toBeLessThanOrEqual(50)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('SSR buffer grows with unique rules (expected behavior)', () => {
|
|
196
|
-
const s = createSheet()
|
|
197
|
-
const ruleCount = 100
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < ruleCount; i++) {
|
|
200
|
-
s.insert(`ssr-prop-${i}: value;`)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const styles = s.getStyles()
|
|
204
|
-
for (let i = 0; i < ruleCount; i++) {
|
|
205
|
-
expect(styles).toContain(`ssr-prop-${i}`)
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('SSR buffer does not duplicate identical rules', () => {
|
|
210
|
-
const s = createSheet()
|
|
211
|
-
|
|
212
|
-
for (let cycle = 0; cycle < 10; cycle++) {
|
|
213
|
-
s.insert('color: red;')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const matches = s.getStyles().match(/color: red;/g)
|
|
217
|
-
expect(matches).toHaveLength(1)
|
|
218
|
-
})
|
|
219
|
-
})
|
|
220
|
-
})
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { isNativeCompat } from '@pyreon/core'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { ThemeProvider } from '../ThemeProvider'
|
|
4
|
-
|
|
5
|
-
describe('native-compat marker — @pyreon/styler', () => {
|
|
6
|
-
it('ThemeProvider is marked native', () => {
|
|
7
|
-
expect(isNativeCompat(ThemeProvider)).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
})
|