[FEAT] 導入 Playwright E2E 測試環境與登入功能測試腳本
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m36s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m36s
This commit is contained in:
@@ -19,6 +19,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
|||||||
| 跨模組、Service Interface、`Contracts`、模組間通訊、`ServiceProvider` 綁定、禁止跨模組引用 | **跨模組調用與通訊規範** | `.agents/skills/cross-module-communication/SKILL.md` |
|
| 跨模組、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` |
|
| 按鈕樣式、表格規範、圖標、分頁、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` |
|
| 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** — 設定權限
|
1. **permission-management** — 設定權限
|
||||||
2. **ui-consistency** — 遵循 UI 規範
|
2. **ui-consistency** — 遵循 UI 規範
|
||||||
3. **activity-logging** — 若涉及 Model CRUD,需加上操作紀錄
|
3. **activity-logging** — 若涉及 Model CRUD,需加上操作紀錄
|
||||||
|
4. **e2e-testing** — 確認是否需要新增對應的 E2E 測試
|
||||||
|
|
||||||
### 🔴 新增或修改 Model 時
|
### 🔴 新增或修改 Model 時
|
||||||
必須讀取:
|
必須讀取:
|
||||||
|
|||||||
266
.agents/skills/e2e-testing/SKILL.md
Normal file
266
.agents/skills/e2e-testing/SKILL.md
Normal file
@@ -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`?
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -34,3 +34,12 @@ docs/f6_1770350984272.xlsx
|
|||||||
.gitignore
|
.gitignore
|
||||||
BOM表自動計算成本.md
|
BOM表自動計算成本.md
|
||||||
公共事業費-類別維護.md
|
公共事業費-類別維護.md
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
node_modules/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
e2e/screenshots/
|
||||||
|
|||||||
14
e2e/helpers/auth.ts
Normal file
14
e2e/helpers/auth.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
72
e2e/login.spec.ts
Normal file
72
e2e/login.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
83
package-lock.json
generated
83
package-lock.json
generated
@@ -43,6 +43,7 @@
|
|||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -846,6 +848,22 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
@@ -2847,6 +2865,7 @@
|
|||||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -2856,6 +2875,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2866,6 +2886,7 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -3001,6 +3022,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3285,7 +3307,8 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
@@ -5379,6 +5402,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5386,6 +5410,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -5475,6 +5547,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -5539,6 +5612,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@@ -5669,7 +5743,8 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -6035,7 +6110,8 @@
|
|||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
@@ -6360,6 +6436,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
115
package.json
115
package.json
@@ -1,59 +1,60 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://www.schemastore.org/package.json",
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
"name": "star-erp",
|
"name": "star-erp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "vite"
|
"dev": "vite"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^25.0.3",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/node": "^25.0.3",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react": "^19.2.7",
|
||||||
"axios": "^1.11.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"concurrently": "^9.0.1",
|
"axios": "^1.11.0",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"concurrently": "^9.0.1",
|
||||||
"tailwindcss": "^4.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"typescript": "^5.9.3",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.0.7"
|
"typescript": "^5.9.3",
|
||||||
},
|
"vite": "^7.0.7"
|
||||||
"dependencies": {
|
},
|
||||||
"@inertiajs/react": "^2.3.4",
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@inertiajs/react": "^2.3.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/lodash": "^4.17.21",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@types/lodash": "^4.17.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"clsx": "^2.1.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"cmdk": "^1.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"cmdk": "^1.1.1",
|
||||||
"jsbarcode": "^3.12.1",
|
"date-fns": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"jsbarcode": "^3.12.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.3.1",
|
"lucide-react": "^0.562.0",
|
||||||
"react-dom": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"recharts": "^3.7.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"recharts": "^3.7.0",
|
||||||
"sonner": "^2.0.7",
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.4.0"
|
"sonner": "^2.0.7",
|
||||||
}
|
"tailwind-merge": "^3.4.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
playwright.config.ts
Normal file
47
playwright.config.ts
Normal file
@@ -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'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user