NỘI DUNG BÀI HỌC

📂 Tối ưu hóa "Sân chơi" với testDir & testMatch: Hiểu rõ cơ chế quét file 3 lớp để tăng tốc độ thực thi, cô lập môi trường test và làm chủ cú pháp Glob Pattern chuyên sâu.

🏗️ Chiến lược Phân tầng Project (Inheritance & Overriding): Kỹ thuật sử dụng cơ chế "Bố mẹ - Con cái" để ghi đè cấu hình, giúp tạo ra ma trận kiểm thử đa trình duyệt và thiết bị chỉ với một file config gọn nhẹ.

🛡️ Phá bỏ "Hộp đen" Global Setup: Tại sao nên chuyển từ globalSetup cổ điển sang Project Dependencies để sở hữu báo cáo full HD (Trace Viewer, Video, Screenshot) ngay cả khi khởi tạo lỗi.

⚙️ Làm chủ Running Sequence & State Management: Nắm vững quy luật "Setup chặn cửa - Test chạy đua - Teardown bám đuôi" để điều phối luồng dữ liệu và lưu trữ trạng thái đăng nhập (Storage State) chuẩn xác.



🕵️ Phần 1: Têu điểm: Cơ chế hoạt động của testDir

Hãy tưởng tượng Project của bạn là một Siêu thị khổng lồ.

  • testDir: Là biển báo "Khu vực thực phẩm tươi sống".
  • Playwright: Là nhân viên đi kiểm hàng.

Nếu không có biển báo (testDir), nhân viên sẽ phải chạy đi tìm thịt cá ở cả quầy quần áo, quầy điện tử, và nhà kho chứa rác.

Tại sao phải cần testDir? (Why?)

Bạn có thể thắc mắc: "Tôi gõ tên file là chạy được, cần gì cấu hình thư mục?". Câu trả lời nằm ở 3 vấn đề cốt tử:

  1. Hiệu năng (Performance): Playwright cần biết phải bắt đầu tìm từ đâu. Quét 50 file trong thư mục tests/ nhanh hơn gấp trăm lần so với việc quét 50,000 file trong toàn bộ dự án (bao gồm cả node_modules, .git, src...).
  2. Tránh nhầm lẫn (Isolation): Trong dự án React/Vue/Angular, bạn thường có các file Unit Test (của Jest/Vitest) nằm lẫn trong thư mục src/. Nếu không khoanh vùng testDir: './tests', Playwright có thể lôi nhầm cả mấy file Unit Test đó ra chạy và gây lỗi.
  3. Quy chuẩn đường dẫn tương đối (Relative Path Resolution): Mọi đường dẫn trong config (như testMatch, snapshotDir) đều được tính toán dựa trên mốc là testDir.
  4. Quy trình 3 Bước: Từ Config đến lúc Chạy Test

Hãy xem xét một cấu trúc thư mục thực tế hỗn độn như sau:

 

my-project/

├── node_modules/ (Rất nặng, chứa 10.000 files)

├── src/

│   └── components/

│       └── Button.test.tsx  (Unit Test của Jest - KHÔNG ĐƯỢC CHẠY)

├── tests/ (Nơi chứa E2E Test - CẦN CHẠY)

│   ├── auth/

│   │   └── login.spec.ts

│   └── payment/

│       └── checkout.spec.ts

└── playwright.config.ts

Trong config:

export default defineConfig({

  testDir: './tests',          // 1. Khoanh vùng

  testMatch: '**/*.spec.ts',   // 2. Nhận dạng

});

🟢 Bước 1: Khoanh vùng (Scoping) - "Dựng Hàng Rào"

Ngay khi bạn gõ lệnh npx playwright test, việc đầu tiên Playwright làm là đọc dòng testDir: './tests'.

  • Hành động: Nó lập tức dựng một "hàng rào ảo" bao quanh thư mục tests.
  • Hệ quả:
    • Thư mục node_modules: BỊ LỜ ĐI (May quá, đỡ phải quét 10k file).
    • Thư mục src: BỊ LỜ ĐI. File test.tsx dù có đuôi .test cũng không được ngó ngàng tới.
    • Chỉ có thư mục tests là được "sáng đèn".

🔵 Bước 2: Lập danh sách ứng viên (Inventory & Matching)

