@pyreon/core 0.24.5 → 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.
Files changed (56) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +53 -31
  3. package/package.json +2 -6
  4. package/src/compat-marker.ts +0 -79
  5. package/src/compat-shared.ts +0 -80
  6. package/src/component.ts +0 -98
  7. package/src/context.ts +0 -349
  8. package/src/defer.ts +0 -279
  9. package/src/dynamic.ts +0 -32
  10. package/src/env.d.ts +0 -6
  11. package/src/error-boundary.ts +0 -90
  12. package/src/for.ts +0 -51
  13. package/src/h.ts +0 -80
  14. package/src/index.ts +0 -80
  15. package/src/jsx-dev-runtime.ts +0 -2
  16. package/src/jsx-runtime.ts +0 -747
  17. package/src/lazy.ts +0 -25
  18. package/src/lifecycle.ts +0 -152
  19. package/src/manifest.ts +0 -579
  20. package/src/map-array.ts +0 -42
  21. package/src/portal.ts +0 -39
  22. package/src/props.ts +0 -269
  23. package/src/ref.ts +0 -32
  24. package/src/show.ts +0 -121
  25. package/src/style.ts +0 -102
  26. package/src/suspense.ts +0 -52
  27. package/src/telemetry.ts +0 -120
  28. package/src/tests/compat-marker.test.ts +0 -96
  29. package/src/tests/compat-shared.test.ts +0 -99
  30. package/src/tests/component.test.ts +0 -281
  31. package/src/tests/context.test.ts +0 -629
  32. package/src/tests/core.test.ts +0 -1290
  33. package/src/tests/cx.test.ts +0 -70
  34. package/src/tests/defer.test.ts +0 -359
  35. package/src/tests/dynamic.test.ts +0 -87
  36. package/src/tests/error-boundary.test.ts +0 -181
  37. package/src/tests/extract-props-overloads.types.test.ts +0 -135
  38. package/src/tests/for.test.ts +0 -117
  39. package/src/tests/h.test.ts +0 -221
  40. package/src/tests/jsx-compat.test.tsx +0 -86
  41. package/src/tests/lazy.test.ts +0 -100
  42. package/src/tests/lifecycle.test.ts +0 -350
  43. package/src/tests/manifest-snapshot.test.ts +0 -100
  44. package/src/tests/map-array.test.ts +0 -313
  45. package/src/tests/native-marker-error-boundary.test.ts +0 -12
  46. package/src/tests/portal.test.ts +0 -48
  47. package/src/tests/props-extended.test.ts +0 -157
  48. package/src/tests/props.test.ts +0 -250
  49. package/src/tests/reactive-context.test.ts +0 -69
  50. package/src/tests/reactive-props.test.ts +0 -157
  51. package/src/tests/ref.test.ts +0 -70
  52. package/src/tests/show.test.ts +0 -314
  53. package/src/tests/style.test.ts +0 -157
  54. package/src/tests/suspense.test.ts +0 -139
  55. package/src/tests/telemetry.test.ts +0 -297
  56. package/src/types.ts +0 -116
