NỘI DUNG BÀI HỌC

  • Tư duy "Anti-Fragil
  • Thuật toán "Smart Map" & Normalization


Phần 1: XỬ LÝ BẢNG ĐỘNG (DYNAMIC TABLE) & SMART MAPPING

Trước khi nói về giải pháp, chúng ta phải hiểu tại sao cách làm truyền thống lại "chết" nhanh chóng trong các dự án thực tế.

Cách làm cũ trông như thế nào?

Trong cách làm cũ (Legacy), Tester thường đếm tay vị trí cột trên màn hình và code cứng (hardcode) con số đó vào script.

// ❌ CÁCH CŨ: Dựa vào số thứ tự (index)

// Giả sử: Cột 1 là Checkbox, Cột 2 là Company, Cột 3 là Phone

const companyName = await row.locator('td:nth-child(2)').textContent();

const phone = await row.locator('td:nth-child(3)').textContent();


Bất cập chí mạng (Why it fails?)

Giả sử ngày mai, khách hàng yêu cầu Dev chèn thêm cột "Status" vào giữa Company và Phone.

Lúc này trên UI:

Checkbox

Company

Status (MỚI)

Phone (Bị đẩy xuống vị trí 4)

Hậu quả trong Code:

Dòng lệnh lấy phone (td:nth-child(3)) bây giờ sẽ lấy nhầm dữ liệu của cột Status.

Kết quả: Test fail, hoặc tệ hơn là Test pass nhưng dữ liệu sai (False Positive).

Bảo trì: Bạn phải đi tìm tất cả các file test, sửa số 3 thành số 4. Nếu có 100 test case dùng bảng này, bạn sửa 100 lần.

Kết luận: Dựa vào index cứng là xây nhà trên cát. UI thay đổi, Test sụp đổ.


GIẢI PHÁP - SMART COLUMN MAP (BẢN ĐỒ CỘT THÔNG MINH)

Thay vì nhớ "số nhà" (index), chúng ta dạy cho Automation Robot cách đọc "biển tên đường" (Header).

Nguyên lý hoạt động

Robot đọc toàn bộ Header của bảng.

Robot ghi nhớ: "À, cột 'Company Name' đang nằm ở vị trí số 2".

Khi Test Script hỏi: "Lấy cho tôi cột Company", Robot tra cứu trí nhớ và tự tìm đến vị trí số 2.

Phân tích chi tiết từng Method trong Code của bạn

Chúng ta sẽ đi sâu vào file TableColumnHelpers.ts và CRMCustomersPage.ts để xem giải pháp này được hiện thực hóa như thế nào.

Method createColumnMapSimple: Trái tim của giải pháp

Đây là method nằm trong Helper, chịu trách nhiệm quét Header và lập bản đồ.

Phân tích code:

export async function createColumnMapSimple(headers: Locator): Promise<ColumnMap> {

  const count = await headers.count();

  const map: ColumnMap = {};

  for (let index = 0; index < count; index++) {

    // BƯỚC 1: Đọc và làm sạch text

    // Ví dụ raw: "  Company   Name  " -> clean: "Company Name"

    const rawText = await headers.nth(index).innerText();

    const clean = cleanHeaderText(rawText);

    const info = { index, text: clean };

    // BƯỚC 2: Tạo nhiều KEY để map linh hoạt (QUAN TRỌNG)

    // Key CamelCase (Dành cho Dev): "Company Name" -> "companyName"

    // Giúp code test sạch đẹp: getColumnValues('companyName')

    const camelKey = toCamelCase(clean);

    if (camelKey) map[camelKey] = info;

    // Key Lowercase (Dành cho sự đơn giản): "Company Name" -> "company name"

    // Giúp dễ match text: getColumnValues('company name')

    const lowerKey = clean.toLowerCase();

    if (lowerKey) map[lowerKey] = info;

 

    // Key Index (Fallback): "column2"

    map[`column${index + 1}`] = info;

  }

  return map;

}

