Skip to content

Strategies ​

All strategy options live under config.strategies. Every strategy is optional and additive — PST uses built-in defaults for anything not configured.

typescript
import { useTable, Strategies } from '@rickcedwhat/playwright-smart-table';
const { Pagination, Stabilization } = Strategies;

const table = await useTable(locator, {
  strategies: {
    pagination: Pagination.infiniteScroll({ ... }),
    loading: { isCellLoading: async (cell) => ... },
  },
}).init();

→ API: Config Options


Pagination ​

pagination ​

Signature ​

typescript
pagination?: PaginationStrategy

Controls how PST advances through pages during findRow, findRows, forEach, map, and filter.

Built-in factories via Strategies.Pagination:

Pagination.click ​

Advances by clicking a "Next page" button.

typescript
pagination: Strategies.Pagination.click({
  nextButton: page.locator('[aria-label="Next page"]'),
  prevButton: page.locator('[aria-label="Previous page"]'),
})

Pagination.infiniteScroll ​

Advances by scrolling the table container.

typescript
pagination: Strategies.Pagination.infiniteScroll({
  action: 'js-scroll',        // 'scroll' | 'js-scroll'
  scrollTarget: (root) => root,
  scrollAmount: 300,
  stabilization: Strategies.Stabilization.contentChanged({ scope: 'all', timeout: 3000 }),
})

→ Guide: Pagination


Loading ​

loading ​

Signature ​

typescript
loading?: LoadingStrategy

Strategies for detecting loading states at the table, row, and cell level.

typescript
strategies: {
  loading: {
    isTableLoading: async (context) =>
      (await context.root.locator('.spinner').count()) > 0,

    isRowLoading: async (row) =>
      (await row.locator('[aria-busy="true"]').count()) > 0,

    isCellLoading: async (cell) =>
      (await cell.locator('.skeleton').count()) > 0,
    cellLoadingTimeout: 10_000,
    onCellLoadingTimeout: 'read-as-is', // 'skip' | 'read-as-is' | 'throw'
  },
}

Timeouts:

  • loadingTimeout — max ms to wait while isTableLoading returns true (before counting, finding, and between pages). Default 10000. The wait gives up and continues when the budget runs out — it never hangs.
  • sortStabilizationTimeout — overrides loadingTimeout for the wait after sorting.apply() only.
  • rowLoadingTimeout / onRowLoadingTimeout — how long to wait for a loading row, then 'skip' · 'read-as-is' · 'throw'.
  • cellLoadingTimeout / onCellLoadingTimeout — same for cells, or pass a callback that returns the value to use.
typescript
loading: {
  isTableLoading: async ({ root }) => root.locator('.spinner').isVisible(),
  loadingTimeout: 30_000, // slow backend
}

Built-in presets via Strategies.Loading:

  • Strategies.Loading.Table.never — table is never loading
  • Strategies.Loading.Row.never — rows are never loading

header ​

Signature ​

typescript
header?: HeaderStrategy

Custom logic for discovering column names. Use when headers require scrolling or special parsing.

Built-in factories via Strategies.Header:

  • Strategies.Header.visible — reads headers currently in the DOM
  • Strategies.Header.horizontalScroll — starts at the left edge and scans right to discover virtualized headers in column order, then returns to the left edge (requires { selector } for the scroll container; omitting it returns visible headers only). Uses logical order when all headers have aria-colindex, otherwise content-space horizontal positions for the entire result; custom layouts without stable header positions should supply a custom header strategy. Duplicate labels are preserved for mapper validation or headerTransformer.
typescript
// Discover virtualized headers by scrolling a known container
header: Strategies.Header.horizontalScroll({ selector: '.dvn-scroller' })
typescript
// Custom header strategy
header: async ({ root, page }) => {
  const headers = [];
  // ... scroll and collect header text ...
  return headers; // string[]
}

→ Guide: Custom Header Text


Viewport ​

viewport ​

Signature ​

typescript
viewport?: ViewportStrategy

Oracle strategies for 2D virtualized tables (e.g., react-data-grid, AG Grid) where both rows and columns are recycled as the user scrolls. When present, PST uses these to detect and recover from the 2D scroll hazard.

typescript
viewport: {
  getVisibleRowRange: async ({ root }) =>
    root.evaluate(el => {
      const rows = [...el.querySelectorAll('[aria-rowindex]')];
      const idxs = rows.map(r => Number(r.getAttribute('aria-rowindex')) - 1);
      return idxs.length
        ? { first: Math.min(...idxs), last: Math.max(...idxs) }
        : { first: 0, last: 0 };
    }),

  getVisibleColumnRange: async ({ root }) =>
    root.evaluate(el => {
      const cells = [...el.querySelectorAll('[aria-colindex]')];
      const idxs = cells.map(c => Number(c.getAttribute('aria-colindex')) - 1);
      return idxs.length
        ? { first: Math.min(...idxs), last: Math.max(...idxs) }
        : { first: 0, last: 0 };
    }),

  scrollToRow: async ({ root }, rowIndex) =>
    root.evaluate((el, idx) => {
      const prevScrollLeft = el.scrollLeft;
      const row = el.querySelector(`[aria-rowindex="${idx + 1}"]`);
      if (row) {
        el.scrollTop = Math.max(0, parseInt(row.style.top || '0') - el.clientHeight / 3);
      } else {
        el.scrollTop = Math.max(0, idx * 35 - el.clientHeight / 3);
      }
      el.scrollLeft = prevScrollLeft; // preserve horizontal position
      return new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
    }, rowIndex),

  scrollToColumn: async ({ root }, colIndex) =>
    root.evaluate((el, idx) => {
      const prevScrollTop = el.scrollTop;
      const header = el.querySelector(`[aria-colindex="${idx + 1}"]`);
      if (header) el.scrollLeft = header.offsetLeft;
      el.scrollTop = prevScrollTop; // preserve vertical position
      return new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
    }, colIndex),
}