@@ -1,135 +0,0 @@
1
- /**
2
- * Compile-time type tests for `ExtractProps` multi-overload narrowing.
3
- *
4
- * Regression: pre-fix, `ExtractProps<T>` collapsed multi-overload functions
5
- * to the LAST overload's props — TS's overload-resolution-against-conditional-
6
- * types semantics. Multi-overload primitives (Iterator / List / Element in
7
- * `@pyreon/elements`) silently downgraded their public prop surface to the
8
- * loosest overload when wrapped through `rocketstyle()` / `attrs()`. The
9
- * fix matches up to 4 call signatures via pattern matching and produces the
10
- * UNION of every overload's first-argument type.
11
- *
12
- * Mirrors vitus-labs PR #222. Kept in sync across the 4 copies in
13
- * `@pyreon/core`, `@pyreon/elements`, `@pyreon/attrs`, and `@pyreon/rocketstyle`
14
- * — the canonical reference test lives here.
15
- */
16
-
17
- import { describe, expectTypeOf, it } from 'vitest'
18
- import type { ComponentFn, ExtractProps, VNodeChild } from '../index'
19
-
20
- describe('ExtractProps — single-overload functions still work', () => {
21
- it('extracts props from a ComponentFn<P>', () => {
22
- type Greet = ComponentFn<{ name: string }>
23
- expectTypeOf<ExtractProps<Greet>>().toEqualTypeOf<{ name: string }>()
24
- })
25
-
26
- it('extracts props from a bare (props: P) => any signature', () => {
27
- type Fn = (props: { count: number }) => string
28
- expectTypeOf<ExtractProps<Fn>>().toEqualTypeOf<{ count: number }>()
29
- })
30
-
31
- it('passes through a non-function shape unchanged', () => {
32
- type Props = { id: string; value: number }
33
- expectTypeOf<ExtractProps<Props>>().toEqualTypeOf<Props>()
34
- })
35
- })
36
-
37
- describe('ExtractProps — multi-overload narrowing (load-bearing assertions)', () => {
38
- it('unions both arms of a 2-overload function', () => {
39
- interface TwoOverloads {
40
- (props: { kind: 'a'; value: number }): VNodeChild
41
- (props: { kind: 'b'; value: string }): VNodeChild
42
- }
43
- type Props = ExtractProps<TwoOverloads>
44
- // Both shapes appear in the extracted union.
45
- expectTypeOf<Props>().toEqualTypeOf<
46
- { kind: 'a'; value: number } | { kind: 'b'; value: string }
47
- >()
48
- })
49
-
50
- it('unions all three arms of a 3-overload function (Iterator/List/Element shape)', () => {
51
- interface ThreeOverloads {
52
- (props: { mode: 'simple'; data: string[] }): VNodeChild
53
- (props: { mode: 'object'; data: { id: number }[] }): VNodeChild
54
- (props: { mode: 'children'; children: unknown }): VNodeChild
55
- }
56
- type Props = ExtractProps<ThreeOverloads>
57
- expectTypeOf<Props>().toEqualTypeOf<
58
- | { mode: 'simple'; data: string[] }
59
- | { mode: 'object'; data: { id: number }[] }
60
- | { mode: 'children'; children: unknown }
61
- >()
62
- })
63
-
64
- it('unions all four arms of a 4-overload function', () => {
65
- interface FourOverloads {
66
- (props: { variant: 'a' }): VNodeChild
67
- (props: { variant: 'b' }): VNodeChild
68
- (props: { variant: 'c' }): VNodeChild
69
- (props: { variant: 'd' }): VNodeChild
70
- }
71
- type Props = ExtractProps<FourOverloads>
72
- expectTypeOf<Props>().toEqualTypeOf<
73
- { variant: 'a' } | { variant: 'b' } | { variant: 'c' } | { variant: 'd' }
74
- >()
75
- })
76
- })
77
-
78
- describe('ExtractProps — bisect-load-bearing: pre-fix shape would FAIL these', () => {
79
- /**
80
- * If `ExtractProps<T>` were reverted to `T extends ComponentFn<infer P> ? P : T`,
81
- * each of these would extract only the LAST overload's props and the
82
- * `toEqualTypeOf<union>` check would fail at compile time. This is the
83
- * structural anchor — the load-bearing regression guard.
84
- */
85
-
86
- it('a 2-overload function MUST extract BOTH arms (not just the last)', () => {
87
- interface OverloadedComp {
88
- (props: { mode: 'a'; valueA: number }): VNodeChild
89
- (props: { mode: 'b'; valueB: string }): VNodeChild
90
- }
91
- // The first arm `{ mode: 'a'; valueA: number }` must be present in the
92
- // union. Pre-fix, the conditional collapsed to just the LAST arm.
93
- type Props = ExtractProps<OverloadedComp>
94
- // Assignability check: both shapes must be assignable to the extracted type.
95
- const a: Props = { mode: 'a', valueA: 1 }
96
- const b: Props = { mode: 'b', valueB: 'x' }
97
- void a
98
- void b
99
- })
100
-
101
- it("a 3-overload Iterator-shaped surface MUST surface SimpleProps + ObjectProps + ChildrenProps", () => {
102
- // Synthetic Iterator overload-shape — mirrors the real
103
- // `@pyreon/elements` Iterator. The structural failure mode pre-fix:
104
- // `ExtractProps<typeof Iterator>` returned just `ChildrenProps`, so any
105
- // HOC wrapping (rocketstyle, attrs) lost the SimpleProps + ObjectProps
106
- // surfaces from the public typed API.
107
- type SimpleItem = ComponentFn<{ value: string }>
108
- type ObjectItem = ComponentFn<{ id: number }>
109
- interface IteratorLike {
110
- <T extends string | number>(props: {
111
- data: T[]
112
- component: SimpleItem
113
- valueName?: string
114
- }): VNodeChild
115
- <T extends { id: number }>(props: {
116
- data: T[]
117
- component: ObjectItem
118
- }): VNodeChild
119
- (props: { children: VNodeChild }): VNodeChild
120
- }
121
- type Props = ExtractProps<IteratorLike>
122
-
123
- const noopSimple: SimpleItem = () => null
124
- const noopObject: ObjectItem = () => null
125
- // SimpleProps arm assignable:
126
- const simple: Props = { data: ['a', 'b'], component: noopSimple, valueName: 'text' }
127
- // ObjectProps arm assignable:
128
- const obj: Props = { data: [{ id: 1 }], component: noopObject }
129
- // ChildrenProps arm assignable:
130
- const ch: Props = { children: null }
131
- void simple
132
- void obj
133
- void ch
134
- })
135
- })
@@ -1,117 +0,0 @@
1
- import { For, ForSymbol } from '../for'
2
- import { h } from '../h'
3
- import type { VNode } from '../types'
4
-
5
- describe('For', () => {
6
- test('returns VNode with ForSymbol type', () => {
7
- const node = For({
8
- each: () => [1, 2, 3],
9
- by: (item) => item,
10
- children: (item) => h('li', null, String(item)),
11
- })
12
- expect(node.type).toBe(ForSymbol)
13
- })
14
-
15
- test('VNode has empty children array', () => {
16
- const node = For({
17
- each: () => [],
18
- by: (item: number) => item,
19
- children: (item) => h('span', null, String(item)),
20
- })
21
- expect(node.children).toEqual([])
22
- })
23
-
24
- test('VNode has null key', () => {
25
- const node = For({
26
- each: () => [1],
27
- by: (item) => item,
28
- children: (item) => h('li', null, String(item)),
29
- })
30
- expect(node.key).toBeNull()
31
- })
32
-
33
- test('props contain each, by, children functions', () => {
34
- const eachFn = () => ['a', 'b']
35
- const byFn = (item: string) => item
36
- const childFn = (item: string) => h('span', null, item)
37
- const node = For({ each: eachFn, by: byFn, children: childFn })
38
-
39
- const props = node.props as unknown as {
40
- each: typeof eachFn
41
- by: typeof byFn
42
- children: typeof childFn
43
- }
44
- expect(props.each).toBe(eachFn)
45
- expect(props.by).toBe(byFn)
46
- expect(props.children).toBe(childFn)
47
- })
48
-
49
- test('ForSymbol is a unique symbol', () => {
50
- expect(typeof ForSymbol).toBe('symbol')
51
- expect(ForSymbol.toString()).toContain('pyreon.For')
52
- })
53
-
54
- test('works with object items', () => {
55
- interface Item {
56
- id: number
57
- name: string
58
- }
59
- const items: Item[] = [
60
- { id: 1, name: 'one' },
61
- { id: 2, name: 'two' },
62
- ]
63
- const node = For<Item>({
64
- each: () => items,
65
- by: (item) => item.id,
66
- children: (item) => h('li', null, item.name),
67
- })
68
- expect(node.type).toBe(ForSymbol)
69
- const props = node.props as unknown as { each: () => Item[] }
70
- expect(props.each()).toBe(items)
71
- })
72
-
73
- test('works with string keys', () => {
74
- const node = For({
75
- each: () => [{ slug: 'hello' }, { slug: 'world' }],
76
- by: (item) => item.slug,
77
- children: (item) => h('div', null, item.slug),
78
- })
79
- expect(node.type).toBe(ForSymbol)
80
- })
81
-
82
- test('children function produces VNodes', () => {
83
- const childFn = (n: number) => h('li', { key: n }, String(n))
84
- const node = For({
85
- each: () => [1, 2, 3],
86
- by: (n) => n,
87
- children: childFn,
88
- })
89
- const props = node.props as unknown as { children: typeof childFn }
90
- const result = props.children(1)
91
- expect((result as VNode).type).toBe('li')
92
- expect((result as VNode).key).toBe(1)
93
- })
94
-
95
- // Regression: `ForProps.each` previously typed as `() => T[]` only.
96
- // Users writing `<For each={items}>` (with `items: T[]` directly) hit
97
- // a confusing TS error: `Type 'T[]' is not assignable to type
98
- // '() => T[]'`. The runtime in `runtime-dom/src/mount.ts:144-147`
99
- // already accepted both shapes — only the type was forcing the
100
- // accessor form. Type now accepts `T[] | (() => T[])` so users with
101
- // already-resolved arrays don't need to wrap them in a thunk just to
102
- // satisfy the type.
103
- test('each accepts T[] directly (not just () => T[])', () => {
104
- // TypeScript-level test: this would not compile pre-fix.
105
- const items = [1, 2, 3]
106
- const childFn = (n: number): VNode => h('li', { key: n }, String(n))
107
- const node = For<number>({ each: items, by: (n) => n, children: childFn })
108
- expect(node.type).toBe(ForSymbol as unknown as string)
109
- // Both shapes still work — function form continues to typecheck.
110
- const node2 = For<number>({
111
- each: () => items,
112
- by: (n) => n,
113
- children: childFn,
114
- })
115
- expect(node2.type).toBe(ForSymbol as unknown as string)
116
- })
117
- })
@@ -1,221 +0,0 @@
1
- import { EMPTY_PROPS, Fragment, h } from '../h'
2
- import type { ComponentFn, VNode, VNodeChild } from '../types'
3
-
4
- describe('h() — VNode creation', () => {
5
- describe('basic element creation', () => {
6
- test('creates VNode with string tag', () => {
7
- const node = h('div', null)
8
- expect(node.type).toBe('div')
9
- expect(node.props).toBe(EMPTY_PROPS)
10
- expect(node.children).toEqual([])
11
- expect(node.key).toBeNull()
12
- })
13
-
14
- test('creates VNode with props', () => {
15
- const node = h('div', { id: 'main', class: 'container' })
16
- expect(node.props.id).toBe('main')
17
- expect(node.props.class).toBe('container')
18
- })
19
-
20
- test('null props becomes EMPTY_PROPS sentinel', () => {
21
- const node1 = h('div', null)
22
- const node2 = h('span', null)
23
- // Both should use the same EMPTY_PROPS object (identity check)
24
- expect(node1.props).toBe(node2.props)
25
- expect(node1.props).toBe(EMPTY_PROPS)
26
- })
27
- })
28
-
29
- describe('key extraction', () => {
30
- test('extracts string key from props', () => {
31
- const node = h('li', { key: 'item-1' })
32
- expect(node.key).toBe('item-1')
33
- })
34
-
35
- test('extracts numeric key from props', () => {
36
- const node = h('li', { key: 42 })
37
- expect(node.key).toBe(42)
38
- })
39
-
40
- test('key is null when not provided', () => {
41
- const node = h('div', { class: 'x' })
42
- expect(node.key).toBeNull()
43
- })
44
-
45
- test('key is null for null props', () => {
46
- const node = h('div', null)
47
- expect(node.key).toBeNull()
48
- })
49
-
50
- test('key 0 is preserved (falsy but valid)', () => {
51
- const node = h('li', { key: 0 })
52
- expect(node.key).toBe(0)
53
- })
54
- })
55
-
56
- describe('children handling', () => {
57
- test('string children', () => {
58
- const node = h('p', null, 'hello')
59
- expect(node.children).toEqual(['hello'])
60
- })
61
-
62
- test('multiple string children', () => {
63
- const node = h('p', null, 'hello', ' ', 'world')
64
- expect(node.children).toEqual(['hello', ' ', 'world'])
65
- })
66
-
67
- test('number children', () => {
68
- const node = h('span', null, 42)
69
- expect(node.children).toEqual([42])
70
- })
71
-
72
- test('VNode children', () => {
73
- const child = h('span', null, 'inner')
74
- const parent = h('div', null, child)
75
- expect(parent.children).toHaveLength(1)
76
- expect((parent.children[0] as VNode).type).toBe('span')
77
- })
78
-
79
- test('mixed children types', () => {
80
- const child = h('em', null)
81
- const getter = () => 'reactive'
82
- const node = h('div', null, 'text', 42, child, null, undefined, true, false, getter)
83
- expect(node.children).toHaveLength(8)
84
- expect(node.children[0]).toBe('text')
85
- expect(node.children[1]).toBe(42)
86
- expect((node.children[2] as VNode).type).toBe('em')
87
- expect(node.children[3]).toBeNull()
88
- expect(node.children[4]).toBeUndefined()
89
- expect(node.children[5]).toBe(true)
90
- expect(node.children[6]).toBe(false)
91
- expect(typeof node.children[7]).toBe('function')
92
- })
93
-
94
- test('function children (reactive getters) are preserved', () => {
95
- const getter = () => 'dynamic'
96
- const node = h('div', null, getter)
97
- expect(node.children).toHaveLength(1)
98
- expect(typeof node.children[0]).toBe('function')
99
- expect((node.children[0] as () => string)()).toBe('dynamic')
100
- })
101
-
102
- test('no children produces empty array', () => {
103
- const node = h('br', null)
104
- expect(node.children).toEqual([])
105
- })
106
- })
107
-
108
- describe('children flattening', () => {
109
- test('flattens single-level array children', () => {
110
- const node = h('ul', null, [h('li', null, 'a'), h('li', null, 'b')])
111
- expect(node.children).toHaveLength(2)
112
- expect((node.children[0] as VNode).type).toBe('li')
113
- expect((node.children[1] as VNode).type).toBe('li')
114
- })
115
-
116
- test('flattens deeply nested arrays', () => {
117
- const node = h('div', null, [[['deep']]] as unknown as VNodeChild)
118
- expect(node.children).toEqual(['deep'])
119
- })
120
-
121
- test('flattens mixed nested/flat children', () => {
122
- const node = h('div', null, 'flat', ['nested-a', 'nested-b'] as unknown as VNodeChild)
123
- expect(node.children).toEqual(['flat', 'nested-a', 'nested-b'])
124
- })
125
-
126
- test('fast path: no allocation when children have no nested arrays', () => {
127
- // normalizeChildren returns as-is when no element is an array
128
- const node = h('div', null, 'a', 'b', 'c')
129
- expect(node.children).toEqual(['a', 'b', 'c'])
130
- expect(node.children).toHaveLength(3)
131
- })
132
-
133
- test('flattens multiple levels of nesting', () => {
134
- const node = h('div', null, [['a', ['b', ['c']]]] as unknown as VNodeChild)
135
- expect(node.children).toEqual(['a', 'b', 'c'])
136
- })
137
- })
138
-
139
- describe('component function type', () => {
140
- test('accepts component function as type', () => {
141
- const Comp: ComponentFn<{ name: string }> = (props) => h('span', null, props.name)
142
- const node = h(Comp, { name: 'test' })
143
- expect(node.type).toBe(Comp)
144
- expect(node.props.name).toBe('test')
145
- })
146
-
147
- test('component with no props', () => {
148
- const Comp: ComponentFn = () => h('div', null)
149
- const node = h(Comp, null)
150
- expect(node.type).toBe(Comp)
151
- expect(node.props).toBe(EMPTY_PROPS)
152
- })
153
-
154
- test('component with children rest args', () => {
155
- const Comp: ComponentFn = () => null
156
- const node = h(Comp, { id: 'x' }, 'child1', 'child2')
157
- expect(node.children).toEqual(['child1', 'child2'])
158
- })
159
- })
160
-
161
- describe('symbol type (Fragment)', () => {
162
- test('Fragment as type', () => {
163
- const node = h(Fragment, null, 'a', 'b')
164
- expect(node.type).toBe(Fragment)
165
- expect(node.children).toEqual(['a', 'b'])
166
- })
167
-
168
- test('Fragment with VNode children', () => {
169
- const node = h(Fragment, null, h('span', null, 'x'), h('em', null, 'y'))
170
- expect(node.children).toHaveLength(2)
171
- })
172
-
173
- test('nested Fragments', () => {
174
- const inner = h(Fragment, null, 'a', 'b')
175
- const outer = h(Fragment, null, inner, 'c')
176
- expect(outer.children).toHaveLength(2)
177
- expect((outer.children[0] as VNode).type).toBe(Fragment)
178
- })
179
-
180
- // Regression: pre-fix, `Fragment` was `Symbol('Pyreon.Fragment')` — a
181
- // fresh symbol per module evaluation. When `h.ts` got bundled into BOTH
182
- // `lib/index.js` AND `lib/jsx-runtime.js` (each a separate published
183
- // entry point), each bundle created a DISTINCT Symbol identity. JSX
184
- // `<>` compiles to `jsx(Fragment, ...)` referring to jsx-runtime's
185
- // Fragment; `runtime-server` checks `vnode.type === Fragment` against
186
- // `@pyreon/core`'s main-entry Fragment. The two never matched →
187
- // fell through to `renderElement` → tried to stringify the Symbol →
188
- // SSG crashed with `TypeError: Cannot convert a Symbol value to
189
- // a string`.
190
- //
191
- // Fix: use `Symbol.for('Pyreon.Fragment')` — the global registry keys
192
- // by string, so all bundles inlining h.ts share the same identity.
193
- //
194
- // This test asserts the global-registry contract: Fragment IS
195
- // retrievable from the registry. Bisect-verifiable: reverting h.ts to
196
- // `Symbol(...)` makes this fail.
197
- test('Fragment uses Symbol.for() for cross-bundle identity stability', () => {
198
- expect(Fragment).toBe(Symbol.for('Pyreon.Fragment'))
199
- })
200
- })
201
- })
202
-
203
- describe('EMPTY_PROPS', () => {
204
- test('is a plain object', () => {
205
- expect(typeof EMPTY_PROPS).toBe('object')
206
- expect(EMPTY_PROPS).not.toBeNull()
207
- })
208
-
209
- test('is the same reference for all null-prop VNodes', () => {
210
- const a = h('div', null)
211
- const b = h('span', null)
212
- expect(a.props).toBe(b.props)
213
- })
214
- })
215
-
216
- describe('Fragment', () => {
217
- test('is a unique symbol', () => {
218
- expect(typeof Fragment).toBe('symbol')
219
- expect(Fragment.toString()).toContain('Pyreon.Fragment')
220
- })
221
- })
@@ -1,86 +0,0 @@
1
- /**
2
- * JSX type compatibility test — verifies all JSX patterns produce correct VNodes.
3
- * Uses h() directly since core's vitest config doesn't have JSX transform.
4
- * TypeScript already validates JSX types via typecheck (tsc --noEmit).
5
- */
6
- import { createRef, Fragment, h } from '../index'
7
-
8
- describe('JSX type compat (via h)', () => {
9
- test('basic element', () => {
10
- const el = h('div', { class: 'hello' }, 'world')
11
- expect(el.type).toBe('div')
12
- expect(el.props.class).toBe('hello')
13
- })
14
-
15
- test('callback ref', () => {
16
- let _captured: Element | null = null
17
- const el = h(
18
- 'div',
19
- {
20
- ref: (e: Element) => {
21
- _captured = e
22
- },
23
- },
24
- 'test',
25
- )
26
- expect(typeof el.props.ref).toBe('function')
27
- })
28
-
29
- test('object ref', () => {
30
- const myRef = createRef<HTMLDivElement>()
31
- const el = h('div', { ref: myRef }, 'test')
32
- expect(el.props.ref).toBe(myRef)
33
- })
34
-
35
- test('reactive class prop', () => {
36
- const el = h('span', { class: () => 'active' }, 'hello')
37
- expect(typeof el.props.class).toBe('function')
38
- })
39
-
40
- test('input with typed props', () => {
41
- const el = h('input', { type: 'text', value: 'test' })
42
- expect(el.type).toBe('input')
43
- expect(el.props.type).toBe('text')
44
- })
45
-
46
- test('component with children', () => {
47
- const MyComp = (props: { name: string; children?: unknown }) => {
48
- return h('div', null, String(props.name))
49
- }
50
- const el = h(MyComp, { name: 'test' }, 'child')
51
- expect(typeof el.type).toBe('function')
52
- expect(el.props.name).toBe('test')
53
- })
54
-
55
- test('fragment', () => {
56
- const el = h(Fragment, null, 'fragment')
57
- expect(el.type).toBe(Fragment)
58
- })
59
-
60
- test('event handler', () => {
61
- const handler = vi.fn()
62
- const el = h('button', { onClick: handler }, 'click')
63
- expect(el.props.onClick).toBe(handler)
64
- })
65
-
66
- test('style as object', () => {
67
- const el = h('div', { style: { color: 'red' } }, 'styled')
68
- expect(el.props.style).toEqual({ color: 'red' })
69
- })
70
-
71
- test('data attributes', () => {
72
- const el = h('div', { 'data-testid': 'foo' }, 'test')
73
- expect(el.props['data-testid']).toBe('foo')
74
- })
75
-
76
- test('aria attributes', () => {
77
- const el = h('div', { 'aria-label': 'close', role: 'button' })
78
- expect(el.props['aria-label']).toBe('close')
79
- expect(el.props.role).toBe('button')
80
- })
81
-
82
- test('key prop', () => {
83
- const el = h('li', { key: 'item-1' }, 'item')
84
- expect(el.key).toBe('item-1')
85
- })
86
- })
@@ -1,100 +0,0 @@
1
- import { h } from '../h'
2
- import { lazy } from '../lazy'
3
- import type { ComponentFn, Props, VNode } from '../types'
4
-
5
- describe('lazy', () => {
6
- test('returns a LazyComponent with __loading flag', () => {
7
- const Comp = lazy<Props>(() => new Promise(() => {})) // never resolves
8
- expect(typeof Comp).toBe('function')
9
- expect(typeof Comp.__loading).toBe('function')
10
- expect(Comp.__loading()).toBe(true)
11
- })
12
-
13
- test('__loading returns true while loading', () => {
14
- const Comp = lazy<Props>(() => new Promise(() => {}))
15
- expect(Comp.__loading()).toBe(true)
16
- })
17
-
18
- test('returns null while loading (component not yet available)', () => {
19
- const Comp = lazy<Props>(() => new Promise(() => {}))
20
- const result = Comp({})
21
- expect(result).toBeNull()
22
- })
23
-
24
- test('resolves to the loaded component', async () => {
25
- const Inner: ComponentFn<{ name: string }> = (props) => h('span', null, props.name)
26
- const Comp = lazy(() => Promise.resolve({ default: Inner }))
27
-
28
- await new Promise((r) => setTimeout(r, 0))
29
-
30
- expect(Comp.__loading()).toBe(false)
31
- const result = Comp({ name: 'test' })
32
- expect(result).not.toBeNull()
33
- expect((result as VNode).type).toBe(Inner)
34
- expect((result as VNode).props).toEqual({ name: 'test' })
35
- })
36
-
37
- test('throws on import error', async () => {
38
- const Comp = lazy<Props>(() => Promise.reject(new Error('load failed')))
39
-
40
- await new Promise((r) => setTimeout(r, 0))
41
-
42
- expect(Comp.__loading()).toBe(false)
43
- expect(() => Comp({})).toThrow('load failed')
44
- })
45
-
46
- test('wraps non-Error rejection in Error', async () => {
47
- const Comp = lazy<Props>(() => Promise.reject('string-error'))
48
-
49
- await new Promise((r) => setTimeout(r, 0))
50
-
51
- expect(() => Comp({})).toThrow('string-error')
52
- })
53
-
54
- test('wraps numeric rejection in Error', async () => {
55
- const Comp = lazy<Props>(() => Promise.reject(404))
56
-
57
- await new Promise((r) => setTimeout(r, 0))
58
-
59
- expect(() => Comp({})).toThrow('404')
60
- })
61
-
62
- test('__loading is false after successful load', async () => {
63
- const Inner: ComponentFn = () => null
64
- const Comp = lazy(() => Promise.resolve({ default: Inner }))
65
-
66
- expect(Comp.__loading()).toBe(true)
67
- await new Promise((r) => setTimeout(r, 0))
68
- expect(Comp.__loading()).toBe(false)
69
- })
70
-
71
- test('__loading is false after failed load', async () => {
72
- const Comp = lazy<Props>(() => Promise.reject(new Error('fail')))
73
-
74
- expect(Comp.__loading()).toBe(true)
75
- await new Promise((r) => setTimeout(r, 0))
76
- expect(Comp.__loading()).toBe(false)
77
- })
78
-
79
- test('multiple calls after load return consistent results', async () => {
80
- const Inner: ComponentFn = () => h('div', null, 'content')
81
- const Comp = lazy(() => Promise.resolve({ default: Inner }))
82
-
83
- await new Promise((r) => setTimeout(r, 0))
84
-
85
- const result1 = Comp({})
86
- const result2 = Comp({})
87
- expect((result1 as VNode).type).toBe(Inner)
88
- expect((result2 as VNode).type).toBe(Inner)
89
- })
90
-
91
- test('passes props through to loaded component via h()', async () => {
92
- const Inner: ComponentFn<{ count: number }> = (props) => h('span', null, String(props.count))
93
- const Comp = lazy(() => Promise.resolve({ default: Inner }))
94
-
95
- await new Promise((r) => setTimeout(r, 0))
96
-
97
- const result = Comp({ count: 42 })
98
- expect((result as VNode).props).toEqual({ count: 42 })
99
- })
100
- })