---
name: script-sync
description: Sync test cases với Playwright scripts theo cơ chế hash. Với mỗi TC_ID trong TC file, kiểm tra script hiện tại bằng @tc-hash comment — gen mới nếu chưa có, update nếu TC thay đổi, skip nếu không đổi. Dùng Playwright MCP để lấy selector thật khi gen/update.
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_close
---

# Skill: script-sync

Sync test cases (từ TC file `.md`) với Playwright spec files, sử dụng **hash-based change detection**
để chỉ gen/update những TC thực sự thay đổi — tránh overwrite script đang chạy tốt.

---

## Input

Được gọi từ `execute-flow` skill (Gate 2) với context:

- `tcFile`: path đến TC file markdown (vd `testcases/AD10.md`)
- `repo`: tên repo (vd `repo-fe`)
- `screenId`: mã màn hình, always lowercase (vd `ad10`, not `AD10`) — directory paths use lowercase; spec file name `{ScreenID}.spec.ts` remains uppercase
- `featureDir`: `{ScreenID}_{Screen-Name-kebab-case}` (vd `AD10_create-product`)
- `baseUrl`: URL của app để Playwright MCP snapshot
- `scriptsRoot`: `AK-Docs/03.Testing/05.Scripts/{repo}/` — script + Page Object sinh ra nằm ở đây, `Shared/evidence-helper.ts` nằm ở `AK-Docs/03.Testing/05.Scripts/Shared/`

---

## Hash Computation

**Công thức:** `SHA1(TC_ID + "|" + Steps + "|" + Expected Result)` → lấy 8 ký tự đầu.

Các trường lấy từ TC file:
- `TC_ID`: cột "Test Case ID"
- `Steps`: cột "Steps" (giữ nguyên text, bao gồm số thứ tự)
- `Expected Result`: cột "Expected Result"

Không tính vào hash: `Severity`, `Pre-condition`, `Data Test`, `Ticket ID`, `Comment`, `R1/R2 columns`.

**Mục đích:** Hash chỉ thay đổi khi steps hoặc expected result thay đổi — sửa comment/severity không trigger update.

---

## Spec File Structure

Mỗi TC trong spec file có metadata comment ngay trước `test()`. File nằm ở `AK-Docs/03.Testing/05.Scripts/{repo}/{featureDir}/{ScreenID}.spec.ts`:

```typescript
// @screen-id: AD10
// @screen-name: Create Product
// @repo: repo-fe
// @screen-url: /admin/products/create
// @generated: YYYY-MM-DD

import { test, expect } from '../../Shared/fixtures/test'
import { captureStepEvidence } from '../../Shared/evidence-helper'
import { AD10Page } from '../pages/AD10Page'

test.describe('AD10 - Create Product', () => {

  // @tc-hash: a3f9b2c1
  // @tc-id: AD10_001
  test('AD10_001 - Truy cập trực tiếp qua URL khi chưa đăng nhập', async ({ page }, testInfo) => {
    const pageObj = new AD10Page(page)
    // ...
    // xem "Assertion Rules" + "Evidence Capture per Step" bên dưới — mỗi step UI trong TC
    // phải gọi captureStepEvidence(page, testInfo, N, 'desc', {...}) và assertion phải khẳng
    // định đúng nội dung Expected Result, không chỉ kiểm tra element tồn tại.
  })

  // @tc-hash: d7e2f4a9
  // @tc-id: AD10_002
  test('AD10_002 - Truy cập trực tiếp qua URL khi đã đăng nhập', async ({ page }, testInfo) => {
    // ...
  })

})
```

---

## Sync Algorithm

### Bước 1: Parse TC file

Đọc TC file, extract từ Section 3 (Danh sách Test Cases Chi Tiết):
- TC_ID, Test Case Name, Steps, Expected Result, Severity

Nhận diện **Manual TCs** — các TC không thể automation:
- Steps đề cập "kiểm tra DB", "kiểm tra email", "kiểm tra file system"
- Steps quá mơ hồ: "Quan sát giao diện", "Kiểm tra layout, màu sắc"
- Steps yêu cầu hành động ngoài browser: restart server, check log file

### Bước 2: Scan spec file hiện tại

Tìm `AK-Docs/03.Testing/05.Scripts/{repo}/{featureDir}/{ScreenID}.spec.ts` (`scriptsRoot` từ input + `{featureDir}/{ScreenID}.spec.ts`).

Nếu tồn tại: đọc file, extract tất cả cặp `@tc-hash` + `@tc-id` bằng regex:
```
// @tc-hash: ([a-f0-9]{8})
// @tc-id: ([A-Za-z0-9_]+)
```

