@pyreon/rocketstyle 0.11.0 → 0.11.2
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 +14 -12
- package/src/__tests__/attrs.test.ts +190 -0
- package/src/__tests__/chaining.test.ts +86 -0
- package/src/__tests__/collection.test.ts +35 -0
- package/src/__tests__/compose.test.ts +36 -0
- package/src/__tests__/context.test.ts +200 -0
- package/src/__tests__/createLocalProvider.test.ts +248 -0
- package/src/__tests__/dimensions.test.ts +183 -0
- package/src/__tests__/e2e-styler.test.ts +291 -0
- package/src/__tests__/hooks.test.ts +207 -0
- package/src/__tests__/isRocketComponent.test.ts +48 -0
- package/src/__tests__/misc.test.ts +204 -0
- package/src/__tests__/providerConsumer.test.ts +248 -0
- package/src/__tests__/rocketstyleIntegration.test.ts +615 -0
- package/src/__tests__/themeUtils.test.ts +463 -0
- package/src/cache/LocalThemeManager.ts +14 -0
- package/src/cache/index.ts +3 -0
- package/src/constants/booleanTags.ts +32 -0
- package/src/constants/defaultDimensions.ts +23 -0
- package/src/constants/index.ts +44 -0
- package/src/context/context.ts +51 -0
- package/src/context/createLocalProvider.ts +84 -0
- package/src/context/localContext.ts +37 -0
- package/src/hoc/index.ts +3 -0
- package/src/hoc/rocketstyleAttrsHoc.ts +63 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/usePseudoState.ts +79 -0
- package/src/hooks/useTheme.ts +36 -0
- package/src/index.ts +77 -0
- package/src/init.ts +93 -0
- package/src/isRocketComponent.ts +16 -0
- package/src/rocketstyle.ts +320 -0
- package/src/types/attrs.ts +13 -0
- package/src/types/config.ts +48 -0
- package/src/types/configuration.ts +69 -0
- package/src/types/dimensions.ts +106 -0
- package/src/types/hoc.ts +5 -0
- package/src/types/pseudo.ts +19 -0
- package/src/types/rocketComponent.ts +24 -0
- package/src/types/rocketstyle.ts +156 -0
- package/src/types/styles.ts +46 -0
- package/src/types/theme.ts +19 -0
- package/src/types/utils.ts +55 -0
- package/src/utils/attrs.ts +134 -0
- package/src/utils/chaining.ts +58 -0
- package/src/utils/collection.ts +9 -0
- package/src/utils/compose.ts +11 -0
- package/src/utils/dimensions.ts +126 -0
- package/src/utils/statics.ts +44 -0
- package/src/utils/styles.ts +18 -0
- package/src/utils/theme.ts +196 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { popContext, pushContext } from "@pyreon/core"
|
|
2
|
+
import { config } from "@pyreon/ui-core"
|
|
3
|
+
import { context } from "../context/context"
|
|
4
|
+
import rocketstyle from "../init"
|
|
5
|
+
import isRocketComponent from "../isRocketComponent"
|
|
6
|
+
|
|
7
|
+
// Mock styled function that returns the component unchanged
|
|
8
|
+
const mockStyled = (component: any) => {
|
|
9
|
+
const taggedTemplate = (_strings: any, ..._args: any[]) => component
|
|
10
|
+
return taggedTemplate
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const mockCss = (_strings: any, ..._args: any[]) => ""
|
|
14
|
+
|
|
15
|
+
// Store originals to restore later
|
|
16
|
+
const originalStyled = config.styled
|
|
17
|
+
const originalCss = config.css
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
config.init({
|
|
21
|
+
css: mockCss as any,
|
|
22
|
+
styled: mockStyled as any,
|
|
23
|
+
component: "div",
|
|
24
|
+
textComponent: "span",
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
config.styled = originalStyled
|
|
30
|
+
config.css = originalCss
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base component that filters internal props and returns a VNode-like object.
|
|
35
|
+
* In Pyreon, components are plain functions — no forwardRef needed.
|
|
36
|
+
*/
|
|
37
|
+
const BaseComponent: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
38
|
+
type: "div",
|
|
39
|
+
props: rest,
|
|
40
|
+
children,
|
|
41
|
+
key: null,
|
|
42
|
+
$rocketstyle,
|
|
43
|
+
$rocketstate,
|
|
44
|
+
})
|
|
45
|
+
BaseComponent.displayName = "BaseComponent"
|
|
46
|
+
|
|
47
|
+
/** Helper to push a theme context for testing */
|
|
48
|
+
const withThemeContext = (fn: () => any) => {
|
|
49
|
+
pushContext(
|
|
50
|
+
new Map([
|
|
51
|
+
[
|
|
52
|
+
context.id,
|
|
53
|
+
{
|
|
54
|
+
theme: { rootSize: 16 },
|
|
55
|
+
mode: "light",
|
|
56
|
+
isDark: false,
|
|
57
|
+
isLight: true,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
]),
|
|
61
|
+
)
|
|
62
|
+
try {
|
|
63
|
+
return fn()
|
|
64
|
+
} finally {
|
|
65
|
+
popContext()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Helper: call the component and return its output for inspection. */
|
|
70
|
+
const renderProps = (Component: any, props: Record<string, any> = {}) => {
|
|
71
|
+
return withThemeContext(() => {
|
|
72
|
+
const vnode = Component(props) as any
|
|
73
|
+
return vnode?.props ?? vnode
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --------------------------------------------------------
|
|
78
|
+
// rocketstyle factory
|
|
79
|
+
// --------------------------------------------------------
|
|
80
|
+
describe("rocketstyle factory", () => {
|
|
81
|
+
it("creates a component from factory", () => {
|
|
82
|
+
const Button = rocketstyle()({
|
|
83
|
+
name: "TestButton",
|
|
84
|
+
component: BaseComponent,
|
|
85
|
+
})
|
|
86
|
+
expect(Button).toBeDefined()
|
|
87
|
+
expect(typeof Button).toBe("function")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("sets IS_ROCKETSTYLE on the component", () => {
|
|
91
|
+
const Button = rocketstyle()({
|
|
92
|
+
name: "TestButton",
|
|
93
|
+
component: BaseComponent,
|
|
94
|
+
})
|
|
95
|
+
expect(Button.IS_ROCKETSTYLE).toBe(true)
|
|
96
|
+
expect(isRocketComponent(Button)).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("sets displayName on the component", () => {
|
|
100
|
+
const Button = rocketstyle()({
|
|
101
|
+
name: "MyButton",
|
|
102
|
+
component: BaseComponent,
|
|
103
|
+
})
|
|
104
|
+
expect(Button.displayName).toBe("MyButton")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("throws when component is missing", () => {
|
|
108
|
+
expect(() => {
|
|
109
|
+
rocketstyle()({ name: "Test", component: undefined as any })
|
|
110
|
+
}).toThrow("component")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("throws when name is missing", () => {
|
|
114
|
+
expect(() => {
|
|
115
|
+
rocketstyle()({ name: "", component: BaseComponent })
|
|
116
|
+
}).toThrow("name")
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("throws when dimension uses reserved key", () => {
|
|
120
|
+
expect(() => {
|
|
121
|
+
rocketstyle({ dimensions: { attrs: "attrs" } as any })({
|
|
122
|
+
name: "Test",
|
|
123
|
+
component: BaseComponent,
|
|
124
|
+
})
|
|
125
|
+
}).toThrow("invalid")
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("allows custom dimensions", () => {
|
|
129
|
+
const Button = rocketstyle({
|
|
130
|
+
dimensions: { colors: "color", shapes: "shape" },
|
|
131
|
+
})({ name: "CustomButton", component: BaseComponent })
|
|
132
|
+
expect(Button).toBeDefined()
|
|
133
|
+
expect(Button.IS_ROCKETSTYLE).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("defaults useBooleans to true", () => {
|
|
137
|
+
const Button = rocketstyle()({
|
|
138
|
+
name: "Test",
|
|
139
|
+
component: BaseComponent,
|
|
140
|
+
})
|
|
141
|
+
expect(Button).toBeDefined()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------------
|
|
146
|
+
// chaining methods
|
|
147
|
+
// --------------------------------------------------------
|
|
148
|
+
describe("chaining methods", () => {
|
|
149
|
+
const Button: any = rocketstyle()({
|
|
150
|
+
name: "ChainButton",
|
|
151
|
+
component: BaseComponent,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it(".attrs() returns a new component", () => {
|
|
155
|
+
const Enhanced = Button.attrs(() => ({ label: "test" }))
|
|
156
|
+
expect(Enhanced).toBeDefined()
|
|
157
|
+
expect(Enhanced.IS_ROCKETSTYLE).toBe(true)
|
|
158
|
+
expect(Enhanced).not.toBe(Button)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it(".attrs() with priority option", () => {
|
|
162
|
+
const Enhanced = Button.attrs(() => ({ label: "priority" }), {
|
|
163
|
+
priority: true,
|
|
164
|
+
})
|
|
165
|
+
expect(Enhanced).toBeDefined()
|
|
166
|
+
expect(Enhanced.IS_ROCKETSTYLE).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it(".attrs() with filter option", () => {
|
|
170
|
+
const Enhanced = Button.attrs(() => ({ label: "filtered" }), {
|
|
171
|
+
filter: ["internal"],
|
|
172
|
+
})
|
|
173
|
+
expect(Enhanced).toBeDefined()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it(".config() returns a new component", () => {
|
|
177
|
+
const Enhanced = Button.config({ DEBUG: true })
|
|
178
|
+
expect(Enhanced).toBeDefined()
|
|
179
|
+
expect(Enhanced.IS_ROCKETSTYLE).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it(".statics() returns a new component", () => {
|
|
183
|
+
const Enhanced = Button.statics({ customMeta: "value" })
|
|
184
|
+
expect(Enhanced).toBeDefined()
|
|
185
|
+
expect(Enhanced.meta.customMeta).toBe("value")
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it(".theme() returns a new component", () => {
|
|
189
|
+
const Enhanced = Button.theme(() => ({ color: "blue" }))
|
|
190
|
+
expect(Enhanced).toBeDefined()
|
|
191
|
+
expect(Enhanced.IS_ROCKETSTYLE).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it(".styles() returns a new component", () => {
|
|
195
|
+
const Enhanced = Button.styles(() => "color: red;")
|
|
196
|
+
expect(Enhanced).toBeDefined()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it(".compose() returns a new component", () => {
|
|
200
|
+
const hoc = (C: any) => C
|
|
201
|
+
const Enhanced = Button.compose({ myHoc: hoc })
|
|
202
|
+
expect(Enhanced).toBeDefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("supports chaining multiple methods", () => {
|
|
206
|
+
const Enhanced = Button.theme(() => ({ color: "blue" }))
|
|
207
|
+
.attrs(() => ({ label: "test" }))
|
|
208
|
+
.config({ name: "EnhancedButton" })
|
|
209
|
+
.statics({ version: "1.0" })
|
|
210
|
+
|
|
211
|
+
expect(Enhanced.IS_ROCKETSTYLE).toBe(true)
|
|
212
|
+
expect(Enhanced.meta.version).toBe("1.0")
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it(".getStaticDimensions() returns dimension info", () => {
|
|
216
|
+
const Themed = Button.states(() => ({
|
|
217
|
+
primary: { color: "red" },
|
|
218
|
+
secondary: { color: "blue" },
|
|
219
|
+
}))
|
|
220
|
+
|
|
221
|
+
const info = Themed.getStaticDimensions({ rootSize: 16 })
|
|
222
|
+
expect(info.dimensions).toBeDefined()
|
|
223
|
+
expect(info.useBooleans).toBe(true)
|
|
224
|
+
expect(info.multiKeys).toBeDefined()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it(".getDefaultAttrs() evaluates attrs chain", () => {
|
|
228
|
+
const WithAttrs = Button.attrs((props: any) => ({
|
|
229
|
+
label: "default",
|
|
230
|
+
...props,
|
|
231
|
+
}))
|
|
232
|
+
const result = WithAttrs.getDefaultAttrs({}, {}, "light")
|
|
233
|
+
expect(result.label).toBe("default")
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// --------------------------------------------------------
|
|
238
|
+
// rendering
|
|
239
|
+
// --------------------------------------------------------
|
|
240
|
+
describe("rendering", () => {
|
|
241
|
+
it("renders a basic rocketstyle component", () => {
|
|
242
|
+
const Button: any = rocketstyle()({
|
|
243
|
+
name: "RenderButton",
|
|
244
|
+
component: BaseComponent,
|
|
245
|
+
}).config({})
|
|
246
|
+
|
|
247
|
+
const result = renderProps(Button, { children: "Hello" })
|
|
248
|
+
expect(result).toBeDefined()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("adds data-rocketstyle attribute in dev mode", () => {
|
|
252
|
+
const Button: any = rocketstyle()({
|
|
253
|
+
name: "DevButton",
|
|
254
|
+
component: BaseComponent,
|
|
255
|
+
}).config({})
|
|
256
|
+
|
|
257
|
+
const result = renderProps(Button)
|
|
258
|
+
expect(result["data-rocketstyle"]).toBe("DevButton")
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it("renders with attrs defaults", () => {
|
|
262
|
+
const Button: any = rocketstyle()({
|
|
263
|
+
name: "AttrsButton",
|
|
264
|
+
component: BaseComponent,
|
|
265
|
+
}).attrs((() => ({ "data-default": "yes" })) as any)
|
|
266
|
+
|
|
267
|
+
const result = renderProps(Button)
|
|
268
|
+
expect(result["data-default"]).toBe("yes")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("explicit props override attrs", () => {
|
|
272
|
+
const Button: any = rocketstyle()({
|
|
273
|
+
name: "OverrideButton",
|
|
274
|
+
component: BaseComponent,
|
|
275
|
+
}).attrs((() => ({ "data-val": "from-attrs" })) as any)
|
|
276
|
+
|
|
277
|
+
const result = renderProps(Button, { "data-val": "from-props" })
|
|
278
|
+
expect(result["data-val"]).toBe("from-props")
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("renders with theme", () => {
|
|
282
|
+
const Button: any = rocketstyle()({
|
|
283
|
+
name: "ThemedButton",
|
|
284
|
+
component: BaseComponent,
|
|
285
|
+
}).theme(() => ({ fontSize: 14 }))
|
|
286
|
+
|
|
287
|
+
const result = renderProps(Button)
|
|
288
|
+
expect(result).toBeDefined()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("renders with dimension states", () => {
|
|
292
|
+
const Button: any = rocketstyle()({
|
|
293
|
+
name: "StatesButton",
|
|
294
|
+
component: BaseComponent,
|
|
295
|
+
})
|
|
296
|
+
.theme(() => ({ color: "default" }))
|
|
297
|
+
.states(() => ({
|
|
298
|
+
primary: { color: "blue" },
|
|
299
|
+
secondary: { color: "green" },
|
|
300
|
+
}))
|
|
301
|
+
|
|
302
|
+
const result = renderProps(Button, { state: "primary" })
|
|
303
|
+
expect(result).toBeDefined()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it("renders with boolean dimension props", () => {
|
|
307
|
+
const Button: any = rocketstyle()({
|
|
308
|
+
name: "BoolButton",
|
|
309
|
+
component: BaseComponent,
|
|
310
|
+
}).states(() => ({
|
|
311
|
+
primary: { color: "blue" },
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
// boolean prop 'primary' should map to state='primary'
|
|
315
|
+
const result = renderProps(Button, { primary: true })
|
|
316
|
+
expect(result).toBeDefined()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it("renders with priority attrs", () => {
|
|
320
|
+
const Button: any = rocketstyle()({
|
|
321
|
+
name: "PriorityButton",
|
|
322
|
+
component: BaseComponent,
|
|
323
|
+
}).attrs((() => ({ "data-priority": "yes" })) as any, { priority: true })
|
|
324
|
+
|
|
325
|
+
const result = renderProps(Button)
|
|
326
|
+
expect(result["data-priority"]).toBe("yes")
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// --------------------------------------------------------
|
|
331
|
+
// DEBUG option
|
|
332
|
+
// --------------------------------------------------------
|
|
333
|
+
describe("DEBUG option", () => {
|
|
334
|
+
it("calls console.debug when DEBUG is enabled", () => {
|
|
335
|
+
const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {
|
|
336
|
+
/* no-op */
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const Button: any = rocketstyle()({
|
|
340
|
+
name: "DebugButton",
|
|
341
|
+
component: BaseComponent,
|
|
342
|
+
}).config({ DEBUG: true })
|
|
343
|
+
|
|
344
|
+
renderProps(Button)
|
|
345
|
+
expect(debugSpy).toHaveBeenCalledWith(
|
|
346
|
+
"[rocketstyle] DebugButton render:",
|
|
347
|
+
expect.objectContaining({
|
|
348
|
+
component: "DebugButton",
|
|
349
|
+
rocketstate: expect.any(Object),
|
|
350
|
+
rocketstyle: expect.any(Object),
|
|
351
|
+
dimensions: expect.any(Object),
|
|
352
|
+
mode: expect.any(String),
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
debugSpy.mockRestore()
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("does not call console.debug when DEBUG is not set", () => {
|
|
360
|
+
const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {
|
|
361
|
+
/* no-op */
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const Button: any = rocketstyle()({
|
|
365
|
+
name: "NoDebugButton",
|
|
366
|
+
component: BaseComponent,
|
|
367
|
+
}).config({})
|
|
368
|
+
|
|
369
|
+
renderProps(Button)
|
|
370
|
+
expect(debugSpy).not.toHaveBeenCalled()
|
|
371
|
+
|
|
372
|
+
debugSpy.mockRestore()
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// --------------------------------------------------------
|
|
377
|
+
// passProps option
|
|
378
|
+
// --------------------------------------------------------
|
|
379
|
+
describe("passProps option", () => {
|
|
380
|
+
it("passes styling props through when passProps is configured", () => {
|
|
381
|
+
const PassPropsComponent: any = ({
|
|
382
|
+
children,
|
|
383
|
+
$rocketstyle,
|
|
384
|
+
$rocketstate,
|
|
385
|
+
state,
|
|
386
|
+
...rest
|
|
387
|
+
}: any) => ({
|
|
388
|
+
type: "div",
|
|
389
|
+
props: { ...rest, "data-state": state },
|
|
390
|
+
children,
|
|
391
|
+
key: null,
|
|
392
|
+
})
|
|
393
|
+
PassPropsComponent.displayName = "PassPropsComponent"
|
|
394
|
+
|
|
395
|
+
const Button: any = rocketstyle()({
|
|
396
|
+
name: "PassPropsButton",
|
|
397
|
+
component: PassPropsComponent,
|
|
398
|
+
})
|
|
399
|
+
.states(() => ({
|
|
400
|
+
primary: { color: "blue" },
|
|
401
|
+
secondary: { color: "green" },
|
|
402
|
+
}))
|
|
403
|
+
.config({ passProps: ["state"] } as any)
|
|
404
|
+
|
|
405
|
+
const result = renderProps(Button, { state: "primary" })
|
|
406
|
+
expect(result["data-state"]).toBe("primary")
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it("does not pass styling props without passProps", () => {
|
|
410
|
+
const PassPropsComponent: any = ({
|
|
411
|
+
children,
|
|
412
|
+
$rocketstyle,
|
|
413
|
+
$rocketstate,
|
|
414
|
+
state,
|
|
415
|
+
...rest
|
|
416
|
+
}: any) => ({
|
|
417
|
+
type: "div",
|
|
418
|
+
props: { ...rest, "data-state": state ?? "none" },
|
|
419
|
+
children,
|
|
420
|
+
key: null,
|
|
421
|
+
})
|
|
422
|
+
PassPropsComponent.displayName = "NoPassPropsComponent"
|
|
423
|
+
|
|
424
|
+
const Button: any = rocketstyle()({
|
|
425
|
+
name: "NoPassPropsButton",
|
|
426
|
+
component: PassPropsComponent,
|
|
427
|
+
}).states(() => ({
|
|
428
|
+
primary: { color: "blue" },
|
|
429
|
+
}))
|
|
430
|
+
|
|
431
|
+
const result = renderProps(Button, { state: "primary" })
|
|
432
|
+
// Without passProps, the state prop should be filtered out
|
|
433
|
+
expect(result["data-state"]).toBe("none")
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// --------------------------------------------------------
|
|
438
|
+
// IS_ROCKETSTYLE component wrapping
|
|
439
|
+
// --------------------------------------------------------
|
|
440
|
+
describe("IS_ROCKETSTYLE component wrapping", () => {
|
|
441
|
+
it("skips styled() wrapping when component already has IS_ROCKETSTYLE", () => {
|
|
442
|
+
const MarkedComponent: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
443
|
+
type: "div",
|
|
444
|
+
props: rest,
|
|
445
|
+
children,
|
|
446
|
+
key: null,
|
|
447
|
+
})
|
|
448
|
+
MarkedComponent.IS_ROCKETSTYLE = true
|
|
449
|
+
MarkedComponent.displayName = "MarkedComponent"
|
|
450
|
+
|
|
451
|
+
const Outer: any = rocketstyle()({
|
|
452
|
+
name: "OuterComponent",
|
|
453
|
+
component: MarkedComponent,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
expect(Outer).toBeDefined()
|
|
457
|
+
expect(Outer.IS_ROCKETSTYLE).toBe(true)
|
|
458
|
+
expect(Outer.displayName).toBe("OuterComponent")
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it("renders IS_ROCKETSTYLE component when chained with config", () => {
|
|
462
|
+
const MarkedComponent: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
463
|
+
type: "div",
|
|
464
|
+
props: rest,
|
|
465
|
+
children,
|
|
466
|
+
key: null,
|
|
467
|
+
})
|
|
468
|
+
MarkedComponent.IS_ROCKETSTYLE = true
|
|
469
|
+
MarkedComponent.displayName = "MarkedComponent"
|
|
470
|
+
|
|
471
|
+
const Outer: any = rocketstyle()({
|
|
472
|
+
name: "OuterChained",
|
|
473
|
+
component: MarkedComponent,
|
|
474
|
+
}).config({})
|
|
475
|
+
|
|
476
|
+
const result = renderProps(Outer, { children: "Wrapped" })
|
|
477
|
+
expect(result).toBeDefined()
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// --------------------------------------------------------
|
|
482
|
+
// empty dimensions validation
|
|
483
|
+
// --------------------------------------------------------
|
|
484
|
+
describe("empty dimensions validation", () => {
|
|
485
|
+
it("throws when dimensions is an empty object", () => {
|
|
486
|
+
expect(() => {
|
|
487
|
+
rocketstyle({ dimensions: {} as any })({
|
|
488
|
+
name: "EmptyDimensions",
|
|
489
|
+
component: BaseComponent,
|
|
490
|
+
})
|
|
491
|
+
}).toThrow("dimensions")
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// --------------------------------------------------------
|
|
496
|
+
// multiple dimension values
|
|
497
|
+
// --------------------------------------------------------
|
|
498
|
+
describe("multiple dimension values", () => {
|
|
499
|
+
it("renders with array values for multi-key dimensions", () => {
|
|
500
|
+
const Button: any = rocketstyle()({
|
|
501
|
+
name: "MultiButton",
|
|
502
|
+
component: BaseComponent,
|
|
503
|
+
}).multiple(() => ({
|
|
504
|
+
bold: { fontWeight: "bold" },
|
|
505
|
+
italic: { fontStyle: "italic" },
|
|
506
|
+
underline: { textDecoration: "underline" },
|
|
507
|
+
}))
|
|
508
|
+
|
|
509
|
+
const result = renderProps(Button, { multiple: ["bold", "italic"] })
|
|
510
|
+
expect(result).toBeDefined()
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it("renders with single value for non-multi dimensions", () => {
|
|
514
|
+
const Button: any = rocketstyle()({
|
|
515
|
+
name: "SingleDimButton",
|
|
516
|
+
component: BaseComponent,
|
|
517
|
+
})
|
|
518
|
+
.states(() => ({
|
|
519
|
+
primary: { color: "blue" },
|
|
520
|
+
secondary: { color: "green" },
|
|
521
|
+
}))
|
|
522
|
+
.sizes(() => ({
|
|
523
|
+
small: { fontSize: 12 },
|
|
524
|
+
large: { fontSize: 18 },
|
|
525
|
+
}))
|
|
526
|
+
|
|
527
|
+
const result = renderProps(Button, { state: "primary", size: "large" })
|
|
528
|
+
expect(result).toBeDefined()
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it("renders with boolean shorthand for multi-key dimensions", () => {
|
|
532
|
+
const Button: any = rocketstyle()({
|
|
533
|
+
name: "MultiBoolButton",
|
|
534
|
+
component: BaseComponent,
|
|
535
|
+
}).multiple(() => ({
|
|
536
|
+
bold: { fontWeight: "bold" },
|
|
537
|
+
italic: { fontStyle: "italic" },
|
|
538
|
+
}))
|
|
539
|
+
|
|
540
|
+
// Boolean shorthand for multi-key: both bold and italic as boolean props
|
|
541
|
+
const result = renderProps(Button, { bold: true, italic: true })
|
|
542
|
+
expect(result).toBeDefined()
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// --------------------------------------------------------
|
|
547
|
+
// rendering without Provider context
|
|
548
|
+
// --------------------------------------------------------
|
|
549
|
+
describe("rendering without Provider context", () => {
|
|
550
|
+
it("renders component without any Provider (useContext returns default)", () => {
|
|
551
|
+
const Button: any = rocketstyle()({
|
|
552
|
+
name: "NoProviderButton",
|
|
553
|
+
component: BaseComponent,
|
|
554
|
+
}).config({})
|
|
555
|
+
|
|
556
|
+
// Call without any context pushed
|
|
557
|
+
const vnode = Button({ children: "NoCtx" }) as any
|
|
558
|
+
const result = vnode?.props ?? vnode
|
|
559
|
+
expect(result).toBeDefined()
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
// --------------------------------------------------------
|
|
564
|
+
// $rocketstyle and $rocketstate are passed to inner component
|
|
565
|
+
// --------------------------------------------------------
|
|
566
|
+
describe("theme and state injection", () => {
|
|
567
|
+
it("passes $rocketstyle theme to inner component", () => {
|
|
568
|
+
const Receiver: any = ({ $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
569
|
+
type: "div",
|
|
570
|
+
props: rest,
|
|
571
|
+
children: [],
|
|
572
|
+
key: null,
|
|
573
|
+
$rocketstyle,
|
|
574
|
+
$rocketstate,
|
|
575
|
+
})
|
|
576
|
+
Receiver.displayName = "Receiver"
|
|
577
|
+
|
|
578
|
+
const Button: any = rocketstyle()({
|
|
579
|
+
name: "ThemeInjButton",
|
|
580
|
+
component: Receiver,
|
|
581
|
+
})
|
|
582
|
+
.theme(() => ({ color: "blue", bg: "white" }))
|
|
583
|
+
.states(() => ({
|
|
584
|
+
primary: { color: "red" },
|
|
585
|
+
}))
|
|
586
|
+
|
|
587
|
+
const vnode = withThemeContext(() => Button({ state: "primary" }))
|
|
588
|
+
expect(vnode.$rocketstyle).toBeDefined()
|
|
589
|
+
expect(vnode.$rocketstyle.color).toBe("red")
|
|
590
|
+
expect(vnode.$rocketstyle.bg).toBe("white")
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it("passes $rocketstate with active dimensions to inner component", () => {
|
|
594
|
+
const Receiver: any = ({ $rocketstyle, $rocketstate, ...rest }: any) => ({
|
|
595
|
+
type: "div",
|
|
596
|
+
props: rest,
|
|
597
|
+
children: [],
|
|
598
|
+
key: null,
|
|
599
|
+
$rocketstyle,
|
|
600
|
+
$rocketstate,
|
|
601
|
+
})
|
|
602
|
+
Receiver.displayName = "Receiver"
|
|
603
|
+
|
|
604
|
+
const Button: any = rocketstyle()({
|
|
605
|
+
name: "StateInjButton",
|
|
606
|
+
component: Receiver,
|
|
607
|
+
}).states(() => ({
|
|
608
|
+
primary: { color: "blue" },
|
|
609
|
+
}))
|
|
610
|
+
|
|
611
|
+
const vnode = withThemeContext(() => Button({ state: "primary" }))
|
|
612
|
+
expect(vnode.$rocketstate).toBeDefined()
|
|
613
|
+
expect(vnode.$rocketstate.state).toBe("primary")
|
|
614
|
+
})
|
|
615
|
+
})
|