-
-
-
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index b06eaad3..9674b996 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -5,21 +5,19 @@ import DefaultTheme from 'vitepress/theme'
import LabBeforeAfter from './components/lab/LabBeforeAfter.vue'
import LabBeforeAfterV2 from './components/lab/LabBeforeAfterV2.vue'
import LabQueryBuilder from './components/lab/LabQueryBuilder.vue'
-import LabDebugPlayback from './components/lab/LabDebugPlayback.vue'
-import LabFailureStates from './components/lab/LabFailureStates.vue'
import LabFindRowPaginationDebug from './components/lab/LabFindRowPaginationDebug.vue'
import LabInitGetRowDebug from './components/lab/LabInitGetRowDebug.vue'
import LabFeedbackMark from './components/lab/LabFeedbackMark.vue'
-import LabMethodWalkthrough from './components/lab/LabMethodWalkthrough.vue'
-import LabStrategyPicker from './components/lab/LabStrategyPicker.vue'
-import LabTableTypeGallery from './components/lab/LabTableTypeGallery.vue'
import LabGetRowTrace from './components/lab/LabGetRowTrace.vue'
import LabForEachTrace from './components/lab/LabForEachTrace.vue'
import LabPaginationSandbox from './components/lab/LabPaginationSandbox.vue'
+import LabConcurrencyAnimator from './components/lab/LabConcurrencyAnimator.vue'
import HeaderMapping from './components/HeaderMapping.vue'
import PaginationStrategies from './components/PaginationStrategies.vue'
import TableAnatomy from './components/TableAnatomy.vue'
import HomepageHero from './components/HomepageHero.vue'
+import MethodBadge from './components/MethodBadge.vue'
+import ConfigSwatch from './components/ConfigSwatch.vue'
import './style.css'
import { initDevAnnotations } from './annotate-dev'
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
@@ -32,20 +30,18 @@ export default {
app.component('LabBeforeAfter', LabBeforeAfter)
app.component('LabBeforeAfterV2', LabBeforeAfterV2)
app.component('LabQueryBuilder', LabQueryBuilder)
- app.component('LabDebugPlayback', LabDebugPlayback)
- app.component('LabFailureStates', LabFailureStates)
app.component('LabFindRowPaginationDebug', LabFindRowPaginationDebug)
app.component('LabInitGetRowDebug', LabInitGetRowDebug)
app.component('LabFeedbackMark', LabFeedbackMark)
- app.component('LabMethodWalkthrough', LabMethodWalkthrough)
- app.component('LabStrategyPicker', LabStrategyPicker)
- app.component('LabTableTypeGallery', LabTableTypeGallery)
app.component('LabGetRowTrace', LabGetRowTrace)
app.component('LabForEachTrace', LabForEachTrace)
app.component('LabPaginationSandbox', LabPaginationSandbox)
+ app.component('LabConcurrencyAnimator', LabConcurrencyAnimator)
app.component('HeaderMapping', HeaderMapping)
app.component('PaginationStrategies', PaginationStrategies)
app.component('TableAnatomy', TableAnatomy)
app.component('HomepageHero', HomepageHero)
+ app.component('MethodBadge', MethodBadge)
+ app.component('ConfigSwatch', ConfigSwatch)
}
} satisfies Theme
diff --git a/docs/advanced/custom-strategies.md b/docs/advanced/custom-strategies.md
new file mode 100644
index 00000000..1ac510aa
--- /dev/null
+++ b/docs/advanced/custom-strategies.md
@@ -0,0 +1,100 @@
+# Custom Strategies
+
+Strategies allow you to adapt the library to any table implementation, no matter how complex.
+
+## Overview
+
+A Strategy is simply a function that implements specific behavior. You can override ANY default behavior by passing a custom strategy.
+
+## Custom Pagination
+
+Handle complex pagination logic like "Load More" buttons or infinite scroll triggers.
+
+```typescript
+import { useTable, Strategies } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(page.locator('table'), {
+ strategies: {
+ pagination: {
+ goNext: async ({ page, root }) => {
+ // 1. Detect if there's a next page
+ const loadMore = page.locator('button:has-text("Load More")');
+
+ if (await loadMore.isDisabled() || !(await loadMore.isVisible())) {
+ return false; // No more pages
+ }
+
+ // 2. Perform navigation
+ await loadMore.click();
+
+ // 3. Wait for new content
+ await page.waitForResponse(resp => resp.url().includes('/api/data'));
+
+ return true; // Successfully navigated
+ }
+ }
+ }
+});
+```
+
+## Custom Filling
+
+Customize how data is entered into cells (e.g., custom dropdowns, date pickers).
+
+```typescript
+// Example: select a value from a custom dropdown
+const customSelect = async ({ cell, value }) => {
+ // 1. Click cell to open dropdown
+ await cell.click();
+
+ // 2. Wait for dropdown (often attached to body)
+ const option = cell.page().locator(`.dropdown-option:has-text("${value}")`);
+ await option.click();
+
+ // 3. Verify value was set
+ await expect(cell).toHaveText(value);
+};
+
+const table = useTable(loc, {
+ strategies: {
+ fill: customSelect
+ }
+});
+
+// Usage
+await row.smartFill({ Status: 'Active' });
+```
+
+## Custom Cell Resolution
+
+If your table uses a non-standard layout (e.g., grid divs instead of `td`), you can define how to find a cell for a given column.
+
+```typescript
+const table = useTable(loc, {
+ strategies: {
+ getCellLocator: ({ row, columnName, columnIndex }) => {
+ // Example: Cells are identified by a 'data-field' attribute matching the column name
+ return row.locator(`div[data-field="${columnName}"]`);
+ }
+ }
+});
+```
+
+## Reusing Strategies
+
+Strategies are just functions, so you can easily reuse and share them.
+
+```typescript
+// my-strategies.ts
+export const MyCompanyTableStrategies = {
+ pagination: { goNext: async (...) => { ... } },
+ fill: async (...) => { ... }
+};
+
+// test.spec.ts
+import { MyCompanyTableStrategies } from './my-strategies';
+
+const table = useTable(loc, {
+ strategies: MyCompanyTableStrategies
+});
+```
diff --git a/docs/advanced/index.md b/docs/advanced/index.md
new file mode 100644
index 00000000..0d26164e
--- /dev/null
+++ b/docs/advanced/index.md
@@ -0,0 +1,8 @@
+# Advanced
+
+Deep-dive topics for complex setups and custom behavior.
+
+- [Custom Resolution](/advanced/custom-resolution) — ARIA structures, duplicate columns, complex grids.
+- [TypeScript Tips](/advanced/typescript) — type safety, generics, and autocomplete.
+- [Custom Strategies](/advanced/custom-strategies) — pagination, fill, cell locator, and viewport strategies.
+- [Performance Tips](/advanced/performance) — keeping large-table tests fast and reliable.
diff --git a/docs/advanced/performance.md b/docs/advanced/performance.md
new file mode 100644
index 00000000..3a2e23f6
--- /dev/null
+++ b/docs/advanced/performance.md
@@ -0,0 +1,75 @@
+# Performance Tuning
+
+Tips for keeping your table tests fast and reliable, especially with large datasets.
+
+## 1. Use Iteration Methods (`map`, `forEach`, `filter`)
+
+Instead of paging manually:
+
+```typescript
+// ❌ Slow: Looping manually
+for (let i = 0; i < 10; i++) {
+ await table.findRows({}); // or manually clicking next
+}
+
+// ✅ Fast: Built-in iteration
+await table.forEach(async ({ row }) => {
+ // Automation handles pagination seamlessly
+});
+```
+
+## 2. Prefetch Columns
+
+When converting rows to JSON, specify only the columns you need. This prevents the library from trying to read every single cell (which might involve scrolling or complex resolution).
+
+```typescript
+// ❌ Reads ALL columns (slow if many columns)
+const data = await row.toJSON();
+
+// ✅ Reads only specified columns (fast)
+const data = await row.toJSON({
+ columns: ['ID', 'Status', 'Email']
+});
+```
+
+## 3. Avoid `findRow` for simple checks
+
+If you know the row is on the first page, use `getRow()` (sync-like) instead of `findRow()` (async search).
+
+```typescript
+// Slower: async search path, and may scan multiple pages when maxPages is raised
+await table.findRow({ ID: '123' });
+
+// Faster: checks the current page immediately
+table.getRow({ ID: '123' });
+```
+
+## 4. Cache Table Initialization
+
+The `init()` method analyzes headers. You only need to call it once per page load.
+
+```typescript
+const table = useTable(loc);
+
+// Setup (runs init once)
+await table.findRow({ ... });
+
+// Subsequent calls re-use the cached header map
+await table.findRow({ ... });
+```
+
+> [!TIP]
+> If the page creates a fresh table DOM (e.g., during sorting/filtering), the library detects the detached elements and re-initializes automatically.
+
+## 5. Locators vs Text
+
+Always prefer assertions on Locators rather than extracting text.
+
+```typescript
+// ❌ Slow: Extracts text (round trip to browser)
+const text = await row.getCell('Status').innerText();
+expect(text).toBe('Active');
+
+// ✅ Fast: Playwright assertion (runs in browser context)
+await expect(row.getCell('Status')).toHaveText('Active');
+```
diff --git a/docs/api/table-methods.md b/docs/api/table-methods.md
new file mode 100644
index 00000000..37b2949d
--- /dev/null
+++ b/docs/api/table-methods.md
@@ -0,0 +1,693 @@
+
+# Table Methods
+
+Methods available on the `TableResult` object returned by `useTable()`.
+
+
+
+## init()
+
+Initialize the table by reading headers and setting up the column map.
+
+
+Parameters & examples
+
+
+
+### Signature
+
+```typescript
+init(options?: { timeout?: number }): Promise
+```
+
+### Parameters
+
+- `options` - Optional timeout for header resolution (default: 3000ms)
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## isInitialized()
+
+Check if the table has been initialized.
+
+
+Parameters & examples
+
+> [!TIP]
+> This is mostly used internally or for advanced debugging. Async methods like `findRow` call `init()` automatically, so you rarely need to check this manually.
+
+
+
+### Signature
+
+```typescript
+isInitialized(): boolean
+```
+
+
+
+### Returns
+
+`boolean` - true if init() has been called and completed
+
+### Example
+
+```typescript
+const table = useTable(page.locator('#table'));
+
+console.log(table.isInitialized()); // false
+
+await table.init();
+
+console.log(table.isInitialized()); // true
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## getRow()
+
+Get the first row matching the filter criteria on the current **page**. Requires `init()`. For multi-page search use [findRow()](#findrow).
+
+
+Parameters & examples
+
+Filters support `string`, `RegExp`, `number`, or `(cell: Locator) => Locator` for custom locator logic (e.g. checkbox checked).
+
+
+
+### Signature
+
+```typescript
+getRow(
+ filters: Record,
+ options?: { exact?: boolean }
+): SmartRow
+```
+
+
+
+### Example
+
+```typescript
+// ✅ Simple single-column filter
+const row = table.getRow({ Name: 'John' });
+
+// ✅ Multi-column filter (must match ALL)
+const adminRow = table.getRow({
+ Role: 'Admin',
+ Status: 'Active'
+});
+
+// ✅ Regex matching
+const gmailRow = table.getRow({
+ Email: /@gmail\.com$/
+});
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## getRowByIndex()
+
+Get a row by its 0-based index on the current page.
+
+
+Parameters & examples
+
+> [!TIP]
+> Use this when you need stable iteration or access by position, which is faster than filtering by content.
+
+
+
+
+### Signature
+
+```typescript
+getRowByIndex(index: number): SmartRow
+```
+
+### Parameters
+
+- `index` - 0-based row index
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## findRow()
+
+Find **exactly one** row matching the filter. Throws an `"Ambiguous Row"` error if more than one match is found on a page. Use [`findRows()`](#findrows) if you expect multiple matches.
+
+
+Parameters & examples
+
+By default this scans one page (`maxPages: 1`); increase `maxPages` to search through pagination.
+
+> [!NOTE]
+> **Not-found behaviour:** when no row matches, `findRow` returns a sentinel `SmartRow` rather than throwing immediately. Any subsequent interaction on it (`.click()`, `.innerText()`, etc.) will fail with a Playwright locator error at that point. Use [`findRows()`](#findrows) and check `.length` if you need an explicit "not found" assertion.
+
+
+
+### Signature
+
+```typescript
+findRow(
+ filters: Record,
+ options?: { exact?: boolean, maxPages?: number }
+): Promise
+```
+
+### Parameters
+
+- `filters` - The filter criteria to match
+- `options` - Search options including exact match and max pages
+
+
+
+### Example
+
+```typescript
+// Expects exactly one match — throws if two rows both have Name: 'John Doe'
+const row = await table.findRow({ Name: 'John Doe' });
+
+// Search up to the first 5 pages
+const rowWithinFivePages = await table.findRow(
+ { Name: 'John Doe' },
+ { maxPages: 5 }
+);
+
+// With exact match
+const exactRow = await table.findRow(
+ { Email: 'john@example.com' },
+ { exact: true }
+);
+```
+
+### Interactive trace
+
+Step through how `findRow` scans pages, highlights matches, and surfaces the `maxPages` exhausted error — without running a real browser.
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## findRows()
+
+Find rows matching the filter. By default this scans one page (`maxPages: 1`); increase `maxPages` to collect rows across pagination.
+
+
+Parameters & examples
+
+
+
+### Signature
+
+```typescript
+findRows(
+ filters?: Record,
+ options?: { exact?: boolean, maxPages?: number }
+): Promise>
+```
+
+### Parameters
+
+- `filters` - The filter criteria to match (omit or pass {} for all rows)
+- `options` - Search options including exact match and max pages
+
+
+
+To get the JSON content of the rows (using `columnOverrides.read` if configured), simply chain `.toJSON()` to the result:
+
+```ts
+const rows = await table.findRows({ Status: 'Active' });
+const data = await rows.toJSON();
+```
+
+
+### Example
+
+```typescript
+// Find rows matching criteria on the default scan range
+const rows = await table.findRows({ Status: 'Active' });
+
+// Search up to the first 10 pages
+const rowsWithinTenPages = await table.findRows(
+ { Status: 'Active' },
+ { maxPages: 10 }
+);
+
+// With exact match
+const exactRows = await table.findRows(
+ { Department: 'Engineering' },
+ { exact: true }
+);
+```
+
+### See it in action
+
+Walk through a full `findRows` flow end-to-end: table init, header mapping, multi-page scan, match highlight, and the final `getCell` + checkbox interaction — all animated step by step.
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## countRows()
+
+Count the rows visible on the current page. Auto-initializes the table if needed.
+
+
+Parameters & examples
+
+> [!TIP]
+> `countRows()` counts rows on the **current page only**. To count across all pages, use `findRows({}, { maxPages: N })` and check `.length`.
+
+
+
+### Signature
+
+```typescript
+countRows: () => Promise
+```
+
+
+
+### Example
+
+```typescript
+const count = await table.countRows();
+expect(count).toBe(10);
+
+// Current page only — navigate first if needed
+await table.reset();
+const firstPageCount = await table.countRows();
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## forEach()
+
+Iterate rows in the configured scan range, calling the callback for side effects. Sequential by default. Call `stop()` to end early.
+
+
+Parameters & examples
+
+Execution is sequential by default (safe for interactions like clicking/filling). Increase `maxPages` to iterate beyond the first page. Call `stop()` in the callback to end iteration early.
+
+
+
+### Signature
+
+```typescript
+forEach(
+ callback: (ctx: RowIterationContext) => void | Promise,
+ options?: RowIterationOptions
+): Promise
+```
+
+### Parameters
+
+- `callback` - Function receiving { row, rowIndex, stop }
+- `options` - maxPages, concurrency, dedupe, useBulkPagination
+
+
+
+> [!NOTE]
+> `index` is a **visit counter** (0, 1, 2…) — the order this row was encountered during iteration. It is not a DOM position, `data-index`, or grid-internal row identity. With infinite-scroll and deduplication it can diverge from any of those. Use the `row` locator for element-scoped lookups.
+
+### Example
+
+```typescript
+await table.forEach(async ({ row, index, stop }) => {
+ if (await row.getCell('Status').innerText() === 'Done') stop();
+ await row.getCell('Checkbox').click();
+});
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## map()
+
+Transform rows in the configured scan range into values. Returns a flat array. Parallel within each page by default.
+
+
+Parameters & examples
+
+Increase `maxPages` to map beyond the first page. Call `stop()` to halt after the current page finishes.
+
+> [!WARNING]
+> `map` defaults to `concurrency: 'parallel'`. If your callback opens popovers, fills inputs, or mutates UI state, pass `{ concurrency: 'sequential' }` or `{ concurrency: 'synchronized' }` as appropriate.
+
+
+
+### Signature
+
+```typescript
+map(
+ callback: (ctx: RowIterationContext) => R | Promise,
+ options?: RowIterationOptions
+): Promise
+```
+
+### Example
+
+```typescript
+// Data extraction — parallel is safe
+const emails = await table.map(({ row, index }) => row.getCell('Email').innerText());
+
+// UI interactions — use sequential (or synchronized) concurrency
+const assignees = await table.map(async ({ row }) => {
+ await row.getCell('Assignee').locator('button').click();
+ const name = await page.locator('.popover .name').innerText();
+ await page.keyboard.press('Escape');
+ return name;
+}, { concurrency: 'sequential' });
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## filter()
+
+Filter rows in the configured scan range by an async predicate. Returns a [SmartRowArray](/api/smart-row-array).
+
+
+Parameters & examples
+
+Execution is sequential by default. Increase `maxPages` to filter beyond the first page. Call `bringIntoView()` on each row if you need to interact after pagination.
+
+
+
+### Signature
+
+```typescript
+filter(
+ predicate: (ctx: RowIterationContext) => boolean | Promise,
+ options?: RowIterationOptions
+): Promise>
+```
+
+### Example
+
+```typescript
+const active = await table.filter(async ({ row }) =>
+ await row.getCell('Status').innerText() === 'Active'
+);
+
+for (const row of active) {
+ await row.bringIntoView();
+ await row.getCell('Checkbox').click();
+}
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## Async Iterator (`for await...of`)
+
+The table is async iterable. Use `for await...of` for low-level page-by-page iteration.
+
+
+Parameters & examples
+
+```typescript
+for await (const { row, index } of table) {
+ console.log(index, await row.getCell('Name').innerText());
+}
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## getHeaders()
+
+Get all column names.
+
+
+Parameters & examples
+
+
+
+### Signature
+
+```typescript
+getHeaders(): Promise
+```
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## getHeaderCell()
+
+Get the header cell Locator for a specific column.
+
+
+Parameters & examples
+
+
+
+### Signature
+
+```typescript
+getHeaderCell(columnName: string): Promise
+```
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## scrollToColumn()
+
+Scrolls the table horizontally to bring the given column's header into view.
+
+
+Parameters & examples
+
+
+
+### Signature
+
+```typescript
+scrollToColumn(columnName: string): Promise
+```
+
+
+
+### Example
+
+```typescript
+// Scroll to a column that's off-screen
+await table.scrollToColumn('Email');
+
+// Now interact with cells in that column
+const row = table.getRow({ Name: 'John' });
+await row.getCell('Email').click();
+```
+
+
+
+---
+
+[Back to Top](#table-methods)
+
+---
+
+## reset()
+
+Reset table state and invoke the `onReset` strategy.
+
+
+Parameters & examples
+
+> [!WARNING]
+> `reset()` clears internal row cache and flags (`tableMapper.clear()`), calls `pagination.goToFirst()` (if configured) to scroll or paginate back to page 1, and exits any active filter or sort state applied outside the library. Calling `reset()` around filtered/sorted reads may silently return unfiltered data — re-apply filters and sorts after calling it.
+
+Use this between independent test operations to return the table to a clean baseline.
+
+
+
+
+### Signature
+
+```typescript
+reset(): Promise
+```
+
+
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+
+## revalidate()
+
+Revalidate the table's structure without resetting pagination or state.
+
+
+Parameters & examples
+
+Use this when the DOM has changed (e.g. columns toggled) but you want to keep the current pagination/filter state.
+
+
+
+### Signature
+
+```typescript
+revalidate(): Promise
+```
+
+
+
+### Example
+
+```typescript
+// Columns changed dynamically
+await page.click('#toggle-columns');
+
+// Revalidate to pick up new column structure
+await table.revalidate();
+
+// Now you can access the new columns
+const row = table.getRow({ Name: 'John' });
+await row.getCell('NewColumn').click();
+```
+
+### Notes
+
+- Useful when columns change visibility or order dynamically
+- Does not reset pagination state
+- Does not clear row cache
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## sorting
+
+Access sorting methods.
+
+
+Parameters & examples
+
+### apply()
+
+Apply sorting to a column.
+
+```typescript
+await table.sorting.apply('Name', 'asc');
+await table.sorting.apply('Salary', 'desc');
+```
+
+### getState()
+
+Get current sort state for a column.
+
+```typescript
+const state = await table.sorting.getState('Name');
+console.log(state); // 'asc' | 'desc' | 'none'
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## generateConfig()
+
+Generates an AI-friendly configuration prompt for debugging. **Throws an Error** containing the prompt (does not return).
+
+
+Parameters & examples
+
+Outputs table HTML and TypeScript definitions to help AI assistants generate config.
+
+### Signature
+
+```typescript
+generateConfig(): Promise
+```
+
+
+
+[Back to Top](#table-methods)
+
+---
+
+## generateConfigPrompt()
+
+Deprecated alias for `generateConfig()`. Use `generateConfig()` in new code; `generateConfigPrompt()` will be removed in v7.0.0.
+
+
+Parameters & examples
+
+### Signature
+
+```typescript
+generateConfigPrompt(): Promise
+```
+
+
diff --git a/docs/examples/ag-grid.md b/docs/examples/ag-grid.md
new file mode 100644
index 00000000..5a046cd0
--- /dev/null
+++ b/docs/examples/ag-grid.md
@@ -0,0 +1,146 @@
+
+# AG Grid Example
+
+AG Grid is a high-performance table library that is fully virtualized and uses a complex DOM structure.
+
+## Configuration
+
+> [!WARNING]
+> AG Grid has a complex DOM that relies on specific class names (e.g. `.ag-cell-value`) which are internal implementation details. These may change in future AG Grid versions. Using stable test attributes (like `data-test-id`) is recommended if you have control over the source code.
+
+AG Grid is effectively tested by targeting its specific class names.
+
+### Basic Setup
+
+```typescript
+import { useTable, Strategies } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(page.locator('.ag-root-wrapper'), {
+ // AG Grid standard classes
+ headerSelector: '.ag-header-cell-text', // Target the text container directly
+ rowSelector: '.ag-row',
+ cellSelector: '.ag-cell',
+
+ strategies: {
+ // AG Grid uses aria-sort
+ sorting: Strategies.Sorting.AriaSort(),
+
+ // Custom pagination (if using standard paging)
+ pagination: Strategies.Pagination.click({ next: '.ag-paging-button[aria-label="Next Page"]' })
+ }
+});
+
+await table.init();
+```
+
+## Virtualization & Column Virtualization
+
+AG Grid virtualizes both rows (vertical) and columns (horizontal).
+
+### Horizontal Scrolling
+
+If columns are off-screen, `getCell()` won't find them by default. You need a `navigation` strategy or to use `scrollToColumn()`.
+
+```typescript
+// 1. Scroll to column manually
+await table.scrollToColumn('Status');
+await row.getCell('Status').click();
+
+// 2. Or configure a strategy to auto-scroll using right arrow
+strategies: {
+ navigation: {
+ goRight: async () => {
+ await page.keyboard.press('ArrowRight');
+ },
+ goLeft: async () => {
+ await page.keyboard.press('ArrowLeft');
+ }
+ }
+}
+```
+
+### Row Virtualization
+
+For "Infinite Scroll" or "Server Side Row Model" in AG Grid:
+
+## The Structure
+
+AG Grid is complex because it splits headers and body content, and often uses row virtualization.
+
+```mermaid
+graph TD
+ subgraph "AG Grid Container"
+ H[Header Container .ag-header]
+ B[Body Container .ag-body-viewport]
+ end
+
+ H --> H1[.ag-header-cell]
+ H --> H2[.ag-header-cell]
+
+ B --> R1[.ag-row]
+ B --> R2[.ag-row]
+
+ R1 --> C1[.ag-cell value="Name"]
+ R1 --> C2[.ag-cell value="Role"]
+
+ style H fill:#f9f,stroke:#333,stroke-width:2px
+ style B fill:#bbf,stroke:#333,stroke-width:2px
+```
+
+```typescript
+strategies: {
+ pagination: Strategies.Pagination.infiniteScroll({
+ scrollTarget: '.ag-body-viewport',
+ })
+}
+```
+
+## Checkbox Selection
+
+AG Grid often has a checkbox column. You can resolve it using `getRowByIndex` or custom selectors.
+
+```typescript
+test('select row', async ({ page }) => {
+ const table = useTable(page.locator('#myGrid'));
+
+ const row = await table.findRow({ Model: 'Y' });
+
+ // Assuming the first column is the checkbox
+ await row.locator('.ag-selection-checkbox').click();
+
+ // Or if it's a named column
+ await row.getCell('Select').locator('input').check();
+});
+```
+
+## Complete Test Example
+
+```typescript
+test('AG Grid flow', async ({ page }) => {
+ await page.goto('https://www.ag-grid.com/example/');
+
+ const table = useTable(page.locator('#myGrid'), {
+ headerSelector: '.ag-header-cell-text',
+ rowSelector: 'div[role="row"]',
+ cellSelector: 'div[role="gridcell"]',
+ });
+
+ // Sort by Price
+ await table.sorting.apply('Price', 'desc');
+
+ // Find expensive car
+ const row = await table.findRow({
+ Make: 'Porsche',
+ Model: 'Boxster'
+ });
+
+ // Verify
+ await expect(row.getCell('Price')).toHaveText('72000');
+});
+```
+
+## Next Steps
+
+Learn about advanced patterns, validation, and scraping.
+
+[Custom Strategies >](/advanced/custom-strategies)
diff --git a/docs/examples/data-scraping.md b/docs/examples/data-scraping.md
new file mode 100644
index 00000000..ccb398c7
--- /dev/null
+++ b/docs/examples/data-scraping.md
@@ -0,0 +1,100 @@
+# Data Scraping
+
+Learn how to efficiently extract data from tables, whether they are small, paginated, or infinitely scrolled.
+
+## Extracting All Data
+
+The most efficient way to scrape a table is using `table.map()`. It processes rows in chunks and follows pagination up to the configured `maxPages` limit.
+
+```typescript
+// Define the data shape you want to extract
+interface User {
+ id: string;
+ name: string;
+ email: string;
+}
+
+const allUsers = await table.map(async ({ row }) => {
+ return {
+ id: await row.getCell('ID').innerText(),
+ name: await row.getCell('Name').innerText(),
+ email: await row.getCell('Email').innerText()
+ };
+});
+
+console.log(`Extracted ${allUsers.length} users`);
+```
+
+## Handling Large Datasets
+
+For very large tables (1000+ rows), accumulation in memory might be too heavy. You can process data in chunks or write to a file directly.
+
+```typescript
+import fs from 'fs';
+
+const stream = fs.createWriteStream('users.csv');
+stream.write('ID,Name,Email\n');
+
+await table.forEach(
+ async ({ row }) => {
+ const id = await row.getCell('ID').innerText();
+ const name = await row.getCell('Name').innerText();
+ const email = await row.getCell('Email').innerText();
+
+ stream.write(`${id},${name},${email}\n`);
+ }
+);
+
+stream.end();
+```
+
+## Scraping Specific Columns
+
+If you only need values from a single column, use `table.map()`. Increase `maxPages` when you want to scan beyond the first page.
+
+```typescript
+const emails = await table.map(
+ ({ row }) => row.getCell('Email').innerText(),
+ { maxPages: 5 }
+);
+
+// Get and transform values (e.g., parse currency)
+const salaries = await table.map(async ({ row }) => {
+ const text = await row.getCell('Salary').innerText();
+ return parseFloat(text.replace('$', '').replace(',', ''));
+});
+```
+
+## Handling Dynamic Content
+
+Some tables load data lazily. You might need to wait for cell content to be non-empty.
+
+```typescript
+const allData = await table.map(
+ async ({ row }) => {
+ // Wait for specific cell to have content
+ await expect(row.getCell('Status')).not.toBeEmpty();
+ // Or wait for a specific condition
+ await row.getCell('Status').locator('.badge').waitFor();
+
+ // Now extract data...
+ return row.toJSON();
+ }
+);
+```
+
+## Exporting to JSON
+
+You can easily dump the current page or specific rows to JSON.
+
+```typescript
+// Dump current page
+const pageData = await table.findRows({}).then(r => r.toJSON());
+
+// Dump specific rows
+const activeUsers = await table.findRows({ Status: 'Active' });
+const json = await activeUsers.toJSON();
+
+// Write to file
+fs.writeFileSync('active-users.json', JSON.stringify(json, null, 2));
+```
diff --git a/docs/examples/index.md b/docs/examples/index.md
index 684c525d..d5776ac3 100644
--- a/docs/examples/index.md
+++ b/docs/examples/index.md
@@ -1,25 +1,48 @@
# Examples
-Real tables, real configs, real queries.
+Pick the example closest to what you are trying to do. If you are new, start with Basic Usage, then Pagination.
-_Future: pick a grid type and see a live table + config + queries inline. Default: MUI DataGrid._
+## By Task
-## MUI DataGrid
+| I want to... | Start here |
+|---|---|
+| Find a row and assert a cell | [Basic Usage](/examples/basic) |
+| Search across pages | [Pagination](/examples/pagination) |
+| Work with infinite scroll | [Infinite Scroll](/examples/infinite-scroll) |
+| Extract many rows into data | [Data Scraping](/examples/data-scraping) |
+| Use MUI DataGrid | [MUI DataGrid](/examples/mui-datagrid) |
+| Use AG Grid | [AG Grid](/examples/ag-grid) |
+| Write a custom strategy | [Custom Strategies](/advanced/custom-strategies) |
-_TBD — config, then 3–4 common queries_
+## Common Starting Points
-## AG Grid
+### Assert a Cell by Column Name
-_TBD_
+```typescript
+const row = table.getRow({ Name: 'John Doe' });
+await expect(row.getCell('Email')).toHaveText('john@example.com');
+```
-## Standard HTML table
+### Find Rows Across Pages
-_TBD_
+```typescript
+const engineers = await table.findRows({ Department: 'Engineering' });
+expect(engineers.length).toBeGreaterThan(0);
+```
-## Infinite scroll
+### Extract Data
-_TBD_
+```typescript
+const data = await table.map(({ row }) => row.toJSON());
+```
----
+### Fill Editable Cells
-_Outline — content TBD_
+```typescript
+const row = table.getRow({ ID: '12345' });
+await row.smartFill({ Email: 'new.email@example.com' });
+```
+
+## Need API Details?
+
+Use the [API Reference](/api/) when you know the method or config option and need exact signatures.
diff --git a/docs/guide/concurrency.md b/docs/guide/concurrency.md
new file mode 100644
index 00000000..c890c2d4
--- /dev/null
+++ b/docs/guide/concurrency.md
@@ -0,0 +1,131 @@
+---
+aside: false
+pageClass: table-anatomy-doc
+---
+
+
+
+# Concurrency Modes
+
+When you call `forEach`, `map`, or `filter` across a paginated table, Smart Table has to decide how to schedule the work for each row. That scheduling decision is the **concurrency mode**.
+
+Getting it right matters: the wrong mode for your table can produce flaky reads, corrupt writes, or needlessly slow test runs.
+
+## The three modes
+
+### Sequential
+
+```typescript
+await table.forEach(async (row) => {
+ const name = await row.getCell('Name').innerText()
+ console.log(name)
+})
+```
+
+One row at a time, in order. The callback for row _N_ finishes before the callback for row _N+1_ begins. Pages advance only after all callbacks on the current page complete.
+
+**When to use it:**
+
+- Your callback clicks a button, opens a modal, or triggers any DOM change that affects other rows.
+- You need predictable order for logging or assertions.
+- The table renders cells into a tooltip or popover that can only be open for one row at a time (scenario 3 in the interactive below).
+- Debugging: sequential makes traces easy to read.
+
+**Cost:** All rows are processed in series — total time scales with `rows × time-per-row`.
+
+---
+
+### Parallel
+
+```typescript
+await table.forEach(async (row) => {
+ const value = await row.getCell('Status').innerText()
+ results.push(value)
+}, { concurrency: 'parallel' })
+```
+
+All rows on the current page run their callbacks at the same time. Pages still advance one at a time (all callbacks on page _N_ must settle before page _N+1_ is loaded), but within a page, callbacks overlap freely.
+
+**When to use it:**
+
+- Read-only operations where each callback is independent — extracting text, asserting values, or pushing data into an accumulator.
+- All the cells you need are already in the DOM on every page (no column scrolling, no tooltips).
+- Speed is a priority and you have confirmed there are no shared side-effects between callbacks.
+
+**Cost:** Much faster than sequential for read-heavy workloads. Be cautious: if callbacks interact with shared UI state, races will produce wrong reads or silent data corruption.
+
+---
+
+### Synchronized
+
+```typescript
+await table.forEach(async (row) => {
+ const dept = await row.getCell('Department').innerText()
+ results.push(dept)
+}, { concurrency: 'synchronized' })
+```
+
+Rows within a page run their callbacks in parallel up to a _barrier_: any operation that touches shared viewport state (such as `scrollToColumn`) pauses until all sibling callbacks have reached the same point, then all proceed together. Pages still advance sequentially.
+
+**When to use it:**
+
+- Your table virtualizes columns (only a subset of columns are in the DOM at once) and callbacks call `scrollToColumn`.
+- Without a barrier, parallel callbacks scroll the column viewport independently and land out of sync with each other.
+- You want page-level parallelism for speed, but need coordinated navigation for correctness.
+
+**Cost:** Slightly slower than plain parallel because of barrier synchronization, but significantly faster than sequential across many rows.
+
+---
+
+## Setting a default
+
+You can set the default mode for the whole table in `useTable` config:
+
+```typescript
+// All forEach / map / filter calls default to parallel
+const table = useTable(locator, {
+ concurrency: 'parallel'
+})
+```
+
+Per-call options always override the table-level default:
+
+```typescript
+// Table default is parallel, but this call runs sequentially
+await table.forEach(async (row) => {
+ await row.getCell('Action').locator('button').click()
+}, { concurrency: 'sequential' })
+```
+
+---
+
+## Interactive
+
+The animator below runs the same 3-page `forEach` in all three modes side by side. Pick a scenario to see how the mode that works for simple tables can break down when the table has shared UI state.
+
+
+
+## What to notice
+
+- **Scenario 1 — All cells in DOM:** Parallel and synchronized finish in roughly the same time and both produce correct results. Sequential is noticeably slower. Synchronized is marked "same as parallel" here because there is no column scroll barrier to coordinate — prefer parallel when columns are always in the DOM.
+
+- **Scenario 2 — Columns virtualized:** Parallel breaks. Each row fires `scrollToColumn` independently; they race to scroll the same viewport and settle out of sync, producing wrong or missing reads. Synchronized introduces a barrier so all rows scroll together and land on the correct column every time.
+
+- **Scenario 3 — Click for name (tooltip):** Only one cell can be open at a time — clicking a second row closes the first. Parallel tries to click all rows on the page simultaneously; almost every read gets nothing. Synchronized coordinates the column scroll, not the row callbacks, so the conflict still occurs. Sequential is the only correct choice here: it opens, reads, and closes each row before moving to the next.
+
+- **Elapsed time badge:** Shows the real (simulated) wall-clock time for each mode. With many pages and fast callbacks, the speed advantage of parallel over sequential can be substantial.
+
+- **Speed picker:** Use "2× slower" or "4× slower" to see the scheduling pattern clearly. "Realistic" collapses the gaps — at real Playwright speeds the difference in timing is dramatic but harder to see in the animator.
+
+---
+
+## Quick-reference table
+
+| Scenario | Sequential | Parallel | Synchronized |
+| :--- | :---: | :---: | :---: |
+| Read-only, all columns in DOM | ✓ | ✓ recommended | ✓ (same as parallel) |
+| Read-only, columns virtualized | ✓ | ✗ races | ✓ recommended |
+| Click / mutation per row | ✓ recommended | ✗ conflicts | ✗ conflicts |
+| Tooltip / modal per row | ✓ recommended | ✗ conflicts | ✗ conflicts |
+
+See [`concurrency`](/api/table-config#concurrency) in the API reference for the exact type signature and per-call override syntax.
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
new file mode 100644
index 00000000..328d92a2
--- /dev/null
+++ b/docs/guide/configuration.md
@@ -0,0 +1,145 @@
+
+# Configuration Guide
+
+The default configuration works out of the box for standard HTML tables (`
`, ``, ``, `
`, `
`).
+
+However, modern web applications often use `div` structures, virtualization, or custom components. This guide explains how to adapt Playwright Smart Table to any structure.
+
+## Selectors
+
+The most common configuration is telling the library where to find headers, rows, and cells.
+
+### Standard Tables
+
+If your HTML looks like this, you don't need any config:
+
+```html
+
+
+
Name
Email
+
+
+
John
john@example.com
+
+
+```
+
+### Div-Based Tables
+
+For grids built with `div`s (e.g., AG Grid, React Data Grid), you need to specify selectors:
+
+```typescript
+const table = useTable(page.locator('.grid-container'), {
+ headerSelector: '.header-row .header-cell',
+ rowSelector: '.body-row',
+ cellSelector: '.cell'
+});
+```
+
+> [!TIP]
+> You can pass functions for `headerSelector` and `cellSelector` when you need complex locator logic:
+> ```typescript
+> cellSelector: (row) => row.locator('div.cell').filter({ hasNotText: 'Loading...' })
+> ```
+
+
+Header Transformation
+
+Sometimes the text visible in the header isn't what you want to use in your tests.
+
+- **Whitespace**: " First Name " -> "First Name"
+- **Case**: "EMAIL" -> "Email"
+- **Updates**: "Status (Sortable)" -> "Status"
+
+Use `headerTransformer` to normalize these names BEFORE they are used in the library.
+
+```typescript
+const table = useTable(loc, {
+ headerTransformer: async ({ text, index }) => {
+ // 1. Trim whitespace
+ let normalized = text.trim();
+
+ // 2. Remove icons/metadata
+ normalized = normalized.replace(/🔼|🔽/g, '');
+
+ // 3. Normalize case (optional)
+ return normalized;
+ }
+});
+
+// Now you can use the clean name:
+const row = table.getRow({ 'First Name': 'Ada' });
+row.getCell('First Name');
+```
+
+
+
+
+Strategies
+
+Configuration is also where you attach behavior strategies.
+
+```typescript
+import { Strategies } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(loc, {
+ strategies: {
+ // Handle "Load More" buttons
+ pagination: Strategies.Pagination.click({ next: '.load-more-btn' }),
+
+ // Handle column sorting
+ sorting: Strategies.Sorting.AriaSort()
+ }
+});
+```
+
+See [Strategies](/api/strategies) for full details.
+
+
+
+
+Debugging Config
+
+Enable debug mode to see exactly what the library is doing: detecting headers, matching rows, and executing strategies.
+
+```typescript
+const table = useTable(loc, {
+ debug: {
+ logLevel: 'verbose', // See every decision
+ slow: 500 // Slow down interactions by 500ms
+ }
+});
+```
+
+
+
+
+Dynamic Config (Reuse Across Tests)
+
+You can reuse configuration objects across tests.
+
+```typescript
+// table-config.ts
+export const AGGridConfig = {
+ headerSelector: '.ag-header-cell',
+ rowSelector: '.ag-row',
+ cellSelector: '.ag-cell',
+ headerTransformer: ({ text }) => text.trim()
+};
+
+// test.spec.ts
+import { AGGridConfig } from './table-config';
+
+test('verify grid', async ({ page }) => {
+ const table = useTable(page.locator('#my-grid'), AGGridConfig);
+ await table.init();
+});
+```
+
+
+
+## Next Steps
+
+Now that you've configured your table, explore the API reference to see what methods are available.
+
+[Go to API Reference >](/api/)
diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md
new file mode 100644
index 00000000..04a07785
--- /dev/null
+++ b/docs/guide/filtering.md
@@ -0,0 +1,155 @@
+
+# Filtering & Queries
+
+Smart Table locates rows by matching column values rather than by DOM position. This page explains how that filter model works, what happens when something goes wrong, and how to build queries that hold up in real test suites.
+
+## The filter model
+
+A filter is a plain object where each key is a **column name** (exactly as it appears in the table header) and each value is the **cell text** to match against.
+
+```typescript
+// Match the row where the "Name" column contains "Airi Satou"
+const row = table.getRow({ Name: 'Airi Satou' });
+
+// Match on multiple columns simultaneously — all conditions must hold
+const row = table.getRow({ Office: 'Tokyo', Status: 'Active' });
+```
+
+Under the hood, Smart Table turns each key/value pair into a Playwright `.filter({ has: ... })` call. The filter scopes to the correct column index using the header map built during `init()`, so column order in the DOM does not matter.
+
+### Exact vs. partial matching
+
+By default, Smart Table uses Playwright's `getByText(value, { exact: true })`. An exact match requires the full cell text — not a substring. Pass `exact: false` in the table config if you need substring matching.
+
+```typescript
+const table = useTable(page.locator('#employees'), { exact: false });
+
+// Matches "Airi Satou" even if the cell reads "Airi Satou (lead)"
+const row = table.getRow({ Name: 'Airi Satou' });
+```
+
+### Filter values
+
+Values can be:
+
+| Type | Example | Behavior |
+|---|---|---|
+| `string` | `'Tokyo'` | Text match (exact or partial, per config) |
+| `number` | `42` | Converted to string, then matched |
+| `(cell) => Locator` | `(cell) => cell.locator('[aria-checked]')` | Locator-based — use for checkboxes, icons, nested elements |
+
+
+`getRow` vs `findRow`
+
+Both accept the same filter object. The difference is pagination.
+
+```typescript
+// Synchronous — current page only. Requires init() first.
+const row = table.getRow({ Name: 'Airi Satou' });
+
+// Async — paginates until found or maxPages exhausted. Auto-initializes.
+const row = await table.findRow({ Name: 'Colleen Hurst' });
+```
+
+`getRow` throws if zero rows match and also if more than one row matches — it expects exactly one result. Use `findRows` when you expect multiple matches.
+
+```typescript
+// Collect all engineers across pages
+const engineers = await table.findRows({ Role: 'Engineer' });
+```
+
+
+
+
+Interactive query builder
+
+Edit the filter object below and watch how it maps to the table on the right. Add fields, combine conditions, and misspell a column name to see the error message Smart Table produces.
+
+
+
+
+
+
+Typos and the column-not-found error
+
+Column names are case-sensitive. When a key does not match any header, Smart Table throws immediately — before any network requests or page navigation — with a message that includes fuzzy suggestions:
+
+```text
+Column 'Nme' not found
+
+Did you mean:
+ • Name (86% match)
+
+Available columns: Name, Role, Office, Status, Department
+
+Tip: Column names are case-sensitive
+```
+
+The suggestions come from a weighted Levenshtein distance, so a single transposition or missing character typically surfaces the right column at the top of the list.
+
+### Type safety as a first line of defence
+
+If you declare your row type, TypeScript catches typos at compile time — no runtime error needed.
+
+```typescript
+type Employee = {
+ Name: string;
+ Role: string;
+ Office: string;
+ Status: string;
+ Department: string;
+};
+
+const table = useTable(page.locator('#employees'));
+
+// TypeScript error: '"Nme"' is not assignable to keyof Employee
+const row = await table.findRow({ Nme: 'Airi Satou' });
+```
+
+
+
+
+Locator-based filters
+
+For cells whose content cannot be read as plain text — checkboxes, icon-only status columns, custom renderers — pass a function instead of a string.
+
+```typescript
+// Find the row where the "Active" column contains a checked checkbox
+const row = table.getRow({
+ Name: 'Airi Satou',
+ Active: (cell) => cell.locator('[aria-checked="true"]')
+});
+```
+
+The function receives the `cell` locator (already scoped to the correct column) and must return a locator that Playwright uses as a `has` constraint.
+
+
+
+
+Combining filters with pagination
+
+Pass a pagination strategy when the target row may not be on the first page.
+
+```typescript
+import { Strategies, useTable } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(page.locator('#employees'), {
+ strategies: {
+ pagination: Strategies.Pagination.click({
+ next: () => page.getByRole('link', { name: 'Next' })
+ })
+ },
+ maxPages: 10
+});
+
+// Searches up to 10 pages
+const row = await table.findRow({ Department: 'Engineering', Office: 'Berlin' });
+await expect(row.getCell('Name')).toHaveText('George Fox');
+```
+
+
+
+## Next steps
+
+- [Table Methods API](/api/table-methods) — full signatures for `getRow`, `findRow`, `findRows`, `filter`, `forEach`, and `map`.
+- [Configuration](/guide/configuration) — `exact`, `maxPages`, `strategies`, and selector overrides.
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
new file mode 100644
index 00000000..c6ce1f77
--- /dev/null
+++ b/docs/guide/getting-started.md
@@ -0,0 +1,124 @@
+
+# Getting Started
+
+This guide gets you from install to a useful table test. Start here if you are new to the library.
+
+## Installation
+
+```bash
+npm install @rickcedwhat/playwright-smart-table
+```
+
+## Your First Test
+
+```typescript
+import { test, expect } from '@playwright/test';
+import { useTable } from '@rickcedwhat/playwright-smart-table';
+
+test('find and verify employee', async ({ page }) => {
+ await page.goto('https://datatables.net/examples/data_sources/dom');
+
+ const table = await useTable(page.locator('#example')).init();
+ const row = table.getRow({ Name: 'Airi Satou' });
+
+ await expect(row.getCell('Position')).toHaveText('Accountant');
+ await expect(row.getCell('Office')).toHaveText('Tokyo');
+});
+```
+
+## Why This Works
+
+Those three lines hide a small but important pipeline. Press **Play** to step through it and see what happens internally each time a line runs.
+
+
+
+Here is what you just saw:
+
+1. **`init()` builds the header map.** Smart Table reads the `
` elements once and records which column index belongs to each name — for example `Name → 1`, `Office → 4`. This map is frozen for the lifetime of the table object, so every subsequent lookup is O(1).
+
+2. **`getRow()` translates your filter into a Playwright locator.** It looks up each key in the header map and builds a `.filter()` chain — one filter per column. No DOM access happens yet; you get back a locator that is ready to evaluate.
+
+3. **`getCell()` returns a plain Playwright `Locator`.** You can pass it straight to `expect()`, `click()`, `fill()`, or any other Playwright API.
+
+4. **Typos are caught at the filter stage.** If a key does not match any header, Smart Table throws immediately with a list of close matches — no cryptic “element not found” error deep in a test run.
+
+That is why your test can say “the row where `Name` is `Airi Satou`, then the `Office` cell” instead of relying on `nth()` indexes that break the moment a column is reordered.
+
+## Which Method Should I Use?
+
+| If you need to... | Use this |
+|---|---|
+| Find a row on the current page | `table.getRow(filters)` |
+| Search through paginated data for one row | `await table.findRow(filters, { maxPages })` |
+| Collect matching rows across pages | `await table.findRows(filters, { maxPages })` |
+| Read a value from every row | `await table.map(({ row }) => ...)` |
+| Click, fill, or assert every row in order | `await table.forEach(async ({ row }) => ...)` |
+
+### Current Page
+
+Use `getRow()` when the row is already visible. Because it is synchronous, call `init()` first.
+
+```typescript
+const table = await useTable(page.locator('#example')).init();
+
+const row = table.getRow({ Name: 'Airi Satou' });
+await expect(row.getCell('Office')).toHaveText('Tokyo');
+```
+
+### Paginated Tables
+
+Use `findRow()` or `findRows()` when the table may need to move through pages. Add a pagination strategy so Smart Table knows how to click Next, and increase `maxPages` above the default of `1`.
+
+```typescript
+import { Strategies, useTable } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(page.locator('#example'), {
+ strategies: {
+ pagination: Strategies.Pagination.click({
+ next: () => page.getByRole('link', { name: 'Next' })
+ })
+ },
+ maxPages: 5
+});
+
+const row = await table.findRow({ Name: 'Colleen Hurst' });
+await expect(row.getCell('Office')).toHaveText('San Francisco');
+```
+
+### Reading Every Row
+
+Use `map()` for data extraction. It returns a plain array.
+
+```typescript
+const offices = await table.map(({ row }) => row.getCell('Office').innerText());
+expect(offices).toContain('Tokyo');
+```
+
+Use `forEach()` for ordered interactions.
+
+```typescript
+await table.forEach(async ({ row }) => {
+ await expect(row.getCell('Status')).toHaveText('Active');
+});
+```
+
+## When Defaults Do Not Work
+
+Standard HTML tables often work with no selector config. For `div`-based grids, tell Smart Table where headers, rows, and cells live.
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ headerSelector: '[role="columnheader"]',
+ rowSelector: '[role="row"]',
+ cellSelector: '[role="gridcell"]'
+});
+```
+
+If your table needs special behavior, use a strategy. Common examples are pagination, sorting, virtualized columns, and custom filling.
+
+## Next Steps
+
+- [How It Works](/guide/table-anatomy): understand the selector model, header mapping, and pagination.
+- [Examples](/examples/): pick a task-based example.
+- [Configuration](/guide/configuration): adapt Smart Table to a custom DOM.
+- [API Reference](/api/): look up exact method and config details.
diff --git a/docs/guide/header-mapping.md b/docs/guide/header-mapping.md
new file mode 100644
index 00000000..e955af6a
--- /dev/null
+++ b/docs/guide/header-mapping.md
@@ -0,0 +1,22 @@
+---
+aside: false
+pageClass: table-anatomy-doc
+---
+
+
+
+# Header Mapping
+
+Headers become the names you use in filters and `row.getCell()`. Real app tables often include unlabeled checkbox columns, sort arrows, counts, percentages, or other UI-only text.
+
+This visual focuses on how Smart Table turns rendered headers into stable column names.
+
+
+
+## What To Notice
+
+- Blank headers are still addressable with fallback names like `__col_0`.
+- `headerTransformer` runs before names are stored, so you can remove sort arrows, counters, badges, or suffixes.
+- The final names are what you use in calls like `table.getRow({ Name: 'Airi Satou' })` and `row.getCell('Office')`.
+
+For the exact API, see [`headerSelector`](/api/table-config#headerselector) and [`headerTransformer`](/api/table-config#headertransformer).
diff --git a/docs/guide/pagination.md b/docs/guide/pagination.md
new file mode 100644
index 00000000..5202332e
--- /dev/null
+++ b/docs/guide/pagination.md
@@ -0,0 +1,41 @@
+---
+aside: false
+pageClass: table-anatomy-doc
+---
+
+
+
+# Pagination Strategies
+
+Pagination is the part of table testing that most often depends on your app. Smart Table does not guess how your UI moves; you describe the movement with a pagination strategy.
+
+This page shows common pagination shapes and the strategy primitive that usually fits.
+
+
+
+## What To Notice
+
+- `goNext` and `goPrevious` are enough for simple one-page-at-a-time searches.
+- `goToFirst` is useful when a scan or reset should start from page one.
+- `goToLast` enables optimal path planning (wrap-around) when navigating to distant pages.
+- `goToPage` fits numbered pagination or page inputs.
+- `numberOfPages` allows Smart Table to calculate the most efficient path between distant pages (e.g., jumping to the last page first).
+- Infinite scroll and load-more UIs still use the pagination strategy slot; the strategy just scrolls or loads instead of clicking a numbered pagination component.
+
+Each primitive should return `true` when movement happened and `false` when there is nowhere else to go. For exact signatures, see [Pagination Strategies](/api/strategies#pagination-strategies).
+
+---
+
+## Try It — Pagination Config Builder
+
+The sandbox below lets you explore how different pagination setups translate directly into library config. Switch between pagination types, toggle optional selectors on and off, and watch the generated config update live. Use the plan builder to pick a target page and see exactly which primitives Smart Table would call to get there — then step through or run them all at once against the mock table.
+
+
+
+### What to notice
+
+- **Toggling `first` / `last` off** removes those lines from the config and changes the plan: without `goToLast`, wrap-around paths are unavailable; without `goToFirst`, backward navigation must rely on `previous` or `previousBulk` alone.
+- **`nextBulk` / `previousBulk` trade fewer clicks for a coarser granularity.** Enable them to see the planner skip entire decades of pages instead of stepping one at a time — but note that `nextBulkPages` must match the actual number of pages your UI advances per click.
+- **`numberOfPages` unlocks wrap-around.** When it is enabled and `last` is present, the planner can jump to the end and work backwards — often the shortest path to a high-numbered page.
+- **Load More and Infinite Scroll are forward-only.** Once content is loaded it stays in the DOM; the library only needs to advance, never retreat. That is why `goToFirst` and `goToPrevious` do not appear in those configs.
+- **Every config is minimal by design.** You only describe the controls that actually exist in your UI. Anything you omit is simply not used by Smart Table during a search.
diff --git a/docs/guide/strategies.md b/docs/guide/strategies.md
new file mode 100644
index 00000000..2a6296db
--- /dev/null
+++ b/docs/guide/strategies.md
@@ -0,0 +1,101 @@
+# Understanding Strategies
+
+This library uses the **Strategy Pattern** to handle the wide variety of table implementations found on the web.
+
+## What is a Strategy?
+
+Think of a Strategy as a **"Driver"** for a specific mechanism.
+
+Playwright Smart Table knows *what* it wants to do (e.g., "Go to the next page"), but it doesn't know *how* to do it for your specific table (e.g., "Click the button with class `.next-btn`").
+
+A Strategy is a function you provide that tells the library **how** to perform that specific action.
+
+| Feature | **Strategy** | **Callback / Event** |
+| :--- | :--- | :--- |
+| **Purpose** | Defines **HOW** to do something (Logic) | Reacts **WHEN** something happens (Side Effect) |
+| **Necessity** | Essential (Defaults provided, but replaceable) | Optional (Table works fine without them) |
+| **Return Value** | **Critical** (Controls flow, logic, success/fail) | **Ignored** (Usually void) |
+| **Mental Model** | "Plug-in Engine Component" | "Event Subscriber" |
+
+## Example: Pagination Strategy
+
+When you ask the table to find a row that isn't on the current page, the library relies on the `pagination` strategy.
+
+The library asks: *"I need to see more rows. Please execute the pagination logic and tell me if it worked."*
+
+```typescript
+// Your custom strategy
+const myPaginationStrategy = {
+ goNext: async (context) => {
+ const nextBtn = context.page.locator('.next-page-button');
+
+ // 1. Perform the action (The "How")
+ if (await nextBtn.isVisible() && await nextBtn.isEnabled()) {
+ await nextBtn.click();
+ await context.page.waitForLoadState('networkidle');
+
+ // 2. Return the result (Crucial!)
+ return true; // "Yes, I successfully navigated to a new page"
+ }
+
+ return false; // "No, I couldn't paginate anymore (end of data)"
+ }
+};
+
+// Configuring it
+const table = useTable(loc, {
+ strategies: {
+ pagination: myPaginationStrategy
+ }
+});
+```
+
+
+## Example: Sorting Strategy
+
+When you call `table.sorting.apply('Name', 'asc')`, the library delegates the action to your sorting strategy.
+
+```typescript
+// Custom sorting logic for a table where headers are clickable
+const mySortingStrategy = {
+ doSort: async ({ columnName, direction, context }) => {
+ const header = await context.table.getHeaderCell(columnName);
+
+ // Check current state (maybe it's already sorted?)
+ const currentState = await header.getAttribute('aria-sort');
+ if (currentState === direction) return;
+
+ // Click to sort
+ await header.click();
+
+ // Wait for sort to apply
+ await context.page.waitForResponse(resp => resp.url().includes('/api/data'));
+ },
+ getSortState: async ({ columnName, context }) => {
+ const header = await context.table.getHeaderCell(columnName);
+ return await header.getAttribute('aria-sort') as 'asc' | 'desc' | 'none';
+ }
+};
+```
+
+## Example: Loading Strategy
+
+The `loading` strategy is critical for stability. It tells the library *"Wait! The table is busy."*
+
+The table uses these strategies under the hood to determine if it needs to wait before proceeding with an action (like finding a row).
+
+If this strategy returns `true`, the library will assume the table is busy and retry the operation until it returns `false`.
+
+```typescript
+const myLoadingStrategy = {
+ // Return true if the table is currently fetching data
+ isTableLoading: async (context) => {
+ const spinner = context.page.locator('.loading-spinner');
+ return await spinner.isVisible();
+ },
+ // Return true if a specific row is busy (e.g. saving)
+ isRowLoading: async (row) => {
+ return await row.locator('.row-spinner').isVisible();
+ }
+};
+```
diff --git a/docs/guide/table-anatomy.md b/docs/guide/table-anatomy.md
new file mode 100644
index 00000000..183ec1f6
--- /dev/null
+++ b/docs/guide/table-anatomy.md
@@ -0,0 +1,50 @@
+---
+aside: false
+pageClass: table-anatomy-doc
+---
+
+
+
+# Table Anatomy
+
+Smart Table starts with one Playwright locator: the table root. Selectors in the config are resolved from that root, then cells are resolved from each matched row.
+
+This page focuses on the core selector model.
+
+
+
+## What To Notice
+
+- `headerSelector` is scoped to the table root and defines the column map.
+- `rowSelector` is also scoped to the table root and defines searchable records.
+- `cellSelector` is scoped to each row, not the table root.
+
+Once those pieces are mapped, test code can stay focused on intent:
+
+```typescript
+const row = table.getRow({ Name: 'Airi Satou' });
+await expect(row.getCell('Office')).toHaveText('Tokyo');
+```
+
+For messy headers, see [Header Mapping](/guide/header-mapping). For paginated tables, see [Pagination](/guide/pagination).
+
+## Why Column Order Must Not Matter
+
+The selector model above keeps your test code stable across layout changes — but there is a subtler threat: **column reordering**.
+
+Real tables get reshuffled all the time. A product team adds a "Priority" column to the front. A user drags "Status" to the right. An A/B test swaps two columns for half of your traffic. Any of those changes silently breaks locators that refer to columns by their numeric position (`.nth(2)`, `td:eq(3)`, etc.).
+
+Smart Table resolves cells by **header name**, not position. The column map is built lazily on first use from the live header elements resolved by your `headerSelector` (or header strategy), then cached in memory. Subsequent calls return the cached map — it is only rebuilt when `revalidate()` or `remapHeaders()` clears the cache, so the mapping always reflects the layout at the time of the last (re)initialization.
+
+### See it in action
+
+The demo below drives the same five questions against the same table data. Click **Shuffle columns** and watch what happens to the two approaches:
+
+- **Brittle (fixed indices):** the locator code is frozen — column numbers baked in at write time. After a shuffle it reads from whichever cell happens to land in that slot.
+- **Smart Table:** resolves column positions at runtime from the header row. The answer is always correct, regardless of column order.
+
+
+
+The underlying employee data never changes — only column positions do. Yet one approach returns wrong answers the moment the table is reshuffled, while the other stays correct every time.
+
+This is the core guarantee that `headerSelector` + `cellSelector` together provide: your test assertions are tied to **meaning** (the column name), not to **position** (the nth cell in a row).
diff --git a/docs/guide/why.md b/docs/guide/why.md
new file mode 100644
index 00000000..05dfb24e
--- /dev/null
+++ b/docs/guide/why.md
@@ -0,0 +1,36 @@
+# Why Smart Table?
+
+Playwright is excellent at finding elements. But tables introduce a specific class of problems that plain locators handle poorly.
+
+## The problem with raw selectors
+
+Table cells have no stable identity. A cell is just "the 4th `
` in this `
`." That index is meaningless to a human reading the test, and it breaks silently the moment a column is added, removed, or reordered.
+
+```typescript
+// What does td:nth-child(4) mean? No one knows without opening the app.
+const cell = row.locator('td:nth-child(4)');
+```
+
+Smart Table solves this by reading the header row once and building a name→index map. Every subsequent operation uses column names, not positions.
+
+```typescript
+// Immediately obvious. Works regardless of column order.
+const cell = row.getCell('Office');
+```
+
+## What it handles
+
+- **Paginated tables** — `findRow` and `findRows` page through automatically. You set `maxPages`; the library handles clicking Next, detecting the last page, and stopping.
+- **Virtualized tables** — viewport strategies keep column resolution correct when columns unmount off-screen.
+- **Div-based grids** — AG Grid, MUI DataGrid, and any other `div`-based table work with a few selector overrides.
+- **Editable tables** — `smartFill` and fill strategies handle input cells, dropdowns, and custom editors.
+
+## When you don't need it
+
+If your table is a simple static HTML `
` with a handful of visible rows and no pagination, plain Playwright locators may be enough. Smart Table adds value when tables are paginated, sorted, virtualized, or when tests need to stay stable as the schema changes.
+
+## Next steps
+
+- [Getting Started](/guide/getting-started) — install and write your first test.
+- [Configuration](/guide/configuration) — adapt to a custom DOM structure.
+- [Examples](/examples/) — pick a complete working example.
diff --git a/docs/index.md b/docs/index.md
index 68aea08a..3076077e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,3 +3,27 @@ layout: page
---
+
+## Why not just use Playwright selectors?
+
+Raw selectors break when columns reorder, pages paginate, or the DOM structure changes.
+
+**Without Smart Table — fragile:**
+```typescript
+// Breaks if a column is added before "Office"
+const office = row.locator('td:nth-child(4)');
+```
+
+**With Smart Table — stable:**
+```typescript
+// Works regardless of column order
+await expect(row.getCell('Office')).toHaveText('Tokyo');
+```
+
+| | Raw Playwright | Smart Table |
+|---|---|---|
+| Column reorder | ❌ breaks `nth-child` | ✅ looks up by name |
+| Paginated search | ❌ manual loop | ✅ built-in `findRow` |
+| Multi-column filter | ❌ chained locators | ✅ `{ Name: 'X', Status: 'Active' }` |
+| Typo in column name | ❌ silent wrong element | ✅ throws with suggestions |
+| Works with div grids | ❌ need custom selectors | ✅ configurable selectors |
diff --git a/docs/lab/index.md b/docs/lab/index.md
new file mode 100644
index 00000000..ef6dc520
--- /dev/null
+++ b/docs/lab/index.md
@@ -0,0 +1,101 @@
+---
+aside: false
+pageClass: table-anatomy-doc
+---
+
+# Lab (draft visuals)
+
+**Local preview:** run `npm run docs:dev`, then open **Lab** in the top nav or go to [`/lab/`](/lab/) (with the site base, e.g. `http://localhost:5173/playwright-smart-table/lab/`).
+**Published site:** draft pages under `docs/lab` are **not** shipped; `npm run docs:build` (used in CI) excludes them. To build the full site including Lab, use `npm run docs:build:all`.
+
+Interactive rough drafts for docs and UX. Polished teaching pages live under [How It Works](/guide/table-anatomy).
+
+## Pagination sandbox
+
+Switch between pagination types. Toggle selectors on/off — disabled buttons go dark in the mock UI and drop out of the generated config. The library page counter updates as you navigate.
+
+
+
+## findRow trace (step-through)
+
+
+
+## forEach trace (animated)
+
+
+
+> Want to see how parallel and synchronized modes change this pattern? See [Concurrency modes](#concurrency-modes) below.
+
+## Live query builder
+
+Edit the `getRow` call directly — add and remove key/value pairs, watch the table respond in real time. Misspell a column name to see the guided error with fuzzy suggestions, right inline.
+
+
+
+## The column shuffle test
+
+Pick a question. Shuffle the columns. Brittle index-based code reads from the wrong cell — Smart Table stays tied to header names regardless of column order.
+
+
+
+## Init → getRow (debugger-style)
+
+
+
+## findRow + pagination + checkbox (debugger-style)
+
+
+
+## Concurrency modes
+
+Run the same 3-page `forEach` in each concurrency mode and watch how the row callbacks are scheduled. The elapsed time badge shows the relative cost of each approach. Use the speed picker to see the difference clearly or simulate realistic timing.
+
+
+
+## Inline components
+
+### MethodBadge
+
+Inline async/sync pill for use next to method names in API docs.
+
+`getRow()` returns the first matching row from the current page without paginating.
+
+`findRow()` paginates until a match is found or `maxPages` is exhausted.
+
+`findRows()` collects all matching rows across pages.
+
+`init()` resolves headers and populates the column map.
+
+### ConfigSwatch
+
+Collapsible config block for embedding ready-to-use configs inline in example pages.
+
+
+
+
+
+
+
+## Existing interactives
+
+- [Table Anatomy](/guide/table-anatomy) — selector scoping.
+- [Header Mapping](/guide/header-mapping) — `__col_*` + `headerTransformer`.
+- [Pagination Strategies](/guide/pagination) — pagination shapes.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..ec6906c7
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,473 @@
+
+# Troubleshooting & Debugging
+
+Common issues, solutions, and debugging techniques for Playwright Smart Table.
+
+
+Column Not Found Errors
+
+### Problem
+
+```
+Error: Column "Email" not found
+Available columns: Name, Position, Office, Age, Start date, Salary
+```
+
+### Solutions
+
+**1. Check column name spelling and case**
+
+Column names are case-sensitive:
+
+```typescript
+// ❌ Wrong
+row.getCell('email');
+
+// ✅ Correct
+row.getCell('Email');
+```
+
+**2. Use headerTransformer to normalize**
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ headerTransformer: async ({ text }) => {
+ return text.toLowerCase().trim();
+ }
+});
+
+// Now you can use lowercase
+row.getCell('email'); // Works!
+```
+
+**3. Check actual headers**
+
+```typescript
+const headers = await table.getHeaders();
+console.log('Available columns:', headers);
+```
+
+Also check for extra text like sort icons or counts — normalize with `headerTransformer`:
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ headerTransformer: ({ text }) => text.replace(/Sort$/, '').trim()
+});
+```
+
+
+
+
+Table Not Initialized
+
+### Problem
+
+```
+Error: Table not initialized. Call init() first.
+```
+
+### Solutions
+
+**1. Call init() before synchronous methods**
+
+```typescript
+const table = useTable(page.locator('#table'));
+
+// ❌ Wrong - getRowByIndex is sync
+const row = table.getRowByIndex(0);
+
+// ✅ Correct
+await table.init();
+const row = table.getRowByIndex(0);
+```
+
+**2. Use async methods (auto-initialize)**
+
+```typescript
+// These methods auto-initialize
+const row = await table.findRow({ Name: 'John' }); // ✅
+const rows = await table.findRows({}); // ✅
+```
+
+**Check initialization state:**
+
+```typescript
+if (table.isInitialized()) {
+ const headers = await table.getHeaders();
+ console.log('Mapped Columns:', headers);
+}
+```
+
+
+
+
+Pagination Not Working
+
+### Problem
+
+`findRows()` only returns results from the first page.
+
+### Solutions
+
+**1. Configure pagination strategy**
+
+```typescript
+import { Strategies } from '@rickcedwhat/playwright-smart-table';
+
+const table = useTable(page.locator('#table'), {
+ strategies: {
+ pagination: Strategies.Pagination.click({ next: '.next-button' })
+ },
+ maxPages: 10
+});
+```
+
+**2. Check pagination selector**
+
+```typescript
+// Verify the selector works
+await page.locator('.next-button').click(); // Should navigate
+```
+
+**3. Set maxPages**
+
+```typescript
+const rows = await table.findRows(
+ { Office: 'Tokyo' },
+ { maxPages: 10 } // Default is 1; raise it to scan more pages
+);
+```
+
+
+
+
+Flaky Tests
+
+### Problem
+
+Tests pass sometimes but fail randomly.
+
+### Solutions
+
+**1. Enable debug mode**
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ debug: {
+ slow: 500,
+ logLevel: 'verbose'
+ }
+});
+```
+
+**2. Wait for table to load**
+
+```typescript
+await page.waitForSelector('#table tbody tr');
+await table.init();
+```
+
+**3. Use proper waits**
+
+```typescript
+// ❌ Avoid
+await page.waitForTimeout(1000);
+
+// ✅ Better
+await expect(row.getCell('Status')).toHaveText('Active');
+```
+
+**4. Handle stale element references**
+
+If the table re-renders (e.g., React/Vue update) and locators go stale, the library handles this automatically in most cases. If you see "Element is not attached" errors, try:
+
+```typescript
+await table.revalidate();
+```
+
+
+
+
+Duplicate Column Names
+
+### Problem
+
+```
+Error: Duplicate column names found after transformation: ["Name", "Name"]
+```
+
+### Solutions
+
+**1. Use headerTransformer to make unique**
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ headerTransformer: async ({ text, index }) => {
+ // Add index to duplicates
+ const normalized = text.trim();
+ return `${normalized}_${index}`;
+ }
+});
+```
+
+**2. Use column index**
+
+```typescript
+// Access by index instead
+const cell = row.locator('td').nth(2);
+```
+
+
+
+
+Cells Not Found in Row
+
+### Problem
+
+`getCell()` returns wrong cell or throws error.
+
+### Solutions
+
+**1. Check cell selector**
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ cellSelector: 'td' // Default
+});
+
+// For custom tables
+const table = useTable(page.locator('#table'), {
+ cellSelector: '.table-cell'
+});
+```
+
+**2. Verify selectors against your DOM**
+
+If `getHeaders()` returns an empty list, the table root or selectors are wrong. Start with the smallest config that matches your DOM:
+
+```typescript
+const table = useTable(page.locator('.grid-root'), {
+ headerSelector: '[role="columnheader"]',
+ rowSelector: '[role="row"]',
+ cellSelector: '[role="gridcell"]'
+});
+```
+
+**3. Use custom resolution strategy**
+
+```typescript
+strategies: {
+ getCellLocator: ({ row, columnName, columnIndex }) => {
+ return row.locator(`[data-column="${columnName}"]`);
+ }
+}
+```
+
+
+
+
+Performance Issues
+
+### Problem
+
+Tests are slow when iterating through large tables.
+
+### Solutions
+
+**1. Use built-in iteration methods**
+
+```typescript
+// ❌ Slow: Looping manually
+for (let i = 0; i < 10; i++) {
+ await table.findRows({});
+}
+
+// ✅ Fast: Built-in iteration
+await table.forEach(async ({ row }) => {
+ // Automation handles pagination seamlessly
+});
+```
+
+**2. Limit pages**
+
+```typescript
+const rows = await table.findRows(
+ { Department: 'Engineering' },
+ { maxPages: 5 } // Don't search entire table
+);
+```
+
+**3. Use filtering in DOM, not in code**
+
+```typescript
+// ❌ Slow - gets all rows then filters in code
+const allRows = await table.findRows({});
+const filtered = allRows.filter(/* ... */);
+
+// ✅ Fast - filters in DOM
+const filtered = await table.findRows({ Office: 'Tokyo' });
+```
+
+**4. Use Locator assertions over text extraction**
+
+```typescript
+// ❌ Slow: Extracts text (round trip to browser)
+const text = await row.getCell('Status').innerText();
+expect(text).toBe('Active');
+
+// ✅ Fast: Playwright assertion (runs in browser context)
+await expect(row.getCell('Status')).toHaveText('Active');
+```
+
+**5. Avoid `findRow` for simple on-screen checks**
+
+```typescript
+// Slower: async search path
+await table.findRow({ ID: '123' });
+
+// Faster: checks the current page immediately
+table.getRow({ ID: '123' });
+```
+
+
+
+
+TypeScript Type Errors
+
+### Problem
+
+TypeScript doesn't recognize column names.
+
+### Solutions
+
+**1. Define table type**
+
+```typescript
+type Employee = {
+ Name: string;
+ Email: string;
+ Office: string;
+};
+
+const table = useTable(page.locator('#table'));
+
+// Now TypeScript knows the columns
+const row = await table.findRow({
+ Name: 'John', // ✅ Autocomplete works
+ InvalidColumn: 'x' // ❌ TypeScript error
+});
+```
+
+**2. Use Record for dynamic columns**
+
+```typescript
+const table = useTable>(page.locator('#table'));
+```
+
+
+
+
+Smart Errors Not Showing
+
+### Problem
+
+Not getting helpful error messages with column suggestions or pagination diagnostics.
+
+### Solutions
+
+**Enable verbose logging**
+
+Smart errors (column not found, pagination failures) are emitted automatically. For maximum diagnostic output, enable verbose logging:
+
+```typescript
+const table = useTable(page.locator('#table'), {
+ debug: {
+ logLevel: 'verbose'
+ }
+});
+```
+
+Smart errors are automatic for:
+- `getCell()` — Column not found (with nearest-match suggestion)
+- `findRow()` — No matching rows found
+- `init()` — Empty or duplicate column names detected
+- Pagination — Lists available primitives when navigation fails
+
+
+
+
+Responsive / Mobile Tables
+
+### Problem
+
+Table transforms into a list or card view on mobile screens (responsive design).
+
+### Solution
+
+The library works on the *visible* DOM. If the table structure changes significantly (e.g., `
` becomes `
` cards), you may need conditional logic.
+
+```typescript
+if (isMobile) {
+ // Mobile strategy (cards)
+ const cards = page.locator('.card');
+ // ... customized logic for cards
+} else {
+ // Desktop strategy (table)
+ const table = useTable(page.locator('#table'));
+ await table.findRow({ ... });
+}
+```
+
+> [!NOTE]
+> `SmartRow` locators are resilient to minor layout shifts, but fundamental structure changes require different selectors.
+
+
+
+
+Enabling Debug Logging
+
+The easiest way to see what the library is doing is to enable logging in the table config.
+
+```typescript
+const table = useTable(loc, {
+ debug: {
+ logLevel: 'verbose', // 'none' | 'error' | 'info' | 'verbose'
+ slow: 500 // formatting delay in ms
+ }
+});
+```
+
+**Output:**
+```
+🔍 [SmartTable] Finding row with filters: { Name: 'John' }
+ℹ️ [SmartTable] Scanned 10 rows on page 1
+🔍 [SmartTable] Checking row 1: Name="Alice" (Mismatch)
+🔍 [SmartTable] Checking row 2: Name="John" (Match!)
+```
+
+See [`debug`](/api/table-config#debug) in the API reference for the exact config shape.
+
+
+
+
+Visual Debugging
+
+Since `SmartRow` returns standard Playwright Locators, you can use Playwright's built-in visual tools:
+
+```typescript
+// Highlight the specific cell
+await row.getCell('Status').highlight();
+
+// Pause execution to inspect DOM
+await page.pause();
+```
+
+
+
+---
+
+## Need More Help?
+
+- Check [Examples](/examples/) for working code
+- Review [API Reference](/api/) for method details
+- Open an issue on [GitHub](https://github.com/rickcedwhat/playwright-smart-table/issues)