A PHP Error was encountered

Severity: Warning

Message: unlink(/home/anhtest2/public_html/application/cache/Category_model_SelectByParentArrayId.cache): No such file or directory

Filename: drivers/Cache_file.php

Line Number: 279

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Category_model.php
Line: 60
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 109
Function: SelectByParentArrayId

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

A PHP Error was encountered

Severity: Warning

Message: unlink(/home/anhtest2/public_html/application/cache/Config_model_SelectByKeyemail.cache): No such file or directory

Filename: drivers/Cache_file.php

Line Number: 279

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Config_model.php
Line: 42
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 121
Function: SelectByKey

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

A PHP Error was encountered

Severity: Warning

Message: file_get_contents(/home/anhtest2/public_html/application/cache/Config_model_SelectByKeyyoutube.cache): failed to open stream: No such file or directory

Filename: drivers/Cache_file.php

Line Number: 275

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Config_model.php
Line: 42
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 132
Function: SelectByKey

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

A PHP Error was encountered

Severity: Notice

Message: Trying to access array offset on value of type bool

Filename: drivers/Cache_file.php

Line Number: 277

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Config_model.php
Line: 42
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 132
Function: SelectByKey

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

A PHP Error was encountered

Severity: Warning

Message: file_get_contents(/home/anhtest2/public_html/application/cache/Config_model_SelectByKeybanner_home.cache): failed to open stream: No such file or directory

Filename: drivers/Cache_file.php

Line Number: 275

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Config_model.php
Line: 42
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 136
Function: SelectByKey

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

A PHP Error was encountered

Severity: Notice

Message: Trying to access array offset on value of type bool

Filename: drivers/Cache_file.php

Line Number: 277

Backtrace:

File: /home/anhtest2/public_html/application/core/MY_Model.php
Line: 25
Function: get

File: /home/anhtest2/public_html/application/models/Config_model.php
Line: 42
Function: query_cache

File: /home/anhtest2/public_html/application/core/MY_Controller.php
Line: 136
Function: SelectByKey

File: /home/anhtest2/public_html/application/controllers/frontend/Lesson.php
Line: 9
Function: __construct

File: /home/anhtest2/public_html/index.php
Line: 315
Function: require_once

[Playwright Typescript] Bài 12 - Bổ trợ kiến thức TS nâng cao: Từ syntax rời rạc đến design pattern 'Locator Factory' | Anh Tester



NỘI DUNG BÀI HỌC

🛠️ Phần 1: Các "Viên gạch" Nền tảng (Utility Types)

🔑 Phần 2: Phép biến hình dữ liệu (Type Operations)

🏹 Phần 3: Giải mã Cú pháp Hàm (Function Types)

🧠 Phần 4: Tư duy Logic & Trừu tượng (Core Concepts)

🚀 Phần 5: THỰC CHIẾN - Combo "Hủy Diệt" (The Killer Pattern)



Utility Types là gì?

Trong TypeScript, chúng ta thường định nghĩa các interface hoặc type cố định. Tuy nhiên, trong thực tế code, dữ liệu thường biến đổi linh hoạt.

  • Vấn đề: Đôi khi chúng ta cần một type giống hệt User, nhưng tất cả các trường đều không bắt buộc. Hoặc chúng ta cần một object map từ ID sang Data mà không muốn viết thủ công.
  • Giải pháp: TypeScript cung cấp Utility Types (các kiểu tiện ích) để biến đổi các type có sẵn thành type mới một cách tự động.

 

Partial<T> - Biến tất cả thành "Tùy chọn"

Khái niệm

Partial<T> tạo ra một type mới bằng cách sao chép type T nhưng biến tất cả các property (thuộc tính) của nó thành optional (có dấu ?).

Cú pháp:

Partial<TypeCanBienDoi>

Tại sao cần dùng?

Dùng nhiều nhất trong tính năng Update (Cập nhật). Khi bạn sửa thông tin user, bạn không cần gửi lên toàn bộ thông tin (như email, password, ngày tạo...), mà chỉ gửi những trường cần sửa.

Ví dụ minh họa

Bước 1: Định nghĩa type gốc

interface Product {

  id: number;

  name: string;

  price: number;

  description: string;

}

Bước 2: Vấn đề gặp phải (Nếu không dùng Partial)

// Hàm update này bắt buộc phải truyền đủ cả name, price, description

