@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.13.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.13.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.13.0",
48
- "@pyreon/ui-core": "^0.13.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
- type: 'div',
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
- type: 'span',
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
- expect(result.children).toBe('swapped')
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
- type: 'span',
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).toBe('from-attrs')
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
- type: 'div',
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
- expect(result.children.children).toBe('composed')
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
- type: 'div',
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).toBe('no-hoc')
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
+ })