@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.
@@ -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
+ })