✅ Tác dụng:

Dù Dev có đổi vị trí cột Company Name xuống cuối bảng, vòng lặp này sẽ chạy lại và cập nhật index mới. Code test gọi map['companyName'] vẫn chạy đúng mà không cần sửa dòng nào.

Method ensureColumnMapCache: Tối ưu hiệu năng (Performance)

Đọc DOM (giao diện) rất chậm. Nếu bảng có 20 cột, mỗi lần tìm dòng lại đọc lại 20 cái header thì test sẽ chạy như rùa. Giải pháp là Caching trong POM.

Phân tích code:

// (Trong CRMCustomersPage.ts)

private columnMapCache: ColumnMap | null = null; // Biến lưu trữ tạm

private async ensureColumnMapCache(): Promise<ColumnMap> {

  // Nếu chưa có bản đồ (Cache = null)

  if (!this.columnMapCache) {

    await this.waitForTableReady();

    // Robot đi quét map lần đầu tiên và lưu vào túi (Cache)

    this.columnMapCache = await createColumnMapSimple(this.getLocator('tableHeaders'));

  }

  // Những lần sau, cứ lấy bản đồ trong túi ra dùng, không cần quét lại

  return this.columnMapCache;

}

✅ Tác dụng:

Tăng tốc độ test lên gấp nhiều lần khi thao tác nhiều row trên cùng một bảng. Chỉ tốn công đọc header 1 lần duy nhất.

Bản chất của Helper: "Stateless" (Không lưu trạng thái)

Các hàm trong TableColumnHelpers.ts như getColumnValuesSimple, findRowByFiltersSimple được thiết kế theo dạng Pure Function.

Đặc điểm: Nó nhận đầu vào (Input) Trả kết quả (Output).

Vấn đề: Nó KHÔNG CÓ BỘ NHỚ. Nó không biết lần trước nó đã chạy chưa, hay đã tính toán cái gì rồi.

Hãy xem kỹ hàm getColumnInfoSimple (Hàm con được gọi bên trong mọi hàm lấy dữ liệu):

export async function getColumnInfoSimple(

  headersLocator: Locator,

  columnKey: string,

  columnMapCache?: ColumnMap | null // 👈 Tham số này là optional (có thể null)

): Promise<{ info: ColumnInfo; columnMap: ColumnMap }> {

  // Bước 1: Kiểm tra xem người dùng có truyền cái Map cũ vào không?

  let map: ColumnMap | null = columnMapCache || null;

  // ⚠️ ĐIỂM QUAN TRỌNG NHẤT Ở ĐÂY ⚠️

  // Nếu không có Map truyền vào -> NÓ SẼ TẠO LẠI TỪ ĐẦU

  if (!map) {

    map = await createColumnMapSimple(headersLocator); // <--- Đọc DOM tại đây

  }


  // ... (đoạn sau tìm column trong map)

}

 

Kịch bản: Chuyện gì xảy ra nếu KHÔNG CÓ Cache?

Giả sử trong Test Script bạn cần lấy dữ liệu của 3 cột để verify. Code sẽ chạy như sau nếu POM không lưu cache:

Lệnh 1: await customersPage.getColumnValues('companyName')

Gọi getColumnValuesSimple với columnMapCache = undefined.

Helper thấy map rỗng -> Truy cập Browser  -> Đọc 10 thẻ <th> -> Tạo Map lần 1.

Tìm thấy 'companyName' ở index 2. Trả về dữ liệu.

Kết thúc hàm, biến map vừa tạo bị hủy (vì không ai lưu nó lại).

Lệnh 2: await customersPage.getColumnValues('phoneNumber')

Gọi getColumnValuesSimple với columnMapCache = undefined.

Helper thấy map rỗng (lại nữa!) -> Truy cập Browser -> Đọc 10 thẻ <th> -> Tạo Map lần 2.