Sau khi vào được bên trong hàng rào tests, Playwright bắt đầu quét file và áp dụng testMatch.

  1. Nó thấy file tests/auth/login.spec.ts.
    • Check testMatch (**/*.spec.ts): Khớp! -> Thêm vào danh sách.
  2. Nó thấy file tests/payment/checkout.spec.ts.
    • Check testMatch: Khớp! -> Thêm vào danh sách.
  3. Giả sử có file tests/readme.txt.
    • Check testMatch: Không khớp. -> Loại.

👉 Kết quả của Bước 2: Playwright tạo ra một "Danh sách nội bộ" (Test Suite Manifest) gồm 2 file hợp lệ. Đây là "Vũ trụ Test" của bạn.

🔴 Bước 3: Lọc theo lệnh gõ (CLI Filtering)

Đây là lúc cái lệnh bạn gõ phát huy tác dụng.

Trường hợp A: Bạn gõ npx playwright test (Không tham số)

  • Playwright lấy toàn bộ danh sách ở Bước 2 đem đi chạy.
  • => Chạy cả login và checkout.

Trường hợp B: Bạn gõ npx playwright test login (Có tham số)

  • Playwright cầm từ khóa "login" đi so với danh sách ở Bước 2.
    • So với tests/auth/login.spec.ts: KHỚP (Có chứa chữ login).
    • So với tests/payment/checkout.spec.ts: KHÔNG KHỚP.
  • => Chỉ chạy file spec.ts.


Minh chứng: Tại sao gõ ngắn được?

Tại sao bạn gõ npx playwright test login (thiếu tests/auth/...) mà nó vẫn hiểu?

Là vì ở Bước 3, Playwright thực hiện phép so sánh chuỗi dạng Contains (Chứa) chứ không phải Equals (Bằng chính xác).

  • Đường dẫn đầy đủ (trong danh sách nội bộ): tests/auth/login.spec.ts
  • Từ khóa bạn nhập: login

Logic kiểm tra:

// Mã giả mô phỏng logic Playwright

const allTests = ['tests/auth/login.spec.ts', 'tests/payment/checkout.spec.ts'];

const userInput = 'login';

const testsToRun = allTests.filter(file => file.includes(userInput));

// Kết quả: ['tests/auth/login.spec.ts']


Nó thấy chuỗi tests/auth/login.spec.ts có chứa chữ login, nên nó chọn.


Một ví dụ "Đau thương" nếu không có
testDir

Giả sử bạn xóa dòng testDir đi (mặc định nó lấy thư mục gốc .).

Lúc này, bạn gõ: npx playwright tes login

  1. Bước 1 (Khoanh vùng): Quét từ gốc. Nó chạy vào node_modules.
  2. Bước 2 (Lập danh sách): Vô tình trong node_modules của một thư viện nào đó có file ví dụ tên là login-example.spec.ts. Playwright không biết, nó tưởng là test của bạn -> Thêm vào danh sách.
  3. Bước 3 (Lọc): Bạn lọc chữ login.
  4. Kết quả: Nó chạy cả file test của bạn VÀ file test rác trong node_modules.

=> Test Fail tùm lum, chạy rất chậm.

🎯 Tóm tắt vai trò

  • testDir: Định nghĩa "Sân chơi" (Chỉ chơi trong sân này, cấm ra ngoài đường).
  • testMatch: Định nghĩa "Luật chơi" (Chỉ chơi với file đuôi .spec.ts).
  • CLI Argument (npx ...): Định nghĩa "Lượt chơi" (Hôm nay chỉ chơi bài login).

Nếu không có testDir, sân chơi của bạn là cả cái ổ cứng, và việc tìm kiếm file sẽ trở thành thảm họa.


Deep dive về cách chọn testDir

Chúng ta sẽ mổ xẻ từng ký tự trong chuỗi **/*.spec.ts và ./tests để bạn làm chủ nó hoàn toàn.

Giải phẫu testDir: './tests'

Đây là địa chỉ nhà (Starting Point).

  • . (Dấu chấm): Nghĩa là "Tại đây" (Thư mục gốc nơi chứa file config).
  • /: Dấu ngăn cách thư mục.
  • tests: Tên thư mục bạn muốn vào.

👉 Ý nghĩa: "Hãy bắt đầu đứng ở thư mục gốc, bước vào folder tests và chuẩn bị quét."

Ví dụ khác:

  • Nếu test bạn để trong folder e2e: -> testDir: './e2e'
  • Nếu test nằm sâu trong src/testing: -> testDir: './src/testing'


Giải phẫu testMatch: '**/*.spec.ts'

Đây là phần thú vị nhất. Đây là cú pháp Glob (không phải Regex).

Hãy chia nhỏ nó ra: ** + / + * + .spec.ts

🟢 ** (Hai dấu sao - Recursive Wildcard)

  • Ý nghĩa: "Bất kỳ thư mục nào, sâu bao nhiêu cũng được".
  • Tác dụng: Nó bảo Playwright: "Hãy chui vào mọi ngóc ngách, mọi folder con (sub-folder), folder cháu, folder chắt... đừng bỏ sót folder nào".
  • Nếu thiếu nó: Nó chỉ quét ngay tại cửa, không chịu đi vào phòng ngủ, nhà bếp.

🔵 * (Một dấu sao - File Wildcard)

  • Ý nghĩa: "Bất kỳ tên gì".
  • Tác dụng: Nó đại diện cho tên file. login, register, home... đều khớp với *.

🟠 .spec.ts (Extension)

  • Ý nghĩa: "Đuôi file cố định".
  • Tác dụng: Chỉ bắt những file kết thúc bằng đuôi này. File .ts thường hoặc .js sẽ bị bỏ qua.


So sánh trực quan: Có và Không có **

Giả sử bạn có cấu trúc thư mục:

tests/

├── login.spec.ts          (Nằm ngay ngoài)

└── auth/                  (Thư mục con)

    └── password.spec.ts   (Nằm bên trong)

Mẫu (Pattern)

Kết quả tìm kiếm

Giải thích

'*.spec.ts'

✅ login.spec.ts



❌ password.spec.ts

Sai lầm phổ biến. Một dấu sao * KHÔNG đi xuyên qua thư mục (auth/). Nó chỉ tìm file nằm ngay tại tests/.

'**/*.spec.ts'

✅ login.spec.ts



✅ password.spec.ts

Chuẩn. Hai dấu sao ** giúp nó chui vào folder auth/ để tìm tiếp.

'auth/*.spec.ts'

❌ login.spec.ts



✅ password.spec.ts

Chỉ định rõ là chỉ tìm trong auth/.



Các Ví Dụ Thực Tế

Dưới đây là các tình huống bạn sẽ gặp khi đi làm và cách viết config tương ứng.

📌 Ví dụ 1: Test nằm trong folder e2e thay vì tests

Cấu trúc:

my-project/

├── e2e/

│   ├── login.spec.ts

│   └── home.spec.ts

Config:

export default defineConfig({

  testDir: './e2e',           // Trỏ vào folder e2e

  testMatch: '**/*.spec.ts',  // Quét mọi ngóc ngách

});

📌 Ví dụ 2: Test nằm lẫn trong code nguồn (src)

Nhiều dự án React/Next.js thích để test ngay cạnh component.

Cấu trúc:

src/

├── components/

│   ├── Header.tsx

│   └── Header.spec.ts  <-- File test nằm đây

├── pages/

│   └── Login.spec.ts   <-- File test nằm đây

Config:

export default defineConfig({

  testDir: './src',           // Quét từ folder src

  testMatch: '**/*.spec.ts',  // Tìm mọi file đuôi .spec.ts trong src

});


📌 Ví dụ 3: Bạn dùng đuôi file khác (Ví dụ .test.ts)

Một số team thích đặt tên là login.test.ts thay vì login.spec.ts.

Config:

export default defineConfig({

  testDir: './tests',

  testMatch: '**/*.test.ts', // Sửa đuôi file

})

📌 Ví dụ 4: Muốn chạy nhiều loại đuôi file cùng lúc

Bạn muốn chạy cả .spec.ts (Playwright) và .test.ts (Jest cũ chuyển sang).

Config (Dùng ngoặc nhọn {} để gom nhóm):

export default defineConfig({

  testDir: './tests',

  // Tìm file có đuôi .spec.ts HOẶC .test.ts

  testMatch: '**/*.{spec,test}.ts',

});

📌 Ví dụ 5: Chỉ muốn chạy folder smoke (Test nhanh)

Cấu trúc:

tests/

├── regression/ (Test kỹ - Chạy lâu)

└── smoke/      (Test nhanh - Chạy lẹ)

Config (Nếu bạn muốn tạo một Project riêng cho Smoke):

{

  name: 'Smoke Tests',

  testDir: './tests',

  // Chỉ chui vào folder smoke để tìm

  testMatch: 'smoke/**/*.spec.ts',

}

🎯 Tóm lại quy tắc vàng

  1. testDir: Là cái cổng làng. (./tests, ./src, ./e2e).
  2. **: Là chìa khóa vạn năng để mở mọi cánh cửa phòng (thư mục con).
  3. *: Là đại diện cho tên file bất kỳ.

Câu thần chú: "Vào thư mục này (testDir), lặn sâu xuống đáy (**), tìm mọi file (*) có đuôi là (.spec.ts)".


Phần 2 Tìm hiểu về project trong playwright.config.ts

Tại sao cần chia nhỏ nhiều Project? (The "Why")

Hãy tưởng tượng file config của bạn là Tổng chỉ huy một chiến dịch quân sự. Nếu không chia Project, bạn chỉ có một binh đoàn làm tất cả mọi thứ giống hệt nhau.

Chia Project giúp bạn đạt được 3 mục đích tối thượng:

🎯 Mục đích 1: Tối ưu hóa Tài nguyên (Resource Optimization)

  • Vấn đề: Bạn có 100 bài test API và 100 bài test UI.
  • Nếu không chia Project: Playwright sẽ bật trình duyệt Chrome lên cho cả 200 bài test.
    • Test UI: Cần Chrome (Hợp lý).
    • Test API: Chỉ cần gửi request, không cần Chrome (Lãng phí RAM/CPU khủng khiếp).
  • Giải pháp:
    • Project API: Set use: { browserName: undefined } (Không bật browser).
    • Project UI: Set use: { browserName: 'chromium' }.
    • 👉 Kết quả: Chạy nhanh gấp đôi, máy mát rượi.

🎯 Mục đích 2: Ma trận kiểm thử (Test Matrix/Cross-browser)

  • Vấn đề: Sếp yêu cầu: "Bài test spec.ts phải chạy ngon trên cả Chrome, Firefox và iPhone 12".
  • Giải pháp: Bạn không cần copy file test ra làm 3 bản. Bạn chỉ cần tạo 3 Projects trỏ vào CÙNG MỘT testMatch.
    • Project A (Chrome) -> chạy spec.ts.
    • Project B (Firefox) -> chạy spec.ts.
    • Project C (iPhone) -> chạy spec.ts.

🎯 Mục đích 3: Điều phối luồng chạy (Orchestration)

  • Ví dụ chúng ta có cấu hìn: Setup DB -> Chạy Test -> Teardown. Nếu không có Project và dependencies, bạn không thể bắt Test chờ Setup được.


Phân tích sâu các thuộc tính (Góc độ Chiến lược)

Cấu trúc config hoạt động theo nguyên tắc: Cái riêng (Project config) sẽ ghi đè cái chung (Top-level config).


Nguyên tắc "Thác đổ" (Cascading)

Playwright sẽ tìm giá trị cấu hình theo thứ tự ưu tiên từ dưới lên trên:

  1. Ưu tiên 1: Cấu hình trong từng file test (use({...})).
  2. Ưu tiên 2: Cấu hình trong Project Object (Cái chúng ta đang bàn).
  3. Ưu tiên 3: Cấu hình Global (Nằm ngoài mảng projects).
  4. Ưu tiên 4: Giá trị Mặc định của Playwright (Nếu cả Global cũng không viết gì).

 

Bài toán đặt ra để chứng minh cơ chế "Kế thừa & Ghi đè" (Inheritance & Override) như sau:

  1. Global Config (Bố Mẹ): Quy định URL chung là anhtester.com và mặc định chạy ẩn (Headless) để cho nhanh.
  2. Project 1 (Con Ngoan): Chạy đúng theo bố mẹ bảo (Chạy ẩn trên Chrome).
  3. Project 2 (Con Cá Tính - Debug): Muốn GHI ĐÈ để hiện trình duyệt lên (Headless: false), chạy chậm lại để nhìn cho rõ (slowMo), và quay video lại.

 

📂 Bước 1: File playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({

  // 🎒 1. GLOBAL CONFIG (Cấu hình chung cho toàn bộ dự án)

  // Bố mẹ bảo: "Mặc định là chạy ẩn (headless) và tắt video nhé!"

  use: {

    baseURL: 'https://crm.anhtester.com', // Base URL dùng chung

    headless: true,                       // Mặc định chạy ngầm (Không hiện UI)

    viewport: { width: 1280, height: 720 },

    video: 'off',                         // Mặc định không quay video

    screenshot: 'only-on-failure',

  },

  projects: [

    // 👶 2. PROJECT: Standard Run (Con ngoan)

    {

      name: 'chromium-standard',

      use: {

        browserName: 'chromium',

        // 👉 KHÔNG GHI ĐÈ GÌ CẢ -> Sẽ kế thừa toàn bộ Global

        // Kết quả: Chạy ẩn, vào crm.anhtester.com

      },

    },

    // 🧑‍🎤 3. PROJECT: Visual Debug (Con cá tính)

    {

      name: 'firefox-debug',

      use: {

        browserName: 'firefox',

        // 🔥 GHI ĐÈ (Override) - Thay đổi lời bố mẹ

        headless: false,       // Bật trình duyệt lên cho tôi xem!

        video: 'on',           // Quay video lại toàn bộ quá trình!

       

        // ➕ THÊM MỚI (Add) - Cái này Global chưa có

        launchOptions: {

          slowMo: 1000,        // Chạy chậm lại 1 giây mỗi bước để nhìn cho sướng

        },

      },

    },

  ],

});


📂 Bước 2: File Test tests/crm-login.spec.ts

Lưu ý: Trong file test, mình dùng /admin/authentication. Vì đã có baseURL trong config, Playwright sẽ tự nối vào thành https://crm.anhtester.com/admin/authentication.

 

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


test('Đăng nhập CRM Anh Tester', async ({ page }) => {

  console.log(`🌍 Đang chạy trên Base URL: ${test.info().project.use.baseURL}`);

  // 1. Vào trang Login (Tự nối với baseURL trong config)

  await page.goto('/admin/authentication');

  // 2. Điền Email & Password (Acc Demo của Anh Tester)

  await page.fill('input[name="email"]', 'admin@example.com');

  await page.fill('input[name="password"]', '123456');

  // 3. Bấm nút Login

  await page.click('button[type="submit"]');


  // 4. Kiểm tra login thành công (Có menu Dashboard)

  await expect(page.locator('span:has-text("Dashboard")')).toBeVisible();

  console.log('✅ Đăng nhập thành công!');

});

 

🚀 Bước 3: Chạy và Kiểm chứng (Quan trọng)

Bây giờ bạn hãy chạy lần lượt 2 lệnh này để thấy sự khác biệt rõ rệt.

Lần 1: Chạy Project "Con ngoan" (chromium-standard)

npx playwright test --project=chromium-standard

  • Hiện tượng: Bạn KHÔNG THẤY GÌ CẢ. Terminal chỉ hiện dòng chữ xanh Running 1 test... rồi Passed.
  • Giải thích: Vì nó Kế thừa headless: true từ Global. Nó chạy ngầm hoàn toàn.

Lần 2: Chạy Project "Con cá tính" (firefox-debug)

 

npx playwright test --project=firefox-debug

  • Hiện tượng:
    1. Trình duyệt Firefox BẬT LÊN (Do headless: false).
    2. Nó tự động điền Email... chờ 1s... điền Pass... chờ 1s (Do slowMo: 1000).
    3. Sau khi chạy xong, bạn vào folder test-results, bạn sẽ thấy có file VIDEO quay lại cảnh login (Do video: 'on').

📊 Bảng Phân Tích Kết Quả "Hợp Nhất"

Đây là những gì thực sự xảy ra trong bộ não của Playwright khi bạn chạy Lần 2 (firefox-debug):

Cấu hình

Giá trị Global (Gốc)

Giá trị Project (Ghi đè)

GIÁ TRỊ THỰC TẾ KHI CHẠY

baseURL

...anhtester.com

(Không viết gì)

👉 ...anhtester.com (Kế thừa)

browserName

(Không có)

'firefox'

👉 'firefox' (Thêm mới)

headless

true (Ẩn)

false (Hiện)

👉 false (Project thắng!)

video

'off'

'on'

👉 'on' (Project thắng!)

slowMo

(Không có)

1000

👉 1000 (Thêm mới)


Bạn thấy đó, chỉ với 1 file config, bạn có thể tạo ra nhiều "kiểu chạy" khác nhau cho cùng 1 bài test trên trang CRM Anh Tester mà không cần sửa code test!

 

👑 Ưu tiên 1: test.use({...}) - "Lệnh bài miễn tử"

Nếu Global là Ông Bà, Project là Bố Mẹ, thì File Test chính là Bản Thân Bạn.

Dù Bố Mẹ (Project) bắt bạn mặc áo xanh, nhưng nếu ngay trong file test bạn ghi test.use({ áo: 'đỏ' }), thì Playwright sẽ nghe theo bạn tuyệt đối.

💻 Ví dụ Code Thực Chiến

Chúng ta vẫn dùng file config cũ (Global: chạy ẩn, Project: chạy ẩn).

Nhưng sẽ tạo một file test đặc biệt: tests/crm-mobile-view.spec.ts.

Trong file này, tôi muốn ép buộc Playwright phải:

  1. Hiện trình duyệt lên (headless: false).
  2. Chạy với kích thước màn hình điện thoại (viewport), dù Project đang cấu hình là Desktop.


File:
tests/crm-mobile-view.spec.ts

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


// 👑 1. GHI ĐÈ CẤP FILE (Priority 1)

// Dòng này có quyền lực tối cao, chấp hết mọi config bên ngoài

test.use({

  // Bố mẹ (Project) bảo chạy ẩn, nhưng tôi thích hiện!

  headless: false,

  // Bố mẹ (Project) set màn hình HD 1280x720, nhưng tôi thích màn dọc!

  viewport: { width: 375, height: 812 }, // iPhone X size

  // Thêm một cái header lạ hoắc

  extraHTTPHeaders: {

    'My-Custom-Header': 'Playwright-Override',

  },

});


test('Check giao diện Login trên màn hình dọc', async ({ page }) => {

  console.log(`📏 Viewport hiện tại: ${page.viewportSize()?.width}x${page.viewportSize()?.height}`);

  // Vào trang CRM (Vẫn lấy baseURL từ config cha/ông)

  await page.goto('/admin/authentication');

  // Kiểm tra xem logo có bị vỡ layout ở màn hình nhỏ không

  const logo = page.locator('.brand-logo img');

  await expect(logo).toBeVisible();


  // Dừng 3s để bạn kịp nhìn thấy cái màn hình bé tẹo

  await page.waitForTimeout(3000);

});


💥 Chuyện gì xảy ra khi chạy?

Giả sử bạn chạy lệnh gọi đúng project Chromium chuẩn (đang set chạy ẩn, màn to):

npx playwright test tests/crm-mobile-view.spec.ts --project=chromium-standard

Kết quả thực tế:

  1. Trình duyệt BẬT LÊN (Dù Project chromium-standard đang set headless: true).
  2. Cửa sổ trình duyệt BÉ TÍ (Size 375x812) giống điện thoại.


📊
Bảng Phân Tích Quyền Lực (Hierarchy of Power)

Hãy xem test.use nó "đè bẹp" các cấp trên như thế nào trong ví dụ này:

Thuộc tính

👴 Global (Ông)

🧑 Project (Bố)

👶 Test File (test.use)

👉 KẾT QUẢ CUỐI CÙNG

baseURL

crm.anhtester.com

(Kế thừa)

(Kế thừa)

crm.anhtester.com

browserName

(Trống)

'chromium'

(Kế thừa)

'chromium'

headless

true (Ẩn)

true (Ẩn)

false (Hiện)

false (Theo File Test)

viewport

1280x720

(Kế thừa)

375x812

375x812 (Theo File Test)


💡
Khi nào nên dùng test.use?

Bạn không nên lạm dụng nó. Chỉ dùng trong các trường hợp đặc biệt (Edge Cases):

  1. Test giao diện Mobile: Project chung là Desktop, nhưng có 1-2 bài test bạn muốn check xem trên Mobile nó hiển thị ra sao (như ví dụ trên).
  2. Test múi giờ/Locale: Project chạy Tiếng Anh, nhưng riêng bài này cần test Tiếng Nhật -> use({ locale: 'ja-JP' }).
  3. Test quyền (Permission): Riêng bài này cần cấp quyền Geolocation -> use({ permissions: ['geolocation'] }).
  4. Bỏ qua lỗi SSL: Riêng trang này cert bị lỗi -> use({ ignoreHTTPSErrors: true }).

🎯 Tóm lại

Thứ tự ưu tiên tuyệt đối của Playwright (từ mạnh nhất đến yếu nhất):

  1. 🥇 use({...}) trong file test.
  2. 🥈 use: {...} trong projects: [...] (File config).
  3. 🥉 use: {...} toàn cục (File config).
  4. 🏅 Mặc định của Playwright.


Cơ chế hợp nhất merge của thuộc tính use

Hãy tưởng tượng use giống như một chiếc "Ba Lô Đi Học".

  • Global Config: Là những món đồ cơ bản Bố Mẹ soạn sẵn vào ba lô cho con (Bút, Vở, Nước).
  • Project Config: Là những món đồ riêng mà đứa con tự bỏ thêm vào hoặc đổi lại (Đổi bút xanh thành bút đỏ, bỏ thêm truyện tranh).


Ví dụ Code thực tế

Hãy xem kỹ file config dưới đây. Chúng ta sẽ soi vào Project B (Mobile) để xem chiếc ba lô cuối cùng của nó chứa những gì.

// playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({

  // 🎒 GLOBAL USE (Bố mẹ soạn sẵn 3 món)

  use: {

    // Món 1: Môi trường

    baseURL: 'http://localhost:3000',

   

    // Món 2: Chế độ chạy

    headless: true, // Chạy ngầm

 
    // Món 3: Chụp ảnh

    screenshot: 'only-on-failure', // Chỉ chụp khi lỗi

  },


  projects: [

    // 👶 PROJECT A: Đứa con ngoan (Dùng y nguyên đồ bố mẹ)

    {

      name: 'chromium-desktop',

      use: {

        browserName: 'chromium',

      }

      // 👉 Kết quả: Có đủ 3 món của Global + browserName

    },


    // 🧑‍🎤 PROJECT B: Đứa con cá tính (Mobile Test)

    {

      name: 'iphone-test',


      // 🎒 PROJECT USE (Nó tự sửa lại ba lô)

      use: {

        // 1. GHI ĐÈ (Override): Con không thích localhost, con thích Staging!

        baseURL: 'https://staging.example.com',

        // 2. THÊM MỚI (Add): Con cần thêm cấu hình điện thoại

        ...devices['iPhone 12'], // Thêm userAgent, viewport...

        // 3. THÊM MỚI: Con muốn quay video

        video: 'on',

        // ⚠️ CHÚ Ý: Nó KHÔNG nhắc gì đến 'headless' và 'screenshot'

      },

    },

  ],

});

