# 🎭 E2E cơ bản với Playwright

By Better Wise

Smoke test bạn dựng ở module trước chỉ trả lời được một câu: "app còn sống không?". Nó không trả lời được câu quan trọng hơn: "user còn đăng nhập và mua được hàng không?". Đó là việc của E2E, và Playwright là công cụ giúp bạn làm điều đó một cách lặp lại được. Với tư duy release engineer: E2E là thứ đứng giữa bạn và nút deploy, nên nó phải đáng tin.

Sau bài này bạn sẽ:

- Phân biệt được smoke test và E2E, biết flow nào đáng viết E2E.
- Dựng được Playwright chạy trên staging với `baseURL` lấy từ biến môi trường.
- Viết được một test cho flow quan trọng nhất, dùng locator bền.
- Biết đọc trace/report khi test đỏ và xử lý test flaky.

## Smoke test và E2E khác nhau ở đâu

Smoke test thường là gọi vài endpoint hoặc mở trang chủ, kiểm tra status code và một dòng text. E2E thì mở browser thật, bấm nút thật, đi qua toàn bộ luồng: đăng nhập, thêm vào giỏ, thanh toán, thấy thông báo thành công.

E2E chậm và đắt hơn nhiều, nên nguyên tắc là: chỉ E2E những flow mà nếu hỏng thì bạn phải rollback ngay. Với một app vibe-code điển hình, đó thường là 1 đến 3 flow: đăng nhập, tạo bản ghi chính (đơn hàng, bài viết, booking), và thanh toán nếu có. Mọi thứ còn lại để unit test hoặc smoke test lo.

Nếu flow đó hỏng trên staging, bạn dừng. Không có chuyện "chạy lại cho xanh rồi deploy".

**Bài tập 1.** App của bạn có 6 tính năng: đăng nhập, đổi mật khẩu, tạo đơn hàng, lọc sản phẩm, xem trang giới thiệu, xuất hóa đơn PDF. Chọn tối đa 2 flow để viết E2E và giải thích lý do.

**Xem gợi ý**

Chọn **đăng nhập** và **tạo đơn hàng**. Đăng nhập là cửa vào của mọi thứ, hỏng là mất toàn bộ người dùng. Tạo đơn hàng là flow tạo ra tiền, hỏng là hỏng nghiệp vụ. Đổi mật khẩu và xuất PDF có thể kiểm bằng test tích hợp; lọc sản phẩm và trang giới thiệu chỉ cần smoke test.

## Dựng Playwright và trỏ nó vào staging

1. Khởi tạo project test: `npm init -y`
2. Cài Playwright: `npm i -D @playwright/test`
3. Tải browser: `npx playwright install`
4. Tạo file `playwright.config.ts` ở gốc repo
5. Chạy thử: `npx playwright test`

Cấu hình tối thiểu, chú ý `baseURL`:

```ts
import { defineConfig } from '@playwright/test';

const baseURL = process.env.E2E_BASE_URL;
if (!baseURL) throw new Error('Thiếu E2E_BASE_URL, không chạy E2E mà không biết trỏ vào đâu');

export default defineConfig({
  testDir: './e2e',
  retries: process.env.CI ? 1 : 0,
  use: {
    baseURL,
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});
```

> **Important:** 
> Không bao giờ để `baseURL` mặc định trỏ vào production. Nếu test của bạn tạo đơn hàng thật và gửi email thật cho khách thật, bạn không có E2E, bạn có một con bot phá production theo lịch.

Biến `E2E_BASE_URL`, `E2E_USER`, `E2E_PASSWORD` đặt trong `.env` local và trong secrets của CI, đúng như cách bạn đã quản lý secrets ở module 1. Không hardcode trong file test.

**Bài tập 2.** Đồng nghiệp bảo "cứ để mặc định `http://localhost:3000` cho nhanh". Vì sao cách đó phá vỡ mục tiêu của E2E?

**Xem gợi ý**

Vì test sẽ chỉ chạy được khi có dev server trên máy, và trong CI nó hoặc fail hoặc (tệ hơn) trỏ nhầm môi trường. E2E chỉ có giá trị khi nó chạy trên **staging**, cùng cấu hình với production. `baseURL` phải đến từ biến môi trường và phải fail-fast khi thiếu.

## Viết test cho flow quan trọng nhất

Ví dụ flow đăng nhập rồi tạo đơn hàng:

```ts
import { test, expect } from '@playwright/test';

test('user đăng nhập và tạo được đơn hàng', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.E2E_USER!);
  await page.getByLabel('Mật khẩu').fill(process.env.E2E_PASSWORD!);
  await page.getByRole('button', { name: 'Đăng nhập' }).click();

  await expect(page.getByRole('heading', { name: 'Bảng điều khiển' })).toBeVisible();

  await page.getByRole('link', { name: 'Sản phẩm' }).click();
  await page.getByRole('button', { name: 'Thêm vào giỏ' }).first().click();
  await page.getByRole('link', { name: 'Giỏ hàng' }).click();
  await page.getByRole('button', { name: 'Thanh toán' }).click();

  await expect(page.getByText('Đơn hàng đã được tạo')).toBeVisible();
});
```

Ba điều đáng chú ý:

- Locator ưu tiên `getByRole` và `getByLabel` vì chúng bám vào thứ user nhìn thấy. CSS class đổi là test đỏ oan.
- Playwright tự chờ phần tử xuất hiện và actionable, nên không cần _____.
- `expect(...).toBeVisible()` là web-first assertion, nó tự retry đến khi hết timeout. Đây là lý do test không cần `sleep`.

Trước khi deploy, lệnh bạn chạy là _____ trên staging, và khi có test đỏ bạn mở report bằng _____ để xem trace, screenshot, console log.

**Bài tập 3.** Test trên còn ba điểm yếu. Tìm ít nhất hai.

**Xem gợi ý**

1. `.first()` trên nút "Thêm vào giỏ" có thể trỏ nhầm sản phẩm, nên assert tên sản phẩm trước khi bấm.
2. Không kiểm tra giỏ hàng có đúng món trước khi thanh toán.
3. Mỗi lần chạy tạo thêm một đơn hàng thật trên staging, dữ liệu sẽ phình ra. Cần user test riêng, dữ liệu riêng, và bước dọn dẹp sau khi chạy.
4. Thiếu assert ở bước trung gian khiến khi đỏ bạn không biết hỏng ở đâu.

## Chạy E2E ở đâu trong pipeline

```mermaid
flowchart TD
    A["Code xong"] --> B["Deploy staging"]
    B --> C["Smoke test"]
    C --> D["🧪 E2E Playwright"]
    D -->|"Đỏ"| E["⛔ Dừng, sửa hoặc rollback"]
    D -->|"Xanh"| F["🚀 Deploy production"]
```

E2E chạy **sau** smoke test và **trước** bước promote lên production. Thứ tự này quan trọng: smoke test rẻ, chạy trước để loại lỗi "app chết"; E2E đắt, chạy sau để kiểm tra nghiệp vụ.

## Khi test đỏ rồi xanh sau khi chạy lại

Đó là _____, và nó là lỗi, không phải may mắn. Một test lúc đỏ lúc xanh nghĩa là gate của bạn không còn giá trị: lần sau nó đỏ, bạn sẽ bấm chạy lại theo phản xạ thay vì điều tra.

Nguyên nhân thường gặp: `waitForTimeout` thay vì assertion, locator bám vào CSS class, nhiều test dùng chung một tài khoản, hoặc test phụ thuộc thứ tự chạy. Cách chữa: mỗi test tự tạo dữ liệu của mình, không share state, và bật _____ để xem chính xác bước nào chậm.

**Bài tập 4.** Test "tạo đơn hàng" đỏ 1/5 lần chạy, luôn ở bước thanh toán. Bạn làm gì trước khi báo cáo cho team?

**Xem gợi ý**

Mở trace của lần đỏ, xem network request thanh toán và console log. Kiểm tra xem test có đang dùng chung tồn kho hoặc chung user với test khác không. Chỉ khi biết nguyên nhân mới sửa; không được tăng `retries` để "cho xanh".

## Những chỗ release engineer hay trượt

- **Trỏ E2E vào production.** Lỗi tốn tiền nhất và cũng phổ biến nhất. Chặn bằng `throw` khi thiếu biến môi trường.
- **Viết 15 test E2E.** Bộ test chậm sẽ bị bỏ qua. Giữ 1 đến 3 flow quan trọng nhất.
- **Hardcode tài khoản test trong repo.** Đó là secret, đưa vào `.env` và CI secrets.
- **Không có artifact.** Test đỏ mà không có trace hay screenshot thì bạn phải đoán. Bật `trace: 'on-first-retry'` từ đầu.
- **Chữa flaky bằng `retries`.** Retry để có artifact, không phải để che lỗi.
- **Bỏ qua bước assert trung gian.** Test đỏ ở dòng cuối cùng mà không biết hỏng ở đâu là test vô dụng.

