@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.
Files changed (47) hide show
  1. package/package.json +5 -7
  2. package/src/ThemeProvider.ts +0 -65
  3. package/src/__tests__/ThemeProvider.test.ts +0 -67
  4. package/src/__tests__/benchmark.bench.ts +0 -200
  5. package/src/__tests__/composition-chain.test.ts +0 -537
  6. package/src/__tests__/css.test.ts +0 -70
  7. package/src/__tests__/dev-gate-treeshake.test.ts +0 -85
  8. package/src/__tests__/forward.test.ts +0 -282
  9. package/src/__tests__/globalStyle.test.ts +0 -72
  10. package/src/__tests__/hash.test.ts +0 -70
  11. package/src/__tests__/hybrid-injection.test.ts +0 -225
  12. package/src/__tests__/index.ts +0 -14
  13. package/src/__tests__/inject-rules.browser.test.ts +0 -40
  14. package/src/__tests__/insertion-effect.test.ts +0 -119
  15. package/src/__tests__/integration-dom.test.ts +0 -58
  16. package/src/__tests__/integration.test.ts +0 -179
  17. package/src/__tests__/keyframes.test.ts +0 -68
  18. package/src/__tests__/memory-growth.test.ts +0 -220
  19. package/src/__tests__/native-marker.test.ts +0 -9
  20. package/src/__tests__/p3-features.test.ts +0 -316
  21. package/src/__tests__/resolve-cache.test.ts +0 -94
  22. package/src/__tests__/resolve.test.ts +0 -308
  23. package/src/__tests__/shared.test.ts +0 -133
  24. package/src/__tests__/sheet-advanced.test.ts +0 -659
  25. package/src/__tests__/sheet-split-atrules.test.ts +0 -410
  26. package/src/__tests__/sheet.test.ts +0 -250
  27. package/src/__tests__/static-styler-resolve-cost.test.ts +0 -160
  28. package/src/__tests__/styled-reactive.test.ts +0 -74
  29. package/src/__tests__/styled-ssr.test.ts +0 -75
  30. package/src/__tests__/styled.test.ts +0 -511
  31. package/src/__tests__/styler.browser.test.tsx +0 -194
  32. package/src/__tests__/theme.test.ts +0 -33
  33. package/src/__tests__/useCSS.test.ts +0 -172
  34. package/src/css.ts +0 -13
  35. package/src/env.d.ts +0 -6
  36. package/src/forward.ts +0 -308
  37. package/src/globalStyle.ts +0 -53
  38. package/src/hash.ts +0 -28
  39. package/src/index.ts +0 -15
  40. package/src/keyframes.ts +0 -36
  41. package/src/manifest.ts +0 -332
  42. package/src/resolve.ts +0 -225
  43. package/src/shared.ts +0 -22
  44. package/src/sheet.ts +0 -635
  45. package/src/styled.tsx +0 -503
  46. package/src/tests/manifest-snapshot.test.ts +0 -51
  47. package/src/useCSS.ts +0 -20
@@ -1,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
- })