@rickcedwhat/playwright-smart-table 5.4.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -964
- package/dist/examples/glide-strategies/columns.d.ts +13 -0
- package/dist/examples/glide-strategies/columns.js +43 -0
- package/dist/examples/glide-strategies/headers.d.ts +9 -0
- package/dist/examples/glide-strategies/headers.js +68 -0
- package/dist/src/filterEngine.d.ts +11 -0
- package/dist/src/filterEngine.js +39 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +18 -0
- package/dist/src/plugins.d.ts +32 -0
- package/dist/src/plugins.js +13 -0
- package/dist/src/smartRow.d.ts +7 -0
- package/dist/src/smartRow.js +160 -0
- package/dist/src/strategies/columns.d.ts +18 -0
- package/dist/src/strategies/columns.js +21 -0
- package/dist/src/strategies/dedupe.d.ts +9 -0
- package/dist/src/strategies/dedupe.js +27 -0
- package/dist/src/strategies/fill.d.ts +7 -0
- package/dist/src/strategies/fill.js +88 -0
- package/dist/src/strategies/glide.d.ts +29 -0
- package/dist/src/strategies/glide.js +98 -0
- package/dist/src/strategies/headers.d.ts +13 -0
- package/dist/src/strategies/headers.js +30 -0
- package/dist/src/strategies/index.d.ts +54 -0
- package/dist/src/strategies/index.js +43 -0
- package/dist/src/strategies/loading.d.ts +48 -0
- package/dist/src/strategies/loading.js +82 -0
- package/dist/src/strategies/pagination.d.ts +33 -0
- package/dist/src/strategies/pagination.js +79 -0
- package/dist/src/strategies/rdg.d.ts +25 -0
- package/dist/src/strategies/rdg.js +100 -0
- package/dist/src/strategies/resolution.d.ts +22 -0
- package/dist/src/strategies/resolution.js +30 -0
- package/dist/src/strategies/sorting.d.ts +12 -0
- package/dist/src/strategies/sorting.js +68 -0
- package/dist/src/strategies/stabilization.d.ts +29 -0
- package/dist/src/strategies/stabilization.js +91 -0
- package/dist/src/strategies/validation.d.ts +22 -0
- package/dist/src/strategies/validation.js +54 -0
- package/dist/src/strategies/virtualizedPagination.d.ts +32 -0
- package/dist/src/strategies/virtualizedPagination.js +80 -0
- package/dist/src/typeContext.d.ts +6 -0
- package/dist/src/typeContext.js +465 -0
- package/dist/src/types.d.ts +458 -0
- package/dist/src/types.js +2 -0
- package/dist/src/useTable.d.ts +44 -0
- package/dist/src/useTable.js +642 -0
- package/dist/src/utils/debugUtils.d.ts +17 -0
- package/dist/src/utils/debugUtils.js +62 -0
- package/dist/src/utils/smartRowArray.d.ts +14 -0
- package/dist/src/utils/smartRowArray.js +22 -0
- package/dist/src/utils/stringUtils.d.ts +22 -0
- package/dist/src/utils/stringUtils.js +73 -0
- package/dist/src/utils.d.ts +7 -0
- package/dist/src/utils.js +29 -0
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,1016 +1,130 @@
|
|
|
1
|
-
# Playwright Smart Table
|
|
1
|
+
# Playwright Smart Table
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Production-ready table testing for Playwright with smart column-aware locators.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @rickcedwhat/playwright-smart-table
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
> **Note:** Requires `@playwright/test` as a peer dependency.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 🎯 Getting Started
|
|
16
|
-
|
|
17
|
-
### Step 1: Basic Table Interaction
|
|
18
|
-
|
|
19
|
-
For standard HTML tables (`<table>`, `<tr>`, `<td>`), the library works out of the box with sensible defaults:
|
|
20
|
-
|
|
21
|
-
<!-- embed: quick-start -->
|
|
22
|
-
```typescript
|
|
23
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
24
|
-
const table = await useTable(page.locator('#example'), {
|
|
25
|
-
headerSelector: 'thead th' // Override for this specific site
|
|
26
|
-
}).init();
|
|
27
|
-
|
|
28
|
-
// Find the row with Name="Airi Satou", then get the Position cell
|
|
29
|
-
const row = table.getRow({ Name: 'Airi Satou' });
|
|
30
|
-
|
|
31
|
-
const positionCell = row.getCell('Position');
|
|
32
|
-
await expect(positionCell).toHaveText('Accountant');
|
|
33
|
-
```
|
|
34
|
-
<!-- /embed: quick-start -->
|
|
35
|
-
|
|
36
|
-
**What's happening here?**
|
|
37
|
-
- `useTable()` creates a smart table wrapper around your table locator
|
|
38
|
-
- `getRow()` finds a specific row by column values
|
|
39
|
-
- The returned `SmartRow` knows its column structure, so `.getCell('Position')` works directly
|
|
40
|
-
|
|
41
|
-
### Step 2: Understanding SmartRow
|
|
42
|
-
|
|
43
|
-
The `SmartRow` is the core power of this library. Unlike a standard Playwright `Locator`, it understands your table's column structure.
|
|
44
|
-
|
|
45
|
-
<!-- embed: smart-row -->
|
|
46
|
-
```typescript
|
|
47
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
48
|
-
|
|
49
|
-
// Get SmartRow via getByRow
|
|
50
|
-
const row = table.getRow({ Name: 'Airi Satou' });
|
|
51
|
-
|
|
52
|
-
// Interact with cell using column name (resilient to column reordering)
|
|
53
|
-
const positionCell = row.getCell('Position');
|
|
54
|
-
await positionCell.click();
|
|
55
|
-
|
|
56
|
-
// Extract row data as JSON
|
|
57
|
-
const data = await row.toJSON();
|
|
58
|
-
console.log(data);
|
|
59
|
-
// { Name: "Airi Satou", Position: "Accountant", ... }
|
|
60
|
-
```
|
|
61
|
-
<!-- /embed: smart-row -->
|
|
62
|
-
|
|
63
|
-
**Key Benefits:**
|
|
64
|
-
- ✅ Column names instead of indices (survives column reordering)
|
|
65
|
-
- ✅ Extends Playwright's `Locator` API (all `.click()`, `.isVisible()`, etc. work)
|
|
66
|
-
- ✅ `.toJSON()` for quick data extraction (uses `columnStrategy` to ensure visibility)
|
|
5
|
+
[](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
67
7
|
|
|
68
8
|
---
|
|
69
9
|
|
|
70
|
-
##
|
|
71
|
-
|
|
72
|
-
### Working with Paginated Tables
|
|
73
|
-
|
|
74
|
-
For tables that span multiple pages, configure a pagination strategy:
|
|
75
|
-
|
|
76
|
-
<!-- embed: pagination -->
|
|
77
|
-
```typescript
|
|
78
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
79
|
-
const table = useTable(page.locator('#example'), {
|
|
80
|
-
rowSelector: 'tbody tr',
|
|
81
|
-
headerSelector: 'thead th',
|
|
82
|
-
cellSelector: 'td',
|
|
83
|
-
// Strategy: Tell it how to find the next page
|
|
84
|
-
strategies: {
|
|
85
|
-
pagination: Strategies.Pagination.clickNext(() =>
|
|
86
|
-
page.getByRole('link', { name: 'Next' })
|
|
87
|
-
)
|
|
88
|
-
},
|
|
89
|
-
maxPages: 5 // Allow scanning up to 5 pages
|
|
90
|
-
});
|
|
91
|
-
await table.init();
|
|
92
|
-
|
|
93
|
-
// ✅ Verify Colleen is NOT visible initially
|
|
94
|
-
await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
|
|
95
|
-
|
|
96
|
-
// Use findRow for pagination
|
|
97
|
-
await expect(await table.findRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
98
|
-
// NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
|
|
99
|
-
```
|
|
100
|
-
<!-- /embed: pagination -->
|
|
101
|
-
|
|
102
|
-
### Debug Mode
|
|
103
|
-
|
|
104
|
-
Enable debug logging to see exactly what the library is doing:
|
|
105
|
-
|
|
106
|
-
<!-- embed: advanced-debug -->
|
|
107
|
-
```typescript
|
|
108
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
109
|
-
const table = useTable(page.locator('#example'), {
|
|
110
|
-
headerSelector: 'thead th',
|
|
111
|
-
debug: {
|
|
112
|
-
logLevel: 'verbose' // Enables verbose logging of internal operations
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
await table.init();
|
|
116
|
-
|
|
117
|
-
const row = table.getRow({ Name: 'Airi Satou' });
|
|
118
|
-
await expect(row).toBeVisible();
|
|
119
|
-
```
|
|
120
|
-
<!-- /embed: advanced-debug -->
|
|
121
|
-
|
|
122
|
-
This will log header mappings, row scans, and pagination triggers to the console, and slow down operations to help you see what's happening.
|
|
123
|
-
|
|
124
|
-
### Resetting Table State
|
|
125
|
-
|
|
126
|
-
If your tests navigate deep into a paginated table, use `.reset()` to return to the first page. You can also configure an `onReset` hook to define custom reset behavior (e.g., clicking a "First Page" button):
|
|
127
|
-
|
|
128
|
-
<!-- embed: advanced-reset -->
|
|
129
|
-
```typescript
|
|
130
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
131
|
-
// Navigate deep into the table by searching for a row on a later page
|
|
132
|
-
try {
|
|
133
|
-
await table.findRow({ Name: 'Angelica Ramos' });
|
|
134
|
-
} catch (e) { }
|
|
135
|
-
|
|
136
|
-
// Reset internal state (and potentially UI) to initial page
|
|
137
|
-
await table.reset();
|
|
138
|
-
await table.init(); // Re-init after reset
|
|
139
|
-
|
|
140
|
-
// Now subsequent searches start from the beginning
|
|
141
|
-
const currentPageRow = table.getRow({ Name: 'Airi Satou' });
|
|
142
|
-
await expect(currentPageRow).toBeVisible();
|
|
143
|
-
```
|
|
144
|
-
<!-- /embed: advanced-reset -->
|
|
145
|
-
|
|
146
|
-
**Custom Reset Behavior:**
|
|
147
|
-
|
|
148
|
-
Use the `onReset` configuration option to define what happens when `table.reset()` is called:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
const table = useTable(page.locator('#example'), {
|
|
152
|
-
strategies: {
|
|
153
|
-
pagination: Strategies.Pagination.clickNext(() => page.getByRole('link', { name: 'Next' }))
|
|
154
|
-
},
|
|
155
|
-
// Define custom reset logic
|
|
156
|
-
onReset: async ({ page }) => {
|
|
157
|
-
// Click "First" button to return to page 1
|
|
158
|
-
await page.getByRole('link', { name: 'First' }).click();
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Column Scanning
|
|
164
|
-
|
|
165
|
-
Efficiently extract all values from a specific column:
|
|
166
|
-
|
|
167
|
-
<!-- embed: advanced-column-scan -->
|
|
168
|
-
```typescript
|
|
169
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
170
|
-
// Quickly grab all text values from the "Office" column
|
|
171
|
-
const offices = await table.getColumnValues('Office');
|
|
172
|
-
expect(offices).toContain('Tokyo');
|
|
173
|
-
expect(offices.length).toBeGreaterThan(0);
|
|
174
|
-
```
|
|
175
|
-
<!-- /embed: advanced-column-scan -->
|
|
176
|
-
|
|
177
|
-
### Filling Row Data
|
|
178
|
-
|
|
179
|
-
Use `smartFill()` to intelligently populate form fields in a table row. The method automatically detects input types (text inputs, selects, checkboxes, contenteditable divs) and fills them appropriately. You can still use Locator's standard `fill()` method for single-input scenarios.
|
|
180
|
-
|
|
181
|
-
<!-- embed: fill-basic -->
|
|
182
|
-
```typescript
|
|
183
|
-
// Find a row and fill it with new data
|
|
184
|
-
const row = table.getRow({ ID: '1' });
|
|
185
|
-
|
|
186
|
-
await row.smartFill({
|
|
187
|
-
Name: 'John Updated',
|
|
188
|
-
Status: 'Inactive',
|
|
189
|
-
Active: false,
|
|
190
|
-
Notes: 'Updated notes here'
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Verify the values were filled correctly
|
|
194
|
-
const nameCell = row.getCell('Name');
|
|
195
|
-
const statusCell = row.getCell('Status');
|
|
196
|
-
const activeCell = row.getCell('Active');
|
|
197
|
-
const notesCell = row.getCell('Notes');
|
|
198
|
-
await expect(nameCell.locator('input')).toHaveValue('John Updated');
|
|
199
|
-
await expect(statusCell.locator('select')).toHaveValue('Inactive');
|
|
200
|
-
await expect(activeCell.locator('input[type="checkbox"]')).not.toBeChecked();
|
|
201
|
-
await expect(notesCell.locator('textarea')).toHaveValue('Updated notes here');
|
|
202
|
-
```
|
|
203
|
-
<!-- /embed: fill-basic -->
|
|
204
|
-
|
|
205
|
-
**Auto-detection supports:**
|
|
206
|
-
- Text inputs (`input[type="text"]`, `textarea`)
|
|
207
|
-
- Select dropdowns (`select`)
|
|
208
|
-
- Checkboxes/radios (`input[type="checkbox"]`, `input[type="radio"]`, `[role="checkbox"]`)
|
|
209
|
-
- Contenteditable divs (`[contenteditable="true"]`)
|
|
210
|
-
|
|
211
|
-
**Custom input mappers:**
|
|
212
|
-
|
|
213
|
-
For edge cases where auto-detection doesn't work (e.g., custom components, multiple inputs in a cell), use per-column mappers:
|
|
214
|
-
|
|
215
|
-
<!-- embed: fill-custom-mappers -->
|
|
216
|
-
```typescript
|
|
217
|
-
// Use custom input mappers for specific columns
|
|
218
|
-
await row.smartFill({
|
|
219
|
-
Name: 'John Updated',
|
|
220
|
-
Status: 'Inactive'
|
|
221
|
-
}, {
|
|
222
|
-
inputMappers: {
|
|
223
|
-
// Name column has multiple inputs - target the primary one
|
|
224
|
-
Name: (cell) => cell.locator('.primary-input'),
|
|
225
|
-
// Status uses standard select, but we could customize if needed
|
|
226
|
-
Status: (cell) => cell.locator('select')
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Verify the values
|
|
231
|
-
const nameCell = row.getCell('Name');
|
|
232
|
-
const statusCell = row.getCell('Status');
|
|
233
|
-
await expect(nameCell.locator('.primary-input')).toHaveValue('John Updated');
|
|
234
|
-
await expect(statusCell.locator('select')).toHaveValue('Inactive');
|
|
235
|
-
```
|
|
236
|
-
<!-- /embed: fill-custom-mappers -->
|
|
237
|
-
|
|
238
|
-
### Transforming Column Headers
|
|
239
|
-
|
|
240
|
-
Use `headerTransformer` to normalize or rename column headers. This is especially useful for tables with empty headers, inconsistent formatting, or when you want to use cleaner names in your tests.
|
|
241
|
-
|
|
242
|
-
**Example 1: Renaming Empty Columns**
|
|
243
|
-
|
|
244
|
-
Tables with empty header cells (like Material UI DataGrids) get auto-assigned names like `__col_0`, `__col_1`. Transform them to meaningful names:
|
|
245
|
-
|
|
246
|
-
<!-- embed: header-transformer -->
|
|
247
|
-
```typescript
|
|
248
|
-
// Example from: https://mui.com/material-ui/react-table/
|
|
249
|
-
const table = useTable(page.locator('.MuiDataGrid-root').first(), {
|
|
250
|
-
rowSelector: '.MuiDataGrid-row',
|
|
251
|
-
headerSelector: '.MuiDataGrid-columnHeader',
|
|
252
|
-
cellSelector: '.MuiDataGrid-cell',
|
|
253
|
-
strategies: {
|
|
254
|
-
pagination: Strategies.Pagination.clickNext(
|
|
255
|
-
(root) => root.getByRole("button", { name: "Go to next page" })
|
|
256
|
-
)
|
|
257
|
-
},
|
|
258
|
-
maxPages: 5,
|
|
259
|
-
// Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
|
|
260
|
-
headerTransformer: ({ text }) => {
|
|
261
|
-
// We know there is only one empty column which we will rename to "Actions" for easier reference
|
|
262
|
-
if (text.includes('__col_') || text.trim() === '') {
|
|
263
|
-
return 'Actions';
|
|
264
|
-
}
|
|
265
|
-
return text;
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
await table.init();
|
|
269
|
-
|
|
270
|
-
const headers = await table.getHeaders();
|
|
271
|
-
// Now we can reference the "Actions" column even if it has no header text
|
|
272
|
-
expect(headers).toContain('Actions');
|
|
273
|
-
|
|
274
|
-
// Use the renamed column
|
|
275
|
-
// First check it's not on the current page
|
|
276
|
-
const currentPageRow = table.getRow({ "Last name": "Melisandre" });
|
|
277
|
-
await expect(currentPageRow).not.toBeVisible();
|
|
278
|
-
|
|
279
|
-
// Then find it across pages
|
|
280
|
-
const row = await table.findRow({ "Last name": "Melisandre" });
|
|
281
|
-
const actionsCell = row.getCell('Actions');
|
|
282
|
-
await actionsCell.getByLabel("Select row").click();
|
|
283
|
-
```
|
|
284
|
-
<!-- /embed: header-transformer -->
|
|
285
|
-
|
|
286
|
-
**Example 2: Normalizing Column Names**
|
|
287
|
-
|
|
288
|
-
Clean up inconsistent column names (extra spaces, inconsistent casing):
|
|
289
|
-
|
|
290
|
-
<!-- embed: header-transformer-normalize -->
|
|
291
|
-
```typescript
|
|
292
|
-
// Example from: https://the-internet.herokuapp.com/tables
|
|
293
|
-
const table = useTable(page.locator('#table1'), {
|
|
294
|
-
// Normalize column names: remove extra spaces, handle inconsistent casing
|
|
295
|
-
headerTransformer: ({ text }) => {
|
|
296
|
-
return text.trim()
|
|
297
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
298
|
-
.replace(/^\s*|\s*$/g, ''); // Remove leading/trailing spaces
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
await table.init();
|
|
302
|
-
|
|
303
|
-
// Now column names are consistent
|
|
304
|
-
const row = table.getRow({ "Last Name": "Doe" });
|
|
305
|
-
const emailCell = row.getCell("Email");
|
|
306
|
-
await expect(emailCell).toHaveText("jdoe@hotmail.com");
|
|
307
|
-
```
|
|
308
|
-
<!-- /embed: header-transformer-normalize -->
|
|
309
|
-
|
|
310
|
-
### Iterating Through Paginated Tables
|
|
311
|
-
|
|
312
|
-
Use `iterateThroughTable()` to process all rows across multiple pages. This is perfect for data scraping, validation, or bulk operations.
|
|
313
|
-
|
|
314
|
-
<!-- embed: iterate-through-table -->
|
|
315
|
-
```typescript
|
|
316
|
-
// Iterate through all pages and collect data
|
|
317
|
-
const allNames = await table.iterateThroughTable(async ({ rows, index }) => {
|
|
318
|
-
// Return names from this iteration - automatically appended to allData
|
|
319
|
-
return await Promise.all(rows.map(r => r.getCell('Name').innerText()));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// allNames contains all names from all iterations
|
|
323
|
-
// Verify sorting across allNames
|
|
324
|
-
expect(allNames.flat().length).toBeGreaterThan(10);
|
|
325
|
-
```
|
|
326
|
-
<!-- /embed: iterate-through-table -->
|
|
327
|
-
|
|
328
|
-
**With Deduplication (for infinite scroll):**
|
|
329
|
-
|
|
330
|
-
<!-- embed: iterate-through-table-dedupe -->
|
|
331
|
-
```typescript
|
|
332
|
-
// Scrape all data with deduplication (useful for infinite scroll)
|
|
333
|
-
const allData = await table.iterateThroughTable(
|
|
334
|
-
async ({ rows }) => {
|
|
335
|
-
// Return row data - automatically appended to allData
|
|
336
|
-
return await Promise.all(rows.map(r => r.toJSON()));
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
dedupeStrategy: (row) => row.getCell(dedupeColumn).innerText(),
|
|
340
|
-
getIsLast: ({ paginationResult }) => !paginationResult
|
|
341
|
-
}
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
// allData contains all row data from all iterations (deduplicated at row level)
|
|
345
|
-
expect(allData.flat().length).toBeGreaterThan(0);
|
|
346
|
-
```
|
|
347
|
-
<!-- /embed: iterate-through-table-dedupe -->
|
|
348
|
-
|
|
349
|
-
**With Hooks:**
|
|
350
|
-
|
|
351
|
-
<!-- embed: iterate-through-table-hooks -->
|
|
352
|
-
```typescript
|
|
353
|
-
const allData = await table.iterateThroughTable(
|
|
354
|
-
async ({ rows, index, isFirst, isLast }) => {
|
|
355
|
-
// Normal logic for each iteration - return value appended to allData
|
|
356
|
-
return await Promise.all(rows.map(r => r.toJSON()));
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
getIsLast: ({ paginationResult }) => !paginationResult,
|
|
360
|
-
beforeFirst: async ({ allData }) => {
|
|
361
|
-
console.log('Starting data collection...');
|
|
362
|
-
// Could perform setup actions
|
|
363
|
-
},
|
|
364
|
-
afterLast: async ({ allData }) => {
|
|
365
|
-
console.log(`Collected ${allData.length} total items`);
|
|
366
|
-
// Could perform cleanup or final actions
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
);
|
|
370
|
-
```
|
|
371
|
-
<!-- /embed: iterate-through-table-hooks -->
|
|
372
|
-
|
|
373
|
-
**Hook Timing:**
|
|
374
|
-
- `beforeFirst`: Runs **before** your callback processes the first page
|
|
375
|
-
- `afterLast`: Runs **after** your callback processes the last page
|
|
376
|
-
- Both are optional and receive `{ index, rows, allData }`
|
|
377
|
-
|
|
378
|
-
#### Batching (v5.1+)
|
|
379
|
-
|
|
380
|
-
Process multiple pages at once for better performance:
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
const results = await table.iterateThroughTable(
|
|
384
|
-
async ({ rows, batchInfo }) => {
|
|
385
|
-
// rows contains data from multiple pages
|
|
386
|
-
console.log(`Processing pages ${batchInfo.startIndex}-${batchInfo.endIndex}`);
|
|
387
|
-
console.log(`Batch has ${rows.length} total rows from ${batchInfo.size} pages`);
|
|
388
|
-
|
|
389
|
-
// Bulk process (e.g., batch database insert)
|
|
390
|
-
await bulkInsert(rows);
|
|
391
|
-
return rows.length;
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
batchSize: 3 // Process 3 pages at a time
|
|
395
|
-
}
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
// With 6 pages total:
|
|
399
|
-
// - Batch 1: pages 0,1,2 (batchInfo.size = 3)
|
|
400
|
-
// - Batch 2: pages 3,4,5 (batchInfo.size = 3)
|
|
401
|
-
// results.length === 2 (fewer callbacks than pages)
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
**Key Points:**
|
|
405
|
-
- `batchSize` = number of **pages**, not rows
|
|
406
|
-
- `batchInfo` is undefined when not batching (`batchSize` undefined or `1`)
|
|
407
|
-
- Works with deduplication, pagination strategies, and hooks
|
|
408
|
-
- Reduces callback overhead for bulk operations
|
|
409
|
-
- Default: no batching (one callback per page)
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## 📖 API Reference
|
|
414
|
-
|
|
415
|
-
### Method Comparison
|
|
416
|
-
|
|
417
|
-
Quick reference for choosing the right method:
|
|
418
|
-
|
|
419
|
-
| Method | Async/Sync | Paginates? | Returns | Use When |
|
|
420
|
-
|--------|------------|------------|---------|----------|
|
|
421
|
-
| `getRow()` | **Sync** | ❌ No | Single `SmartRow` | Finding row on current page only |
|
|
422
|
-
| `findRow()` | **Async** | ✅ Yes | Single `SmartRow` | Searching across pages |
|
|
423
|
-
| `getRows()` | **Async** | ❌ No | `SmartRow[]` | Getting all rows on current page |
|
|
424
|
-
| `findRows()` | **Async** | ✅ Yes | `SmartRow[]` | Getting all matching rows across pages |
|
|
425
|
-
| `iterateThroughTable()` | **Async** | ✅ Yes | `T[]` | Processing/scraping all pages with custom logic |
|
|
426
|
-
|
|
427
|
-
**Naming Pattern:**
|
|
428
|
-
- `get*` = Current page only (fast, no pagination)
|
|
429
|
-
- `find*` = Search across pages (slower, uses pagination)
|
|
430
|
-
|
|
431
|
-
### Table Methods
|
|
432
|
-
|
|
433
|
-
#### <a name="getrow"></a>`getRow(filters, options?)`
|
|
434
|
-
|
|
435
|
-
**Purpose:** Strict retrieval - finds exactly one row matching the filters on the **current page**.
|
|
436
|
-
|
|
437
|
-
**Behavior:**
|
|
438
|
-
- ✅ Returns `SmartRow` if exactly one match
|
|
439
|
-
- ❌ Throws error if multiple matches (ambiguous query)
|
|
440
|
-
- 👻 Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
441
|
-
- ℹ️ **Sync method**: Returns immediate locator result.
|
|
442
|
-
- 🔍 **Filtering**: Uses "contains" matching by default (e.g., "Tokyo" matches "Tokyo Office"). Set `exact: true` for strict equality.
|
|
443
|
-
|
|
444
|
-
**Type Signature:**
|
|
445
|
-
```typescript
|
|
446
|
-
getRow: <T extends { asJSON?: boolean }>(
|
|
447
|
-
filters: Record<string, string | RegExp | number>,
|
|
448
|
-
options?: { exact?: boolean } & T
|
|
449
|
-
) => SmartRow;
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
<!-- embed: get-by-row -->
|
|
453
|
-
```typescript
|
|
454
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
455
|
-
const table = useTable(page.locator('#example'), { headerSelector: 'thead th' });
|
|
456
|
-
await table.init();
|
|
457
|
-
|
|
458
|
-
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
459
|
-
const row = table.getRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
460
|
-
await expect(row).toBeVisible();
|
|
10
|
+
## 📚 [Full Documentation →](https://rickcedwhat.github.io/playwright-smart-table/)
|
|
461
11
|
|
|
462
|
-
|
|
463
|
-
await expect(table.getRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
464
|
-
```
|
|
465
|
-
<!-- /embed: get-by-row -->
|
|
466
|
-
|
|
467
|
-
Get row data as JSON:
|
|
468
|
-
<!-- embed: get-by-row-json -->
|
|
469
|
-
```typescript
|
|
470
|
-
// Get row data as JSON object
|
|
471
|
-
const row = table.getRow({ Name: 'Airi Satou' });
|
|
472
|
-
const data = await row.toJSON();
|
|
473
|
-
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
474
|
-
|
|
475
|
-
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
476
|
-
expect(data).toHaveProperty('Position');
|
|
477
|
-
|
|
478
|
-
// Get specific columns only (faster for large tables)
|
|
479
|
-
const partial = await row.toJSON({ columns: ['Name'] });
|
|
480
|
-
expect(partial).toEqual({ Name: 'Airi Satou' });
|
|
481
|
-
```
|
|
482
|
-
<!-- /embed: get-by-row-json -->
|
|
483
|
-
|
|
484
|
-
#### <a name="findrow"></a>`findRow(filters, options?)`
|
|
485
|
-
|
|
486
|
-
**Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
|
|
487
|
-
|
|
488
|
-
**Behavior:**
|
|
489
|
-
- 🔄 Auto-initializes table if needed
|
|
490
|
-
- 🔎 Paginates through data until match is found or `maxPages` reached
|
|
491
|
-
- ✅ Returns `SmartRow` if found
|
|
492
|
-
- 👻 Returns sentinel locator if not found
|
|
493
|
-
|
|
494
|
-
**Type Signature:**
|
|
495
|
-
```typescript
|
|
496
|
-
findRow: (
|
|
497
|
-
filters: Record<string, string | RegExp | number>,
|
|
498
|
-
options?: { exact?: boolean, maxPages?: number }
|
|
499
|
-
) => Promise<SmartRow>;
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
#### <a name="getrows"></a>`getRows(options?)`
|
|
503
|
-
|
|
504
|
-
**Purpose:** Inclusive retrieval - gets all rows on the **current page** matching optional filters.
|
|
505
|
-
|
|
506
|
-
**Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
|
|
507
|
-
|
|
508
|
-
**Type Signature:**
|
|
509
|
-
```typescript
|
|
510
|
-
getRows: <T extends { asJSON?: boolean }>(
|
|
511
|
-
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
512
|
-
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
<!-- embed: get-all-rows -->
|
|
516
|
-
```typescript
|
|
517
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
518
|
-
// 1. Get ALL rows on the current page
|
|
519
|
-
const allRows = await table.getRows();
|
|
520
|
-
expect(allRows.length).toBeGreaterThan(0);
|
|
521
|
-
|
|
522
|
-
// 2. Get subset of rows (Filtering)
|
|
523
|
-
const tokyoUsers = await table.getRows({
|
|
524
|
-
filter: { Office: 'Tokyo' }
|
|
525
|
-
});
|
|
526
|
-
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
527
|
-
|
|
528
|
-
// 3. Dump data to JSON
|
|
529
|
-
const rows = await table.getRows();
|
|
530
|
-
const data = await rows.toJSON();
|
|
531
|
-
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
532
|
-
expect(data.length).toBeGreaterThan(0);
|
|
533
|
-
expect(data[0]).toHaveProperty('Name');
|
|
534
|
-
```
|
|
535
|
-
<!-- /embed: get-all-rows -->
|
|
536
|
-
|
|
537
|
-
Filter rows with exact match:
|
|
538
|
-
<!-- embed: get-all-rows-exact -->
|
|
539
|
-
```typescript
|
|
540
|
-
// Get rows with exact match (default is fuzzy/contains match)
|
|
541
|
-
const exactMatches = await table.getRows({
|
|
542
|
-
filter: { Office: 'Tokyo' },
|
|
543
|
-
exact: true // Requires exact string match
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
expect(exactMatches.length).toBeGreaterThan(0);
|
|
547
|
-
```
|
|
548
|
-
<!-- /embed: get-all-rows-exact -->
|
|
549
|
-
|
|
550
|
-
#### <a name="findrows"></a>`findRows(filters, options?)`
|
|
551
|
-
|
|
552
|
-
**Purpose:** Async retrieval - finds **all** rows matching filters **across multiple pages**.
|
|
553
|
-
|
|
554
|
-
**Behavior:**
|
|
555
|
-
- 🔄 Paginates and accumulates matches
|
|
556
|
-
- ⚠️ Can be slow on large datasets, use `maxPages` to limit scope
|
|
557
|
-
|
|
558
|
-
**Type Signature:**
|
|
559
|
-
```typescript
|
|
560
|
-
findRows: (
|
|
561
|
-
filters: Record<string, string | RegExp | number>,
|
|
562
|
-
options?: { exact?: boolean, maxPages?: number }
|
|
563
|
-
) => Promise<SmartRow[]>;
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
567
|
-
|
|
568
|
-
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
569
|
-
|
|
570
|
-
**Type Signature:**
|
|
571
|
-
```typescript
|
|
572
|
-
getColumnValues: <V = string>(
|
|
573
|
-
column: string,
|
|
574
|
-
options?: {
|
|
575
|
-
mapper?: (cell: Locator) => Promise<V> | V,
|
|
576
|
-
maxPages?: number
|
|
577
|
-
}
|
|
578
|
-
) => Promise<V[]>;
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
Basic usage:
|
|
582
|
-
<!-- embed: advanced-column-scan -->
|
|
583
|
-
```typescript
|
|
584
|
-
// Example from: https://datatables.net/examples/data_sources/dom
|
|
585
|
-
// Quickly grab all text values from the "Office" column
|
|
586
|
-
const offices = await table.getColumnValues('Office');
|
|
587
|
-
expect(offices).toContain('Tokyo');
|
|
588
|
-
expect(offices.length).toBeGreaterThan(0);
|
|
589
|
-
```
|
|
590
|
-
<!-- /embed: advanced-column-scan -->
|
|
591
|
-
|
|
592
|
-
With custom mapper:
|
|
593
|
-
<!-- embed: advanced-column-scan-mapper -->
|
|
594
|
-
```typescript
|
|
595
|
-
// Extract numeric values from a column
|
|
596
|
-
const ages = await table.getColumnValues('Age', {
|
|
597
|
-
mapper: async (cell) => {
|
|
598
|
-
const text = await cell.innerText();
|
|
599
|
-
return parseInt(text, 10);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// Now ages is an array of numbers
|
|
604
|
-
expect(ages.every(age => typeof age === 'number')).toBe(true);
|
|
605
|
-
expect(ages.length).toBeGreaterThan(0);
|
|
606
|
-
```
|
|
607
|
-
<!-- /embed: advanced-column-scan-mapper -->
|
|
608
|
-
|
|
609
|
-
#### <a name="getheaders"></a>`getHeaders()`
|
|
610
|
-
|
|
611
|
-
Returns an array of all column names in the table.
|
|
612
|
-
|
|
613
|
-
**Type Signature:**
|
|
614
|
-
```typescript
|
|
615
|
-
getHeaders: () => Promise<string[]>;
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
#### <a name="getheadercell"></a>`getHeaderCell(columnName)`
|
|
619
|
-
|
|
620
|
-
Returns a Playwright `Locator` for the specified header cell.
|
|
621
|
-
|
|
622
|
-
**Type Signature:**
|
|
623
|
-
```typescript
|
|
624
|
-
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
#### <a name="reset"></a>`reset()`
|
|
628
|
-
|
|
629
|
-
Resets table state (clears cache, pagination flags) and invokes the `onReset` strategy to return to the first page.
|
|
630
|
-
|
|
631
|
-
**Type Signature:**
|
|
632
|
-
```typescript
|
|
633
|
-
reset: () => Promise<void>;
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
#### <a name="sorting"></a>`sorting.apply(column, direction)` & `sorting.getState(column)`
|
|
637
|
-
|
|
638
|
-
If you've configured a sorting strategy, use these methods to sort columns and check their current sort state.
|
|
639
|
-
|
|
640
|
-
**Apply Sort:**
|
|
641
|
-
```typescript
|
|
642
|
-
// Configure table with sorting strategy
|
|
643
|
-
const table = useTable(page.locator('#sortable-table'), {
|
|
644
|
-
strategies: {
|
|
645
|
-
sorting: Strategies.Sorting.AriaSort()
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
await table.init();
|
|
649
|
-
|
|
650
|
-
// Sort by Name column (ascending)
|
|
651
|
-
await table.sorting.apply('Name', 'asc');
|
|
652
|
-
|
|
653
|
-
// Sort by Age column (descending)
|
|
654
|
-
await table.sorting.apply('Age', 'desc');
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
**Check Sort State:**
|
|
658
|
-
```typescript
|
|
659
|
-
// Get current sort state of a column
|
|
660
|
-
const nameSort = await table.sorting.getState('Name');
|
|
661
|
-
// Returns: 'asc' | 'desc' | 'none'
|
|
662
|
-
|
|
663
|
-
expect(nameSort).toBe('asc');
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
**Type Signatures:**
|
|
667
|
-
```typescript
|
|
668
|
-
sorting: {
|
|
669
|
-
apply: (columnName: string, direction: 'asc' | 'desc') => Promise<void>;
|
|
670
|
-
getState: (columnName: string) => Promise<'asc' | 'desc' | 'none'>;
|
|
671
|
-
}
|
|
672
|
-
```
|
|
12
|
+
**Visit the complete documentation at: https://rickcedwhat.github.io/playwright-smart-table/**
|
|
673
13
|
|
|
674
14
|
---
|
|
675
15
|
|
|
676
|
-
##
|
|
677
|
-
|
|
678
|
-
This library uses the **Strategy Pattern** for pagination. Use built-in strategies or write custom ones.
|
|
16
|
+
## Why Playwright Smart Table?
|
|
679
17
|
|
|
680
|
-
|
|
18
|
+
Testing HTML tables in Playwright is painful. Traditional approaches are fragile and hard to maintain.
|
|
681
19
|
|
|
682
|
-
|
|
20
|
+
### The Problem
|
|
683
21
|
|
|
684
|
-
|
|
22
|
+
**Traditional approach:**
|
|
685
23
|
|
|
686
24
|
```typescript
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
root.page().getByRole('button', { name: 'Next' })
|
|
690
|
-
)
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
#### <a name="tablestrategiesinfinitescroll"></a>`Strategies.Pagination.infiniteScroll()`
|
|
25
|
+
// ❌ Fragile - breaks if columns reorder
|
|
26
|
+
const email = await page.locator('tbody tr').nth(2).locator('td').nth(3).textContent();
|
|
695
27
|
|
|
696
|
-
|
|
28
|
+
// ❌ Brittle XPath
|
|
29
|
+
const row = page.locator('//tr[td[contains(text(), "John")]]');
|
|
697
30
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
31
|
+
// ❌ Manual column mapping
|
|
32
|
+
const headers = await page.locator('thead th').allTextContents();
|
|
33
|
+
const emailIndex = headers.indexOf('Email');
|
|
34
|
+
const email = await row.locator('td').nth(emailIndex).textContent();
|
|
702
35
|
```
|
|
703
36
|
|
|
704
|
-
|
|
37
|
+
### The Solution
|
|
705
38
|
|
|
706
|
-
|
|
39
|
+
**Playwright Smart Table:**
|
|
707
40
|
|
|
708
41
|
```typescript
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
```
|
|
42
|
+
// ✅ Column-aware - survives column reordering
|
|
43
|
+
const row = await table.findRow({ Name: 'John Doe' });
|
|
44
|
+
const email = await row.getCell('Email').textContent();
|
|
715
45
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
A pagination strategy is a function that receives a `TableContext` and returns `Promise<boolean>` (true if more data loaded, false if no more pages):
|
|
719
|
-
|
|
720
|
-
<!-- embed-type: PaginationStrategy -->
|
|
721
|
-
```typescript
|
|
722
|
-
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
723
|
-
```
|
|
724
|
-
<!-- /embed-type: PaginationStrategy -->
|
|
46
|
+
// ✅ Auto-pagination
|
|
47
|
+
const allEngineers = await table.findRows({ Department: 'Engineering' });
|
|
725
48
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
root: Locator;
|
|
730
|
-
config: FinalTableConfig;
|
|
731
|
-
page: Page;
|
|
732
|
-
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
733
|
-
}
|
|
49
|
+
// ✅ Type-safe
|
|
50
|
+
type Employee = { Name: string; Email: string; Department: string };
|
|
51
|
+
const table = useTable<Employee>(page.locator('#table'));
|
|
734
52
|
```
|
|
735
|
-
<!-- /embed-type: TableContext -->
|
|
736
|
-
|
|
737
|
-
---
|
|
738
53
|
|
|
739
|
-
##
|
|
54
|
+
## Quick Start
|
|
740
55
|
|
|
741
|
-
###
|
|
56
|
+
### Installation
|
|
742
57
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
```typescript
|
|
746
|
-
await table.generateConfigPrompt({ output: 'console' });
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
### <a name="generatestrategyprompt"></a>`generateStrategyPrompt(options?)`
|
|
750
|
-
|
|
751
|
-
Generates a prompt to help you write a custom pagination strategy.
|
|
752
|
-
|
|
753
|
-
```typescript
|
|
754
|
-
await table.generateStrategyPrompt({ output: 'console' });
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
**Options:**
|
|
758
|
-
<!-- embed-type: PromptOptions -->
|
|
759
|
-
```typescript
|
|
760
|
-
export interface PromptOptions {
|
|
761
|
-
/**
|
|
762
|
-
* Output Strategy:
|
|
763
|
-
* - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).
|
|
764
|
-
* - 'console': Standard console logs (Default).
|
|
765
|
-
*/
|
|
766
|
-
output?: 'console' | 'error';
|
|
767
|
-
/**
|
|
768
|
-
* Include TypeScript type definitions in the prompt
|
|
769
|
-
* @default true
|
|
770
|
-
*/
|
|
771
|
-
includeTypes?: boolean;
|
|
772
|
-
}
|
|
58
|
+
```bash
|
|
59
|
+
npm install @rickcedwhat/playwright-smart-table
|
|
773
60
|
```
|
|
774
|
-
<!-- /embed-type: PromptOptions -->
|
|
775
|
-
|
|
776
|
-
---
|
|
777
|
-
|
|
778
|
-
## 📚 Type Reference
|
|
779
|
-
|
|
780
|
-
### Core Types
|
|
781
|
-
|
|
782
|
-
#### <a name="smartrow"></a>`SmartRow`
|
|
783
61
|
|
|
784
|
-
|
|
62
|
+
### Basic Usage
|
|
785
63
|
|
|
786
|
-
<!-- embed-type: SmartRow -->
|
|
787
64
|
```typescript
|
|
788
|
-
|
|
789
|
-
* Function to get the currently active/focused cell.
|
|
790
|
-
* Returns null if no cell is active.
|
|
791
|
-
*/
|
|
792
|
-
export type GetActiveCellFn = (args: TableContext) => Promise<{
|
|
793
|
-
rowIndex: number;
|
|
794
|
-
columnIndex: number;
|
|
795
|
-
columnName?: string;
|
|
796
|
-
locator: Locator;
|
|
797
|
-
} | null>;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* SmartRow - A Playwright Locator with table-aware methods.
|
|
802
|
-
*
|
|
803
|
-
* Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.
|
|
804
|
-
*
|
|
805
|
-
* @example
|
|
806
|
-
* const row = table.getRow({ Name: 'John Doe' });
|
|
807
|
-
* await row.click(); // Standard Locator method
|
|
808
|
-
* const email = row.getCell('Email'); // Table-aware method
|
|
809
|
-
* const data = await row.toJSON(); // Extract all row data
|
|
810
|
-
* await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields
|
|
811
|
-
*/
|
|
812
|
-
export type SmartRow<T = any> = Locator & {
|
|
813
|
-
/** Optional row index (0-based) if known */
|
|
814
|
-
rowIndex?: number;
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Get a cell locator by column name.
|
|
818
|
-
* @param column - Column name (case-sensitive)
|
|
819
|
-
* @returns Locator for the cell
|
|
820
|
-
* @example
|
|
821
|
-
* const emailCell = row.getCell('Email');
|
|
822
|
-
* await expect(emailCell).toHaveText('john@example.com');
|
|
823
|
-
*/
|
|
824
|
-
getCell(column: string): Locator;
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Extract all cell data as a key-value object.
|
|
828
|
-
* @param options - Optional configuration
|
|
829
|
-
* @param options.columns - Specific columns to extract (extracts all if not specified)
|
|
830
|
-
* @returns Promise resolving to row data
|
|
831
|
-
* @example
|
|
832
|
-
* const data = await row.toJSON();
|
|
833
|
-
* // { Name: 'John', Email: 'john@example.com', ... }
|
|
834
|
-
*
|
|
835
|
-
* const partial = await row.toJSON({ columns: ['Name', 'Email'] });
|
|
836
|
-
* // { Name: 'John', Email: 'john@example.com' }
|
|
837
|
-
*/
|
|
838
|
-
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Scrolls/paginates to bring this row into view.
|
|
842
|
-
* Only works if rowIndex is known (e.g., from getRowByIndex).
|
|
843
|
-
* @throws Error if rowIndex is unknown
|
|
844
|
-
*/
|
|
845
|
-
bringIntoView(): Promise<void>;
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Intelligently fills form fields in the row.
|
|
849
|
-
* Automatically detects input types (text, select, checkbox, contenteditable).
|
|
850
|
-
*
|
|
851
|
-
* @param data - Column-value pairs to fill
|
|
852
|
-
* @param options - Optional configuration
|
|
853
|
-
* @param options.inputMappers - Custom input selectors per column
|
|
854
|
-
* @example
|
|
855
|
-
* // Auto-detection
|
|
856
|
-
* await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });
|
|
857
|
-
*
|
|
858
|
-
* // Custom input mappers
|
|
859
|
-
* await row.smartFill(
|
|
860
|
-
* { Name: 'John' },
|
|
861
|
-
* { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }
|
|
862
|
-
* );
|
|
863
|
-
*/
|
|
864
|
-
smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
865
|
-
};
|
|
866
|
-
```
|
|
867
|
-
<!-- /embed-type: SmartRow -->
|
|
868
|
-
|
|
869
|
-
**Methods:**
|
|
870
|
-
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
871
|
-
- `toJSON()`: Extracts all cell data as a key-value object
|
|
872
|
-
- `smartFill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types (text, select, checkbox) or use `inputMappers` option for custom control.
|
|
65
|
+
import { useTable } from '@rickcedwhat/playwright-smart-table';
|
|
873
66
|
|
|
874
|
-
|
|
67
|
+
const table = await useTable(page.locator('#my-table')).init();
|
|
875
68
|
|
|
876
|
-
|
|
69
|
+
// Find row by column values
|
|
70
|
+
const row = await table.findRow({ Name: 'John Doe' });
|
|
877
71
|
|
|
878
|
-
|
|
72
|
+
// Access cells by column name
|
|
73
|
+
const email = await row.getCell('Email').textContent();
|
|
879
74
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
<!-- embed-type: TableConfig -->
|
|
883
|
-
```typescript
|
|
884
|
-
/**
|
|
885
|
-
* Strategy to filter rows based on criteria.
|
|
886
|
-
*/
|
|
887
|
-
export interface FilterStrategy {
|
|
888
|
-
apply(options: {
|
|
889
|
-
rows: Locator;
|
|
890
|
-
filter: { column: string, value: string | RegExp | number };
|
|
891
|
-
colIndex: number;
|
|
892
|
-
tableContext: TableContext;
|
|
893
|
-
}): Locator;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Organized container for all table interaction strategies.
|
|
898
|
-
*/
|
|
899
|
-
export interface TableStrategies {
|
|
900
|
-
/** Strategy for discovering/scanning headers */
|
|
901
|
-
header?: HeaderStrategy;
|
|
902
|
-
/** Strategy for navigating to specific cells (row + column) */
|
|
903
|
-
cellNavigation?: CellNavigationStrategy;
|
|
904
|
-
/** Strategy for filling form inputs */
|
|
905
|
-
fill?: FillStrategy;
|
|
906
|
-
/** Strategy for paginating through data */
|
|
907
|
-
pagination?: PaginationStrategy;
|
|
908
|
-
/** Strategy for sorting columns */
|
|
909
|
-
sorting?: SortingStrategy;
|
|
910
|
-
/** Function to get a cell locator */
|
|
911
|
-
getCellLocator?: GetCellLocatorFn;
|
|
912
|
-
/** Function to get the currently active/focused cell */
|
|
913
|
-
getActiveCell?: GetActiveCellFn;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/**
|
|
917
|
-
* Configuration options for useTable.
|
|
918
|
-
*/
|
|
919
|
-
export interface TableConfig {
|
|
920
|
-
/** Selector for the table headers */
|
|
921
|
-
headerSelector?: string;
|
|
922
|
-
/** Selector for the table rows */
|
|
923
|
-
rowSelector?: string;
|
|
924
|
-
/** Selector for the cells within a row */
|
|
925
|
-
cellSelector?: string;
|
|
926
|
-
/** Number of pages to scan for verification */
|
|
927
|
-
maxPages?: number;
|
|
928
|
-
/** Hook to rename columns dynamically */
|
|
929
|
-
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
930
|
-
/** Automatically scroll to table on init */
|
|
931
|
-
autoScroll?: boolean;
|
|
932
|
-
/** Debug options for development and troubleshooting */
|
|
933
|
-
debug?: DebugConfig;
|
|
934
|
-
/** Reset hook */
|
|
935
|
-
onReset?: (context: TableContext) => Promise<void>;
|
|
936
|
-
/** All interaction strategies */
|
|
937
|
-
strategies?: TableStrategies;
|
|
938
|
-
}
|
|
75
|
+
// Search across paginated tables
|
|
76
|
+
const allActive = await table.findRows({ Status: 'Active' });
|
|
939
77
|
```
|
|
940
|
-
<!-- /embed-type: TableConfig -->
|
|
941
78
|
|
|
942
|
-
|
|
79
|
+
## Key Features
|
|
943
80
|
|
|
944
|
-
-
|
|
945
|
-
-
|
|
946
|
-
-
|
|
947
|
-
-
|
|
948
|
-
-
|
|
949
|
-
-
|
|
950
|
-
-
|
|
951
|
-
- `debug`: Enable verbose logging (default: `false`)
|
|
952
|
-
- `onReset`: Strategy called when `table.reset()` is invoked
|
|
81
|
+
- 🎯 **Smart Locators** - Find rows by content, not position
|
|
82
|
+
- 📄 **Auto-Pagination** - Search across all pages automatically
|
|
83
|
+
- 🔍 **Column-Aware Access** - Access cells by column name
|
|
84
|
+
- 🛠️ **Debug Mode** - Visual debugging with slow motion and logging
|
|
85
|
+
- 🔌 **Extensible Strategies** - Support any table implementation
|
|
86
|
+
- 💪 **Type-Safe** - Full TypeScript support
|
|
87
|
+
- 🚀 **Production-Ready** - Battle-tested in real-world applications
|
|
953
88
|
|
|
954
|
-
|
|
89
|
+
## When to Use This Library
|
|
955
90
|
|
|
956
|
-
|
|
91
|
+
**Use this library when you need to:**
|
|
957
92
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
*
|
|
966
|
-
* // Function selector
|
|
967
|
-
* rowSelector: (root) => root.locator('[role="row"]')
|
|
968
|
-
*/
|
|
969
|
-
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Function to get a cell locator given row, column info.
|
|
973
|
-
* Replaces the old cellResolver.
|
|
974
|
-
*/
|
|
975
|
-
```
|
|
976
|
-
<!-- /embed-type: Selector -->
|
|
93
|
+
- ✅ Find rows by column values
|
|
94
|
+
- ✅ Access cells by column name
|
|
95
|
+
- ✅ Search across paginated tables
|
|
96
|
+
- ✅ Handle column reordering
|
|
97
|
+
- ✅ Extract structured data
|
|
98
|
+
- ✅ Fill/edit table cells
|
|
99
|
+
- ✅ Work with dynamic tables (MUI DataGrid, AG Grid, etc.)
|
|
977
100
|
|
|
978
|
-
**
|
|
979
|
-
```typescript
|
|
980
|
-
// String selector
|
|
981
|
-
rowSelector: 'tbody tr'
|
|
101
|
+
**You might not need this library if:**
|
|
982
102
|
|
|
983
|
-
|
|
984
|
-
|
|
103
|
+
- ❌ You don't interact with tables at all
|
|
104
|
+
- ❌ You don't need to find a row based on a value in a cell
|
|
105
|
+
- ❌ You don't need to find a cell based on a value in another cell in the same row
|
|
985
106
|
|
|
986
|
-
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
#### <a name="paginationstrategy"></a>`PaginationStrategy`
|
|
107
|
+
## Documentation
|
|
990
108
|
|
|
991
|
-
|
|
109
|
+
**📚 Full documentation available at: https://rickcedwhat.github.io/playwright-smart-table/**
|
|
992
110
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
111
|
+
- [Getting Started Guide](https://rickcedwhat.github.io/playwright-smart-table/guide/getting-started)
|
|
112
|
+
- [Core Concepts](https://rickcedwhat.github.io/playwright-smart-table/guide/core-concepts)
|
|
113
|
+
- [API Reference](https://rickcedwhat.github.io/playwright-smart-table/api/)
|
|
114
|
+
- [Examples](https://rickcedwhat.github.io/playwright-smart-table/examples/)
|
|
115
|
+
- [Troubleshooting](https://rickcedwhat.github.io/playwright-smart-table/troubleshooting)
|
|
999
116
|
|
|
1000
|
-
|
|
117
|
+
## Contributing
|
|
1001
118
|
|
|
1002
|
-
|
|
119
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
1003
120
|
|
|
1004
|
-
|
|
1005
|
-
2. **Use Debug Mode**: When troubleshooting, enable `debug: true` to see what the library is doing
|
|
1006
|
-
3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
|
|
1007
|
-
4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
|
|
1008
|
-
5. **Pagination Strategies**: Create reusable strategies for tables with similar pagination patterns
|
|
1009
|
-
6. **Async vs Sync**: Use `findRow()` for paginated searches and `getRow()` for strict, single-page assertions.
|
|
1010
|
-
7. **Sorting**: Use `table.sorting.apply()` to sort columns and `table.sorting.getState()` to check sort state.
|
|
121
|
+
## License
|
|
1011
122
|
|
|
1012
|
-
|
|
123
|
+
MIT © Cedrick Catalan
|
|
1013
124
|
|
|
1014
|
-
##
|
|
125
|
+
## Links
|
|
1015
126
|
|
|
1016
|
-
|
|
127
|
+
- [Documentation](https://rickcedwhat.github.io/playwright-smart-table/)
|
|
128
|
+
- [npm Package](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
|
|
129
|
+
- [GitHub Repository](https://github.com/rickcedwhat/playwright-smart-table)
|
|
130
|
+
- [Issues](https://github.com/rickcedwhat/playwright-smart-table/issues)
|