## Ghi nhớ để mang đi

E2E là gate nghiệp vụ: 1 đến 3 flow, chạy trên staging, `baseURL` từ biến môi trường, locator theo role, assertion tự retry, và trace khi đỏ. Test đỏ thì không deploy, và test flaky thì phải sửa chứ không phải chạy lại.

Bài tiếp theo: khi E2E phát hiện tính năng mới hỏng mà bạn vẫn cần deploy phần còn lại, bạn sẽ dùng feature flag để tắt đúng tính năng đó thay vì rollback cả bản phát hành.

Front: E2E test khác smoke test ở điểm nào?
Back: E2E đi hết một luồng nghiệp vụ trong browser thật; smoke test chỉ kiểm tra app còn sống.

Front: Nên viết E2E cho bao nhiêu flow?
Back: Từ 1 đến 3 flow quan trọng nhất, hỏng là phải rollback.

Front: Lệnh cài browser cho Playwright?
Back: npx playwright install

Front: baseURL của E2E nên lấy từ đâu?
Back: Từ biến môi trường (ví dụ E2E_BASE_URL), và fail-fast nếu thiếu.

Front: E2E chạy ở môi trường nào trước khi deploy production?
Back: Staging, sau smoke test và trước bước promote.

Front: Loại locator nào nên ưu tiên trong Playwright?
Back: getByRole và getByLabel, vì bám vào thứ user nhìn thấy.

Front: Vì sao không dùng waitForTimeout trong test?
Back: Playwright đã tự chờ phần tử actionable; sleep tạo test chậm và flaky.

Front: Web-first assertion trong Playwright là gì?
Back: expect(...).toBeVisible() và tương tự, tự retry đến khi hết timeout.

Front: Option nào giúp giữ trace khi test fail?
Back: trace: 'on-first-retry' trong playwright.config.ts

Front: Flaky test là gì?
Back: Test lúc đỏ lúc xanh trên cùng một code, làm gate mất giá trị.

Front: Lệnh chạy toàn bộ test Playwright?
Back: npx playwright test

Front: Lệnh mở HTML report sau khi test chạy?
Back: npx playwright show-report

Front: Vì sao mỗi test phải tự tạo dữ liệu riêng?
Back: Tránh phụ thuộc thứ tự chạy và tránh dữ liệu rác tích tụ trên staging.

Front: Điều gì phải làm khi E2E đỏ trên staging?
Back: Dừng deploy, tìm nguyên nhân từ trace; không chạy lại cho xanh.

---

## Contents

Part of [🚀 Ra mắt an toàn cho app vibe-code: staging, smoke test và rollback](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback.md)

- **🔧 Môi trường và cấu hình an toàn**
  - [🏗️ Tách môi trường dev, staging, production](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/tach-moi-truong-dev-staging-production.md)
  - [🔐 Quản lý biến môi trường và secrets](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/quan-ly-bien-moi-truong-va-secrets.md) (sign-in required)
  - [🔥 Smoke test sau mỗi lần deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/smoke-test-sau-moi-lan-deploy.md)
  - [✅ Checkpoint: Môi trường, secrets và smoke test](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-moi-truong-secrets-smoke-test.md)
- **🧪 Kiểm thử và kiểm soát phát hành**
  - [🎭 E2E cơ bản với Playwright](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/e2e-co-ban-voi-playwright.md) — this lesson
  - [🚩 Feature flag để tắt tính năng lỗi](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/feature-flag-de-tat-tinh-nang-loi.md)
  - [💾 Sao lưu và rollback trước khi deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/sao-luu-va-rollback-truoc-deploy.md)
  - [📝 Checkpoint: E2E, feature flag và rollback](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-e2e-feature-flag-rollback.md)
- **🚦 Go-live và runbook phát hành**
  - [📋 Checklist go-live ngắn gọn](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checklist-go-live-ngan-gon.md)
  - [🧯 Runbook phát hành và xử lý sự cố sau deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/runbook-phat-hanh-va-xu-ly-su-co-sau-deploy.md) (premium content)
  - [🏁 Checkpoint: Go-live và runbook](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-go-live-va-runbook.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/cheatsheet.md) (premium content)
