@pyreon/attrs 0.11.1 → 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 +6 -5
- package/src/__tests__/attrs.test.ts +488 -0
- package/src/__tests__/attrsHoc.test.ts +136 -0
- package/src/__tests__/utils.test.ts +241 -0
- package/src/attrs.ts +126 -0
- package/src/hoc/attrsHoc.ts +60 -0
- package/src/hoc/index.ts +3 -0
- package/src/index.ts +26 -0
- package/src/init.ts +59 -0
- package/src/isAttrsComponent.ts +16 -0
- package/src/types/AttrsComponent.ts +83 -0
- package/src/types/InitAttrsComponent.ts +19 -0
- package/src/types/attrs.ts +2 -0
- package/src/types/config.ts +13 -0
- package/src/types/configuration.ts +40 -0
- package/src/types/hoc.ts +10 -0
- package/src/types/utils.ts +106 -0
- package/src/utils/attrs.ts +40 -0
- package/src/utils/chaining.ts +21 -0
- package/src/utils/collection.ts +14 -0
- package/src/utils/compose.ts +14 -0
- package/src/utils/statics.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/attrs",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/pyreon/pyreon",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"!lib/**/*.map",
|
|
25
25
|
"!lib/analysis",
|
|
26
26
|
"README.md",
|
|
27
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"src"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">= 22"
|
|
@@ -43,11 +44,11 @@
|
|
|
43
44
|
"typecheck": "tsc --noEmit"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
|
-
"@pyreon/core": "^0.11.
|
|
47
|
-
"@pyreon/ui-core": "^0.11.
|
|
47
|
+
"@pyreon/core": "^0.11.2",
|
|
48
|
+
"@pyreon/ui-core": "^0.11.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@vitus-labs/tools-rolldown": "^1.15.3",
|
|
51
|
-
"@pyreon/typescript": "^0.11.
|
|
52
|
+
"@pyreon/typescript": "^0.11.2"
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import attrsComponent from "../attrs"
|
|
2
|
+
import attrs from "../init"
|
|
3
|
+
import isAttrsComponent from "../isAttrsComponent"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Simple base component for testing.
|
|
7
|
+
* Returns a VNode-like object so we can inspect the final props.
|
|
8
|
+
*/
|
|
9
|
+
const BaseComponent = (props: any) => ({
|
|
10
|
+
type: "div",
|
|
11
|
+
props: { ...props, "data-testid": "base" },
|
|
12
|
+
children: props.children ?? props.label ?? null,
|
|
13
|
+
key: null,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/** Helper: call the component and return its output for inspection. */
|
|
17
|
+
const renderProps = (Component: any, props: Record<string, any> = {}) => {
|
|
18
|
+
const vnode = Component(props) as any
|
|
19
|
+
return vnode?.props ?? vnode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --------------------------------------------------------
|
|
23
|
+
// attrs() initialization
|
|
24
|
+
// --------------------------------------------------------
|
|
25
|
+
describe("attrs initialization", () => {
|
|
26
|
+
it("should create an attrs component from a base component", () => {
|
|
27
|
+
const Component = attrs({ name: "TestComponent", component: BaseComponent })
|
|
28
|
+
expect(Component).toBeDefined()
|
|
29
|
+
expect(Component.IS_ATTRS).toBe(true)
|
|
30
|
+
expect(Component.displayName).toBe("TestComponent")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should throw when component is missing (dev mode)", () => {
|
|
34
|
+
expect(() => attrs({ name: "Test", component: undefined as any })).toThrow()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("should throw when name is missing (dev mode)", () => {
|
|
38
|
+
expect(() => attrs({ name: undefined as any, component: BaseComponent })).toThrow()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should render the wrapped component", () => {
|
|
42
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
43
|
+
const result = renderProps(Component, { label: "Hello" })
|
|
44
|
+
expect(result.label).toBe("Hello")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("should add data-attrs in development mode", () => {
|
|
48
|
+
const Component = attrs({ name: "MyComponent", component: BaseComponent })
|
|
49
|
+
const result = renderProps(Component)
|
|
50
|
+
expect(result["data-attrs"]).toBe("MyComponent")
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// --------------------------------------------------------
|
|
55
|
+
// .attrs() chaining
|
|
56
|
+
// --------------------------------------------------------
|
|
57
|
+
describe(".attrs() chaining", () => {
|
|
58
|
+
it("should apply default attrs to the component", () => {
|
|
59
|
+
const Component = attrs({
|
|
60
|
+
name: "Test",
|
|
61
|
+
component: BaseComponent,
|
|
62
|
+
}).attrs(() => ({ label: "Default Label" }))
|
|
63
|
+
|
|
64
|
+
const result = renderProps(Component)
|
|
65
|
+
expect(result.label).toBe("Default Label")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should allow props to override default attrs", () => {
|
|
69
|
+
const Component = attrs({
|
|
70
|
+
name: "Test",
|
|
71
|
+
component: BaseComponent,
|
|
72
|
+
}).attrs(() => ({ label: "Default" }))
|
|
73
|
+
|
|
74
|
+
const result = renderProps(Component, { label: "Override" })
|
|
75
|
+
expect(result.label).toBe("Override")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should support multiple .attrs() chains", () => {
|
|
79
|
+
const Component = attrs({
|
|
80
|
+
name: "Test",
|
|
81
|
+
component: BaseComponent,
|
|
82
|
+
})
|
|
83
|
+
.attrs(() => ({ "data-first": "yes" }))
|
|
84
|
+
.attrs(() => ({ "data-second": "yes" }))
|
|
85
|
+
|
|
86
|
+
const result = renderProps(Component)
|
|
87
|
+
expect(result["data-first"]).toBe("yes")
|
|
88
|
+
expect(result["data-second"]).toBe("yes")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("should pass current props to attrs callback", () => {
|
|
92
|
+
const Component = attrs({
|
|
93
|
+
name: "Test",
|
|
94
|
+
component: BaseComponent,
|
|
95
|
+
}).attrs((props: any) => ({
|
|
96
|
+
"data-variant": props.variant === "primary" ? "is-primary" : "is-default",
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
const result = renderProps(Component, { variant: "primary" })
|
|
100
|
+
expect(result["data-variant"]).toBe("is-primary")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should support object-based attrs", () => {
|
|
104
|
+
const Component = attrs({
|
|
105
|
+
name: "Test",
|
|
106
|
+
component: BaseComponent,
|
|
107
|
+
}).attrs({ label: "Static Label" })
|
|
108
|
+
|
|
109
|
+
const result = renderProps(Component)
|
|
110
|
+
expect(result.label).toBe("Static Label")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("should support priority attrs", () => {
|
|
114
|
+
const Component = attrs({
|
|
115
|
+
name: "Test",
|
|
116
|
+
component: BaseComponent,
|
|
117
|
+
})
|
|
118
|
+
.attrs(() => ({ label: "Normal" }))
|
|
119
|
+
.attrs(() => ({ label: "Priority" }), { priority: true })
|
|
120
|
+
|
|
121
|
+
// Priority attrs have lower precedence than normal attrs
|
|
122
|
+
const result = renderProps(Component)
|
|
123
|
+
expect(result.label).toBe("Normal")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should support filter option to remove attrs from final props", () => {
|
|
127
|
+
const Component = attrs({
|
|
128
|
+
name: "Test",
|
|
129
|
+
component: BaseComponent,
|
|
130
|
+
}).attrs(() => ({ label: "Visible" }), {
|
|
131
|
+
filter: ["data-internal"],
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const result = renderProps(Component, {
|
|
135
|
+
"data-internal": "secret",
|
|
136
|
+
label: "test",
|
|
137
|
+
})
|
|
138
|
+
expect(result["data-internal"]).toBeUndefined()
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// --------------------------------------------------------
|
|
143
|
+
// .config() chaining
|
|
144
|
+
// --------------------------------------------------------
|
|
145
|
+
describe(".config() chaining", () => {
|
|
146
|
+
it("should return a new component instance", () => {
|
|
147
|
+
const Original = attrs({ name: "Test", component: BaseComponent })
|
|
148
|
+
const Configured = Original.config({})
|
|
149
|
+
expect(Configured).not.toBe(Original)
|
|
150
|
+
expect(Configured.IS_ATTRS).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("should update displayName when name is changed", () => {
|
|
154
|
+
const Original = attrs({ name: "Original", component: BaseComponent })
|
|
155
|
+
const Renamed = Original.config({ name: "Renamed" })
|
|
156
|
+
expect(Renamed.displayName).toBe("Renamed")
|
|
157
|
+
expect(Original.displayName).toBe("Original")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("should swap the rendered component", () => {
|
|
161
|
+
const AltComponent = (props: any) => ({
|
|
162
|
+
type: "span",
|
|
163
|
+
props: { ...props, "data-testid": "alt" },
|
|
164
|
+
children: props.label,
|
|
165
|
+
key: null,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const Original = attrs({ name: "Test", component: BaseComponent })
|
|
169
|
+
const Swapped = Original.config({ component: AltComponent })
|
|
170
|
+
|
|
171
|
+
const result = Swapped({ label: "swapped" }) as any
|
|
172
|
+
expect(result.props["data-testid"]).toBe("alt")
|
|
173
|
+
expect(result.children).toBe("swapped")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("should preserve attrs chain after config swap", () => {
|
|
177
|
+
const AltComponent = (props: any) => ({
|
|
178
|
+
type: "span",
|
|
179
|
+
props: { ...props, "data-testid": "alt" },
|
|
180
|
+
children: props.label,
|
|
181
|
+
key: null,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
185
|
+
.attrs(() => ({ label: "from-attrs" }))
|
|
186
|
+
.config({ component: AltComponent })
|
|
187
|
+
|
|
188
|
+
const result = Component({}) as any
|
|
189
|
+
expect(result.children).toBe("from-attrs")
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// --------------------------------------------------------
|
|
194
|
+
// .statics() chaining
|
|
195
|
+
// --------------------------------------------------------
|
|
196
|
+
describe(".statics() chaining", () => {
|
|
197
|
+
it("should assign statics to component meta", () => {
|
|
198
|
+
const Component = attrs({
|
|
199
|
+
name: "Test",
|
|
200
|
+
component: BaseComponent,
|
|
201
|
+
}).statics({ theme: "dark", sizes: ["sm", "md", "lg"] })
|
|
202
|
+
|
|
203
|
+
expect(Component.meta).toEqual({
|
|
204
|
+
theme: "dark",
|
|
205
|
+
sizes: ["sm", "md", "lg"],
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("should merge statics across chains", () => {
|
|
210
|
+
const Component = attrs({
|
|
211
|
+
name: "Test",
|
|
212
|
+
component: BaseComponent,
|
|
213
|
+
})
|
|
214
|
+
.statics({ theme: "dark" })
|
|
215
|
+
.statics({ variant: "primary" })
|
|
216
|
+
|
|
217
|
+
expect(Component.meta).toEqual({
|
|
218
|
+
theme: "dark",
|
|
219
|
+
variant: "primary",
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// --------------------------------------------------------
|
|
225
|
+
// .compose() chaining
|
|
226
|
+
// --------------------------------------------------------
|
|
227
|
+
describe(".compose() chaining", () => {
|
|
228
|
+
it("should wrap component with a HOC", () => {
|
|
229
|
+
const withWrapper = (WrappedComponent: any) => (props: any) => ({
|
|
230
|
+
type: "div",
|
|
231
|
+
props: { "data-testid": "hoc-wrapper" },
|
|
232
|
+
children: WrappedComponent(props),
|
|
233
|
+
key: null,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const Component = attrs({
|
|
237
|
+
name: "Test",
|
|
238
|
+
component: BaseComponent,
|
|
239
|
+
}).compose({ withWrapper })
|
|
240
|
+
|
|
241
|
+
const result = Component({ label: "composed" }) as any
|
|
242
|
+
expect(result.props["data-testid"]).toBe("hoc-wrapper")
|
|
243
|
+
expect(result.children.children).toBe("composed")
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("should apply multiple HOCs in correct order", () => {
|
|
247
|
+
const order: string[] = []
|
|
248
|
+
|
|
249
|
+
const withOuter = (Wrapped: any) => (props: any) => {
|
|
250
|
+
order.push("outer")
|
|
251
|
+
return Wrapped(props)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const withInner = (Wrapped: any) => (props: any) => {
|
|
255
|
+
order.push("inner")
|
|
256
|
+
return Wrapped(props)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const Component = attrs({
|
|
260
|
+
name: "Test",
|
|
261
|
+
component: BaseComponent,
|
|
262
|
+
}).compose({ withOuter, withInner })
|
|
263
|
+
|
|
264
|
+
Component({})
|
|
265
|
+
// calculateHocsFuncs reverses the order: last-defined runs first
|
|
266
|
+
expect(order).toEqual(["inner", "outer"])
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("should remove a HOC by setting it to false", () => {
|
|
270
|
+
const withWrapper = (WrappedComponent: any) => (props: any) => ({
|
|
271
|
+
type: "div",
|
|
272
|
+
props: { "data-testid": "hoc-wrapper" },
|
|
273
|
+
children: WrappedComponent(props),
|
|
274
|
+
key: null,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const WithHoc = attrs({
|
|
278
|
+
name: "Test",
|
|
279
|
+
component: BaseComponent,
|
|
280
|
+
}).compose({ withWrapper })
|
|
281
|
+
|
|
282
|
+
const WithoutHoc = WithHoc.compose({ withWrapper: false })
|
|
283
|
+
|
|
284
|
+
const result = WithoutHoc({ label: "no-hoc" }) as any
|
|
285
|
+
// Should render base component directly, no wrapper
|
|
286
|
+
expect(result.props["data-testid"]).toBe("base")
|
|
287
|
+
expect(result.children).toBe("no-hoc")
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// --------------------------------------------------------
|
|
292
|
+
// .getDefaultAttrs()
|
|
293
|
+
// --------------------------------------------------------
|
|
294
|
+
describe(".getDefaultAttrs()", () => {
|
|
295
|
+
it("should return computed default attrs for given props", () => {
|
|
296
|
+
const Component = attrs({
|
|
297
|
+
name: "Test",
|
|
298
|
+
component: BaseComponent,
|
|
299
|
+
}).attrs((props: any) => ({
|
|
300
|
+
computed: props.variant === "primary" ? "blue" : "gray",
|
|
301
|
+
}))
|
|
302
|
+
|
|
303
|
+
const defaults = Component.getDefaultAttrs({ variant: "primary" })
|
|
304
|
+
expect(defaults).toEqual({ computed: "blue" })
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("should return empty object when no attrs defined", () => {
|
|
308
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
309
|
+
const defaults = Component.getDefaultAttrs({})
|
|
310
|
+
expect(defaults).toEqual({})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("should merge multiple attrs chains", () => {
|
|
314
|
+
const Component = attrs({
|
|
315
|
+
name: "Test",
|
|
316
|
+
component: BaseComponent,
|
|
317
|
+
})
|
|
318
|
+
.attrs(() => ({ color: "blue" }))
|
|
319
|
+
.attrs(() => ({ size: "lg" }))
|
|
320
|
+
|
|
321
|
+
const defaults = Component.getDefaultAttrs({})
|
|
322
|
+
expect(defaults).toEqual({ color: "blue", size: "lg" })
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// --------------------------------------------------------
|
|
327
|
+
// isAttrsComponent
|
|
328
|
+
// --------------------------------------------------------
|
|
329
|
+
describe("isAttrsComponent", () => {
|
|
330
|
+
it("should return true for attrs components", () => {
|
|
331
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
332
|
+
expect(isAttrsComponent(Component)).toBe(true)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it("should return false for plain components", () => {
|
|
336
|
+
expect(isAttrsComponent(BaseComponent)).toBe(false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("should return false for null", () => {
|
|
340
|
+
expect(isAttrsComponent(null)).toBe(false)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it("should return false for undefined", () => {
|
|
344
|
+
expect(isAttrsComponent(undefined)).toBe(false)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("should return false for non-objects", () => {
|
|
348
|
+
expect(isAttrsComponent("string")).toBe(false)
|
|
349
|
+
expect(isAttrsComponent(123)).toBe(false)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it("should return true for objects with IS_ATTRS property", () => {
|
|
353
|
+
expect(isAttrsComponent({ IS_ATTRS: true })).toBe(true)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// --------------------------------------------------------
|
|
358
|
+
// displayName fallback
|
|
359
|
+
// --------------------------------------------------------
|
|
360
|
+
describe("displayName resolution", () => {
|
|
361
|
+
it("should fall back to component.displayName when name is not provided", () => {
|
|
362
|
+
const NamedComponent = (props: any) => ({
|
|
363
|
+
type: "div",
|
|
364
|
+
props,
|
|
365
|
+
children: props.children,
|
|
366
|
+
key: null,
|
|
367
|
+
})
|
|
368
|
+
NamedComponent.displayName = "MyDisplayName"
|
|
369
|
+
|
|
370
|
+
const Component = attrsComponent({
|
|
371
|
+
name: undefined as any,
|
|
372
|
+
component: NamedComponent,
|
|
373
|
+
attrs: [],
|
|
374
|
+
priorityAttrs: [],
|
|
375
|
+
filterAttrs: [],
|
|
376
|
+
compose: {},
|
|
377
|
+
statics: {},
|
|
378
|
+
})
|
|
379
|
+
expect(Component.displayName).toBe("MyDisplayName")
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it("should fall back to component.name when name and displayName are not provided", () => {
|
|
383
|
+
function ExplicitNameComponent(props: any) {
|
|
384
|
+
return {
|
|
385
|
+
type: "div",
|
|
386
|
+
props,
|
|
387
|
+
children: props.children,
|
|
388
|
+
key: null,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const Component = attrsComponent({
|
|
393
|
+
name: undefined as any,
|
|
394
|
+
component: ExplicitNameComponent,
|
|
395
|
+
attrs: [],
|
|
396
|
+
priorityAttrs: [],
|
|
397
|
+
filterAttrs: [],
|
|
398
|
+
compose: {},
|
|
399
|
+
statics: {},
|
|
400
|
+
})
|
|
401
|
+
expect(Component.displayName).toBe("ExplicitNameComponent")
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// --------------------------------------------------------
|
|
406
|
+
// Ref as normal prop
|
|
407
|
+
// --------------------------------------------------------
|
|
408
|
+
describe("ref passthrough", () => {
|
|
409
|
+
it("should pass ref as a normal prop through the chain", () => {
|
|
410
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
411
|
+
const refObj = { current: null }
|
|
412
|
+
|
|
413
|
+
const result = renderProps(Component, { ref: refObj })
|
|
414
|
+
expect(result.ref).toBe(refObj)
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// --------------------------------------------------------
|
|
419
|
+
// Immutability
|
|
420
|
+
// --------------------------------------------------------
|
|
421
|
+
describe("immutability", () => {
|
|
422
|
+
it("should return new instances on each chain call", () => {
|
|
423
|
+
const Base = attrs({ name: "Test", component: BaseComponent })
|
|
424
|
+
const WithAttrs = Base.attrs(() => ({ label: "a" }))
|
|
425
|
+
const WithStatics = Base.statics({ x: 1 })
|
|
426
|
+
|
|
427
|
+
expect(Base).not.toBe(WithAttrs)
|
|
428
|
+
expect(Base).not.toBe(WithStatics)
|
|
429
|
+
expect(WithAttrs).not.toBe(WithStatics)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it("should not affect parent when child is modified", () => {
|
|
433
|
+
const Parent = attrs({
|
|
434
|
+
name: "Parent",
|
|
435
|
+
component: BaseComponent,
|
|
436
|
+
}).attrs(() => ({ label: "Parent" }))
|
|
437
|
+
|
|
438
|
+
const Child = Parent.attrs(() => ({ label: "Child" }))
|
|
439
|
+
|
|
440
|
+
const parentResult = renderProps(Parent)
|
|
441
|
+
expect(parentResult.label).toBe("Parent")
|
|
442
|
+
|
|
443
|
+
const childResult = renderProps(Child)
|
|
444
|
+
expect(childResult.label).toBe("Child")
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// --------------------------------------------------------
|
|
449
|
+
// Deep chaining
|
|
450
|
+
// --------------------------------------------------------
|
|
451
|
+
describe("deep chaining", () => {
|
|
452
|
+
it("should accumulate attrs across 3+ levels", () => {
|
|
453
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
454
|
+
.attrs(() => ({ "data-a": "1" }))
|
|
455
|
+
.attrs(() => ({ "data-b": "2" }))
|
|
456
|
+
.attrs(() => ({ "data-c": "3" }))
|
|
457
|
+
|
|
458
|
+
const result = renderProps(Component)
|
|
459
|
+
expect(result["data-a"]).toBe("1")
|
|
460
|
+
expect(result["data-b"]).toBe("2")
|
|
461
|
+
expect(result["data-c"]).toBe("3")
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it("should combine attrs, statics, and config in a single chain", () => {
|
|
465
|
+
const Component = attrs({ name: "Base", component: BaseComponent })
|
|
466
|
+
.attrs(() => ({ label: "hello" }))
|
|
467
|
+
.statics({ variant: "primary" })
|
|
468
|
+
.config({ name: "FinalName" })
|
|
469
|
+
.attrs(() => ({ "data-extra": "yes" }))
|
|
470
|
+
|
|
471
|
+
expect(Component.displayName).toBe("FinalName")
|
|
472
|
+
expect(Component.meta).toEqual({ variant: "primary" })
|
|
473
|
+
|
|
474
|
+
const result = renderProps(Component)
|
|
475
|
+
expect(result.label).toBe("hello")
|
|
476
|
+
expect(result["data-extra"]).toBe("yes")
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it("should allow later attrs to override earlier ones", () => {
|
|
480
|
+
const Component = attrs({ name: "Test", component: BaseComponent })
|
|
481
|
+
.attrs(() => ({ label: "first" }))
|
|
482
|
+
.attrs(() => ({ label: "second" }))
|
|
483
|
+
.attrs(() => ({ label: "third" }))
|
|
484
|
+
|
|
485
|
+
const result = renderProps(Component)
|
|
486
|
+
expect(result.label).toBe("third")
|
|
487
|
+
})
|
|
488
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import createAttrsHOC from "../hoc/attrsHoc"
|
|
2
|
+
|
|
3
|
+
const Receiver = (props: any) => ({
|
|
4
|
+
type: "div",
|
|
5
|
+
props: { ...props, "data-testid": "receiver" },
|
|
6
|
+
children: props.label ?? "",
|
|
7
|
+
key: null,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// --------------------------------------------------------
|
|
11
|
+
// attrsHoc - props merging
|
|
12
|
+
// --------------------------------------------------------
|
|
13
|
+
describe("attrsHoc - props merging", () => {
|
|
14
|
+
it("should pass through props unchanged when no attrs defined", () => {
|
|
15
|
+
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
16
|
+
const Enhanced = hoc(Receiver)
|
|
17
|
+
|
|
18
|
+
const result = Enhanced({ label: "hello", "data-custom": "yes" }) as any
|
|
19
|
+
expect(result.children).toBe("hello")
|
|
20
|
+
expect(result.props["data-custom"]).toBe("yes")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("should apply attrs as default props", () => {
|
|
24
|
+
const hoc = createAttrsHOC({
|
|
25
|
+
attrs: [(_props: any) => ({ label: "default" })],
|
|
26
|
+
priorityAttrs: [],
|
|
27
|
+
})
|
|
28
|
+
const Enhanced = hoc(Receiver)
|
|
29
|
+
|
|
30
|
+
const result = Enhanced({}) as any
|
|
31
|
+
expect(result.children).toBe("default")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("should let explicit props override attrs", () => {
|
|
35
|
+
const hoc = createAttrsHOC({
|
|
36
|
+
attrs: [() => ({ label: "from-attrs" })],
|
|
37
|
+
priorityAttrs: [],
|
|
38
|
+
})
|
|
39
|
+
const Enhanced = hoc(Receiver)
|
|
40
|
+
|
|
41
|
+
const result = Enhanced({ label: "explicit" }) as any
|
|
42
|
+
expect(result.children).toBe("explicit")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should apply priorityAttrs with lowest precedence", () => {
|
|
46
|
+
const hoc = createAttrsHOC({
|
|
47
|
+
attrs: [(_props: any) => ({ label: "from-attrs" })],
|
|
48
|
+
priorityAttrs: [(_props: any) => ({ label: "from-priority" })],
|
|
49
|
+
})
|
|
50
|
+
const Enhanced = hoc(Receiver)
|
|
51
|
+
|
|
52
|
+
const result = Enhanced({}) as any
|
|
53
|
+
expect(result.children).toBe("from-attrs")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should merge results from multiple attrs functions", () => {
|
|
57
|
+
const hoc = createAttrsHOC({
|
|
58
|
+
attrs: [() => ({ "data-first": "a" }), () => ({ "data-second": "b" })],
|
|
59
|
+
priorityAttrs: [],
|
|
60
|
+
})
|
|
61
|
+
const Enhanced = hoc(Receiver)
|
|
62
|
+
|
|
63
|
+
const result = Enhanced({}) as any
|
|
64
|
+
expect(result.props["data-first"]).toBe("a")
|
|
65
|
+
expect(result.props["data-second"]).toBe("b")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("should remove undefined props so they dont override defaults", () => {
|
|
69
|
+
const hoc = createAttrsHOC({
|
|
70
|
+
attrs: [() => ({ label: "default-label" })],
|
|
71
|
+
priorityAttrs: [],
|
|
72
|
+
})
|
|
73
|
+
const Enhanced = hoc(Receiver)
|
|
74
|
+
|
|
75
|
+
const result = Enhanced({ label: undefined }) as any
|
|
76
|
+
expect(result.children).toBe("default-label")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("should allow null to override defaults", () => {
|
|
80
|
+
const hoc = createAttrsHOC({
|
|
81
|
+
attrs: [() => ({ label: "default-label" })],
|
|
82
|
+
priorityAttrs: [],
|
|
83
|
+
})
|
|
84
|
+
const Enhanced = hoc(Receiver)
|
|
85
|
+
|
|
86
|
+
const result = Enhanced({ label: null }) as any
|
|
87
|
+
expect(result.children).toBe("")
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// --------------------------------------------------------
|
|
92
|
+
// attrsHoc - attrs callback receives props
|
|
93
|
+
// --------------------------------------------------------
|
|
94
|
+
describe("attrsHoc - attrs callback receives props", () => {
|
|
95
|
+
it("should pass filtered props to attrs callback", () => {
|
|
96
|
+
const attrsFn = vi.fn(() => ({}))
|
|
97
|
+
const hoc = createAttrsHOC({
|
|
98
|
+
attrs: [attrsFn],
|
|
99
|
+
priorityAttrs: [],
|
|
100
|
+
})
|
|
101
|
+
const Enhanced = hoc(Receiver)
|
|
102
|
+
|
|
103
|
+
Enhanced({ variant: "primary", size: "lg" })
|
|
104
|
+
expect(attrsFn).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({ variant: "primary", size: "lg" }),
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("should pass priority attrs merged with props to attrs callback", () => {
|
|
110
|
+
const attrsFn = vi.fn(() => ({}))
|
|
111
|
+
const hoc = createAttrsHOC({
|
|
112
|
+
attrs: [attrsFn],
|
|
113
|
+
priorityAttrs: [() => ({ fromPriority: true })],
|
|
114
|
+
})
|
|
115
|
+
const Enhanced = hoc(Receiver)
|
|
116
|
+
|
|
117
|
+
Enhanced({ variant: "primary" })
|
|
118
|
+
expect(attrsFn).toHaveBeenCalledWith(
|
|
119
|
+
expect.objectContaining({ variant: "primary", fromPriority: true }),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// --------------------------------------------------------
|
|
125
|
+
// attrsHoc - ref passthrough
|
|
126
|
+
// --------------------------------------------------------
|
|
127
|
+
describe("attrsHoc - ref passthrough", () => {
|
|
128
|
+
it("should pass ref as a normal prop to wrapped component", () => {
|
|
129
|
+
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
130
|
+
const Enhanced = hoc(Receiver)
|
|
131
|
+
|
|
132
|
+
const refObj = { current: null }
|
|
133
|
+
const result = Enhanced({ ref: refObj }) as any
|
|
134
|
+
expect(result.props.ref).toBe(refObj)
|
|
135
|
+
})
|
|
136
|
+
})
|