// Dù ta chỉ muốn sửa giá tiền.

function updateProduct(id: number, newData: Product) {

  // logic update

}




// Lỗi: Thiếu name, description...

updateProduct(1, { price: 20000 });

Bước 3: Giải pháp với Partial

// newData bây giờ có kiểu là { id?: number; name?: string; ... }

function updateProduct(id: number, newData: Partial<Product>) {

  console.log(`Updating product ${id} with data:`, newData);

  // Gọi API update...

}

// Hợp lệ: Chỉ update giá

updateProduct(1, { price: 20000 });

// Hợp lệ: Chỉ update tên và mô tả

updateProduct(1, { name: "Iphone 15", description: "Mới tinh" });

Record<K, T> - Tạo Object Key-Value an toàn

Khái niệm

Record<K, T> dùng để tạo ra một object type mà:

  • K (Key): Là danh sách các key hợp lệ (thường là string, number, hoặc union type).
  • T (Type): Là kiểu dữ liệu của value tương ứng với key đó.

Cú pháp:

Record<Keys, Type>

Tại sao cần dùng?

  1. Thay thế cho cú pháp lỏng lẻo index signature (ví dụ: {[key: string]: any}).
  2. Bắt buộc lập trình viên phải khai báo đầy đủ các trường hợp (khi Key là Union Type).

Ví dụ 1: Object Dictionary đơn giản (Map ID -> Data)

Giả sử bạn có một danh sách user, nhưng muốn truy xuất nhanh theo ID thay vì dùng mảng .find().

interface UserInfo {

  name: string;

  age: number;

}


// Key là string (ID), Value là UserInfo

const userMap: Record<string, UserInfo> = {

  "user_01": { name: "Tùng", age: 25 },

  "user_02": { name: "Hoa", age: 22 },

};

// Truy xuất cực nhanh

console.log(userMap["user_01"]);

Ví dụ 2: Ràng buộc Key chặt chẽ (Quyền lực của Record)

Đây là ví dụ "ăn tiền" của Record. Giả sử hệ thống có 3 vai trò (Role), bạn muốn định nghĩa cấu hình cho 3 vai trò này. Nếu thiếu 1 vai trò, code phải báo lỗi.


Kết hợp
RecordPartial: Combo thực tế

Trong Automaton, thay vì hardcode dữ liệu rải rác trong từng file test, chúng ta sẽ tổ chức dữ liệu tập trung.

Record: Giúp quản lý các bộ dữ liệu theo từng kịch bản (Scenario) cụ thể, tránh việc đặt tên biến lung tung.

Partial: Giúp tạo ra các bộ dữ liệu "biến thể" (chỉ chứa các trường cần thay đổi) mà không cần copy-paste lại toàn bộ object gốc.

Ví dụ: Test tính năng "Cập nhật hồ sơ người dùng" (Update Profile)

Giả sử form Profile có 4 trường: username, email, bio, avatarUrl.

Bước 1: Định nghĩa Interface gốc

Đây là khuôn mẫu đầy đủ của một User.

interface UserProfile {

  username: string;

  email: string;

  bio: string;

  avatarUrl: string;

}

Bước 2: Định nghĩa các kịch bản test (Keys)

Chúng ta dùng Union Type để liệt kê các trường hợp cần test. Việc này giúp code không bị "magic string" (gõ nhầm tên case).

type TestScenario =

  | "standard_update"   // Case cập nhật thông thường

  | "email_change_only" // Case chỉ đổi email

  | "long_bio"          // Case bio quá dài

  | "remove_avatar";    // Case xóa avatar

Bước 3: Tạo kho dữ liệu dùng Record và Partial

Đây là phần quan trọng nhất. Chúng ta khai báo biến testDataMap.

Key phải là TestScenario.

Value là Partial<UserProfile> (Nghĩa là: Dữ liệu test không bắt buộc phải đủ 4 trường, chỉ cần chứa trường muốn test).

const testDataMap: Record<TestScenario, Partial<UserProfile>> = {

  // Case 1: Cập nhật đầy đủ (Dùng gần hết các trường)

  standard_update: {

    username: "user_moi_123",

    email: "new_email@example.com",

    bio: "Đây là bio mới",

  },


  // Case 2: Chỉ muốn test đổi email, các cái khác giữ nguyên (Partial phát huy tác dụng)

  email_change_only: {

    email: "only_change_this@test.com"

  },

  // Case 3: Test boundary (giới hạn) của Bio

  long_bio: {

    bio: "A".repeat(1000) // Chuỗi dài 1000 ký tự

  },

  // Case 4: Xóa avatar (truyền chuỗi rỗng)

  remove_avatar: {

    avatarUrl: ""

  }

};