Phân tích kết quả "Hợp Nhất" (The Merge Logic)

Bây giờ, hãy mở chiếc ba lô của Project B (iphone-test) ra xem bên trong có gì.

Playwright sẽ thực hiện phép cộng object như sau:

Object_{Final} = Object_{Global} + Object_{Project}

Tên Thuộc Tính

Giá trị Global (Gốc)

Giá trị Project (Con)

GIÁ TRỊ CUỐI CÙNG (Kết quả)

Giải thích

baseURL

localhost:3000

staging.com

👉 staging.com

Ghi đè (Con thắng).

headless

true

(Không có)

👉 true

Kế thừa (Lấy của Bố mẹ).

screenshot

'only-on-failure'

(Không có)

👉 'only-on-failure'

Kế thừa (Lấy của Bố mẹ).

userAgent

(Không có)

'Mozilla/5.0...'

👉 'Mozilla/5.0...'

Thêm mới (Từ devices).

video

(Không có)

'on'

👉 'on'

Thêm mới.



Cảnh báo quan trọng: "Merge Shallow" (Hợp nhất nông) là gì?

Nghĩa là nó chỉ hợp nhất ở lớp ngoài cùng của object use. Nếu bên trong use có một object con nữa, nó sẽ thay thế hoàn toàn chứ không hợp nhất sâu bên trong.

Ví dụ "Tai nạn" thường gặp:

Global Config:

use: {

  launchOptions: {

    slowMo: 100,      // Làm chậm 100ms

    devtools: false,  // Tắt devtools

  }

}

Project Config:

use: {

  // Bạn muốn bật devtools lên, nhưng bạn viết thế này:

  launchOptions: {

    devtools: true

  }

}

🔴 Kết quả sai lầm:

Playwright sẽ VỨT BỎ toàn bộ cục launchOptions của Global (mất luôn cái slowMo: 100). Nó thay thế bằng cục mới chỉ có mỗi devtools: true.

Cách sửa (Phải spread cả cái cũ ra):

use: {

  launchOptions: {

    ...config.use.launchOptions, // Giữ lại cái cũ

    devtools: true               // Ghi đè cái mới

  }

}

🎯 Tóm lại 

  1. Tính chất: use của Project hoạt động theo kiểu "Bổ sung & Thay thế".
  2. Cái gì trùng tên: Project sẽ đè lên Global.
  3. Cái gì Project thiếu: Sẽ tự động lấy từ Global đắp vào.
  4. Cái gì Global không có: Project tự thêm vào dùng riêng.
  5. Với testMatch, testIgnore, testDir: Project đá bay Global và dùng luật riêng của nó (Replace).

Đây là cơ chế cực kỳ mạnh mẽ giúp file config gọn gàng, không phải copy-paste code lặp đi lặp lại!


Phần 3: Cấu trúc (Syntax) của một Project Object trong Playwright.

Hãy hình dung file playwright.config.ts là một Tập đoàn, còn mỗi Object trong mảng projects: [] là một Phòng ban chuyên biệt. Mỗi phòng ban có nội quy, nhân sự và nhiệm vụ riêng.

Dưới đây là "bản giải phẫu" toàn diện của một Project Object:

🦴 Cấu trúc xương sống (Template)

Một Project Object đầy đủ sẽ trông như thế này:

{

  // 1️⃣ NHÓM ĐỊNH DANH (Identity)

  name: 'chromium-desktop',


  // 2️⃣ NHÓM LỌC FILE (Filter - Quan trọng nhất)

  testDir: './tests/e2e',       // Phạm vi tìm kiếm

  testMatch: /.*\.spec\.ts/,    // Lấy cái gì?

  testIgnore: /.*mobile\.ts/,   // Bỏ cái gì?


  // 3️⃣ NHÓM MÔI TRƯỜNG (Environment / Context)

  use: {

    browserName: 'chromium',

    baseURL: 'https://staging.example.com',

    storageState: 'auth.json',

    headless: false,

    ...devices['Desktop Chrome'],

  },


  // 4️⃣ NHÓM ĐIỀU PHỐI (Orchestration)

  dependencies: ['setup-db'],   // Phải chờ ai?

  teardown: 'cleanup-db',       // Ai dọn rác cho mình? (Thường dùng cho project setup)


  // 5️⃣ NHÓM VẬN HÀNH (Behavior Overrides)

  retries: 2,                   // Thử lại bao nhiêu lần?

  timeout: 60000,               // Hạn mức thời gian riêng cho project này

  fullyParallel: true,          // Có chạy song song hết tốc lực không?

}


🕵️ Phân tích chi tiết từng thành phần

Dưới đây là ý nghĩa và cách dùng của từng thuộc tính:

  1. name (Định danh)
  • Kiểu dữ liệu: string (Chuỗi).
  • Vai trò: Là cái tên để bạn gọi khi chạy lệnh CLI hoặc để hiển thị trong báo cáo.
  • Ví dụ:
    • Đặt name: 'api'.
    • Gọi chạy: npx playwright test --project=api
  1. Bộ 3 Lọc File (testDir, testMatch, testIgnore)

Đây là bộ phận quyết định "Project này sẽ chạy những file nào?".

  • testDir (Sân chơi):
    • Xác định thư mục gốc mà project này quản lý.
    • Nếu không khai báo, nó sẽ lấy testDir của config tổng (Top-level).
    • Ví dụ: testDir: './tests/api' (Chỉ quét trong folder API).
  • testMatch (Luật nhận):
    • Quy định file nào được chấp nhận.
    • Syntax: String, Regex, hoặc Mảng.
    • Ví dụ: testMatch: '**/*.spec.ts' (Lấy hết file spec).
  • testIgnore (Luật cấm):
    • Quy định file nào bị loại trừ (dù đã khớp testMatch).
    • Ví dụ: testIgnore: '**/smoke/*.ts' (Không chạy folder smoke).
  1. use (Môi trường & Fixtures)

Đây là phần "linh hồn", quyết định bài test chạy như thế nào (trình duyệt gì, màn hình to hay nhỏ, đã login chưa). Nó sẽ ghi đè (override) cấu hình use chung của cả file config.

Các thuộc tính quan trọng trong use:

  • browserName: 'chromium', 'firefox', 'webkit'.
  • channel: 'chrome', 'msedge' (Nếu muốn dùng trình duyệt thật thay vì bản build của Playwright).
  • baseURL: Đường dẫn gốc (VD: chạy localhost hay staging).
  • storageState: Đường dẫn file JSON chứa cookie/token (để bỏ qua bước login).
  • viewport: Kích thước màn hình { width: 1280, height: 720 }.
  • headless: true (chạy ngầm) hoặc false (hiện UI).
  • device: Cấu hình giả lập thiết bị di động (UserAgent, màn hình...).
  1. dependencies (Sự phụ thuộc)
  • Kiểu dữ liệu: string[] (Mảng chứa các name của project khác).
  • Vai trò: Thiết lập thứ tự chạy.
  • Nguyên tắc: "Tôi chỉ bắt đầu chạy khi TẤT CẢ các thằng có tên trong danh sách này đã chạy xong và thành công (Passed)".
  • Ví dụ: dependencies: ['setup'] -> Project này sẽ bị Block lại cho đến khi Project 'setup' hoàn thành.
  1. teardown (Người dọn dẹp)
  • Kiểu dữ liệu: string (Tên của một project khác).
  • Vai trò: Chỉ định project nào sẽ chạy sau cùng để dọn dẹp rác.
  • Lưu ý: Thuộc tính này thường chỉ được đặt ở Project Setup.
  • Logic: Setup chạy xong -> (Các project chính chạy) -> Teardown chạy.


🧩 Ví dụ tổng hợp: Một cấu trúc Project chuẩn

Dưới đây là ví dụ minh họa cách phối hợp các cú pháp trên:

