NỘI DUNG BÀI HỌC

✅ Nhận diện các loại Table trên UI thực tế
✅ Phân tích đặc điểm DOM & hành vi load dữ liệu của từng loại table
✅ Đọc dữ liệu table theo row / column / cell
✅ Thao tác action theo từng row (Edit / Delete / View)
✅ Xử lý Pagination / Sorting / Dynamic loading
✅ Xử lý Dropdown (native & custom)
✅ Viết hàm reusable để code ngắn gọn, ổn định, tránh flaky



I. Các loại Table thường gặp trên UI & CÁCH GIẢI QUYẾT


🟦 1.1 Static Table (Table tĩnh)

Khái niệm

  • Table HTML thuần (<table>)

  • Dữ liệu render sẵn khi page load

  • Không gọi API khi filter/sort

Dấu hiệu nhận biết

  • Không spinner

  • Không request mới khi thao tác

  • DOM không thay đổi

Ví dụ thực tế

  • Bảng thông số kỹ thuật

  • Bảng cấu hình hệ thống

  • Trang so sánh plan

Cách xử lý

  • Đọc trực tiếp DOM

  • Không cần chờ load lại

table = page.locator("table")
rows = table.locator("tbody tr")

Mẹo

  • Dùng get_by_role("table") hoặc data-testid để locator bền


🟦 1.2 Dynamic Table – Client-side

Khái niệm

  • Dữ liệu load 1 lần

  • Filter / sort bằng JavaScript

  • Không gọi API mới

Dấu hiệu nhận biết

  • Filter/sort không thấy request mới

  • DOM thay đổi ngay

Ví dụ thực tế

  • Bảng dữ liệu nhỏ

  • Trang report nhanh

Cách xử lý

  • Chờ UI ổn định (row count / text)

  • Không cần chờ API

Mẹo

  • Normalize text (strip, lower) khi assert sort


🟦 1.3 Dynamic Table – Server-side

Khái niệm

  • Mỗi thao tác filter/sort/paginate → gọi API mới

  • UI render lại hoàn toàn

Dấu hiệu nhận biết

  • Có spinner/loading

  • Network tab có request mới

Ví dụ thực tế

  • User list

  • Order management

  • Admin dashboard

Cách xử lý

  • Chờ spinner biến mất hoặc

  • Chờ response API cụ thể

with page.expect_response("**/api/users**"):
    page.get_by_role("button", name="Search").click()

Mẹo

  • Chờ API ổn định hơn chờ text đổi


🟦 1.4 Paginated Table (Table phân trang)

Khái niệm

  • Dữ liệu chia nhiều trang

  • Có Next / Prev / Page number

Ví dụ thực tế

  • Order list (10–20 item/page)

  • User list

Cách xử lý

  • So sánh dữ liệu trước & sau khi paginate

    first_before = table.locator("tbody tr").first.inner_text()
    next_btn.click()
    expect(table.locator("tbody tr").first).not_to_have_text(first_before)
    
 

Mẹo

  • Không dùng sleep

  • Có thể dùng page number label để assert


🟦 1.5 Sortable Table

Khái niệm

  • Click header để sort ASC/DESC

  • Sort theo text / number / date

Ví dụ thực tế

  • Sort Name

  • Sort Price

  • Sort Created Date

Cách xử lý

  • Click header

  • Lấy list values

  • Parse đúng kiểu dữ liệu

  • So sánh với sorted()


🟥 1.6 Virtualized Table (Table ảo – nâng cao)

Khái niệm

  • Chỉ render các rows trong viewport

  • Scroll mới render row khác

Dấu hiệu nhận biết

  • rows.count() luôn nhỏ

  • Scroll làm DOM thay đổi

Ví dụ thực tế

  • AG Grid

  • MUI DataGrid (large data)

  • Infinite scroll

Cách giải quyết

  • Không đọc toàn bộ table

  • Dùng search/filter để đưa record cần test vào viewport

  • Tránh assert “tất cả rows”


Tóm tắt phân loại table:

Loại Table Đặc điểm nhận dạng Chiến thuật xử lý (Strategy)
Static Table Dữ liệu render sẵn trong HTML. View source thấy ngay <table>. Đọc trực tiếp DOM bằng locator().
Dynamic (Client) Filter/Sort không thấy request Network mới. Chờ UI thay đổi (row count/text) rồi mới thao tác.
Dynamic (Server) Mỗi thao tác gọi API mới. Có Spinner/Loading. Bắt buộc dùng expect_response hoặc chờ Spinner biến mất.
Paginated Có thanh phân trang (Next/Prev). Lưu trạng thái dòng đầu -> Click Next -> Chờ dòng đầu thay đổi.
Virtualized DOM chỉ có ~10-20 dòng dù data có 1000. Scroll mới load. Tránh "đọc toàn bộ". Dùng Search để đưa record vào viewport.