Bước 4: Viết Helper Function trong Playwright

Chúng ta cần một hàm updateProfile thông minh. Hàm này sẽ nhận vào Partial data, sau đó merge (gộp) với dữ liệu mặc định để điền vào form.

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

// Hàm này nhận vào "dataOverrides" - tức là chỉ những gì cần sửa

async function fillProfileForm(page: Page, dataOverrides: Partial<UserProfile>) {

  // 1. Dữ liệu mặc định (để đảm bảo form luôn được điền đủ nếu cần)

  const defaultData: UserProfile = {

    username: "default_user",

    email: "default@test.com",

    bio: "Default Bio",

    avatarUrl: "default.png"

  };

  // 2. Merge dữ liệu: Lấy default đè partial lên

  // Những trường nào trong dataOverrides có thì sẽ thay thế default

  const finalData = { ...defaultData, ...dataOverrides }

  // 3. Thực hiện thao tác với Playwright

  if (dataOverrides.username) {

      await page.fill('#username', finalData.username);

  }

  if (dataOverrides.email) {

      await page.fill('#email', finalData.email);

  }


  if (dataOverrides.bio) {

      await page.fill('#bio', finalData.bio);

  }


  await page.click('#save-btn');

}

Bước 5: Sử dụng trong Test Case

Bây giờ file test của bạn sẽ cực kỳ gọn gàng và dễ đọc.

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

test('Should update email successfully', async ({ page }) => {

  // Chỉ lấy đúng cục data dành cho case email

  const data = testDataMap.email_change_only;

  // Gọi hàm fill, hàm sẽ tự hiểu chỉ cần quan tâm email

  await fillProfileForm(page, data);

  // Assertion

  await expect(page.locator('#success-msg')).toBeVisible();

});


test('Should handle long bio validation', async ({ page }) => {

  const data = testDataMap.long_bio;

  await fillProfileForm(page, data);

  await expect(page.locator('#error-bio')).toHaveText('Bio too long');

});

 

typeof (Trong ngữ cảnh TypeScript)

Khác với JavaScript (trả về chuỗi "string", "number"), typeof trong TypeScript dùng để "chụp ảnh" kiểu dữ liệu của một biến đang tồn tại.

Tác dụng: Bạn lười viết interface? Bạn có sẵn một object mẫu? Dùng typeof để tự động sinh ra Type từ object đó.

const product = {

    id: 1,

    title: "Iphone 15",

    isStock: true

};


// Thay vì ngồi hì hục viết interface Product {...}

// Ta dùng typeof để trích xuất type từ biến product

type ProductType = typeof product;


// Kết quả của ProductType sẽ tương đương:

// { id: number; title: string; isStock: boolean; }

 

keyof

keyof là toán tử lấy ra danh sách các Keys (tên thuộc tính) của một Type (Lưu ý: Chỉ hoạt động trên Type, không hoạt động trên biến).

interface Staff {

    name: string;

    salary: number;

}

type StaffKeys = keyof Staff;

// Kết quả: "name" | "salary"

 

Tại sao phải kết hợp keyof typeof?

Đây là câu hỏi học viên hay thắc mắc nhất: "Tại sao không dùng keyof biencuatoi luôn cho nhanh?"

Vấn đề:

keyof chỉ ăn được Type (Interface, Type Alias).

Biến (Object const, let) là Value.

=> keyof không thể đứng trước một biến.

Giải pháp: Cần một "cây cầu" bắc ngang.

Dùng typeof để biến Value -> Type.

Dùng keyof để lấy key từ Type đó.

Công thức:

Biến Object (Value)  --->  typeof  --->  Type  --->  keyof  --->  Union Keys

Ví dụ thực tế trong Automation

Trong Automation, chúng ta hay có một file chứa danh sách URL hoặc Locators. Chúng ta muốn viết một hàm gotoPage mà chỉ chấp nhận các tên page có trong danh sách đó.

Bước 1: Khai báo dữ liệu cố định (Config)

Lưu ý: Trong thực tế nên dùng thêm as const để khóa chặt giá trị.

