@pyreon/core 0.11.2 → 0.11.4
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 +2 -2
- package/src/tests/component.test.ts +281 -0
- package/src/tests/context.test.ts +263 -0
- package/src/tests/dynamic.test.ts +55 -0
- package/src/tests/error-boundary.test.ts +181 -0
- package/src/tests/for.test.ts +94 -0
- package/src/tests/h.test.ts +200 -0
- package/src/tests/lazy.test.ts +100 -0
- package/src/tests/lifecycle.test.ts +208 -0
- package/src/tests/map-array.test.ts +313 -0
- package/src/tests/portal.test.ts +48 -0
- package/src/tests/props-extended.test.ts +157 -0
- package/src/tests/ref.test.ts +70 -0
- package/src/tests/show.test.ts +238 -0
- package/src/tests/style.test.ts +157 -0
- package/src/tests/suspense.test.ts +139 -0
- package/src/tests/telemetry.test.ts +142 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { dispatchToErrorBoundary, popErrorBoundary, runWithHooks } from "../component"
|
|
2
|
+
import { ErrorBoundary } from "../error-boundary"
|
|
3
|
+
import { h } from "../h"
|
|
4
|
+
import type { VNodeChild } from "../types"
|
|
5
|
+
|
|
6
|
+
describe("ErrorBoundary", () => {
|
|
7
|
+
// Clean up error boundary stack after each test
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
// Pop all boundaries that tests may have left
|
|
10
|
+
while (dispatchToErrorBoundary("cleanup")) {
|
|
11
|
+
popErrorBoundary()
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("is a function", () => {
|
|
16
|
+
expect(typeof ErrorBoundary).toBe("function")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("returns a reactive getter", () => {
|
|
20
|
+
let result: VNodeChild = null
|
|
21
|
+
runWithHooks(() => {
|
|
22
|
+
result = ErrorBoundary({
|
|
23
|
+
fallback: (err) => `Error: ${err}`,
|
|
24
|
+
children: "child",
|
|
25
|
+
})
|
|
26
|
+
return null
|
|
27
|
+
}, {})
|
|
28
|
+
expect(typeof result).toBe("function")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("renders children when no error", () => {
|
|
32
|
+
let result: VNodeChild = null
|
|
33
|
+
runWithHooks(() => {
|
|
34
|
+
result = ErrorBoundary({
|
|
35
|
+
fallback: (err) => `Error: ${err}`,
|
|
36
|
+
children: "child content",
|
|
37
|
+
})
|
|
38
|
+
return null
|
|
39
|
+
}, {})
|
|
40
|
+
const getter = result as unknown as () => VNodeChild
|
|
41
|
+
expect(getter()).toBe("child content")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("renders function children by calling them", () => {
|
|
45
|
+
let result: VNodeChild = null
|
|
46
|
+
runWithHooks(() => {
|
|
47
|
+
result = ErrorBoundary({
|
|
48
|
+
fallback: (err) => `Error: ${err}`,
|
|
49
|
+
children: () => "dynamic child",
|
|
50
|
+
})
|
|
51
|
+
return null
|
|
52
|
+
}, {})
|
|
53
|
+
const getter = result as unknown as () => VNodeChild
|
|
54
|
+
expect(getter()).toBe("dynamic child")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("renders VNode children", () => {
|
|
58
|
+
let result: VNodeChild = null
|
|
59
|
+
const child = h("div", null, "content")
|
|
60
|
+
runWithHooks(() => {
|
|
61
|
+
result = ErrorBoundary({
|
|
62
|
+
fallback: (err) => `Error: ${err}`,
|
|
63
|
+
children: child,
|
|
64
|
+
})
|
|
65
|
+
return null
|
|
66
|
+
}, {})
|
|
67
|
+
const getter = result as unknown as () => VNodeChild
|
|
68
|
+
expect(getter()).toBe(child)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("registers unmount cleanup hook", () => {
|
|
72
|
+
const { hooks } = runWithHooks(() => {
|
|
73
|
+
ErrorBoundary({
|
|
74
|
+
fallback: (err) => `Error: ${err}`,
|
|
75
|
+
children: "child",
|
|
76
|
+
})
|
|
77
|
+
return null
|
|
78
|
+
}, {})
|
|
79
|
+
expect(hooks.unmount.length).toBeGreaterThanOrEqual(1)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("warns when fallback is not a function", () => {
|
|
83
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
84
|
+
runWithHooks(() => {
|
|
85
|
+
ErrorBoundary({
|
|
86
|
+
fallback: "not-a-function" as unknown as (err: unknown, reset: () => void) => VNodeChild,
|
|
87
|
+
children: "child",
|
|
88
|
+
})
|
|
89
|
+
return null
|
|
90
|
+
}, {})
|
|
91
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("<ErrorBoundary>"))
|
|
92
|
+
warnSpy.mockRestore()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("dispatched error triggers fallback rendering", () => {
|
|
96
|
+
let result: VNodeChild = null
|
|
97
|
+
runWithHooks(() => {
|
|
98
|
+
result = ErrorBoundary({
|
|
99
|
+
fallback: (err) => `Caught: ${err}`,
|
|
100
|
+
children: "normal",
|
|
101
|
+
})
|
|
102
|
+
return null
|
|
103
|
+
}, {})
|
|
104
|
+
const getter = result as unknown as () => VNodeChild
|
|
105
|
+
expect(getter()).toBe("normal")
|
|
106
|
+
|
|
107
|
+
dispatchToErrorBoundary(new Error("boom"))
|
|
108
|
+
expect(getter()).toBe("Caught: Error: boom")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("fallback receives reset function that clears error", () => {
|
|
112
|
+
let result: VNodeChild = null
|
|
113
|
+
let capturedReset: (() => void) | undefined
|
|
114
|
+
runWithHooks(() => {
|
|
115
|
+
result = ErrorBoundary({
|
|
116
|
+
fallback: (_err, reset) => {
|
|
117
|
+
capturedReset = reset
|
|
118
|
+
return "error-ui"
|
|
119
|
+
},
|
|
120
|
+
children: "child",
|
|
121
|
+
})
|
|
122
|
+
return null
|
|
123
|
+
}, {})
|
|
124
|
+
const getter = result as unknown as () => VNodeChild
|
|
125
|
+
expect(getter()).toBe("child")
|
|
126
|
+
|
|
127
|
+
dispatchToErrorBoundary("test-error")
|
|
128
|
+
expect(getter()).toBe("error-ui")
|
|
129
|
+
expect(capturedReset).toBeDefined()
|
|
130
|
+
|
|
131
|
+
capturedReset?.()
|
|
132
|
+
expect(getter()).toBe("child")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("second error while already in error state is not handled", () => {
|
|
136
|
+
let result: VNodeChild = null
|
|
137
|
+
runWithHooks(() => {
|
|
138
|
+
result = ErrorBoundary({
|
|
139
|
+
fallback: (err) => `Error: ${err}`,
|
|
140
|
+
children: "child",
|
|
141
|
+
})
|
|
142
|
+
return null
|
|
143
|
+
}, {})
|
|
144
|
+
const getter = result as unknown as () => VNodeChild
|
|
145
|
+
|
|
146
|
+
// First error handled
|
|
147
|
+
expect(dispatchToErrorBoundary("first")).toBe(true)
|
|
148
|
+
expect(getter()).toBe("Error: first")
|
|
149
|
+
|
|
150
|
+
// Second error not handled (already in error state)
|
|
151
|
+
expect(dispatchToErrorBoundary("second")).toBe(false)
|
|
152
|
+
// Still showing first error
|
|
153
|
+
expect(getter()).toBe("Error: first")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("after reset, new error can be caught again", () => {
|
|
157
|
+
let result: VNodeChild = null
|
|
158
|
+
let capturedReset: (() => void) | undefined
|
|
159
|
+
runWithHooks(() => {
|
|
160
|
+
result = ErrorBoundary({
|
|
161
|
+
fallback: (err, reset) => {
|
|
162
|
+
capturedReset = reset
|
|
163
|
+
return `Error: ${err}`
|
|
164
|
+
},
|
|
165
|
+
children: "child",
|
|
166
|
+
})
|
|
167
|
+
return null
|
|
168
|
+
}, {})
|
|
169
|
+
const getter = result as unknown as () => VNodeChild
|
|
170
|
+
|
|
171
|
+
dispatchToErrorBoundary("first-error")
|
|
172
|
+
expect(getter()).toBe("Error: first-error")
|
|
173
|
+
|
|
174
|
+
capturedReset?.()
|
|
175
|
+
expect(getter()).toBe("child")
|
|
176
|
+
|
|
177
|
+
// Can catch new error after reset
|
|
178
|
+
expect(dispatchToErrorBoundary("second-error")).toBe(true)
|
|
179
|
+
expect(getter()).toBe("Error: second-error")
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
})
|
|
181
|
+
|
|
182
|
+
describe("EMPTY_PROPS", () => {
|
|
183
|
+
test("is a plain object", () => {
|
|
184
|
+
expect(typeof EMPTY_PROPS).toBe("object")
|
|
185
|
+
expect(EMPTY_PROPS).not.toBeNull()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("is the same reference for all null-prop VNodes", () => {
|
|
189
|
+
const a = h("div", null)
|
|
190
|
+
const b = h("span", null)
|
|
191
|
+
expect(a.props).toBe(b.props)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("Fragment", () => {
|
|
196
|
+
test("is a unique symbol", () => {
|
|
197
|
+
expect(typeof Fragment).toBe("symbol")
|
|
198
|
+
expect(Fragment.toString()).toContain("Pyreon.Fragment")
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
})
|