@aiquants/daily-report 0.20.0 → 0.21.0

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.
Files changed (116) hide show
  1. package/README.md +229 -8
  2. package/dist/client.d.mts +201 -739
  3. package/dist/client.d.ts +201 -739
  4. package/dist/client.js +5 -6
  5. package/dist/client.mjs +5 -6
  6. package/dist/index.d.mts +5 -197
  7. package/dist/index.d.ts +5 -197
  8. package/dist/index.js +1 -2
  9. package/dist/index.mjs +1 -2
  10. package/dist/logger-D3krZrNK.d.mts +0 -14
  11. package/dist/logger-D3krZrNK.d.ts +0 -14
  12. package/dist/server.d.mts +114 -744
  13. package/dist/server.d.ts +114 -744
  14. package/dist/server.js +1 -2
  15. package/dist/server.mjs +1 -2
  16. package/dist/{sse-schema-ChSFm-4q.d.mts → sse-schema-DCxpdETM.d.mts} +1106 -1090
  17. package/dist/{sse-schema-BdUGThiz.d.ts → sse-schema-DoQw8_om.d.ts} +1106 -1090
  18. package/dist/types-CqgYQgbH.d.mts +0 -34
  19. package/dist/types-CqgYQgbH.d.ts +0 -34
  20. package/package.json +9 -10
  21. package/dist/client.js.map +0 -1
  22. package/dist/client.mjs.map +0 -1
  23. package/dist/index.js.map +0 -1
  24. package/dist/index.mjs.map +0 -1
  25. package/dist/server.js.map +0 -1
  26. package/dist/server.mjs.map +0 -1
  27. package/src/client/components/business-day-thumb-overlay.tsx +0 -19
  28. package/src/client/components/daily-report-attachment-indicator.spec.tsx +0 -51
  29. package/src/client/components/daily-report-attachment-indicator.tsx +0 -45
  30. package/src/client/components/daily-report-attachment-list.spec.tsx +0 -94
  31. package/src/client/components/daily-report-attachment-list.tsx +0 -112
  32. package/src/client/components/daily-report-comment-item.tsx +0 -89
  33. package/src/client/components/daily-report-comment-section.tsx +0 -166
  34. package/src/client/components/daily-report-detail-list.tsx +0 -842
  35. package/src/client/components/daily-report-edit-form.tsx +0 -87
  36. package/src/client/components/daily-report-ids-stream-status.spec.tsx +0 -93
  37. package/src/client/components/daily-report-ids-stream-status.tsx +0 -67
  38. package/src/client/components/daily-report-list.tsx +0 -1232
  39. package/src/client/components/daily-report-page.spec.tsx +0 -119
  40. package/src/client/components/daily-report-page.tsx +0 -140
  41. package/src/client/components/daily-report-resize-panels.spec.tsx +0 -307
  42. package/src/client/components/daily-report-resolved-content.tsx +0 -207
  43. package/src/client/components/daily-report-scroll-restoration.spec.tsx +0 -422
  44. package/src/client/components/report-views.spec.tsx +0 -375
  45. package/src/client/components/unread-indicator.tsx +0 -13
  46. package/src/client/config-context.tsx +0 -184
  47. package/src/client/contexts/daily-report-action-context.spec.tsx +0 -433
  48. package/src/client/contexts/daily-report-action-context.tsx +0 -1147
  49. package/src/client/contexts/daily-report-error-context.spec.tsx +0 -66
  50. package/src/client/contexts/daily-report-error-context.tsx +0 -98
  51. package/src/client/hooks/use-animated-number.spec.ts +0 -103
  52. package/src/client/hooks/use-animated-number.ts +0 -69
  53. package/src/client/hooks/use-daily-report-comments.ts +0 -73
  54. package/src/client/hooks/use-daily-report-sse-connection.ts +0 -86
  55. package/src/client/hooks/use-daily-report.spec.ts +0 -546
  56. package/src/client/hooks/use-daily-report.ts +0 -549
  57. package/src/client/hooks/use-dynamic-viewport-height.ts +0 -127
  58. package/src/client/hooks/use-responsive-layout.spec.ts +0 -74
  59. package/src/client/hooks/use-responsive-layout.ts +0 -63
  60. package/src/client/route-helpers.spec.ts +0 -100
  61. package/src/client/route-helpers.ts +0 -61
  62. package/src/client/streaming/daily-report-ids-stream-client.spec.ts +0 -910
  63. package/src/client/streaming/daily-report-ids-stream-client.ts +0 -730
  64. package/src/client/streaming/daily-report-ids-stream-session.spec.ts +0 -401
  65. package/src/client/streaming/daily-report-ids-stream-session.ts +0 -319
  66. package/src/client/ui/button.tsx +0 -42
  67. package/src/client/ui/cn.ts +0 -14
  68. package/src/client/ui/input.tsx +0 -21
  69. package/src/client/ui/label.tsx +0 -16
  70. package/src/client/ui/switch.tsx +0 -19
  71. package/src/client/ui/tabs.tsx +0 -40
  72. package/src/client/ui/textarea.tsx +0 -19
  73. package/src/client/utils/constants.spec.ts +0 -96
  74. package/src/client/utils/constants.ts +0 -84
  75. package/src/client.ts +0 -28
  76. package/src/index.ts +0 -11
  77. package/src/lucide-icons.d.ts +0 -6
  78. package/src/server/authz.spec.ts +0 -57
  79. package/src/server/authz.ts +0 -99
  80. package/src/server/cache.spec.ts +0 -556
  81. package/src/server/cache.ts +0 -303
  82. package/src/server/etag.ts +0 -18
  83. package/src/server/external-source.ts +0 -60
  84. package/src/server/handlers.action.spec.ts +0 -44
  85. package/src/server/handlers.attachment.spec.ts +0 -470
  86. package/src/server/handlers.ids-stream.spec.ts +0 -618
  87. package/src/server/handlers.ts +0 -1638
  88. package/src/server/handlers.unauthorized-cookie.spec.ts +0 -187
  89. package/src/server/handlers.visibility.spec.ts +0 -774
  90. package/src/server/ports.ts +0 -222
  91. package/src/server/response.ts +0 -37
  92. package/src/server/schema.spec.ts +0 -30
  93. package/src/server/schema.ts +0 -344
  94. package/src/server/service.spec.ts +0 -1130
  95. package/src/server/service.ts +0 -2045
  96. package/src/server/sse-reader.spec.ts +0 -302
  97. package/src/server/sse-reader.ts +0 -267
  98. package/src/server/test-helpers/handlers-config.ts +0 -321
  99. package/src/server/transaction-retry.spec.ts +0 -63
  100. package/src/server/visibility.spec.ts +0 -403
  101. package/src/server/visibility.ts +0 -373
  102. package/src/server.spec.ts +0 -160
  103. package/src/server.ts +0 -131
  104. package/src/shared/business-date.spec.ts +0 -82
  105. package/src/shared/business-date.ts +0 -104
  106. package/src/shared/comment-adapter.spec.ts +0 -38
  107. package/src/shared/comment-adapter.ts +0 -80
  108. package/src/shared/ids-stream.spec.ts +0 -154
  109. package/src/shared/ids-stream.ts +0 -279
  110. package/src/shared/logger.ts +0 -47
  111. package/src/shared/sse-schema.spec.ts +0 -50
  112. package/src/shared/sse-schema.ts +0 -198
  113. package/src/shared/text-utils.spec.ts +0 -42
  114. package/src/shared/text-utils.ts +0 -65
  115. package/src/shared/types.ts +0 -109
  116. package/src/styles/standalone.entry.css +0 -51