projects: [

  // ------------------------------------------------

  // 1. PROJECT SETUP (Chạy đầu tiên)

  // ------------------------------------------------

  {

    name: 'setup',

    testMatch: /global\.setup\.ts/,

    teardown: 'cleanup', // 👉 Xong việc thì nhớ gọi thằng cleanup

  },


  // ------------------------------------------------

  // 2. PROJECT TEARDOWN (Chạy cuối cùng)

  // ------------------------------------------------

  {

    name: 'cleanup',

    testMatch: /global\.teardown\.ts/,

  },


  // ------------------------------------------------

  // 3. PROJECT E2E CHROME (Chạy chính)

  // ------------------------------------------------

  {

    name: 'chromium',

    // 👉 Phụ thuộc vào setup

    dependencies: ['setup'],


    // 👉 Ghi đè môi trường

    use: {

      browserName: 'chromium',

      storageState: 'user-data.json', // Dùng data do setup tạo

    },


    // 👉 Lọc file: Chỉ chạy file spec, trừ file smoke

    testMatch: '**/*.spec.ts',

    testIgnore: '**/*.smoke.spec.ts',

  },


  // ------------------------------------------------

  // 4. PROJECT SMOKE (Chạy nhanh)

  // ------------------------------------------------

  {

    name: 'smoke-test',

    // 👉 Không cần dependencies (Chạy luôn cho nhanh)

    use: { viewport: { width: 1920, height: 1080 } },

    testMatch: '**/*.smoke.spec.ts',

  },

]

🎯 Tóm tắt

Một Object Project trong Playwright trả lời cho 4 câu hỏi:

  1. Tôi là ai? (name)
  2. Tôi làm việc với ai? (testDir, testMatch, testIgnore)
  3. Tôi làm việc trong điều kiện nào? (use)
  4. Tôi phải chờ ai? (dependencies)

 

Cách playwright chạy khi có nhiều project


Chúng ta sẽ đi sâu vào "bộ não" của Playwright để xem nó xử lý luồng dữ liệu như thế nào khi bạn có Nhiều Project và gõ lệnh chạy (có hoặc không có bộ lọc).

Hãy hình dung quy trình này giống như một "Nhà máy sàng lọc 3 lớp".

📂 Thiết lập bối cảnh (Ví dụ)

Giả sử bạn có 3 file test và 3 Project như sau:

File thực tế trong ổ cứng:

  1. tests/api/user.spec.ts
  2. tests/ui/login.spec.ts
  3. tests/ui/cart.spec.ts

Config (3 Project):

  1. Project API: Chỉ nhận folder api/.
  2. Project Desktop (Chrome): Chỉ nhận folder ui/.
  3. Project Mobile (iPhone): Cũng nhận folder ui/ (Chạy song song để test giao diện mobile).


🏭 Quy trình Sàng Lọc 3 Lớp

Khi bạn gõ lệnh npx playwright test [filter], Playwright không chạy ngay đâu. Nó thực hiện quy trình sau:

🟢 Lớp 1: Quét Ổ Cứng (Physical Scan)

  • Nhiệm vụ: Tìm tất cả các file có thể là test.
  • Dựa vào: testDir (Ví dụ ./tests).
  • Kết quả: Nó tìm thấy 3 file (user, login, cart).
  • Lưu ý: Lúc này nó chưa quan tâm đến Project nào cả.


🟡 Lớp 2: Ghép Cặp Dự Án (Project Mapping Matrix)

  • Nhiệm vụ: Đem từng file tìm được ở Lớp 1, ướm thử vào từng Project.
  • Logic:
    • Cầm spec.ts hỏi Project API: "Mày nhận không?" -> ✅ Có (testMatch khớp).
    • Cầm spec.ts hỏi Project Desktop: "Mày nhận không?" -> ❌ Không (testMatch lệch).
    • Cầm spec.ts hỏi Project Desktop: "Mày nhận không?" -> ✅ Có.
    • Cầm spec.ts hỏi Project Mobile: "Mày nhận không?" -> ✅ Có (Mobile cũng test UI).


👉 Kết quả của Lớp 2 (Danh sách công việc tiềm năng):

  1. Job A: spec.ts chạy trên Project API.
  2. Job B: spec.ts chạy trên Project Desktop.
  3. Job C: spec.ts chạy trên Project Desktop.
  4. Job D: spec.ts chạy trên Project Mobile.
  5. Job E: spec.ts chạy trên Project Mobile.

(Tổng cộng 5 jobs được tạo ra từ 3 file thực tế)

🔴 Lớp 3: Bộ Lọc CLI (User Filter)

  • Nhiệm vụ: Lọc lại danh sách 5 Jobs trên dựa theo những gì bạn gõ trên dòng lệnh.



🧪 Phân tích các tình huống gõ lệnh

Bây giờ chúng ta xem điều gì xảy ra với danh sách 5 Jobs kia khi bạn gõ lệnh.

Trường hợp 1: Gõ npx playwright test (Không tham số)

  • Lệnh: "Chạy hết cho tao!"
  • Hành động: Giữ nguyên cả 5 Jobs.
  • Hiện tượng:
    • File user chạy 1 lần.
    • File login chạy 2 lần (1 lần Desktop, 1 lần Mobile).
    • File cart chạy 2 lần.


Trường hợp 2: Gõ
npx playwright test login (Filter theo tên file)

  • Lệnh: "Chỉ chạy những job nào mà file test có chữ login".
  • Sàng lọc:
    • Job A (user): ❌ Loại.
    • Job B (login - Desktop): ✅ Giữ.
    • Job C (cart): ❌ Loại.
    • Job D (login - Mobile): ✅ Giữ.
    • Job E (cart): ❌ Loại.
  • Kết quả: Chạy 2 Tests (Cùng là bài login, nhưng trên 2 môi trường Desktop và Mobile).


Trường hợp 3: Gõ
npx playwright test ui/ (Filter theo thư mục)

  • Lệnh: "Chỉ chạy job nào file nằm trong folder ui".
  • Sàng lọc:
    • Job A (api/user): ❌ Loại.
    • Còn lại 4 jobs của UI (Desktop + Mobile) đều được giữ.
  • Kết quả: Chạy 4 Tests.


Trường hợp 4: Gõ
npx playwright test --project=Mobile (Filter theo Project)

  • Lệnh: "Tao chỉ quan tâm đến thằng Mobile thôi".
  • Sàng lọc:
    • Job A (API): ❌ Loại.
    • Job B, C (Desktop): ❌ Loại.
    • Job D (login - Mobile): ✅ Giữ.
    • Job E (cart - Mobile): ✅ Giữ.
  • Kết quả: Chạy 2 Tests (Chỉ login và cart trên Mobile).


Trường hợp 5: Combo
npx playwright test login --project=Desktop

  • Lệnh: "Tìm bài login, nhưng chỉ chạy trên Desktop thôi".
  • Sàng lọc:
    • Bước 1 (Lọc tên login): Giữ Job B (Desktop) và Job D (Mobile).
    • Bước 2 (Lọc project Desktop): Loại Job D (Mobile).
  • Kết quả: Chạy duy nhất 1 Test (Job B).

📊 Bảng Ma Trận Quét (Visualization)

Để dễ hình dung, Playwright âm thầm kẻ một cái bảng trong đầu như thế này:

File Test

Project API

Project Desktop

Project Mobile

api/user.spec.ts

Match

❌ Ignore

❌ Ignore

ui/login.spec.ts

❌ Ignore

Match

Match

ui/cart.spec.ts

❌ Ignore

Match

Match

Khi bạn gõ lệnh test login:

  1. Nó nhìn vào dòng ui/login.spec.ts.
  2. Nó thấy có 2 dấu tích xanh (Desktop & Mobile).
  3. Nó kích hoạt cả 2.

🎯 Tóm lại

Khi bạn gõ lệnh chạy:

  1. Playwright KHÔNG chọn Project trước rồi mới tìm file.
  2. Nó tạo ra TẤT CẢ các khả năng có thể xảy ra (File X chạy trên Project Y).
  3. Sau đó nó dùng cái chuỗi bạn gõ (login, ui/...) để cắt bớt những khả năng không khớp.

Hiểu được điều này, bạn sẽ không bị bất ngờ khi gõ tên 1 file mà thấy nó chạy ầm ầm trên 3-4 trình duyệt khác nhau (nếu bạn cấu hình nhiều Project cùng trỏ vào file đó).

 

Code thực tế cho phần bên trên

Để bạn thấy rõ luồng chạy, bí quyết nằm ở việc sử dụng test.info().project.name để in ra xem Project nào đang chiếm quyền điều khiển.

📂 1. Cấu trúc thư mục

Bạn hãy tạo các file theo đúng cây thư mục này nhé:

my-project/

├── playwright.config.ts

└── tests/

    ├── api/

    │   └── user.spec.ts    (Chỉ dành cho API Project)

    └── ui/

        ├── login.spec.ts   (Dành cho Desktop & Mobile)

        └── cart.spec.ts    (Dành cho Desktop & Mobile)



⚙️ 2. File Config: playwright.config.ts

Đây là nơi phân chia lãnh thổ cho 3 Project.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({

  testDir: './tests', // 🟢 LỚP 1: Quét toàn bộ folder tests

  // Dùng reporter list để in log ra terminal cho dễ nhìn

  reporter: 'list',

  projects: [

    // ----------------------------------------------------

    // 1️⃣ PROJECT API (Chuyên xử lý folder api)

    // ----------------------------------------------------

    {

      name: 'api-engine',

      // Chỉ nhận file trong folder api

      testMatch: '**/api/*.spec.ts',

      use: {

        // Không bật browser (tiết kiệm tài nguyên)

        browserName: undefined,

      },

    },

    // ----------------------------------------------------

    // 2️⃣ PROJECT DESKTOP (Chuyên xử lý folder ui - Chrome)

    // ----------------------------------------------------

    {

      name: 'desktop-chrome',

      // Chỉ nhận file trong folder ui

      testMatch: '**/ui/*.spec.ts',

      use: {

        ...devices['Desktop Chrome'],

        headless: true,

      },

    },

    // ----------------------------------------------------

    // 3️⃣ PROJECT MOBILE (Chuyên xử lý folder ui - iPhone)

    // ----------------------------------------------------

    {

      name: 'mobile-ios',

      // Cũng nhận folder ui (Chạy song song với Desktop)

      testMatch: '**/ui/*.spec.ts',

      use: {

        ...devices['iPhone 12'],

        headless: true,

      },

    },

  ],

});


📝 3. Nội dung các file Test (Có Log định danh)

Chúng ta sẽ chèn console.log để biết ai đang chạy mình.

File 1: tests/api/user.spec.ts

import { test } from '@playwright/test';

test('Get User Info', async ({ request }, testInfo) => {

  // 👇 Dòng này quan trọng: In ra tên Project đang chạy

  console.log(`📡 [${testInfo.project.name}] đang chạy file: API User`);

});

 

File 2: tests/ui/login.spec.ts

import { test } from '@playwright/test';

test('Login System', async ({ page }, testInfo) => {

  console.log(`💻📱 [${testInfo.project.name}] đang chạy file: UI Login`);

});


File 3:
tests/ui/cart.spec.ts

import { test } from '@playwright/test';


test('Check Cart', async ({ page }, testInfo) => {

  console.log(`🛒 [${testInfo.project.name}] đang chạy file: UI Cart`);

});



🚀
4. Thực Chiến: Chạy và Xem Kết Quả

Bây giờ hãy mở Terminal lên và xem "Nhà máy" hoạt động qua các câu lệnh sau.

Case 1: Chạy tất cả (npx playwright test)

Playwright sẽ kích hoạt cả 3 Project.

  • Lệnh:

npx playwright test

  • Kết quả in ra (Tổng 5 Tests):

📡 [api-engine] đang chạy file: API User        ✅ Passed

💻📱 [desktop-chrome] đang chạy file: UI Login  ✅ Passed

💻📱 [mobile-ios] đang chạy file: UI Login      ✅ Passed  <-- (Login chạy 2 lần)