TIP

After scrolling one axis in a 2D virtualized grid, the other axis is often reset by the grid's React re-render. Save and restore the other axis's scroll position inside the same evaluate() call, then wait two requestAnimationFrame ticks for React to commit.

→ Guide: Virtual Scroll


Sorting ​

sorting ​

Signature ​

typescript
sorting?: SortingStrategy

Implements column sort apply/detect for table.sorting.apply() and table.sorting.getState().

typescript
sorting: {
  apply: async ({ root, page }, columnName, direction) => {
    await root.locator(`[data-col="${columnName}"] .sort-btn`).click();
  },
  getState: async ({ root }, columnName) => {
    const attr = await root.locator(`[data-col="${columnName}"]`).getAttribute('aria-sort');
    return attr === 'ascending' ? 'asc' : attr === 'descending' ? 'desc' : 'none';
  },
}

Fill ​

fill ​

Signature ​

typescript
fill?: FillStrategy

Global fill strategy for smartFill. Applied to all columns unless overridden by columnOverrides.write.

Built-in via Strategies.Fill:

  • Strategies.Fill.default — auto-detects input type (text, select, checkbox, contenteditable)
typescript
fill: Strategies.Fill.default

→ Guide: Fill Cells


Dedupe ​

dedupe ​

Signature ​

typescript
dedupe?: DedupeStrategy

Prevents a record from being yielded twice during a scan. Keys must identify the record across scrolling and pagination, rather than its mounted slot or page-local index.

typescript
// Dedupe by a unique, stable record ID
dedupe: async (row) => row.getCell('ID').innerText()

Avoid viewport-position keys ​

Strategies.Dedupe.byTopPosition() is deprecated. It groups rows by their rounded screen Y coordinate, not by record identity. It only works when each position represents the same record throughout a scan. Scrolling and pagination invalidate that assumption: Alice and Bob can occupy the same slot, causing Bob to be dropped. The helper retains its existing behavior for compatibility.

An index attribute is suitable only if it identifies the record across the whole scan. If no stable key exists, omit dedupe and rely on built-in DOM tracking; this cannot guarantee record-level uniqueness across replacement nodes. Do not substitute random keys or screen coordinates.


Row Index ​

resolveRowIndex ​

Signature ​

typescript
resolveRowIndex?: (row: Locator) => Promise<RowIndexResult | undefined>

Converts a DOM row into its logical (data-model) index. Used by findRow / findRows / iteration for rowIndex, by bringIntoView for virtual-scroll positioning, and required by findRowByIndex.

Return undefined to fall back to DOM position.

Return { index, selector } instead of a plain number when the index lives in a DOM attribute. The library then builds a self-healing row locator from selector that re-queries the DOM on every action, so getCell, smartFill, and toJSON keep pointing at the right row even after the virtualizer recycles DOM nodes.

typescript
// MUI DataGrid: self-healing via data-rowindex
resolveRowIndex: async (row) => {
  const v = await row.getAttribute('data-rowindex').catch(() => null);
  if (!v?.trim() || !Number.isFinite(Number(v))) return undefined;
  return { index: Number(v), selector: `[data-rowindex="${v}"]` };
}

→ Guide: Virtualization


Content Ready ​

contentReady ​

Signature ​

typescript
contentReady?: ContentReadyStrategy

Waits until a row's content has stabilized before toJSON({ atomic: true }) snapshots it. Needed for recycling virtualizers (react-window, react-virtuoso with React concurrent mode) where the row moves into position before its cells re-render — without it, the snapshot can capture the previous row's text.

typescript
strategies: {
  contentReady: Strategies.ContentReady.textStable(),
}

Cell Locator ​

getCellLocator ​

Signature ​

typescript
getCellLocator?: GetCellLocatorFn

Custom function for resolving a cell locator from a row and column index. Use for grids where the default cellSelector + nth-child approach doesn't match DOM structure.

typescript
// react-data-grid: use aria-colindex
getCellLocator: ({ row, columnIndex }) =>
  row.locator(`[role="gridcell"][aria-colindex="${columnIndex + 1}"]`)

Before Cell Read ​

beforeCellRead ​

Signature ​

typescript
beforeCellRead?: BeforeCellReadFn

Hook called before each cell value is read in toJSON and columnOverrides.read. Fires for both default innerText extraction and custom read mappers.

typescript
// Scroll an off-screen column into view before reading
beforeCellRead: async ({ root, columnName, columnIndex }) => {
  await root.evaluate((el, idx) => { el.scrollLeft = idx * 120; }, columnIndex);
}

Signature ​

typescript
navigation?: NavigationPrimitives

Primitive navigation functions used internally for keyboard-based cell navigation (goUp, goDown, goLeft, goRight, goHome).


Filter ​

filter ​

Signature ​

typescript
filter?: FilterStrategy

Pluggable row filter strategy. When present, FilterEngine delegates filter application to this strategy instead of using the default column-value comparison.


Stabilization ​

Used inside pagination strategies to detect when a page transition has settled.

Built-in factories via Strategies.Stabilization:

typescript
// Wait until table content stops changing
Strategies.Stabilization.contentChanged({ scope: 'all', timeout: 3000 })

// Wait a fixed delay
Strategies.Stabilization.timeout(500)