II. TABLE — ĐỌC DỮ LIỆU THEO ROW / COLUMN / CELL (📊)

2.1 Đọc số lượng rows

rows = table.locator("tbody tr")
row_count = rows.count()


2.2 Đọc 1 cell cụ thể

def get_cell_text(table, row_index, col_index):
    return (
        table.locator("tbody tr")
        .nth(row_index)
        .locator("td")
        .nth(col_index)
        .inner_text()
        .strip()
    )


Ý nghĩa từng dòng (bóc tách từng bước)

  1. table.locator("tbody tr")

    • Lấy tất cả row trong phần thân table (tbody).

    • Kết quả là một Locator (danh sách các phần tử tr), chưa đọc dữ liệu ngay.

  2. .nth(row_index)

    • Chọn row thứ row_index (0-based).

    • nth(0) = row đầu tiên, nth(1) = row thứ 2…

  3. .locator("td")

    • Trong row vừa chọn, lấy tất cả cell dạng td.

  4. .nth(col_index)

    • Chọn cell thứ col_index trong row đó (0-based).

  5. .inner_text()

    • Lấy text hiển thị “như người dùng nhìn thấy” (đã xử lý layout/line breaks).

  6. .strip()

    • Xóa khoảng trắng đầu/cuối để assert ổn định hơn.


 Khi nào dùng cách đọc theo index là hợp lý?

✅ Dùng tốt khi:

  • Table có cấu trúc cột cố định, ít thay đổi.

  • Test muốn verify nhanh “row i, cột j”.

  • Bạn đang viết helper chung để đọc dữ liệu.

⚠️ Cẩn thận khi:

  • Cột có thể bị ẩn/hiện (toggle columns).

  • Table có checkbox/select ở cột đầu → làm lệch index.

  • Có cột “Actions” chứa button/icon (text rỗng).

Mẹo: Nếu cột dễ thay đổi, nên đọc theo tên header thay vì index (phần 6).


Các lỗi hay gặp & cách xử lý

a. Row không tồn tại → lỗi timeout/locator

Ví dụ: row_index lớn hơn số row thực tế.

✅ Cách phòng:

rows = table.locator("tbody tr")
assert row_index < rows.count(), "Row index out of range"


b.  Cell không phải td (một số table dùng div)

Nhiều DataGrid/Antd/MUI không dùng <td> thật.

✅ Cách xử lý:

  • Xem DOM thật, đôi khi cell là:

    • div[role="gridcell"]

    • div.ag-cell

    • div[data-field="status"]

Ví dụ grid role:

cell = table.get_by_role("row").nth(row_index).get_by_role("gridcell").nth(col_index)


c.  Table reload sau thao tác (filter/paginate) → đọc sai dữ liệu

Bạn click filter xong đọc ngay, table chưa kịp cập nhật.

✅ Cách xử lý:

  • Chờ spinner hidden hoặc chờ first row đổi.

    expect(spinner).to_be_hidden()
    
 

d. .inner_text() vs .text_content() khác nhau

  • inner_text() = text “rendered”, có thể chậm hơn, chuẩn cho UI.

  • text_content() = text raw trong DOM, nhanh hơn nhưng có thể dính whitespace.

Khuyến nghị: dùng inner_text() cho table UI; nếu performance cần thiết thì tối ưu sau.


2.3 Đọc toàn bộ 1 cột

def get_column_values(table, col_index):
    rows = table.locator("tbody tr")
    return [
        rows.nth(i).locator("td").nth(col_index).inner_text().strip()
        for i in range(rows.count())
    ]


Áp dụng

  • Assert tất cả status = Active

  • Assert kết quả filter


2.4 Chuyển table thành List[Dict]

def table_to_dicts(table):
    headers = [
        h.inner_text().strip()
        for h in table.locator("thead th").all()
    ]
    rows = table.locator("tbody tr")

    data = []
    for i in range(rows.count()):
        cells = [
            c.inner_text().strip()
            for c in rows.nth(i).locator("td").all()
        ]
        data.append(dict(zip(headers, cells)))
    return data