// Đây là một Object (Value), không phải Type

const APP_URLS = {

    home: '/dashboard',

    profile: '/user/profile',

    settings: '/settings/general',

    login: '/auth/login'

} as const; // as const giúp biến các value thành literal type (chính xác từng chữ)

Bước 2: Tạo Type động từ dữ liệu trên

 

// 1. Lấy type của object APP_URLS

type AppUrlsType = typeof APP_URLS;

 

// 2. Lấy danh sách key (home | profile | settings | login)

type PageName = keyof AppUrlsType;

 

// Viết tắt (Combo):

type PageNameShort = keyof typeof APP_URLS;

// Kết quả: "home" | "profile" | "settings" | "login"

Bước 3: Ứng dụng vào hàm Playwright

Bây giờ hàm của bạn cực kỳ thông minh. Nếu bạn thêm 1 url mới vào APP_URLS, hàm này tự động update type, không cần sửa interface nào cả.

async function navigateTo(pageName: PageName) {

    const url = APP_URLS[pageName];

    console.log(`Navigating to: ${url}`);

    // await page.goto(url);

}


// ✅ Hợp lệ - Code gợi ý (Intellisense) tự hiện ra list cho chọn

navigateTo('home');

navigateTo('settings');

// ❌ Lỗi ngay lập tức (Compile error)

navigateTo('admin'); // Lỗi: Argument of type '"admin"' is not assignable...

Kỹ thuật nâng cao: keyof typeof đi kèm as const

Nếu không có as const:

const LOCATORS = {

    btnSubmit: "#submit",

    inputUser: "#user"

};

// Type suy luận là: { btnSubmit: string; inputUser: string; }

// Key vẫn là "btnSubmit" | "inputUser" -> OK