🛒 [desktop-chrome] đang chạy file: UI Cart     ✅ Passed

🛒 [mobile-ios] đang chạy file: UI Cart         ✅ Passed  <-- (Cart chạy 2 lần)

👉 Giải thích: Project API chạy 1 file. Project Desktop chạy 2 file UI. Project Mobile cũng chạy 2 file UI. Tổng = 1 + 2 + 2 = 5.


Case 2: Filter theo tên file (... test login)

Bạn muốn chạy bài login, nhưng không nói rõ project nào -> Nó lôi tất cả project có chứa bài login ra chạy.

  • Lệnh:

npx playwright test login

  • Kết quả in ra (Tổng 2 Tests):

💻📱 [desktop-chrome] đang chạy file: UI Login  ✅ Passed

💻📱 [mobile-ios] đang chạy file: UI Login      ✅ Passed

👉 Giải thích:

  • api-engine: Không có file login -> Bỏ qua.
  • desktop-chrome: Có file login -> Chạy.
  • mobile-ios: Có file login -> Chạy.


Case 3: Filter theo folder (... test ui/)

Bạn muốn chạy tất cả test UI.

  • Lệnh:

npx playwright test ui/

  • Kết quả in ra (Tổng 4 Tests):

💻📱 [desktop-chrome] đang chạy file: UI Login  ✅ Passed

💻📱 [mobile-ios] đang chạy file: UI Login      ✅ Passed

🛒 [desktop-chrome] đang chạy file: UI Cart     ✅ Passed

🛒 [mobile-ios] đang chạy file: UI Cart         ✅ Passed

👉 Giải thích: Project api-engine bị loại vì nó nằm ở folder api/, không khớp với từ khóa ui/.



Case 4: Filter Combo (... test login --project=mobile-ios)

Đây là cách chạy chính xác nhất để debug. Chỉ chạy bài Login trên iPhone.

  • Lệnh:

npx playwright test login --project=mobile-ios

  • Kết quả in ra (Tổng 1 Test duy nhất):

💻📱 [mobile-ios] đang chạy file: UI Login      ✅ Passed

👉 Giải thích:

  • api-engine: Loại (Sai project).
  • desktop-chrome: Loại (Sai project).
  • mobile-ios: Đúng project -> Tìm bài login -> Chạy.

🎯 Kết luận

Nhìn vào log output, bạn sẽ thấy rõ:

  1. Cùng 1 file code (spec.ts) nhưng được in ra 2 lần với 2 cái tên Project khác nhau (desktop-chrome và mobile-ios).
  2. Playwright thực sự tạo ra các "Job" riêng biệt dựa trên File + Project.

 

🕵️ Phân Tích Chuyên Sâu: testMatch trong Playwright

testMatch là bộ lọc quyết định file nào được phép tham gia vào Project. Nếu file không vượt qua được bộ lọc này, Playwright sẽ coi như nó không tồn tại.

Có 3 cách viết chính, và việc chọn cách nào sẽ ảnh hưởng trực tiếp đến độ linh hoạt của dự án khi bạn thay đổi cấu trúc thư mục.


1️⃣ Cách 1: Dùng String & Glob Pattern (Dễ đọc - Phổ biến nhất)

Đây là cách viết sử dụng các ký tự đại diện (Wildcards) giống như cách bạn tìm kiếm file trong Windows/Linux.


Cú pháp cơ bản

  • Chuỗi tĩnh (Static String): Chỉ định chính xác tên file.

testMatch: 'global.setup.ts',

👉 Ý nghĩa: Tìm file tên là global.setup.ts nằm NGAY TẠI thư mục gốc (testDir).

  • Glob Pattern (Khuyên dùng): Sử dụng ký tự đặc biệt.

testMatch: '**/*.spec.ts',

👉 Giải mã:

  • ** (Double Star): "Bất kỳ thư mục con nào, sâu bao nhiêu cũng được".
  • * (Single Star): "Bất kỳ tên file nào".
  • .spec.ts: "Kết thúc bằng đuôi này".

Cái bẫy chết người của String (Without **)

Giả sử cấu hình của bạn là: testMatch: 'global.setup.ts' (Thiếu **).

  • Chạy OK: Nếu file nằm ở tests/global.setup.ts.
  • LỖI (No tests found): Nếu bạn "dọn nhà", di chuyển file vào tests/config/global.setup.ts.
    • Lý do: Chuỗi tĩnh rất cứng nhắc, nó chỉ tìm ở ngay cửa, không chịu đi sâu vào phòng trong (folder con).


2️⃣ Cách 2: Dùng Regex - Biểu thức chính quy (Mạnh mẽ - Linh hoạt)

Đây là cách viết dùng logic toán học để quét chuỗi ký tự. Nó không quan tâm file nằm ở đâu, nó chỉ quan tâm "trong đường dẫn có chứa cụm từ này không".


Cú pháp

testMatch: /global\.setup\.ts/,

Tại sao lại viết loằng ngoằng thế?

Trong Regex, các ký tự đặc biệt cần được "Escape" (Thoát) bằng dấu gạch chéo ngược \.

  • Dấu chấm . trong Regex nghĩa là: "Bất kỳ ký tự nào" (chữ A, số 1, dấu cách...).
  • Cụm \. nghĩa là: "Dấu chấm thực sự" (ký tự dot).

Ví dụ so sánh:

  • /login.spec.ts/: Khớp với ts, login-spec.ts (Sai ý đồ).
  • /login\.spec\.ts/: Chỉ khớp với spec.ts (Đúng chuẩn).

Sức mạnh "Dính như Sam"

Khác với String, Regex hoạt động theo cơ chế Partial Match (Khớp một phần).

Giả sử testMatch: /global\.setup\.ts/.

  • File: tests/global.setup.ts -> ✅ Có chứa chuỗi -> Khớp.
  • File: tests/auth/config/global.setup.ts -> ✅ Vẫn chứa chuỗi -> Khớp.

👉 Ưu điểm: Bạn di chuyển file đi đâu cũng được, miễn tên file không đổi thì Regex vẫn bắt được.


3️⃣ Cách 3: Dùng Mảng - Array (Combo đa năng)

Dùng khi bạn muốn Project này "ăn tạp", nhận nhiều loại file khác nhau.

 

testMatch: [

    '**/*.spec.ts',        // Lấy hết các file test chính

    '!**/experimental/**', // Dấu ! nghĩa là LOẠI TRỪ folder này

    'utils/global.setup.ts' // Lấy thêm file setup cụ thể

],

🥊 Màn So Găng Thực Tế: String vs. Regex

Để bạn hiểu rõ sự khác biệt, hãy xem kịch bản "Dọn nhà" sau đây.

Giả sử testDir: './tests'.

Bạn có file setup ban đầu nằm ở: tests/global.setup.ts.

Sau 1 tháng, bạn muốn gọn gàng nên chuyển nó vào: tests/auth/global.setup.ts.

Cấu hình testMatch

Kết quả khi file ở tests/

Kết quả sau khi chuyển vào tests/auth/

Giải thích

String tĩnh



'global.setup.ts'

✅ OK

FAIL (Không tìm thấy)

String tĩnh chỉ tìm ở thư mục gốc của testDir, không tìm trong folder con.

Regex



/global\.setup\.ts/

✅ OK

OK

Regex quét toàn bộ đường dẫn, thấy chuỗi khớp là nhận.

Glob (Có **)



'**/global.setup.ts'

✅ OK

OK

Dấu ** giúp nó đào sâu vào mọi thư mục con.


🎯 Lời khuyên (Best Practice) - Nên dùng cái nào?

Dù Regex rất mạnh, nhưng cú pháp của nó khó nhớ và dễ sai (\ lung tung).

💡 Giải pháp "Trung Đạo" (Khuyên dùng):

Hãy dùng String kết hợp với Glob Pattern (**). Nó vừa dễ đọc như String, vừa mạnh mẽ tìm kiếm sâu như Regex.

// ✅ GOOD: Dễ đọc, cân mọi thư mục con

testMatch: '**/global.setup.ts',

 

// ✅ GOOD: Tiêu chuẩn cho file test

testMatch: '**/*.spec.ts',

 

// 😐 OK nhưng khó đọc:

testMatch: /.*global\.setup\.ts/,

 

// ❌ BAD: Dễ gãy khi đổi cấu trúc folder

testMatch: 'global.setup.ts',

Tổng kết: Hãy luôn thêm **/ vào trước tên file trong testMatch nếu bạn dùng String. Đó là chìa khóa để file config của bạn "sống dai" qua các đợt refactor code!



Phần 3: Global setup là gì. Cách thiết lập mặc định (bad practice)

Hãy tưởng tượng quy trình Mở Quán Phở:

  1. Global Setup (Sáng sớm): Ông chủ đến, mở cửa cuốn, bật cầu dao điện tổng, đun nồi nước dùng to đùng. Việc này chỉ làm 1 lần duy nhất vào đầu ngày.
  2. Tests (Trong ngày): Khách vào ăn (Test case). Khách A ăn phở bò, khách B ăn phở gà. Họ dùng chung điện và nước dùng mà ông chủ đã bật.
  3. Global Teardown (Đêm muộn): Ông chủ tắt điện, đổ rác, đóng cửa cuốn. Việc này chỉ làm 1 lần duy nhất khi hết khách.

Định nghĩa "Dân dã"

  • Global Setup: Là file chứa đoạn code chạy đầu tiên, trước khi bất kỳ bài test nào được phép chạy. Nhiệm vụ: Chuẩn bị môi trường chung (Set biến môi trường, Bật Server, Tạo thư mục tạm...).
  • Global Teardown: Là file chứa đoạn code chạy cuối cùng, sau khi tất cả các bài test đã xong (dù đỗ hay trượt). Nhiệm vụ: Dọn dẹp (Tắt Server, Xóa thư mục tạm...).

Ví dụ Code: Giả lập kết nối Database (Không đụng UI)

Trong ví dụ này, chúng ta sẽ không bật trình duyệt. Chúng ta sẽ giả vờ thiết lập một đường dẫn Database (DB_URL) để các bài test sử dụng.


Bước 1: File global-setup.ts (Ông chủ mở cửa)

Nhiệm vụ: Tạo ra một cái URL kết nối Database và gán vào biến môi trường để cả dự án dùng chung.

// global-setup.ts

import { FullConfig } from '@playwright/test';


async function globalSetup(config: FullConfig) {

  console.log('🌅 [GLOBAL SETUP] Bắt đầu khởi động hệ thống...');


  // Giả sử ta khởi động một Server DB ảo mất 1 giây

  await new Promise(r => setTimeout(r, 1000));


  // QUAN TRỌNG: Truyền dữ liệu cho các bài Test bằng biến môi trường (Environment Variables)

  process.env.DB_CONNECTION_URL = 'postgres://admin:123456@localhost:5432/my_db';

  process.env.API_PORT = '8080';

  console.log('✅ [GLOBAL SETUP] Đã bật Server tại port 8080. Sẵn sàng!');

}

export default globalSetup;

 

Bước 2: File global-teardown.ts (Ông chủ đóng cửa)

Nhiệm vụ: Thông báo ngắt kết nối khi test xong.

// global-teardown.ts

import { FullConfig } from '@playwright/test';

async function globalTeardown(config: FullConfig) {

  console.log('🌇 [GLOBAL TEARDOWN] Hết giờ làm việc...');


  // Dọn dẹp biến môi trường (nếu cần)

  console.log(`🗑️ Đang ngắt kết nối khỏi: ${process.env.DB_CONNECTION_URL}`);

  console.log('👋 [GLOBAL TEARDOWN] Đã tắt Server. Tạm biệt!');

}


export default globalTeardown;


Bước 3: Khai báo trong playwright.config.ts

Nối dây cho 2 file trên vào hệ thống.

// playwright.config.ts

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