@@ -1,401 +0,0 @@
1
- /**
2
- * Lifecycle tests for the module-resident ids stream session.
3
- * モジュール常駐 ids ストリームセッションのライフサイクルテスト。
4
- *
5
- * ここで固定する不変条件のうち、特に重要なもの:
6
- *
7
- * - **userKey (難読化ユーザー ID) が変わったらセッションを作り直す** — 別ユーザーの
8
- * 受信済みデータを次のユーザーへ見せてはならない。
9
- * - **failed セッションへの再訪 (ensure) は自動 retry する** — 再訪はユーザーの再試行意思。
10
- * - **完走から一定時間を過ぎた再訪はバックグラウンド再検証する** (SWR)。
11
- * - **フックはセッション生成前でも安全に IDLE を返し、生成後は再購読なしで追従する。**
12
- *
13
- * モジュールスコープ状態を分離するため、テストごとに `vi.resetModules()` +
14
- * 動的 import でモジュールを作り直す (use-daily-report.spec.ts と同じ流儀)。
15
- */
16
- import { act, renderHook } from "@testing-library/react"
17
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
18
- import type { DailyReportIdsStreamState } from "./daily-report-ids-stream-client"
19
-
20
- /** FakeClient の呼び出し記録と状態操作面。 */
21
- type FakeClientHandle = {
22
- startCalls: number
23
- retryCalls: number
24
- refreshCalls: Array<{ forceRefresh?: boolean }>
25
- disposeCalls: number
26
- upserts: Array<{ reportHubId: number; businessDate: string | null }>
27
- removals: number[]
28
- setState: (partial: Partial<DailyReportIdsStreamState>) => void
29
- options: { apiBasePath?: string }
30
- }
31
-
32
- /** 生成された FakeClient のハンドル一覧 (生成順)。 */
33
- const createdClients: FakeClientHandle[] = []
34
-
35
- /** ユーザー切替時の詳細キャッシュ破棄の呼び出し記録。 */
36
- const clearCacheMock = vi.fn()
37
-
38
- vi.mock("../hooks/use-daily-report", () => ({
39
- clearDailyReportCache: () => clearCacheMock(),
40
- }))
41
-
42
- vi.mock("./daily-report-ids-stream-client", () => {
43
- class FakeClient {
44
- private listeners = new Set<() => void>()
45
- private state: DailyReportIdsStreamState = {
46
- items: [],
47
- loadedCount: 0,
48
- totalCount: null,
49
- phase: "idle",
50
- isRevalidating: false,
51
- hasCompletedOnce: false,
52
- completedAt: null,
53
- error: null,
54
- }
55
- readonly handle: FakeClientHandle
56
-
57
- constructor(options: { apiBasePath?: string } = {}) {
58
- this.handle = {
59
- startCalls: 0,
60
- retryCalls: 0,
61
- refreshCalls: [],
62
- disposeCalls: 0,
63
- upserts: [],
64
- removals: [],
65
- options,
66
- setState: (partial) => {
67
- this.state = { ...this.state, ...partial }
68
- for (const listener of this.listeners) listener()
69
- },
70
- }
71
- createdClients.push(this.handle)
72
- }
73
-
74
- getState() {
75
- return this.state
76
- }
77
-
78
- subscribe(listener: () => void) {
79
- this.listeners.add(listener)
80
- return () => this.listeners.delete(listener)
81
- }
82
-
83
- start() {
84
- this.handle.startCalls += 1
85
- }
86
-
87
- retry() {
88
- this.handle.retryCalls += 1
89
- }
90
-
91
- refresh(options: { forceRefresh?: boolean } = {}) {
92
- this.handle.refreshCalls.push(options)
93
- }
94
-
95
- dispose() {
96
- this.handle.disposeCalls += 1
97
- }
98
-
99
- applyExternalUpsert(item: { reportHubId: number; businessDate: string | null }) {
100
- this.handle.upserts.push(item)
101
- }
102
-
103
- applyExternalRemoval(reportHubId: number) {
104
- this.handle.removals.push(reportHubId)
105
- }
106
- }
107
- return { DailyReportIdsStreamClient: FakeClient }
108
- })
109
-
110
- /** テスト対象モジュールを新品で読み込む。 */
111
- const importSession = async () => await import("./daily-report-ids-stream-session")
112
-
113
- beforeEach(() => {
114
- vi.resetModules()
115
- createdClients.length = 0
116
- clearCacheMock.mockClear()
117
- vi.useFakeTimers()
118
- })
119
-
120
- afterEach(() => {
121
- vi.useRealTimers()
122
- })
123
-
124
- describe("ensureDailyReportIdsStreamSession", () => {
125
- it("初回 ensure でクライアントを生成して start する", async () => {
126
- const session = await importSession()
127
- session.ensureDailyReportIdsStreamSession({ apiBasePath: "/daily_report/api", userKey: "userA" })
128
- expect(createdClients).toHaveLength(1)
129
- expect(createdClients[0].startCalls).toBe(1)
130
- expect(createdClients[0].options.apiBasePath).toBe("/daily_report/api")
131
- session.resetDailyReportIdsStreamSessionForTesting()
132
- })
133
-
134
- it("同一 userKey の再 ensure はセッションを再利用する", async () => {
135
- const session = await importSession()
136
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
137
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
138
- expect(createdClients).toHaveLength(1)
139
- expect(createdClients[0].disposeCalls).toBe(0)
140
- session.resetDailyReportIdsStreamSessionForTesting()
141
- })
142
-
143
- it("userKey が変わったら旧セッションと詳細キャッシュを破棄して作り直す", async () => {
144
- const session = await importSession()
145
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
146
- session.ensureDailyReportIdsStreamSession({ userKey: "userB" })
147
- expect(createdClients).toHaveLength(2)
148
- expect(createdClients[0].disposeCalls).toBe(1)
149
- expect(createdClients[1].startCalls).toBe(1)
150
- // 旧ユーザーが閲覧した詳細 (下書き含む) を新ユーザーへ供給しない
151
- expect(clearCacheMock).toHaveBeenCalledTimes(1)
152
- session.resetDailyReportIdsStreamSessionForTesting()
153
- })
154
-
155
- it("apiBasePath が変わったら作り直す (同一ユーザーなので詳細キャッシュは保持)", async () => {
156
- const session = await importSession()
157
- session.ensureDailyReportIdsStreamSession({ apiBasePath: "/a", userKey: "userA" })
158
- session.ensureDailyReportIdsStreamSession({ apiBasePath: "/b", userKey: "userA" })
159
- expect(createdClients).toHaveLength(2)
160
- expect(createdClients[0].disposeCalls).toBe(1)
161
- expect(clearCacheMock).not.toHaveBeenCalled()
162
- session.resetDailyReportIdsStreamSessionForTesting()
163
- })
164
-
165
- it("failed セッションへの ensure は retry する", async () => {
166
- const session = await importSession()
167
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
168
- createdClients[0].setState({ phase: "failed" })
169
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
170
- expect(createdClients[0].retryCalls).toBe(1)
171
- expect(createdClients[0].refreshCalls).toHaveLength(0)
172
- session.resetDailyReportIdsStreamSessionForTesting()
173
- })
174
-
175
- it("完走直後の ensure は再検証しないが、鮮度窓を過ぎた ensure は refresh する", async () => {
176
- const session = await importSession()
177
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
178
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
179
-
180
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
181
- expect(createdClients[0].refreshCalls).toHaveLength(0)
182
-
183
- // 鮮度窓 (60 秒) を越えて再訪
184
- vi.advanceTimersByTime(61_000)
185
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
186
- expect(createdClients[0].refreshCalls).toHaveLength(1)
187
- session.resetDailyReportIdsStreamSessionForTesting()
188
- })
189
-
190
- it("再検証の諦め (completedAt 据え置き) 後の ensure は再び refresh する — 古いデータを新鮮扱いしない", async () => {
191
- const session = await importSession()
192
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
193
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
194
-
195
- vi.advanceTimersByTime(61_000)
196
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
197
- expect(createdClients[0].refreshCalls).toHaveLength(1)
198
-
199
- // 再検証の力尽き publish: phase は complete のまま、completedAt は据え置き
200
- createdClients[0].setState({ isRevalidating: false, error: new Error("gave up") })
201
-
202
- // 次の ensure は据え置かれた completedAt を根拠に再検証を再度走らせる
203
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
204
- expect(createdClients[0].refreshCalls).toHaveLength(2)
205
- session.resetDailyReportIdsStreamSessionForTesting()
206
- })
207
-
208
- it("undefined と既定文字列の apiBasePath は同一視され、セッションを破棄しない", async () => {
209
- const session = await importSession()
210
- // clientLoader 相当 (apiBasePath 未指定 = undefined)
211
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
212
- // ページ effect 相当 (config の既定文字列)
213
- session.ensureDailyReportIdsStreamSession({ apiBasePath: "/daily_report/api", userKey: "userA" })
214
- expect(createdClients).toHaveLength(1)
215
- expect(createdClients[0].disposeCalls).toBe(0)
216
- session.resetDailyReportIdsStreamSessionForTesting()
217
- })
218
- })
219
-
220
- describe("ensure / isRevalidating ガード", () => {
221
- it("走行中の再検証 (isRevalidating) がある間の ensure は refresh を重ねない", async () => {
222
- const session = await importSession()
223
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
224
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
225
- vi.advanceTimersByTime(61_000)
226
-
227
- // 1 回目の ensure が再検証を起動する想定 (FakeClient は refresh を記録するだけ)
228
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
229
- expect(createdClients[0].refreshCalls).toHaveLength(1)
230
-
231
- // 実クライアントは再検証中 isRevalidating を立てる。以後の ensure は重ねない
232
- createdClients[0].setState({ isRevalidating: true })
233
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
234
- session.primeDailyReportIdsStreamSession()
235
- expect(createdClients[0].refreshCalls).toHaveLength(1)
236
- session.resetDailyReportIdsStreamSessionForTesting()
237
- })
238
- })
239
-
240
- describe("bootstrapDailyReportIdsStreamSession", () => {
241
- it("セッションが無ければローダー時点で生成して start する (コールドロードの最重要最適化)", async () => {
242
- const session = await importSession()
243
- session.bootstrapDailyReportIdsStreamSession({ apiBasePath: "/daily_report/api", userKey: "userA" })
244
- expect(createdClients).toHaveLength(1)
245
- expect(createdClients[0].startCalls).toBe(1)
246
- session.resetDailyReportIdsStreamSessionForTesting()
247
- })
248
-
249
- it("userKey 不一致なら描画前に破棄 (詳細キャッシュ含む) し、新ユーザーで即時再生成する", async () => {
250
- const session = await importSession()
251
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
252
- session.bootstrapDailyReportIdsStreamSession({ userKey: "userB" })
253
- expect(createdClients[0].disposeCalls).toBe(1)
254
- expect(createdClients).toHaveLength(2)
255
- expect(createdClients[1].startCalls).toBe(1)
256
- expect(clearCacheMock).toHaveBeenCalledTimes(1)
257
- session.resetDailyReportIdsStreamSessionForTesting()
258
- })
259
-
260
- it("userKey 一致なら既存セッションを保ち鮮度確認を行う (failed は retry)。パスは照合しない", async () => {
261
- const session = await importSession()
262
- session.ensureDailyReportIdsStreamSession({ apiBasePath: "/custom/api", userKey: "userA" })
263
- createdClients[0].setState({ phase: "failed" })
264
- // factory 未指定 (undefined → 既定値) でもセッションは破棄されない —
265
- // 照合するとナビゲーションごとのスラッシングが再発する
266
- session.bootstrapDailyReportIdsStreamSession({ userKey: "userA" })
267
- expect(createdClients).toHaveLength(1)
268
- expect(createdClients[0].disposeCalls).toBe(0)
269
- expect(createdClients[0].retryCalls).toBe(1)
270
- session.resetDailyReportIdsStreamSessionForTesting()
271
- })
272
- })
273
-
274
- describe("外部ミューテーション中継", () => {
275
- it("upsert / removal はセッションのクライアントへ委譲され、セッション無しでは何もしない", async () => {
276
- const session = await importSession()
277
- // セッション無し: 例外なく無視される
278
- session.upsertDailyReportIdsStreamItem({ reportHubId: 1, businessDate: "2026-08-01" })
279
- session.removeDailyReportIdsStreamItem(1)
280
- expect(createdClients).toHaveLength(0)
281
-
282
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
283
- session.upsertDailyReportIdsStreamItem({ reportHubId: 2, businessDate: "2026-08-02" })
284
- session.removeDailyReportIdsStreamItem(3)
285
- expect(createdClients[0].upserts).toEqual([{ reportHubId: 2, businessDate: "2026-08-02" }])
286
- expect(createdClients[0].removals).toEqual([3])
287
- session.resetDailyReportIdsStreamSessionForTesting()
288
- })
289
- })
290
-
291
- describe("primeDailyReportIdsStreamSession", () => {
292
- it("セッションが無ければ何もしない", async () => {
293
- const session = await importSession()
294
- session.primeDailyReportIdsStreamSession()
295
- expect(createdClients).toHaveLength(0)
296
- })
297
-
298
- it("既存セッションがあれば userKey 未確定でも鮮度確認を先行できる", async () => {
299
- const session = await importSession()
300
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
301
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
302
- vi.advanceTimersByTime(61_000)
303
- session.primeDailyReportIdsStreamSession()
304
- expect(createdClients[0].refreshCalls).toHaveLength(1)
305
- // prime はセッションを作り直さない
306
- expect(createdClients).toHaveLength(1)
307
- session.resetDailyReportIdsStreamSessionForTesting()
308
- })
309
- })
310
-
311
- describe("useDailyReportIdsStream", () => {
312
- it("セッション生成前は IDLE、生成後は再購読なしで追従する", async () => {
313
- const session = await importSession()
314
- const { result } = renderHook(() => session.useDailyReportIdsStream())
315
- expect(result.current.phase).toBe("idle")
316
- expect(result.current.items).toEqual([])
317
-
318
- act(() => {
319
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
320
- })
321
- act(() => {
322
- createdClients[0].setState({
323
- phase: "streaming",
324
- loadedCount: 2,
325
- items: [
326
- { reportHubId: 2, businessDate: "2026-08-01" },
327
- { reportHubId: 1, businessDate: "2026-08-01" },
328
- ],
329
- })
330
- })
331
- expect(result.current.phase).toBe("streaming")
332
- expect(result.current.loadedCount).toBe(2)
333
- session.resetDailyReportIdsStreamSessionForTesting()
334
- })
335
-
336
- it("retry / refresh のエントリポイントはクライアントへ委譲する", async () => {
337
- const session = await importSession()
338
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
339
- session.retryDailyReportIdsStream()
340
- session.refreshDailyReportIdsStream({ forceRefresh: true })
341
- expect(createdClients[0].retryCalls).toBe(1)
342
- expect(createdClients[0].refreshCalls).toEqual([{ forceRefresh: true }])
343
- session.resetDailyReportIdsStreamSessionForTesting()
344
- })
345
-
346
- it("resetDailyReportIdsStreamSessionForTesting はセッションを破棄して IDLE へ戻す", async () => {
347
- const session = await importSession()
348
- const { result } = renderHook(() => session.useDailyReportIdsStream())
349
- act(() => {
350
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
351
- createdClients[0].setState({ phase: "streaming", loadedCount: 1 })
352
- })
353
- expect(result.current.phase).toBe("streaming")
354
- act(() => {
355
- session.resetDailyReportIdsStreamSessionForTesting()
356
- })
357
- expect(createdClients[0].disposeCalls).toBe(1)
358
- expect(result.current.phase).toBe("idle")
359
- })
360
- })
361
-
362
- describe("時間駆動の再検証 (再訪以外の新規走査起動経路)", () => {
363
- // ❗ これが無いと新規走査を起動できるのは「再訪 (ナビゲーション)」だけになる。
364
- // REVALIDATE_AFTER_MS はタイマーではなく再訪時の閾値なので、タブを開いたまま
365
- // 放置する使い方では走査が二度と走らず、走査時点の不整合が残り続ける
366
- it("可視化復帰 (visibilitychange) は鮮度切れなら再検証する", async () => {
367
- const session = await importSession()
368
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
369
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
370
-
371
- // 鮮度窓の内側では起動しない (無用な全量走査を増やさない)
372
- document.dispatchEvent(new Event("visibilitychange"))
373
- expect(createdClients[0].refreshCalls).toHaveLength(0)
374
-
375
- vi.advanceTimersByTime(61_000)
376
- document.dispatchEvent(new Event("visibilitychange"))
377
- expect(createdClients[0].refreshCalls).toHaveLength(1)
378
- session.resetDailyReportIdsStreamSessionForTesting()
379
- })
380
-
381
- it("フォーカス取得 (focus) も同じ経路で再検証する", async () => {
382
- const session = await importSession()
383
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
384
- createdClients[0].setState({ phase: "complete", hasCompletedOnce: true, completedAt: Date.now() })
385
-
386
- vi.advanceTimersByTime(61_000)
387
- window.dispatchEvent(new Event("focus"))
388
- expect(createdClients[0].refreshCalls).toHaveLength(1)
389
- session.resetDailyReportIdsStreamSessionForTesting()
390
- })
391
-
392
- it("セッション破棄後に発火しても落ちない (リスナーはモジュール寿命で残る)", async () => {
393
- const session = await importSession()
394
- session.ensureDailyReportIdsStreamSession({ userKey: "userA" })
395
- session.resetDailyReportIdsStreamSessionForTesting()
396
- expect(() => {
397
- document.dispatchEvent(new Event("visibilitychange"))
398
- window.dispatchEvent(new Event("focus"))
399
- }).not.toThrow()
400
- })
401
- })