Tạo map: `{ [TC_ID]: hash }` từ spec file hiện tại.

### Bước 3: Classify từng TC

Với mỗi TC trong TC file:

| Điều kiện | Status | Hành động |
|-----------|--------|-----------|
| TC là Manual TC | `manual` | Bỏ qua, đánh dấu Untest |
| Không có trong spec file | `new` | Gen script mới |
| Có trong spec file, hash khớp | `skipped` | Không làm gì |
| Có trong spec file, hash khác | `updated` | Update test block |

### Bước 4: Gen script cho TCs có status `new`

> **Cache theo (screen, state), không theo TC** (xem `docs/internal/Token Problems.md` §2.4/§3.4, Vấn đề D). Trước đây mỗi TC gọi lại trọn bộ `navigate` + `snapshot` + `generate_locator` từ đầu, dù nhiều TC cùng màn hình chia sẻ chung state ban đầu. Thuật toán dưới đây **vẫn giữ `browser_snapshot` cho mỗi TC** (bước rẻ, dùng để verify DOM chưa trôi — không được bỏ, xem "Assertion Rules") — chỉ cache phần đắt (`browser_generate_locator`) khi element đã có sẵn locator đáng tin từ Page Object.

Với TC **đầu tiên** cần gen mới của 1 màn hình (`{ScreenID}`) trong lần chạy Gate 2 này:

1. Tính hash mới: `SHA1(TC_ID + "|" + Steps + "|" + Expected Result)[:8]`
2. `mcp__playwright__browser_navigate` → `baseUrl + screenUrl`, rồi `mcp__playwright__browser_snapshot` → lấy accessibility tree của state ban đầu.
3. Với **mọi** element tương tác nhìn thấy trên snapshot này (không chỉ element của TC đang xử lý) chạy `mcp__playwright__browser_generate_locator`, lưu hết vào `{ScreenID}Page.ts` (Page Object) — dựng Page Object "đủ dùng cho cả màn hình" ngay từ TC đầu tiên, không dựng dần từng phần.
   - Nếu locator match **nhiều hơn 1 element** trên snapshot → không dùng locator đó, tìm locator scope hẹp hơn (trong đúng container/row/modal) hoặc dùng `data-testid` cụ thể. Xem "Assertion Rules" — locator mơ hồ là nguyên nhân chính gây false pass/fail.

Với **các TC tiếp theo** cùng màn hình trong cùng lần chạy Gate 2:

1. Tính hash mới như trên.
2. Vẫn chạy `mcp__playwright__browser_snapshot` cho state mà TC này bắt đầu từ đó — **không bỏ bước này**, đây là verify bắt buộc, không phải chỗ cần tiết kiệm.
3. Với từng element trong Steps của TC: **tra `{ScreenID}Page.ts` trước.**
   - Element đã có locator trong Page Object **và** snapshot vừa lấy xác nhận element đó vẫn khớp đúng 1 node duy nhất, không đổi → **dùng lại locator có sẵn, không gọi `browser_generate_locator` lại.**
   - Element chưa có trong Page Object (Step dẫn tới UI state mới — mở modal, chuyển trang, dropdown mới), hoặc snapshot cho thấy element đã đổi/không còn khớp đúng 1 node như Page Object đang lưu → gọi `browser_generate_locator` như bình thường, cập nhật lại Page Object.
4. Sinh test block với metadata comments — **mỗi Step trong TC file map 1:1 với 1 action + 1 evidence capture**, assertion cuối phải khẳng định đúng nội dung Expected Result:

```typescript
  // @tc-hash: {hash}
  // @tc-id: {TC_ID}
  test('{TC_ID} - {Test Case Name}', async ({ page }, testInfo) => {
    const pageObj = new {ScreenID}Page(page)

    // Step 1: {step text — vd "Trên màn hình danh sách, click item A để mở A1"}
    await pageObj.{action}()
    await captureStepEvidence(page, testInfo, 1, '{step-1-desc}', { highlightSelector: pageObj.itemALocator })

    // Step 2: {step text — vd "Trên A1, click button B để mở modal B1"}
    await pageObj.{action}()
    await captureStepEvidence(page, testInfo, 2, '{step-2-desc}', { highlightSelector: pageObj.buttonBLocator })

    // Step 3 (nếu step mở popup/modal): chờ modal hiện + settle rồi mới chụp + assert
    await expect(pageObj.modalB1Locator).toBeVisible()
    await captureStepEvidence(page, testInfo, 3, '{step-3-desc}', {
      highlightSelector: pageObj.confirmFieldLocator,   // đúng vùng cần confirm theo Expected Result
      scrollSelector: pageObj.confirmFieldLocator,       // nếu modal có scroll
    })

    // Expected: {expected result — assertion PHẢI check đúng nội dung, không chỉ tồn tại}
    await expect(pageObj.confirmFieldLocator).toHaveText('{expected text từ TC file}')
  })
```

