@pyreon/core 0.11.3 → 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,139 @@
|
|
|
1
|
+
import { Fragment, h } from "../h"
|
|
2
|
+
import { Suspense } from "../suspense"
|
|
3
|
+
import type { ComponentFn, VNodeChild } from "../types"
|
|
4
|
+
|
|
5
|
+
describe("Suspense", () => {
|
|
6
|
+
test("returns a Fragment VNode", () => {
|
|
7
|
+
const node = Suspense({
|
|
8
|
+
fallback: h("div", null, "loading"),
|
|
9
|
+
children: h("div", null, "content"),
|
|
10
|
+
})
|
|
11
|
+
expect(node.type).toBe(Fragment)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("Fragment contains a single reactive getter child", () => {
|
|
15
|
+
const node = Suspense({
|
|
16
|
+
fallback: h("span", null, "loading"),
|
|
17
|
+
children: h("div", null, "content"),
|
|
18
|
+
})
|
|
19
|
+
expect(node.children).toHaveLength(1)
|
|
20
|
+
expect(typeof node.children[0]).toBe("function")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("renders children when not loading (plain VNode)", () => {
|
|
24
|
+
const child = h("div", null, "loaded")
|
|
25
|
+
const node = Suspense({
|
|
26
|
+
fallback: h("span", null, "loading"),
|
|
27
|
+
children: child,
|
|
28
|
+
})
|
|
29
|
+
const getter = node.children[0] as () => VNodeChild
|
|
30
|
+
expect(getter()).toBe(child)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("renders children when child type has no __loading", () => {
|
|
34
|
+
const regularComp: ComponentFn = () => h("div", null)
|
|
35
|
+
const child = h(regularComp, null)
|
|
36
|
+
const node = Suspense({
|
|
37
|
+
fallback: "loading",
|
|
38
|
+
children: child,
|
|
39
|
+
})
|
|
40
|
+
const getter = node.children[0] as () => VNodeChild
|
|
41
|
+
expect(getter()).toBe(child)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("renders fallback when child __loading() is true", () => {
|
|
45
|
+
const fallback = h("span", null, "loading...")
|
|
46
|
+
const lazyFn = (() => h("div", null)) as unknown as ComponentFn & {
|
|
47
|
+
__loading: () => boolean
|
|
48
|
+
}
|
|
49
|
+
lazyFn.__loading = () => true
|
|
50
|
+
const child = h(lazyFn, null)
|
|
51
|
+
|
|
52
|
+
const node = Suspense({ fallback, children: child })
|
|
53
|
+
const getter = node.children[0] as () => VNodeChild
|
|
54
|
+
expect(getter()).toBe(fallback)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("renders children when __loading() is false", () => {
|
|
58
|
+
const fallback = h("span", null, "loading...")
|
|
59
|
+
const lazyFn = (() => h("div", null)) as unknown as ComponentFn & {
|
|
60
|
+
__loading: () => boolean
|
|
61
|
+
}
|
|
62
|
+
lazyFn.__loading = () => false
|
|
63
|
+
const child = h(lazyFn, null)
|
|
64
|
+
|
|
65
|
+
const node = Suspense({ fallback, children: child })
|
|
66
|
+
const getter = node.children[0] as () => VNodeChild
|
|
67
|
+
expect(getter()).toBe(child)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("handles function children (reactive getter)", () => {
|
|
71
|
+
const child = h("div", null, "content")
|
|
72
|
+
const node = Suspense({
|
|
73
|
+
fallback: h("span", null, "loading"),
|
|
74
|
+
children: () => child,
|
|
75
|
+
})
|
|
76
|
+
const getter = node.children[0] as () => VNodeChild
|
|
77
|
+
expect(getter()).toBe(child)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("evaluates function fallback", () => {
|
|
81
|
+
const fbNode = h("div", null, "fb")
|
|
82
|
+
const lazyFn = (() => h("div", null)) as unknown as ComponentFn & {
|
|
83
|
+
__loading: () => boolean
|
|
84
|
+
}
|
|
85
|
+
lazyFn.__loading = () => true
|
|
86
|
+
const child = h(lazyFn, null)
|
|
87
|
+
|
|
88
|
+
const node = Suspense({ fallback: () => fbNode, children: child })
|
|
89
|
+
const getter = node.children[0] as () => VNodeChild
|
|
90
|
+
expect(getter()).toBe(fbNode)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("handles null/undefined children", () => {
|
|
94
|
+
const node = Suspense({ fallback: "loading" })
|
|
95
|
+
const getter = node.children[0] as () => VNodeChild
|
|
96
|
+
// undefined children — not a VNode, not loading
|
|
97
|
+
expect(getter()).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test("handles string children", () => {
|
|
101
|
+
const node = Suspense({ fallback: "loading", children: "text content" })
|
|
102
|
+
const getter = node.children[0] as () => VNodeChild
|
|
103
|
+
expect(getter()).toBe("text content")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("handles array children (not loading)", () => {
|
|
107
|
+
const children = [h("a", null), h("b", null)]
|
|
108
|
+
const node = Suspense({
|
|
109
|
+
fallback: "loading",
|
|
110
|
+
children: children as unknown as VNodeChild,
|
|
111
|
+
})
|
|
112
|
+
const getter = node.children[0] as () => VNodeChild
|
|
113
|
+
expect(getter()).toBe(children)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("warns when fallback prop is missing", () => {
|
|
117
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
118
|
+
Suspense({ fallback: undefined as unknown as VNodeChild, children: "x" })
|
|
119
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("<Suspense>"))
|
|
120
|
+
warnSpy.mockRestore()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("transition from loading to loaded", () => {
|
|
124
|
+
let isLoading = true
|
|
125
|
+
const lazyFn = (() => h("div", null)) as unknown as ComponentFn & {
|
|
126
|
+
__loading: () => boolean
|
|
127
|
+
}
|
|
128
|
+
lazyFn.__loading = () => isLoading
|
|
129
|
+
const child = h(lazyFn, null)
|
|
130
|
+
const fallback = h("span", null, "loading")
|
|
131
|
+
|
|
132
|
+
const node = Suspense({ fallback, children: child })
|
|
133
|
+
const getter = node.children[0] as () => VNodeChild
|
|
134
|
+
|
|
135
|
+
expect(getter()).toBe(fallback)
|
|
136
|
+
isLoading = false
|
|
137
|
+
expect(getter()).toBe(child)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ErrorContext } from "../telemetry"
|
|
2
|
+
import { registerErrorHandler, reportError } from "../telemetry"
|
|
3
|
+
|
|
4
|
+
describe("registerErrorHandler", () => {
|
|
5
|
+
test("registers handler that receives error context", () => {
|
|
6
|
+
const contexts: ErrorContext[] = []
|
|
7
|
+
const unsub = registerErrorHandler((ctx) => {
|
|
8
|
+
contexts.push(ctx)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const ctx: ErrorContext = {
|
|
12
|
+
component: "TestComp",
|
|
13
|
+
phase: "render",
|
|
14
|
+
error: new Error("test"),
|
|
15
|
+
timestamp: 1234567890,
|
|
16
|
+
}
|
|
17
|
+
reportError(ctx)
|
|
18
|
+
expect(contexts).toHaveLength(1)
|
|
19
|
+
expect(contexts[0]).toBe(ctx)
|
|
20
|
+
|
|
21
|
+
unsub()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("returns unregister function", () => {
|
|
25
|
+
let count = 0
|
|
26
|
+
const unsub = registerErrorHandler(() => {
|
|
27
|
+
count++
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
reportError({ component: "A", phase: "setup", error: "e1", timestamp: 0 })
|
|
31
|
+
expect(count).toBe(1)
|
|
32
|
+
|
|
33
|
+
unsub()
|
|
34
|
+
|
|
35
|
+
reportError({ component: "B", phase: "render", error: "e2", timestamp: 0 })
|
|
36
|
+
expect(count).toBe(1) // not called after unregister
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("multiple handlers are all called", () => {
|
|
40
|
+
let count = 0
|
|
41
|
+
const unsub1 = registerErrorHandler(() => count++)
|
|
42
|
+
const unsub2 = registerErrorHandler(() => count++)
|
|
43
|
+
const unsub3 = registerErrorHandler(() => count++)
|
|
44
|
+
|
|
45
|
+
reportError({ component: "X", phase: "mount", error: "err", timestamp: 0 })
|
|
46
|
+
expect(count).toBe(3)
|
|
47
|
+
|
|
48
|
+
unsub1()
|
|
49
|
+
unsub2()
|
|
50
|
+
unsub3()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("handler errors are swallowed — subsequent handlers still called", () => {
|
|
54
|
+
let secondCalled = false
|
|
55
|
+
let thirdCalled = false
|
|
56
|
+
|
|
57
|
+
const unsub1 = registerErrorHandler(() => {
|
|
58
|
+
throw new Error("handler crash")
|
|
59
|
+
})
|
|
60
|
+
const unsub2 = registerErrorHandler(() => {
|
|
61
|
+
secondCalled = true
|
|
62
|
+
})
|
|
63
|
+
const unsub3 = registerErrorHandler(() => {
|
|
64
|
+
thirdCalled = true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Should not throw
|
|
68
|
+
expect(() =>
|
|
69
|
+
reportError({ component: "Y", phase: "unmount", error: "err", timestamp: 0 }),
|
|
70
|
+
).not.toThrow()
|
|
71
|
+
expect(secondCalled).toBe(true)
|
|
72
|
+
expect(thirdCalled).toBe(true)
|
|
73
|
+
|
|
74
|
+
unsub1()
|
|
75
|
+
unsub2()
|
|
76
|
+
unsub3()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("unregistering one handler does not affect others", () => {
|
|
80
|
+
const calls: string[] = []
|
|
81
|
+
const unsub1 = registerErrorHandler(() => calls.push("a"))
|
|
82
|
+
const unsub2 = registerErrorHandler(() => calls.push("b"))
|
|
83
|
+
const unsub3 = registerErrorHandler(() => calls.push("c"))
|
|
84
|
+
|
|
85
|
+
unsub2() // remove middle handler
|
|
86
|
+
|
|
87
|
+
reportError({ component: "Z", phase: "effect", error: "e", timestamp: 0 })
|
|
88
|
+
expect(calls).toEqual(["a", "c"])
|
|
89
|
+
|
|
90
|
+
unsub1()
|
|
91
|
+
unsub3()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe("reportError", () => {
|
|
96
|
+
test("no-op when no handlers registered", () => {
|
|
97
|
+
// Should not throw
|
|
98
|
+
expect(() =>
|
|
99
|
+
reportError({ component: "None", phase: "setup", error: "err", timestamp: 0 }),
|
|
100
|
+
).not.toThrow()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("passes full ErrorContext to handler", () => {
|
|
104
|
+
let received: ErrorContext | null = null
|
|
105
|
+
const unsub = registerErrorHandler((ctx) => {
|
|
106
|
+
received = ctx
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const ctx: ErrorContext = {
|
|
110
|
+
component: "MyComp",
|
|
111
|
+
phase: "render",
|
|
112
|
+
error: new Error("detail"),
|
|
113
|
+
timestamp: 999,
|
|
114
|
+
props: { a: 1, b: "two" },
|
|
115
|
+
}
|
|
116
|
+
reportError(ctx)
|
|
117
|
+
|
|
118
|
+
expect(received).not.toBeNull()
|
|
119
|
+
expect(received!.component).toBe("MyComp")
|
|
120
|
+
expect(received!.phase).toBe("render")
|
|
121
|
+
expect(received!.error).toBeInstanceOf(Error)
|
|
122
|
+
expect(received!.timestamp).toBe(999)
|
|
123
|
+
expect(received!.props).toEqual({ a: 1, b: "two" })
|
|
124
|
+
|
|
125
|
+
unsub()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("handles all phase types", () => {
|
|
129
|
+
const phases: ErrorContext["phase"][] = ["setup", "render", "mount", "unmount", "effect"]
|
|
130
|
+
const seen: string[] = []
|
|
131
|
+
const unsub = registerErrorHandler((ctx) => {
|
|
132
|
+
seen.push(ctx.phase)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
for (const phase of phases) {
|
|
136
|
+
reportError({ component: "X", phase, error: "e", timestamp: 0 })
|
|
137
|
+
}
|
|
138
|
+
expect(seen).toEqual(phases)
|
|
139
|
+
|
|
140
|
+
unsub()
|
|
141
|
+
})
|
|
142
|
+
})
|