Tìm thấy 'phoneNumber' ở index 3. Trả về dữ liệu.

Lại hủy biến map.

Lệnh 3: await customersPage.getColumnValues('email')

Lại thấy rỗng - > Truy cập Browser -> Đọc Header lần 3...

Kết luận: Nếu bảng có 20 cột và bạn cần check 5 cột, bạn sẽ bắt Playwright đọc đi đọc lại cái Header đó 5 lần. Việc giao tiếp giữa Code (Node.js) và Browser (Chrome) qua giao thức CDP rất tốn thời gian (Network I/O).

Giải pháp: Caching tại POM (Stateful)

Vì Helper là "người làm thuê không có bộ nhớ", nên ông chủ POM (CRMCustomersPage) phải là người giữ cuốn sổ tay (Cache).

Hãy xem cách POM khắc phục vấn đề này:

// Trích từ CRMCustomersPage.ts

// 1. Biến lưu trữ (Cuốn sổ tay)

private columnMapCache: ColumnMap | null = null;

// 2. Hàm đảm bảo luôn có Map

private async ensureColumnMapCache(): Promise<ColumnMap> {

  // Nếu sổ tay chưa có gì (Lần đầu tiên gọi)

  if (!this.columnMapCache) {

    await this.waitForTableReady();

    // Đọc DOM 1 lần duy nhất và ghi vào sổ

    this.columnMapCache = await createColumnMapSimple(this.getLocator('tableHeaders'));

  }

  // Nếu sổ đã có dữ liệu -> Trả về ngay lập tức, KHÔNG ĐỌC DOM NỮA

  return this.columnMapCache;

}

// 3. Khi dùng

async getColumnValues(columnKey: string) {

  // Lấy bản đồ từ sổ tay ra trước

  const map = await this.ensureColumnMapCache();

  // Truyền cái bản đồ đó xuống cho Helper

  return getColumnValuesSimple(..., map);

}

 

So sánh hiệu năng

Hoạt động

Không dùng Cache (Helper thuần)

Dùng Cache (POM Wrapper)

Lần gọi 1

Đọc DOM (Chậm)

Đọc DOM (Chậm)

Lần gọi 2

Đọc DOM (Chậm)

Đọc biến RAM (Siêu nhanh)

Lần gọi 3

Đọc DOM (Chậm)

Đọc biến RAM (Siêu nhanh)

Độ trễ

Phụ thuộc vào tốc độ mạng/browser

Gần như tức thì (Instant)

Tóm lại

Chính vì thế, kỹ thuật Caching trong POM (ensureColumnMapCache) không phải là code dư thừa, mà là bắt buộc để biến một Framework "chạy được" thành một Framework "chạy nhanh và chuyên nghiệp".

 

🛠 Sơ đồ luồng dữ liệu (Data Flow Visualization)

Hãy tưởng tượng bạn đang đi siêu thị (là hàm buildRowDataSimple) mua 3 món hàng (3 cột). Bạn cần một bản đồ siêu thị (columnMap) để biết hàng nằm ở đâu.

🔍 Phân tích chi tiết từng mắt xích

buildRowDataSimple: Người Nhạc Trưởng (Orchestrator)

Hàm này chịu trách nhiệm lấy dữ liệu cho 1 dòng. Nó chạy vòng lặp qua từng cột cần lấy.

