@relipa/ai-flow-kit 0.1.2 → 0.1.3
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 +26 -0
- package/bin/aiflow.js +40 -1
- package/custom/harness/playwright/.env.example +10 -0
- package/custom/harness/playwright/playwright.config.ts +34 -0
- package/custom/harness/playwright/tests/e2e/auth.setup.ts +25 -0
- package/custom/harness/playwright/tests/e2e/fixtures/test.ts +6 -0
- package/custom/harness/playwright/tests/e2e/pages/BasePage.ts +9 -0
- package/custom/harness/playwright/tests/e2e/support/auth.ts +5 -0
- package/custom/mcp-presets/playwright.json +8 -0
- package/custom/rules/test-patterns.md +70 -0
- package/custom/skills/automation-testing/SKILL.md +239 -0
- package/custom/skills/automation-testing/templates/BasePage.ts +29 -0
- package/custom/skills/automation-testing/templates/PageObject.example.ts +29 -0
- package/custom/skills/automation-testing/templates/playwright.config.ts +39 -0
- package/custom/skills/automation-testing/templates/spec.example.ts +29 -0
- package/custom/skills/coverage-check/SKILL.md +202 -0
- package/custom/skills/evidence-aggregation/SKILL.md +246 -0
- package/custom/skills/execute-flow/SKILL.md +479 -0
- package/custom/skills/execute-flow/templates/playwright.config.ts +39 -0
- package/custom/skills/generate-test-report/SKILL.md +99 -0
- package/custom/skills/generate-test-report/templates/test-report.md +58 -0
- package/custom/skills/generate-testcase/SKILL.md +303 -0
- package/custom/skills/generate-testcase/templates/testcase.md +88 -0
- package/custom/skills/log-bug/SKILL.md +131 -0
- package/custom/skills/pr-impact-analysis/SKILL.md +180 -0
- package/custom/skills/retest-orchestration/SKILL.md +191 -0
- package/custom/skills/script-sync/SKILL.md +208 -0
- package/custom/skills/test-analysis/SKILL.md +262 -0
- package/custom/templates/shared/gate-workflow.md +306 -2
- package/docs/common/CHANGELOG.md +52 -0
- package/docs/common/QUICK_START.md +30 -0
- package/docs/common/cli-reference.md +48 -7
- package/package.json +1 -1
- package/scripts/create-score-excel.js +20 -7
- package/scripts/detect.js +10 -0
- package/scripts/guide.js +9 -0
- package/scripts/prompt.js +36 -0
- package/scripts/scaffold-playwright.js +106 -0
- package/scripts/task.js +21 -7
- package/scripts/update.js +124 -124
- package/scripts/use.js +30 -12
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ npm uninstall -g @relipa/ai-flow-kit
|
|
|
91
91
|
|---------|-------------|
|
|
92
92
|
| `aiflow init` | Setup framework, adapters, and multi-AI rules. |
|
|
93
93
|
| `aiflow use <ticket...>` | Load context from one or more tickets/files. First target is primary; rest become supplementary. |
|
|
94
|
+
| `aiflow execute <target>` | **QA:** Execute test cases — load TC file, sync Playwright scripts, run and collect evidence. |
|
|
94
95
|
| `aiflow fetch-links <url>` | Fetch a Backlog/Jira link and print SupplementaryContext JSON (used by AI at runtime). |
|
|
95
96
|
| `aiflow task` | Manage multiple tasks — pause, switch, resume. |
|
|
96
97
|
| `aiflow task next` | Finalize gate approval and prepare for fresh session. |
|
|
@@ -141,6 +142,31 @@ aiflow init --framework spring-boot,reactjs --adapter backlog,jira
|
|
|
141
142
|
|
|
142
143
|
---
|
|
143
144
|
|
|
145
|
+
### `aiflow execute <target>`
|
|
146
|
+
|
|
147
|
+
Execute test cases from a Backlog/Jira ticket, a TC file, or manually. The AI syncs Playwright scripts, runs them, collects evidence, and generates a test report.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# From a ticket (taskType must be "execute")
|
|
151
|
+
aiflow execute PROJ-44
|
|
152
|
+
|
|
153
|
+
# From a TC file directly
|
|
154
|
+
aiflow execute ./testcases/AD10.md
|
|
155
|
+
|
|
156
|
+
# Manual — AI asks for TC file path
|
|
157
|
+
aiflow execute
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**What happens (4-gate Execute Flow):**
|
|
161
|
+
1. **Gate 1 — Pre-flight:** Parse TC file, verify `ak-test/{repo}/` exists, check `BASE_URL` and `playwright.config.ts`
|
|
162
|
+
2. **Gate 2 — Script Sync:** Hash-based TC↔script sync — gen new, update changed, skip unchanged (never fabricates selectors — uses Playwright MCP)
|
|
163
|
+
3. **Gate 3 — Execute & Evidence:** Run Playwright, organize evidence per `run-{N}/`, update TC file R1/R2 columns
|
|
164
|
+
4. **Gate 4 — Report & Bug Logging:** Generate `testreport.md`, ask per-bug Jira confirmation
|
|
165
|
+
|
|
166
|
+
> **Multi-AI:** Works with Claude, Gemini, Cursor, Copilot — the execute-flow skill is loaded via `ak init`/`ak up`.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
144
170
|
### `aiflow sync-skills`
|
|
145
171
|
|
|
146
172
|
Synchronize AI instruction files (`CLAUDE.md`, `GEMINI.md`, etc.) and Skills without upgrading the package version.
|
package/bin/aiflow.js
CHANGED
|
@@ -19,6 +19,7 @@ const taskCommand = require('../scripts/task');
|
|
|
19
19
|
const checkpointCommand = require('../scripts/checkpoint');
|
|
20
20
|
const { record } = require('../scripts/telemetry/record');
|
|
21
21
|
const { updateTaskGateState } = require('../scripts/task');
|
|
22
|
+
const scaffoldPlaywrightCommand = require('../scripts/scaffold-playwright');
|
|
22
23
|
const semver = require('semver');
|
|
23
24
|
const { execSync: execSyncChild, spawnSync } = require('child_process');
|
|
24
25
|
|
|
@@ -63,7 +64,8 @@ program
|
|
|
63
64
|
'up': 'update',
|
|
64
65
|
'sync': 'sync-skills',
|
|
65
66
|
'dr': 'doctor',
|
|
66
|
-
'tel': 'telemetry'
|
|
67
|
+
'tel': 'telemetry',
|
|
68
|
+
'sc': 'scaffold'
|
|
67
69
|
};
|
|
68
70
|
if (aliases[first]) {
|
|
69
71
|
first = aliases[first];
|
|
@@ -575,4 +577,41 @@ program
|
|
|
575
577
|
telemetryCommand(action || 'status', options);
|
|
576
578
|
});
|
|
577
579
|
|
|
580
|
+
// ── scaffold ──────────────────────────────────────────────────
|
|
581
|
+
const scaffoldCmd = program.command('scaffold').alias('sc').description('Scaffold project harness files');
|
|
582
|
+
|
|
583
|
+
scaffoldCmd
|
|
584
|
+
.command('playwright')
|
|
585
|
+
.alias('pw')
|
|
586
|
+
.description('Scaffold Playwright E2E harness (playwright.config.ts, BasePage, fixtures, auth, .mcp.json)')
|
|
587
|
+
.option('-f, --force', 'overwrite existing files')
|
|
588
|
+
.option('-d, --dir <path>', 'target directory (default: current directory)')
|
|
589
|
+
.addHelpText('after', `
|
|
590
|
+
Examples:
|
|
591
|
+
$ ak scaffold playwright Scaffold into current directory
|
|
592
|
+
$ ak scaffold playwright --force Overwrite existing files
|
|
593
|
+
$ ak scaffold playwright --dir ./my-app Scaffold into ./my-app
|
|
594
|
+
|
|
595
|
+
Next steps after scaffolding:
|
|
596
|
+
1. npm install --save-dev @playwright/test
|
|
597
|
+
2. npx playwright install chromium
|
|
598
|
+
3. cp .env.example .env # then fill in BASE_URL
|
|
599
|
+
4. npx playwright test
|
|
600
|
+
`)
|
|
601
|
+
.action(async (options) => {
|
|
602
|
+
const targetDir = options.dir ? require('path').resolve(options.dir) : process.cwd();
|
|
603
|
+
console.log(chalk.cyan('\n⟳ Scaffolding Playwright E2E harness...\n'));
|
|
604
|
+
const result = await scaffoldPlaywrightCommand.scaffoldPlaywright(targetDir, { force: !!options.force });
|
|
605
|
+
console.log(chalk.cyan('\n📦 Next steps:'));
|
|
606
|
+
console.log(chalk.gray(' 1. npm install --save-dev @playwright/test'));
|
|
607
|
+
console.log(chalk.gray(' 2. npx playwright install chromium'));
|
|
608
|
+
console.log(chalk.gray(' 3. cp .env.example .env # then fill in BASE_URL'));
|
|
609
|
+
console.log(chalk.gray(' 4. npx playwright test'));
|
|
610
|
+
if (result.copied.length === 0 && result.skipped.length > 0) {
|
|
611
|
+
console.log(chalk.yellow('\n ⚠ All files already exist. Use --force to overwrite.\n'));
|
|
612
|
+
} else {
|
|
613
|
+
console.log(chalk.green('\n✨ Scaffold complete!\n'));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
578
617
|
program.parse(process.argv);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sao chép thành .env rồi điền giá trị. KHÔNG commit .env (đã .gitignore).
|
|
2
|
+
|
|
3
|
+
BASE_URL=http://localhost:3000
|
|
4
|
+
|
|
5
|
+
# --- Đăng nhập — chỉ cần khi app yêu cầu login ---
|
|
6
|
+
# Có E2E_USER → auth tự bật: đăng nhập 1 lần, lưu phiên, các test tái dùng.
|
|
7
|
+
E2E_USER=
|
|
8
|
+
E2E_PASSWORD=
|
|
9
|
+
E2E_LOGIN_PATH=/login
|
|
10
|
+
E2E_POST_LOGIN_PATH=/dashboard
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
try { require('dotenv').config(); } catch (_) {}
|
|
4
|
+
|
|
5
|
+
const baseURL = process.env.BASE_URL || 'http://localhost:3000';
|
|
6
|
+
const authFile = 'tests/e2e/.auth/user.json';
|
|
7
|
+
const useAuth = Boolean(process.env.E2E_USER);
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
testDir: './tests/e2e/specs',
|
|
11
|
+
fullyParallel: false,
|
|
12
|
+
forbidOnly: !!process.env.CI,
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
reporter: [
|
|
15
|
+
['html', { outputFolder: 'playwright-report', open: 'never' }],
|
|
16
|
+
['list'],
|
|
17
|
+
],
|
|
18
|
+
use: {
|
|
19
|
+
baseURL,
|
|
20
|
+
screenshot: 'only-on-failure',
|
|
21
|
+
video: 'on-first-retry',
|
|
22
|
+
trace: 'on-first-retry',
|
|
23
|
+
},
|
|
24
|
+
projects: useAuth
|
|
25
|
+
? [
|
|
26
|
+
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
|
|
27
|
+
{
|
|
28
|
+
name: 'chromium',
|
|
29
|
+
use: { ...devices['Desktop Chrome'], storageState: authFile },
|
|
30
|
+
dependencies: ['setup'],
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
: [{ name: 'chromium', use: devices['Desktop Chrome'] }],
|
|
34
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { test as setup, expect } from '@playwright/test';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
const authFile = path.join('tests', 'e2e', '.auth', 'user.json');
|
|
5
|
+
|
|
6
|
+
setup('xác thực', async ({ page }) => {
|
|
7
|
+
const user = process.env.E2E_USER;
|
|
8
|
+
const password = process.env.E2E_PASSWORD;
|
|
9
|
+
const loginPath = process.env.E2E_LOGIN_PATH || '/login';
|
|
10
|
+
const postLoginPath = process.env.E2E_POST_LOGIN_PATH || '/dashboard';
|
|
11
|
+
|
|
12
|
+
if (!user || !password) {
|
|
13
|
+
throw new Error('E2E_USER và E2E_PASSWORD phải được thiết lập trong .env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await page.goto(loginPath);
|
|
17
|
+
// Cập nhật các locator bên dưới bằng selector thật từ app (dùng automation-testing skill)
|
|
18
|
+
await page.getByRole('textbox', { name: 'Email' }).fill(user);
|
|
19
|
+
await page.getByLabel('Mật khẩu').fill(password);
|
|
20
|
+
await page.getByRole('button', { name: 'Đăng nhập' }).click();
|
|
21
|
+
|
|
22
|
+
const escaped = postLoginPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
+
await expect(page).toHaveURL(new RegExp(escaped));
|
|
24
|
+
await page.context().storageState({ path: authFile });
|
|
25
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Test Patterns — Quy ước chung cho automation-testing
|
|
2
|
+
|
|
3
|
+
Tài liệu này được skill `automation-testing` đọc ở Phase 1 và áp dụng khi sinh test.
|
|
4
|
+
Mục tiêu: test ổn định, dễ đọc, ít vỡ khi UI đổi.
|
|
5
|
+
|
|
6
|
+
## Nguyên tắc chọn locator (ưu tiên từ trên xuống)
|
|
7
|
+
|
|
8
|
+
1. `getByRole(role, { name })` — bám theo accessibility, bền nhất.
|
|
9
|
+
2. `getByLabel(text)` — cho input có `<label>`.
|
|
10
|
+
3. `getByPlaceholder(text)` — khi không có label.
|
|
11
|
+
4. `getByText(text)` — cho message, nội dung tĩnh.
|
|
12
|
+
5. `getByTestId(id)` — khi dev đã gắn `data-testid`.
|
|
13
|
+
6. CSS / XPath — **chỉ khi bất đắc dĩ**, kèm comment lý do.
|
|
14
|
+
|
|
15
|
+
> Locator phải lấy từ DOM thật (Phase 3 qua Playwright MCP), không tự bịa tên.
|
|
16
|
+
|
|
17
|
+
## Quy ước chung
|
|
18
|
+
|
|
19
|
+
- Mỗi test độc lập: tự `navigate` tới trang cần test, không phụ thuộc test trước.
|
|
20
|
+
- Page Object: 1 file / 1 màn hình; locator khai báo ở constructor; action là method.
|
|
21
|
+
- Spec import `{ test, expect }` từ `tests/e2e/fixtures/test.ts`.
|
|
22
|
+
- ID test (`TC01`, `TC02`...) khớp bảng test case đã chốt ở Phase 2.
|
|
23
|
+
- Tên test, comment viết tiếng Việt.
|
|
24
|
+
- Không hardcode tài khoản/dữ liệu thật; dùng dữ liệu test do tester cung cấp.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Pattern 1 — Login
|
|
29
|
+
|
|
30
|
+
Steps điển hình: vào trang login → nhập email/password → submit.
|
|
31
|
+
- Thành công: `await expect(page).toHaveURL(/dashboard/)`.
|
|
32
|
+
- Sai thông tin: `await expect(errorMessage).toBeVisible()`.
|
|
33
|
+
- Validation rỗng: submit ngay → `await expect(page.getByText('... bắt buộc')).toBeVisible()`.
|
|
34
|
+
|
|
35
|
+
## Pattern 2 — Form validation
|
|
36
|
+
|
|
37
|
+
- Submit khi field rỗng → kiểm tra message bắt buộc của từng field.
|
|
38
|
+
- Nhập sai định dạng (email, số điện thoại) → kiểm tra message định dạng.
|
|
39
|
+
- Nhập hợp lệ → submit thành công (toast / redirect / record mới xuất hiện).
|
|
40
|
+
|
|
41
|
+
## Pattern 3 — CRUD (Create / Read / Update / Delete)
|
|
42
|
+
|
|
43
|
+
- **Create**: mở form → điền → submit → khẳng định record xuất hiện trong list.
|
|
44
|
+
- **Read**: tìm/lọc → khẳng định kết quả đúng.
|
|
45
|
+
- **Update**: mở record → sửa → lưu → khẳng định giá trị mới hiển thị.
|
|
46
|
+
- **Delete**: xóa → xác nhận → khẳng định record biến mất.
|
|
47
|
+
- Mẹo: tạo dữ liệu có hậu tố thời gian/ngẫu nhiên để tránh trùng giữa các lần chạy.
|
|
48
|
+
|
|
49
|
+
## Pattern 4 — Modal / Dialog
|
|
50
|
+
|
|
51
|
+
- Mở modal → `await expect(dialog).toBeVisible()`.
|
|
52
|
+
- Thao tác trong modal dùng locator scope trong dialog: `dialog.getByRole(...)`.
|
|
53
|
+
- Đóng/confirm → khẳng định modal đóng (`toBeHidden`) và side-effect đúng.
|
|
54
|
+
|
|
55
|
+
## Pattern 5 — Table / Pagination / Search
|
|
56
|
+
|
|
57
|
+
- Search: nhập từ khóa → khẳng định hàng khớp xuất hiện, hàng không khớp biến mất.
|
|
58
|
+
- Pagination: sang trang → khẳng định nội dung trang đổi.
|
|
59
|
+
- Dùng `getByRole('row')`, `getByRole('cell')` để bám bảng theo accessibility.
|
|
60
|
+
|
|
61
|
+
## Pattern 6 — Auth-gated pages (storageState)
|
|
62
|
+
|
|
63
|
+
Cơ chế có sẵn trong repo — **ưu tiên dùng**, đừng đăng nhập lại trong từng test:
|
|
64
|
+
|
|
65
|
+
- Đăng nhập **1 lần** ở `tests/e2e/auth.setup.ts`, lưu phiên vào `.auth/user.json`.
|
|
66
|
+
- Bật bằng env `E2E_USER` (+ `E2E_PASSWORD`, `E2E_LOGIN_PATH`, `E2E_POST_LOGIN_PATH`).
|
|
67
|
+
- Khi bật, mọi test chạy ở trạng thái **đã đăng nhập** sẵn — không cần `beforeEach` login.
|
|
68
|
+
- Test cần **chưa đăng nhập** (test login/logout): `test.use({ storageState: LOGGED_OUT })`
|
|
69
|
+
ở đầu spec (`LOGGED_OUT` import từ `../../fixtures/test`).
|
|
70
|
+
- Selector trong `auth.setup.ts` phải là selector THẬT của trang login (lấy ở Phase 3).
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: automation-testing
|
|
3
|
+
description: Sinh Playwright TypeScript E2E test từ test case draft của tester. Dùng Playwright MCP để xem DOM THẬT của app và lấy selector chính xác (không đoán). Dùng khi tester muốn tạo script test tự động từ mô tả test case bằng ngôn ngữ tự nhiên.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_generate_locator, mcp__playwright__browser_click, mcp__playwright__browser_type, mcp__playwright__browser_fill_form, mcp__playwright__browser_select_option, mcp__playwright__browser_press_key, mcp__playwright__browser_wait_for, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages, mcp__playwright__browser_navigate_back, mcp__playwright__browser_close
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill: automation-testing
|
|
8
|
+
|
|
9
|
+
Sinh **Playwright TypeScript** test scripts từ test case draft của tester.
|
|
10
|
+
Điểm khác biệt cốt lõi so với "sinh code thuần": skill **mở app thật bằng Playwright MCP**,
|
|
11
|
+
chụp cây accessibility/DOM và sinh selector từ element CÓ THẬT — nên script chạy được ngay,
|
|
12
|
+
tester không cần biết TypeScript và hạn chế tối đa việc nhờ dev sửa selector.
|
|
13
|
+
|
|
14
|
+
## Cách gọi
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/automation-testing baseUrl: <url> [autoRun: true|false] [headed: true|false] [ticketId: <id>]
|
|
18
|
+
---
|
|
19
|
+
<test case draft của tester — text tự do, mỗi dòng một case>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Ví dụ:
|
|
23
|
+
```
|
|
24
|
+
/automation-testing baseUrl: http://localhost:3000 autoRun: false
|
|
25
|
+
---
|
|
26
|
+
1. Vào trang login, nhập đúng email/password → vào được dashboard
|
|
27
|
+
2. Nhập sai password → hiện thông báo lỗi
|
|
28
|
+
3. Để trống email → hiện validation lỗi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Tham số (parse từ phần đầu lời gọi)
|
|
32
|
+
|
|
33
|
+
Đọc các tham số từ text người dùng truyền vào (dạng `key: value`). Nếu không thấy, dùng default.
|
|
34
|
+
|
|
35
|
+
| Param | Default | Ý nghĩa |
|
|
36
|
+
|------------|---------|---------|
|
|
37
|
+
| `baseUrl` | — | URL web app đang test. **Bắt buộc.** Nếu thiếu → hỏi tester rồi mới tiếp tục. |
|
|
38
|
+
| `autoRun` | `false` | Tự chạy `npx playwright test` sau khi sinh script. |
|
|
39
|
+
| `headed` | `true` | Hiện cửa sổ browser khi `autoRun=true`. |
|
|
40
|
+
| `ticketId` | suy ra | Thư mục con cho spec (`tests/e2e/specs/<ticketId>/`). Nếu thiếu → suy từ tên feature (vd `login`). |
|
|
41
|
+
|
|
42
|
+
Phần text sau dấu `---` là **test case draft**.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Phase 1 — Collect (Thu thập input)
|
|
47
|
+
|
|
48
|
+
Đọc tất cả nguồn có sẵn, theo thứ tự ưu tiên:
|
|
49
|
+
|
|
50
|
+
1. `plan/<ticketId>/requirement.md` (nếu có) — dùng `Read`.
|
|
51
|
+
2. `plan/<ticketId>/plan.md` (nếu có).
|
|
52
|
+
3. **Test case draft** — phần text sau `---` trong lời gọi.
|
|
53
|
+
4. `custom/rules/test-patterns.md` — các pattern chuẩn (login, CRUD, form, modal...). **Luôn đọc file này** để áp dụng quy ước chung.
|
|
54
|
+
|
|
55
|
+
Kiểm tra điều kiện tiên quyết:
|
|
56
|
+
- Thiếu `baseUrl` → hỏi tester, dừng lại chờ.
|
|
57
|
+
- Không có bất kỳ test case nào (cả draft lẫn requirement) → hỏi:
|
|
58
|
+
> "Bạn muốn test feature gì? Hãy mô tả các test case hoặc paste test case draft vào đây."
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Phase 2 — Analyze (Sinh danh sách test case có cấu trúc)
|
|
63
|
+
|
|
64
|
+
Phân tích tất cả input và lập bảng test case:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
## Test Cases — <Feature Name>
|
|
68
|
+
|
|
69
|
+
| ID | Tên test case | Steps | Expected Result |
|
|
70
|
+
|------|--------------------------|------------------------------------------------|----------------------------|
|
|
71
|
+
| TC01 | Đăng nhập thành công | 1. Vào /login 2. Nhập email+pass hợp lệ 3. Submit | Redirect về /dashboard |
|
|
72
|
+
| TC02 | Sai mật khẩu | 1. Vào /login 2. Nhập sai pass 3. Submit | Hiện error message |
|
|
73
|
+
| TC03 | Để trống email | 1. Vào /login 2. Submit ngay | Hiện "Email là bắt buộc" |
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> ⚠️ **GATE 1 — BẮT BUỘC.** Hiển thị bảng và hỏi:
|
|
77
|
+
> "Danh sách test cases trên đã đúng chưa? Cần thêm/sửa/xóa case nào không?
|
|
78
|
+
> Xác nhận để tôi mở app và lấy selector thật."
|
|
79
|
+
>
|
|
80
|
+
> **Dừng lại, chờ tester xác nhận.** Không tự chuyển Phase 3.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Phase 3 — Explore (Khám phá DOM thật qua Playwright MCP) 🔑
|
|
85
|
+
|
|
86
|
+
Đây là bước quyết định độ chính xác. **Không được đoán selector** — phải lấy từ app thật.
|
|
87
|
+
|
|
88
|
+
Với mỗi trang/màn hình liên quan trong các test case đã chốt:
|
|
89
|
+
|
|
90
|
+
1. `mcp__playwright__browser_navigate` tới `<baseUrl><path>` (vd `http://localhost:3000/login`).
|
|
91
|
+
2. `mcp__playwright__browser_snapshot` để lấy cây accessibility (role, name, ref của từng element).
|
|
92
|
+
3. Với từng element cần thao tác (input, button, message...), dùng
|
|
93
|
+
`mcp__playwright__browser_generate_locator` để lấy **locator Playwright chuẩn** từ element thật.
|
|
94
|
+
4. Nếu một test case cần đi qua nhiều bước (vd phải submit mới hiện error), dùng
|
|
95
|
+
`browser_type` / `browser_fill_form` / `browser_click` để **đi tới đúng trạng thái** rồi
|
|
96
|
+
`browser_snapshot` lại để lấy selector của element xuất hiện sau đó (error message, modal...).
|
|
97
|
+
5. Ghi lại bảng ánh xạ: phần tử logic → locator thật. Ví dụ:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Email field → getByRole('textbox', { name: 'Email' })
|
|
101
|
+
Password field→ getByLabel('Mật khẩu')
|
|
102
|
+
Submit button → getByRole('button', { name: 'Đăng nhập' })
|
|
103
|
+
Error message → getByText('Sai tên đăng nhập hoặc mật khẩu')
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Quy tắc ưu tiên locator (theo Playwright best practice):
|
|
107
|
+
`getByRole` > `getByLabel` > `getByPlaceholder` > `getByText` > `getByTestId` > CSS.
|
|
108
|
+
Chỉ dùng CSS/XPath khi không còn cách nào khác, và ghi chú lý do.
|
|
109
|
+
|
|
110
|
+
> ⚠️ **Nếu không mở được app** (baseUrl sai, app chưa chạy, route không tồn tại):
|
|
111
|
+
> báo rõ cho tester, **không** sinh selector đoán mò. Hỏi tester sửa baseUrl / khởi động app.
|
|
112
|
+
>
|
|
113
|
+
> Đóng browser bằng `mcp__playwright__browser_close` khi khám phá xong.
|
|
114
|
+
|
|
115
|
+
### Nếu app yêu cầu đăng nhập (auth)
|
|
116
|
+
|
|
117
|
+
Khi feature cần test nằm sau màn hình login:
|
|
118
|
+
|
|
119
|
+
1. Khám phá selector trang login bằng MCP (như trên).
|
|
120
|
+
2. Cập nhật `tests/e2e/auth.setup.ts`: thay 3 dòng locator mặc định (Email / Mật khẩu /
|
|
121
|
+
nút Đăng nhập) bằng **selector thật** vừa khám phá, và chỉnh `expectPathAfterLogin`.
|
|
122
|
+
3. Hỏi tester **tài khoản test** (không hardcode tài khoản thật). Hướng dẫn họ điền vào `.env`:
|
|
123
|
+
`E2E_USER`, `E2E_PASSWORD`, `E2E_LOGIN_PATH`, `E2E_POST_LOGIN_PATH` (xem `.env.example`).
|
|
124
|
+
Có `E2E_USER` → auth tự bật: project `setup` đăng nhập 1 lần, lưu `.auth/user.json`,
|
|
125
|
+
các test sau tái dùng phiên (không login lại từng test).
|
|
126
|
+
4. Test mà bản thân nó cần trạng thái **CHƯA đăng nhập** (vd chính test login/logout):
|
|
127
|
+
thêm ở đầu spec `test.use({ storageState: LOGGED_OUT })` (import `LOGGED_OUT` từ
|
|
128
|
+
`../../fixtures/test`).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Phase 4 — Generate (Sinh script với selector thật)
|
|
133
|
+
|
|
134
|
+
Sinh file theo cấu trúc:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
tests/e2e/
|
|
138
|
+
├── pages/
|
|
139
|
+
│ ├── BasePage.ts ← chỉ tạo nếu chưa có (đã có sẵn trong repo)
|
|
140
|
+
│ └── <Feature>Page.ts ← Page Object, locator lấy từ Phase 3
|
|
141
|
+
├── specs/
|
|
142
|
+
│ └── <ticketId>/
|
|
143
|
+
│ └── <feature>.spec.ts ← nhóm nhiều TCs (xem quy tắc bên dưới)
|
|
144
|
+
└── fixtures/test.ts ← chỉ tạo nếu chưa có (đã có sẵn)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Khi được gọi từ `coverage-check` (Phase 2d), output vào `test-plan/test-scripts/`:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
test-plan/test-scripts/
|
|
151
|
+
├── AD10_001.spec.ts ← mỗi TC_ID một file riêng
|
|
152
|
+
├── AD10_002.spec.ts
|
|
153
|
+
└── ...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Quy tắc sinh code:
|
|
157
|
+
|
|
158
|
+
- **`<Feature>Page.ts`**: kế thừa `BasePage`. Khai báo tất cả `Locator` ở constructor,
|
|
159
|
+
**dùng đúng locator lấy được ở Phase 3**. Mỗi hành động là một async method (vd `login()`).
|
|
160
|
+
Tham khảo `templates/PageObject.example.ts` để biết style.
|
|
161
|
+
|
|
162
|
+
- **Đặt tên test function — bắt buộc chứa TC_ID:**
|
|
163
|
+
```typescript
|
|
164
|
+
// ScreenID là mã màn hình/chức năng của từng dự án (AD10, LOGIN, ORD, ... — không cố định)
|
|
165
|
+
test.describe('[ScreenID] — Tên màn hình', () => {
|
|
166
|
+
test('[ScreenID_001] Mô tả scenario đầu tiên', async ({ page }) => {
|
|
167
|
+
// ...
|
|
168
|
+
})
|
|
169
|
+
test('[ScreenID_002] Mô tả scenario thứ hai', async ({ page }) => {
|
|
170
|
+
// ...
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
Ví dụ thực tế (ScreenID tùy dự án): `[AD10_001]`, `[LOGIN_001]`, `[ORD_002]`
|
|
175
|
+
|
|
176
|
+
Format bắt buộc: `'[TC_ID] Mô tả scenario'` — TC_ID phải nằm ở **đầu tên test**, trong dấu `[]`.
|
|
177
|
+
Mục đích: khi chạy `--grep "AD10_001"` (hoặc TC_ID thực tế) sẽ select đúng test case, và kết quả map 1-1 với testcase document.
|
|
178
|
+
|
|
179
|
+
- **`<feature>.spec.ts`**: import `{ test, expect }` từ `../../fixtures/test`.
|
|
180
|
+
dùng `test.describe('[ScreenID]', ...)` để nhóm. Assertion rõ ràng (`expect(...).toBeVisible()`...).
|
|
181
|
+
- **Không tạo lại** `BasePage.ts` / `playwright.config.ts` / `fixtures/test.ts` /
|
|
182
|
+
`auth.setup.ts` / `support/auth.ts` nếu đã tồn tại (kiểm tra bằng `Glob` trước) —
|
|
183
|
+
với `auth.setup.ts` chỉ **sửa locator/đường dẫn**, không ghi đè toàn bộ.
|
|
184
|
+
|
|
185
|
+
### Incremental Update — chỉ generate scripts mới hoặc đã thay đổi
|
|
186
|
+
|
|
187
|
+
Khi được gọi với danh sách TC_IDs (từ `coverage-check` hoặc sau khi TESTER update testcases):
|
|
188
|
+
|
|
189
|
+
1. **Kiểm tra scripts đã tồn tại:** `Glob` tìm các file trong `test-plan/test-scripts/*.spec.ts`
|
|
190
|
+
2. **Phân loại từng TC_ID:**
|
|
191
|
+
- Nếu `[TC_ID].spec.ts` **chưa tồn tại** → `[NEW]` — sinh script mới
|
|
192
|
+
- Nếu `[TC_ID].spec.ts` **đã tồn tại** và TC nằm trong danh sách "đã thay đổi" (do TESTER chỉ định) → `[UPDATED]` — regenerate, overwrite
|
|
193
|
+
- Nếu `[TC_ID].spec.ts` **đã tồn tại** và không trong danh sách thay đổi → `[SKIP]` — không đụng vào
|
|
194
|
+
3. **Chỉ mở DOM/Playwright MCP cho TCs cần generate** (`[NEW]` và `[UPDATED]`)
|
|
195
|
+
4. **Báo cáo kết quả:**
|
|
196
|
+
```
|
|
197
|
+
Script generation complete:
|
|
198
|
+
[NEW] AD10_005.spec.ts — created
|
|
199
|
+
[UPDATED] AD10_002.spec.ts — regenerated (TC content changed)
|
|
200
|
+
[SKIP] AD10_001.spec.ts — unchanged, kept existing
|
|
201
|
+
[SKIP] AD10_003.spec.ts — unchanged, kept existing
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Khi TESTER không chỉ định danh sách thay đổi (chạy lần đầu từ `coverage-check`), generate TẤT CẢ TCs có `Type = Auto` trong `final-testcases.md` mà chưa có script.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Phase 5 — Execute
|
|
209
|
+
|
|
210
|
+
Set `BASE_URL` = `baseUrl` khi chạy để Playwright dùng đúng app.
|
|
211
|
+
|
|
212
|
+
### autoRun = false (default)
|
|
213
|
+
In hướng dẫn chạy thủ công:
|
|
214
|
+
```
|
|
215
|
+
✅ Đã sinh script xong!
|
|
216
|
+
|
|
217
|
+
Chạy test:
|
|
218
|
+
BASE_URL=<baseUrl> npx playwright test tests/e2e/specs/<ticketId>/
|
|
219
|
+
BASE_URL=<baseUrl> npx playwright test --headed # hiện browser
|
|
220
|
+
npx playwright test --ui # mở Playwright UI
|
|
221
|
+
Xem report:
|
|
222
|
+
npx playwright show-report
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### autoRun = true
|
|
226
|
+
1. Chạy bằng `Bash`:
|
|
227
|
+
`BASE_URL=<baseUrl> npx playwright test tests/e2e/specs/<ticketId>/ <--headed nếu headed=true>`
|
|
228
|
+
2. Tóm tắt pass/fail cho tester (dễ đọc, không bắt đọc log thô).
|
|
229
|
+
3. Nếu có test fail: chỉ ra TC nào fail, lý do, và đề xuất sửa.
|
|
230
|
+
In `npx playwright show-report` để mở HTML report.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Nguyên tắc chung
|
|
235
|
+
|
|
236
|
+
- **Gate trước khi sinh code** (Phase 2) là bắt buộc — tránh sinh sai hàng loạt.
|
|
237
|
+
- **Selector phải đến từ DOM thật** (Phase 3) — đây là lý do tồn tại của skill này.
|
|
238
|
+
- Viết test case, tên test, comment bằng **tiếng Việt** cho khớp draft của tester.
|
|
239
|
+
- Nếu app yêu cầu đăng nhập trước, hỏi tester credentials test (đừng hardcode tài khoản thật).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export abstract class BasePage {
|
|
4
|
+
readonly page: Page
|
|
5
|
+
|
|
6
|
+
constructor(page: Page) {
|
|
7
|
+
this.page = page
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async navigate(path: string) {
|
|
11
|
+
await this.page.goto(path)
|
|
12
|
+
await this.waitForLoad()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async waitForLoad() {
|
|
16
|
+
await this.page.waitForLoadState('networkidle')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async takeScreenshot(name: string) {
|
|
20
|
+
await this.page.screenshot({
|
|
21
|
+
path: `evidence/${name}.png`,
|
|
22
|
+
fullPage: true,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getPageTitle(): Promise<string> {
|
|
27
|
+
return this.page.title()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// TEMPLATE THAM CHIẾU — không phải file chạy thật.
|
|
2
|
+
// Skill dùng style này để sinh tests/e2e/pages/<Feature>Page.ts.
|
|
3
|
+
// CÁC LOCATOR DƯỚI ĐÂY LÀ VÍ DỤ — khi sinh thật phải thay bằng locator
|
|
4
|
+
// lấy từ Phase 3 (Playwright MCP browser_snapshot / browser_generate_locator).
|
|
5
|
+
|
|
6
|
+
import { Page, Locator } from '@playwright/test';
|
|
7
|
+
import { BasePage } from './BasePage';
|
|
8
|
+
|
|
9
|
+
export class LoginPage extends BasePage {
|
|
10
|
+
readonly emailInput: Locator;
|
|
11
|
+
readonly passwordInput: Locator;
|
|
12
|
+
readonly submitButton: Locator;
|
|
13
|
+
readonly errorMessage: Locator;
|
|
14
|
+
|
|
15
|
+
constructor(page: Page) {
|
|
16
|
+
super(page);
|
|
17
|
+
// ↓ Locator THẬT lấy từ DOM ở Phase 3, ưu tiên getByRole > getByLabel > ...
|
|
18
|
+
this.emailInput = page.getByRole('textbox', { name: 'Email' });
|
|
19
|
+
this.passwordInput = page.getByLabel('Mật khẩu');
|
|
20
|
+
this.submitButton = page.getByRole('button', { name: 'Đăng nhập' });
|
|
21
|
+
this.errorMessage = page.getByText('Sai tên đăng nhập hoặc mật khẩu');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async login(email: string, password: string): Promise<void> {
|
|
25
|
+
await this.emailInput.fill(email);
|
|
26
|
+
await this.passwordInput.fill(password);
|
|
27
|
+
await this.submitButton.click();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: './tests/e2e/specs',
|
|
5
|
+
fullyParallel: false,
|
|
6
|
+
forbidOnly: !!process.env.CI,
|
|
7
|
+
retries: process.env.CI ? 2 : 0,
|
|
8
|
+
workers: 1,
|
|
9
|
+
reporter: [
|
|
10
|
+
['html', { outputFolder: 'playwright-report' }],
|
|
11
|
+
['json', { outputFile: 'test-results/results.json' }],
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
use: {
|
|
15
|
+
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
16
|
+
trace: 'on-first-retry',
|
|
17
|
+
screenshot: 'only-on-failure',
|
|
18
|
+
video: 'on-first-retry',
|
|
19
|
+
headless: !process.env.HEADED,
|
|
20
|
+
locale: 'vi-VN',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
projects: [
|
|
24
|
+
// Auth setup (chỉ chạy khi có E2E_USER trong .env)
|
|
25
|
+
...(process.env.E2E_USER
|
|
26
|
+
? [{ name: 'setup', testMatch: /auth\.setup\.ts/ }]
|
|
27
|
+
: []),
|
|
28
|
+
{
|
|
29
|
+
name: 'chromium',
|
|
30
|
+
use: {
|
|
31
|
+
...devices['Desktop Chrome'],
|
|
32
|
+
...(process.env.E2E_USER ? { storageState: '.auth/user.json' } : {}),
|
|
33
|
+
},
|
|
34
|
+
dependencies: process.env.E2E_USER ? ['setup'] : [],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
outputDir: 'test-results',
|
|
39
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// TEMPLATE THAM CHIẾU — không phải file chạy thật.
|
|
2
|
+
// Skill dùng style này để sinh tests/e2e/specs/<ticketId>/<feature>.spec.ts.
|
|
3
|
+
// ID test (TC01...) phải khớp bảng test case đã chốt ở Phase 2.
|
|
4
|
+
|
|
5
|
+
import { test, expect } from '../../fixtures/test';
|
|
6
|
+
import { LoginPage } from '../../pages/LoginPage';
|
|
7
|
+
|
|
8
|
+
test.describe('Đăng nhập hệ thống', () => {
|
|
9
|
+
test('TC01 - Đăng nhập thành công với credentials hợp lệ', async ({ page }) => {
|
|
10
|
+
const loginPage = new LoginPage(page);
|
|
11
|
+
await loginPage.navigate('/login');
|
|
12
|
+
await loginPage.login('admin@example.com', 'Password123');
|
|
13
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('TC02 - Hiển thị lỗi khi nhập sai mật khẩu', async ({ page }) => {
|
|
17
|
+
const loginPage = new LoginPage(page);
|
|
18
|
+
await loginPage.navigate('/login');
|
|
19
|
+
await loginPage.login('admin@example.com', 'wrong-password');
|
|
20
|
+
await expect(loginPage.errorMessage).toBeVisible();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('TC03 - Hiển thị validation khi để trống email', async ({ page }) => {
|
|
24
|
+
const loginPage = new LoginPage(page);
|
|
25
|
+
await loginPage.navigate('/login');
|
|
26
|
+
await loginPage.submitButton.click();
|
|
27
|
+
await expect(page.getByText('Email là bắt buộc')).toBeVisible();
|
|
28
|
+
});
|
|
29
|
+
});
|