@rickcedwhat/playwright-smart-table 3.2.0 → 5.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.
- package/README.md +357 -100
- package/dist/filterEngine.d.ts +11 -0
- package/dist/filterEngine.js +38 -0
- package/dist/smartRow.d.ts +7 -0
- package/dist/smartRow.js +152 -0
- package/dist/strategies/columns.d.ts +5 -17
- package/dist/strategies/columns.js +2 -34
- package/dist/strategies/headers.d.ts +0 -16
- package/dist/strategies/headers.js +1 -113
- package/dist/strategies/index.d.ts +25 -0
- package/dist/strategies/index.js +19 -1
- package/dist/strategies/pagination.d.ts +0 -21
- package/dist/strategies/pagination.js +1 -23
- package/dist/strategies/resolution.d.ts +22 -0
- package/dist/strategies/resolution.js +30 -0
- package/dist/strategies/validation.d.ts +22 -0
- package/dist/strategies/validation.js +54 -0
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +188 -58
- package/dist/types.d.ts +177 -66
- package/dist/useTable.d.ts +5 -9
- package/dist/useTable.js +139 -268
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ const table = await useTable(page.locator('#example'), {
|
|
|
26
26
|
}).init();
|
|
27
27
|
|
|
28
28
|
// Find the row with Name="Airi Satou", then get the Position cell
|
|
29
|
-
const row = table.
|
|
29
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
30
30
|
|
|
31
31
|
const positionCell = row.getCell('Position');
|
|
32
32
|
await expect(positionCell).toHaveText('Accountant');
|
|
@@ -35,7 +35,7 @@ await expect(positionCell).toHaveText('Accountant');
|
|
|
35
35
|
|
|
36
36
|
**What's happening here?**
|
|
37
37
|
- `useTable()` creates a smart table wrapper around your table locator
|
|
38
|
-
- `
|
|
38
|
+
- `getRow()` finds a specific row by column values
|
|
39
39
|
- The returned `SmartRow` knows its column structure, so `.getCell('Position')` works directly
|
|
40
40
|
|
|
41
41
|
### Step 2: Understanding SmartRow
|
|
@@ -47,7 +47,7 @@ The `SmartRow` is the core power of this library. Unlike a standard Playwright `
|
|
|
47
47
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
48
48
|
|
|
49
49
|
// Get SmartRow via getByRow
|
|
50
|
-
const row = table.
|
|
50
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
51
51
|
|
|
52
52
|
// Interact with cell using column name (resilient to column reordering)
|
|
53
53
|
const positionCell = row.getCell('Position');
|
|
@@ -63,7 +63,7 @@ console.log(data);
|
|
|
63
63
|
**Key Benefits:**
|
|
64
64
|
- ✅ Column names instead of indices (survives column reordering)
|
|
65
65
|
- ✅ Extends Playwright's `Locator` API (all `.click()`, `.isVisible()`, etc. work)
|
|
66
|
-
- ✅ `.toJSON()` for quick data extraction
|
|
66
|
+
- ✅ `.toJSON()` for quick data extraction (uses `columnStrategy` to ensure visibility)
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
@@ -81,9 +81,11 @@ const table = useTable(page.locator('#example'), {
|
|
|
81
81
|
headerSelector: 'thead th',
|
|
82
82
|
cellSelector: 'td',
|
|
83
83
|
// Strategy: Tell it how to find the next page
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
strategies: {
|
|
85
|
+
pagination: Strategies.Pagination.clickNext(() =>
|
|
86
|
+
page.getByRole('link', { name: 'Next' })
|
|
87
|
+
)
|
|
88
|
+
},
|
|
87
89
|
maxPages: 5 // Allow scanning up to 5 pages
|
|
88
90
|
});
|
|
89
91
|
await table.init();
|
|
@@ -91,8 +93,8 @@ await table.init();
|
|
|
91
93
|
// ✅ Verify Colleen is NOT visible initially
|
|
92
94
|
await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
|
|
93
95
|
|
|
94
|
-
// Use
|
|
95
|
-
await expect(await table.
|
|
96
|
+
// Use findRow for pagination
|
|
97
|
+
await expect(await table.findRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
96
98
|
// NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
|
|
97
99
|
```
|
|
98
100
|
<!-- /embed: pagination -->
|
|
@@ -110,7 +112,7 @@ const table = useTable(page.locator('#example'), {
|
|
|
110
112
|
});
|
|
111
113
|
await table.init();
|
|
112
114
|
|
|
113
|
-
const row = table.
|
|
115
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
114
116
|
await expect(row).toBeVisible();
|
|
115
117
|
```
|
|
116
118
|
<!-- /embed: advanced-debug -->
|
|
@@ -119,14 +121,14 @@ This will log header mappings, row scans, and pagination triggers to help troubl
|
|
|
119
121
|
|
|
120
122
|
### Resetting Table State
|
|
121
123
|
|
|
122
|
-
If your tests navigate deep into a paginated table, use `.reset()` to return to the first page:
|
|
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):
|
|
123
125
|
|
|
124
126
|
<!-- embed: advanced-reset -->
|
|
125
127
|
```typescript
|
|
126
128
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
127
129
|
// Navigate deep into the table by searching for a row on a later page
|
|
128
130
|
try {
|
|
129
|
-
await table.
|
|
131
|
+
await table.findRow({ Name: 'Angelica Ramos' });
|
|
130
132
|
} catch (e) { }
|
|
131
133
|
|
|
132
134
|
// Reset internal state (and potentially UI) to initial page
|
|
@@ -134,11 +136,28 @@ await table.reset();
|
|
|
134
136
|
await table.init(); // Re-init after reset
|
|
135
137
|
|
|
136
138
|
// Now subsequent searches start from the beginning
|
|
137
|
-
const currentPageRow = table.
|
|
139
|
+
const currentPageRow = table.getRow({ Name: 'Airi Satou' });
|
|
138
140
|
await expect(currentPageRow).toBeVisible();
|
|
139
141
|
```
|
|
140
142
|
<!-- /embed: advanced-reset -->
|
|
141
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
|
+
|
|
142
161
|
### Column Scanning
|
|
143
162
|
|
|
144
163
|
Efficiently extract all values from a specific column:
|
|
@@ -160,7 +179,7 @@ Use `smartFill()` to intelligently populate form fields in a table row. The meth
|
|
|
160
179
|
<!-- embed: fill-basic -->
|
|
161
180
|
```typescript
|
|
162
181
|
// Find a row and fill it with new data
|
|
163
|
-
const row = table.
|
|
182
|
+
const row = table.getRow({ ID: '1' });
|
|
164
183
|
|
|
165
184
|
await row.smartFill({
|
|
166
185
|
Name: 'John Updated',
|
|
@@ -229,9 +248,11 @@ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
|
|
|
229
248
|
rowSelector: '.MuiDataGrid-row',
|
|
230
249
|
headerSelector: '.MuiDataGrid-columnHeader',
|
|
231
250
|
cellSelector: '.MuiDataGrid-cell',
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
strategies: {
|
|
252
|
+
pagination: Strategies.Pagination.clickNext(
|
|
253
|
+
(root) => root.getByRole("button", { name: "Go to next page" })
|
|
254
|
+
)
|
|
255
|
+
},
|
|
235
256
|
maxPages: 5,
|
|
236
257
|
// Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
|
|
237
258
|
headerTransformer: ({ text }) => {
|
|
@@ -250,11 +271,11 @@ expect(headers).toContain('Actions');
|
|
|
250
271
|
|
|
251
272
|
// Use the renamed column
|
|
252
273
|
// First check it's not on the current page
|
|
253
|
-
const currentPageRow = table.
|
|
274
|
+
const currentPageRow = table.getRow({ "Last name": "Melisandre" });
|
|
254
275
|
await expect(currentPageRow).not.toBeVisible();
|
|
255
276
|
|
|
256
277
|
// Then find it across pages
|
|
257
|
-
const row = await table.
|
|
278
|
+
const row = await table.findRow({ "Last name": "Melisandre" });
|
|
258
279
|
const actionsCell = row.getCell('Actions');
|
|
259
280
|
await actionsCell.getByLabel("Select row").click();
|
|
260
281
|
```
|
|
@@ -278,34 +299,119 @@ const table = useTable(page.locator('#table1'), {
|
|
|
278
299
|
await table.init();
|
|
279
300
|
|
|
280
301
|
// Now column names are consistent
|
|
281
|
-
const row = table.
|
|
302
|
+
const row = table.getRow({ "Last Name": "Doe" });
|
|
282
303
|
const emailCell = row.getCell("Email");
|
|
283
304
|
await expect(emailCell).toHaveText("jdoe@hotmail.com");
|
|
284
305
|
```
|
|
285
306
|
<!-- /embed: header-transformer-normalize -->
|
|
286
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
|
+
onFirst: async ({ allData }) => {
|
|
359
|
+
console.log('Starting data collection...');
|
|
360
|
+
// Could perform setup actions
|
|
361
|
+
},
|
|
362
|
+
onLast: 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
|
+
- `onFirst`: Runs **before** your callback processes the first page
|
|
373
|
+
- `onLast`: Runs **after** your callback processes the last page
|
|
374
|
+
- Both are optional and receive `{ index, rows, allData }`
|
|
375
|
+
|
|
287
376
|
---
|
|
288
377
|
|
|
289
378
|
## 📖 API Reference
|
|
290
379
|
|
|
380
|
+
### Method Comparison
|
|
381
|
+
|
|
382
|
+
Quick reference for choosing the right method:
|
|
383
|
+
|
|
384
|
+
| Method | Async/Sync | Paginates? | Returns | Use When |
|
|
385
|
+
|--------|------------|------------|---------|----------|
|
|
386
|
+
| `getRow()` | **Sync** | ❌ No | Single `SmartRow` | Finding row on current page only |
|
|
387
|
+
| `findRow()` | **Async** | ✅ Yes | Single `SmartRow` | Searching across pages |
|
|
388
|
+
| `getRows()` | **Async** | ❌ No | `SmartRow[]` | Getting all rows on current page |
|
|
389
|
+
| `findRows()` | **Async** | ✅ Yes | `SmartRow[]` | Getting all matching rows across pages |
|
|
390
|
+
| `iterateThroughTable()` | **Async** | ✅ Yes | `T[]` | Processing/scraping all pages with custom logic |
|
|
391
|
+
|
|
392
|
+
**Naming Pattern:**
|
|
393
|
+
- `get*` = Current page only (fast, no pagination)
|
|
394
|
+
- `find*` = Search across pages (slower, uses pagination)
|
|
395
|
+
|
|
291
396
|
### Table Methods
|
|
292
397
|
|
|
293
|
-
#### <a name="
|
|
398
|
+
#### <a name="getrow"></a>`getRow(filters, options?)`
|
|
294
399
|
|
|
295
|
-
**Purpose:** Strict retrieval - finds exactly one row matching the filters
|
|
400
|
+
**Purpose:** Strict retrieval - finds exactly one row matching the filters on the **current page**.
|
|
296
401
|
|
|
297
402
|
**Behavior:**
|
|
298
403
|
- ✅ Returns `SmartRow` if exactly one match
|
|
299
404
|
- ❌ Throws error if multiple matches (ambiguous query)
|
|
300
405
|
- 👻 Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
301
|
-
-
|
|
406
|
+
- ℹ️ **Sync method**: Returns immediate locator result.
|
|
407
|
+
- 🔍 **Filtering**: Uses "contains" matching by default (e.g., "Tokyo" matches "Tokyo Office"). Set `exact: true` for strict equality.
|
|
302
408
|
|
|
303
409
|
**Type Signature:**
|
|
304
410
|
```typescript
|
|
305
|
-
|
|
411
|
+
getRow: <T extends { asJSON?: boolean }>(
|
|
306
412
|
filters: Record<string, string | RegExp | number>,
|
|
307
|
-
options?: { exact?: boolean
|
|
308
|
-
) =>
|
|
413
|
+
options?: { exact?: boolean } & T
|
|
414
|
+
) => SmartRow;
|
|
309
415
|
```
|
|
310
416
|
|
|
311
417
|
<!-- embed: get-by-row -->
|
|
@@ -315,11 +421,11 @@ const table = useTable(page.locator('#example'), { headerSelector: 'thead th' })
|
|
|
315
421
|
await table.init();
|
|
316
422
|
|
|
317
423
|
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
318
|
-
const row = table.
|
|
424
|
+
const row = table.getRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
319
425
|
await expect(row).toBeVisible();
|
|
320
426
|
|
|
321
427
|
// Assert it does NOT exist
|
|
322
|
-
await expect(table.
|
|
428
|
+
await expect(table.getRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
323
429
|
```
|
|
324
430
|
<!-- /embed: get-by-row -->
|
|
325
431
|
|
|
@@ -327,26 +433,46 @@ Get row data as JSON:
|
|
|
327
433
|
<!-- embed: get-by-row-json -->
|
|
328
434
|
```typescript
|
|
329
435
|
// Get row data as JSON object
|
|
330
|
-
const row = table.
|
|
436
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
331
437
|
const data = await row.toJSON();
|
|
332
438
|
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
333
439
|
|
|
334
440
|
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
335
441
|
expect(data).toHaveProperty('Position');
|
|
442
|
+
|
|
443
|
+
// Get specific columns only (faster for large tables)
|
|
444
|
+
const partial = await row.toJSON({ columns: ['Name'] });
|
|
445
|
+
expect(partial).toEqual({ Name: 'Airi Satou' });
|
|
336
446
|
```
|
|
337
447
|
<!-- /embed: get-by-row-json -->
|
|
338
448
|
|
|
339
|
-
#### <a name="
|
|
449
|
+
#### <a name="findrow"></a>`findRow(filters, options?)`
|
|
340
450
|
|
|
341
|
-
**Purpose:**
|
|
451
|
+
**Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
|
|
342
452
|
|
|
343
|
-
**
|
|
453
|
+
**Behavior:**
|
|
454
|
+
- 🔄 Auto-initializes table if needed
|
|
455
|
+
- 🔎 Paginates through data until match is found or `maxPages` reached
|
|
456
|
+
- ✅ Returns `SmartRow` if found
|
|
457
|
+
- 👻 Returns sentinel locator if not found
|
|
458
|
+
|
|
459
|
+
**Type Signature:**
|
|
460
|
+
```typescript
|
|
461
|
+
findRow: (
|
|
462
|
+
filters: Record<string, string | RegExp | number>,
|
|
463
|
+
options?: { exact?: boolean, maxPages?: number }
|
|
464
|
+
) => Promise<SmartRow>;
|
|
465
|
+
```
|
|
344
466
|
|
|
345
|
-
|
|
467
|
+
#### <a name="getrows"></a>`getRows(options?)`
|
|
468
|
+
|
|
469
|
+
**Purpose:** Inclusive retrieval - gets all rows on the **current page** matching optional filters.
|
|
470
|
+
|
|
471
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
|
|
346
472
|
|
|
347
473
|
**Type Signature:**
|
|
348
474
|
```typescript
|
|
349
|
-
|
|
475
|
+
getRows: <T extends { asJSON?: boolean }>(
|
|
350
476
|
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
351
477
|
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
352
478
|
```
|
|
@@ -355,17 +481,17 @@ getAllCurrentRows: <T extends { asJSON?: boolean }>(
|
|
|
355
481
|
```typescript
|
|
356
482
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
357
483
|
// 1. Get ALL rows on the current page
|
|
358
|
-
const allRows = await table.
|
|
484
|
+
const allRows = await table.getRows();
|
|
359
485
|
expect(allRows.length).toBeGreaterThan(0);
|
|
360
486
|
|
|
361
487
|
// 2. Get subset of rows (Filtering)
|
|
362
|
-
const tokyoUsers = await table.
|
|
488
|
+
const tokyoUsers = await table.getRows({
|
|
363
489
|
filter: { Office: 'Tokyo' }
|
|
364
490
|
});
|
|
365
491
|
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
366
492
|
|
|
367
493
|
// 3. Dump data to JSON
|
|
368
|
-
const data = await table.
|
|
494
|
+
const data = await table.getRows({ asJSON: true });
|
|
369
495
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
370
496
|
expect(data.length).toBeGreaterThan(0);
|
|
371
497
|
expect(data[0]).toHaveProperty('Name');
|
|
@@ -376,7 +502,7 @@ Filter rows with exact match:
|
|
|
376
502
|
<!-- embed: get-all-rows-exact -->
|
|
377
503
|
```typescript
|
|
378
504
|
// Get rows with exact match (default is fuzzy/contains match)
|
|
379
|
-
const exactMatches = await table.
|
|
505
|
+
const exactMatches = await table.getRows({
|
|
380
506
|
filter: { Office: 'Tokyo' },
|
|
381
507
|
exact: true // Requires exact string match
|
|
382
508
|
});
|
|
@@ -385,6 +511,22 @@ expect(exactMatches.length).toBeGreaterThan(0);
|
|
|
385
511
|
```
|
|
386
512
|
<!-- /embed: get-all-rows-exact -->
|
|
387
513
|
|
|
514
|
+
#### <a name="findrows"></a>`findRows(filters, options?)`
|
|
515
|
+
|
|
516
|
+
**Purpose:** Async retrieval - finds **all** rows matching filters **across multiple pages**.
|
|
517
|
+
|
|
518
|
+
**Behavior:**
|
|
519
|
+
- 🔄 Paginates and accumulates matches
|
|
520
|
+
- ⚠️ Can be slow on large datasets, use `maxPages` to limit scope
|
|
521
|
+
|
|
522
|
+
**Type Signature:**
|
|
523
|
+
```typescript
|
|
524
|
+
findRows: (
|
|
525
|
+
filters: Record<string, string | RegExp | number>,
|
|
526
|
+
options?: { exact?: boolean, maxPages?: number }
|
|
527
|
+
) => Promise<SmartRow[]>;
|
|
528
|
+
```
|
|
529
|
+
|
|
388
530
|
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
389
531
|
|
|
390
532
|
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
@@ -455,6 +597,44 @@ Resets table state (clears cache, pagination flags) and invokes the `onReset` st
|
|
|
455
597
|
reset: () => Promise<void>;
|
|
456
598
|
```
|
|
457
599
|
|
|
600
|
+
#### <a name="sorting"></a>`sorting.apply(column, direction)` & `sorting.getState(column)`
|
|
601
|
+
|
|
602
|
+
If you've configured a sorting strategy, use these methods to sort columns and check their current sort state.
|
|
603
|
+
|
|
604
|
+
**Apply Sort:**
|
|
605
|
+
```typescript
|
|
606
|
+
// Configure table with sorting strategy
|
|
607
|
+
const table = useTable(page.locator('#sortable-table'), {
|
|
608
|
+
strategies: {
|
|
609
|
+
sorting: Strategies.Sorting.AriaSort()
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
await table.init();
|
|
613
|
+
|
|
614
|
+
// Sort by Name column (ascending)
|
|
615
|
+
await table.sorting.apply('Name', 'asc');
|
|
616
|
+
|
|
617
|
+
// Sort by Age column (descending)
|
|
618
|
+
await table.sorting.apply('Age', 'desc');
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Check Sort State:**
|
|
622
|
+
```typescript
|
|
623
|
+
// Get current sort state of a column
|
|
624
|
+
const nameSort = await table.sorting.getState('Name');
|
|
625
|
+
// Returns: 'asc' | 'desc' | 'none'
|
|
626
|
+
|
|
627
|
+
expect(nameSort).toBe('asc');
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Type Signatures:**
|
|
631
|
+
```typescript
|
|
632
|
+
sorting: {
|
|
633
|
+
apply: (columnName: string, direction: 'asc' | 'desc') => Promise<void>;
|
|
634
|
+
getState: (columnName: string) => Promise<'asc' | 'desc' | 'none'>;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
458
638
|
---
|
|
459
639
|
|
|
460
640
|
## 🧩 Pagination Strategies
|
|
@@ -463,32 +643,38 @@ This library uses the **Strategy Pattern** for pagination. Use built-in strategi
|
|
|
463
643
|
|
|
464
644
|
### Built-in Strategies
|
|
465
645
|
|
|
466
|
-
#### <a name="tablestrategiesclicknext"></a>`
|
|
646
|
+
#### <a name="tablestrategiesclicknext"></a>`Strategies.Pagination.clickNext(selector)`
|
|
467
647
|
|
|
468
648
|
Best for standard paginated tables (Datatables, lists). Clicks a button/link and waits for table content to change.
|
|
469
649
|
|
|
470
650
|
```typescript
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
)
|
|
651
|
+
strategies: {
|
|
652
|
+
pagination: Strategies.Pagination.clickNext((root) =>
|
|
653
|
+
root.page().getByRole('button', { name: 'Next' })
|
|
654
|
+
)
|
|
655
|
+
}
|
|
474
656
|
```
|
|
475
657
|
|
|
476
|
-
#### <a name="tablestrategiesinfinitescroll"></a>`
|
|
658
|
+
#### <a name="tablestrategiesinfinitescroll"></a>`Strategies.Pagination.infiniteScroll()`
|
|
477
659
|
|
|
478
660
|
Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
479
661
|
|
|
480
662
|
```typescript
|
|
481
|
-
|
|
663
|
+
strategies: {
|
|
664
|
+
pagination: Strategies.Pagination.infiniteScroll()
|
|
665
|
+
}
|
|
482
666
|
```
|
|
483
667
|
|
|
484
|
-
#### <a name="tablestrategiesclickloadmore"></a>`
|
|
668
|
+
#### <a name="tablestrategiesclickloadmore"></a>`Strategies.Pagination.clickLoadMore(selector)`
|
|
485
669
|
|
|
486
|
-
Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
670
|
+
Best for "Load More" buttons that may not be part of the table. Clicks and waits for row count to increase.
|
|
487
671
|
|
|
488
672
|
```typescript
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
)
|
|
673
|
+
strategies: {
|
|
674
|
+
pagination: Strategies.Pagination.clickLoadMore((page) =>
|
|
675
|
+
page.getByRole('button', { name: 'Load More' })
|
|
676
|
+
)
|
|
677
|
+
}
|
|
492
678
|
```
|
|
493
679
|
|
|
494
680
|
### Custom Strategies
|
|
@@ -559,19 +745,83 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
559
745
|
|
|
560
746
|
<!-- embed-type: SmartRow -->
|
|
561
747
|
```typescript
|
|
562
|
-
|
|
563
|
-
|
|
748
|
+
/**
|
|
749
|
+
* Function to get the currently active/focused cell.
|
|
750
|
+
* Returns null if no cell is active.
|
|
751
|
+
*/
|
|
752
|
+
export type GetActiveCellFn = (args: TableContext) => Promise<{
|
|
753
|
+
rowIndex: number;
|
|
754
|
+
columnIndex: number;
|
|
755
|
+
columnName?: string;
|
|
756
|
+
locator: Locator;
|
|
757
|
+
} | null>;
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* SmartRow - A Playwright Locator with table-aware methods.
|
|
762
|
+
*
|
|
763
|
+
* Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* const row = table.getRow({ Name: 'John Doe' });
|
|
767
|
+
* await row.click(); // Standard Locator method
|
|
768
|
+
* const email = row.getCell('Email'); // Table-aware method
|
|
769
|
+
* const data = await row.toJSON(); // Extract all row data
|
|
770
|
+
* await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields
|
|
771
|
+
*/
|
|
772
|
+
export type SmartRow<T = any> = Locator & {
|
|
773
|
+
/** Optional row index (0-based) if known */
|
|
564
774
|
rowIndex?: number;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Get a cell locator by column name.
|
|
778
|
+
* @param column - Column name (case-sensitive)
|
|
779
|
+
* @returns Locator for the cell
|
|
780
|
+
* @example
|
|
781
|
+
* const emailCell = row.getCell('Email');
|
|
782
|
+
* await expect(emailCell).toHaveText('john@example.com');
|
|
783
|
+
*/
|
|
565
784
|
getCell(column: string): Locator;
|
|
566
|
-
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Extract all cell data as a key-value object.
|
|
788
|
+
* @param options - Optional configuration
|
|
789
|
+
* @param options.columns - Specific columns to extract (extracts all if not specified)
|
|
790
|
+
* @returns Promise resolving to row data
|
|
791
|
+
* @example
|
|
792
|
+
* const data = await row.toJSON();
|
|
793
|
+
* // { Name: 'John', Email: 'john@example.com', ... }
|
|
794
|
+
*
|
|
795
|
+
* const partial = await row.toJSON({ columns: ['Name', 'Email'] });
|
|
796
|
+
* // { Name: 'John', Email: 'john@example.com' }
|
|
797
|
+
*/
|
|
798
|
+
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
799
|
+
|
|
567
800
|
/**
|
|
568
|
-
*
|
|
801
|
+
* Scrolls/paginates to bring this row into view.
|
|
802
|
+
* Only works if rowIndex is known (e.g., from getRowByIndex).
|
|
803
|
+
* @throws Error if rowIndex is unknown
|
|
569
804
|
*/
|
|
570
|
-
|
|
805
|
+
bringIntoView(): Promise<void>;
|
|
806
|
+
|
|
571
807
|
/**
|
|
572
|
-
*
|
|
808
|
+
* Intelligently fills form fields in the row.
|
|
809
|
+
* Automatically detects input types (text, select, checkbox, contenteditable).
|
|
810
|
+
*
|
|
811
|
+
* @param data - Column-value pairs to fill
|
|
812
|
+
* @param options - Optional configuration
|
|
813
|
+
* @param options.inputMappers - Custom input selectors per column
|
|
814
|
+
* @example
|
|
815
|
+
* // Auto-detection
|
|
816
|
+
* await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });
|
|
817
|
+
*
|
|
818
|
+
* // Custom input mappers
|
|
819
|
+
* await row.smartFill(
|
|
820
|
+
* { Name: 'John' },
|
|
821
|
+
* { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }
|
|
822
|
+
* );
|
|
573
823
|
*/
|
|
574
|
-
smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
824
|
+
smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
575
825
|
};
|
|
576
826
|
```
|
|
577
827
|
<!-- /embed-type: SmartRow -->
|
|
@@ -579,10 +829,12 @@ export type SmartRow = Locator & {
|
|
|
579
829
|
**Methods:**
|
|
580
830
|
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
581
831
|
- `toJSON()`: Extracts all cell data as a key-value object
|
|
582
|
-
- `smartFill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types or use `inputMappers` for custom control.
|
|
832
|
+
- `smartFill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types (text, select, checkbox) or use `inputMappers` option for custom control.
|
|
583
833
|
|
|
584
834
|
All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
|
|
585
835
|
|
|
836
|
+
**Note**: `getRequestIndex()` is an internal method for row tracking - you typically won't need it.
|
|
837
|
+
|
|
586
838
|
#### <a name="tableconfig"></a>`TableConfig`
|
|
587
839
|
|
|
588
840
|
Configuration options for `useTable()`.
|
|
@@ -590,29 +842,36 @@ Configuration options for `useTable()`.
|
|
|
590
842
|
<!-- embed-type: TableConfig -->
|
|
591
843
|
```typescript
|
|
592
844
|
/**
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
845
|
+
* Strategy to filter rows based on criteria.
|
|
846
|
+
*/
|
|
847
|
+
export interface FilterStrategy {
|
|
848
|
+
apply(options: {
|
|
849
|
+
rows: Locator;
|
|
850
|
+
filter: { column: string, value: string | RegExp | number };
|
|
851
|
+
colIndex: number;
|
|
852
|
+
tableContext: TableContext;
|
|
853
|
+
}): Locator;
|
|
599
854
|
}
|
|
600
855
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Organized container for all table interaction strategies.
|
|
858
|
+
*/
|
|
859
|
+
export interface TableStrategies {
|
|
860
|
+
/** Strategy for discovering/scanning headers */
|
|
861
|
+
header?: HeaderStrategy;
|
|
862
|
+
/** Strategy for navigating to specific cells (row + column) */
|
|
863
|
+
cellNavigation?: CellNavigationStrategy;
|
|
864
|
+
/** Strategy for filling form inputs */
|
|
865
|
+
fill?: FillStrategy;
|
|
866
|
+
/** Strategy for paginating through data */
|
|
867
|
+
pagination?: PaginationStrategy;
|
|
868
|
+
/** Strategy for sorting columns */
|
|
869
|
+
sorting?: SortingStrategy;
|
|
870
|
+
/** Function to get a cell locator */
|
|
871
|
+
getCellLocator?: GetCellLocatorFn;
|
|
872
|
+
/** Function to get the currently active/focused cell */
|
|
873
|
+
getActiveCell?: GetActiveCellFn;
|
|
874
|
+
}
|
|
616
875
|
|
|
617
876
|
/**
|
|
618
877
|
* Configuration options for useTable.
|
|
@@ -624,22 +883,9 @@ export interface TableConfig {
|
|
|
624
883
|
rowSelector?: string;
|
|
625
884
|
/** Selector for the cells within a row */
|
|
626
885
|
cellSelector?: string;
|
|
627
|
-
/** Strategy for filling forms within the table */
|
|
628
|
-
fillStrategy?: FillStrategy;
|
|
629
|
-
/** Strategy for discovering headers */
|
|
630
|
-
headerStrategy?: HeaderStrategy;
|
|
631
|
-
/** Strategy for navigating to columns */
|
|
632
|
-
columnStrategy?: ColumnStrategy;
|
|
633
886
|
/** Number of pages to scan for verification */
|
|
634
887
|
maxPages?: number;
|
|
635
|
-
|
|
636
|
-
/** Pagination Strategy */
|
|
637
|
-
pagination?: PaginationStrategy;
|
|
638
|
-
/** Sorting Strategy */
|
|
639
|
-
sorting?: SortingStrategy;
|
|
640
|
-
/**
|
|
641
|
-
* Hook to rename columns dynamically.
|
|
642
|
-
*/
|
|
888
|
+
/** Hook to rename columns dynamically */
|
|
643
889
|
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
644
890
|
/** Automatically scroll to table on init */
|
|
645
891
|
autoScroll?: boolean;
|
|
@@ -647,12 +893,8 @@ export interface TableConfig {
|
|
|
647
893
|
debug?: boolean;
|
|
648
894
|
/** Reset hook */
|
|
649
895
|
onReset?: (context: TableContext) => Promise<void>;
|
|
650
|
-
/**
|
|
651
|
-
|
|
652
|
-
* Overrides cellSelector logic if provided.
|
|
653
|
-
* Useful for virtualized tables where nth() index doesn't match DOM index.
|
|
654
|
-
*/
|
|
655
|
-
cellResolver?: (args: { row: Locator, columnName: string, columnIndex: number, rowIndex?: number }) => Locator;
|
|
896
|
+
/** All interaction strategies */
|
|
897
|
+
strategies?: TableStrategies;
|
|
656
898
|
}
|
|
657
899
|
```
|
|
658
900
|
<!-- /embed-type: TableConfig -->
|
|
@@ -662,7 +904,7 @@ export interface TableConfig {
|
|
|
662
904
|
- `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
|
|
663
905
|
- `headerSelector`: CSS selector or function for header cells (default: `"th"`)
|
|
664
906
|
- `cellSelector`: CSS selector or function for data cells (default: `"td"`)
|
|
665
|
-
- `
|
|
907
|
+
- `strategies`: Configuration object for interaction strategies (pagination, sorting, etc.)
|
|
666
908
|
- `maxPages`: Maximum pages to scan when searching (default: `1`)
|
|
667
909
|
- `headerTransformer`: Function to transform/rename column headers dynamically
|
|
668
910
|
- `autoScroll`: Automatically scroll table into view (default: `true`)
|
|
@@ -675,7 +917,21 @@ Flexible selector type supporting strings, functions, or existing locators.
|
|
|
675
917
|
|
|
676
918
|
<!-- embed-type: Selector -->
|
|
677
919
|
```typescript
|
|
920
|
+
/**
|
|
921
|
+
* Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.
|
|
922
|
+
* @example
|
|
923
|
+
* // String selector
|
|
924
|
+
* rowSelector: 'tbody tr'
|
|
925
|
+
*
|
|
926
|
+
* // Function selector
|
|
927
|
+
* rowSelector: (root) => root.locator('[role="row"]')
|
|
928
|
+
*/
|
|
678
929
|
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Function to get a cell locator given row, column info.
|
|
933
|
+
* Replaces the old cellResolver.
|
|
934
|
+
*/
|
|
679
935
|
```
|
|
680
936
|
<!-- /embed-type: Selector -->
|
|
681
937
|
|
|
@@ -699,7 +955,6 @@ Function signature for custom pagination logic.
|
|
|
699
955
|
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
700
956
|
```
|
|
701
957
|
<!-- /embed-type: PaginationStrategy -->
|
|
702
|
-
|
|
703
958
|
Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
704
959
|
|
|
705
960
|
---
|
|
@@ -711,6 +966,8 @@ Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
|
711
966
|
3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
|
|
712
967
|
4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
|
|
713
968
|
5. **Pagination Strategies**: Create reusable strategies for tables with similar pagination patterns
|
|
969
|
+
6. **Async vs Sync**: Use `findRow()` for paginated searches and `getRow()` for strict, single-page assertions.
|
|
970
|
+
7. **Sorting**: Use `table.sorting.apply()` to sort columns and `table.sorting.getState()` to check sort state.
|
|
714
971
|
|
|
715
972
|
---
|
|
716
973
|
|