Data Table Automation Practice
Practice reading, counting, sorting, and locating rows and cells in a realistic HTML table — essential skills for Selenium & Playwright table automation.
Interactive Table
| Sr No. | Book Name | Book Genre | Book Author | Book ISBN | Book Published | Actions |
|---|---|---|---|---|---|---|
| 1 | The Pragmatic Programmer | Technology | Andrew Hunt | ISBN-9780135957059 | 1999-10-20 | |
| 2 | Clean Code | Technology | Robert C. Martin | ISBN-9780132350884 | 2008-08-01 | |
| 3 | Design Patterns | Technology | Erich Gamma | ISBN-9780201633610 | 1994-10-31 | |
| 4 | The Hobbit | Fantasy | J.R.R. Tolkien | ISBN-9780547928227 | 1937-09-21 | |
| 5 | Dune | Science Fiction | Frank Herbert | ISBN-9780441013593 | 1965-08-01 |
| ID | Scenario | Type | Priority | |
|---|---|---|---|---|
| DT_001 | All 7 column headers are present and correctly labelled Expected: Headers read: Sr No., Book Name, Book Genre, Book Author, Book ISBN, Book Published, Actions ✅ positivehigh | positive | high | |
| DT_002 | Table displays exactly 5 rows on page 1 (25 total across 5 pages) Expected: Row count on page 1 equals 5; row-count indicator shows '25 books — page 1 of 5' ✅ positivehigh | positive | high | |
| DT_003 | Row 2, Column 2 contains the book name 'Clean Code' Expected: Cell text at row 2, column 2 equals 'Clean Code' ✅ positivehigh | positive | high | |
| DT_004 | Find the row for author 'George Orwell' and click its Edit button Expected: The Edit button in the George Orwell row is clicked successfully ✅ positivehigh | positive | high | |
| DT_005 | Table is not empty after initial page load Expected: tbody contains at least one visible row ✅ positivehigh | positive | high | |
| DT_006 | All values in the Book ISBN column start with 'ISBN-' Expected: Every ISBN cell begins with the prefix 'ISBN-' ✅ positivemedium | positive | medium | |
| DT_007 | Searching by a book name filters the visible rows Expected: Only rows matching the search term remain visible ✅ positivemedium | positive | medium | |
| DT_008 | Genre filter reduces visible rows to the selected genre only Expected: Only books in the chosen genre are shown after filtering ✅ positivemedium | positive | medium | |
| DT_009 | Delete button for a row has no data-testid — located via aria-label Expected: Delete button is found and accessible via aria-label or XPath ✅ positivemedium | positive | medium | |
| DT_010 | Row can be located by its data-book-id attribute Expected: Row with data-book-id='book-004' contains 'The Hobbit' ✅ positivemedium | positive | medium | |
| DT_011 | Clearing the search input restores all rows and resets pagination Expected: After clearing search, page 1 shows 5 rows and pagination shows 5 pages ⚠️ edgemedium | edge | medium | |
| DT_012 | Row-count display updates after filtering Expected: The row-count indicator reflects the filtered count ⚠️ edgelow | edge | low | |
| DT_013 | Clicking page 2 loads the next set of rows Expected: Page 2 shows rows 6-10 and the active page button is highlighted ✅ positivehigh | positive | high | |
| DT_014 | Clicking Next navigates to the following page Expected: Next button advances pagination by one page ✅ positivemedium | positive | medium | |
| DT_015 | Previous button is disabled on page 1 and enabled on page 2+ Expected: Prev is disabled on first page, enabled on all others ⚠️ edgemedium | edge | medium | |
| DT_016 | Clicking a sortable column header sorts rows ascending then descending Expected: First click sorts A to Z, second click sorts Z to A, third click resets sort ✅ positivehigh | positive | high | |
| DT_017 | Sorting resets to page 1 when a different page is active Expected: Changing sort while on page 3 jumps back to page 1 ⚠️ edgemedium | edge | medium | |
| DT_018 | Add new book via the Add Book dialog and verify it appears in the table Expected: New book row appears on the last page and persists after reload ✅ positivehigh | positive | high | |
| DT_019 | Add Book dialog shows validation errors when required fields are empty Expected: Submitting with empty Name or Author shows inline error messages ❌ negativemedium | negative | medium | |
| DT_020 | Edit a book and verify the updated values are saved Expected: Edited fields reflect new values in the table row and persist after reload ✅ positivehigh | positive | high | |
| DT_021 | Delete a book and verify the row is removed from all pages Expected: Deleted book no longer appears and row count decreases by 1 ✅ positivehigh | positive | high |
HTML tables are everywhere in dashboards, reports, and data-driven UIs. Automating them requires counting rows, reading specific cells, finding rows by content, and verifying headers — all of which have slightly different approaches across frameworks.
Row and column counts are the first assertion you'll make on any table. Use CSS selectors targeting tbody tr for rows and thead th for columns.
// Playwright — count rows and columns
const rowCount = await page.locator("#dataTable tbody tr").count();
const colCount = await page.locator("#dataTable thead th").count();
console.log("Rows:", rowCount, "Cols:", colCount);Reading a specific cell means combining a row selector (nth-child) with a column selector. The same pattern works for reading any cell at a known position.
// Playwright — row 2, column 2 (1-based CSS)
const cellText = await page
.locator("#dataTable tbody tr:nth-child(2) td:nth-child(2)")
.textContent();
console.log("Cell:", cellText?.trim()); // "Clean Code"Headers are th elements inside thead. allTextContents() in Playwright and findElements by tagName in Selenium both return them as a list you can assert against.
// Playwright — read all header names
const headers = await page
.locator("#dataTable thead th")
.allTextContents();
console.log(headers);
// ["Sr No.", "Book Name", "Book Genre", "Book Author", "Book ISBN", "Book Published", "Actions"]Finding a row by content is a pattern you'll use constantly — filter rows until you find one containing the target text, then scope your next action inside that row.
// Playwright — find row by author, click Edit
const row = page
.locator("[data-testid='book-row']")
.filter({ hasText: "George Orwell" });
await expect(row).toBeVisible();
await row.locator("[data-testid='btn-edit-book']").click();Iterating all rows is useful for building lists, validating column-wide constraints, or checking every row satisfies a condition. Loop with nth() in Playwright or a for-loop in Selenium.
// Playwright — collect all book names from column 2
const rows = page.locator("#dataTable tbody tr");
const count = await rows.count();
const names: string[] = [];
for (let i = 0; i < count; i++) {
const name = await rows
.nth(i)
.locator("td:nth-child(2)")
.textContent();
names.push(name?.trim() ?? "");
}
console.log(names);Empty-state assertions prevent false positives when a table hasn't loaded yet. Assert row count is zero, or assert the empty-state message is visible.
// Playwright — assert table is not empty
const rowCount = await page
.locator("#dataTable tbody tr")
.count();
expect(rowCount).toBeGreaterThan(0);
// Assert empty-state message is hidden
await expect(
page.locator("[data-testid='empty-table-msg']")
).toBeHidden();Quick reference across all three frameworks for common table automation tasks.
| Action | Selenium | Playwright JS | Playwright PY | Cypress |
|---|---|---|---|---|
| Count rows | findElements(cssSelector("tbody tr")).size() | locator("tbody tr").count() | locator("tbody tr").count() | .get("tbody tr").its("length") |
| Count columns | findElements(cssSelector("thead th")).size() | locator("thead th").count() | locator("thead th").count() | .get("thead th").its("length") |
| Get cell text | findElement(cssSelector("tr:nth-child(2) td:nth-child(2)")) | locator("tr:nth-child(2) td:nth-child(2)").textContent() | text_content() | .get("tr:nth-child(2) td:nth-child(2)").invoke("text") |
| Get all headers | findElements(By.tagName("th")) | locator("th").allTextContents() | all_text_contents() | .get("th").invoke("text") |
| Find row by text | row.getText().contains("text") | .filter({ hasText: "text" }) | .filter(has_text="text") | .contains("text").closest("tr") |