export async function buildRowDataSimple(..., columnMapCache?: ColumnMap | null) {

  const rowData: Record<string, string> = {};

  // 1. Khởi tạo biến giữ bản đồ. Ban đầu nó là cái cache cũ (có thể null)

  let currentColumnMap = columnMapCache;

  // 2. Vòng lặp qua từng cột (Ví dụ: ['name', 'email'])

  for (const key of columnKeys) {

    // GỌI HÀM CON: "Này, tìm cho tao vị trí cột 'name',

    // cầm theo cái bản đồ hiện tại (currentColumnMap) mà dùng nhé"

    const result = await getColumnInfoSimple(..., key, currentColumnMap);

    // ⚠️ ĐIỂM QUAN TRỌNG NHẤT: CẬP NHẬT BẢN ĐỒ ⚠️

    // Hàm con trả về { info, columnMap }.

    // Nếu hàm con vừa phải vẽ lại bản đồ mới, nó sẽ trả ra ở đây.

    // Ta phải lưu lại để vòng lặp sau (cột 'email') dùng tiếp, đỡ phải vẽ lại.

    currentColumnMap = result.columnMap;

    // ... (Lấy cell text) ...

  }

  // 3. Trả về cả dữ liệu dòng VÀ cái bản đồ mới nhất

  return { rowData, columnMap: currentColumnMap! };

}

Tại sao return như vậy?

Vì buildRowDataSimple thường được gọi bởi getTableDataSimple (lấy toàn bộ bảng).

Nếu dòng 1 đã tốn công vẽ bản đồ, nó phải trả cái bản đồ đó ra ngoài để dòng 2 dùng tiếp.

Nếu chỉ return rowData, công sức vẽ bản đồ sẽ bị mất, dòng 2 lại phải vẽ lại $\rightarrow$ Chậm.

getColumnInfoSimple: Người Dò Đường (Map Reader/Builder)

Hàm này nhận lệnh tìm vị trí cột. Nó thông minh ở chỗ: có bản đồ rồi thì dùng, chưa có thì đi vẽ, vẽ xong thì phải trả lại bản đồ cho người gọi.

export async function getColumnInfoSimple(..., columnMapCache?: ColumnMap | null) {

  // 1. Kiểm tra xem người gọi có đưa bản đồ không?

  let map = columnMapCache || null;


  // 2. Nếu chưa có bản đồ -> PHẢI ĐI VẼ (Tốn thời gian đọc DOM)

  if (!map) {

    map = await createColumnMapSimple(headersLocator);

  }


  // 3. Tìm thông tin cột

  let info = map[columnKey];


  // (Logic retry nếu map cũ bị lỗi thời...)


  // 4. Return

  // Trả về info (để lấy dữ liệu) VÀ trả về map (để lưu cache)

  return { info, columnMap: map };

}


Tại sao return { info, columnMap }?

info: Là mục đích chính (Vị trí cột ở đâu? Index mấy?).

columnMap: Là "tác dụng phụ" có giá trị. Vì hàm này có thể vừa tốn công tạo ra map mới (ở bước 2), nó phải trả cái map này ngược lên cho buildRowDataSimple để thằng cha cập nhật biến currentColumnMap.

getCellTextSimple: Công nhân bốc vác (Worker)

Hàm này đơn giản nhất, chỉ làm việc cụ thể, không quan tâm đến bản đồ.

export async function getCellTextSimple(...) {

  // ... Logic clean text ...

  return text; // Chỉ trả về string

}

Tại sao chỉ return string?

Vì hàm này không tạo ra tài nguyên gì mới cần tái sử dụng. Nó chỉ tiêu thụ và trả kết quả cuối cùng.

🎬 Kịch bản chạy thực tế (Scenario Walkthrough)

Giả sử bạn gọi buildRowDataSimple để lấy 2 cột: ['Company', 'Email'] và lúc đầu chưa có cache (columnMapCache = null).

Vòng lặp 1: Cột 'Company'

buildRowDataSimple gọi getColumnInfoSimple(..., 'Company', null).

getColumnInfoSimple thấy map là null -> Đọc DOM, tạo Map Mới.

getColumnInfoSimple trả về { info: {index: 1}, columnMap: Map_Mới }.

buildRowDataSimple cập nhật: currentColumnMap = Map_Mới.

Lấy text cột Company.

Vòng lặp 2: Cột 'Email'

buildRowDataSimple gọi getColumnInfoSimple(..., 'Email', Map_Mới).

getColumnInfoSimple thấy có Map_Mới rồi -> Dùng luôn, KHÔNG Đọc DOM nữa.

