@pyreon/rocketstyle 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 +8 -10
- package/src/__tests__/attrs-overloads.test.ts +0 -97
- package/src/__tests__/attrs.test.ts +0 -190
- package/src/__tests__/cache-key-boolean-collision.test.ts +0 -54
- package/src/__tests__/chaining.test.ts +0 -86
- package/src/__tests__/collection.test.ts +0 -35
- package/src/__tests__/compose.test.ts +0 -36
- package/src/__tests__/context.test.ts +0 -200
- package/src/__tests__/createLocalProvider.test.ts +0 -280
- package/src/__tests__/dimensions.test.ts +0 -183
- package/src/__tests__/e2e-styler.test.ts +0 -299
- package/src/__tests__/hooks.test.ts +0 -178
- package/src/__tests__/isRocketComponent.test.ts +0 -48
- package/src/__tests__/memo-cap.test.ts +0 -174
- package/src/__tests__/minimal-theme.test.ts +0 -62
- package/src/__tests__/misc.test.ts +0 -204
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/providerConsumer.test.ts +0 -183
- package/src/__tests__/reactive-props-preservation.test.ts +0 -195
- package/src/__tests__/rocketstyle.browser.test.tsx +0 -481
- package/src/__tests__/rocketstyleIntegration.test.ts +0 -711
- package/src/__tests__/theme-integration.test.tsx +0 -254
- package/src/__tests__/themeUtils.test.ts +0 -463
- package/src/cache/LocalThemeManager.ts +0 -14
- package/src/cache/index.ts +0 -3
- package/src/constants/booleanTags.ts +0 -32
- package/src/constants/defaultDimensions.ts +0 -23
- package/src/constants/index.ts +0 -59
- package/src/context/context.ts +0 -70
- package/src/context/createLocalProvider.ts +0 -97
- package/src/context/localContext.ts +0 -37
- package/src/env.d.ts +0 -6
- package/src/hoc/index.ts +0 -3
- package/src/hoc/rocketstyleAttrsHoc.ts +0 -76
- package/src/hooks/index.ts +0 -4
- package/src/hooks/usePseudoState.ts +0 -79
- package/src/hooks/useTheme.ts +0 -48
- package/src/index.ts +0 -95
- package/src/init.ts +0 -93
- package/src/isRocketComponent.ts +0 -16
- package/src/rocketstyle.ts +0 -640
- package/src/types/attrs.ts +0 -23
- package/src/types/config.ts +0 -48
- package/src/types/configuration.ts +0 -69
- package/src/types/dimensions.ts +0 -109
- package/src/types/hoc.ts +0 -5
- package/src/types/pseudo.ts +0 -19
- package/src/types/rocketComponent.ts +0 -24
- package/src/types/rocketstyle.ts +0 -220
- package/src/types/styles.ts +0 -61
- package/src/types/theme.ts +0 -18
- package/src/types/utils.ts +0 -98
- package/src/utils/attrs.ts +0 -181
- package/src/utils/chaining.ts +0 -58
- package/src/utils/collection.ts +0 -9
- package/src/utils/compose.ts +0 -11
- package/src/utils/dimensions.ts +0 -126
- package/src/utils/statics.ts +0 -44
- package/src/utils/styles.ts +0 -18
- package/src/utils/theme.ts +0 -211
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* End-to-end test: rocketstyle theme computation pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Tests that rocketstyle components correctly compute theme values
|
|
5
|
-
* through the full chain: base theme → dimension themes → transform modifiers.
|
|
6
|
-
*
|
|
7
|
-
* Unlike the React version which tested CSS injection in the DOM,
|
|
8
|
-
* this Pyreon version tests the computed $rocketstyle output directly.
|
|
9
|
-
*/
|
|
10
|
-
import { ThemeCapture, getComputedTheme, initTestConfig, withThemeContext } from '@pyreon/test-utils'
|
|
11
|
-
import rocketstyle from '../init'
|
|
12
|
-
|
|
13
|
-
let cleanup: () => void
|
|
14
|
-
beforeAll(() => {
|
|
15
|
-
cleanup = initTestConfig()
|
|
16
|
-
})
|
|
17
|
-
afterAll(() => cleanup())
|
|
18
|
-
|
|
19
|
-
describe('e2e: rocketstyle theme computation', () => {
|
|
20
|
-
it('base theme values are passed through', () => {
|
|
21
|
-
const Comp: any = rocketstyle()({
|
|
22
|
-
name: 'BaseComp',
|
|
23
|
-
component: ThemeCapture,
|
|
24
|
-
}).theme({
|
|
25
|
-
backgroundColor: '#0070f3',
|
|
26
|
-
color: '#fff',
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const theme = getComputedTheme(Comp)
|
|
30
|
-
expect(theme.backgroundColor).toBe('#0070f3')
|
|
31
|
-
expect(theme.color).toBe('#fff')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('state dimension overrides base theme values', () => {
|
|
35
|
-
const Comp: any = rocketstyle()({
|
|
36
|
-
name: 'StateComp',
|
|
37
|
-
component: ThemeCapture,
|
|
38
|
-
})
|
|
39
|
-
.theme({
|
|
40
|
-
backgroundColor: '#0070f3',
|
|
41
|
-
color: '#fff',
|
|
42
|
-
})
|
|
43
|
-
.states({
|
|
44
|
-
danger: {
|
|
45
|
-
backgroundColor: '#dc3545',
|
|
46
|
-
color: '#fff',
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const theme = getComputedTheme(Comp, { state: 'danger' })
|
|
51
|
-
expect(theme.backgroundColor).toBe('#dc3545')
|
|
52
|
-
expect(theme.color).toBe('#fff')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('modifier transform derives styles from accumulated state theme', () => {
|
|
56
|
-
const Comp: any = rocketstyle()({
|
|
57
|
-
name: 'ModifierComp',
|
|
58
|
-
component: ThemeCapture,
|
|
59
|
-
})
|
|
60
|
-
.theme({
|
|
61
|
-
backgroundColor: '#0070f3',
|
|
62
|
-
color: '#fff',
|
|
63
|
-
})
|
|
64
|
-
.states({
|
|
65
|
-
danger: {
|
|
66
|
-
backgroundColor: '#dc3545',
|
|
67
|
-
color: '#fff',
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
.modifiers({
|
|
71
|
-
outlined: (accTheme: any) => ({
|
|
72
|
-
color: accTheme.backgroundColor,
|
|
73
|
-
backgroundColor: 'transparent',
|
|
74
|
-
}),
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// danger state + outlined modifier
|
|
78
|
-
const theme = getComputedTheme(Comp, {
|
|
79
|
-
state: 'danger',
|
|
80
|
-
modifier: 'outlined',
|
|
81
|
-
})
|
|
82
|
-
// outlined should flip: color becomes the danger backgroundColor
|
|
83
|
-
expect(theme.color).toBe('#dc3545')
|
|
84
|
-
expect(theme.backgroundColor).toBe('transparent')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('modifier without active state uses base theme only', () => {
|
|
88
|
-
const Comp: any = rocketstyle()({
|
|
89
|
-
name: 'ModifierBaseComp',
|
|
90
|
-
component: ThemeCapture,
|
|
91
|
-
})
|
|
92
|
-
.theme({
|
|
93
|
-
backgroundColor: '#0070f3',
|
|
94
|
-
color: '#fff',
|
|
95
|
-
})
|
|
96
|
-
.modifiers({
|
|
97
|
-
outlined: (accTheme: any) => ({
|
|
98
|
-
color: accTheme.backgroundColor,
|
|
99
|
-
backgroundColor: 'transparent',
|
|
100
|
-
}),
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// just outlined modifier, no state — derive from base theme
|
|
104
|
-
const theme = getComputedTheme(Comp, { modifier: 'outlined' })
|
|
105
|
-
expect(theme.color).toBe('#0070f3')
|
|
106
|
-
expect(theme.backgroundColor).toBe('transparent')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('variant dimension values are applied correctly', () => {
|
|
110
|
-
const Comp: any = rocketstyle()({
|
|
111
|
-
name: 'VariantComp',
|
|
112
|
-
component: ThemeCapture,
|
|
113
|
-
})
|
|
114
|
-
.theme({
|
|
115
|
-
backgroundColor: '#FFFFFF',
|
|
116
|
-
borderRadius: 8,
|
|
117
|
-
})
|
|
118
|
-
.variants({
|
|
119
|
-
box: {
|
|
120
|
-
height: 64,
|
|
121
|
-
padding: 8,
|
|
122
|
-
backgroundColor: 'transparent',
|
|
123
|
-
},
|
|
124
|
-
circle: {
|
|
125
|
-
width: 72,
|
|
126
|
-
height: 72,
|
|
127
|
-
padding: 4,
|
|
128
|
-
backgroundColor: '#F0F0F0',
|
|
129
|
-
borderRadius: 180,
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const theme = getComputedTheme(Comp, { variant: 'circle' })
|
|
134
|
-
expect(theme.width).toBe(72)
|
|
135
|
-
expect(theme.height).toBe(72)
|
|
136
|
-
expect(theme.backgroundColor).toBe('#F0F0F0')
|
|
137
|
-
expect(theme.borderRadius).toBe(180)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('variant box values override base theme', () => {
|
|
141
|
-
const Comp: any = rocketstyle()({
|
|
142
|
-
name: 'VariantBoxComp',
|
|
143
|
-
component: ThemeCapture,
|
|
144
|
-
})
|
|
145
|
-
.theme({
|
|
146
|
-
backgroundColor: '#FFFFFF',
|
|
147
|
-
borderRadius: 8,
|
|
148
|
-
})
|
|
149
|
-
.variants({
|
|
150
|
-
box: {
|
|
151
|
-
height: 64,
|
|
152
|
-
padding: 8,
|
|
153
|
-
backgroundColor: 'transparent',
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
const theme = getComputedTheme(Comp, { variant: 'box' })
|
|
158
|
-
expect(theme.backgroundColor).toBe('transparent')
|
|
159
|
-
expect(theme.borderRadius).toBe(8) // inherited from base
|
|
160
|
-
expect(theme.height).toBe(64)
|
|
161
|
-
expect(theme.padding).toBe(8)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('size dimension values are applied', () => {
|
|
165
|
-
const Comp: any = rocketstyle()({
|
|
166
|
-
name: 'SizeComp',
|
|
167
|
-
component: ThemeCapture,
|
|
168
|
-
})
|
|
169
|
-
.theme({ fontSize: 14 })
|
|
170
|
-
.sizes({
|
|
171
|
-
small: { fontSize: 12, padding: 4 },
|
|
172
|
-
large: { fontSize: 18, padding: 8 },
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const theme = getComputedTheme(Comp, { size: 'large' })
|
|
176
|
-
expect(theme.fontSize).toBe(18)
|
|
177
|
-
expect(theme.padding).toBe(8)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('multiple dimensions combine', () => {
|
|
181
|
-
const Comp: any = rocketstyle()({
|
|
182
|
-
name: 'MultiDimComp',
|
|
183
|
-
component: ThemeCapture,
|
|
184
|
-
})
|
|
185
|
-
.theme({ color: 'black' })
|
|
186
|
-
.states({ primary: { color: 'blue' } })
|
|
187
|
-
.sizes({ large: { fontSize: 18 } })
|
|
188
|
-
|
|
189
|
-
const theme = getComputedTheme(Comp, {
|
|
190
|
-
state: 'primary',
|
|
191
|
-
size: 'large',
|
|
192
|
-
})
|
|
193
|
-
expect(theme.color).toBe('blue')
|
|
194
|
-
expect(theme.fontSize).toBe(18)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('multiple modifier transforms compose sequentially', () => {
|
|
198
|
-
const Comp: any = rocketstyle()({
|
|
199
|
-
name: 'MultiModComp',
|
|
200
|
-
component: ThemeCapture,
|
|
201
|
-
})
|
|
202
|
-
.theme({ backgroundColor: 'blue', color: 'white' })
|
|
203
|
-
.modifiers({
|
|
204
|
-
outlined: (accTheme: any) => ({
|
|
205
|
-
color: accTheme.backgroundColor,
|
|
206
|
-
backgroundColor: 'transparent',
|
|
207
|
-
}),
|
|
208
|
-
rounded: () => ({ borderRadius: '999px' }),
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
const theme = getComputedTheme(Comp, {
|
|
212
|
-
modifier: ['outlined', 'rounded'],
|
|
213
|
-
})
|
|
214
|
-
expect(theme.color).toBe('blue')
|
|
215
|
-
expect(theme.backgroundColor).toBe('transparent')
|
|
216
|
-
expect(theme.borderRadius).toBe('999px')
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('later transform sees earlier transform results', () => {
|
|
220
|
-
const Comp: any = rocketstyle()({
|
|
221
|
-
name: 'ChainedModComp',
|
|
222
|
-
component: ThemeCapture,
|
|
223
|
-
})
|
|
224
|
-
.modifiers({
|
|
225
|
-
first: () => ({ step: 'one' }),
|
|
226
|
-
second: (accTheme: any) => ({ sawStep: accTheme.step }),
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const theme = getComputedTheme(Comp, {
|
|
230
|
-
modifier: ['first', 'second'],
|
|
231
|
-
})
|
|
232
|
-
expect(theme.step).toBe('one')
|
|
233
|
-
expect(theme.sawStep).toBe('one')
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
// ── Reactive dimension props ──────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
describe('reactive $rocketstyle accessor', () => {
|
|
240
|
-
it('$rocketstyleAccessor resolves different themes for different dimension props', () => {
|
|
241
|
-
const Comp: any = rocketstyle()({
|
|
242
|
-
name: 'ReactiveComp',
|
|
243
|
-
component: ThemeCapture,
|
|
244
|
-
})
|
|
245
|
-
.theme({ color: 'black', bg: 'white' })
|
|
246
|
-
.states({
|
|
247
|
-
primary: { color: 'blue' },
|
|
248
|
-
secondary: { color: 'green' },
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
// First call with state=primary
|
|
252
|
-
const theme1 = getComputedTheme(Comp, { state: 'primary' })
|
|
253
|
-
expect(theme1.color).toBe('blue')
|
|
254
|
-
|
|
255
|
-
// Second call with state=secondary — should produce different theme
|
|
256
|
-
const theme2 = getComputedTheme(Comp, { state: 'secondary' })
|
|
257
|
-
expect(theme2.color).toBe('green')
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('$rocketstyleAccessor is a function, not a plain object', () => {
|
|
261
|
-
const Comp: any = rocketstyle()({
|
|
262
|
-
name: 'AccessorComp',
|
|
263
|
-
component: ThemeCapture,
|
|
264
|
-
}).theme({ color: 'red' })
|
|
265
|
-
|
|
266
|
-
const vnode = withThemeContext(() => Comp({}))
|
|
267
|
-
// ThemeCapture resolves the accessor — result should be the theme object
|
|
268
|
-
expect(vnode.$rocketstyle).toBeDefined()
|
|
269
|
-
expect(vnode.$rocketstyle.color).toBe('red')
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('$rocketstateAccessor resolves active dimensions', () => {
|
|
273
|
-
const Comp: any = rocketstyle()({
|
|
274
|
-
name: 'StateAccessorComp',
|
|
275
|
-
component: ThemeCapture,
|
|
276
|
-
}).states({
|
|
277
|
-
primary: { color: 'blue' },
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
const vnode = withThemeContext(() => Comp({ state: 'primary' }))
|
|
281
|
-
expect(vnode.$rocketstate).toBeDefined()
|
|
282
|
-
expect(vnode.$rocketstate.state).toBe('primary')
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('mode change produces different theme via accessor', () => {
|
|
286
|
-
const Comp: any = rocketstyle()({
|
|
287
|
-
name: 'ModeReactiveComp',
|
|
288
|
-
component: ThemeCapture,
|
|
289
|
-
}).theme((t: any, m: any) => ({
|
|
290
|
-
color: m('light-color', 'dark-color'),
|
|
291
|
-
}))
|
|
292
|
-
|
|
293
|
-
const lightTheme = getComputedTheme(Comp, {}, { mode: 'light' })
|
|
294
|
-
expect(lightTheme.color).toBe('light-color')
|
|
295
|
-
|
|
296
|
-
const darkTheme = getComputedTheme(Comp, {}, { mode: 'dark' })
|
|
297
|
-
expect(darkTheme.color).toBe('dark-color')
|
|
298
|
-
})
|
|
299
|
-
})
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { popContext, pushContext } from '@pyreon/core'
|
|
2
|
-
import { buildThemeContextMap } from '@pyreon/test-utils'
|
|
3
|
-
import { localContext, useLocalContext } from '../context/localContext'
|
|
4
|
-
import usePseudoState from '../hooks/usePseudoState'
|
|
5
|
-
import useThemeAttrs from '../hooks/useTheme'
|
|
6
|
-
|
|
7
|
-
describe('usePseudoState', () => {
|
|
8
|
-
it('returns initial state with all false', () => {
|
|
9
|
-
const { state } = usePseudoState({})
|
|
10
|
-
expect(state.hover).toBe(false)
|
|
11
|
-
expect(state.focus).toBe(false)
|
|
12
|
-
expect(state.pressed).toBe(false)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('returns event handlers', () => {
|
|
16
|
-
const { events } = usePseudoState({})
|
|
17
|
-
expect(typeof events.onMouseEnter).toBe('function')
|
|
18
|
-
expect(typeof events.onMouseLeave).toBe('function')
|
|
19
|
-
expect(typeof events.onMouseDown).toBe('function')
|
|
20
|
-
expect(typeof events.onMouseUp).toBe('function')
|
|
21
|
-
expect(typeof events.onFocus).toBe('function')
|
|
22
|
-
expect(typeof events.onBlur).toBe('function')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('sets hover on mouseEnter', () => {
|
|
26
|
-
const { state, events } = usePseudoState({})
|
|
27
|
-
events.onMouseEnter({} as any)
|
|
28
|
-
expect(state.hover).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('clears hover and pressed on mouseLeave', () => {
|
|
32
|
-
const { state, events } = usePseudoState({})
|
|
33
|
-
events.onMouseEnter({} as any)
|
|
34
|
-
events.onMouseDown({} as any)
|
|
35
|
-
expect(state.hover).toBe(true)
|
|
36
|
-
expect(state.pressed).toBe(true)
|
|
37
|
-
|
|
38
|
-
events.onMouseLeave({} as any)
|
|
39
|
-
expect(state.hover).toBe(false)
|
|
40
|
-
expect(state.pressed).toBe(false)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('sets pressed on mouseDown, clears on mouseUp', () => {
|
|
44
|
-
const { state, events } = usePseudoState({})
|
|
45
|
-
events.onMouseDown({} as any)
|
|
46
|
-
expect(state.pressed).toBe(true)
|
|
47
|
-
|
|
48
|
-
events.onMouseUp({} as any)
|
|
49
|
-
expect(state.pressed).toBe(false)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('sets focus on focus, clears on blur', () => {
|
|
53
|
-
const { state, events } = usePseudoState({})
|
|
54
|
-
events.onFocus({} as any)
|
|
55
|
-
expect(state.focus).toBe(true)
|
|
56
|
-
|
|
57
|
-
events.onBlur({} as any)
|
|
58
|
-
expect(state.focus).toBe(false)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('calls user-provided event handlers', () => {
|
|
62
|
-
const onMouseEnter = vi.fn()
|
|
63
|
-
const onMouseLeave = vi.fn()
|
|
64
|
-
const onMouseDown = vi.fn()
|
|
65
|
-
const onMouseUp = vi.fn()
|
|
66
|
-
const onFocus = vi.fn()
|
|
67
|
-
const onBlur = vi.fn()
|
|
68
|
-
|
|
69
|
-
const { events } = usePseudoState({
|
|
70
|
-
onMouseEnter,
|
|
71
|
-
onMouseLeave,
|
|
72
|
-
onMouseDown,
|
|
73
|
-
onMouseUp,
|
|
74
|
-
onFocus,
|
|
75
|
-
onBlur,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const mockEvent = {} as any
|
|
79
|
-
events.onMouseEnter(mockEvent)
|
|
80
|
-
events.onMouseLeave(mockEvent)
|
|
81
|
-
events.onMouseDown(mockEvent)
|
|
82
|
-
events.onMouseUp(mockEvent)
|
|
83
|
-
events.onFocus(mockEvent)
|
|
84
|
-
events.onBlur(mockEvent)
|
|
85
|
-
|
|
86
|
-
expect(onMouseEnter).toHaveBeenCalledWith(mockEvent)
|
|
87
|
-
expect(onMouseLeave).toHaveBeenCalledWith(mockEvent)
|
|
88
|
-
expect(onMouseDown).toHaveBeenCalledWith(mockEvent)
|
|
89
|
-
expect(onMouseUp).toHaveBeenCalledWith(mockEvent)
|
|
90
|
-
expect(onFocus).toHaveBeenCalledWith(mockEvent)
|
|
91
|
-
expect(onBlur).toHaveBeenCalledWith(mockEvent)
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('useThemeAttrs', () => {
|
|
96
|
-
let pushed = false
|
|
97
|
-
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
if (pushed) {
|
|
100
|
-
popContext()
|
|
101
|
-
pushed = false
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('returns default values when no context', () => {
|
|
106
|
-
const result = useThemeAttrs({ inversed: false })
|
|
107
|
-
expect(result.theme).toEqual({})
|
|
108
|
-
expect(result.mode).toBe('light')
|
|
109
|
-
expect(result.isLight).toBe(true)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('reads theme from context', () => {
|
|
113
|
-
pushContext(buildThemeContextMap())
|
|
114
|
-
pushed = true
|
|
115
|
-
|
|
116
|
-
const result = useThemeAttrs({ inversed: false })
|
|
117
|
-
expect(result.theme).toEqual({ rootSize: 16 })
|
|
118
|
-
expect(result.mode).toBe('light')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('inverts mode when inversed is true', () => {
|
|
122
|
-
pushContext(buildThemeContextMap())
|
|
123
|
-
pushed = true
|
|
124
|
-
|
|
125
|
-
const result = useThemeAttrs({ inversed: true })
|
|
126
|
-
expect(result.mode).toBe('dark')
|
|
127
|
-
expect(result.isDark).toBe(true)
|
|
128
|
-
expect(result.isLight).toBe(false)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('inverts dark to light', () => {
|
|
132
|
-
pushContext(
|
|
133
|
-
buildThemeContextMap({
|
|
134
|
-
theme: {},
|
|
135
|
-
mode: 'dark',
|
|
136
|
-
isDark: true,
|
|
137
|
-
isLight: false,
|
|
138
|
-
}),
|
|
139
|
-
)
|
|
140
|
-
pushed = true
|
|
141
|
-
|
|
142
|
-
const result = useThemeAttrs({ inversed: true })
|
|
143
|
-
expect(result.mode).toBe('light')
|
|
144
|
-
expect(result.isDark).toBe(false)
|
|
145
|
-
expect(result.isLight).toBe(true)
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
describe('useLocalContext', () => {
|
|
150
|
-
let pushed = false
|
|
151
|
-
|
|
152
|
-
afterEach(() => {
|
|
153
|
-
if (pushed) {
|
|
154
|
-
popContext()
|
|
155
|
-
pushed = false
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('returns default pseudo when no consumer', () => {
|
|
160
|
-
const result = useLocalContext(null)
|
|
161
|
-
expect(result).toEqual({ pseudo: {} })
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('returns default pseudo when consumer is undefined', () => {
|
|
165
|
-
const result = useLocalContext(undefined)
|
|
166
|
-
expect(result).toEqual({ pseudo: {} })
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('calls consumer with getter function', () => {
|
|
170
|
-
pushContext(new Map([[localContext.id, { pseudo: { hover: true } }]]))
|
|
171
|
-
pushed = true
|
|
172
|
-
|
|
173
|
-
const consumer = (getter: any) => getter((ctx: any) => ({ myPseudo: ctx.pseudo }))
|
|
174
|
-
|
|
175
|
-
const result = useLocalContext(consumer)
|
|
176
|
-
expect(result.myPseudo).toEqual({ hover: true })
|
|
177
|
-
})
|
|
178
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import isRocketComponent from '../isRocketComponent'
|
|
2
|
-
|
|
3
|
-
describe('isRocketComponent', () => {
|
|
4
|
-
it('returns true for object with IS_ROCKETSTYLE property', () => {
|
|
5
|
-
const component = { IS_ROCKETSTYLE: true }
|
|
6
|
-
expect(isRocketComponent(component)).toBe(true)
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('returns false for plain object without IS_ROCKETSTYLE', () => {
|
|
10
|
-
expect(isRocketComponent({})).toBe(false)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('returns false for null', () => {
|
|
14
|
-
expect(isRocketComponent(null)).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('returns false for undefined', () => {
|
|
18
|
-
expect(isRocketComponent(undefined)).toBe(false)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('returns false for primitives', () => {
|
|
22
|
-
expect(isRocketComponent('string')).toBe(false)
|
|
23
|
-
expect(isRocketComponent(42)).toBe(false)
|
|
24
|
-
expect(isRocketComponent(true)).toBe(false)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('returns false for plain functions without IS_ROCKETSTYLE', () => {
|
|
28
|
-
expect(
|
|
29
|
-
isRocketComponent(() => {
|
|
30
|
-
/* no-op */
|
|
31
|
-
}),
|
|
32
|
-
).toBe(false)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('returns true for functions with IS_ROCKETSTYLE', () => {
|
|
36
|
-
const fn = () => {
|
|
37
|
-
/* no-op */
|
|
38
|
-
}
|
|
39
|
-
;(fn as any).IS_ROCKETSTYLE = true
|
|
40
|
-
expect(isRocketComponent(fn)).toBe(true)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('returns true even if IS_ROCKETSTYLE is falsy', () => {
|
|
44
|
-
// hasOwnProperty check only, doesn't check truthiness
|
|
45
|
-
const component = { IS_ROCKETSTYLE: false }
|
|
46
|
-
expect(isRocketComponent(component)).toBe(true)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression: `_rsMemo` LRU cap was 32, which thrashed on real-world
|
|
3
|
-
* high-cardinality workloads (data tables with state derived from row
|
|
4
|
-
* data, design systems with many tokens × axes, dashboards rendering many
|
|
5
|
-
* small interactive components). The rs-precompute spike (closed PR #761,
|
|
6
|
-
* branch `spike/rocketstyle-precompute`) bisect-verified that a 60-unique-
|
|
7
|
-
* tuple Button mount loop had 45% cache-miss rate at cap=32 (888 out of
|
|
8
|
-
* 2000 lookups were cold resolves) and 46% wall-clock regression vs the
|
|
9
|
-
* cap-fits-workload case.
|
|
10
|
-
*
|
|
11
|
-
* Fix: raise `RS_MEMO_CAP` from 32 to 128. Memory cost ~12KB per
|
|
12
|
-
* definition per theme (128 × ~100 bytes) — negligible vs the wall-clock
|
|
13
|
-
* win.
|
|
14
|
-
*
|
|
15
|
-
* This test locks the cap behavior via the counter contract: with N=64
|
|
16
|
-
* unique state variants (above the OLD cap of 32, below the NEW cap of
|
|
17
|
-
* 128), a two-pass cold-then-warm render loop must have ZERO cold
|
|
18
|
-
* resolves on the warm pass. Pre-fix (cap=32): the warm pass would
|
|
19
|
-
* re-cold-resolve any tuples the first pass had evicted (the oldest 32
|
|
20
|
-
* of the 64). Post-fix (cap=128): all 64 fit, second pass is fully
|
|
21
|
-
* cached.
|
|
22
|
-
*
|
|
23
|
-
* Bisect verification (run manually before merging):
|
|
24
|
-
* 1. Revert `RS_MEMO_CAP` to 32 in rocketstyle.ts
|
|
25
|
-
* 2. Run this test — both warm-pass assertions fail with "expected N to be 0"
|
|
26
|
-
* 3. Restore cap to 128
|
|
27
|
-
* 4. Run test — both pass
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { initTestConfig, withThemeContext } from '@pyreon/test-utils'
|
|
31
|
-
import rocketstyle from '../init'
|
|
32
|
-
|
|
33
|
-
let cleanup: () => void
|
|
34
|
-
beforeAll(() => {
|
|
35
|
-
cleanup = initTestConfig()
|
|
36
|
-
})
|
|
37
|
-
afterAll(() => cleanup())
|
|
38
|
-
|
|
39
|
-
// Lightweight counter sink — rocketstyle emits via `globalThis.__pyreon_count__`
|
|
40
|
-
// without an import dep on @pyreon/perf-harness. We install our own sink to
|
|
41
|
-
// observe the cold-resolve count.
|
|
42
|
-
interface CounterGlobal {
|
|
43
|
-
__pyreon_count__?: ((name: string) => void) | undefined
|
|
44
|
-
}
|
|
45
|
-
function installCounter(): { snapshot: () => Record<string, number>; reset: () => void; uninstall: () => void } {
|
|
46
|
-
const counts: Record<string, number> = {}
|
|
47
|
-
const prev = (globalThis as CounterGlobal).__pyreon_count__
|
|
48
|
-
;(globalThis as CounterGlobal).__pyreon_count__ = (name: string) => {
|
|
49
|
-
counts[name] = (counts[name] ?? 0) + 1
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
snapshot: () => ({ ...counts }),
|
|
53
|
-
reset: () => {
|
|
54
|
-
for (const k of Object.keys(counts)) delete counts[k]
|
|
55
|
-
},
|
|
56
|
-
uninstall: () => {
|
|
57
|
-
;(globalThis as CounterGlobal).__pyreon_count__ = prev
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Capture the rocketstyle theme accessor's resolved value so a render
|
|
63
|
-
// actually invokes `_resolveRsEntry` (the function that emits the
|
|
64
|
-
// `rocketstyle.getTheme` counter on cache miss and
|
|
65
|
-
// `rocketstyle.dimensionMemo.hit` on cache hit).
|
|
66
|
-
const ThemeCapture: any = ({ $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
67
|
-
type: 'div',
|
|
68
|
-
props: rest,
|
|
69
|
-
$rocketstyle: typeof $rocketstyle === 'function' ? $rocketstyle() : $rocketstyle,
|
|
70
|
-
$rocketstate: typeof $rocketstate === 'function' ? $rocketstate() : $rocketstate,
|
|
71
|
-
})
|
|
72
|
-
ThemeCapture.displayName = 'ThemeCapture'
|
|
73
|
-
|
|
74
|
-
// Build N state variants in a single rocketstyle component. Each render
|
|
75
|
-
// with a different `state` prop produces a different memo key.
|
|
76
|
-
function makeHighCardinalityComponent(N: number): any {
|
|
77
|
-
const states: Record<string, { color: string }> = {}
|
|
78
|
-
for (let i = 0; i < N; i++) {
|
|
79
|
-
states[`s${i}`] = { color: `rgb(${i % 256}, 0, 0)` }
|
|
80
|
-
}
|
|
81
|
-
return rocketstyle()({
|
|
82
|
-
name: `HighCard${N}`,
|
|
83
|
-
component: ThemeCapture,
|
|
84
|
-
}).states(() => states)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe('rocketstyle — _rsMemo LRU cap (regression PR #762)', () => {
|
|
88
|
-
it('warm pass over 64 unique tuples has ZERO cold resolves (cap >= 64)', () => {
|
|
89
|
-
const N = 64
|
|
90
|
-
const Comp: any = makeHighCardinalityComponent(N)
|
|
91
|
-
const counter = installCounter()
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
// Cold pass — fills the memo with N entries.
|
|
95
|
-
for (let i = 0; i < N; i++) {
|
|
96
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
97
|
-
}
|
|
98
|
-
const afterCold = counter.snapshot()
|
|
99
|
-
const coldGetTheme = afterCold['rocketstyle.getTheme'] ?? 0
|
|
100
|
-
// Sanity: cold pass must have ~N resolves (one per unique state).
|
|
101
|
-
expect(coldGetTheme).toBeGreaterThanOrEqual(N)
|
|
102
|
-
|
|
103
|
-
// Warm pass — same N tuples, in same order. With cap >= N, every
|
|
104
|
-
// lookup hits cache. Pre-fix (cap=32, N=64): the cold pass filled
|
|
105
|
-
// the memo and evicted the oldest 32 entries (entries 0..31),
|
|
106
|
-
// leaving entries 32..63 cached. The warm pass would re-cold-
|
|
107
|
-
// resolve entries 0..31 → 32 cold resolves. Post-fix (cap=128):
|
|
108
|
-
// 0 cold resolves on the warm pass.
|
|
109
|
-
counter.reset()
|
|
110
|
-
for (let i = 0; i < N; i++) {
|
|
111
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
112
|
-
}
|
|
113
|
-
const afterWarm = counter.snapshot()
|
|
114
|
-
const warmColdGetTheme = afterWarm['rocketstyle.getTheme'] ?? 0
|
|
115
|
-
expect(warmColdGetTheme).toBe(0)
|
|
116
|
-
} finally {
|
|
117
|
-
counter.uninstall()
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('warm pass over 100 unique tuples has ZERO cold resolves (cap = 128)', () => {
|
|
122
|
-
// Second probe at the cap's effective boundary: 100 tuples is comfortably
|
|
123
|
-
// within the cap=128 limit. If the cap were anything less than 100,
|
|
124
|
-
// this would fail. The 64-vs-100 split lets a future bump (e.g. to 256)
|
|
125
|
-
// be detected at the boundary without rewriting tests.
|
|
126
|
-
const N = 100
|
|
127
|
-
const Comp: any = makeHighCardinalityComponent(N)
|
|
128
|
-
const counter = installCounter()
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
for (let i = 0; i < N; i++) {
|
|
132
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
133
|
-
}
|
|
134
|
-
const afterCold = counter.snapshot()
|
|
135
|
-
expect(afterCold['rocketstyle.getTheme'] ?? 0).toBeGreaterThanOrEqual(N)
|
|
136
|
-
|
|
137
|
-
counter.reset()
|
|
138
|
-
for (let i = 0; i < N; i++) {
|
|
139
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
140
|
-
}
|
|
141
|
-
const afterWarm = counter.snapshot()
|
|
142
|
-
expect(afterWarm['rocketstyle.getTheme'] ?? 0).toBe(0)
|
|
143
|
-
} finally {
|
|
144
|
-
counter.uninstall()
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('cap still bounds growth — workload of 200 tuples DOES evict (cap < 200)', () => {
|
|
149
|
-
// Control: at workload > cap, the LRU SHOULD evict. This guards against
|
|
150
|
-
// an accidental "remove the cap entirely" change that would let the
|
|
151
|
-
// memo grow unbounded.
|
|
152
|
-
const N = 200
|
|
153
|
-
const Comp: any = makeHighCardinalityComponent(N)
|
|
154
|
-
const counter = installCounter()
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
for (let i = 0; i < N; i++) {
|
|
158
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
159
|
-
}
|
|
160
|
-
counter.reset()
|
|
161
|
-
// Re-render in same order: with cap=128, the cold pass kept entries
|
|
162
|
-
// 72..199 (last 128 of the 200), so the warm pass will re-resolve
|
|
163
|
-
// entries 0..71 → 72 cold resolves. Some non-zero number must
|
|
164
|
-
// appear; the exact value depends on insertion order.
|
|
165
|
-
for (let i = 0; i < N; i++) {
|
|
166
|
-
withThemeContext(() => Comp({ state: `s${i}` }))
|
|
167
|
-
}
|
|
168
|
-
const afterWarm = counter.snapshot()
|
|
169
|
-
expect(afterWarm['rocketstyle.getTheme'] ?? 0).toBeGreaterThan(0)
|
|
170
|
-
} finally {
|
|
171
|
-
counter.uninstall()
|
|
172
|
-
}
|
|
173
|
-
})
|
|
174
|
-
})
|