Nhưng nếu bạn muốn lấy Value (ví dụ muốn lấy danh sách các chuỗi locator #submit | #user) thì bắt buộc cần as const.

Ví dụ lấy cả Key và Value:

const ERROR_MESSAGES = {

    REQUIRED: "Trường này là bắt buộc",

    INVALID_EMAIL: "Email không đúng định dạng",

    MIN_LENGTH: "Phải tối thiểu 6 ký tự"

} as const; // <--- QUAN TRỌNG: Khóa cứng giá trị, không cho thành string chung chung


// 1. Lấy Keys: "REQUIRED" | "INVALID_EMAIL" | "MIN_LENGTH"

type ErrorKeys = keyof typeof ERROR_MESSAGES;


// 2. Lấy Values: "Trường này là bắt buộc" | ... (Lấy các value thực tế)

type ErrorValues = (typeof ERROR_MESSAGES)[ErrorKeys];


// Ứng dụng: Hàm assert thông báo lỗi

function expectErrorMessage(msg: ErrorValues) {

    // Hàm này chỉ chấp nhận đúng 3 câu thông báo lỗi chuẩn đã quy định

}


expectErrorMessage("Trường này là bắt buộc"); // ✅ OK

expectErrorMessage("Lỗi linh tinh"); // ❌ Error


Closure là gì? (Giải thích bình dân)

Hãy tưởng tượng một hàm (Function) giống như một căn phòng.

Khi hàm chạy: Đèn bật sáng, biến được tạo ra.

Khi hàm chạy xong (return): Đèn tắt, phòng dọn sạch, tất cả biến bị xóa khỏi bộ nhớ. -> Đây là cơ chế bình thường.

Nhưng Closure thì khác:

Khi một hàm con được sinh ra trong hàm cha và được mang ra ngoài sử dụng, nó mang theo một "chiếc balo". Trong chiếc balo đó chứa tất cả các biến của hàm cha mà nó cần dùng. Dù hàm cha đã chạy xong, đã "tắt đèn", nhưng những biến đó vẫn sống mãi trong chiếc balo của hàm con.

Định nghĩa kỹ thuật:

Closure là sự kết hợp giữa một hàm và môi trường nơi nó được khai báo (Lexical Environment). Nó cho phép hàm truy cập vào scope của hàm cha ngay cả khi hàm cha đã thực thi xong.

Ví dụ kinh điển: Bộ đếm (Counter)

Đây là ví dụ dễ hiểu nhất để thấy Closure hoạt động.

function createCounter() {

    let count = 0; // Biến này nằm trong hàm cha (Local scope)




    // Hàm con được return ra ngoài

    return function() {

        count++; // Hàm con dùng biến count của cha

        console.log("Current count:", count);

    };

}


// 1. Chạy hàm cha. Lúc này createCounter chạy xong,

// Lẽ ra biến `count` phải bị hủy.

const myCounter = createCounter();


// 2. NHƯNG KHÔNG! myCounter vẫn nhớ `count` nhờ Closure.

myCounter(); // Output: Current count: 1

myCounter(); // Output: Current count: 2

myCounter(); // Output: Current count: 3


// 3. Tạo một counter mới -> Nó có một môi trường (balo) mới riêng biệt

const anotherCounter = createCounter();

anotherCounter(); // Output: Current count: 1 (Không liên quan đến cái trên)

Tại sao quan trọng?

Nếu bạn khai báo let count = 0 ở biến toàn cục (Global), bất kỳ ai cũng có thể sửa nó thành count = 1000. Nhưng với Closure, biến count được bảo vệ tuyệt đối bên trong hàm, chỉ có hàm con mới sửa được nó.

 

Function Factory (Nhà máy tạo hàm)

Closure giúp bạn viết code tổng quát (Generic) và dùng nó để tạo ra các hàm cụ thể. Kỹ thuật này gọi là Currying hoặc Partial Application.

Ví dụ: Tạo bộ log thông minh cho các môi trường khác nhau.

function createLogger(prefix: string) {

    return function(message: string) {

        console.log(`[${prefix}] ${message}`);

    };

}


// Tạo ra các hàm log chuyên biệt

const infoLog = createLogger("INFO");

const errorLog = createLogger("ERROR");

const playWrightLog = createLogger("PLAYWRIGHT");


// Sử dụng

infoLog("Server started");       // Output: [INFO] Server started

errorLog("Database failed");     // Output: [ERROR] Database failed

playWrightLog("Clicking button");// Output: [PLAYWRIGHT] Clicking button



Các mảnh ghép cú pháp (Syntax Blocks)

Chúng ta sẽ xây dựng một ví dụ đời thường: "Hệ thống lấy thông báo (Notification System)". Hệ thống này vừa chứa các câu thông báo tĩnh (cố định), vừa chứa các thông báo động (cần tham số).


Mảnh ghép 1: Record kết hợp Union Type (Cấu trúc dữ liệu hỗn hợp)

Bình thường, Record chỉ chứa 1 kiểu dữ liệu. Nhưng thực tế ta cần lưu trữ lẫn lộn: vừa là chuỗi (String), vừa là hàm (Function).

Cú pháp:

// Key luôn là string

// Value là: HOẶC là string, HOẶC là một Function nhận vào string trả về string

type SmartMessage = Record<string, string | ((name: string) => string)>;

Ví dụ thực tế:

const database: SmartMessage = {

    // Dạng 1: Giá trị tĩnh (String)

    success: "Thao tác thành công!",

   

    // Dạng 2: Giá trị động (Function)

    welcome: (name) => `Xin chào ${name}, chúc ngày mới tốt lành!`

};


Mảnh ghép 2: typeof (Cảnh sát giao thông tại Runtime)

Vì biến database ở trên chứa lẫn lộn string và function, nên khi lấy ra dùng, code sẽ không biết phải xử lý thế nào. Ta cần typeof để phân luồng.

Cú pháp:

const value = database['welcome']; // TypeScript chưa biết đây là String hay Function


if (typeof value === 'function') {

    // A ha! Đây là hàm, mình phải gọi nó ()

    console.log(value("Tùng"));

} else {

    // Ồ, đây chỉ là chuỗi bình thường, in ra thôi

    console.log(value);

}


Mảnh ghép 3: Generic <T> kết hợp keyof T (Siêu liên kết)

Đây là phần khó nhất nhưng quan trọng nhất. Làm sao để viết một hàm mà khi người dùng gõ get(...), nó tự động hiện ra danh sách key có trong object dữ liệu?

Công thức: Input là T --> Output chỉ nhận keyof T.

Cú pháp:

// <T> là cái khuôn, nó sẽ co giãn tùy theo dữ liệu data truyền vào

function createGetter<T>(data: T) {

    // Hàm trả về chỉ chấp nhận tham số 'key' nằm trong danh sách key của T

    return (key: keyof T) => {

        return data[key];

    };

}

Tại sao cần cái này?

Nếu không có nó, bạn có thể gõ get('cai_gi_do_sai_bet') mà code không báo lỗi. Có nó, gõ sai là đỏ lòm ngay.


Mảnh ghép 4: Closure (Chiếc balo ký ức)

Làm sao để hàm get bên trong truy cập được dữ liệu data đã truyền vào hàm cha createGetter từ đời nào rồi? Đó là nhờ Closure.

Cú pháp:

const makeWallet = (money: number) => {

    // Hàm con này được mang ra ngoài dùng, nhưng nó vẫn "nhớ" biến money của hàm cha

    return (cost: number) => {

        money = money - cost;

        console.log(`Còn lại: ${money}`);

    }

}


TỔNG HỢP: Ghép 4 mảnh thành "Combo Hủy Diệt"

Bây giờ, hãy ghép 4 mảnh trên thành một hàm xử lý thông báo hoàn chỉnh 

Bước 1: Chuẩn bị dữ liệu (Data Source)

Dùng as const để "đóng băng" dữ liệu, giúp TypeScript hiểu chính xác từng key.

 

const MESSAGES = {

    error404: "Không tìm thấy trang này",

    hello: (name: string) => `Hi ${name}, welcome back!`,

    diskSpace: (percent: number) => `Cảnh báo! Ổ cứng đã đầy ${percent}%`

} as const;


Bước 2: Viết hàm Factory (Kết hợp Generic + Closure + typeof)

// 1. GENERIC <T>: Chấp nhận object đầu vào bất kỳ (miễn là đúng format Record hỗn hợp)

function createMessageReader<T extends Record<string, string | ((...args: any[]) => string)>>(

    source: T // Mảnh ghép 1: Input

) {

    // 2. CLOSURE: Trả về hàm con, hàm này "nhớ" biến source

    // 3. KEYOF: Hàm con bắt buộc nhập đúng key có trong source

    return (key: keyof T, ...args: any[]) => {

        const value = source[key];


        // 4. TYPEOF: Kiểm tra xem value lấy ra là String hay Function

        if (typeof value === 'function') {

            // @ts-ignore: Bỏ qua check type chi tiết cho ví dụ đơn giản

            return value(...args);

        } 

        return value; // Nếu là string thì trả về luôn

    };

}

Bước 3: Hưởng thụ thành quả

// Khởi tạo máy đọc tin nhắn

const read = createMessageReader(MESSAGES);


// ✅ Case 1: Gọi key tĩnh (String) -> Output: "Không tìm thấy trang này"

console.log(read('error404'));


// ✅ Case 2: Gọi key động (Function) -> Output: "Hi Huy, welcome back!"

console.log(read('hello', 'Huy'));



// ❌ Case 3: Gõ sai key -> Lỗi ngay lập tức (nhờ keyof T)

// read('goodbye'); // Error: Argument of type '"goodbye"' is not assignable...

Kết luận phần Syntax Blocks:
Logic của đoạn code Playwright phức tạp kia thực chất chỉ là sự kết hợp của việc:

Định nghĩa Data hỗn hợp (Record).

Viết hàm nhận Data (Generic).

Trả về hàm con (Closure) chỉ nhận đúng Key (keyof).

Bên trong hàm con thì check kiểu (typeof) để xử lý.


The Ultimate Locator Factory Pattern

Phân tích "Vũ khí" (Thành phần Code)

Trước khi đi vào cách dùng, hãy "mổ xẻ" đoạn code. Mỗi từ khóa đều có một sứ mệnh quan trọng:

// 1. GENERIC <T>: Giúp hàm chấp nhận bất kỳ Object Locator nào, không cố định.

// 2. RECORD: Ràng buộc cấu trúc đầu vào (Key là string, Value là string HOẶC function).

protected createLocatorGetter<T extends Record<string, string | ((page: Page) => Locator)>>(

    locatorMap: T // 3. INPUT: Cái bản đồ chứa locator

): (locatorName: keyof T) => Locator { // 4. RETURN TYPE: Trả về 1 hàm, hàm này chỉ nhận key của T

 

    // 5. CLOSURE: Hàm con được return, "nhớ" biến locatorMap và this.page

    return (locatorName: keyof T): Locator => {

        const locatorDef = locatorMap[locatorName];

        // 6. TYPE GUARD (typeof runtime): Kiểm tra xem value là String selector hay Function

        if (typeof locatorDef === 'function') {

            return locatorDef(this.page); // Nếu là function thì gọi nó

        }

        return this.page.locator(locatorDef); // Nếu là string thì dùng page.locator()

    };

}


Tại sao đây là "Combo hủy diệt"?

Tính linh hoạt (Record & Union Type): Bạn có thể định nghĩa locator dạng chuỗi tĩnh (#id) hoặc dạng hàm động (page.getByRole...).

Gợi ý code tuyệt đối (Generic & keyof): Khi bạn gõ hàm trả về, IDE sẽ gợi ý chính xác từng key trong locatorMap. Gõ sai 1 chữ là lỗi ngay.

Gọn gàng (Closure): Logic xử lý typeof (string hay function) được giấu kín bên trong. Người dùng ở ngoài chỉ cần gọi get('key') là xong.

Triển khai thực tế: Xây dựng BasePage

Để combo này hoạt động, chúng ta cần đặt nó vào một class cha (BasePage) để các trang khác kế thừa.

 Setup Class Cha (BasePage)

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


// Định nghĩa kiểu dữ liệu cho Map locator (để code gọn hơn)

type LocatorMap = Record<string, string | ((page: Page) => Locator)>;


export class BasePage {

  constructor(protected page: Page) {}


  // "Hàm thần thánh" của bạn nằm ở đây

  protected createLocatorGetter<T extends LocatorMap>(locatorMap: T) {

    return (name: keyof T): Locator => {

      const def = locatorMap[name];

      if (typeof def === 'function') {

        return def(this.page);

      }

      return this.page.locator(def);

    };

  }

}

Ứng dụng: Xây dựng LoginPage (Nơi phép màu xảy ra)

Ở đây chúng ta sẽ dùng as const để khóa chặt object locator, giúp TypeScript suy luận chính xác từng key (Literal Types).

export class LoginPage extends BasePage {

  // 1. Định nghĩa MAP các locator

  // Dùng as const để biến nó thành Readonly và giữ nguyên giá trị chuỗi

  private static readonly LOCATORS = {

    usernameInput: '#username',                 // Dạng String đơn giản

    passwordInput: "input[name='password']",    // Dạng String

    submitButton: (page: Page) => page.getByRole('button', { name: 'Login' }), // Dạng Function (Complex locator)

    errorMessage: (page: Page) => page.locator('.error-msg').first()           // Dạng Function

  } as const;

  // 2. Khởi tạo "Getter" bằng Closure

  // this.get bây giờ là một hàm, nó "nhớ" cái LOCATORS ở trên

  private get = this.createLocatorGetter(LoginPage.LOCATORS);

  constructor(page: Page) {

    super(page);

  }

  // 3. Viết các hành động (Actions)

  async login(user: string, pass: string) {

    // KHI GÕ this.get('...') -> IDE SẼ TỰ ĐỘNG GỢI Ý:

    // "usernameInput" | "passwordInput" | "submitButton" | "errorMessage"


    await this.get('usernameInput').fill(user);

    await this.get('passwordInput').fill(pass);

    await this.get('submitButton').click();

  }


  async getErrorText() {

    return await this.get('errorMessage').textContent();

  }

}

Giải thích luồng chạy (Flow)

Điều gì thực sự diễn ra khi bạn gọi this.get('submitButton')?

Gọi hàm: this.get('submitButton') được kích hoạt.

Closure kích hoạt: Hàm get (là hàm con trong createLocatorGetter) lục lại "ký ức" để tìm biến locatorMap (chính là LoginPage.LOCATORS).

Truy xuất: Nó lấy giá trị của key submitButton.

Giá trị là: (page: Page) => page.getByRole(...).

Kiểm tra (Type Guard): typeof value === 'function'. -> ĐÚNG.

Thực thi: Nó gọi hàm đó, truyền this.page hiện tại vào.

Kết quả: Trả về một Locator chuẩn của Playwright để bạn .click().


Tại sao cách này tốt hơn cách truyền thống?

Cách truyền thống (Khai báo mỏi tay):

class OldLoginPage {

    // Phải khai báo từng property, lặp đi lặp lại chữ 'Locator'

    readonly username: Locator;

    readonly submitBtn: Locator;

   

    constructor(page: Page) {

        // Phải khởi tạo từng cái một

        this.username = page.locator('#user');

        this.submitBtn = page.getByRole('button');

    }

}

Cách dùng Combo Hủy Diệt:

Tập trung dữ liệu: Toàn bộ selector nằm gọn trong 1 object LOCATORS. Dễ sửa, dễ quản lý.

Không lặp code: Không cần khai báo readonly xyz: Locator hàng chục lần.

Intellisense cực mạnh: Nhờ keyof T và as const, bạn không bao giờ gọi sai tên locator.

Support Dynamic Locator: Hỗ trợ cả getByRole, getByText... chứ không chỉ mỗi string selector.

 

Khái niệm: "Bản thiết kế" của một hàm

Trong TypeScript, biến không chỉ lưu số (number) hay chuỗi (string), mà nó còn có thể lưu cả một hàm (function).

Khi chúng ta viết:

 

type MyFunction = () => Locator;

Đây KHÔNG PHẢI là code chạy (implementation).

Đây là BẢN THIẾT KẾ (Type Definition) quy định hình dáng của hàm.

Công thức đọc:

( Danh sách tham số đầu vào ) => Kiểu dữ liệu trả về (Output)

Các cấp độ ví dụ từ dễ đến khó

Cấp độ 1: Hàm không cần tham số (Basic)

Đây là cú pháp: () => string.

Ý nghĩa: "Tôi là một cái biến, tôi chứa một hàm. Hàm này không cần đầu vào, nhưng khi chạy xong chắc chắn sẽ nhả ra một string."

// 1. Định nghĩa Type (Bản thiết kế)

// Hàm này không nhận gì cả, và bắt buộc trả về string

type GreetingFunc = () => string;


// 2. Áp dụng vào biến thực tế

const getHello: GreetingFunc = () => {

    return "Hello World"; // ✅ Đúng, trả về string

};

const getNumber: GreetingFunc = () => {

    return 100; // ❌ LỖI: Type quy định trả về string, ông lại trả về number

};


Cấp độ 2: Hàm có tham số đầu vào

Ví dụ: (a: number, b: number) => number.

Ý nghĩa: "Hàm này cần nạp vào 2 số, và sẽ trả ra 1 số."

// Định nghĩa kiểu cho một hàm tính toán

type MathFunc = (a: number, b: number) => number;



// Gán hàm cộng

const add: MathFunc = (x, y) => x + y;




// Gán hàm nhân

const multiply: MathFunc = (x, y) => x * y;

Áp dụng vào bài toán Playwright 

Quay lại đoạn code "hủy diệt" của chúng ta:

Record<string, string | ((page: Page) => Locator)>

Hãy tách phần (page: Page) => Locator ra để giải thích.

Vị trí: Nó nằm ở phần Value của Record.

Ý nghĩa: Giá trị này là một hàm.

Input: Bắt buộc phải nhận vào một biến page (kiểu Page của Playwright).

Output: Bắt buộc phải trả về một Locator.

Tại sao lại cần cú pháp này?

Bởi vì có những Locator rất phức tạp, không thể viết bằng chuỗi String đơn giản (#id). Chúng ta cần quyền năng của biến page để tìm kiếm nâng cao (như page.getByRole, page.getByText).

Ví dụ so sánh trực quan:

// Định nghĩa Type

type LocatorFinder = (page: Page) => Locator;

// 1. Hàm tìm nút Submit (Phức tạp, cần dùng page)

const findSubmitBtn: LocatorFinder = (p) => {

    // p chính là 'page' được truyền vào

    return p.getByRole('button', { name: 'Submit' });

};


// 2. Hàm tìm nút Login

const findLoginBtn: LocatorFinder = (page) => {

    return page.locator('#login-btn');

};


Tóm tắt nhanh 

Khi nhìn thấy dấu => trong TypeScript:

Nếu nó nằm sau dấu hai chấm : hoặc trong type ... =, nó là Luật lệ (Type Definition).

Ví dụ: let myFunc: () => void; (Luật: Biến này phải là hàm, không trả về gì).

Nếu nó nằm sau dấu bằng = trong code logic, nó là Hàm thực thi (Arrow Function).

Ví dụ: const myFunc = () => { console.log("Hi") }; (Hành động: In ra chữ Hi).

Bảng so sánh:

Cú pháp

Loại

Ý nghĩa

() => Locator

Type

Đây là kiểu dữ liệu: Một hàm trả về Locator.

() => page.locator(...)

Value

Đây là giá trị: Một hàm đang thực hiện việc tìm Locator.

 

 

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