Trả về { info: {index: 2}, columnMap: Map_Mới }.

Lấy text cột Email.

Kết thúc hàm:

Trả về { rowData: {Company: 'A', Email: 'B'}, columnMap: Map_Mới }.

Cái Map_Mới này sẽ được hàm cha (ví dụ getTableDataSimple) chuyền tiếp cho Dòng thứ 2.

💡 Tổng kết

Cấu trúc return { data, map } này là biểu hiện của kỹ thuật "Propagating State" (Lan truyền trạng thái) trong lập trình hàm (Functional Programming).

Vì Helper không có biến toàn cục để lưu map.

Nên map phải được chuyền tay từ hàm này sang hàm khác như một quả bóng.

Ai cầm bóng thì dùng, ai tạo ra bóng mới thì phải chuyền quả bóng mới đó đi tiếp.

Đó là lý do tại sao code nhìn có vẻ rườm rà ở đoạn return, nhưng lại đảm bảo Hiệu năng tối đa (chỉ đọc DOM 1 lần duy nhất cho cả bảng).



KỸ THUẬT "PASSING DROP-DOWN CLEANING" (Dependency Injection)

Đây là phần tinh vi nhất: Làm sao Helper (dùng chung) hiểu được logic xử lý dữ liệu đặc thù của từng trang (POM)?

Ví dụ: Cột "Company" trên trang Customers bị dính chữ "View" và Icon, nhưng cột "Company" trên trang Orders thì lại sạch. Helper không thể đoán mò được.

Giải pháp: POM sẽ "gói" logic xử lý vào một hàm, và "thả" (pass down) hàm đó xuống cho Helper.


Tại POM: Định nghĩa quy tắc làm sạch (columnCleaners)
// (Trong CRMCustomersPage.ts)

private get columnCleaners(): Record<string, ColumnTextCleaner> {

  return {

    // Định nghĩa riêng cho cột 'company'

    company: async (cell: Locator) => {

      // Logic: Tìm thẻ <a>, lấy text, bỏ qua icon/nút View

      const linkText = (await cell.locator('a').first().textContent())?.trim();

      return linkText || '';

    }

    // Các cột khác không khai báo sẽ dùng logic mặc định

  };

}

 

Tại Method gọi: Truyền quy tắc xuống dưới
// (Trong CRMCustomersPage.ts)

async getColumnValues(columnKey: string) {

  return getColumnValuesSimple(

    headers,

    rows,

    columnKey,

    this.columnCleaners, // 👈 KEY: Ném bộ quy tắc xuống Helper

    columnMap

  );

}

 

Tại Helper: Thực thi quy tắc (getCellTextSimple)

Helper đóng vai trò là "người thực thi mù" (blind executor). Nó nhận công cụ từ POM và dùng nó.

// (Trong TableColumnHelpers.SIMPLE.ts)

export async function getCellTextSimple(

  cell: Locator,

  columnKey: string,

  columnCleaners?: Record<string, ColumnTextCleaner> // Nhận bộ quy tắc

) {

  // 1. Kiểm tra: Có quy tắc nào cho cột này không?

  const cleaner = columnCleaners?.[columnKey];


  // 2. Nếu CÓ: Dùng quy tắc đó để xử lý cell

  if (cleaner) {

    return cleaner(cell); // Chạy logic của POM

  }


  // 3. Nếu KHÔNG: Dùng mặc định (lấy textContent)

  return (await cell.textContent() || '').trim();

}

✅ Tác dụng:

Decoupling (Tách biệt): Helper TableColumnHelpers hoàn toàn trong sạch, không chứa logic nghiệp vụ (không biết "View" hay "Edit" button là gì).

Flexibility (Linh hoạt): Mỗi trang (Customer, Product, User) tự định nghĩa cách clean data của riêng mình mà vẫn dùng chung 1 bộ khung Helper.

 

