@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.
Files changed (56) hide show
  1. package/README.md +78 -964
  2. package/dist/examples/glide-strategies/columns.d.ts +13 -0
  3. package/dist/examples/glide-strategies/columns.js +43 -0
  4. package/dist/examples/glide-strategies/headers.d.ts +9 -0
  5. package/dist/examples/glide-strategies/headers.js +68 -0
  6. package/dist/src/filterEngine.d.ts +11 -0
  7. package/dist/src/filterEngine.js +39 -0
  8. package/dist/src/index.d.ts +2 -0
  9. package/dist/src/index.js +18 -0
  10. package/dist/src/plugins.d.ts +32 -0
  11. package/dist/src/plugins.js +13 -0
  12. package/dist/src/smartRow.d.ts +7 -0
  13. package/dist/src/smartRow.js +160 -0
  14. package/dist/src/strategies/columns.d.ts +18 -0
  15. package/dist/src/strategies/columns.js +21 -0
  16. package/dist/src/strategies/dedupe.d.ts +9 -0
  17. package/dist/src/strategies/dedupe.js +27 -0
  18. package/dist/src/strategies/fill.d.ts +7 -0
  19. package/dist/src/strategies/fill.js +88 -0
  20. package/dist/src/strategies/glide.d.ts +29 -0
  21. package/dist/src/strategies/glide.js +98 -0
  22. package/dist/src/strategies/headers.d.ts +13 -0
  23. package/dist/src/strategies/headers.js +30 -0
  24. package/dist/src/strategies/index.d.ts +54 -0
  25. package/dist/src/strategies/index.js +43 -0
  26. package/dist/src/strategies/loading.d.ts +48 -0
  27. package/dist/src/strategies/loading.js +82 -0
  28. package/dist/src/strategies/pagination.d.ts +33 -0
  29. package/dist/src/strategies/pagination.js +79 -0
  30. package/dist/src/strategies/rdg.d.ts +25 -0
  31. package/dist/src/strategies/rdg.js +100 -0
  32. package/dist/src/strategies/resolution.d.ts +22 -0
  33. package/dist/src/strategies/resolution.js +30 -0
  34. package/dist/src/strategies/sorting.d.ts +12 -0
  35. package/dist/src/strategies/sorting.js +68 -0
  36. package/dist/src/strategies/stabilization.d.ts +29 -0
  37. package/dist/src/strategies/stabilization.js +91 -0
  38. package/dist/src/strategies/validation.d.ts +22 -0
  39. package/dist/src/strategies/validation.js +54 -0
  40. package/dist/src/strategies/virtualizedPagination.d.ts +32 -0
  41. package/dist/src/strategies/virtualizedPagination.js +80 -0
  42. package/dist/src/typeContext.d.ts +6 -0
  43. package/dist/src/typeContext.js +465 -0
  44. package/dist/src/types.d.ts +458 -0
  45. package/dist/src/types.js +2 -0
  46. package/dist/src/useTable.d.ts +44 -0
  47. package/dist/src/useTable.js +642 -0
  48. package/dist/src/utils/debugUtils.d.ts +17 -0
  49. package/dist/src/utils/debugUtils.js +62 -0
  50. package/dist/src/utils/smartRowArray.d.ts +14 -0
  51. package/dist/src/utils/smartRowArray.js +22 -0
  52. package/dist/src/utils/stringUtils.d.ts +22 -0
  53. package/dist/src/utils/stringUtils.js +73 -0
  54. package/dist/src/utils.d.ts +7 -0
  55. package/dist/src/utils.js +29 -0
  56. package/package.json +2 -4
package/README.md CHANGED
@@ -1,1016 +1,130 @@
1
- # Playwright Smart Table 🧠
1
+ # Playwright Smart Table
2
2
 
3
- A production-ready, type-safe table wrapper for Playwright that abstracts away the complexity of testing dynamic web tables. Handles pagination, infinite scroll, virtualization, and data grids (MUI, AG-Grid) so your tests remain clean and readable.
3
+ **Production-ready table testing for Playwright with smart column-aware locators.**
4
4
 
