NỘI DUNG BÀI HỌC

🔗 Kiểm soát luồng chạy Fixture Chaining (Chuỗi phụ thuộc) theo quy tắc ngăn xếp LIFO (Vào trước - Ra sau).

🧩 Chiến lược Module hóa dự án: Chia nhỏ file con và hợp nhất tại index.ts bằng toán tử Spread (...).

🧬  So sánh và ứng dụng hàm mergeTests() để hợp nhất các Test Object độc lập (Fusion vs Mixing).

🏛️ Triển khai Pattern "Gatekeeper" (3 Tầng) để tự động hóa Authentication và quản lý Page Object tập trung.



Phần 1: Giải Mã "Hộp Đen" - Luồng Thực Thi & Vòng Đời (Lifecycle)


"Các bạn đã biết cách tạo Fixture. Nhưng khi chạy test, điều gì thực sự diễn ra? Tại sao Robot lại biết đứng chờ chúng ta ăn xong mới đi dọn dẹp?

Hôm nay chúng ta sẽ mổ xẻ Luồng thời gian (Execution Flow) để thấy cấu trúc 'Bánh Mì Kẹp' (Sandwich) của Playwright."

1. Mô hình "Bánh Mì Kẹp" (The Sandwich Model)

Hãy tưởng tượng một bài test hoàn chỉnh là một chiếc bánh mì kẹp thịt:

  • Lớp vỏ trên (Part A): Code chạy TRƯỚC khi test bắt đầu (Setup).
  • Lớp nhân thịt (Part B): Chính là Hàm Test của bạn.
  • Lớp vỏ dưới (Part C): Code chạy SAU khi test kết thúc (Teardown).

Fixture chính là người tạo ra cả lớp vỏ trên và lớp vỏ dưới, bao bọc lấy bài test của bạn.

2. Code Minh Họa: Nhật Ký Hành Trình

Để thấy rõ luồng chạy, chúng ta sẽ dùng console.log để theo dõi từng bước chân của Robot.

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

// 1. Định nghĩa Fixture với các Log

export const test = base.extend({

demoFlow: async ({}, use) => {

// --- GIAI ĐOẠN 1: SETUP ---

console.log('🅰️ [Fixture] Bắt đầu: Chuẩn bị môi trường');

const data = "DATA XỊN";

// --- GIAI ĐOẠN 2: HANDOVER ---

console.log('⏸️ [Fixture] TẠM DỪNG: Giao data cho Test và chờ...');

await use(data);

// 👉 Tại dòng này, Fixture "đóng băng" hoàn toàn!

// --- GIAI ĐOẠN 3: TEARDOWN ---

// Dòng này CHỈ chạy khi bài Test đã kết thúc (Pass hoặc Fail)

console.log('©️ [Fixture] Kết thúc: Dọn dẹp chiến trường');

},

});

// 2. Chạy bài Test

test('Test kiểm tra luồng chạy', async ({ demoFlow }) => {

console.log(`🅱️ [Test] Đang chạy test với: ${demoFlow}`);

});


3. Kết Quả Chạy (Output)

Khi chạy bài test trên, các bạn sẽ thấy thứ tự Log hiện ra màn hình như sau:

  1. 🅰️ [Fixture] Bắt đầu: Chuẩn bị môi trường
  2. ⏸️ [Fixture] TẠM DỪNG: Giao data cho Test và chờ...
  3. 🅱️ [Test] Đang chạy test với: DATA XỊN (Đây là lúc Fixture đang chờ)
  4. ©️ [Fixture] Kết thúc: Dọn dẹp chiến trường


4. Phân Tích Chuyên Sâu: Tại sao phải await use()?

Đây là phần kiến thức đắt giá nhất của bài này.

Tại sao không phải là return?

Nếu dùng return, hàm Fixture sẽ kết thúc ngay lập tức. Nó không thể quay lại để dọn dẹp (Teardown) được nữa.


Tại sao phải có await?

  • Bài test của bạn là một tác vụ bất đồng bộ (async). Nó có thể chạy mất 5 giây, 10 giây.
  • Lệnh await use(...) nói với Fixture rằng: "Hãy giữ kết nối! Đừng đóng trình duyệt, đừng xóa dữ liệu vội. Hãy đợi cho đến khi thằng Test kia chạy xong đã."

Cơ chế bảo vệ (Safety Net):

Điều tuyệt vời nhất là: Ngay cả khi Bài Test bị Lỗi (Fail) ở bước B, Playwright vẫn đảm bảo bước C (Teardown) luôn luôn được chạy.

  • Điều này giúp tránh việc rác dữ liệu tồn đọng hoặc trình duyệt bị treo không tắt được.

5. Tổng kết luồng chạy (Visual Flow)

Cần ghi nhớ sơ đồ chữ U này:

Fixture (Setup)

⬇️

[ await use() ] -----> Chuyển sang Test

⬇️ ⬇️

(Đứng chờ...) Chạy Test Logic

⬇️ ⬇️

Fixture (Teardown) <----- Test Kết thúc

🔗 Phần 2: Hiệu Ứng Domino - Kỹ Thuật Fixture Chaining

"Các bạn hãy tưởng tượng việc setup một góc làm việc. Để bật được cái Đèn Bàn, các bạn không thể cứ thế mà bật. Nó tuân theo một chuỗi dây chuyền vật lý:

  1. Phải có Nguồn Điện (Cầu dao tổng) trước.
  2. Sau đó phải cắm cái Ổ Cắm Nối vào nguồn điện.
  3. Cuối cùng mới cắm Đèn Bàn vào ổ nối và bật sáng.

Trong Playwright, khi Fixture C cần Fixture B, và Fixture B cần Fixture A, ta gọi đó là Fixture Chaining (Chuỗi phụ thuộc)."

1. Code Mô Phỏng: Hệ Thống Chiếu Sáng

Chúng ta sẽ dạy Robot 3 món: nguonDien (Gốc), oCam (Trung gian), và denBan (Đích).

Hãy chú ý cách denBan gọi oCam, và oCam gọi nguonDien.

Chúng ta sẽ khai báo kiểu dữ liệu trả về cho từng Fixture.

// Định nghĩa kiểu dữ liệu (Tên món ăn và thành phần của nó)

export type ElectricityFixtures = {

// nguonDien trả về số (ví dụ: 220V)

nguonDien: number;

// oCam trả về chuỗi (ví dụ: 'Ổ Lioa')

oCam: string;

// denBan trả về chuỗi (ví dụ: 'Đèn Đang Sáng')

denBan: string;

};


Chúng ta sẽ đưa ElectricityFixtures vào hàm base.extend để Playwright và TypeScript biết được kiểu dữ liệu của các Fixture.

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

// 1. Định nghĩa kiểu dữ liệu cho toàn bộ Fixtures

