@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,119 +0,0 @@
1
- /**
2
- * Phase-gating tests for the stream-driven daily report page.
3
- * ストリーム駆動日報ページの局面ゲーティングテスト。
4
- *
5
- * ここで固定する不変条件のうち、特に重要なもの:
6
- *
7
- * - **最初のチャンク到着 (items > 0) で即座に本体を描画する** — 全件解決を待つ
8
- * ゲートが存在しないことがストリーミング化の成果そのもの。
9
- * - **failed でも受信済みアイテムがあれば本体を描画し続ける** (データを捨てない)。
10
- * - **マウント時にセッションを ensure する** (clientLoader を経ないマウントの保険)。
11
- * - **auth-required はフルリロードへ倒す** (サーバー loader のリダイレクトに委譲)。
12
- */
13
- import { fireEvent, render, screen } from "@testing-library/react"
14
- import { describe, expect, it, vi } from "vitest"
15
- import type { DailyReportItem } from "../../shared/types"
16
- import type { DailyReportIdsStreamState } from "../streaming/daily-report-ids-stream-client"
17
- import { DailyReportPage } from "./daily-report-page"
18
-
19
- /** テストが差し替えるストリーム状態 (各テスト冒頭で上書きする)。 */
20
- let mockStreamState: DailyReportIdsStreamState = {
21
- items: [],
22
- loadedCount: 0,
23
- totalCount: null,
24
- phase: "idle",
25
- isRevalidating: false,
26
- hasCompletedOnce: false,
27
- completedAt: null,
28
- error: null,
29
- }
30
-
31
- const ensureMock = vi.fn()
32
- const retryMock = vi.fn()
33
- const reloadMock = vi.fn()
34
-
35
- vi.mock("../streaming/daily-report-ids-stream-session", () => ({
36
- useDailyReportIdsStream: () => mockStreamState,
37
- ensureDailyReportIdsStreamSession: (options: unknown) => ensureMock(options),
38
- retryDailyReportIdsStream: () => retryMock(),
39
- refreshDailyReportIdsStream: vi.fn(),
40
- primeDailyReportIdsStreamSession: vi.fn(),
41
- reloadDocument: () => reloadMock(),
42
- }))
43
-
44
- /** 重量級の子孫 (仮想スクロール・SSE) はマーカーへ置換し、ゲーティングだけを検証する。 */
45
- vi.mock("./daily-report-resolved-content", () => ({
46
- DailyReportResolvedContent: () => <div data-testid="resolved-content" />,
47
- }))
48
-
49
- /** provider は initialItems の受け取りを記録する透過スタブにする。 */
50
- const providerItemsLog: DailyReportItem[][] = []
51
- vi.mock("../contexts/daily-report-action-context", () => ({
52
- DailyReportActionProvider: ({ children, initialItems }: { children: React.ReactNode; initialItems?: DailyReportItem[] }) => {
53
- providerItemsLog.push(initialItems ?? [])
54
- return <div>{children}</div>
55
- },
56
- }))
57
-
58
- /** 状態を部分上書きして描画するヘルパー。 */
59
- const renderPage = (partial: Partial<DailyReportIdsStreamState>) => {
60
- mockStreamState = { ...mockStreamState, items: [], loadedCount: 0, totalCount: null, phase: "idle", isRevalidating: false, hasCompletedOnce: false, completedAt: null, error: null, ...partial }
61
- return render(<DailyReportPage userId="ENC8" />)
62
- }
63
-
64
- const twoItems: DailyReportItem[] = [
65
- { reportHubId: 2, businessDate: "2026-08-02" },
66
- { reportHubId: 1, businessDate: "2026-08-01" },
67
- ]
68
-
69
- describe("DailyReportPage / 局面ゲーティング", () => {
70
- it("未受信 (idle/streaming) はローディング画面を表示する", () => {
71
- renderPage({ phase: "streaming" })
72
- expect(screen.getByText("日報 ID を読み込み中です...")).toBeInTheDocument()
73
- expect(screen.queryByTestId("resolved-content")).not.toBeInTheDocument()
74
- })
75
-
76
- it("最初のチャンク到着 (items > 0) で本体を描画し、provider へアイテムを渡す", () => {
77
- providerItemsLog.length = 0
78
- renderPage({ phase: "streaming", items: twoItems, loadedCount: 2 })
79
- expect(screen.getByTestId("resolved-content")).toBeInTheDocument()
80
- expect(providerItemsLog.at(-1)).toEqual(twoItems)
81
- })
82
-
83
- it("空リストの完走 (complete) は本体を描画する (0 件表示は下層の責務)", () => {
84
- renderPage({ phase: "complete", hasCompletedOnce: true })
85
- expect(screen.getByTestId("resolved-content")).toBeInTheDocument()
86
- })
87
-
88
- it("未受信のまま failed ならエラー画面を出し、再試行がセッション retry を呼ぶ", () => {
89
- retryMock.mockClear()
90
- renderPage({ phase: "failed", error: new Error("x") })
91
- expect(screen.getByText("日報データの読み込みに失敗しました。")).toBeInTheDocument()
92
- fireEvent.click(screen.getByTestId("daily-report-load-error-retry"))
93
- expect(retryMock).toHaveBeenCalledTimes(1)
94
- })
95
-
96
- it("failed でも受信済みアイテムがあれば本体を描画し続ける", () => {
97
- renderPage({ phase: "failed", items: twoItems, loadedCount: 2, error: new Error("x") })
98
- expect(screen.getByTestId("resolved-content")).toBeInTheDocument()
99
- expect(screen.queryByText("日報データの読み込みに失敗しました。")).not.toBeInTheDocument()
100
- })
101
-
102
- it("マウント時にセッションを ensure する (userKey = 難読化ユーザー ID)", () => {
103
- ensureMock.mockClear()
104
- renderPage({ phase: "streaming" })
105
- expect(ensureMock).toHaveBeenCalledWith({ apiBasePath: "/daily_report/api", userKey: "ENC8" })
106
- })
107
-
108
- it("auth-required はドキュメント再読込へ倒す (サーバー loader のリダイレクトに委譲)", () => {
109
- reloadMock.mockClear()
110
- renderPage({ phase: "auth-required" })
111
- expect(reloadMock).toHaveBeenCalledTimes(1)
112
- })
113
-
114
- it("auth-required 以外の局面ではドキュメント再読込しない", () => {
115
- reloadMock.mockClear()
116
- renderPage({ phase: "streaming", items: twoItems, loadedCount: 2 })
117
- expect(reloadMock).not.toHaveBeenCalled()
118
- })
119
- })
@@ -1,140 +0,0 @@
1
- /**
2
- * Top-level daily-report page content (chrome-agnostic, stream-driven).
3
- * アプリのレイアウトに依存しない日報ページ本体コンポーネント (ストリーム駆動)。
4
- *
5
- * ID 一覧はモジュール常駐の NDJSON ストリームセッションから購読し、最初の
6
- * チャンク到着と同時に一覧を描画する (全件解決を待つ Await ゲートは廃止)。
7
- * 再訪時はセッションに残る受信済みデータで即座に復元し、鮮度はバックグラウンド
8
- * 再検証 (SWR) と SSE が担う。アプリ側のルートは本コンポーネントを自前の
9
- * レイアウトで包み、`config` でヘッダー描画・パス・ラベル名を注入する。
10
- */
11
- import { useEffect } from "react"
12
- import type { DailyReportUser } from "../../shared/types"
13
- import { type DailyReportClientConfigInput, DailyReportConfigProvider, useDailyReportConfig } from "../config-context"
14
- import { DailyReportActionProvider } from "../contexts/daily-report-action-context"
15
- import { DailyReportErrorProvider } from "../contexts/daily-report-error-context"
16
- import { ensureDailyReportIdsStreamSession, reloadDocument, retryDailyReportIdsStream, useDailyReportIdsStream } from "../streaming/daily-report-ids-stream-session"
17
- import { DAILY_REPORT_FETCH_DEBOUNCE_MS } from "../utils/constants"
18
- import { DailyReportIdsStreamStatus } from "./daily-report-ids-stream-status"
19
- import { DailyReportResolvedContent } from "./daily-report-resolved-content"
20
-
21
- /**
22
- * Error display when the ids stream terminally failed before any data arrived.
23
- * データ未受信のまま ID ストリームが力尽きたときのエラー表示コンポーネント。
24
- *
25
- * 再試行はストリームセッションの retry へ直結する (受信済みカーソルから再開)。
26
- */
27
- export const DailyReportLoadError = () => {
28
- const { title, renderHeader } = useDailyReportConfig()
29
- return (
30
- <>
31
- {renderHeader({ title, annotation: <span className="font-bold text-red-500">Error</span> })}
32
- <div className="h-full bg-slate-50 p-2">
33
- <div className="mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6">
34
- <div className="flex h-full flex-1 flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-white p-4 text-slate-500 shadow-sm dark:border-red-900 dark:bg-slate-900/70 dark:text-slate-400">
35
- <p className="font-medium text-red-600">日報データの読み込みに失敗しました。</p>
36
- <button
37
- type="button"
38
- data-testid="daily-report-load-error-retry"
39
- onClick={() => retryDailyReportIdsStream()}
40
- className="rounded bg-slate-100 px-4 py-2 text-slate-700 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700">
41
- 再読み込み
42
- </button>
43
- </div>
44
- </div>
45
- </div>
46
- </>
47
- )
48
- }
49
-
50
- /**
51
- * Loading screen shown only until the first stream chunk arrives.
52
- * 最初のストリームチャンクが届くまでの間だけ表示するローディング画面。
53
- */
54
- export const DailyReportLoading = () => {
55
- const { title, renderHeader } = useDailyReportConfig()
56
- return (
57
- <>
58
- {renderHeader({
59
- title,
60
- annotation: (
61
- <div className="flex flex-wrap items-center gap-2 leading-none">
62
- <DailyReportIdsStreamStatus />
63
- <span>総件数: 読み込み中...</span>
64
- <span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>
65
- </div>
66
- ),
67
- })}
68
- <div className="h-full bg-slate-50 p-2">
69
- <div className="mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6">
70
- <div className="flex h-full flex-1 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">日報 ID を読み込み中です...</div>
71
- </div>
72
- </div>
73
- </>
74
- )
75
- }
76
-
77
- export type DailyReportPageProps = {
78
- /** 表示中ユーザー (楽観的コメントの作者名等に使用)。 */
79
- user?: DailyReportUser
80
- /** 難読化済みの内部ユーザー ID (未登録ユーザーは null)。 */
81
- userId?: string | null
82
- /** クライアント設定の部分上書き (ヘッダー描画・パス・ラベル名・フィールドラベルなど)。 */
83
- config?: DailyReportClientConfigInput
84
- }
85
-
86
- /**
87
- * Stream-driven page body: session bootstrap, phase gating, and provider wiring.
88
- * ストリーム駆動のページ本体。セッション確立・局面ゲーティング・プロバイダー結線を担う。
89
- */
90
- const DailyReportPageInner = ({ user, userId }: Omit<DailyReportPageProps, "config">) => {
91
- const { apiBasePath } = useDailyReportConfig()
92
- const stream = useDailyReportIdsStream()
93
-
94
- /**
95
- * ストリームセッション確立の保険。
96
- * 通常は clientLoader が先に確立するが、clientLoader を経ないマウントや
97
- * ユーザー切替 (userKey 不一致でセッション再生成) をここで拾う。
98
- * 依存配列: apiBasePath / userId (いずれかの変化で再確認)。
99
- * クリーンアップ: なし (セッションはモジュール常駐で、アンマウント後も生存させる)。
100
- */
101
- useEffect(() => {
102
- ensureDailyReportIdsStreamSession({ apiBasePath, userKey: userId ?? null })
103
- }, [apiBasePath, userId])
104
-
105
- /**
106
- * 認証切れ (ストリームが 401/403) はドキュメントのフルリロードへ倒し、
107
- * サーバー loader のログインリダイレクトに委譲する。
108
- * 依存配列: stream.phase。クリーンアップ: なし。
109
- */
110
- useEffect(() => {
111
- if (stream.phase === "auth-required") {
112
- reloadDocument()
113
- }
114
- }, [stream.phase])
115
-
116
- // 1 件でも届いたら (または空リストで完走したら) 本体を描画する
117
- const showContent = stream.items.length > 0 || stream.phase === "complete"
118
- return (
119
- // key でユーザーごとにプロバイダーをリマウントする。リロード無しのユーザー切替
120
- // (再ログイン・成り代わり) で items / removedIds / 保留キュー等の状態が旧ユーザーの
121
- // まま残ると、render-phase マージが旧ユーザーの行を新ユーザーの一覧へ持ち越す
122
- // エラーサーフェスはユーザー切替のリマウント境界より外側 (バナーは切替を跨いで消えてよい)。
123
- // アクションプロバイダーはこの notifyError を購読して失敗を可視化する
124
- <DailyReportErrorProvider>
125
- <DailyReportActionProvider key={userId ?? "anonymous"} user={user} initialItems={stream.items} userId={userId}>
126
- {showContent ? <DailyReportResolvedContent userId={userId} /> : stream.phase === "failed" ? <DailyReportLoadError /> : <DailyReportLoading />}
127
- </DailyReportActionProvider>
128
- </DailyReportErrorProvider>
129
- )
130
- }
131
-
132
- /**
133
- * Chrome-agnostic daily report page with client configuration provider.
134
- * 設定プロバイダー込みの日報ページコンポーネント。
135
- */
136
- export const DailyReportPage = ({ config, ...rest }: DailyReportPageProps) => (
137
- <DailyReportConfigProvider config={config}>
138
- <DailyReportPageInner {...rest} />
139
- </DailyReportConfigProvider>
140
- )
@@ -1,307 +0,0 @@
1
- /**
2
- * Tests for resize panels integration in DailyReportList (desktop and mobile layouts).
3
- * DailyReportList におけるリサイズパネル統合 (デスクトップおよびモバイルレイアウト) のテスト。
4
- */
5
-
6
- import { fireEvent, render, screen } from "@testing-library/react"
7
- import { type ReactNode, useState } from "react"
8
- import { beforeEach, describe, expect, it, vi } from "vitest"
9
- import type { DailyReportDetail, DailyReportItem } from "../../shared/types"
10
- import { DailyReportConfigProvider } from "../config-context"
11
-
12
- vi.mock("@aiquants/virtualscroll", async () => {
13
- const { forwardRef: fwd, useImperativeHandle: useHandle } = await import("react")
14
- return {
15
- tapScrollCircleSampleVisual: () => null,
16
- VirtualScroll: fwd((props: { testId?: string; itemCount: number; getItem: (index: number) => unknown; children: (item: unknown, index: number) => ReactNode }, ref: React.Ref<unknown>) => {
17
- useHandle(ref, () => ({ scrollTo: () => 0, scrollToIndex: () => {}, getScrollPosition: () => 0, getContentSize: () => 0, getViewportSize: () => 0, updateItemSize: () => {}, focusItemAtIndex: () => {} }))
18
- return (
19
- <div data-testid={props.testId}>
20
- {Array.from({ length: props.itemCount }, (_, index) => (
21
- // biome-ignore lint/suspicious/noArrayIndexKey: 代役なので index キーで十分
22
- <div key={index}>{props.children(props.getItem(index), index)}</div>
23
- ))}
24
- </div>
25
- )
26
- }),
27
- }
28
- })
29
-
30
- vi.mock("../contexts/daily-report-action-context", async (importOriginal) => {
31
- const actual = await importOriginal<typeof import("../contexts/daily-report-action-context")>()
32
- const noop = async () => {}
33
- return {
34
- ...actual,
35
- useDailyReportActionContext: () => ({
36
- pendingAddCommentIds: { current: new Map() },
37
- pendingDeleteCommentIds: { current: new Map() },
38
- pendingReadUpdates: { current: new Map() },
39
- pendingStarUpdates: { current: new Map() },
40
- resolvedIdMap: { current: new Map() },
41
- version: 0,
42
- items: [],
43
- user: null,
44
- isSseEnabled: false,
45
- toggleSse: () => {},
46
- addComment: noop,
47
- deleteComment: noop,
48
- toggleStar: noop,
49
- toggleRead: noop,
50
- createReport: async () => 0,
51
- updateReport: noop,
52
- publishReport: noop,
53
- deleteReport: noop,
54
- removeStaleItem: () => {},
55
- refetchData: noop,
56
- clearCacheAndRefetch: noop,
57
- }),
58
- }
59
- })
60
-
61
- const detailRef: { current: DailyReportDetail | null } = { current: null }
62
- vi.mock("../hooks/use-daily-report", async (importOriginal) => {
63
- const actual = await importOriginal<typeof import("../hooks/use-daily-report")>()
64
- return {
65
- ...actual,
66
- useDailyReportDetail: () => ({ report: detailRef.current, error: null, isLoading: false, isRefetching: false }),
67
- useDailyReportPrefetch: () => () => {},
68
- }
69
- })
70
-
71
- const { DailyReportList } = await import("./daily-report-list")
72
-
73
- const REPORT_HUB_ID = 100
74
- const item: DailyReportItem = { reportHubId: REPORT_HUB_ID, businessDate: "2026-09-08" }
75
-
76
- const buildDetail = (): DailyReportDetail => ({
77
- reportHubId: REPORT_HUB_ID,
78
- date: "2026-09-08",
79
- interviewers: [],
80
- externalComments: [],
81
- isRead: false,
82
- isStarred: false,
83
- labels: [],
84
- postedComments: [],
85
- attachments: [],
86
- })
87
-
88
- const renderWithConfig = (node: ReactNode) =>
89
- render(
90
- <DailyReportConfigProvider
91
- config={{
92
- apiBasePath: "/daily_report/api",
93
- fieldLabels: { attachments: "添付ファイル", attachmentMissing: "実体が見つかりません", attachmentDownload: "ダウンロード" },
94
- }}>
95
- {node}
96
- </DailyReportConfigProvider>,
97
- )
98
-
99
- describe("DailyReportList / リサイズパネル統合", () => {
100
- beforeEach(() => {
101
- detailRef.current = buildDetail()
102
- vi.stubGlobal(
103
- "ResizeObserver",
104
- class {
105
- observe() {}
106
- unobserve() {}
107
- disconnect() {}
108
- },
109
- )
110
- })
111
-
112
- describe("デスクトップ表示 (isMobileLayout === false)", () => {
113
- beforeEach(() => {
114
- vi.stubGlobal(
115
- "matchMedia",
116
- vi.fn((query: string) => ({
117
- matches: false,
118
- media: query,
119
- onchange: null,
120
- addEventListener: vi.fn(),
121
- removeEventListener: vi.fn(),
122
- addListener: vi.fn(),
123
- removeListener: vi.fn(),
124
- dispatchEvent: vi.fn(),
125
- })),
126
- )
127
- })
128
-
129
- it("PanelGroup が水平方向で描画される", () => {
130
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
131
- const panelGroup = container.querySelector('[data-panel-group-direction="horizontal"]')
132
- expect(panelGroup).toBeInTheDocument()
133
- })
134
-
135
- it("左側の日報一覧パネル (Panel) が正しく配置される", () => {
136
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
137
- const listPanel = container.querySelector('[data-panel-id="daily-report-list-panel"]')
138
- expect(listPanel).toBeInTheDocument()
139
- expect(listPanel?.querySelector('[data-testid="daily-report-list"]')).toBeInTheDocument()
140
- })
141
-
142
- it("右側の詳細ペインパネル (Panel) が正しく配置される", () => {
143
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
144
- const detailPanel = container.querySelector('[data-panel-id="daily-report-detail-panel"]')
145
- expect(detailPanel).toBeInTheDocument()
146
- expect(detailPanel?.querySelector('[data-testid="daily-report-side-pane"]')).toBeInTheDocument()
147
- })
148
-
149
- it("リサイズハンドルがアクセシビリティ属性とインジケーターを備えて描画される", () => {
150
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
151
- const handle = container.querySelector('[data-resize-handle-id="daily-report-list-handle"]')
152
- expect(handle).toBeInTheDocument()
153
- expect(handle).toHaveAttribute("aria-label")
154
- // 支援技術が読み上げるのは、このパッケージが与えた名前でなければならない
155
- // (@aiquants/resize-panels 1.x は利用者の aria-label を黙って捨て、自前の英語名を出していた)
156
- expect(handle?.getAttribute("aria-label")).toBe("日報一覧と詳細ペインの境界リサイズ")
157
-
158
- // 視覚インジケーターおよび内部バーの存在検証
159
- const indicator = handle?.querySelector("[data-resize-handle-indicator]")
160
- expect(indicator).toBeInTheDocument()
161
- const indicatorBar = indicator?.querySelector("[data-resize-handle-indicator-bar]")
162
- expect(indicatorBar).toBeInTheDocument()
163
- })
164
-
165
- it("リサイズハンドルはキーボード操作可能なボタン要素である", () => {
166
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
167
- const handle = container.querySelector('[data-resize-handle-id="daily-report-list-handle"]') as HTMLButtonElement
168
- expect(handle).toBeInTheDocument()
169
- expect(handle.tagName).toBe("BUTTON")
170
- expect(handle.type).toBe("button")
171
- expect(handle.disabled).toBe(false)
172
- })
173
-
174
- it("詳細ペインパネルに境界侵食防止用の左パディングクラス (pl-1.5) が付与されている", () => {
175
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
176
- const detailPanel = container.querySelector('[data-panel-id="daily-report-detail-panel"]')
177
- expect(detailPanel).toBeInTheDocument()
178
- expect(detailPanel?.className).toContain("pl-1.5")
179
- })
180
-
181
- it("キーボードリサイズ操作により矢印キー操作が安全に処理される", () => {
182
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
183
- const handle = container.querySelector('[data-resize-handle-id="daily-report-list-handle"]') as HTMLButtonElement
184
- expect(handle).toBeInTheDocument()
185
-
186
- // 左右の矢印キーでリサイズを実行
187
- fireEvent.keyDown(handle, { key: "ArrowRight" })
188
- fireEvent.keyDown(handle, { key: "ArrowLeft" })
189
- expect(handle).toBeInTheDocument()
190
- })
191
-
192
- it("ポインタードラッグ操作によるリサイズ処理が安全に完了する", () => {
193
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
194
- const handle = container.querySelector('[data-resize-handle-id="daily-report-list-handle"]') as HTMLButtonElement
195
- expect(handle).toBeInTheDocument()
196
-
197
- fireEvent.pointerDown(handle, { clientX: 360, pointerId: 1 })
198
- fireEvent.pointerMove(handle, { clientX: 420, pointerId: 1 })
199
- fireEvent.pointerUp(handle, { clientX: 420, pointerId: 1 })
200
- expect(handle).toBeInTheDocument()
201
- })
202
-
203
- it("データが 0 件のとき、デスクトップ表示で空状態表示と PanelGroup が整合して描画される", () => {
204
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[]} selectedReportHubId={null} onSelectItem={() => {}} />)
205
- const panelGroup = container.querySelector('[data-panel-group-direction="horizontal"]')
206
- expect(panelGroup).toBeInTheDocument()
207
- expect(screen.getByText("データがありません")).toBeInTheDocument()
208
- expect(screen.getByText("左側の一覧から日報を選択してください。")).toBeInTheDocument()
209
- })
210
-
211
- it('ルート要素に data-testid="daily-report-root" が付与される', () => {
212
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
213
- expect(container.querySelector('[data-testid="daily-report-root"]')).toBeInTheDocument()
214
- })
215
- })
216
-
217
- describe("コンテナ幅狭小化時のフォールバック (isContainerNarrow === true)", () => {
218
- let resizeCallback: (entries: { target: Element; contentRect: { width: number } }[]) => void
219
-
220
- beforeEach(() => {
221
- vi.stubGlobal(
222
- "matchMedia",
223
- vi.fn((query: string) => ({
224
- matches: false, // デスクトップ扱い
225
- media: query,
226
- onchange: null,
227
- addEventListener: vi.fn(),
228
- removeEventListener: vi.fn(),
229
- addListener: vi.fn(),
230
- removeListener: vi.fn(),
231
- dispatchEvent: vi.fn(),
232
- })),
233
- )
234
-
235
- vi.stubGlobal(
236
- "ResizeObserver",
237
- class {
238
- constructor(callback: (entries: { target: Element; contentRect: { width: number } }[]) => void) {
239
- resizeCallback = callback
240
- }
241
- observe(el: Element) {
242
- // 460px 未満の狭小コンテナ幅を即時シミュレート
243
- resizeCallback([{ target: el, contentRect: { width: 400 } }])
244
- }
245
- unobserve() {}
246
- disconnect() {}
247
- },
248
- )
249
- })
250
-
251
- it("利用可能幅が狭小化した場合は 2 カラムではなく 1 カラム表示へフォールバックする", () => {
252
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
253
- // 狭小幅時は PanelGroup がアンマウントされ、一覧のみが描画される
254
- expect(container.querySelector('[data-panel-group-direction="horizontal"]')).toBeNull()
255
- expect(container.querySelector('[data-resize-handle-id="daily-report-list-handle"]')).toBeNull()
256
- expect(screen.getByTestId("daily-report-list")).toBeInTheDocument()
257
- })
258
- })
259
-
260
- describe("モバイル表示 (isMobileLayout === true)", () => {
261
- beforeEach(() => {
262
- vi.stubGlobal(
263
- "matchMedia",
264
- vi.fn((query: string) => ({
265
- matches: true,
266
- media: query,
267
- onchange: null,
268
- addEventListener: vi.fn(),
269
- removeEventListener: vi.fn(),
270
- addListener: vi.fn(),
271
- removeListener: vi.fn(),
272
- dispatchEvent: vi.fn(),
273
- })),
274
- )
275
- })
276
-
277
- it("PanelGroup および PanelResizeHandle は描画されず、一覧が直接描画される", () => {
278
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
279
- expect(container.querySelector('[data-panel-group-direction="horizontal"]')).toBeNull()
280
- expect(container.querySelector('[data-resize-handle-id="daily-report-list-handle"]')).toBeNull()
281
- expect(screen.getByTestId("daily-report-list")).toBeInTheDocument()
282
- })
283
-
284
- it("初期表示時にモバイルオーバーレイが自動展開されないこと", () => {
285
- const { container } = renderWithConfig(<DailyReportList dailyReportItems={[item]} selectedReportHubId={REPORT_HUB_ID} onSelectItem={() => {}} />)
286
- expect(container.querySelector('aside[role="dialog"]')).toBeNull()
287
- })
288
-
289
- it("アイテムタップでモバイルオーバーレイが展開され、--aqdr-overlay-right のインラインスタイルが適用されること", () => {
290
- const TestComponent = () => {
291
- const [selectedId, setSelectedId] = useState<number | null>(null)
292
- return <DailyReportList dailyReportItems={[item]} selectedReportHubId={selectedId} onSelectItem={setSelectedId} />
293
- }
294
- const { container } = renderWithConfig(<TestComponent />)
295
- expect(container.querySelector('[data-daily-report-mobile-overlay="true"]')).toBeNull()
296
-
297
- const card = container.querySelector(`[data-daily-report-button="${REPORT_HUB_ID}"]`)
298
- expect(card).not.toBeNull()
299
- if (!card) throw new Error("card not found")
300
- fireEvent.keyDown(card, { key: "Enter" })
301
-
302
- const overlayRoot = container.querySelector('[data-daily-report-mobile-overlay="true"]')
303
- expect(overlayRoot).not.toBeNull()
304
- expect(overlayRoot).toHaveStyle({ right: "var(--aqdr-overlay-right, 0px)" })
305
- })
306
- })
307
- })