5
- ## 📦 Installation
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
+ [![npm version](https://img.shields.io/npm/v/@rickcedwhat/playwright-smart-table.svg)](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
67
7
 
68
8
  ---
69
9
 
70
- ## 🔧 Configuration & Advanced Scenarios
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
- // Assert it does NOT exist
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
- ## 🧩 Pagination Strategies
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
- ### Built-in Strategies
18
+ Testing HTML tables in Playwright is painful. Traditional approaches are fragile and hard to maintain.
681
19
 
682
- #### <a name="tablestrategiesclicknext"></a>`Strategies.Pagination.clickNext(selector)`
20
+ ### The Problem
683
21
 
684
- Best for standard paginated tables (Datatables, lists). Clicks a button/link and waits for table content to change.
22
+ **Traditional approach:**
685
23
 
686
24
  ```typescript
687
- strategies: {
688
- pagination: Strategies.Pagination.clickNext((root) =>
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
- Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
28
+ // Brittle XPath
29
+ const row = page.locator('//tr[td[contains(text(), "John")]]');
697
30
 
698
- ```typescript
699
- strategies: {
700
- pagination: Strategies.Pagination.infiniteScroll()
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
- #### <a name="tablestrategiesclickloadmore"></a>`Strategies.Pagination.clickLoadMore(selector)`
37
+ ### The Solution
705
38
 
706
- Best for "Load More" buttons that may not be part of the table. Clicks and waits for row count to increase.
39
+ **Playwright Smart Table:**
707
40
 
708
41
  ```typescript
709
- strategies: {
710
- pagination: Strategies.Pagination.clickLoadMore((page) =>
711
- page.getByRole('button', { name: 'Load More' })
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
- ### Custom Strategies
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
- <!-- embed-type: TableContext -->
727
- ```typescript
728
- export interface TableContext {
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
- ## 🛠️ Developer Tools
54
+ ## Quick Start
740
55
 
741
- ### <a name="generateconfigprompt"></a>`generateConfigPrompt(options?)`
56
+ ### Installation
742
57
 
743
- Generates a prompt you can paste into ChatGPT/Gemini to automatically generate the `TableConfig` for your specific HTML.
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
- A `SmartRow` extends Playwright's `Locator` with table-aware methods.
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
- All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
67
+ const table = await useTable(page.locator('#my-table')).init();
875
68
 
876
- **Note**: `getRequestIndex()` is an internal method for row tracking - you typically won't need it.
69
+ // Find row by column values
70
+ const row = await table.findRow({ Name: 'John Doe' });
877
71
 
878
- #### <a name="tableconfig"></a>`TableConfig`
72
+ // Access cells by column name
73
+ const email = await row.getCell('Email').textContent();
879
74
 
880
- Configuration options for `useTable()`.
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
- **Property Descriptions:**
79
+ ## Key Features
943
80
 
944
- - `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
945
- - `headerSelector`: CSS selector or function for header cells (default: `"th"`)
946
- - `cellSelector`: CSS selector or function for data cells (default: `"td"`)
947
- - `strategies`: Configuration object for interaction strategies (pagination, sorting, etc.)
948
- - `maxPages`: Maximum pages to scan when searching (default: `1`)
949
- - `headerTransformer`: Function to transform/rename column headers dynamically
950
- - `autoScroll`: Automatically scroll table into view (default: `true`)
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
- #### <a name="selector"></a>`Selector`
89
+ ## When to Use This Library
955
90
 
956
- Flexible selector type supporting strings, functions, or existing locators.
91
+ **Use this library when you need to:**
957
92
 
958
- <!-- embed-type: Selector -->
959
- ```typescript
960
- /**
961
- * Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.
962
- * @example
963
- * // String selector
964
- * rowSelector: 'tbody tr'
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
- **Examples:**
979
- ```typescript
980
- // String selector
981
- rowSelector: 'tbody tr'
101
+ **You might not need this library if:**
982
102
 
983
- // Function selector (useful for complex cases)
984
- rowSelector: (root) => root.locator('[role="row"]')
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
- // Can also accept a Locator directly
987
- ```
988
-
989
- #### <a name="paginationstrategy"></a>`PaginationStrategy`
107
+ ## Documentation
990
108
 
991
- Function signature for custom pagination logic.
109
+ **📚 Full documentation available at: https://rickcedwhat.github.io/playwright-smart-table/**
992
110
 
993
- <!-- embed-type: PaginationStrategy -->
994
- ```typescript
995
- export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
996
- ```
997
- <!-- /embed-type: PaginationStrategy -->
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
- ## 🚀 Tips & Best Practices
119
+ Contributions are welcome! Please feel free to submit a Pull Request.
1003
120
 
1004
- 1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
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
- ## 📝 License
125
+ ## Links
1015
126
 
1016
- ISC
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)