export type ElectricityFixtures = {

nguonDien: number;

oCam: string;

denBan: string;

};

// 2. Định nghĩa Fixtures (Sử dụng cú pháp extend chuẩn)

// 👇 QUAN TRỌNG: Gán kiểu cho extend<...>

export const test = base.extend<ElectricityFixtures>({




// --- MẮT XÍCH 1: NGUYÊN LIỆU GỐC (nguonDien: number) ---

nguonDien: async ({}, use) => {

console.log('⚡ [1. Tổng] Đóng cầu dao điện. Có điện!');

const vol = 220;

await use(vol); // Trả về số 220 (kiểu number)

console.log('⚡ [1. Tổng] Cắt cầu dao điện.');

},

// --- MẮT XÍCH 2: TRUNG GIAN (oCam: string) ---

// Playwright biết nguonDien là số (number)

oCam: async ({ nguonDien }, use) => {

console.log(`🔌 [2. Ổ Cắm] Cắm ổ nối vào nguồn ${nguonDien}V.`);

const loaiO = 'Ổ Lioa';

await use(loaiO); // Trả về chuỗi 'Ổ Lioa' (kiểu string)

console.log('🔌 [2. Ổ Cắm] Rút phích cắm ổ nối.');

},

// --- MẮT XÍCH 3: ĐÍCH ĐẾN (denBan: string) ---

// Playwright biết oCam là chuỗi (string)

denBan: async ({ oCam }, use) => {

console.log(`💡 [3. Đèn] Cắm đèn vào ${oCam} và Bật công tắc.`);

const trangThai = 'Đèn Đang Sáng';

await use(trangThai); // Trả về chuỗi 'Đèn Đang Sáng' (kiểu string)

console.log('💡 [3. Đèn] Tắt công tắc đèn.');

},

});


2. Sử Dụng Trong Test (Chỉ cần gọi món cuối)

Trong file test, Khách hàng chỉ cần gọi denBan để làm việc. Khách không cần quan tâm dây điện đi như thế nào.

test('Ngồi đọc sách', async ({ denBan }) => {

// LÚC NÀY ROBOT ĐANG ĐỨNG CHỜ...

console.log(`📖 Khách: ${denBan}, tranh thủ đọc sách thôi!`);

});

3. Giải Mã Luồng Chạy (Quy Tắc An Toàn Điện)

Khi chạy đoạn code trên, các bạn sẽ thấy thứ tự Log tuân thủ đúng quy tắc an toàn điện: "Cái gì bật sau cùng thì phải tắt đầu tiên".

Thứ tự Log hiển thị:

  1. ⚡ [1. Tổng] Đóng cầu dao... (Có điện mới làm việc tiếp)
  2. 🔌 [2. Ổ Cắm] Cắm ổ nối...
  3. 💡 [3. Đèn] Bật đèn...
  4. 📖 Khách: Đọc sách... (Test chạy)
  5. 💡 [3. Đèn] Tắt đèn. (Phải tắt đèn trước)
  6. 🔌 [2. Ổ Cắm] Rút ổ nối. (Rồi mới rút dây)
  7. ⚡ [1. Tổng] Cắt cầu dao. (Cuối cùng mới ngắt nguồn)

 

Fixture hoạt động như trò chơi xếp tháp gỗ:

  • Setup: Xếp từ dưới lên (1 -> 2 -> 3).
  • Teardown: Dỡ từ trên xuống (3 -> 2 -> 1). Không thể rút cái số 1 ra khi cái số 3 đang đứng trên nó được!


4. Tại sao kỹ thuật này quan trọng?

"Các bạn thử nghĩ xem, nếu không có Chaining (Chuỗi), code của chúng ta sẽ 'ngớ ngẩn' thế nào?"

  • Cách ngớ ngẩn (Không Chaining):
test('Đọc sách', async ({ nguonDien, oCam, denBan }) => {

// Test phải tự đi cắm dây điện?

// setup(nguonDien);

// setup(oCam);

// -> Bài test biến thành ông thợ điện!

});
  • Cách thông minh (Chaining):

Test chỉ cần gọi denBan. Mọi logic kết nối phức tạp bị giấu kín trong Fixture. Đây gọi là Tính Đóng Gói (Encapsulation).

🧩 Phần 3: Đại Chiến Hợp Thể - Quản Lý & Merge Nhiều File Fixture

Khi dự án lớn lên, cuốn sổ tay công thức sẽ dày cộp hàng nghìn trang. Nếu để tất cả vào một file, việc tìm kiếm sẽ là cơn ác mộng.

Giải pháp: Chia để trị (Modularization).

Hãy tưởng tượng các bạn quản lý một Nhà Hàng 5 Sao. Chúng ta sẽ chia ra các tổ chuyên biệt:

  1. Quầy Bar (bar.fixtures.ts): Chỉ chuyên pha đồ uống.
  2. Bếp Nóng (kitchen.fixtures.ts): Chỉ chuyên nấu món chính.
  3. Văn Phòng Quản Lý (index.ts): Nơi cầm thực đơn tổng hợp."


🛠️ BƯỚC 1: Viết Sổ Tay Cho Quầy Bar (File Con)

File: fixtures/bar.fixtures.ts

Nguyên tắc: Ở file con, chúng ta CHƯA tạo con Robot test vội. Chúng ta chỉ viết ra cái Công thức (Object Logic)Danh sách món (Type) thôi.

import { Page } from '@playwright/test'; // Nếu cần dùng Page

// 1. MENU QUẦY BAR (Định nghĩa kiểu dữ liệu)

// Để TypeScript hiểu: Món này trả về cái gì? (Ở đây là chuỗi string tên món)

export type BarMenu = {

traSua: string;

cafeSuaDa: string;

};

// 2. CÔNG THỨC PHA CHẾ (Logic Object)

// Lưu ý: Đây chỉ là một Object chứa các hàm, chưa phải là test.extend()

export const barRecipes = {

traSua: async ({}, use) => {

console.log('🧋 Bar: Đang lắc trà sữa...');

const monAn = "🧋 Trà sữa trân châu đường đen";

await use(monAn);

},

cafeSuaDa: async ({}, use) => {

console.log('☕ Bar: Đang pha cà phê...');

const monAn = "☕ Cà phê sữa đá Sài Gòn";

await use(monAn);

},

};


🍳 BƯỚC 2: Viết Sổ Tay Cho Bếp Nóng (File Con)

File: fixtures/kitchen.fixtures.ts

Tương tự, bếp nóng chỉ quan tâm đến các món ăn mặn. Code hoàn toàn độc lập với Quầy Bar.

// 1. MENU BẾP NÓNG

export type KitchenMenu = {

phoBo: string;

banhMi: string;

};

// 2. CÔNG THỨC NẤU ĂN

