@rickcedwhat/playwright-smart-table
v6.7.5
Published
Smart, column-aware table interactions for Playwright
Maintainers
Readme
Playwright Smart Table
Production-ready table testing for Playwright with smart column-aware locators.
📚 Full Documentation →
Visit the complete documentation at: https://rickcedwhat.github.io/playwright-smart-table/
Why Playwright Smart Table?
Testing HTML tables in Playwright is painful. Traditional approaches are fragile and hard to maintain.
The Problem
Traditional approach:
// ❌ Fragile - breaks if columns reorder
const email = await page.locator('tbody tr').nth(2).locator('td').nth(3).textContent();
// ❌ Brittle XPath
const row = page.locator('//tr[td[contains(text(), "John")]]');
// ❌ Manual column mapping
const headers = await page.locator('thead th').allTextContents();
const emailIndex = headers.indexOf('Email');
const email = await row.locator('td').nth(emailIndex).textContent();The Solution
Playwright Smart Table:
// ✅ Column-aware - survives column reordering
const row = await table.findRow({ Name: 'John Doe' });
const email = await row.getCell('Email').textContent();
// ✅ Auto-pagination
const allEngineers = await table.findRows({ Department: 'Engineering' });
// ✅ Type-safe
type Employee = { Name: string; Email: string; Department: string };
const table = useTable<Employee>(page.locator('#table'));Quick Start
Installation
npm install @rickcedwhat/playwright-smart-tableBasic Usage
import { useTable } from '@rickcedwhat/playwright-smart-table';
const table = await useTable(page.locator('#my-table')).init();
// Find row by column values
const row = await table.findRow({ Name: 'John Doe' });
// Access cells by column name
const email = await row.getCell('Email').textContent();
// Search across paginated tables
const allActive = await table.findRows({ Status: 'Active' });Iterating Across Pages
// forEach — sequential, safe for interactions (parallel: false default)
await table.forEach(async ({ row, rowIndex, stop }) => {
if (await row.getCell('Status').innerText() === 'Done') stop();
await row.getCell('Checkbox').click();
});
// map — parallel within page, safe for reads (parallel: true default)
const emails = await table.map(({ row }) => row.getCell('Email').innerText());
// filter — async predicate across all pages, returns SmartRowArray
const active = await table.filter(async ({ row }) =>
await row.getCell('Status').innerText() === 'Active'
);
// for await...of — low-level page-by-page iteration
for await (const { row, rowIndex } of table) {
console.log(rowIndex, await row.getCell('Name').innerText());
}When your pagination strategy supports bulk jumps (goNextBulk), pass { useBulkPagination: true } to map/forEach/filter to advance by multiple pages at once.
map+ UI interactions:mapdefaults toparallel: true. If your callback opens popovers, fills inputs, or otherwise mutates UI state, pass{ parallel: false }to avoid overlapping interactions.
filter vs findRows
| Use case | Best tool |
|---|---|
| Match by column value / regex / locator | findRows |
| Computed value (math, range, derived) | filter |
| Cross-column OR logic | filter |
| Multi-step interaction in predicate (click, read, close) | filter |
| Early exit after N matches | filter + stop() |
findRows is faster for column-value matches — Playwright evaluates the locator natively with no DOM reads. filter is more flexible for logic that a CSS selector can't express.
// findRows — structural match, no DOM reads, fast
const notStarted = await table.findRows({
Status: (cell) => cell.locator('[class*="gray"]')
});
// filter — arbitrary async logic
const expensive = await table.filter(async ({ row }) => {
const price = parseFloat(await row.getCell('Price').innerText());
const qty = parseFloat(await row.getCell('Qty').innerText());
return price * qty > 1000;
});Advanced: columnOverrides
For complex DOM structures, custom data extraction, or specialized input widgets, use columnOverrides to intercept how Smart Table interacts with specific columns:
const table = useTable(page.locator('#table'), {
columnOverrides: {
// Override how data is read from the 'Status' column (e.g., for .toJSON())
Status: {
read: async (cell) => {
const isChecked = await cell.locator('input[type="checkbox"]').isChecked();
return isChecked ? 'Active' : 'Inactive';
}
},
// Override how data is written to the 'Tags' column (for .smartFill())
Tags: {
write: async (cell, value) => {
await cell.click();
await page.keyboard.type(value);
await page.keyboard.press('Enter');
}
}
}
});Key Features
- 🎯 Smart Locators - Find rows by content, not position
- 🧠 Fuzzy Matching - Smart suggestions for typos in column names
- ⚡ Smart Initialization - Handles loading states and dynamic headers automatically
- 📄 Auto-Pagination - Search across all pages automatically
- 🔍 Column-Aware Access - Access cells by column name
- 🔁 Iteration Methods -
forEach,map,filter, andfor await...ofacross all pages - 🛠️ Debug Mode - Visual debugging with slow motion and logging
- 🔌 Extensible Strategies - Support any table implementation
- 💪 Type-Safe - Full TypeScript support
- 🚀 Production-Ready - Battle-tested in real-world applications
When to Use This Library
Use this library when you need to:
- ✅ Find rows by column values
- ✅ Access cells by column name
- ✅ Search across paginated tables
- ✅ Handle column reordering
- ✅ Extract structured data
- ✅ Fill/edit table cells
- ✅ Work with dynamic tables (MUI DataGrid, AG Grid, etc.)
You might not need this library if:
- ❌ You don't interact with tables at all
- ❌ You don't need to find a row based on a value in a cell
- ❌ You don't need to find a cell based on a value in another cell in the same row
⚠️ Important Note on Pagination & Interactions
When findRows or filter paginates across pages, returned SmartRow locators point to rows that may be off the current DOM page.
- Data extraction: Safe —
toJSON()and cell reads work while the row is visible during iteration. - Interactions after pagination: Use
await row.bringIntoView()first — it navigates back to the page the row was originally found on, then you can safely click/fill.
const active = await table.filter(async ({ row }) =>
await row.getCell('Status').innerText() === 'Active'
);
for (const row of active) {
await row.bringIntoView(); // navigate back to the row's page
await row.getCell('Checkbox').click(); // safe to interact
}Documentation
📚 Full documentation available at: https://rickcedwhat.github.io/playwright-smart-table/
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © Cedrick Catalan
