@rickcedwhat/playwright-smart-table 5.3.0 → 6.0.0

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