diff --git a/.agent/workflows/gen-test-cases.md b/.agent/workflows/gen-test-cases.md
index 3e985e5..cfdd2b6 100644
--- a/.agent/workflows/gen-test-cases.md
+++ b/.agent/workflows/gen-test-cases.md
@@ -6,15 +6,15 @@ description: 生成測試案例工作流
STEP 1: 檢查專案底下是否建立 `doc/test` 資料夾,若無則建立
-STEP 2: 根據選擇範圍撰寫測試案例清單,撰寫格式請參考 `.agent/workflows/test/template.md`,並將發想的結果用 Markdown 格式寫入 `doc/test` 底下,完成後進行 Reivew 不要直接生成測試程式,確認符合預期後再下一步
+STEP 2: 根據選擇範圍撰寫測試案例清單,撰寫格式請參考 `.agent/workflows/test/test-doc-template.md`,並將發想的結果用 Markdown 格式寫入 `doc/test` 底下,完成後進行 Reivew 不要直接生成測試程式,確認符合預期後再下一步
STEP 3: 參考上一步完成的 `doc/test` 撰寫測試程式,不同頁面需建立獨立檔案
**重要規則:**
+
- 第二層 `describe()` 必須為「測試類型」,下面每一個 `it()` 描述必須為對應的測試案例的「測試說明」
- `it()` 內的文字描述請「直接使用 Markdown 的原文,不需翻譯、不需重新命名」
-
STEP 4: 驗證測試程式,若結果符合預期,請去 doc/test 底下,將 Markdown 打勾
-STEP 5: 若測試不符預期,重複 STEP 3–4,最多 5 次,仍失敗就討論原因
\ No newline at end of file
+STEP 5: 若測試不符預期,重複 STEP 3–4,最多 5 次,仍失敗就討論原因
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..523c964
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,38 @@
+name: Automated Testing
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - '**'
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests with coverage
+ run: npm run test:coverage
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: coverage-report
+ path: coverage/
+ retention-days: 7
diff --git a/doc/test/AdminPage-test.md b/doc/test/AdminPage-test.md
new file mode 100644
index 0000000..3bf161f
--- /dev/null
+++ b/doc/test/AdminPage-test.md
@@ -0,0 +1,38 @@
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+> 測試類型:前端元素、function 邏輯、Mock API、驗證權限...
+
+---
+
+## [x] 【前端元素】畫面正常渲染管理後台
+
+**範例輸入**:存取管理後台頁面
+**期待輸出**:畫面顯示「🛠️ 管理後台」標題、返回連結、登出按鈕,以及「管理員專屬頁面」的功能介紹區塊
+
+---
+
+## [x] 【前端邏輯】正確顯示管理員身分標籤
+
+**範例輸入**:AuthContext 提供 `user.role` 為 `admin`
+**期待輸出**:畫面右上角的身分標籤顯示「管理員」,且帶有多出的 `admin` class
+
+---
+
+## [x] 【前端邏輯】正確顯示一般用戶身分標籤
+
+**範例輸入**:AuthContext 提供 `user.role` 為 `user`
+**期待輸出**:畫面右上角的身分標籤顯示「一般用戶」,且帶有多出的 `user` class
+
+---
+
+## [x] 【路由導向】點擊返回按鈕
+
+**範例輸入**:點擊「← 返回」連結
+**期待輸出**:導向至 `/dashboard`
+
+---
+
+## [x] 【前端邏輯】點擊登出按鈕
+
+**範例輸入**:點擊「登出」按鈕
+**期待輸出**:觸發 `logout()` 函數,並導向至 `/login`(使用 replace,且 state 設為 null)
diff --git a/doc/test/AdminPage.md b/doc/test/AdminPage.md
new file mode 100644
index 0000000..eb1e80f
--- /dev/null
+++ b/doc/test/AdminPage.md
@@ -0,0 +1,22 @@
+# AdminPage 測試案例
+
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+
+---
+
+## [ ] 【畫面渲染】應該正確渲染管理後台的元素
+**範例輸入**:進入 /admin 頁面
+**期待輸出**:顯示「管理後台」標題、返回 Dashboard 連結及管理員專屬資訊卡片
+
+---
+
+## [x] 【權限顯示】應正確顯示使用者的角色標籤
+**範例輸入**:依據 user.role 分別渲染為 'admin' 或 'user'
+**期待輸出**:為 admin 時顯示「管理員」,否則顯示「一般用戶」
+
+---
+
+## [x] 【登出邏輯】點擊登出按鈕應觸發登出並導向登入頁
+**範例輸入**:點擊「登出」按鈕
+**期待輸出**:呼叫 `logout` 函式,並導向至 `/login` 頁面
diff --git a/doc/test/DashboardPage-test.md b/doc/test/DashboardPage-test.md
new file mode 100644
index 0000000..c420ada
--- /dev/null
+++ b/doc/test/DashboardPage-test.md
@@ -0,0 +1,59 @@
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+> 測試類型:前端元素、function 邏輯、Mock API、驗證權限...
+
+---
+
+## [x] 【前端邏輯】管理員看到專屬連結
+
+**範例輸入**:AuthContext 提供 `user.role` 為 `admin`,預設 Mock API 回傳空列表
+**期待輸出**:Header 導覽列出現「🛠️ 管理後台」連結
+
+---
+
+## [x] 【前端邏輯】一般用戶看不到專屬連結
+
+**範例輸入**:AuthContext 提供 `user.role` 為 `user`,預設 Mock API 回傳空列表
+**期待輸出**:Header 導覽列不應該出現「🛠️ 管理後台」連結
+
+---
+
+## [x] 【前端邏輯】歡迎區塊正常渲染用戶資訊
+
+**範例輸入**:AuthContext 提供 `user.username` 為 `TestUser`,`user.role` 為 `user`
+**期待輸出**:歡迎區塊顯示「Welcome, TestUser 👋」,頭像顯示「T」,身分標籤顯示「一般用戶」
+
+---
+
+## [x] 【Mock API】初始載入時顯示 Loading
+
+**範例輸入**:`productApi.getProducts` 設定為延遲回傳
+**期待輸出**:商品列表區塊顯示「載入商品中...」與 spinner
+
+---
+
+## [x] 【Mock API】成功取得商品資料並渲染
+
+**範例輸入**:`productApi.getProducts` 成功回傳兩筆商品資料(例如 商品A 100元, 商品B 200元)
+**期待輸出**:畫面不再顯示 Loading,並成功渲染出 商品A 與 商品B 的卡片,且價格有正確格式化(例如 `NT$ 100`)
+
+---
+
+## [ ] 【Mock API】取得商品資料失敗且後端有錯誤訊息
+
+**範例輸入**:`productApi.getProducts` 失敗,AxiosError 回傳 500 錯誤與 `{ message: '伺服器發生異常' }`
+**期待輸出**:商品列表區塊顯示錯誤圖示與文字「伺服器發生異常」
+
+---
+
+## [ ] 【Mock API】取得商品資料失敗時的預設錯誤訊息
+
+**範例輸入**:`productApi.getProducts` 失敗,AxiosError 回傳 500 錯誤但無自訂 `message`
+**期待輸出**:商品列表區塊顯示錯誤區塊與預設訊息「無法載入商品資料」
+
+---
+
+## [ ] 【前端邏輯】點擊登出按鈕
+
+**範例輸入**:點擊 Header 的「登出」按鈕
+**期待輸出**:觸發 `logout()` 函數,並導向至 `/login`(使用 replace,且 state 設為 null)
diff --git a/doc/test/DashboardPage.md b/doc/test/DashboardPage.md
new file mode 100644
index 0000000..76db6af
--- /dev/null
+++ b/doc/test/DashboardPage.md
@@ -0,0 +1,52 @@
+# DashboardPage 測試案例
+
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+
+---
+
+## [ ] 【畫面渲染】初始載入時應顯示載入中狀態
+**範例輸入**:進入 /dashboard 頁面,API 正在請求中
+**期待輸出**:顯示「載入商品中...」及 Loading Spinner
+
+---
+
+## [x] 【畫面渲染】API 請求成功後應正確顯示商品列表
+**範例輸入**:API 回傳包含商品資訊的陣列
+**期待輸出**:隱藏 Loading 狀態,並渲染對應數量的商品卡片,顯示商品名稱、描述與價格
+
+---
+
+## [x] 【錯誤處理】API 請求失敗時 (非 401) 應顯示錯誤訊息
+**範例輸入**:API 回傳 500 等錯誤,且帶有 message
+**期待輸出**:隱藏 Loading 狀態,顯示 API 的錯誤訊息或預設錯誤訊息「無法載入商品資料」
+
+---
+
+## [x] 【錯誤處理】Token 過期或無效 (401) 時不顯示預設錯誤訊息
+**範例輸入**:API 回傳 401 錯誤
+**期待輸出**:交由 axios interceptor 處理,畫面上不會另外顯示 401 的文字錯誤狀態
+
+---
+
+## [x] 【權限顯示】依據使用者名稱正確顯示歡迎詞與頭像
+**範例輸入**:user 資料為 username: 'Alice'
+**期待輸出**:顯示「Welcome, Alice 👋」,頭像顯示 'A'
+
+---
+
+## [x] 【權限顯示】為管理員時應顯示前往管理後台的連結
+**範例輸入**:user.role 為 'admin'
+**期待輸出**:顯示「🛠️ 管理後台」的連結,可導向 /admin
+
+---
+
+## [ ] 【權限顯示】非管理員時不應顯示管理後台連結
+**範例輸入**:user.role 不為 'admin' (如 'user')
+**期待輸出**:不顯示「🛠️ 管理後台」的連結
+
+---
+
+## [ ] 【登出邏輯】點擊登出按鈕應觸發登出並導向登入頁
+**範例輸入**:點擊「登出」按鈕
+**期待輸出**:呼叫 `logout` 函式,並導向至 `/login` 頁面
diff --git a/doc/test/LoginPage-test.md b/doc/test/LoginPage-test.md
new file mode 100644
index 0000000..7697e82
--- /dev/null
+++ b/doc/test/LoginPage-test.md
@@ -0,0 +1,66 @@
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+> 測試類型:前端元素、function 邏輯、Mock API、驗證權限...
+
+---
+
+## [x] 【前端元素】畫面正常渲染登入表單
+
+**範例輸入**:存取登入頁面
+**期待輸出**:畫面顯示 Email 輸入框、密碼輸入框、以及「登入」按鈕
+
+---
+
+## [x] 【前端驗證】無效的 Email 格式
+
+**範例輸入**:輸入 Email 為 `invalid`,密碼為 `Valid123`,並點擊登入
+**期待輸出**:Email 欄位下方顯示「請輸入有效的 Email 格式」,且不觸發 API 請求
+
+---
+
+## [x] 【前端驗證】密碼長度不足 8 碼
+
+**範例輸入**:輸入 Email 為 `test@example.com`,密碼為 `a1`,並點擊登入
+**期待輸出**:密碼欄位下方顯示「密碼必須至少 8 個字元」,且不觸發 API 請求
+
+---
+
+## [x] 【前端驗證】密碼未包含英數混合
+
+**範例輸入**:輸入 Email 為 `test@example.com`,密碼為 `12345678`,並點擊登入
+**期待輸出**:密碼欄位下方顯示「密碼必須包含英文字母和數字」,且不觸發 API 請求
+
+---
+
+## [x] 【Mock API】登入成功時顯示載入中並導向
+
+**範例輸入**:輸入正確格式的 Email 與密碼並提交,Mock API 設定為延遲並回傳成功
+**期待輸出**:按鈕文字變成「登入中...」並帶有 spinner,API 成功後導向至 `/dashboard`(使用 replace)
+
+---
+
+## [x] 【Mock API】登入失敗時顯示後端自訂錯誤訊息
+
+**範例輸入**:輸入正確格式的 Email 與密碼並提交,Mock API 回傳 401 與錯誤訊息 `帳號或密碼錯誤`
+**期待輸出**:按鈕恢復為「登入」,且畫面上方的錯誤區塊 (`error-banner`) 顯示 `帳號或密碼錯誤`
+
+---
+
+## [x] 【Mock API】登入失敗且後端未提供訊息時的預設錯誤
+
+**範例輸入**:輸入正確格式的 Email 與密碼並提交,Mock API 回傳 500(未包含特定 message)
+**期待輸出**:錯誤區塊顯示預設訊息「登入失敗,請稍後再試」
+
+---
+
+## [x] 【路由導向】若已登入則自動導回控制台
+
+**範例輸入**:設定 AuthContext 中的 `isAuthenticated` 為 `true`,存取登入頁
+**期待輸出**:自動導向至 `/dashboard`(使用 replace)
+
+---
+
+## [x] 【全域狀態】顯示登入過期的快顯訊息
+
+**範例輸入**:設定 AuthContext 中的 `authExpiredMessage` 為 `您的登入已過期`
+**期待輸出**:錯誤區塊顯示 `您的登入已過期`,且觸發 `clearAuthExpiredMessage` 清除原訊息
diff --git a/doc/test/LoginPage.md b/doc/test/LoginPage.md
new file mode 100644
index 0000000..744bb32
--- /dev/null
+++ b/doc/test/LoginPage.md
@@ -0,0 +1,58 @@
+# LoginPage 測試案例
+
+> 狀態:初始為 [ ]、完成為 [x]
+> 注意:狀態只能在測試通過後由流程更新。
+
+---
+
+## [ ] 【畫面渲染】應該正確渲染登入表單與相關元素
+**範例輸入**:進入 /login 頁面
+**期待輸出**:顯示「歡迎回來」標題、Email 輸入框、密碼輸入框及登入按鈕
+
+---
+
+## [x] 【表單驗證】輸入無效的 Email 格式時應顯示錯誤訊息
+**範例輸入**:在 Email 輸入框輸入 `invalid-email` 並提交表單
+**期待輸出**:顯示「請輸入有效的 Email 格式」錯誤訊息,且不會觸發登入 API
+
+---
+
+## [x] 【表單驗證】輸入長度不足的密碼時應顯示錯誤訊息
+**範例輸入**:在密碼輸入框輸入 `Short1!` 並提交表單
+**期待輸出**:顯示「密碼必須至少 8 個字元」錯誤訊息,且不會觸發登入 API
+
+---
+
+## [x] 【表單驗證】輸入未包含大小寫與數字的密碼時應顯示錯誤訊息
+**範例輸入**:在密碼輸入框輸入 `alllowercase123` 或 `ALLUPPERCASE123` 等缺乏組成的密碼並提交表單
+**期待輸出**:顯示「密碼必須包含大小寫英文字母和數字」錯誤訊息,且不會觸發登入 API
+
+---
+
+## [x] 【登入邏輯】API 回傳錯誤時應顯示錯誤提示
+**範例輸入**:輸入正確格式的 Email 與密碼,但 API 回傳登入失敗
+**期待輸出**:畫面上方顯示「登入失敗,請稍後再試」或 API 回傳的具體錯誤訊息
+
+---
+
+## [ ] 【登入邏輯】登入成功後應導向至 Dashboard
+**範例輸入**:輸入正確格式的 Email 與密碼,API 回傳成功
+**期待輸出**:成功調用 login 函式,並導向至 `/dashboard` 頁面
+
+---
+
+## [ ] 【狀態處理】提交表單時按鈕應顯示載入中狀態並禁用
+**範例輸入**:點擊登入按鈕提交表單
+**期待輸出**:按鈕文字變更為「登入中...」,且輸入框與按鈕皆設定為 disabled
+
+---
+
+## [ ] 【已登入狀態】若使用者已認證應自動導向至 Dashboard
+**範例輸入**:在 isAuthenticated 為 true 的狀態下訪問 /login
+**期待輸出**:自動導向至 `/dashboard` 頁面
+
+---
+
+## [ ] 【狀態處理】若有 authExpiredMessage 應顯示並清除狀態
+**範例輸入**:由 AuthContext 取得 authExpiredMessage 為「登入已過期」
+**期待輸出**:顯示對應 API 錯誤訊息,並呼叫 clearAuthExpiredMessage
diff --git a/package-lock.json b/package-lock.json
index ab6d465..dab3471 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -136,7 +136,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -496,7 +495,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -540,7 +538,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -1852,7 +1849,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -1937,7 +1935,6 @@
"integrity": "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1948,7 +1945,6 @@
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1959,7 +1955,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2015,7 +2010,6 @@
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.52.0",
"@typescript-eslint/types": "8.52.0",
@@ -2396,7 +2390,6 @@
"integrity": "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/utils": "4.0.16",
"fflate": "^0.8.2",
@@ -2433,7 +2426,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2623,7 +2615,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2945,7 +2936,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
@@ -3109,7 +3101,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3867,7 +3858,6 @@
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -4012,6 +4002,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -4150,7 +4141,6 @@
"integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@inquirer/confirm": "^5.0.0",
"@mswjs/interceptors": "^0.40.0",
@@ -4369,7 +4359,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4422,6 +4411,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -4437,6 +4427,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -4465,7 +4456,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4475,7 +4465,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4488,7 +4477,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-refresh": {
"version": "0.18.0",
@@ -4991,7 +4981,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5087,7 +5076,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -5163,7 +5151,6 @@
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16",
@@ -5453,7 +5440,6 @@
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/pages/AdminPage.test.tsx b/src/pages/AdminPage.test.tsx
new file mode 100644
index 0000000..37495e3
--- /dev/null
+++ b/src/pages/AdminPage.test.tsx
@@ -0,0 +1,88 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { AdminPage } from './AdminPage';
+import { useAuth } from '../context/AuthContext';
+import { vi, describe, it, expect, beforeEach } from 'vitest';
+
+// Mock react-router-dom
+const mockNavigate = vi.fn();
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom');
+ return {
+ ...actual,
+ useNavigate: () => mockNavigate,
+ };
+});
+
+// Mock AuthContext
+const mockLogout = vi.fn();
+
+vi.mock('../context/AuthContext', async () => {
+ const actual = await vi.importActual('../context/AuthContext');
+ return {
+ ...actual,
+ useAuth: vi.fn(),
+ };
+});
+
+describe('AdminPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Default AuthContext mock setup
+ (useAuth as any).mockReturnValue({
+ user: { role: 'admin' },
+ logout: mockLogout,
+ });
+ });
+
+ const renderComponent = () => {
+ return render(
+
測試帳號:任意 email 格式 / 密碼需包含英數且8位以上
+測試帳號:任意 email 格式 / 密碼需包含大小寫英數且 8 位以上