GIẢI PHẪU TABLE COLUMN HELPERS

Chúng ta sẽ đi qua từng nhóm hàm, từ những viên gạch nhỏ nhất cho đến những cỗ máy phức tạp nhất.

NHÓM 1: CÁC TRỢ THỦ LOGIC (INTERNAL HELPERS)

Nhóm này xử lý logic nghiệp vụ, tính toán, so sánh, không tương tác trực tiếp với trình duyệt.

textMatches: Bộ não so sánh đa hình

Hàm này giúp việc so sánh text trở nên cực kỳ linh hoạt (String, Regex, hoặc Function).

Code trích xuất:

const textMatches = (value: string, matcher: TextMatcher): boolean => {

  // Case 1: String -> So sánh chứa (includes)

  if (typeof matcher === 'string') {

    return value.includes(matcher);

  }

  // Case 2: Regex -> So sánh mẫu (test)

  if (matcher instanceof RegExp) {

    return matcher.test(value);

  }

  // Case 3: Function -> Logic tùy biến

  return matcher(value);

}

Phân tích:

Hàm kiểm tra typeof và instanceof để biết matcher là loại gì.

Tác dụng: Bạn có thể tìm dòng bằng textMatches("Order 123", "Order") (True) hoặc textMatches("Order 123", /\d+/) (True).


resolveColumnKeysForRowData: Bộ máy quyết định ưu tiên

Hàm này quyết định xem sẽ lấy dữ liệu của những cột nào sau khi tìm thấy dòng.

Code trích xuất:

const resolveColumnKeysForRowData = (

  filters: Record<string, TextMatcher>,

  columnKeys?: string[],

  defaultColumnKeys?: string[]

): string[] => {

  // Ưu tiên 1: Người dùng chỉ định rõ

  if (columnKeys && columnKeys.length > 0) {

    return columnKeys;

  }

  // Ưu tiên 2: Cấu hình mặc định (từ POM)

  if (defaultColumnKeys && defaultColumnKeys.length > 0) {

    return defaultColumnKeys;

  }

  // Ưu tiên 3: Lấy luôn các key dùng để filter

  const keys = Object.keys(filters);

  if (keys.length === 0) {

    throw new Error('No column keys provided for row data extraction.');

  }

  return keys;

};

Phân tích:

Logic if... return tuần tự tạo ra cơ chế Priority.

Guard Clause: Dòng cuối cùng ném lỗi nếu không tìm được key nào, ngăn chặn code chạy tiếp mà không có dữ liệu đầu ra.

NHÓM 2: CƠ CHẾ LẤY DỮ LIỆU (DATA RETRIEVAL)

Nhóm này chịu trách nhiệm tương tác với DOM để lấy text.


getCellTextSimple: Công nhân làm sạch (Cleaning Injection)

Hàm này lấy text từ ô (cell) và áp dụng logic làm sạch nếu được cung cấp.

Code trích xuất:

export async function getCellTextSimple(

  cell: Locator,

  columnKey: string,

  columnCleaners?: Record<string, ColumnTextCleaner>

): Promise<string> {

  // 1. Tìm cleaner trong từ điển (Dictionary Lookup)

  const cleaner = columnCleaners?.[columnKey];


  // 2. Nếu có -> Gọi hàm cleaner (Dependency Injection)

  if (cleaner) {

    return cleaner(cell);

  }


  // 3. Nếu không -> Dùng mặc định

  const text = await cell.textContent();

  return (text || '').trim();

}

Phân tích:

Đây là nơi logic của POM (columnCleaners) được thực thi.

Helper không biết làm sạch như thế nào, nó chỉ biết gọi người biết làm.


getColumnInfoSimple: Người dò đường (The Bridge)

Cầu nối giữa Key (tên cột) và Index (vị trí).

Code trích xuất:

