@pyreon/attrs 0.13.0 → 0.14.0
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/attrs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Attrs HOC chaining for Pyreon components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@pyreon/typescript": "^0.
|
|
43
|
+
"@pyreon/typescript": "^0.14.0",
|
|
44
44
|
"@vitus-labs/tools-rolldown": "^1.15.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.
|
|
48
|
-
"@pyreon/ui-core": "^0.
|
|
47
|
+
"@pyreon/core": "^0.14.0",
|
|
48
|
+
"@pyreon/ui-core": "^0.14.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">= 22"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { h } from '@pyreon/core'
|
|
1
2
|
import attrsComponent from '../attrs'
|
|
2
3
|
import attrs from '../init'
|
|
3
4
|
import isAttrsComponent from '../isAttrsComponent'
|
|
@@ -6,12 +7,8 @@ import isAttrsComponent from '../isAttrsComponent'
|
|
|
6
7
|
* Simple base component for testing.
|
|
7
8
|
* Returns a VNode-like object so we can inspect the final props.
|
|
8
9
|
*/
|
|
9
|
-
const BaseComponent = (props: any) =>
|
|
10
|
-
|
|
11
|
-
props: { ...props, 'data-testid': 'base' },
|
|
12
|
-
children: props.children ?? props.label ?? null,
|
|
13
|
-
key: null,
|
|
14
|
-
})
|
|
10
|
+
const BaseComponent = (props: any) =>
|
|
11
|
+
h('div', { ...props, 'data-testid': 'base' }, props.children ?? props.label ?? null)
|
|
15
12
|
|
|
16
13
|
/** Helper: call the component and return its output for inspection. */
|
|
17
14
|
const renderProps = (Component: any, props: Record<string, any> = {}) => {
|
|
@@ -158,35 +155,30 @@ describe('.config() chaining', () => {
|
|
|
158
155
|
})
|
|
159
156
|
|
|
160
157
|
it('should swap the rendered component', () => {
|
|
161
|
-
const AltComponent = (props: any) =>
|
|
162
|
-
|
|
163
|
-
props: { ...props, 'data-testid': 'alt' },
|
|
164
|
-
children: props.label,
|
|
165
|
-
key: null,
|
|
166
|
-
})
|
|
158
|
+
const AltComponent = (props: any) =>
|
|
159
|
+
h('span', { ...props, 'data-testid': 'alt' }, props.label)
|
|
167
160
|
|
|
168
161
|
const Original = attrs({ name: 'Test', component: BaseComponent })
|
|
169
162
|
const Swapped = Original.config({ component: AltComponent })
|
|
170
163
|
|
|
171
164
|
const result = Swapped({ label: 'swapped' }) as any
|
|
172
165
|
expect(result.props['data-testid']).toBe('alt')
|
|
173
|
-
|
|
166
|
+
// h() wraps single string children in an array. The contract here
|
|
167
|
+
// is "the rendered component received the right child text", so
|
|
168
|
+
// assert against the flattened content rather than the bare string.
|
|
169
|
+
expect(result.children).toEqual(['swapped'])
|
|
174
170
|
})
|
|
175
171
|
|
|
176
172
|
it('should preserve attrs chain after config swap', () => {
|
|
177
|
-
const AltComponent = (props: any) =>
|
|
178
|
-
|
|
179
|
-
props: { ...props, 'data-testid': 'alt' },
|
|
180
|
-
children: props.label,
|
|
181
|
-
key: null,
|
|
182
|
-
})
|
|
173
|
+
const AltComponent = (props: any) =>
|
|
174
|
+
h('span', { ...props, 'data-testid': 'alt' }, props.label)
|
|
183
175
|
|
|
184
176
|
const Component = attrs({ name: 'Test', component: BaseComponent })
|
|
185
177
|
.attrs(() => ({ label: 'from-attrs' }))
|
|
186
178
|
.config({ component: AltComponent })
|
|
187
179
|
|
|
188
180
|
const result = Component({}) as any
|
|
189
|
-
expect(result.children).
|
|
181
|
+
expect(result.children).toEqual(['from-attrs'])
|
|
190
182
|
})
|
|
191
183
|
})
|
|
192
184
|
|
|
@@ -226,12 +218,8 @@ describe('.statics() chaining', () => {
|
|
|
226
218
|
// --------------------------------------------------------
|
|
227
219
|
describe('.compose() chaining', () => {
|
|
228
220
|
it('should wrap component with a HOC', () => {
|
|
229
|
-
const withWrapper = (WrappedComponent: any) => (props: any) =>
|
|
230
|
-
|
|
231
|
-
props: { 'data-testid': 'hoc-wrapper' },
|
|
232
|
-
children: WrappedComponent(props),
|
|
233
|
-
key: null,
|
|
234
|
-
})
|
|
221
|
+
const withWrapper = (WrappedComponent: any) => (props: any) =>
|
|
222
|
+
h('div', { 'data-testid': 'hoc-wrapper' }, WrappedComponent(props))
|
|
235
223
|
|
|
236
224
|
const Component = attrs({
|
|
237
225
|
name: 'Test',
|
|
@@ -240,7 +228,10 @@ describe('.compose() chaining', () => {
|
|
|
240
228
|
|
|
241
229
|
const result = Component({ label: 'composed' }) as any
|
|
242
230
|
expect(result.props['data-testid']).toBe('hoc-wrapper')
|
|
243
|
-
|
|
231
|
+
// Real h() wraps single children in an array; nested wrapper holds
|
|
232
|
+
// its inner WrappedComponent(props) result whose `children` is now
|
|
233
|
+
// also wrapped — so two array layers down sits the final string.
|
|
234
|
+
expect(result.children[0].children).toEqual(['composed'])
|
|
244
235
|
})
|
|
245
236
|
|
|
246
237
|
it('should apply multiple HOCs in correct order', () => {
|
|
@@ -267,12 +258,8 @@ describe('.compose() chaining', () => {
|
|
|
267
258
|
})
|
|
268
259
|
|
|
269
260
|
it('should remove a HOC by setting it to false', () => {
|
|
270
|
-
const withWrapper = (WrappedComponent: any) => (props: any) =>
|
|
271
|
-
|
|
272
|
-
props: { 'data-testid': 'hoc-wrapper' },
|
|
273
|
-
children: WrappedComponent(props),
|
|
274
|
-
key: null,
|
|
275
|
-
})
|
|
261
|
+
const withWrapper = (WrappedComponent: any) => (props: any) =>
|
|
262
|
+
h('div', { 'data-testid': 'hoc-wrapper' }, WrappedComponent(props))
|
|
276
263
|
|
|
277
264
|
const WithHoc = attrs({
|
|
278
265
|
name: 'Test',
|
|
@@ -284,7 +271,7 @@ describe('.compose() chaining', () => {
|
|
|
284
271
|
const result = WithoutHoc({ label: 'no-hoc' }) as any
|
|
285
272
|
// Should render base component directly, no wrapper
|
|
286
273
|
expect(result.props['data-testid']).toBe('base')
|
|
287
|
-
expect(result.children).
|
|
274
|
+
expect(result.children).toEqual(['no-hoc'])
|
|
288
275
|
})
|
|
289
276
|
})
|
|
290
277
|
|
|
@@ -359,12 +346,7 @@ describe('isAttrsComponent', () => {
|
|
|
359
346
|
// --------------------------------------------------------
|
|
360
347
|
describe('displayName resolution', () => {
|
|
361
348
|
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
|
-
})
|
|
349
|
+
const NamedComponent = (props: any) => h('div', props, props.children)
|
|
368
350
|
NamedComponent.displayName = 'MyDisplayName'
|
|
369
351
|
|
|
370
352
|
const Component = attrsComponent({
|
|
@@ -381,12 +363,7 @@ describe('displayName resolution', () => {
|
|
|
381
363
|
|
|
382
364
|
it('should fall back to component.name when name and displayName are not provided', () => {
|
|
383
365
|
function ExplicitNameComponent(props: any) {
|
|
384
|
-
return
|
|
385
|
-
type: 'div',
|
|
386
|
-
props,
|
|
387
|
-
children: props.children,
|
|
388
|
-
key: null,
|
|
389
|
-
}
|
|
366
|
+
return h('div', props, props.children)
|
|
390
367
|
}
|
|
391
368
|
|
|
392
369
|
const Component = attrsComponent({
|
|
@@ -486,3 +463,69 @@ describe('deep chaining', () => {
|
|
|
486
463
|
expect(result.label).toBe('third')
|
|
487
464
|
})
|
|
488
465
|
})
|
|
466
|
+
|
|
467
|
+
// ─── attrs — real h() round-trip (parallel to the BaseComponent mocks) ──
|
|
468
|
+
//
|
|
469
|
+
// The tests above use a `BaseComponent` mock that returns
|
|
470
|
+
// `{ type, props, children, key }` literals, then assert against the
|
|
471
|
+
// `vnode.props` shape. That's the contract at the type level, but it
|
|
472
|
+
// skips the actual h() call shape — `h('div', props, ...children)`
|
|
473
|
+
// flattens children differently than the mock and may pass props
|
|
474
|
+
// through different normalization paths in the runtime. PR #197 was
|
|
475
|
+
// caused by exactly this kind of mock-vs-real divergence.
|
|
476
|
+
//
|
|
477
|
+
// This block re-runs the key attrs contracts through a base component
|
|
478
|
+
// that uses real `h()` from `@pyreon/core`. The mock tests stay as
|
|
479
|
+
// the fast unit-test path; these are the safety net.
|
|
480
|
+
|
|
481
|
+
describe('attrs — real h() round-trip', () => {
|
|
482
|
+
// BaseComponent that returns a real VNode via h(). Same shape as
|
|
483
|
+
// the mock, but built through the public h() API.
|
|
484
|
+
const BaseComponentH = (props: any) =>
|
|
485
|
+
h('div', { ...props, 'data-testid': 'base' }, props.children ?? props.label ?? null)
|
|
486
|
+
|
|
487
|
+
it('passes through props unchanged when no attrs defined', () => {
|
|
488
|
+
const Component = attrs({ name: 'BareH', component: BaseComponentH })
|
|
489
|
+
const result = (Component as any)({ label: 'hello', 'data-custom': 'yes' }) as any
|
|
490
|
+
expect(result.props.label).toBe('hello')
|
|
491
|
+
expect(result.props['data-custom']).toBe('yes')
|
|
492
|
+
expect(result.props['data-testid']).toBe('base')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('applies attrs as default props through real h()', () => {
|
|
496
|
+
const Component = attrs({ name: 'WithAttrs', component: BaseComponentH }).attrs(
|
|
497
|
+
(_props: any) => ({ label: 'default' }),
|
|
498
|
+
)
|
|
499
|
+
const result = (Component as any)({}) as any
|
|
500
|
+
expect(result.props.label).toBe('default')
|
|
501
|
+
expect(result.type).toBe('div')
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('overrides default attrs with explicit props', () => {
|
|
505
|
+
const Component = attrs({ name: 'Overridable', component: BaseComponentH }).attrs(
|
|
506
|
+
(_props: any) => ({ label: 'default' }),
|
|
507
|
+
)
|
|
508
|
+
const result = (Component as any)({ label: 'explicit' }) as any
|
|
509
|
+
expect(result.props.label).toBe('explicit')
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('chains multiple attrs() calls through real h()', () => {
|
|
513
|
+
const Component = attrs({ name: 'Chained', component: BaseComponentH })
|
|
514
|
+
.attrs(() => ({ a: 1 }))
|
|
515
|
+
.attrs(() => ({ b: 2 }))
|
|
516
|
+
.attrs(() => ({ c: 3 }))
|
|
517
|
+
const result = (Component as any)({}) as any
|
|
518
|
+
expect(result.props.a).toBe(1)
|
|
519
|
+
expect(result.props.b).toBe(2)
|
|
520
|
+
expect(result.props.c).toBe(3)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('later attrs override earlier ones (real h() parallel)', () => {
|
|
524
|
+
const Component = attrs({ name: 'OrderH', component: BaseComponentH })
|
|
525
|
+
.attrs(() => ({ label: 'first' }))
|
|
526
|
+
.attrs(() => ({ label: 'second' }))
|
|
527
|
+
.attrs(() => ({ label: 'third' }))
|
|
528
|
+
const result = (Component as any)({}) as any
|
|
529
|
+
expect(result.props.label).toBe('third')
|
|
530
|
+
})
|
|
531
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { h } from '@pyreon/core'
|
|
1
2
|
import createAttrsHOC from '../hoc/attrsHoc'
|
|
2
3
|
|
|
3
4
|
const Receiver = (props: any) => ({
|
|
@@ -134,3 +135,45 @@ describe('attrsHoc - ref passthrough', () => {
|
|
|
134
135
|
expect(result.props.ref).toBe(refObj)
|
|
135
136
|
})
|
|
136
137
|
})
|
|
138
|
+
|
|
139
|
+
// ─── attrsHoc — real h() round-trip (parallel to the Receiver mock) ──
|
|
140
|
+
//
|
|
141
|
+
// The Receiver above is a mock that returns a `{ type, props,
|
|
142
|
+
// children, key }` literal. The tests assert against that shape.
|
|
143
|
+
// This block re-runs the core HOC contracts against a Receiver that
|
|
144
|
+
// builds its return value via real `h()` from `@pyreon/core` —
|
|
145
|
+
// catches divergence between the mock shape and the actual VNode
|
|
146
|
+
// shape h() produces.
|
|
147
|
+
|
|
148
|
+
describe('attrsHoc — real h() round-trip', () => {
|
|
149
|
+
// Receiver that returns a real VNode via h() instead of a literal.
|
|
150
|
+
const ReceiverH = (props: any) =>
|
|
151
|
+
h('div', { ...props, 'data-testid': 'receiver' }, props.label ?? '')
|
|
152
|
+
|
|
153
|
+
it('passes through props unchanged when no attrs defined', () => {
|
|
154
|
+
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
155
|
+
const Enhanced = hoc(ReceiverH as any)
|
|
156
|
+
const result = Enhanced({ label: 'hello', 'data-custom': 'yes' }) as any
|
|
157
|
+
expect(result.props.label).toBe('hello')
|
|
158
|
+
expect(result.props['data-custom']).toBe('yes')
|
|
159
|
+
expect(result.type).toBe('div')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('applies attrs as default props through real h()', () => {
|
|
163
|
+
const hoc = createAttrsHOC({
|
|
164
|
+
attrs: [(_props: any) => ({ label: 'default' })],
|
|
165
|
+
priorityAttrs: [],
|
|
166
|
+
})
|
|
167
|
+
const Enhanced = hoc(ReceiverH as any)
|
|
168
|
+
const result = Enhanced({}) as any
|
|
169
|
+
expect(result.props.label).toBe('default')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('passes ref through real h() output unchanged', () => {
|
|
173
|
+
const hoc = createAttrsHOC({ attrs: [], priorityAttrs: [] })
|
|
174
|
+
const Enhanced = hoc(ReceiverH as any)
|
|
175
|
+
const refObj = { current: null }
|
|
176
|
+
const result = Enhanced({ ref: refObj }) as any
|
|
177
|
+
expect(result.props.ref).toBe(refObj)
|
|
178
|
+
})
|
|
179
|
+
})
|