JSPM

  • Created
  • Published
  • Downloads 10674
  • Score
    100M100P100Q152599F
  • License MIT

Smart, column-aware table interactions for Playwright

Package Exports

  • @rickcedwhat/playwright-smart-table
  • @rickcedwhat/playwright-smart-table/presets
  • @rickcedwhat/playwright-smart-table/presets/mui
  • @rickcedwhat/playwright-smart-table/types

Readme

Playwright Smart Table

Production-ready table testing for Playwright with smart column-aware locators.

npm version License: MIT


📚 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 = table.getRow({ Name: 'John Doe' });
const email = await row.getCell('Email').textContent();

// ✅ Auto-pagination across all pages
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-table

Basic Usage

import { useTable } from '@rickcedwhat/playwright-smart-table';

const table = await useTable(page.locator('#my-table')).init();

// Get row by column values (current page)
const row = table.getRow({ Name: 'John Doe' });

// Access cells by column name
const email = await row.getCell('Email').innerText();

// Search across paginated tables
const allActive = await table.findRows({ Status: 'Active' });

Iterating Across Pages

// forEach — sequential by default (concurrency: 'sequential') — safe for interactions
await table.forEach(async ({ row, rowIndex, stop }) => {
  if (await row.getCell('Status').innerText() === 'Done') stop();
  await row.getCell('Checkbox').click();
});

// map — parallel by default (concurrency: 'parallel') — safe for reads
const emails = await table.map(({ row }) => row.getCell('Email').innerText());

// filter — sequential by default; 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());
}

Set a default for all iteration calls with useTable(..., { concurrency: 'sequential' }), or per call: table.map(fn, { concurrency: 'synchronized' }). Modes: parallel (full parallelism), sequential (strictly one row at a time), synchronized (parallel navigation with serialized callbacks — useful for virtualized grids).

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: map defaults to concurrency: 'parallel'. If your callback opens popovers, fills inputs, or otherwise mutates UI state, pass { concurrency: 'sequential' } (or 'synchronized' if you need lock-step navigation with serialized work).

The legacy { parallel: true | false } option still works but is deprecated; prefer concurrency.

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, targetValue }) => {
        await cell.click();
        await page.keyboard.type(targetValue);
        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, and for await...of across 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.

Deprecations

  • generateConfigPrompt() — deprecated. Use generateConfig() instead. generateConfigPrompt() will be removed in v7.0.0.

License

MIT © Cedrick Catalan