export default defineConfig({

  // 👇 KHAI BÁO Ở ĐÂY (Option 2)

  globalSetup: './global-setup.ts',

  globalTeardown: './global-teardown.ts',

  use: {

    baseURL: 'http://localhost:8080',

  },

});


Bước 4: File Test tests/example.spec.ts (Khách hàng)

Bài test này sẽ đọc dữ liệu mà Setup đã chuẩn bị.

// tests/example.spec.ts

import { test } from '@playwright/test';


test('Kiểm tra biến môi trường', async ({}) => {

  // Lấy dữ liệu mà Global Setup đã chuẩn bị

  const dbUrl = process.env.DB_CONNECTION_URL;

  const port = process.env.API_PORT;

  console.log(`🧪 [TEST CASE] Đang chạy test...`);

  console.log(`   --> Đang kết nối tới DB: ${dbUrl}`);

  console.log(`   --> Port hoạt động: ${port}`);


  // Kiểm tra xem Setup có làm ăn đàng hoàng không

  if (!dbUrl) throw new Error('❌ Lỗi: Không tìm thấy DB_URL. Setup chưa chạy?');

});


Kết quả khi chạy (
npx playwright test)

Bạn sẽ thấy Log chạy theo trình tự cực kỳ ngăn nắp:

Running 1 test using 1 worker

🌅 [GLOBAL SETUP] Bắt đầu khởi động hệ thống...

✅ [GLOBAL SETUP] Đã bật Server tại port 8080. Sẵn sàng!

  [chromium] › tests/example.spec.ts:3:5 › Kiểm tra biến môi trường

  🧪 [TEST CASE] Đang chạy test...

     --> Đang kết nối tới DB: postgres://admin:123456@localhost:5432/my_db

     --> Port hoạt động: 8080

🌇 [GLOBAL TEARDOWN] Hết giờ làm việc...

🗑️ Đang ngắt kết nối khỏi: postgres://admin:123456@localhost:5432/my_db

👋 [GLOBAL TEARDOWN] Đã tắt Server. Tạm biệt!

  1 passed (2.0s)

🎯 Tổng kết

  1. Global Setup: Là người đến sớm nhất. Dùng để thiết lập những thông số chung (như URL database, Port server, Key bí mật) và nhét chúng vào env (cái túi chung).
  2. Environment Variables (env): Là cách duy nhất để globalSetup (Option 2) "nhắn gửi" dữ liệu sang cho các file Test.
  3. Global Teardown: Là người về muộn nhất. Dùng để tắt những gì Setup đã bật.


Nhược điểm của global setup


Nhược điểm thứ nhất: "Chiếc Hộp Đen" (Black Box) ⬛

Đây là điểm yếu lớn nhất. globalSetup chạy hoàn toàn tách biệt với quy trình Test Runner chính.

  • Vấn đề: Khi Setup chạy, nó KHÔNG xuất hiện trong HTML Report.
  • Hậu quả:
    • Nếu Setup thành công: Bạn không biết nó đã làm gì, mất bao lâu.
    • Nếu Setup thất bại (Fail): Bạn chỉ nhận được một dòng lỗi khô khốc trên Terminal (console log).
    • "Mù tịt" thông tin: Không có Screenshot, không có Video quay lại, không có Trace Viewer. Bạn không thể biết lúc Setup lỗi thì màn hình (nếu có) trông như thế nào.

Ví dụ: Bạn viết setup kết nối DB. Nó báo lỗi "Connection Timeout". Nhưng bạn không biết do mạng lag hay do sai mật khẩu vì không có log chi tiết trong báo cáo.


Tình huống: Bạn dùng globalSetup để Login. Nhưng hôm đó mạng lag, hoặc nút Login bị đổi ID.

Code global-setup.ts:

import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {

  console.log('🌍 [SETUP] Đang mở trình duyệt để Login...');

  const browser = await chromium.launch(); // Tự bật browser

  const page = await browser.newPage();

  try {

    await page.goto('https://github.com/login');

   

    // ❌ LỖI: Giả sử dev đổi ID nút login từ 'commit-sign-in' thành 'btn-login-new'

    // Dòng này sẽ treo 30s rồi chết (Timeout)

    await page.click('input[name="commit-sign-in"]', { timeout: 5000 });

    await page.context().storageState({ path: 'auth.json' });

  } catch (error) {

    console.error('❌ Login thất bại!');

    throw error;

  } finally {

    await browser.close();

  }

}

export default globalSetup;


🔴
Kết quả bạn nhận được (Pain Point):

Khi mở Report (npx playwright show-report):

  1. Bạn thấy lỗi: TimeoutError: element handle.click: Timeout 5000ms exceeded.
  2. NHƯNG... BẠN KHÔNG CÓ GÌ CẢ:
    • 🚫 Không có Video: Bạn không xem lại được lúc đó trang web load xong chưa? Hay đang xoay vòng vòng?
    • 🚫 Không có Screenshot: Bạn không biết nút bấm có bị che bởi banner quảng cáo không?
    • 🚫 Không có Trace: Bạn không mở được UI để inspect DOM tại thời điểm chết.

👉 Kết luận: Bạn biết là "Timeout", nhưng bạn không biết tại sao. Bạn phải đoán mò. Đây chính là cái "Hộp Đen" nguy hiểm nhất.

 

  1. Nhược điểm thứ hai: Làm thủ công (Manual Labor) 🔨

Trong các bài test bình thường, bạn được Playwright "dâng tận miệng" các công cụ xịn xò như page, request, context (gọi là Fixtures).

Nhưng trong globalSetup cổ điển:

  • Vấn đề: Bạn KHÔNG dùng được Fixtures.
  • Hậu quả: Bạn phải tự làm mọi thứ từ con số 0.
    • Muốn dùng trình duyệt? Phải tự viết: const browser = await chromium.launch();
    • Muốn đóng trình duyệt? Phải nhớ viết: await browser.close(); (Quên là treo máy).
    • Code trở nên dài dòng, rườm rà và dễ mắc lỗi ngớ ngẩn (như quên đóng kết nối).
Ví dụ về file globalSetup khác theo Doc của playwright
import { chromium, type FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const { baseURL, storageState } = config.projects[0].use;
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(baseURL!);
  await page.getByLabel('User Name').fill('user');
  await page.getByLabel('Password').fill('password');
  await page.getByText('Sign in').click();
  await page.context().storageState({ path: storageState as string });
  await browser.close();
}

export default globalSetup;​

  • Quản lý tài nguyên kém: Nếu bạn quên dòng await browser.close(), tiến trình Chrome sẽ treo lơ lửng trong Task Manager, ngốn RAM (Memory Leak).
  • Không có Fixture: Bạn không được dùng ({ page, request }) tiện lợi nữa. Bạn phải tự launch, tự newPage.
  • Vấn đề: Hàm globalSetup là một hàm tách biệt. Nó không tự biết file config.ts có gì. Playwright phải truyền object config vào cho nó.
  • Tại sao phải import { type FullConfig }? Để TypeScript hiểu cái biến config kia có cấu trúc ra sao, giúp gợi ý code.
  • Điểm chết người: projects[0].use
    • Nó đang móc máy vào Project đầu tiên trong mảng projects để lấy baseURL.
    • 🤔 Nếu bạn đổi thứ tự Project trong config thì sao? Ví dụ bạn đưa project mobile lên đầu, mà mobile lại dùng URL khác? -> Code setup vỡ nát ngay lập tức.
    • Trong khi cách mới (Dependencies), nó tự động hiểu context của Project hiện tại.


Cú pháp
export default (Cổ lỗ sĩ)

export default globalSetup;

  • Vấn đề: Đây là cách viết module của Node.js thuần túy.
  • Nó bắt buộc file này chỉ được export DUY NHẤT một hàm.
  • Bạn không thể viết nhiều test(...) nhỏ lẻ trong này được. Nó chỉ là một cục logic to đùng chạy từ trên xuống dưới. Nếu login fail ở bước 2, bạn khó biết chính xác là do nút bấm hay do mạng, vì nó không chia thành các steps rõ ràng như test.

"Beware that globalSetup and globalTeardown lack some features" từ https://playwright.dev/docs/test-global-setup-teardown

  1. Nhược điểm thứ ba: Giao tiếp khó khăn (String Only) 🗣️

Làm sao để globalSetup chuyển dữ liệu (ví dụ: Token, User ID, List sản phẩm...) sang cho các file Test dùng?

  • Vấn đề: Cách duy nhất là nhét vào Biến môi trường (env).
  • Hậu quả: env chỉ chấp nhận CHUỖI (STRING).
    • Nếu bạn muốn truyền một Object phức tạp { id: 1, name: "A", roles: ["admin", "mod"] }.
    • Bạn phải stringify() ở Setup.
    • Rồi sang Test lại phải parse() ra.
    • Rất cực khổ và dễ lỗi format.

  1. Nhược điểm thứ tư: Thiếu linh hoạt (One Size Fits All) 🚫

globalSetup là "Global" - nghĩa là nó chạy 1 lần cho TẤT CẢ mọi thứ.

  • Vấn đề: Giả sử bạn có 2 dự án con:
    • Project A (Admin): Cần setup dữ liệu phức tạp.
    • Project B (Guest): Không cần setup gì cả.
  • Hậu quả: Với cách cũ, globalSetup vẫn cứ chạy kể cả khi bạn chỉ muốn chạy test cho Project B. Bạn rất khó để cấu hình kiểu "Setup riêng cho từng project".

Đây là nhược điểm khiến globalSetup trở nên vô duyên trong các dự án lớn nhiều modules.

Tình huống: Dự án của bạn có 2 loại test:

  1. Smoke Test (Chạy nhanh): Chỉ check trang chủ, không cần Database, không cần Login.
  2. E2E Test (Chạy kỹ): Cần kết nối Database nặng nề (mất 10 giây khởi động).

Code global-setup.ts (Setup nặng nề):

// global-setup.ts

async function globalSetup() {

  console.log('🐘 [SETUP] Đang khởi động Database khổng lồ (Mất 10s)...');

  // Giả lập setup nặng

  await new Promise(r => setTimeout(r, 5000));


  if (Math.random() > 0.5) {

     throw new Error('💥 Database bị sập bất ngờ!');

  }

  console.log('✅ Database đã sẵn sàng.');

}

export default globalSetup;


Code
playwright.config.ts:

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

export default defineConfig({

  globalSetup: './global-setup.ts', // 👈 Nó nằm lù lù ở đây

  projects: [

    { name: 'smoke-test', testMatch: /home.spec.ts/ }, // Test nhẹ

    { name: 'heavy-test', testMatch: /checkout.spec.ts/ }, // Test nặng

  ],

});


🔴
Vấn đề xảy ra (The Problem):

Hôm nay sếp bảo: "Em chạy nhanh cái Smoke Test cho anh xem trang chủ có sống không."

Bạn chạy lệnh:

npx playwright test --project=smoke-test

Kết quả đau lòng:

Plaintext

🐘 [SETUP] Đang khởi động Database khổng lồ (Mất 10s)...

❌ Error: 💥 Database bị sập bất ngờ!

Phân tích:

  1. Bạn chỉ muốn chạy cái test nhẹ nhàng (Smoke).
  2. Nhưng globalSetup VẪN CỨ CHẠY. Nó bắt bạn đợi 10s oan uổng.
  3. Tệ hơn, cái Database bị lỗi -> Làm fail luôn cả bài Smoke Test (mặc dù bài test này chả liên quan gì đến DB cả).

🎯 Tổng kết: Tại sao nên bỏ cách cũ?

Tiêu chí

Cách Cũ (globalSetup option)

Cách Mới (Project Depedencies)

Debug

😭 Khó (Không Video/Trace)

😎 Dễ (Có Video/Trace full HD)

Code

😓 Dài (Tự launch browser)

😍 Ngắn (Dùng page fixture)

Báo cáo

👻 Tàng hình (Không hiện)

📊 Rõ ràng (Hiện xanh/đỏ trong Report)

Truyền data

urm... process.env (String)