export async function getColumnInfoSimple(

  headersLocator: Locator,

  columnKey: string,

  columnMapCache?: ColumnMap | null

): Promise<{ info: ColumnInfo; columnMap: ColumnMap }> {

  // 1. Kiểm tra Cache

  let map: ColumnMap | null = columnMapCache || null;

  if (!map) {

    // 2. Nếu chưa có -> Đọc DOM tạo mới

    map = await createColumnMapSimple(headersLocator);

  }


  // 3. Tra cứu

  let info = map[columnKey];


  // ... (Logic retry nếu map lỗi thời) ...


  // 4. Return cả info lẫn map (để update cache ngược lại)

  return { info, columnMap: map };

}

Phân tích:

Hàm này đảm bảo createColumnMapSimple (hàm nặng nhất) chỉ chạy khi cần thiết.

Return { info, columnMap } để duy trì dòng chảy dữ liệu (Data Flow) cho hàm gọi nó.


buildRowDataSimple: Nhạc trưởng của Row

Xây dựng object dữ liệu cho 1 dòng cụ thể.

Code trích xuất:

export async function buildRowDataSimple(

  headersLocator: Locator,

  row: Locator,

  columnKeys: string[],

  columnCleaners?: Record<string, ColumnTextCleaner>,

  columnMapCache?: ColumnMap | null

): Promise<{ rowData: Record<string, string>; columnMap: ColumnMap }> {

  const rowData: Record<string, string> = {};

  let currentColumnMap = columnMapCache;

  for (const key of columnKeys) {

    // 1. Lấy vị trí cột (và cập nhật map nếu cần)

    const result = await getColumnInfoSimple(headersLocator, key, currentColumnMap);

    currentColumnMap = result.columnMap; // Lưu map mới nhất

    // 2. Xác định Cell (Lưu ý: CSS index bắt đầu từ 1 nên phải +1)

    const cell = row.locator(`td:nth-child(${result.info.index + 1})`);

    // 3. Lấy text

    rowData[key] = await getCellTextSimple(cell, key, columnCleaners);

  }

  return { rowData, columnMap: currentColumnMap! };

}

Phân tích:

Biến currentColumnMap được cập nhật liên tục qua mỗi vòng lặp for, đảm bảo tối ưu hiệu năng (chỉ tạo map 1 lần ở vòng lặp đầu tiên nếu chưa có).

NHÓM 3: THÁM TỬ TÌM KIẾM (SEARCH ENGINES)

Nhóm này quét qua các dòng để tìm Locator.

findRowByColumnValueSimple: Tìm kiếm đơn giản

Tìm dòng dựa trên 1 cột duy nhất.

Code trích xuất:

export async function findRowByColumnValueSimple(...) {

  // 1. Lấy index cột cần tìm

  const result = await getColumnInfoSimple(headersLocator, columnKey, columnMapCache);

  const count = await rowsLocator.count();

  for (let i = 0; i < count; i++) {

    const row = rowsLocator.nth(i);

    // 2. Chọc đúng vào cột đó

    const cell = row.locator(`td:nth-child(${result.info.index + 1})`);

   

    // 3. Lấy text và So sánh

    const text = await getCellTextSimple(cell, columnKey, columnCleaners);

    if (textMatches(text, matcher)) { // Gọi hàm trợ thủ textMatches

      return row; // Tìm thấy -> Trả về ngay

    }

  }

  throw new Error(...); // Không thấy -> Báo lỗi

}

Phân tích:

Tối ưu: Chỉ tìm index cột 1 lần bên ngoài vòng lặp.

Sử dụng textMatches để hỗ trợ cả Regex và String.


findRowByFiltersSimple: Tìm kiếm nâng cao (AND Logic)

Tìm dòng thỏa mãn TẤT CẢ điều kiện.

Code trích xuất:

