@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,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentHooks,
|
|
3
|
+
onErrorCaptured,
|
|
4
|
+
onMount,
|
|
5
|
+
onUnmount,
|
|
6
|
+
onUpdate,
|
|
7
|
+
setCurrentHooks,
|
|
8
|
+
} from "../lifecycle"
|
|
9
|
+
import type { LifecycleHooks } from "../types"
|
|
10
|
+
|
|
11
|
+
describe("setCurrentHooks / getCurrentHooks", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
setCurrentHooks(null)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("getCurrentHooks returns null by default", () => {
|
|
17
|
+
expect(getCurrentHooks()).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("setCurrentHooks sets the current hooks context", () => {
|
|
21
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
22
|
+
setCurrentHooks(hooks)
|
|
23
|
+
expect(getCurrentHooks()).toBe(hooks)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("setCurrentHooks(null) clears the context", () => {
|
|
27
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
28
|
+
setCurrentHooks(hooks)
|
|
29
|
+
expect(getCurrentHooks()).toBe(hooks)
|
|
30
|
+
setCurrentHooks(null)
|
|
31
|
+
expect(getCurrentHooks()).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe("onMount", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
setCurrentHooks(null)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("registers callback on current hooks", () => {
|
|
41
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
42
|
+
setCurrentHooks(hooks)
|
|
43
|
+
const fn = () => undefined
|
|
44
|
+
onMount(fn)
|
|
45
|
+
expect(hooks.mount).toHaveLength(1)
|
|
46
|
+
expect(hooks.mount[0]).toBe(fn)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("multiple onMount calls accumulate", () => {
|
|
50
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
51
|
+
setCurrentHooks(hooks)
|
|
52
|
+
onMount(() => undefined)
|
|
53
|
+
onMount(() => undefined)
|
|
54
|
+
onMount(() => undefined)
|
|
55
|
+
expect(hooks.mount).toHaveLength(3)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("warns when called outside component setup", () => {
|
|
59
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
60
|
+
onMount(() => {})
|
|
61
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
62
|
+
expect.stringContaining("onMount() called outside component setup"),
|
|
63
|
+
)
|
|
64
|
+
warnSpy.mockRestore()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("is a no-op outside component setup (no crash)", () => {
|
|
68
|
+
expect(() => onMount(() => {})).not.toThrow()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("accepts callback returning cleanup function", () => {
|
|
72
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
73
|
+
setCurrentHooks(hooks)
|
|
74
|
+
const cleanup = () => {}
|
|
75
|
+
onMount(() => cleanup)
|
|
76
|
+
expect(hooks.mount).toHaveLength(1)
|
|
77
|
+
expect(hooks.mount[0]!()).toBe(cleanup)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("accepts callback returning void", () => {
|
|
81
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
82
|
+
setCurrentHooks(hooks)
|
|
83
|
+
onMount(() => {})
|
|
84
|
+
expect(hooks.mount).toHaveLength(1)
|
|
85
|
+
expect(hooks.mount[0]!()).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe("onUnmount", () => {
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
setCurrentHooks(null)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("registers callback on current hooks", () => {
|
|
95
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
96
|
+
setCurrentHooks(hooks)
|
|
97
|
+
const fn = () => {}
|
|
98
|
+
onUnmount(fn)
|
|
99
|
+
expect(hooks.unmount).toHaveLength(1)
|
|
100
|
+
expect(hooks.unmount[0]).toBe(fn)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("warns when called outside component setup", () => {
|
|
104
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
105
|
+
onUnmount(() => {})
|
|
106
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
107
|
+
expect.stringContaining("onUnmount() called outside component setup"),
|
|
108
|
+
)
|
|
109
|
+
warnSpy.mockRestore()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("onUpdate", () => {
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
setCurrentHooks(null)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test("registers callback on current hooks", () => {
|
|
119
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
120
|
+
setCurrentHooks(hooks)
|
|
121
|
+
const fn = () => {}
|
|
122
|
+
onUpdate(fn)
|
|
123
|
+
expect(hooks.update).toHaveLength(1)
|
|
124
|
+
expect(hooks.update[0]).toBe(fn)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("warns when called outside component setup", () => {
|
|
128
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
129
|
+
onUpdate(() => {})
|
|
130
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
131
|
+
expect.stringContaining("onUpdate() called outside component setup"),
|
|
132
|
+
)
|
|
133
|
+
warnSpy.mockRestore()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe("onErrorCaptured", () => {
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
setCurrentHooks(null)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("registers callback on current hooks", () => {
|
|
143
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
144
|
+
setCurrentHooks(hooks)
|
|
145
|
+
const fn = () => true
|
|
146
|
+
onErrorCaptured(fn)
|
|
147
|
+
expect(hooks.error).toHaveLength(1)
|
|
148
|
+
expect(hooks.error[0]).toBe(fn)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("warns when called outside component setup", () => {
|
|
152
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
153
|
+
onErrorCaptured(() => true)
|
|
154
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
155
|
+
expect.stringContaining("onErrorCaptured() called outside component setup"),
|
|
156
|
+
)
|
|
157
|
+
warnSpy.mockRestore()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("registered handler receives the error", () => {
|
|
161
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
162
|
+
setCurrentHooks(hooks)
|
|
163
|
+
let captured: unknown = null
|
|
164
|
+
onErrorCaptured((err) => {
|
|
165
|
+
captured = err
|
|
166
|
+
return true
|
|
167
|
+
})
|
|
168
|
+
// Simulate calling the handler
|
|
169
|
+
const testError = new Error("test")
|
|
170
|
+
hooks.error[0]!(testError)
|
|
171
|
+
expect(captured).toBe(testError)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe("lifecycle hooks interaction", () => {
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
setCurrentHooks(null)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("all hook types can be registered in same context", () => {
|
|
181
|
+
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
182
|
+
setCurrentHooks(hooks)
|
|
183
|
+
|
|
184
|
+
onMount(() => undefined)
|
|
185
|
+
onUnmount(() => {})
|
|
186
|
+
onUpdate(() => {})
|
|
187
|
+
onErrorCaptured(() => true)
|
|
188
|
+
|
|
189
|
+
expect(hooks.mount).toHaveLength(1)
|
|
190
|
+
expect(hooks.unmount).toHaveLength(1)
|
|
191
|
+
expect(hooks.update).toHaveLength(1)
|
|
192
|
+
expect(hooks.error).toHaveLength(1)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("hooks from different setCurrentHooks calls go to different stores", () => {
|
|
196
|
+
const hooks1: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
197
|
+
const hooks2: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
198
|
+
|
|
199
|
+
setCurrentHooks(hooks1)
|
|
200
|
+
onMount(() => undefined)
|
|
201
|
+
setCurrentHooks(hooks2)
|
|
202
|
+
onMount(() => undefined)
|
|
203
|
+
onMount(() => undefined)
|
|
204
|
+
|
|
205
|
+
expect(hooks1.mount).toHaveLength(1)
|
|
206
|
+
expect(hooks2.mount).toHaveLength(2)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { mapArray } from "../map-array"
|
|
2
|
+
|
|
3
|
+
describe("mapArray", () => {
|
|
4
|
+
describe("basic mapping", () => {
|
|
5
|
+
test("maps all items on first call", () => {
|
|
6
|
+
const mapped = mapArray(
|
|
7
|
+
() => [1, 2, 3],
|
|
8
|
+
(item) => item,
|
|
9
|
+
(item) => item * 10,
|
|
10
|
+
)
|
|
11
|
+
expect(mapped()).toEqual([10, 20, 30])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("returns empty array for empty source", () => {
|
|
15
|
+
const mapped = mapArray(
|
|
16
|
+
() => [],
|
|
17
|
+
(item: number) => item,
|
|
18
|
+
(item) => item * 10,
|
|
19
|
+
)
|
|
20
|
+
expect(mapped()).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("maps single item", () => {
|
|
24
|
+
const mapped = mapArray(
|
|
25
|
+
() => [42],
|
|
26
|
+
(item) => item,
|
|
27
|
+
(item) => `value-${item}`,
|
|
28
|
+
)
|
|
29
|
+
expect(mapped()).toEqual(["value-42"])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("caching behavior", () => {
|
|
34
|
+
test("caches results — map function called once per key", () => {
|
|
35
|
+
let callCount = 0
|
|
36
|
+
const items = [1, 2, 3]
|
|
37
|
+
const mapped = mapArray(
|
|
38
|
+
() => items,
|
|
39
|
+
(item) => item,
|
|
40
|
+
(item) => {
|
|
41
|
+
callCount++
|
|
42
|
+
return item * 10
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
mapped()
|
|
47
|
+
expect(callCount).toBe(3)
|
|
48
|
+
|
|
49
|
+
// Second call — all cached
|
|
50
|
+
mapped()
|
|
51
|
+
expect(callCount).toBe(3)
|
|
52
|
+
|
|
53
|
+
// Third call — still cached
|
|
54
|
+
mapped()
|
|
55
|
+
expect(callCount).toBe(3)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("only maps new keys when items are added", () => {
|
|
59
|
+
let callCount = 0
|
|
60
|
+
let items = [1, 2, 3]
|
|
61
|
+
const mapped = mapArray(
|
|
62
|
+
() => items,
|
|
63
|
+
(item) => item,
|
|
64
|
+
(item) => {
|
|
65
|
+
callCount++
|
|
66
|
+
return item * 10
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
mapped()
|
|
71
|
+
expect(callCount).toBe(3)
|
|
72
|
+
|
|
73
|
+
items = [1, 2, 3, 4, 5]
|
|
74
|
+
mapped()
|
|
75
|
+
expect(callCount).toBe(5) // only 4 and 5 are new
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("does not re-map when items are removed", () => {
|
|
79
|
+
let callCount = 0
|
|
80
|
+
let items = [1, 2, 3, 4, 5]
|
|
81
|
+
const mapped = mapArray(
|
|
82
|
+
() => items,
|
|
83
|
+
(item) => item,
|
|
84
|
+
(item) => {
|
|
85
|
+
callCount++
|
|
86
|
+
return item * 10
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
mapped()
|
|
91
|
+
expect(callCount).toBe(5)
|
|
92
|
+
|
|
93
|
+
items = [1, 3, 5] // remove 2 and 4
|
|
94
|
+
const result = mapped()
|
|
95
|
+
expect(result).toEqual([10, 30, 50])
|
|
96
|
+
expect(callCount).toBe(5) // no new calls
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe("key eviction", () => {
|
|
101
|
+
test("evicted keys are re-mapped when they return", () => {
|
|
102
|
+
let callCount = 0
|
|
103
|
+
let items = [1, 2, 3]
|
|
104
|
+
const mapped = mapArray(
|
|
105
|
+
() => items,
|
|
106
|
+
(item) => item,
|
|
107
|
+
(item) => {
|
|
108
|
+
callCount++
|
|
109
|
+
return item * 10
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
mapped()
|
|
114
|
+
expect(callCount).toBe(3)
|
|
115
|
+
|
|
116
|
+
// Remove key 2
|
|
117
|
+
items = [1, 3]
|
|
118
|
+
mapped()
|
|
119
|
+
expect(callCount).toBe(3) // no new mapping
|
|
120
|
+
|
|
121
|
+
// Re-add key 2 — should re-map since it was evicted
|
|
122
|
+
items = [1, 2, 3]
|
|
123
|
+
mapped()
|
|
124
|
+
expect(callCount).toBe(4) // key 2 re-mapped
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("evicts all keys when source becomes empty", () => {
|
|
128
|
+
let callCount = 0
|
|
129
|
+
let items: number[] = [1, 2, 3]
|
|
130
|
+
const mapped = mapArray(
|
|
131
|
+
() => items,
|
|
132
|
+
(item) => item,
|
|
133
|
+
(item) => {
|
|
134
|
+
callCount++
|
|
135
|
+
return item * 10
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
mapped()
|
|
140
|
+
expect(callCount).toBe(3)
|
|
141
|
+
|
|
142
|
+
items = []
|
|
143
|
+
mapped()
|
|
144
|
+
expect(callCount).toBe(3)
|
|
145
|
+
|
|
146
|
+
// All keys were evicted — re-adding requires re-mapping
|
|
147
|
+
items = [1, 2, 3]
|
|
148
|
+
mapped()
|
|
149
|
+
expect(callCount).toBe(6)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe("reordering", () => {
|
|
154
|
+
test("reordered items use cached values (no re-mapping)", () => {
|
|
155
|
+
let callCount = 0
|
|
156
|
+
let items = [1, 2, 3]
|
|
157
|
+
const mapped = mapArray(
|
|
158
|
+
() => items,
|
|
159
|
+
(item) => item,
|
|
160
|
+
(item) => {
|
|
161
|
+
callCount++
|
|
162
|
+
return item * 10
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
mapped()
|
|
167
|
+
expect(callCount).toBe(3)
|
|
168
|
+
|
|
169
|
+
items = [3, 1, 2]
|
|
170
|
+
const result = mapped()
|
|
171
|
+
expect(result).toEqual([30, 10, 20])
|
|
172
|
+
expect(callCount).toBe(3) // no new calls
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("reverse order uses cached values", () => {
|
|
176
|
+
let callCount = 0
|
|
177
|
+
let items = [1, 2, 3, 4]
|
|
178
|
+
const mapped = mapArray(
|
|
179
|
+
() => items,
|
|
180
|
+
(item) => item,
|
|
181
|
+
(item) => {
|
|
182
|
+
callCount++
|
|
183
|
+
return `item-${item}`
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
mapped()
|
|
188
|
+
items = [4, 3, 2, 1]
|
|
189
|
+
const result = mapped()
|
|
190
|
+
expect(result).toEqual(["item-4", "item-3", "item-2", "item-1"])
|
|
191
|
+
expect(callCount).toBe(4) // initial 4 only
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("string keys", () => {
|
|
196
|
+
test("works with string keys from objects", () => {
|
|
197
|
+
interface User {
|
|
198
|
+
id: string
|
|
199
|
+
name: string
|
|
200
|
+
}
|
|
201
|
+
let callCount = 0
|
|
202
|
+
let users: User[] = [
|
|
203
|
+
{ id: "a", name: "Alice" },
|
|
204
|
+
{ id: "b", name: "Bob" },
|
|
205
|
+
]
|
|
206
|
+
const mapped = mapArray(
|
|
207
|
+
() => users,
|
|
208
|
+
(u) => u.id,
|
|
209
|
+
(u) => {
|
|
210
|
+
callCount++
|
|
211
|
+
return u.name.toUpperCase()
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
expect(mapped()).toEqual(["ALICE", "BOB"])
|
|
216
|
+
expect(callCount).toBe(2)
|
|
217
|
+
|
|
218
|
+
// Add new user
|
|
219
|
+
users = [
|
|
220
|
+
{ id: "a", name: "Alice" },
|
|
221
|
+
{ id: "b", name: "Bob" },
|
|
222
|
+
{ id: "c", name: "Charlie" },
|
|
223
|
+
]
|
|
224
|
+
expect(mapped()).toEqual(["ALICE", "BOB", "CHARLIE"])
|
|
225
|
+
expect(callCount).toBe(3)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe("mixed additions and removals", () => {
|
|
230
|
+
test("simultaneous add and remove", () => {
|
|
231
|
+
let callCount = 0
|
|
232
|
+
let items = [1, 2, 3]
|
|
233
|
+
const mapped = mapArray(
|
|
234
|
+
() => items,
|
|
235
|
+
(item) => item,
|
|
236
|
+
(item) => {
|
|
237
|
+
callCount++
|
|
238
|
+
return item * 10
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
mapped()
|
|
243
|
+
expect(callCount).toBe(3)
|
|
244
|
+
|
|
245
|
+
// Remove 2, add 4
|
|
246
|
+
items = [1, 3, 4]
|
|
247
|
+
const result = mapped()
|
|
248
|
+
expect(result).toEqual([10, 30, 40])
|
|
249
|
+
expect(callCount).toBe(4) // only key 4 is new
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("complete replacement of all items", () => {
|
|
253
|
+
let callCount = 0
|
|
254
|
+
let items = [1, 2, 3]
|
|
255
|
+
const mapped = mapArray(
|
|
256
|
+
() => items,
|
|
257
|
+
(item) => item,
|
|
258
|
+
(item) => {
|
|
259
|
+
callCount++
|
|
260
|
+
return item * 10
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
mapped()
|
|
265
|
+
expect(callCount).toBe(3)
|
|
266
|
+
|
|
267
|
+
items = [4, 5, 6]
|
|
268
|
+
const result = mapped()
|
|
269
|
+
expect(result).toEqual([40, 50, 60])
|
|
270
|
+
expect(callCount).toBe(6) // all new
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe("duplicate keys", () => {
|
|
275
|
+
test("duplicate keys in source share the same cached value", () => {
|
|
276
|
+
let callCount = 0
|
|
277
|
+
const mapped = mapArray(
|
|
278
|
+
() => [1, 1, 2],
|
|
279
|
+
(item) => item,
|
|
280
|
+
(item) => {
|
|
281
|
+
callCount++
|
|
282
|
+
return item * 10
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const result = mapped()
|
|
287
|
+
// Key 1 mapped once, key 2 mapped once
|
|
288
|
+
expect(callCount).toBe(2)
|
|
289
|
+
// Both occurrences of key 1 get the same cached value
|
|
290
|
+
expect(result).toEqual([10, 10, 20])
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe("map function receives correct item", () => {
|
|
295
|
+
test("map receives the item, not the key", () => {
|
|
296
|
+
const received: Array<{ id: number; val: string }> = []
|
|
297
|
+
const items = [
|
|
298
|
+
{ id: 1, val: "a" },
|
|
299
|
+
{ id: 2, val: "b" },
|
|
300
|
+
]
|
|
301
|
+
const mapped = mapArray(
|
|
302
|
+
() => items,
|
|
303
|
+
(item) => item.id,
|
|
304
|
+
(item) => {
|
|
305
|
+
received.push(item)
|
|
306
|
+
return item.val
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
mapped()
|
|
310
|
+
expect(received).toEqual(items)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { h } from "../h"
|
|
2
|
+
import { Portal, PortalSymbol } from "../portal"
|
|
3
|
+
import type { VNode } from "../types"
|
|
4
|
+
|
|
5
|
+
describe("Portal", () => {
|
|
6
|
+
test("returns VNode with PortalSymbol type", () => {
|
|
7
|
+
const fakeTarget = {} as Element
|
|
8
|
+
const node = Portal({ target: fakeTarget, children: h("div", null) })
|
|
9
|
+
expect(node.type).toBe(PortalSymbol)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test("VNode has null key", () => {
|
|
13
|
+
const node = Portal({ target: {} as Element, children: "content" })
|
|
14
|
+
expect(node.key).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("VNode has empty children array", () => {
|
|
18
|
+
const node = Portal({ target: {} as Element, children: "content" })
|
|
19
|
+
expect(node.children).toEqual([])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test("props contain target and children", () => {
|
|
23
|
+
const fakeTarget = {} as Element
|
|
24
|
+
const child = h("span", null, "content")
|
|
25
|
+
const node = Portal({ target: fakeTarget, children: child })
|
|
26
|
+
const props = node.props as unknown as { target: Element; children: VNode }
|
|
27
|
+
expect(props.target).toBe(fakeTarget)
|
|
28
|
+
expect(props.children).toBe(child)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("PortalSymbol is a unique symbol", () => {
|
|
32
|
+
expect(typeof PortalSymbol).toBe("symbol")
|
|
33
|
+
expect(PortalSymbol.toString()).toContain("pyreon.Portal")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("string children are stored in props", () => {
|
|
37
|
+
const node = Portal({ target: {} as Element, children: "text content" })
|
|
38
|
+
const props = node.props as unknown as { children: string }
|
|
39
|
+
expect(props.children).toBe("text content")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("multiple VNode children via fragment", () => {
|
|
43
|
+
const children = h("div", null, h("span", null, "a"), h("span", null, "b"))
|
|
44
|
+
const node = Portal({ target: {} as Element, children })
|
|
45
|
+
const props = node.props as unknown as { children: VNode }
|
|
46
|
+
expect((props.children as VNode).type).toBe("div")
|
|
47
|
+
})
|
|
48
|
+
})
|