export const kitchenRecipes = {

phoBo: async ({}, use) => {

console.log('🍜 Bếp: Đang chan nước lèo...');

const monAn = "🍜 Phở bò tái nạm";

await use(monAn);

},

banhMi: async ({}, use) => {

console.log('🥖 Bếp: Đang nướng bánh mì...');

const monAn = "🥖 Bánh mì pate";

await use(monAn);

},

};


📑 BƯỚC 3: Hợp Nhất Tại "Văn Phòng Quản Lý" (File Tổng)

File: fixtures/index.ts

Đây là nơi phép màu xảy ra. Chúng ta tạo ra con Robot phục vụ chính thức và dạy nó học công thức từ cả 2 cuốn sổ tay trên.

Chúng ta sử dụng 2 kỹ thuật quan trọng:

  1. Toán tử & (Intersection): Để gộp danh sách món ăn (Type).
  2. Toán tử ... (Spread Operator): Để gộp công thức chế biến (Logic).
import { test as base } from '@playwright/test';

// Nhập công thức từ 2 tổ chuyên biệt

import { BarMenu, barRecipes } from './bar.fixtures';

import { KitchenMenu, kitchenRecipes } from './kitchen.fixtures';

// 1. GỘP MENU (Merge Types)

// Menu Nhà Hàng = Menu Bar + Menu Bếp

// Dấu '&' nghĩa là: Robot phải biết làm cả 2 loại này.

type NhaHangMenu = BarMenu & KitchenMenu;

// 2. TẠO ROBOT PHỤC VỤ (Merge Logic)

export const test = base.extend<NhaHangMenu>({


// Dùng dấu '...' để "đổ" tất cả hàm từ object con vào đây

...barRecipes, // Học hết công thức pha đồ uống

...kitchenRecipes, // Học hết công thức nấu món chính

});

// Export luôn 'expect' để các file test đỡ phải import lắt nhắt

export { expect } from '@playwright/test';


🍽️ BƯỚC 4: Khách Hàng Gọi Món (File Test)

File: tests/khach-an.spec.ts

Bây giờ khách hàng (Hàm Test) có thể gọi bất kỳ món nào từ cả 2 quầy, vì con Robot test này đã học hết rồi.

// ⚠️ QUAN TRỌNG: Import 'test' từ file tổng (index.ts)

// TUYỆT ĐỐI KHÔNG import từ '@playwright/test'

import { test } from '../fixtures/index';

test('Khách gọi Combo Sáng (Phở + Cafe)', async ({ phoBo, cafeSuaDa }) => {
// Gọi món từ Bếp Nóng

console.log(`😋 Khách ăn: ${phoBo}`);

// Gọi món từ Quầy Bar

console.log(`🥤 Khách uống: ${cafeSuaDa}`);

});

test('Khách gọi Combo Chiều (Bánh Mì + Trà Sữa)', async ({ banhMi, traSua }) => {

console.log(`😋 Khách ăn xế: ${banhMi} và ${traSua}`);

});


💡 Tóm Tắt Kỹ Thuật (Key Takeaways)

Để thực hiện "Đại Chiến Hợp Thể" thành công, cần nhớ 3 quy tắc vàng:

  1. File Con (Component): Chỉ chứa Object (công thức) và Type (menu). Không gọi lệnh base.extend.
  2. File Tổng (Index): Là nơi duy nhất dùng lệnh base.extend. Đây là nơi tạo ra con Robot thực sự.
  3. Toán tử Spread (...): Giúp chúng ta "copy-paste" code một cách tự động.
    • ...barRecipes tương đương với việc chép toàn bộ hàm trong barRecipes dán vào base.extend.

Bạn thấy cách tổ chức file như thế này có gọn gàng và dễ hiểu cho một dự án lớn không?

Việc export { expect } từ file index.ts không phải là bắt buộc về mặt kỹ thuật (code vẫn chạy nếu không có nó), nhưng nó là Best Practice để giúp code sạch hơn (Clean Code).

Hãy tưởng tượng fixtures/index.ts là "Văn Phòng Một Cửa".

1. Vấn đề: Nếu KHÔNG export expect

Mỗi lần viết file test, bạn sẽ phải import từ 2 nơi khác nhau (rất lắt nhắt và dễ nhầm):

// ❌ CÁCH CŨ (RƯỜM RÀ)

// 1. Phải nhớ import Robot 'test' từ nhà mình tự làm

import { test } from '../fixtures/index';

// 2. Phải nhớ import công cụ 'expect' từ nhà máy gốc

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

test('Ví dụ', async ({ page }) => {

await expect(page).toHaveTitle('Demo');

});

👉 Bất tiện: Bạn tốn 2 dòng import. Nếu bạn có 100 file test, bạn lãng phí 100 dòng code thừa. Ngoài ra, người mới rất dễ import nhầm test từ @playwright/test (Robot gốc) thay vì ../fixtures/index (Robot xịn).

2. Giải pháp: Văn phòng "Một Cửa" (Re-export)

Khi bạn thêm dòng export { expect } from '@playwright/test'; vào file tổng index.ts, bạn đang biến file này thành nơi cung cấp TRỌN GÓI.

// ✅ CÁCH MỚI (GỌN GÀNG)

// Chỉ cần import TẤT CẢ từ file index của chúng ta

import { test, expect } from '../fixtures/index';

test('Ví dụ', async ({ page }) => {

await expect(page).toHaveTitle('Demo');

});

👉 Lợi ích:

  1. Gọn code: Chỉ tốn 1 dòng import.
  2. Đồng bộ: Tất cả mọi thứ (test runner, fixture, assertion) đều lấy từ một nguồn duy nhất.
  3. Tránh nhầm lẫn: Bạn tạo thói quen cho team: "Cứ muốn lấy đồ nghề thì vào fixtures/index mà lấy, đừng đi đâu xa."

3. Lý do nâng cao (Future Proof)

Sau này, khi trình độ bạn lên cao, bạn sẽ muốn "độ" cả hàm expect nữa.

Ví dụ: Bạn muốn viết await expect(page).toBeLoggedInAs('Admin') (Playwright gốc không có hàm này).

Lúc đó, bạn sẽ viết lại (override) expect trong file index.ts. Nếu bạn đã tập thói quen import expect từ index.ts ngay từ đầu, thì sau này khi bạn nâng cấp expect, tất cả 100 bài test cũ sẽ tự động được hưởng tính năng mới mà không cần sửa dòng code nào.

Tóm lại: Dòng code đó giúp biến fixtures/index.ts thành Tổng hành dinh duy nhất của dự án.

🧬 Phần 4: Kỹ thuật "Hợp Thể" - Merging Tests vs Spread