🚀 File JSON / Memory / Fixtures

 

"Cách Hiện Đại" (Project Dependencies).

Đây là cách mà tài liệu Playwright khuyên dùng (Recommended). Chúng ta sẽ dùng lại đúng kịch bản Login bị lỗi để bạn thấy sự khác biệt "một trời một vực" khi xem báo cáo (Report).

📂 1. Cấu hình (Setup)

Bước 1: File playwright.config.ts

Thay vì dùng globalSetup (option), ta khai báo nó thành một Project riêng biệt.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({

  // Bật tính năng quay phim chụp ảnh khi test chết (Fail)

  use: {

    trace: 'on-first-retry', // Hoặc 'retain-on-failure'

    screenshot: 'only-on-failure',

    video: 'retain-on-failure',

  },

  projects: [

    // 1️⃣ PROJECT SETUP (Đóng vai trò Global Setup)

    {

      name: 'setup',

      testMatch: /global\.setup\.ts/,

    },

    // 2️⃣ PROJECT CHÍNH (Phụ thuộc vào setup)

    {

      name: 'chromium',

      use: { ...devices['Desktop Chrome'] },

      dependencies: ['setup'], // 👈 Phải đợi thằng 'setup' chạy xong

    },

  ],

});


Bước 2: File
tests/global.setup.ts

Bây giờ nó là một file test thực thụ. Ta dùng test và page fixture như bình thường.

import { test as setup, expect } from '@playwright/test';

setup('Login setup', async ({ page }) => {

  console.log('🌍 [MODERN SETUP] Đang mở trình duyệt...');

  await page.goto('https://github.com/login');

  // ❌ CỐ TÌNH GÂY LỖI: ID nút bấm bị sai

  // Playwright sẽ đợi 30s rồi báo Timeout

  console.log('⏳ Đang cố bấm nút sai ID...');

  await page.click('#nut-nay-khong-ton-tai', { timeout: 5000 });


  // Lưu state (Dòng này sẽ không bao giờ chạy tới)

  await page.context().storageState({ path: 'auth.json' });

});

💥 2. Chuyện gì xảy ra khi FAIL?

Bạn chạy lệnh: npx playwright test

Terminal hiện lỗi (Giống cách cũ):

Running 1 test using 1 worker

  1) [setup] › tests/global.setup.ts:3:5 › Login setup ────────────

 

    TimeoutError: page.click: Timeout 5000ms exceeded.

    Call log:

      - waiting for locator('#nut-nay-khong-ton-tai')

 

    > 10 |   await page.click('#nut-nay-khong-ton-tai', { timeout: 5000 });

         |              ^


🔴 ĐIỂM KHÁC BIỆT LÀ Ở ĐÂY 👇

Bạn mở report lên: npx playwright show-report

Thay vì một dòng chữ vô hồn, bạn sẽ nhận được một "Bản hồ sơ vụ án" đầy đủ:

  1. Tab "Errors" (Lỗi)

Nó chỉ đích danh dòng code số 10 bị sai.

  1. Tab "Steps" (Các bước)

Nó hiện rõ quy trình:

  1. goto ✅ (Xanh - Đã xong)
  2. click ❌ (Đỏ - Chết tại đây)
  3. Tab "Traces" (Bằng chứng thép) 💎

Đây là thứ "ăn tiền" nhất. Bạn bấm vào hình cái icon Trace, một cửa sổ mới hiện ra:

  1. Timeline (Dòng thời gian): Bạn kéo chuột tua đi tua lại được như xem Youtube. Bạn sẽ thấy 5 giây đó trình duyệt đang làm gì.
  2. Screenshot (Ảnh chụp): Bạn nhìn thấy màn hình lúc đó.
    • Ví dụ: Bạn thấy trang web đã load xong, nút Login nằm lù lù đó, nhưng ID của nó là #btn-signin chứ không phải #nut-nay-khong-ton-tai.
  3. DOM Snapshot: Bạn có thể trỏ chuột vào các phần tử trên cái ảnh đó để kiểm tra (Inspect) ID, Class ngay trong report mà không cần chạy lại code.
  4. Network: Bạn kiểm tra xem API có bị 404/500 không.

🎯 Tổng kết so sánh khi gặp Lỗi (Failure)

Tính năng

Cách Cũ (globalSetup)

Cách Mới (Project Dependencies)

Cảm giác

Như đi mò kim đáy bể 🌑

Như xem camera an ninh 📹

Hình ảnh

Không có (Mù tịt)

Có Video & Screenshot

Hành động tiếp theo

Phải thêm console.log rồi chạy lại để đoán

Nhìn Trace -> Sửa code luôn

Debug DOM

Không thể

Inspect được ngay trên Report

Đây chính là lý do tại sao DOC khuyên dùng Option 2 (Project Dependencies). Nó biến việc debug setup (vốn dĩ rất đau đầu) trở nên dễ dàng như debug một bài test bình thường.


Phần 5: Deep Dive mô hình project phụ thuộc lưu state đăng nhập 

ây chính là "Bản thiết kế chuẩn SGK" của Playwright cho mô hình Setup - Test - Teardown.

Mô hình này giải quyết bài toán kinh điển: "Tôi muốn tạo User vào DB -> Test mua hàng -> Rồi xóa User đó đi dù test có fail hay pass".

Chúng ta sẽ có 3 file tương ứng với 3 giai đoạn.


📂 Cấu Trúc File

tests/
├── auth.setup.ts    (🟢 Giai đoạn 1: Tạo User & Login)
├── shop.spec.ts     (🔵 Giai đoạn 2: Test mua hàng)
└── auth.teardown.ts (🔴 Giai đoạn 3: Xóa User)


⚙️  Config: Trái tim điều phối (playwright.config.ts)

Đây là nơi thiết lập Sơ đồ phụ thuộc (Dependency Graph). Hãy chú ý kỹ thuộc tính teardown.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  reporter: 'html',

  projects: [
    // 🟢 GIAI ĐOẠN 1: SETUP (Người mở đường)
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
      // 👇 QUAN TRỌNG: Bảo Playwright biết ai là người dọn rác cho thằng setup này
      teardown: 'cleanup', 
    },

    // 🔴 GIAI ĐOẠN 3: TEARDOWN (Người dọn dẹp)
    {
      name: 'cleanup',
      testMatch: /auth\.teardown\.ts/,
      // Teardown không cần dependencies, nó được gọi bởi thằng 'setup'
    },

    // 🔵 GIAI ĐOẠN 2: TESTING (Nhân vật chính)
    {
      name: 'chromium',
      use: { 
        ...devices['Desktop Chrome'],
        storageState: 'user.json', // Dùng cookie do setup tạo ra
      },
      // 👇 Bắt buộc phải chờ thằng 'setup' xong mới được chạy
      dependencies: ['setup'], 
    },
  ],
});


📝  Nội dung chi tiết các File

🟢 File 1: tests/auth.setup.ts

import { test as setup, expect } from '@playwright/test';

setup('Tạo User và Login', async ({ page }) => {
  console.log('🟢 [SETUP] 1. Đang tạo User mới trong Database...');
  // Giả lập gọi API tạo user...
  
  console.log('🟢 [SETUP] 2. Đang thực hiện Login...');
  await page.goto('https://www.saucedemo.com/');
  await page.fill('#user-name', 'standard_user');
  await page.fill('#password', 'secret_sauce');
  await page.click('#login-button');
  
  // Kiểm tra login thành công
  await expect(page).toHaveURL(/inventory/);

  // Lưu lại trạng thái Login (Cookies)
  await page.context().storageState({ path: 'user.json' });
  console.log('🟢 [SETUP] ✅ Đã lưu cookie vào user.json');
});

🔵 File 2: tests/shop.spec.ts (Test chính)

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

test('Mua hàng (Đã Login sẵn)', async ({ page }) => {
  console.log('🔵 [TEST] Bắt đầu test mua hàng...');
  
  // Vào thẳng trang chủ (Sẽ tự nhận cookie từ user.json)
  await page.goto('https://www.saucedemo.com/inventory.html');
  
  // Assert: Chắc chắn là đã login rồi (Có nút Add to cart)
  await expect(page.locator('#add-to-cart-sauce-labs-backpack')).toBeVisible();
  
  console.log('🔵 [TEST] ✅ Test mua hàng thành công!');
});

🔴 File 3: tests/auth.teardown.ts

 
import { test as teardown } from '@playwright/test';

teardown('Dọn dẹp Database', async ({ }) => {
  console.log('🔴 [TEARDOWN] 🧹 Đang xóa User khỏi Database...');
  // Giả lập gọi API xóa user
  console.log('🔴 [TEARDOWN] ✅ Dọn dẹp hoàn tất!');
});


🎬  Phân Tích Luồng Chạy (Running Sequence)

Khi bạn gõ lệnh: npx playwright test

✅ Kịch bản A: Mọi thứ suôn sẻ (Happy Case)

  1. START: Playwright nhìn vào Config.

  2. SETUP: Chạy Project setup trước.

    • Output: 🟢 [SETUP] ... ✅ Đã lưu cookie.

  3. TEST: Setup xanh -> Kích hoạt Project chromium.

    • Output: 🔵 [TEST] ... ✅ Test mua hàng thành công!.

  4. TEARDOWN: Tất cả các Project phụ thuộc vào setup đã xong (ở đây là chromium). -> Kích hoạt Project cleanup.

    • Output: 🔴 [TEARDOWN] 🧹 Đang xóa User....

  5. END: Report báo cáo 3 tests Passed.

❌ Kịch bản B: Test Chính bị Fail (Lỗi phổ biến)

Giả sử file shop.spec.ts bị lỗi (bug giao diện).

  1. SETUP: Chạy ✅ Passed.

  2. TEST: Chạy Project chromium -> 💥 FAILED.

  3. TEARDOWN:

    • Playwright thấy setup có khai báo teardown: 'cleanup'.

    • Nó bảo: "Dù thằng chromium có chết hay sống, miễn là nó chạy xong thì tao phải gọi cleanup để dọn rác".

    • 👉 Project cleanup VẪN CHẠY ✅.

  4. END: Report báo cáo: Setup Pass, Test Fail, Teardown Pass. (Database sạch sẽ).

☠️ Kịch bản C: Setup bị Fail (Lỗi nghiêm trọng)



☠️ Kịch bản 3 (Chuẩn): Setup bị lỗi (Setup Failure)

Giả sử trong file auth.setup.ts bạn có 2 bước:

  1. Bước 1: Gọi API tạo User trong DB. ✅ (Thành công).

  2. Bước 2: Login vào web để lưu cookie. ❌ (Thất bại do timeout/lỗi mạng).

Lúc này, Setup Project báo FAILED.

Chuyện gì xảy ra tiếp theo?

  1. Tests Chính (Chromium/Firefox): Chắc chắn bị SKIPPED (Bỏ qua).

    • Lý do: Móng nhà chưa xong thì không ai cho xây tường cả.

  2. Teardown Project:SẼ CHẠY! 🏃

    • Lý do: Playwright hiểu rằng: "Thằng Setup tuy thất bại nhưng nó đã chạy được một nửa (Bước 1), tức là đã xả rác ra DB rồi. Phải gọi thằng dọn vệ sinh vào ngay!".

👉 Đây chính là điểm "ăn tiền" so với beforeAll/afterAll:

  • Nếu beforeAll chết, afterAll thường không chạy (hoặc worker crash thì chết hẳn).

  • Nhưng Project Setup chết, Project Teardown (vốn là một worker riêng biệt, khỏe mạnh) vẫn được triệu hồi để dọn dẹp.


🛡️ Vấn đề phát sinh: Teardown phải biết "Tự vệ" (Defensive Coding)

Vì Teardown luôn chạy kể cả khi Setup fail, nên nó sẽ gặp một tình huống trớ trêu:

  • Nếu Setup chết ngay từ dòng đầu tiên (Chưa tạo User).

  • Teardown chạy và cố xóa User -> Lỗi "User Not Found".

  • => Kết quả: Setup FailedTeardown cũng Failed nốt. (Report đỏ lòm 2 chỗ).