> Nếu TC chỉ có 1 step / không mở modal mới, vẫn gọi `captureStepEvidence` ít nhất 1 lần ở trạng thái cuối (sau khi `waitForUiSettled`) — không bỏ qua evidence.

**Sau khi sinh test block (áp dụng cho cả TC đầu tiên lẫn TC tiếp theo):**

1. Nếu spec file chưa tồn tại: tạo file mới với file header + describe wrapper + test block.
2. Nếu spec file đã tồn tại: append test block vào trước closing `})` của `test.describe`.
3. Page Object (`{ScreenID}Page.ts`, `class {ScreenID}Page extends BasePage`, import từ `../../Shared/BasePage`): với TC đầu tiên của màn hình, đã dựng đủ locator ở bước 3 phía trên — **đặt tên locator theo đúng item được mô tả trong Step** (vd `itemALocator`, `buttonBLocator`) để dùng lại cho `highlightSelector` ở bước capture evidence. Với TC tiếp theo, chỉ **thêm** locator mới vào Page Object đã có khi Step dẫn tới element/state chưa từng lưu — không tạo lại Page Object từ đầu.

**File header khi tạo spec mới:**
```typescript
// @screen-id: {ScreenID}
// @screen-name: {Screen Name}
// @repo: {repo}
// @screen-url: {url path}
// @generated: {YYYY-MM-DD}

import { test, expect } from '../../Shared/fixtures/test'
import { captureStepEvidence } from '../../Shared/evidence-helper'
import { {ScreenID}Page } from '../pages/{ScreenID}Page'

test.describe('{ScreenID} - {Screen Name}', () => {
```

### Bước 5: Update test block cho TCs có status `updated`

1. Tính hash mới.
2. Vẫn `mcp__playwright__browser_snapshot` (bắt buộc, không cache) cho state liên quan — TC `updated` nghĩa là Steps/Expected Result đã đổi, nên không thể tin locator cũ mà không verify lại. Với element chưa đổi so với Page Object hiện có, áp dụng đúng quy tắc tra-trước-khi-generate ở Bước 4 (TC tiếp theo); chỉ gọi `browser_generate_locator` cho element thật sự mới hoặc đã đổi.
3. Tìm block cũ trong spec file bằng pattern:
   ```
   // @tc-hash: {old-hash}\n  // @tc-id: {TC_ID}\n  test(...)
   ```
4. Replace toàn bộ block cũ (từ `// @tc-hash` đến closing `})` của test) bằng block mới

### Bước 6: Xử lý TCs không gen được

| Lý do | Marker |
|-------|--------|
| TC manual (DB/email/file check) | `[MANUAL]` — Untest |
| Steps mơ hồ, không map được selector | `[NEEDS_CLARIFY]` — hỏi tester |
| Playwright MCP không snapshot được (iframe, native dialog) | `[BLOCKED: {lý do}]` |
| Race condition (double-click, network timing) | `[BLOCKED: timing-dependent]` |

Với `[NEEDS_CLARIFY]`: hỏi tester rõ element nào cần tương tác, sau đó gen.

### Bước 7: Đóng browser

```
mcp__playwright__browser_close
```

---

## Output Summary

```
📊 SCRIPT SYNC SUMMARY — {ScreenID}

✅ Gen mới:  {N} TCs → {ScreenID}.spec.ts
🔄 Updated:  {N} TCs → hash refreshed
⏭️ Skipped:  {N} TCs → không đổi
⚠️ Manual:   {N} TCs → {TC_IDs} (lý do)
❌ Blocked:  {N} TCs → {TC_IDs} (lý do)

→ Script: AK-Docs/03.Testing/05.Scripts/{repo}/{featureDir}/{ScreenID}.spec.ts
→ Page Object: AK-Docs/03.Testing/05.Scripts/{repo}/pages/{ScreenID}Page.ts
```

Trả về danh sách kết quả cho `execute-flow`:
- Danh sách TC_IDs có thể chạy automation (gen mới + updated + skipped)
- Danh sách TC_IDs manual (Untest)
- Danh sách TC_IDs blocked (Pending)

---

## Assertion Rules (bắt buộc — tránh false pass/fail)