"Các bạn tưởng tượng nhé:

  • Cách cũ: Chúng ta có 1 ông Bếp Trưởng. Ông ấy học thuộc lòng công thức pha chế (Bar) VÀ công thức nấu ăn (Kitchen). Ông ấy làm tất cả.
  • Cách mới: Chúng ta thuê 2 ông chuyên gia riêng biệt. 1 ông chuyên Bar, 1 ông chuyên Bếp. Sau đó chúng ta buộc 2 ông này lại thành một đội (Team).
    • Ông Bar không cần biết nấu phở.
    • Ông Bếp không cần biết pha trà sữa.
    • Nhưng khi Khách gọi, cả đội vẫn phục vụ đủ."

Kỹ thuật này trong Playwright gọi là mergeTests.

1. Triển Khai Code: Quy Trình Hợp Thể

Sự khác biệt lớn nhất nằm ở các File Con.

BƯỚC 1: Tạo Robot Quầy Bar (File Độc Lập)

File: fixtures/bar.ts

⚠️ Khác biệt: Ở đây ta dùng base.extend và tạo ra một biến test hoàn chỉnh (có thể chạy được luôn), chứ không chỉ là một cục data Object như bài trước.

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

// 🤖 Robot 1: Chuyên gia pha chế

// Chú ý: Ta export biến 'testBar' (đã extend xong xuôi)

export const testBar = base.extend({

traSua: async ({}, use) => {

await use('🧋 Trà Sữa Trân Châu');

}

});


BƯỚC 2: Tạo Robot Bếp Nóng (File Độc Lập)

File: fixtures/kitchen.ts

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

// 🤖 Robot 2: Chuyên gia nấu nướng

export const testKitchen = base.extend({

phoBo: async ({}, use) => {

await use('🍜 Phở Bò Tái');

}

});


BƯỚC 3: Hợp Thể Tại Tổng Hành Dinh

File: fixtures/index.ts

Chúng ta dùng hàm mergeTests để hàn gắn 2 con robot này lại.

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

import { testBar } from './bar';

import { testKitchen } from './kitchen';

// ✨ PHÉP MÀU: Hợp thể (Fusion)

// Tạo ra Siêu Robot 'test' có sức mạnh của cả hai

export const test = mergeTests(testBar, testKitchen);

// Export expect để dùng kèm

export { expect } from '@playwright/test';


2. Phân Tích Chuyên Sâu: Spread (...) vs mergeTests()

Đây là phần quan trọng nhất để bạn quyết định kiến trúc cho dự án.

Hãy so sánh chi tiết hai cách làm này:

Tiêu chí

Cách 1: Trộn lẫn (Spread ...) 

Cách 2: Hợp thể (mergeTests) 

Bản chất File Con

File con chỉ chứa Object (Nguyên liệu thô). Không thể chạy test độc lập từ file con này.

File con là Robot Test hoàn chỉnh (Đã extend). Có thể viết bài test ngay trong file con này cũng được.

Quản lý Type (Kiểu)

Thủ công. Bạn phải tự import TypeA, TypeB rồi nối lại: type Tong = TypeA & TypeB.

Tự động. mergeTests thông minh tự động gộp các kiểu dữ liệu lại. Bạn ít khi phải khai báo Type thủ công.

Sự phụ thuộc (Dependency)

Chặt chẽ (High Coupling). Vì trộn chung vào một nồi, nên món phoBo có thể dễ dàng gọi món traSua (nếu muốn).

Độc lập (Loose Coupling). Robot Bar và Robot Kitchen là 2 thực thể riêng. Việc gọi chéo nhau (Cross-fixture) khó khăn hơn và cần setup kỹ.

Xung đột tên

Nếu cả 2 file đều có món tên menu, file sau sẽ GHI ĐÈ file trước. (Nguy hiểm).

Playwright sẽ báo lỗi hoặc xử lý xung đột tốt hơn (tuỳ phiên bản), nhưng bản chất là tách biệt scope.

Thích hợp cho

Dự án Team nhỏ/Vừa. Code tập trung, dễ hiểu, dễ gọi qua lại.

Dự án Enterprise / Modular. Khi các module (Bar, Kitchen) do các team khác nhau viết, hoặc import từ thư viện bên ngoài.


3. Khi nào nên dùng cái nào?

✅ Chọn Cách 1 (Spread ...) khi:

  • Bạn là người mới bắt đầu hoặc team quy mô nhỏ (< 5 người).
  • Các Fixture của bạn có liên quan mật thiết (Ví dụ: userPage cần dùng loginPage).
  • Bạn muốn kiểm soát toàn bộ Type và Logic tại một file trung tâm (index.ts).
  • Đây là cách phổ biến nhất (90% dự án dùng cách này).

✅ Chọn Cách 2 (mergeTests) khi:

  • Dự án cực lớn, chia thành nhiều module độc lập (Module Thanh Toán, Module Kho, Module User...).
  • Bạn viết một thư viện Fixture (Library) để chia sẻ cho nhiều dự án khác nhau dùng.
  • Bạn muốn tách biệt hoàn toàn để tránh việc code của team A làm hỏng code của team B.


4. Code Test (Sử dụng giống hệt nhau)

Dù bạn dùng cách nào thì ở phía người dùng (File Test), trải nghiệm là như nhau.

import { test } from '../fixtures/index';

test('Dùng thử Siêu Robot', async ({ traSua, phoBo }) => {

// Robot tự biết:

// - traSua lấy từ phân hệ Bar

// - phoBo lấy từ phân hệ Kitchen

console.log(traSua);

console.log(phoBo);

});


Tóm lại:

Cách 1 (...) giống như Nấu Lẩu: Bỏ hết nguyên liệu vào một nồi. Ngon, hòa quyện, nhưng dễ lẫn mùi.

Cách 2 (mergeTests) giống như Cơm Hộp Bento: Mỗi món nằm một ngăn riêng. Gọn gàng, sạch sẽ, nhưng các món khó trộn lẫn vào nhau.

Với dự án học tập hoặc dự án vừa phải của chúng ta, mình khuyên dùng Cách 1 (Spread ...) vì tính linh hoạt cao của nó.


Phần 5: Kiến trúc Fixture trong Playwright - Cuộc Cách mạng về Quản lý Trạng thái và Page Object Model

1. Mở đầu: Cuộc khủng hoảng trong Kiểm thử Tự động và Sự trỗi dậy của Fixture

