-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResourceDemo.test.ts
More file actions
259 lines (235 loc) · 10.2 KB
/
Copy pathResourceDemo.test.ts
File metadata and controls
259 lines (235 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import { describe, it, expect } from "vitest"
import { testScreen } from "@intent-framework/testing"
import { createScreenRuntime, inspectScreen } from "@intent-framework/core"
import { ResourceDemo, teamLoadCount, cachedTeamLoadCount, dedupeReportLoadCount, keyedTeamLoadCount, timeEvictTeamLoadCount } from "./ResourceDemo.js"
const testServices = {
route: { name: "demo", path: "/:id", params: { id: "team_1" } },
navigate: () => {},
} as const
describe("ResourceDemo", () => {
it("autoLoad resource reaches ready", async () => {
await testScreen(ResourceDemo, async app => {
expect(app.resource("team").status()).toBe("ready")
}, { services: testServices as any })
})
it("autoLoad: false resource starts idle", async () => {
await testScreen(ResourceDemo, async app => {
expect(app.resource("auditLog").status()).toBe("idle")
}, { services: testServices as any })
})
it("manual load transitions resource to ready", async () => {
await testScreen(ResourceDemo, async app => {
expect(app.resource("auditLog").status()).toBe("idle")
await app.resource("auditLog").load()
expect(app.resource("auditLog").status()).toBe("ready")
}, { services: testServices as any })
})
it("reload increments load count", async () => {
await testScreen(ResourceDemo, async app => {
const before = teamLoadCount
await app.resource("team").reload()
expect(teamLoadCount).toBe(before + 1)
}, { services: testServices as any })
})
it("invalidate marks resource stale without reloading", async () => {
await testScreen(ResourceDemo, async app => {
const team = app.resource("team")
expect(team.stale()).toBe(false)
const before = teamLoadCount
team.invalidate()
expect(team.stale()).toBe(true)
expect(teamLoadCount).toBe(before)
}, { services: testServices as any })
})
it("reload clears stale", async () => {
await testScreen(ResourceDemo, async app => {
const team = app.resource("team")
team.invalidate()
expect(team.stale()).toBe(true)
await team.reload()
expect(team.stale()).toBe(false)
}, { services: testServices as any })
})
it("action .invalidates() marks resource stale after success", async () => {
await testScreen(ResourceDemo, async app => {
const team = app.resource("team")
expect(team.stale()).toBe(false)
await app.act("Save team").run()
expect(team.stale()).toBe(true)
}, { services: testServices as any })
})
it("failed loader produces failed status and error", async () => {
await testScreen(ResourceDemo, async app => {
expect(app.resource("unstableReport").status()).toBe("failed")
}, { services: testServices as any })
})
it("failed action does not invalidate resources", async () => {
await testScreen(ResourceDemo, async app => {
const team = app.resource("team")
expect(team.stale()).toBe(false)
await app.act("Broken save").run()
expect(team.stale()).toBe(false)
}, { services: testServices as any })
})
it("cache.staleTime marks resource stale after timeout", async () => {
await testScreen(ResourceDemo, async app => {
const cached = app.resource("cachedTeam")
expect(cached.status()).toBe("ready")
expect(cached.stale()).toBe(false)
await new Promise(r => setTimeout(r, 100))
expect(cached.stale()).toBe(true)
expect(cached.status()).toBe("ready")
}, { services: testServices as any })
})
it("reload resets staleTime timer and clears stale", async () => {
await testScreen(ResourceDemo, async app => {
const cached = app.resource("cachedTeam")
await new Promise(r => setTimeout(r, 60))
expect(cached.stale()).toBe(true)
const before = cachedTeamLoadCount
await cached.reload()
expect(cachedTeamLoadCount).toBe(before + 1)
expect(cached.stale()).toBe(false)
}, { services: testServices as any })
})
it("cache.deduplicate shares in-flight promise", async () => {
await testScreen(ResourceDemo, async app => {
const report = app.resource("dedupeReport")
expect(report.status()).toBe("idle")
const before = dedupeReportLoadCount
await Promise.all([report.load(), report.load()])
expect(dedupeReportLoadCount).toBe(before + 1)
expect(report.status()).toBe("ready")
}, { services: testServices as any })
})
it("cache.key derives resource value from route context", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const keyed = runtime.resources.find(r => r.name === "keyedTeam")!
expect(keyed.status).toBe("ready")
expect((keyed.value as any)?.id).toBe("team_1")
expect((keyed.value as any)?.name).toBe("Team-team_1")
runtime.dispose()
})
it("cache.key: different keys are independent", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const keyed = runtime.resources.find(r => r.name === "keyedTeam")!
const before = keyedTeamLoadCount
// Load with a different key
await keyed.load({ route: { name: "demo", path: "/:id", params: { id: "team_b" } } })
expect(keyed.status).toBe("ready")
expect((keyed.value as any)?.id).toBe("team_b")
expect((keyed.value as any)?.name).toBe("Team-team_b")
expect(keyedTeamLoadCount).toBe(before + 1)
runtime.dispose()
})
it("cache.key: switching active key updates visible value/status", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const keyed = runtime.resources.find(r => r.name === "keyedTeam")!
// Initially keyed by route param "team_1"
expect((keyed.value as any)?.id).toBe("team_1")
// Switch to team_b
await keyed.load({ route: { name: "demo", path: "/:id", params: { id: "team_b" } } })
expect(keyed.status).toBe("ready")
expect((keyed.value as any)?.id).toBe("team_b")
// Switch back to team_1
await keyed.load({ route: { name: "demo", path: "/:id", params: { id: "team_1" } } })
expect(keyed.status).toBe("ready")
expect((keyed.value as any)?.id).toBe("team_1")
runtime.dispose()
})
// === cacheTime tests ===
it("invalidate keeps stale value before cacheTime expires", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
expect(evict.status).toBe("ready")
expect(evict.stale.current).toBe(false)
expect(evict.value).toBeDefined()
const val = evict.value
evict.invalidate()
expect(evict.stale.current).toBe(true)
expect(evict.value).toBe(val)
expect(evict.status).toBe("ready")
runtime.dispose()
})
it("cacheTime expiry resets active entry to idle", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
evict.invalidate()
expect(evict.status).toBe("ready")
expect(evict.stale.current).toBe(true)
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
expect(evict.stale.current).toBe(false)
expect(evict.value).toBeUndefined()
runtime.dispose()
})
it("cacheTime works per key for keyed resource", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "keyedTimeEvict")!
await evict.load({ route: { name: "demo", path: "/:id", params: { id: "team_b" } } })
expect(evict.status).toBe("ready")
evict.invalidate()
expect(evict.stale.current).toBe(true)
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
expect(evict.stale.current).toBe(false)
runtime.dispose()
})
it("no-arg reload after cacheTime eviction reuses last context", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const evict = runtime.resources.find(r => r.name === "timeEvictTeam")!
const before = timeEvictTeamLoadCount
evict.invalidate()
await new Promise(r => setTimeout(r, 1100))
expect(evict.status).toBe("idle")
await evict.reload()
expect(timeEvictTeamLoadCount).toBe(before + 1)
expect(evict.status).toBe("ready")
runtime.dispose()
})
it("cache.key: no-arg reload reuses last active key", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const keyed = runtime.resources.find(r => r.name === "keyedTeam")!
// Load team_b — changes active key
await keyed.load({ route: { name: "demo", path: "/:id", params: { id: "team_b" } } })
const afterFirst = keyedTeamLoadCount
// no-arg reload uses lastContext → reloads team_b
await keyed.reload()
expect(keyedTeamLoadCount).toBe(afterFirst + 1)
expect((keyed.value as any)?.id).toBe("team_b")
runtime.dispose()
})
it("inspectScreen reports resources with status/stale/error", async () => {
const runtime = createScreenRuntime(ResourceDemo, { services: testServices as any })
await runtime.start()
const graph = runtime.graph
const resources = graph.resources
expect(resources).toHaveLength(8)
const teamRes = resources.find(r => r.name === "team")!
expect(teamRes.status).toBe("ready")
expect(teamRes.hasValue).toBe(true)
expect(teamRes.stale).toBe(false)
const auditRes = resources.find(r => r.name === "auditLog")!
expect(auditRes.status).toBe("idle")
expect(auditRes.hasValue).toBe(false)
expect(auditRes.stale).toBe(false)
const failedRes = resources.find(r => r.name === "unstableReport")!
expect(failedRes.status).toBe("failed")
expect(failedRes.hasValue).toBe(false)
expect(failedRes.stale).toBe(false)
expect(failedRes.error).toBe("Report generation failed")
runtime.dispose()
})
it("no unrelated graph diagnostics are introduced", () => {
const graph = inspectScreen(ResourceDemo)
expect(graph.diagnostics).toHaveLength(0)
})
})