- ❌ **Không** dùng locator match nhiều hơn 1 element làm căn cứ assertion — verify qua `browser_snapshot` rằng locator chỉ trúng đúng 1 node trước khi dùng. Nếu snapshot cho thấy nhiều node giống nhau (list item, nhiều modal ẩn/hiện cùng lúc) → scope locator trong đúng container (`getByRole('dialog').getByText(...)`, `row.getByTestId(...)`) hoặc thêm `.filter({ hasText: ... })`.
- ❌ **Không** dùng `toBeVisible()`/`toBeAttached()` làm assertion duy nhất khi Expected Result mô tả **nội dung cụ thể** (text, giá trị, số lượng, trạng thái field) — phải dùng `toHaveText`/`toContainText`/`toHaveValue`/`toBeDisabled`/`toHaveCount` tương ứng.
- ❌ **Không** wrap action trong `try/catch` rồi để pass nếu catch nuốt lỗi mà không có assertion thay thế khẳng định behavior đúng.
- ✅ Với negative/validation TC (kỳ vọng lỗi, kỳ vọng bị chặn) → assertion phải khẳng định đúng **hành vi chặn** đó xảy ra (message lỗi đúng text, hoặc action bị disable, hoặc điều hướng không xảy ra) — không chỉ assert "trang không crash".
- ✅ Mỗi step có mô tả thay đổi UI trong TC Steps → phải có 1 lệnh gọi `captureStepEvidence(...)` tương ứng, đặt **ngay sau** `waitForUiSettled` implicit trong helper — xem "Evidence Capture per Step" dưới.

## Evidence Capture per Step

- Import `captureStepEvidence`, `waitForUiSettled`, `highlightElement` từ `Shared/evidence-helper.ts` (không viết lại logic này trong từng spec).
- Nếu Step yêu cầu click để mở popup/modal → thực hiện click **trước**, chờ modal `toBeVisible()`, rồi mới `captureStepEvidence` cho step đó — không chụp step trước khi hành động mở modal xảy ra.
- Nếu vùng cần confirm nằm ngoài viewport (modal dài, table nhiều dòng) → truyền `scrollSelector` để helper tự `scrollIntoViewIfNeeded()` trước khi chụp.
- Truyền `highlightSelector` = đúng locator của item/nội dung mà Expected Result yêu cầu tester xác nhận — helper tự khoanh viền đỏ rồi remove sau khi chụp.
- **Nếu chụp trong lúc 1 dropdown/popper đang mở** (Element UI/Ant Design/MUI Select, Tooltip, Popover, ...) → **luôn truyền `fullPage: false`**, không dùng mặc định `true`. Popper của các UI kit này teleport ra `document.body`, định vị bằng `position: absolute`/`fixed` theo viewport lúc mở — `fullPage: true` khiến Chromium giãn viewport lên full chiều cao trang để chụp, làm popper lệch vị trí/ảnh xuất hiện nội dung lặp lại (xem execute-flow SKILL.md "Evidence Quality Rules" mục 5 để biết chi tiết cơ chế). Áp dụng cho mọi step chụp trong lúc dropdown còn mở, kể cả khi không highlight trực tiếp vào popper đó.

## Mandatory Rules

- ❌ **Không bao giờ bịa selector** — phải dùng Playwright MCP snapshot
- ❌ **Không overwrite** test block có hash match — skip hoàn toàn
- ✅ **MUST** gọi `browser_snapshot` cho **mỗi TC** (kể cả khi tái sử dụng locator có sẵn từ Page Object) — đây là bước verify DOM chưa trôi, không phải chỗ để cache/bỏ qua. Chỉ được cache/bỏ qua bước `browser_generate_locator` khi Page Object đã có locator đáng tin **và** snapshot mới xác nhận element đó vẫn khớp đúng 1 node duy nhất (xem Bước 4/5, Vấn đề D ở `docs/internal/Token Problems.md` §3.4/§4.2) — không được suy diễn "chắc vẫn giống" mà bỏ qua verify.
- ✅ Ưu tiên locator: `getByRole` > `getByLabel` > `getByPlaceholder` > `getByText` > `getByTestId` > CSS
- ✅ Không tạo lại `BasePage.ts` / `fixtures/test.ts` / `evidence-helper.config.ts` nếu đã tồn tại — nhưng `evidence-helper.ts` thì luôn đồng bộ lại theo template mới nhất (file này generic, không chứa tuỳ biến riêng của app; tuỳ biến riêng sống trong `evidence-helper.config.ts`)
- ✅ Test names/comments theo ngôn ngữ của TC input (xem `custom/rules/output-language.md`) — TC tiếng Việt → tiếng Việt, TC tiếng Anh → tiếng Anh