Trong lịch sử phát triển của kỹ thuật kiểm thử phần mềm tự động (Test Automation), một trong những thách thức dai dẳng và tốn kém nhất không nằm ở việc tương tác với các phần tử giao diện (UI elements), mà nằm ở việc quản lý trạng thái (State Management)môi trường kiểm thử (Test Environment). Các thế hệ công cụ kiểm thử trước đây như Selenium WebDriver hay các framework dựa trên JUnit/TestNG thường tiếp cận vấn đề này thông qua các hooks vòng đời (lifecycle hooks) như beforeAll, beforeEach, afterEach, và afterAll. Mặc dù mô hình này trực quan, nhưng khi áp dụng vào các dự án quy mô lớn với hàng nghìn kịch bản kiểm thử (test cases) chạy song song, nó bộc lộ những điểm yếu chí tử: sự chia sẻ trạng thái không an toàn dẫn đến hiện tượng "flaky tests" (test chập chờn), mã nguồn lặp lại (boilerplate code) khó bảo trì, và sự thiếu linh hoạt trong việc thiết lập môi trường riêng biệt cho từng nhóm test cụ thể.

Playwright, công cụ kiểm thử thế hệ mới do Microsoft phát triển, đã giới thiệu một khái niệm kiến trúc mang tính đột phá để giải quyết triệt để các vấn đề trên: Test Fixtures. Không đơn thuần là một tính năng tiện ích, Fixture trong Playwright đại diện cho sự thay đổi tư duy từ lập trình mệnh lệnh (imperative programming) sang mô hình khai báo (declarative) dựa trên Dependency Injection (Tiêm phụ thuộc). 


2. Cơ sở Lý thuyết: Fixture và Nguyên lý Dependency Injection

2.1. Định nghĩa và Bản chất Kỹ thuật

Trong khoa học máy tính và kỹ thuật kiểm thử, một "Test Fixture" (đồ gá kiểm thử) được định nghĩa là một trạng thái cố định của một tập hợp các đối tượng được sử dụng làm cơ sở để chạy các bài kiểm thử. Mục đích của fixture là đảm bảo rằng tất cả các bài kiểm thử đều được thực thi trong một môi trường đã biết trước và cố định, từ đó đảm bảo tính lặp lại (repeatability) của kết quả kiểm thử.

Trong Playwright, Fixture được nâng tầm thành cơ chế cốt lõi để cung cấp tài nguyên. Playwright Test runner hoạt động dựa trên nguyên lý Inversion of Control (IoC). Thay vì người viết test phải tự tay khởi tạo trình duyệt, mở context, tạo page mới và dọn dẹp chúng sau khi test kết thúc, họ chỉ cần khai báo "tôi cần một page" hoặc "tôi cần một loggedInUser" trong tham số của hàm test. Playwright sẽ tự động phân tích nhu cầu, khởi tạo các đối tượng đó (setup), tiêm chúng vào hàm test, và tự động thu hồi tài nguyên (teardown) khi test hoàn tất.

2.2. So sánh Kiến trúc: Hooks truyền thống vs. Playwright Fixtures

Để hiểu rõ giá trị của Fixture, ta cần so sánh nó với phương pháp hooks truyền thống. Sự khác biệt không chỉ nằm ở cú pháp mà còn ở khả năng cô lập và hiệu năng.

Đặc điểm

Hooks truyền thống (before/afterEach)

Playwright Fixtures

Cơ chế khởi tạo

Chạy vô điều kiện cho mọi test trong scope (file/describe block).

On-demand (Theo yêu cầu): Chỉ khởi tạo khi test khai báo cần sử dụng.

Phạm vi (Scope)

Thường gắn liền với cấu trúc file test.

Linh hoạt, có thể định nghĩa toàn cục và tái sử dụng (reusable) ở bất kỳ đâu.

Quản lý dọn dẹp

Tách rời logic Setup (before) và Teardown (after), dễ dẫn đến rò rỉ tài nguyên nếu code phức tạp.

Đóng gói (Encapsulation): Setup và Teardown nằm trong cùng một hàm, đảm bảo tính nguyên vẹn.

Khả năng kết hợp

Khó khăn khi một setup phụ thuộc vào một setup khác, thường dẫn đến "callback hell" hoặc biến toàn cục.

Composable (Có thể kết hợp): Fixture A có thể gọi Fixture B, Playwright tự động giải quyết dependency graph.

Hiệu năng

Có thể lãng phí tài nguyên nếu setup chạy cho test không cần đến nó.

Tối ưu hóa tuyệt đối, chỉ tốn chi phí cho những gì thực sự được dùng.


2.3. Lợi ích Chiến lược

