@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.
- package/README.md +229 -8
- package/dist/client.d.mts +201 -739
- package/dist/client.d.ts +201 -739
- package/dist/client.js +5 -6
- package/dist/client.mjs +5 -6
- package/dist/index.d.mts +5 -197
- package/dist/index.d.ts +5 -197
- package/dist/index.js +1 -2
- package/dist/index.mjs +1 -2
- package/dist/logger-D3krZrNK.d.mts +0 -14
- package/dist/logger-D3krZrNK.d.ts +0 -14
- package/dist/server.d.mts +114 -744
- package/dist/server.d.ts +114 -744
- package/dist/server.js +1 -2
- package/dist/server.mjs +1 -2
- package/dist/{sse-schema-ChSFm-4q.d.mts → sse-schema-DCxpdETM.d.mts} +1106 -1090
- package/dist/{sse-schema-BdUGThiz.d.ts → sse-schema-DoQw8_om.d.ts} +1106 -1090
- package/dist/types-CqgYQgbH.d.mts +0 -34
- package/dist/types-CqgYQgbH.d.ts +0 -34
- package/package.json +9 -10
- package/dist/client.js.map +0 -1
- package/dist/client.mjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/server.mjs.map +0 -1
- package/src/client/components/business-day-thumb-overlay.tsx +0 -19
- package/src/client/components/daily-report-attachment-indicator.spec.tsx +0 -51
- package/src/client/components/daily-report-attachment-indicator.tsx +0 -45
- package/src/client/components/daily-report-attachment-list.spec.tsx +0 -94
- package/src/client/components/daily-report-attachment-list.tsx +0 -112
- package/src/client/components/daily-report-comment-item.tsx +0 -89
- package/src/client/components/daily-report-comment-section.tsx +0 -166
- package/src/client/components/daily-report-detail-list.tsx +0 -842
- package/src/client/components/daily-report-edit-form.tsx +0 -87
- package/src/client/components/daily-report-ids-stream-status.spec.tsx +0 -93
- package/src/client/components/daily-report-ids-stream-status.tsx +0 -67
- package/src/client/components/daily-report-list.tsx +0 -1232
- package/src/client/components/daily-report-page.spec.tsx +0 -119
- package/src/client/components/daily-report-page.tsx +0 -140
- package/src/client/components/daily-report-resize-panels.spec.tsx +0 -307
- package/src/client/components/daily-report-resolved-content.tsx +0 -207
- package/src/client/components/daily-report-scroll-restoration.spec.tsx +0 -422
- package/src/client/components/report-views.spec.tsx +0 -375
- package/src/client/components/unread-indicator.tsx +0 -13
- package/src/client/config-context.tsx +0 -184
- package/src/client/contexts/daily-report-action-context.spec.tsx +0 -433
- package/src/client/contexts/daily-report-action-context.tsx +0 -1147
- package/src/client/contexts/daily-report-error-context.spec.tsx +0 -66
- package/src/client/contexts/daily-report-error-context.tsx +0 -98
- package/src/client/hooks/use-animated-number.spec.ts +0 -103
- package/src/client/hooks/use-animated-number.ts +0 -69
- package/src/client/hooks/use-daily-report-comments.ts +0 -73
- package/src/client/hooks/use-daily-report-sse-connection.ts +0 -86
- package/src/client/hooks/use-daily-report.spec.ts +0 -546
- package/src/client/hooks/use-daily-report.ts +0 -549
- package/src/client/hooks/use-dynamic-viewport-height.ts +0 -127
- package/src/client/hooks/use-responsive-layout.spec.ts +0 -74
- package/src/client/hooks/use-responsive-layout.ts +0 -63
- package/src/client/route-helpers.spec.ts +0 -100
- package/src/client/route-helpers.ts +0 -61
- package/src/client/streaming/daily-report-ids-stream-client.spec.ts +0 -910
- package/src/client/streaming/daily-report-ids-stream-client.ts +0 -730
- package/src/client/streaming/daily-report-ids-stream-session.spec.ts +0 -401
- package/src/client/streaming/daily-report-ids-stream-session.ts +0 -319
- package/src/client/ui/button.tsx +0 -42
- package/src/client/ui/cn.ts +0 -14
- package/src/client/ui/input.tsx +0 -21
- package/src/client/ui/label.tsx +0 -16
- package/src/client/ui/switch.tsx +0 -19
- package/src/client/ui/tabs.tsx +0 -40
- package/src/client/ui/textarea.tsx +0 -19
- package/src/client/utils/constants.spec.ts +0 -96
- package/src/client/utils/constants.ts +0 -84
- package/src/client.ts +0 -28
- package/src/index.ts +0 -11
- package/src/lucide-icons.d.ts +0 -6
- package/src/server/authz.spec.ts +0 -57
- package/src/server/authz.ts +0 -99
- package/src/server/cache.spec.ts +0 -556
- package/src/server/cache.ts +0 -303
- package/src/server/etag.ts +0 -18
- package/src/server/external-source.ts +0 -60
- package/src/server/handlers.action.spec.ts +0 -44
- package/src/server/handlers.attachment.spec.ts +0 -470
- package/src/server/handlers.ids-stream.spec.ts +0 -618
- package/src/server/handlers.ts +0 -1638
- package/src/server/handlers.unauthorized-cookie.spec.ts +0 -187
- package/src/server/handlers.visibility.spec.ts +0 -774
- package/src/server/ports.ts +0 -222
- package/src/server/response.ts +0 -37
- package/src/server/schema.spec.ts +0 -30
- package/src/server/schema.ts +0 -344
- package/src/server/service.spec.ts +0 -1130
- package/src/server/service.ts +0 -2045
- package/src/server/sse-reader.spec.ts +0 -302
- package/src/server/sse-reader.ts +0 -267
- package/src/server/test-helpers/handlers-config.ts +0 -321
- package/src/server/transaction-retry.spec.ts +0 -63
- package/src/server/visibility.spec.ts +0 -403
- package/src/server/visibility.ts +0 -373
- package/src/server.spec.ts +0 -160
- package/src/server.ts +0 -131
- package/src/shared/business-date.spec.ts +0 -82
- package/src/shared/business-date.ts +0 -104
- package/src/shared/comment-adapter.spec.ts +0 -38
- package/src/shared/comment-adapter.ts +0 -80
- package/src/shared/ids-stream.spec.ts +0 -154
- package/src/shared/ids-stream.ts +0 -279
- package/src/shared/logger.ts +0 -47
- package/src/shared/sse-schema.spec.ts +0 -50
- package/src/shared/sse-schema.ts +0 -198
- package/src/shared/text-utils.spec.ts +0 -42
- package/src/shared/text-utils.ts +0 -65
- package/src/shared/types.ts +0 -109
- 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
|
-
})
|