Vì vậy, khi viết code cho file teardown, bạn bắt buộc phải viết theo kiểu "Xóa nếu tồn tại, không có thì thôi".

💻 Code mẫu Teardown "Thông minh"


// tests/auth.teardown.ts
import { test as teardown } from '@playwright/test';

teardown('Dọn dẹp User', async ({ request }) => {
  console.log('🔴 [TEARDOWN] Đang cố gắng dọn dẹp...');

  // Giả sử logic xóa user
  const userId = 'user-123'; 

  // ✅ CÁCH VIẾT ĐÚNG: Dùng try-catch hoặc check trước khi xóa
  try {
    const response = await request.delete(`/api/users/${userId}`);
    
    if (response.ok()) {
      console.log('🔴 [TEARDOWN] ✅ Đã xóa user thành công.');
    } else if (response.status() === 404) {
      // Setup chết trước khi tạo user -> User không tồn tại
      console.log('🟡 [TEARDOWN] ⚠️ Không tìm thấy user để xóa (Có thể Setup đã fail từ đầu).');
    } else {
      console.error('🔴 [TEARDOWN] ❌ Lỗi khi xóa user:', response.status());
    }
  } catch (error) {
    console.log('🟡 [TEARDOWN] ⚠️ Lỗi kết nối khi dọn dẹp, nhưng bỏ qua để không fail bài test.');
  }
});​

💡 Tại sao cấu hình này lại "Đỉnh"?

  1. Không bao giờ quên dọn rác: Nếu bạn viết code dọn dẹp trong afterAll của file test, khi test crash giữa chừng, đôi khi afterAll không chạy. Nhưng dùng Project Teardown, nó hoạt động ở mức Global, đảm bảo an toàn hơn.

  2. Tái sử dụng: Nếu bạn thêm Project firefox, webkit, mobile... chỉ cần thêm dependencies: ['setup']. Một lần Setup dùng cho cả họ nhà trình duyệt.

Debug sướng: Setup, Test, Teardown là 3 file riêng biệt. Lỗi ở đâu mở Report ra thấy ngay ở đó, có video riêng cho từng giai đoạn.


 "Running Sequence" là gì

🚦 Quy Luật Vận Hành Của "Running Sequence"

Playwright không chạy test theo thứ tự dòng code bạn viết trong file config. Nó chạy theo Sơ đồ mạng lưới (Dependency Graph).

Hãy tưởng tượng sơ đồ này giống như quy trình Xây Nhà:

  1. Làm móng (Setup) phải xong thì mới được Xây tường (Test).

  2. Xây tường xong (dù đẹp hay xấu) thì mới được Dọn vệ sinh (Teardown) để bàn giao.


Dưới đây là 3 nguyên tắc bất di bất dịch:

1️⃣ Nguyên tắc 1: Setup là "Kẻ chặn cửa" (Blocking)

  • Logic: Những project nào được liệt kê trong mảng dependencies: [...] của project khác, nó tự động trở thành Setup Project.

  • Hành vi:

    • Nó luôn chạy ĐẦU TIÊN.

    • CHẶN (Block) tất cả các project phụ thuộc vào nó. Không một ai được phép chạy cho đến khi Setup báo Passed.

  • Nếu Setup thất bại (Failed):

    • Đây là điểm quan trọng nhất. Nếu Setup chết, Playwright sẽ KHÔNG chạy các test chính.

    • Trạng thái các test chính sẽ là: SKIPPED.

    • Lý do: Móng nhà đã sập thì xây tường làm gì cho tốn công?


2️⃣ Nguyên tắc 2: Các Test chính chạy "Đua" (Parallelism)

  • Logic: Sau khi Setup xanh (Passed), Playwright sẽ thả xích cho các Project phụ thuộc.

  • Hành vi:

    • Nếu bạn có 3 Project: chromium, firefox, webkit cùng phụ thuộc vào setup.

    • Ngay khi setup xong, CẢ 3 THẰNG NÀY SẼ CHẠY CÙNG LÚC (Song song).

    • Chúng không chờ nhau. Thằng nào xong trước về trước.


3️⃣ Nguyên tắc 3: Teardown là "Người bám đuôi" (Lifecycle)

  • Logic: Project Teardown được khai báo bằng thuộc tính teardown: 'tên-project' bên trong Project Setup.

  • Hành vi:

    • Nó kiên nhẫn chờ đợi. Chờ cái gì?

    • Nó chờ TẤT CẢ các project phụ thuộc vào Setup chạy xong.

    • Dù các test chính Passed hay Failed, Teardown VẪN CHẠY.

  • Lý do: Dù xây nhà xong hay xây hỏng, vẫn phải dọn rác, trả lại mặt bằng sạch sẽ.

🧪 Minh Họa Bằng Sơ Đồ Thời Gian (Timeline)

Giả sử cấu hình của bạn:

  • Setup: auth (Có teardown là cleanup)

  • Test: chromium, firefox (Phụ thuộc auth)

  • Teardown: cleanup

✅ Kịch bản 1: Mọi thứ hoàn hảo (Happy Path)

Time 0s:  [🟢 Auth Setup]  (Đang chạy...)
Time 5s:  [🟢 Auth Setup]  ✅ Done!
          |
          +---> [🔵 Chromium] (Bắt đầu chạy...)
          +---> [🔵 Firefox ] (Bắt đầu chạy...)
          |
Time 10s: [🔵 Chromium] ✅ Done!
Time 12s: [🔵 Firefox ] ✅ Done!
          |
          +---> (Tất cả con đã xong) -> Kích hoạt Teardown
          |
Time 13s: [🔴 Cleanup] (Bắt đầu dọn...)
Time 15s: [🔴 Cleanup] ✅ Done!


❌ Kịch bản 2: Test chính bị lỗi (Test Failure)

 
Time 0s:  [🟢 Auth Setup] ✅ Done!
          |
          +---> [🔵 Chromium] ❌ FAILED (Lỗi bug)
          +---> [🔵 Firefox ] ✅ Done!
          |
          +---> (Vẫn kích hoạt Teardown vì Setup đã thành công)
          |
Time 15s: [🔴 Cleanup] ✅ Done! (Vẫn dọn dẹp sạch sẽ)


☠️ Kịch bản 3: Setup bị lỗi (Setup Failure)

 
Time 0s:  [🟢 Auth Setup] ❌ FAILED (Lỗi mạng, timeout)
          |
          +---> [🔵 Chromium] ⏭️ SKIPPED (Không chạy)
          +---> [🔵 Firefox ] ⏭️ SKIPPED (Không chạy)
          |
          +---> [🔴 Cleanup]  ✅ Done! (Vẫn dọn dẹp sạch sẽ) 


💡 Điều này khác gì với beforeAll / afterAll?

  • beforeAll/afterAll (Trong file test): Chỉ chạy trong phạm vi 1 file hoặc 1 worker. Nếu Worker crash (sập), afterAll có thể không chạy -> Rác còn nguyên trong DB.

  • Project Setup/Teardown: Chạy ở mức Global System. Dù trình duyệt crash, Playwright Runner vẫn nắm quyền kiểm soát để gọi Teardown chạy -> An toàn hơn nhiều.

🎯 Tóm lại phần Running Sequence

Bạn chỉ cần nhớ câu thần chú này:

"Setup chặn cửa tất cả. Test chính thì đua nhau chạy. Teardown kiên nhẫn chờ tất cả xong mới vào dọn."

Đây chính là Running Sequence chuẩn của Playwright Projects.



🧪 Bài Lab: "Cái chết của Worker"

Bước 1: Tạo file test tests/crash-demo.spec.ts

Copy đoạn code này vào file mới:

 
import { test } from '@playwright/test';

test.describe('Mô phỏng Worker Crash', () => {

  // 1️⃣ SETUP (Chạy OK)
  test.beforeAll(async () => {
    console.log('🟢 [BEFORE ALL] 1. Đang tạo dữ liệu RÁC trong Database...');
    console.log('🟢 [BEFORE ALL] 2. Dữ liệu đã được tạo xong!');
  });

  // 2️⃣ TEST (Chỗ này sẽ gây sập)
  test('Test case này sẽ giết chết Worker', async ({ page }) => {
    console.log('🔵 [TEST] Đang chạy test...');
    
    // Giả vờ làm gì đó...
    await page.waitForTimeout(1000);

    console.log('💀 [CRASH] Giả lập lỗi Fatal Error! Worker sắp sập...');
    
    // 💥 LỆNH NÀY SẼ GIẾT CHẾT TIẾN TRÌNH NGAY LẬP TỨC
    // Giống như rút phích cắm điện, không có lời trăng trối
    process.exit(1); 
  });

  // 3️⃣ TEARDOWN (Hy vọng chạy dòng này để dọn rác)
  test.afterAll(async () => {
    // ❌ DÒNG NÀY SẼ KHÔNG BAO GIỜ HIỆN RA
    console.log('🔴 [AFTER ALL] 🧹 Đang dọn rác... (Nếu bạn thấy dòng này thì Worker chưa chết)');
  });

});

Bước 2: Chạy thử và quan sát hậu quả

Gõ lệnh chạy file này:

npx playwright test crash-demo.spec.ts

Bước 3: Phân tích kết quả (Hiện trường vụ án)

Bạn sẽ thấy Terminal hiện ra (hoặc ngắt quãng) kiểu như sau:

 
Running 1 test using 1 worker
[chromium] › tests/crash-demo.spec.ts:12:3 › Mô phỏng Worker Crash › Test case này sẽ giết chết Worker

🟢 [BEFORE ALL] 1. Đang tạo dữ liệu RÁC trong Database...
🟢 [BEFORE ALL] 2. Dữ liệu đã được tạo xong!
🔵 [TEST] Đang chạy test...
💀 [CRASH] Giả lập lỗi Fatal Error! Worker sắp sập...

Error: Process completed with exit code 1.


😱 BẠN THẤY GÌ?

  1. Log 🟢 BEFORE ALL: Có hiện. (Rác đã được tạo).

  2. Log 💀 CRASH: Có hiện.

  3. Log 🔴 AFTER ALL: MẤT TÍCH HOÀN TOÀN!

👉 Kết luận: Dữ liệu rác tạo ở bước 1 vẫn nằm nguyên trong Database. afterAll không kịp chạy vì Worker (cái xe chở nó) đã lao xuống vực rồi.


🛡️ Tại sao "Project Dependencies" lại cứu được ca này?

Nếu bạn chuyển sang dùng mô hình Project Setup/Teardown (Setup riêng, Test riêng, Teardown riêng) mà chúng ta bàn nãy giờ, kịch bản sẽ khác:

  1. Project Setup (Worker A): Chạy xong, tạo rác OK. ✅

  2. Project Test (Worker B):

    • Đang chạy thì bị process.exit(1) -> Worker B chết. 💀

    • Playwright Orchestrator (Ông trùm quản lý) đứng bên ngoài nhìn thấy: "Ô, thằng Worker B chết rồi."

  3. Project Teardown (Worker C):

    • Ông trùm Playwright vẫn sống. Ông ấy ra lệnh: "Dù thằng B chết, nhưng quy trình là vẫn phải dọn rác. Gọi thằng Worker C dậy đi làm!"

    • Worker C khởi động và chạy teardown. ✅

🎯 Tóm lại

  • afterAll: Ngồi cùng thuyền với Test. Thuyền chìm (worker crash) thì afterAll chết chìm theo -> Rác còn nguyên.

  • Project Teardown: Là đội cứu hộ trên bờ. Thuyền Test chìm thì đội cứu hộ vẫn đi vớt rác được -> Sạch sẽ.

Đó là lý do tại sao trong các hệ thống CI/CD lớn (nơi mà việc Crash do thiếu RAM xảy ra như cơm bữa), người ta bắt buộc phải dùng Project Dependencies.

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học