@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +53 -31
- package/package.json +2 -6
- package/src/compat-marker.ts +0 -79
- package/src/compat-shared.ts +0 -80
- package/src/component.ts +0 -98
- package/src/context.ts +0 -349
- package/src/defer.ts +0 -279
- package/src/dynamic.ts +0 -32
- package/src/env.d.ts +0 -6
- package/src/error-boundary.ts +0 -90
- package/src/for.ts +0 -51
- package/src/h.ts +0 -80
- package/src/index.ts +0 -80
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -747
- package/src/lazy.ts +0 -25
- package/src/lifecycle.ts +0 -152
- package/src/manifest.ts +0 -579
- package/src/map-array.ts +0 -42
- package/src/portal.ts +0 -39
- package/src/props.ts +0 -269
- package/src/ref.ts +0 -32
- package/src/show.ts +0 -121
- package/src/style.ts +0 -102
- package/src/suspense.ts +0 -52
- package/src/telemetry.ts +0 -120
- package/src/tests/compat-marker.test.ts +0 -96
- package/src/tests/compat-shared.test.ts +0 -99
- package/src/tests/component.test.ts +0 -281
- package/src/tests/context.test.ts +0 -629
- package/src/tests/core.test.ts +0 -1290
- package/src/tests/cx.test.ts +0 -70
- package/src/tests/defer.test.ts +0 -359
- package/src/tests/dynamic.test.ts +0 -87
- package/src/tests/error-boundary.test.ts +0 -181
- package/src/tests/extract-props-overloads.types.test.ts +0 -135
- package/src/tests/for.test.ts +0 -117
- package/src/tests/h.test.ts +0 -221
- package/src/tests/jsx-compat.test.tsx +0 -86
- package/src/tests/lazy.test.ts +0 -100
- package/src/tests/lifecycle.test.ts +0 -350
- package/src/tests/manifest-snapshot.test.ts +0 -100
- package/src/tests/map-array.test.ts +0 -313
- package/src/tests/native-marker-error-boundary.test.ts +0 -12
- package/src/tests/portal.test.ts +0 -48
- package/src/tests/props-extended.test.ts +0 -157
- package/src/tests/props.test.ts +0 -250
- package/src/tests/reactive-context.test.ts +0 -69
- package/src/tests/reactive-props.test.ts +0 -157
- package/src/tests/ref.test.ts +0 -70
- package/src/tests/show.test.ts +0 -314
- package/src/tests/style.test.ts +0 -157
- package/src/tests/suspense.test.ts +0 -139
- package/src/tests/telemetry.test.ts +0 -297
- 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
|
-
})
|
package/src/tests/for.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/tests/h.test.ts
DELETED
|
@@ -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
|
-
})
|
package/src/tests/lazy.test.ts
DELETED
|
@@ -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
|
-
})
|