diff --git a/.agents/rules/skill-trigger.md b/.agents/rules/skill-trigger.md index a78b82c..b06eddc 100644 --- a/.agents/rules/skill-trigger.md +++ b/.agents/rules/skill-trigger.md @@ -19,6 +19,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。 | 跨模組、Service Interface、`Contracts`、模組間通訊、`ServiceProvider` 綁定、禁止跨模組引用 | **跨模組調用與通訊規範** | `.agents/skills/cross-module-communication/SKILL.md` | | 按鈕樣式、表格規範、圖標、分頁、Badge、Toast、表單、UI 統一、頁面佈局、`button-filled-*`、`button-outlined-*`、`lucide-react`、色彩系統 | **客戶端後台 UI 統一規範** | `.agents/skills/ui-consistency/SKILL.md` | | Git 分支、commit、push、合併、部署、`feature/`、`hotfix/`、`develop`、`main` | **Git 分支管理與開發規範** | `.agents/skills/git-workflows/SKILL.md` | +| E2E、端到端測試、Playwright、`spec.ts`、功能驗證、自動化測試、回歸測試 | **E2E 端到端測試規範** | `.agents/skills/e2e-testing/SKILL.md` | --- @@ -31,6 +32,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。 1. **permission-management** — 設定權限 2. **ui-consistency** — 遵循 UI 規範 3. **activity-logging** — 若涉及 Model CRUD,需加上操作紀錄 +4. **e2e-testing** — 確認是否需要新增對應的 E2E 測試 ### 🔴 新增或修改 Model 時 必須讀取: diff --git a/.agents/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md new file mode 100644 index 0000000..c4faa2c --- /dev/null +++ b/.agents/skills/e2e-testing/SKILL.md @@ -0,0 +1,266 @@ +--- +name: E2E 端到端測試規範 (E2E Testing with Playwright) +description: 規範 Playwright 端到端測試的撰寫慣例、目錄結構、共用工具與執行方式,確保所有 E2E 測試保持一致性與可維護性。 +--- + +# E2E 端到端測試規範 (E2E Testing with Playwright) + +本技能定義了 Star ERP 系統中端到端 (E2E) 測試的實作標準,使用 Playwright 模擬真實使用者操作瀏覽器,驗證 UI 顯示與功能流程的正確性。 + +--- + +## 1. 專案結構 + +### 1.1 目錄配置 + +``` +star-erp/ +├── playwright.config.ts # Playwright 設定檔 +├── e2e/ # E2E 測試根目錄 +│ ├── helpers/ # 共用工具函式 +│ │ └── auth.ts # 登入 helper +│ ├── screenshots/ # 測試截圖存放 +│ ├── auth.spec.ts # 認證相關測試(登入、登出) +│ ├── inventory.spec.ts # 庫存模組測試 +│ ├── products.spec.ts # 商品模組測試 +│ └── {module}.spec.ts # 依模組命名 +├── playwright-report/ # HTML 測試報告(自動產生,已 gitignore) +└── test-results/ # 失敗截圖與錄影(自動產生,已 gitignore) +``` + +### 1.2 命名規範 + +| 項目 | 規範 | 範例 | +|---|---|---| +| 測試檔案 | 小寫,依模組命名 `.spec.ts` | `inventory.spec.ts` | +| 測試群組 | `test.describe('中文功能名稱')` | `test.describe('庫存查詢')` | +| 測試案例 | 中文描述「**應**」開頭 | `test('應顯示庫存清單')` | +| 截圖檔案 | `{module}-{scenario}.png` | `inventory-search-result.png` | + +--- + +## 2. 設定檔 (playwright.config.ts) + +### 2.1 核心設定 + +```typescript +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:8081', // Sail 開發伺服器 + screenshot: 'only-on-failure', // 失敗時自動截圖 + video: 'retain-on-failure', // 失敗時保留錄影 + trace: 'on-first-retry', // 重試時收集 trace + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); +``` + +### 2.2 重要注意事項 + +> [!IMPORTANT] +> `baseURL` 必須指向本機 Sail 開發伺服器(預設 `http://localhost:8081`)。 +> 確保測試前已執行 `./vendor/bin/sail up -d` 與 `./vendor/bin/sail npm run dev`。 + +--- + +## 3. 共用工具 (Helpers) + +### 3.1 登入 Helper + +位置:`e2e/helpers/auth.ts` + +```typescript +import { Page } from '@playwright/test'; + +/** + * 共用登入函式 + * 使用測試帳號登入 ERP 系統 + */ +export async function login(page: Page, username = 'mama', password = 'mama9453') { + await page.goto('/'); + await page.fill('#username', username); + await page.fill('#password', password); + await page.getByRole('button', { name: '登入系統' }).click(); + // 等待儀表板載入完成 + await page.waitForSelector('text=系統概況', { timeout: 10000 }); +} +``` + +### 3.2 使用方式 + +```typescript +import { login } from './helpers/auth'; + +test('應顯示庫存清單', async ({ page }) => { + await login(page); + await page.goto('/inventory/stock-query'); + // ...斷言 +}); +``` + +--- + +## 4. 測試撰寫規範 + +### 4.1 測試結構模板 + +```typescript +import { test, expect } from '@playwright/test'; +import { login } from './helpers/auth'; + +test.describe('模組功能名稱', () => { + + // 若整個 describe 都需要登入,使用 beforeEach + test.beforeEach(async ({ page }) => { + await login(page); + }); + + test('應正確顯示頁面標題與關鍵元素', async ({ page }) => { + await page.goto('/target-page'); + + // 驗證頁面標題 + await expect(page.getByText('頁面標題')).toBeVisible(); + + // 驗證表格存在 + await expect(page.locator('table')).toBeVisible(); + }); + + test('應能執行 CRUD 操作', async ({ page }) => { + // ... + }); +}); +``` + +### 4.2 斷言 (Assertions) 慣例 + +| 場景 | 優先使用 | 避免使用 | +|---|---|---| +| 驗證頁面載入 | `page.getByText('關鍵文字')` | `page.waitForURL()` ※ | +| 驗證元素存在 | `expect(locator).toBeVisible()` | `.count() > 0` | +| 驗證表格資料 | `page.locator('table tbody tr')` | 硬編碼行數 | +| 等待操作完成 | `expect().toBeVisible({ timeout })` | `page.waitForTimeout()` | + +> [!NOTE] +> ※ Star ERP 使用 Inertia.js,頁面導航不一定改變 URL(例如儀表板路由為 `/`)。 +> 因此**優先使用頁面內容驗證**,而非依賴 URL 變化。 + +### 4.3 選擇器優先順序 + +依照 Playwright 官方建議,選擇器優先順序為: + +1. **Role** — `page.getByRole('button', { name: '登入系統' })` +2. **Text** — `page.getByText('系統概況')` +3. **Label** — `page.getByLabel('帳號')` +4. **Placeholder** — `page.getByPlaceholder('請輸入...')` +5. **Test ID** — `page.getByTestId('submit-btn')`(需在元件加 `data-testid`) +6. **CSS** — `page.locator('#username')`(最後手段) + +### 4.4 禁止事項 + +```typescript +// ❌ 禁止:硬等待(不可預期的等待時間) +await page.waitForTimeout(5000); + +// ✅ 正確:等待特定條件 +await expect(page.getByText('操作成功')).toBeVisible({ timeout: 5000 }); + +// ❌ 禁止:在測試中寫死測試資料的 ID +await page.goto('/products/42/edit'); + +// ✅ 正確:從頁面互動導航 +await page.locator('table tbody tr').first().getByRole('button', { name: '編輯' }).click(); +``` + +--- + +## 5. 截圖與視覺回歸 + +### 5.1 手動截圖(文件用途) + +```typescript +// 成功截圖存於 e2e/screenshots/ +await page.screenshot({ + path: 'e2e/screenshots/inventory-list.png', + fullPage: true, +}); +``` + +### 5.2 視覺回歸測試(偵測 UI 變化) + +```typescript +test('庫存頁面 UI 應保持一致', async ({ page }) => { + await login(page); + await page.goto('/inventory/stock-query'); + // 比對截圖,pixel 級差異會報錯 + await expect(page).toHaveScreenshot('stock-query.png', { + maxDiffPixelRatio: 0.01, // 容許 1% 差異(動態資料) + }); +}); +``` + +> [!NOTE] +> 首次執行 `toHaveScreenshot()` 會自動建立基準截圖。 +> 後續執行會與基準比對,更新基準用:`npx playwright test --update-snapshots` + +--- + +## 6. 執行指令速查 + +```bash +# 執行所有 E2E 測試 +npx playwright test + +# 執行特定模組測試 +npx playwright test e2e/login.spec.ts + +# UI 互動模式(可視化瀏覽器操作) +npx playwright test --ui + +# 帶頭模式(顯示瀏覽器畫面) +npx playwright test --headed + +# 產生 HTML 報告並開啟 +npx playwright test --reporter=html +npx playwright show-report + +# 更新視覺回歸基準截圖 +npx playwright test --update-snapshots + +# 只執行特定測試案例(用 -g 篩選名稱) +npx playwright test -g "登入" + +# Debug 模式(逐步執行) +npx playwright test --debug +``` + +--- + +## 7. 開發檢核清單 (Checklist) + +### 新增頁面或功能時: + +- [ ] 是否已為新頁面建立對應的 `.spec.ts` 測試檔? +- [ ] 測試是否覆蓋主要的 Happy Path(正常操作流程)? +- [ ] 測試是否覆蓋關鍵的 Error Path(錯誤處理)? +- [ ] 共用的登入步驟是否使用 `helpers/auth.ts`? +- [ ] 斷言是否優先使用頁面內容而非 URL? +- [ ] 選擇器是否遵循優先順序(Role > Text > Label > CSS)? +- [ ] 測試是否可獨立執行(不依賴其他測試的狀態)? + +### 提交程式碼前: + +- [ ] 全部 E2E 測試是否通過?(`npx playwright test`) +- [ ] 是否有遺留的 `test.only` 或 `test.skip`? diff --git a/.gitignore b/.gitignore index c1f5f8f..975985c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,12 @@ docs/f6_1770350984272.xlsx .gitignore BOM表自動計算成本.md 公共事業費-類別維護.md + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +e2e/screenshots/ diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts new file mode 100644 index 0000000..4d2aaaf --- /dev/null +++ b/e2e/helpers/auth.ts @@ -0,0 +1,14 @@ +import { Page } from '@playwright/test'; + +/** + * 共用登入函式 + * 使用測試帳號登入 Star ERP 系統 + */ +export async function login(page: Page, username = 'mama', password = 'mama9453') { + await page.goto('/'); + await page.fill('#username', username); + await page.fill('#password', password); + await page.getByRole('button', { name: '登入系統' }).click(); + // 等待儀表板載入完成 + await page.waitForSelector('text=系統概況', { timeout: 10000 }); +} diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts new file mode 100644 index 0000000..bc623b1 --- /dev/null +++ b/e2e/login.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from '@playwright/test'; +import { login } from './helpers/auth'; + +/** + * Star ERP 登入流程端到端測試 + */ +test.describe('登入功能', () => { + + test('頁面應正確顯示登入表單', async ({ page }) => { + // 前往登入頁面 + await page.goto('/'); + + // 驗證:帳號輸入框存在 + await expect(page.locator('#username')).toBeVisible(); + + // 驗證:密碼輸入框存在 + await expect(page.locator('#password')).toBeVisible(); + + // 驗證:登入按鈕存在 + await expect(page.getByRole('button', { name: '登入系統' })).toBeVisible(); + }); + + test('輸入錯誤的帳密應顯示錯誤訊息', async ({ page }) => { + await page.goto('/'); + + // 輸入錯誤的帳號密碼 + await page.fill('#username', 'wronguser'); + await page.fill('#password', 'wrongpassword'); + + // 點擊登入 + await page.getByRole('button', { name: '登入系統' }).click(); + + // 等待頁面回應 + await page.waitForTimeout(2000); + + // 驗證:應該還是停在登入頁面(未成功跳轉) + await expect(page).toHaveURL(/\//); + + // 驗證:頁面上應顯示某種錯誤提示(紅色文字或 toast) + // 先用寬鬆的檢查方式,後續可以根據實際錯誤訊息調整 + const hasError = await page.locator('.text-red-500, .text-red-600, [role="alert"], .toast, [data-sonner-toast]').count(); + expect(hasError).toBeGreaterThan(0); + }); + + test('輸入正確帳密後應成功登入並跳轉', async ({ page }) => { + // 使用共用登入函式 + await login(page); + + // 驗證:頁面右上角應顯示使用者名稱 + await expect(page.getByText('mama')).toBeVisible(); + + // 驗證:儀表板的關鍵指標卡片存在 + await expect(page.getByText('庫存總值')).toBeVisible(); + + // 截圖留存(成功登入畫面) + await page.screenshot({ path: 'e2e/screenshots/login-success.png', fullPage: true }); + }); + + test('空白帳密不應能登入', async ({ page }) => { + await page.goto('/'); + + // 不填任何東西,直接點登入 + await page.getByRole('button', { name: '登入系統' }).click(); + + // 等待頁面回應 + await page.waitForTimeout(1000); + + // 驗證:應該還在登入頁面 + await expect(page.locator('#username')).toBeVisible(); + }); + +}); diff --git a/package-lock.json b/package-lock.json index 16bf295..b2dee62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "tailwind-merge": "^3.4.0" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.0.0", "@types/node": "^25.0.3", "@types/react": "^19.2.7", @@ -83,6 +84,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -846,6 +848,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2847,6 +2865,7 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2856,6 +2875,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2866,6 +2886,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3001,6 +3022,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3285,7 +3307,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -5379,6 +5402,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5386,6 +5410,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5463,6 +5534,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5475,6 +5547,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5539,6 +5612,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5669,7 +5743,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6035,7 +6110,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -6360,6 +6436,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index ae0ff06..b6fa688 100644 --- a/package.json +++ b/package.json @@ -1,59 +1,60 @@ { - "$schema": "https://www.schemastore.org/package.json", - "name": "star-erp", - "private": true, - "type": "module", - "scripts": { - "build": "vite build", - "dev": "vite" - }, - "devDependencies": { - "@tailwindcss/vite": "^4.0.0", - "@types/node": "^25.0.3", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "axios": "^1.11.0", - "concurrently": "^9.0.1", - "laravel-vite-plugin": "^2.0.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.9.3", - "vite": "^7.0.7" - }, - "dependencies": { - "@inertiajs/react": "^2.3.4", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/typography": "^0.5.19", - "@types/lodash": "^4.17.21", - "@vitejs/plugin-react": "^5.1.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^4.1.0", - "jsbarcode": "^3.12.1", - "lodash": "^4.17.21", - "lucide-react": "^0.562.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-hot-toast": "^2.6.0", - "react-markdown": "^10.1.0", - "recharts": "^3.7.0", - "remark-gfm": "^4.0.1", - "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0" - } + "$schema": "https://www.schemastore.org/package.json", + "name": "star-erp", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.9.3", + "vite": "^7.0.7" + }, + "dependencies": { + "@inertiajs/react": "^2.3.4", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/typography": "^0.5.19", + "@types/lodash": "^4.17.21", + "@vitejs/plugin-react": "^5.1.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "jsbarcode": "^3.12.1", + "lodash": "^4.17.21", + "lucide-react": "^0.562.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", + "react-markdown": "^10.1.0", + "recharts": "^3.7.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0" + } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..3563eb1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,47 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Playwright E2E 測試設定檔 + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './e2e', + + /* 平行執行測試 */ + fullyParallel: true, + + /* CI 環境下禁止 test.only */ + forbidOnly: !!process.env.CI, + + /* CI 環境下失敗重試 2 次 */ + retries: process.env.CI ? 2 : 0, + + /* CI 環境下單 worker */ + workers: process.env.CI ? 1 : undefined, + + /* 使用 HTML 報告 */ + reporter: 'html', + + /* 全域共用設定 */ + use: { + /* 本機開發伺服器位址 */ + baseURL: 'http://localhost:8081', + + /* 失敗時自動截圖 */ + screenshot: 'only-on-failure', + + /* 失敗時保留錄影 */ + video: 'retain-on-failure', + + /* 失敗重試時收集 trace */ + trace: 'on-first-retry', + }, + + /* 只使用 Chromium 進行測試(開發階段足夠) */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});