Việc áp dụng Fixture mang lại các lợi ích vĩ mô cho dự án:

  1. Tính cô lập tuyệt đối (Isolation): Mỗi test nhận được một phiên bản fixture mới (đối với test-scoped fixture), đảm bảo không có tác dụng phụ (side-effects) từ test trước ảnh hưởng đến test sau. Đây là chìa khóa để loại bỏ flaky tests.
  2. Tuân thủ nguyên tắc DRY (Don't Repeat Yourself): Thay vì viết lại logic đăng nhập hay khởi tạo DB trong mỗi file test, ta định nghĩa logic đó một lần trong fixture và dùng lại hàng nghìn lần.
  3. Tự động hóa quản lý vòng đời: Giảm thiểu rủi ro quên đóng kết nối DB hoặc quên xóa dữ liệu tạm, vì cơ chế Teardown được kích hoạt tự động ngay cả khi test thất bại.


3. Phân tích Cú pháp và Cơ chế Hoạt động (Lifecycle)

3.1. Cấu trúc Giải phẫu của một Fixture

Cú pháp tạo fixture trong Playwright sử dụng hàm test.extend(). Điểm độc đáo nhất của fixture chính là việc sử dụng pattern "Wrap" (bao bọc) thông qua hàm use.

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

// Định nghĩa kiểu dữ liệu cho Fixture

type MyFixtures = {

database: DatabaseConnection;

};

export const test = base.extend<MyFixtures>({

database: async ({ /* dependencies */ }, use) => {

// --- GIAI ĐOẠN 1: SETUP ---

console.log('1. Khởi tạo kết nối DB...');

const db = await connectToDatabase();

// --- GIAI ĐOẠN 2: EXECUTION ---

// Chuyển quyền điều khiển và giá trị cho Test Function

await use(db);

// --- GIAI ĐOẠN 3: TEARDOWN ---

// Code tại đây chạy SAU KHI test kết thúc (dù pass hay fail)

console.log('3. Đóng kết nối DB...');

await db.close();

},

});


3.2. Phân Tích Chuyên Sâu: Cơ Chế await use()

Đây là phần quan trọng nhất cần nắm vững. Hàm use không chỉ đơn giản là trả về giá trị (return), mà nó là một điểm kiểm soát luồng (Flow Control Point).


Bản chất kỹ thuật

Về mặt kỹ thuật, await use(value) hoạt động giống như yield trong các Generator Function của JavaScript/Python. Nó thực hiện hai nhiệm vụ:

  1. Inject: Tiêm giá trị value vào hàm test (hoặc fixture khác đang cần nó).
  2. Pause & Wait: Tạm dừng thực thi fixture tại dòng đó và chờ cho đến khi hàm test chạy xong hoàn toàn.


Luồng thực thi chi tiết (Execution Flow)

Hãy hình dung quá trình này như một chiếc bánh Sandwich (Mô hình Setup - Test - Teardown):

  1. Playwright bắt đầu: Chạy code phần Setup (trước use).
  2. Gặp lệnh await use(db):
    • Playwright đóng băng fixture database ngay tại dòng này.
    • Playwright chuyển quyền điều khiển sang chạy Test Body.
    • Test Body chạy các lệnh (expect, click, v.v...) sử dụng biến db.
  3. Test Body kết thúc (Pass hoặc Fail):
    • Playwright quay lại fixture database.
    • Playwright "rã đông" và tiếp tục chạy code ngay sau dòng await use(db).
  4. Teardown: Chạy code phần Teardown (đóng kết nối, xóa dữ liệu).


Các quy tắc sống còn với use

  • Quy tắc 1: Luôn phải có await: Nếu bạn viết use(db) mà thiếu await, Playwright sẽ không đợi test chạy xong mà chạy tọt xuống phần Teardown ngay lập tức. Điều này khiến kết nối DB bị đóng trong khi test vẫn đang cố gắng đọc dữ liệu -> Lỗi "Connection closed".
  • Quy tắc 2: Auto-Teardown kể cả khi Test Fail: Đây là tính năng an toàn mạnh mẽ nhất. Nếu Test Body bị lỗi (Assertion Error), Playwright vẫn đảm bảo quay lại chạy phần code sau use() để dọn dẹp.
    • Ngoại lệ: Nếu lỗi xảy ra trong giai đoạn Setup (trước use), thì Teardown sẽ KHÔNG chạy (vì tài nguyên chưa được khởi tạo thành công).
  • Quy tắc 3: Không dùng try/catch bao quanh use (trừ khi có lý do đặc biệt): Playwright tự động xử lý việc bắt lỗi của Test Body. Bạn không cần bọc use trong try/catch để đảm bảo teardown chạy. Teardown luôn chạy mặc định.


3.3. Thứ tự Thực thi và Dependency Graph (Đồ thị phụ thuộc)

Khi một test yêu cầu nhiều fixture, Playwright xây dựng một đồ thị phụ thuộc (DAG).

Giả sử Test cần A và C. A lại phụ thuộc B.

Quy trình thực thi sẽ là:

  1. Setup B (Vì A cần B).
  2. Setup A (Sau khi B đã sẵn sàng).
  3. Setup C (Độc lập).
  4. Chạy Test Body.
  5. Teardown C.
  6. Teardown A.
  7. Teardown B (LIFO - Last In, First Out).

Quy tắc LIFO trong giai đoạn Teardown cực kỳ quan trọng. Nó đảm bảo rằng nếu A phụ thuộc vào B, thì A sẽ được dọn dẹp xong xuôi trong khi B vẫn còn tồn tại.


3.4. Phân loại Scope: Test vs. Worker

Playwright cung cấp hai cấp độ phạm vi (scope) cho fixture.

Test Scope (Mặc định)

  • Cơ chế: Khởi tạo mới cho mỗi bài test và bị hủy ngay sau khi test đó kết thúc.
  • Ưu điểm: Đảm bảo tính cô lập cao nhất.
  • Nhược điểm: Tốn kém tài nguyên nếu setup nặng.

Worker Scope

  • Cơ chế: Khởi tạo một lần cho mỗi tiến trình Worker và tái sử dụng.
  • Khai báo: { scope: 'worker' }.
  • Ứng dụng: Kết nối DB, Docker container, login server-side.
  • Rủi ro: Cần đảm bảo worker fixture là bất biến (immutable) để tránh "State Pollution" giữa các test.11


4. Các Fixture Tích hợp sẵn (Built-in Fixtures)

Trước khi tạo fixture tùy chỉnh, cần hiểu rõ các công cụ mà Playwright cung cấp sẵn.1

Fixture

Kiểu dữ liệu

Chức năng và Vai trò

browser

Browser

Đại diện cho một instance của trình duyệt. Worker-scoped.

context

BrowserContext

Môi trường duyệt web cô lập (tương đương chế độ Incognito). Test-scoped.

page

Page

Một tab hoặc cửa sổ trình duyệt nằm trong context. Test-scoped.

request

APIRequestContext

Dùng để thực hiện các gọi API độc lập với UI.


5. Page Object Model (POM) trong Kỷ nguyên Fixture

Page Object Model (POM) là một design pattern kinh điển, nhưng Playwright Fixture cung cấp một cách tiếp cận "Test-First" hiện đại hơn cho POM.

5.1. Sự bất cập của mô hình POM truyền thống

Trong cách tiếp cận cũ, mã nguồn test thường lặp lại việc khởi tạo: const loginPage = new LoginPage(page);. Điều này vi phạm nguyên tắc DRY và làm test script bị phụ thuộc vào cách khởi tạo object.

5.2. Giải pháp Fixture: Dependency Injection cho POM

Chuyển việc khởi tạo POM vào trong Fixture giúp đạt được mô hình Zero-Configuration. Test script chỉ cần khai báo và sử dụng.

Quy trình Convert từ POM thường -> Fixture POM

Bước 1: Giữ nguyên Class Page Object

// pages/LoginPage.ts

export class LoginPage {

constructor(public page: Page) {}

async login(user: string, pass: string) { /*... */ }

}


Bước 2: Tạo Fixture Wrapper (Lớp trung gian)

Tạo file fixtures/index.ts:

// fixtures/index.ts

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

import { LoginPage } from '../pages/LoginPage';

import { DashboardPage } from '../pages/DashboardPage';

// 1. Định nghĩa Interface

type AppFixtures = {

loginPage: LoginPage;

dashboardPage: DashboardPage;

};

// 2. Mở rộng test object

export const test = base.extend<AppFixtures>({

loginPage: async ({ page }, use) => {

// Setup: Khởi tạo POM

const loginPage = new LoginPage(page);


// Pass to test: Dùng await use

await use(loginPage);


// Teardown: (Không cần thiết với POM thuần, nhưng có thể thêm nếu cần)

},

dashboardPage: async ({ page }, use) => {

await use(new DashboardPage(page));

},

});

export { expect } from '@playwright/test';


Bước 3: Refactor Test Script

// tests/login.spec.ts

import { test, expect } from '../fixtures'; // Import từ custom fixture

test('User login success', async ({ loginPage, dashboardPage }) => {

// KHÔNG CẦN: const loginPage = new LoginPage(page);


await loginPage.login('admin', 'secret');

await expect(dashboardPage.welcomeMessage).toBeVisible();

});

 

🏛️ Phần 6: Best paractice: Kiến Trúc 3 Tầng & Người Gác Cổng (Gatekeeper)

 

"Tưởng tượng công ty của chúng ta là một tòa nhà bảo mật cao.

  1. Tầng 1 (Sảnh chờ): Ai cũng vào được (Trang Login).
  2. Tầng 2 (Cổng an ninh): Phải quẹt thẻ (Đăng nhập thành công).
  3. Tầng 3 (Các phòng ban): Phòng Dashboard, Phòng Kế toán... Muốn vào đây thì bắt buộc phải đi qua Cổng an ninh (Tầng 2) trước."

Hôm nay chúng ta sẽ code lại đúng quy trình này để Playwright tự động đăng nhập giúp chúng ta.

📂 Cấu Trúc File Hệ Thống

Chúng ta sẽ chia nhỏ trách nhiệm ra 3 file chính trong thư mục fixtures/:

  1. auth.fixtures.ts: (Tầng 1 & 2) Chứa logic Login và tạo ra "Người gác cổng".
  2. app.fixtures.ts: (Tầng 3) Chứa công thức tạo các trang nghiệp vụ (Dashboard, Customer...).
  3. gatekeeper.fixture.ts: (Tổng hành dinh) Nơi hợp nhất tất cả lại để Test sử dụng.


🛠️ Bước 1: Xây Dựng Cổng An Ninh (auth.fixtures.ts)

Ở đây chúng ta tạo ra fixture quan trọng nhất: authedPage. Nó đóng vai trò là cái Thẻ Nhân Viên. Bất kỳ trang nào muốn hoạt động đều phải xin cái thẻ này.

import { test as base, Page } from '@playwright/test';

import { CRMLoginPage } from '../pom/crm/CRMLoginPage';

// 1. MENU CỔNG AN NINH

export type AuthFixtures = {

loginPage: CRMLoginPage; // Trang Login (Chưa đăng nhập)

authedPage: Page; // Trang Đã Đăng Nhập (Quan trọng!)

};

// 2. LOGIC

export const auth = base.extend<AuthFixtures>({


// --- TẦNG 1: Trang Login thuần túy ---

loginPage: async ({ page }, use) => {

await use(new CRMLoginPage(page));

},

// --- TẦNG 2: GATEKEEPER (Người Gác Cổng) ---

// Fixture này nhận vào 'loginPage' và trả về 'page' đã login

authedPage: async ({ loginPage, page }, use) => {

console.log('🔐 [Gatekeeper] Đang kiểm tra an ninh...');


// Thực hiện hành động Login

await loginPage.goto();

await loginPage.login('admin@example.com', '123456');

await loginPage.expectLoggedIn();

console.log('✅ [Gatekeeper] Đăng nhập thành công! Mời vào.');


// TRẢ VỀ: Cái 'page' này giờ đã có Cookies xịn

await use(page);

},

});


🏢 Bước 2: Xây Dựng Các Phòng Ban (app.fixtures.ts)

Đây là nơi các liệt kê các trang web của ứng dụng.

Quy tắc vàng: Các trang này KHÔNG xin page trống. Chúng phải xin authedPage (đã login).

import { Page, PlaywrightTestArgs } from '@playwright/test';
import { AuthFixtures } from './auth.fixture';

// Import các Page Object Model (POM)
import { CRMDashboardPage } from '../pom/CRMDashboardPage';
import { CRMCustomerPage } from '../pom/CRMCustomerPage';
import { CRMNewCustomerPage } from '../pom/CRMNewCustomerPage';

// 1. ĐỊNH NGHĨA MENU (Output)
// Đây là danh sách các món mà file này cung cấp
export type AppFixtures = {
  dashboardPage: CRMDashboardPage;
  customersPage: CRMCustomerPage;
  newCustomerPage: CRMNewCustomerPage;
};

// 2. ĐỊNH NGHĨA NGUYÊN LIỆU ĐẦU VÀO (Input)
// 👉 Giải thích: Để nấu được các món trên, ta cần 2 nguồn nguyên liệu:
//    - PlaywrightTestArgs: Đồ có sẵn của Playwright (page, browser, request...)
//    - AuthFixtures: Đồ của file Auth (authedPage, loginPage...)
type AppDeps = PlaywrightTestArgs & AuthFixtures;

// 3. LOGIC (Implementation)
// ⚠️ Lưu ý: Ta khai báo là object thường, KHÔNG dùng ': Fixtures<...>'
// Lý do: Để tránh lỗi xung đột Type khi dùng toán tử spread (...) ở file khác.

export const appFixtures = {
  
  // Món 1: Dashboard
  dashboardPage: async ({ authedPage }: AppDeps, use: (r: CRMDashboardPage) => Promise<void>) => {
    // Robot khởi tạo POM và giao cho bài test
    await use(new CRMDashboardPage(authedPage));
  },

  // Món 2: Customers
  customersPage: async ({ authedPage }: AppDeps, use: (r: CRMCustomerPage) => Promise<void>) => {
    await use(new CRMCustomerPage(authedPage));
  },

  // Món 3: New Customer
  newCustomerPage: async ({ authedPage }: AppDeps, use: (r: CRMNewCustomerPage) => Promise<void>) => {
    await use(new CRMNewCustomerPage(authedPage));
  },
};

🔍 Giải Mã Chi Tiết Cú Pháp (Deep Dive)

Tại sao dòng code này lại dài thế? Hãy mổ xẻ nó:

async ({ authedPage }: AppDeps, use: (r: CRMDashboardPage) => Promise<void>)

A. Tham số thứ nhất: ({ authedPage }: AppDeps)

  • Ý nghĩa: Đây là "Túi Nguyên Liệu Đầu Vào".

  • Vấn đề: Vì ta không dùng Generic Fixtures<...>, nên TypeScript không biết trong cái túi {} kia có gì. Nó coi là any.

  • Giải pháp: Ta ép kiểu bằng AppDeps.

    • AppDeps = PlaywrightTestArgs & AuthFixtures: Nghĩa là cái túi này chứa tất cả đồ nghề của Playwright (page, headless...) tất cả đồ nghề của Auth (authedPage).

    • { authedPage }: Ta thò tay vào túi, chỉ lấy đúng cái authedPage ra dùng.

B. Tham số thứ hai: use: (r: CRMDashboardPage) => Promise<void>

  • Ý nghĩa: Đây là "Hàm Giao Hàng".

  • Cơ chế: Trong Playwright, hàm use là một callback. Khi bạn gọi use(X), thì X sẽ được chuyển đến bài test.

  • Cú pháp:

    • r: Viết tắt của Result (Kết quả).

    • : CRMDashboardPage: Bắt buộc cái món giao đi phải đúng là kiểu này. Nếu bạn giao nhầm new LoginPage() -> Code báo lỗi đỏ ngay.

    • => Promise<void>: Quy định của Playwright, hàm use luôn trả về Promise.


🔗 Bước 3: Hợp Nhất Hệ Thống (gatekeeper.fixture.ts)

Đây là bước chúng ta gộp menu của "Cổng an ninh" và menu "Phòng ban" lại thành một.

// Import Con Robot 'auth' (đã biết login) từ file auth.fixtures

import { auth, AuthFixtures } from './auth.fixtures';

// Import danh sách phòng ban

import { appFixtures, AppFixtures } from './app.fixtures';

// 1. TỔNG HỢP MENU

// Menu Tổng = Menu Auth + Menu App

export type GatekeeperFixtures = AuthFixtures & AppFixtures;

// 2. NÂNG CẤP ROBOT

// Lấy con Robot 'auth' -> Dạy thêm các món trong 'appFixtures'

export const test = auth.extend<GatekeeperFixtures>({

...appFixtures

});

// Xuất khẩu 'expect' để dùng tiện luôn

export { expect } from '@playwright/test';

 

🎯 Tổng kết

Cách viết này ("Manual Typing") có ưu điểm tuyệt đối là Sự Tường Minh (Explicit).

  1. Nhìn vào hàm dashboardPage, bạn biết ngay nó cần AppDeps (nguyên liệu) và trả về CRMDashboardPage (sản phẩm).

  2. Tránh được "địa ngục Type" (Type Hell) khi extend nhiều tầng lớp.

  3. Đây là cách an toàn nhất cho các dự án lớn, nhiều file fixture chồng chéo nhau.



🌊 Bước 4: Luồng Chạy Thực Tế (Execution Flow)

Bây giờ hãy xem điều gì xảy ra khi viết bài test này:

// Import từ file tổng hợp gatekeeper

import { test } from './fixtures/gatekeeper.fixture';

test('Tạo khách hàng mới', async ({ newCustomerPage }) => {

await newCustomerPage.fillCompany('Tesla');

});


Chi tiết dòng chảy dữ liệu (Domino Effect):

  1. Test Start: Test gọi món newCustomerPage.
  2. App Fixture: Robot nhìn công thức newCustomerPage.
    • "Cần authedPage để làm món này!" -> Robot đi tìm authedPage.
  3. Auth Fixture: Robot nhìn công thức authedPage.
    • "Cần loginPage để làm món này!" -> Robot đi tìm loginPage.
  4. Login Page: Robot tạo ra new CRMLoginPage(page).
  5. Quay ngược lại Auth Fixture (Thực thi):
    • Robot dùng loginPage để điền user/pass.
    • Login thành công!
    • Trả page (đã login) về cho người gọi.
  6. Quay ngược lại App Fixture (Thực thi):
    • Robot nhận page (đã login).
    • Khởi tạo new CRMNewCustomerPage(page).
    • Trả về cho bài Test.
  7. Test Run: Bài test bắt đầu chạy với trang web đã login sẵn sàng!

 

Logic ở đây chính là sự Kế thừa (Inheritance)Phụ thuộc (Dependency).

Hãy để mình giải thích bằng tư duy đời thường để bạn thấy tại sao BẮT BUỘC phải lấy con Robot Auth ra để dạy tiếp (extend), chứ không thể lấy con Robot gốc (Base) được.

1. Quy Luật: "Có Bột Mới Gỡ Nên Hồ"

Hãy nhìn vào công thức nấu món dashboardPage trong file app.fixture.ts:

dashboardPage: async ({ authedPage }: AppDeps, use) => { ... }

👉 Yêu cầu: Để tạo ra trang Dashboard, nguyên liệu bắt buộc là authedPage (Trang đã đăng nhập).

  • Robot Gốc (base): Trong túi nó chỉ có page (trang trắng), browser. Nó KHÔNG CÓ authedPage.

    • => Nếu bạn bắt Robot Gốc học Dashboard, nó sẽ hét lên: "Tao đào đâu ra authedPage cho mày bây giờ?"

  • Robot Auth (authFixtures): Trong túi nó đã có sẵn món authedPage (do ta đã dạy nó ở tầng trước).

    • => Nếu bạn bắt Robot Auth học Dashboard, nó bảo: "Ok, tao có authedPage đây rồi, đưa vào nấu Dashboard thôi!"

2. Mô Hình Tiến Hóa Của Robot

Hãy tưởng tượng quy trình nâng cấp Robot giống như việc đi học:

  • Level 0 (Base Robot):

    • Giống như đứa trẻ sơ sinh.

    • Chỉ biết: Mở mắt (page), cầm nắm (request).

  • Level 1 (Auth Robot):

    • Ta lấy đứa trẻ (Base) + Dạy kỹ năng "Đăng nhập".

    • Kết quả: Robot Bảo Vệ. Nó biết tạo ra authedPage.

  • Level 2 (App Robot - Gatekeeper):

    • Ta lấy chú Bảo Vệ (Auth) + Dạy kỹ năng "Nghiệp vụ" (Vào Dashboard, Tạo khách hàng).

    • Lý do: Muốn làm nghiệp vụ thì phải qua cửa bảo vệ trước.

    • Kết quả: Siêu Robot. Biết cả Login lẫn Nghiệp vụ.


3. Điều gì xảy ra nếu ta làm sai?

Nếu ở file gatekeeper.fixture.ts, bạn viết nhầm thành:

// ❌ SAI LẦM: Extend từ 'base' (Robot sơ sinh)
export const test = base.extend({
  ...appFixtures
});

Ngay lập tức, khi chạy test:

  1. Playwright đọc thấy dashboardPage cần authedPage.

  2. Nó lục trong túi của base. Không thấy.

  3. Nó lục trong appFixtures. Cũng không thấy (vì app chỉ dùng chứ không tạo ra auth).

  4. BÙM! Lỗi: Fixture "authedPage" is undefined.

🎯 Tóm lại

Logic là:

Cái Sau (App) cần thành quả của Cái Trước (Auth).

Nên ta phải lấy Cái Trước làm nền tảng để phát triển Cái Sau.

Đó là lý do ta viết: auth.extend(app).



💡 Tổng Kết Bài Học

Tại sao cách làm này "Pro"?

  1. Viết 1 lần, dùng mãi mãi: Login logic chỉ nằm duy nhất ở auth.fixtures.ts. Sửa pass 1 chỗ, cả dự án tự cập nhật.
  2. Lazy Loading (Lười biếng thông minh): Nếu bài test của không gọi các page cần login (ví dụ test trang Forgot Password), Robot sẽ KHÔNG tự động login. Nó chỉ làm việc khi thực sự cần thiết.
  3. Code Test siêu sạch: Không còn beforeEach, không còn loginPage.login(). Chỉ còn nghiệp vụ thuần túy.

 

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