export async function findRowByFiltersSimple(...) {

  const keys = requireFilters(filters);

  const count = await rowsLocator.count();

  // 1. Pre-calculation: Lấy hết index các cột cần check trước

  const columnInfos: ColumnInfo[] = [];

  for (const key of keys) {

    const result = await getColumnInfoSimple(headersLocator, key, currentColumnMap);

    currentColumnMap = result.columnMap;

    columnInfos.push(result.info);

  }

  // 2. Duyệt từng dòng

  for (let i = 0; i < count; i++) {

    const row = rowsLocator.nth(i);

    let matchedAll = true; // Cờ đánh dấu

    // 3. Duyệt từng filter trong dòng đó

    for (let j = 0; j < keys.length; j++) {

      const info = columnInfos[j]; // Lấy index đã cache

      const cell = row.locator(`td:nth-child(${info.index + 1})`);

      const text = await getCellTextSimple(cell, keys[j], columnCleaners);

   
      // Nếu MỘT điều kiện sai -> Dừng check dòng này ngay (Early Exit)

      if (!textMatches(text, filters[keys[j]]!)) {

        matchedAll = false;

        break;

      }

    }

    if (matchedAll) return row; // Thỏa mãn hết -> Trả về

  }

  throw new Error(...);

}

Phân tích:

Chiến thuật: Tách việc tìm index ra khỏi vòng lặp row (Pre-calculation) giúp tăng tốc đáng kể.

Early Exit: break ngay khi một filter sai giúp tiết kiệm thời gian, không cần check các filter còn lại của dòng đó.

NHÓM 4: COMBO TÌM & LẤY (SEARCH & EXTRACT)

Kết hợp tìm dòng và lấy dữ liệu trong 1 nốt nhạc.


getRowDataByFiltersSimple

Code trích xuất:

export async function getRowDataByFiltersSimple(...) {

  // Bước 1: Tìm dòng (Trả về Locator)

  const row = await findRowByFiltersSimple(headersLocator, rowsLocator, filters, ...);


  // Bước 2: Quyết định lấy cột nào (Priority logic)

  const resolvedKeys = resolveColumnKeysForRowData(filters, columnKeys, defaultColumnKeys);


  // Bước 3: Lấy dữ liệu từ dòng đó

  const result = await buildRowDataSimple(headersLocator, row, resolvedKeys, ...);

  return result.rowData;

}

Phân tích:

Hàm này là một Facade Pattern (Mặt tiền), giúp che giấu sự phức tạp của việc kết hợp 3 hàm con (find..., resolve..., build...) lại với nhau.


NHÓM 5: XỬ LÝ PHÂN TRANG (PAGINATION)

Tìm kiếm dữ liệu nằm rải rác trên nhiều trang.


findRowByFiltersAcrossPagesSimple

Code trích xuất:

export async function findRowByFiltersAcrossPagesSimple(

  ...,

  goToNextPage: () => Promise<boolean>, // Callback chuyển trang

  ...

): Promise<Locator> {

  let currentPage = 1;

  // Vòng lặp duyệt trang

  while (currentPage <= maxPages) {

    try {

      // 1. Thử tìm ở trang hiện tại

      const row = await findRowByFiltersSimple(...);

      return row; // Tìm thấy -> Return luôn

    } catch (error) {

      // 2. Chỉ bắt lỗi "Unable to find row", lỗi khác thì throw tiếp

      if (!(error instanceof Error) || !error.message.includes('Unable to find row')) {

        throw error;

      }

      // 3. Không thấy -> Gọi Callback chuyển trang

      const hasNext = await goToNextPage();

      if (!hasNext) break; // Hết trang -> Thoát

      currentPage++;

    }

  }

  throw new Error(...); // Tìm hết các trang vẫn không thấy

}

Phân tích:

Sử dụng cơ chế try...catch để điều khiển luồng (Control Flow). Lỗi "Không tìm thấy" được coi là tín hiệu để chuyển trang.

Callback goToNextPage: Helper không cần biết nút Next nằm ở đâu, ID là gì. Nó chỉ ra lệnh "Next đi", việc thực hiện là của POM.

 

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