Áp dụng

  • Assert dữ liệu như API response

  • Debug dễ dàng

 

Best practive🧠✨:

  •  Ưu tiên data-testid / get_by_role nếu hệ thống hỗ trợ.

  •  Sau thao tác filter/sort/paginate: phải chờ table reload xong.

  •  Nếu cột hay đổi: đọc theo header name, không đọc theo index cứng.

  •  Nếu table là DataGrid (không <td>): cần chọn locator theo role/class đúng.


III. CLICK ACTION THEO ROW (🧩)


3.1 Các kiểu action thường gặp

  • Button trực tiếp trong row (Edit / View / Delete)

  • Kebab menu (⋮)

  • Click row mở detail


3.2 Tìm row theo giá trị key

def find_row_index_by_cell_text(table, col_index, value):
    rows = table.locator("tbody tr")
    for i in range(rows.count()):
        if rows.nth(i).locator("td").nth(col_index).inner_text().strip() == value:
            return i
    return -1

 

3.3 Click Edit theo row

def click_edit_by_key(table, key_col, key_value):
    idx = find_row_index_by_cell_text(table, key_col, key_value)
    assert idx >= 0

    row = table.locator("tbody tr").nth(idx)
    row.get_by_role("button", name="Edit").click()


3.4 Kebab menu (⋮)

def click_delete_in_menu(page, table, key_col, key_value):
    idx = find_row_index_by_cell_text(table, key_col, key_value)
    row = table.locator("tbody tr").nth(idx)

    row.get_by_role("button", name="More").click()
    page.get_by_role("menuitem", name="Delete").click()

Mẹo

  • Menu thường render ở <body>

  • Không locate menu bên trong row


IV. PAGINATION / SORTING / DYNAMIC LOADING (🚀)


4.1 Dynamic loading – chờ đúng cách

from playwright.sync_api import expect

expect(spinner).to_be_hidden()
expect(table).to_be_visible()

Hoặc:

with page.expect_response("**/api/**"):
    button.click()

 

4.2 Pagination

def go_next_page(table, next_btn):
    first_before = table.locator("tbody tr").first.inner_text()
    next_btn.click()
    expect(table.locator("tbody tr").first).not_to_have_text(first_before)


4.3 Sorting

Sort text

values = get_column_values(table, 0)
assert values == sorted(values, key=str.lower)

Sort number

nums = [float(v.replace(",", "")) for v in values]
assert nums == sorted(nums)

Sort date

from datetime import datetime
dates = [datetime.strptime(v, "%Y-%m-%d") for v in values]
assert dates == sorted(dates)


V. DROPDOWNS — TỔNG QUAN & XỬ LÝ


5.1 Các loại Dropdown

  • Native <select>

  • Custom dropdown (React / Antd / MUI)

  • Searchable dropdown

  • Multi-select dropdown

  • Dropdown trong table


5.2 Native dropdown

page.locator("select#country").select_option("VN")


5.3 Custom dropdown

dropdown.click()
page.get_by_role("option", name="Active").click()

Mẹo

  • Option chỉ tồn tại sau khi mở dropdown

  • Không locate option trước


5.4 Searchable dropdown

dropdown.click()
page.locator("input").fill("Admin")
page.get_by_role("option", name="Admin").click()


5.5 Multi-select dropdown

dropdown.click()
for role in ["Admin", "Editor"]:
    page.get_by_role("option", name=role).click()
page.keyboard.press("Escape")


VI. CÁC LỖI PHỔ BIẾN & CÁCH TRÁNH

❌ Đọc table khi chưa load xong
❌ Dùng sleep()
❌ Hard-code row index
❌ Locate option dropdown trước khi mở

✅ Luôn chờ điều kiện rõ ràng
✅ Viết helper reusable
✅ Assert theo dữ liệu, không theo vị trí


VII. TỔNG KẾT BUỔI 14

  • Table & Dropdown là trọng tâm automation UI

  • Cần nhận diện đúng loại table trước khi code

  • Automation tốt = ổn định + tái sử dụng + ít flaky

  • Buổi 14 là nền tảng cho automation Admin / Dashboard / Project thực tế

Teacher

Teacher

Hà Lan

QA Automation

With over 5 years of experience in web, API, and mobile test automation, built strong expertise in designing and maintaining automation frameworks across various domains and international projects. Committed to mentoring and knowledge sharing, I provide practical guidance and proven techniques to help aspiring testers develop their skills and succeed in the automation field.

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