@rickcedwhat/playwright-smart-table 4.0.0 → 5.1.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 +319 -102
- package/dist/smartRow.js +7 -7
- package/dist/strategies/columns.d.ts +0 -34
- package/dist/strategies/columns.js +1 -34
- package/dist/strategies/headers.d.ts +0 -16
- package/dist/strategies/headers.js +1 -113
- package/dist/strategies/index.d.ts +0 -28
- package/dist/strategies/index.js +0 -3
- package/dist/strategies/pagination.d.ts +0 -21
- package/dist/strategies/pagination.js +1 -23
- 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 +102 -26
- package/dist/types.d.ts +97 -26
- package/dist/useTable.d.ts +2 -9
- package/dist/useTable.js +149 -49
- package/dist/utils/stringUtils.d.ts +22 -0
- package/dist/utils/stringUtils.js +73 -0
- package/dist/utils/traceUtils.d.ts +11 -0
- package/dist/utils/traceUtils.js +47 -0
- 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');
|
|
@@ -93,8 +93,8 @@ await table.init();
|
|
|
93
93
|
// ✅ Verify Colleen is NOT visible initially
|
|
94
94
|
await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
|
|
95
95
|
|
|
96
|
-
// Use
|
|
97
|
-
await expect(await table.
|
|
96
|
+
// Use findRow for pagination
|
|
97
|
+
await expect(await table.findRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
98
98
|
// NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
|
|
99
99
|
```
|
|
100
100
|
<!-- /embed: pagination -->
|
|
@@ -112,7 +112,7 @@ const table = useTable(page.locator('#example'), {
|
|
|
112
112
|
});
|
|
113
113
|
await table.init();
|
|
114
114
|
|
|
115
|
-
const row = table.
|
|
115
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
116
116
|
await expect(row).toBeVisible();
|
|
117
117
|
```
|
|
118
118
|
<!-- /embed: advanced-debug -->
|
|
@@ -121,14 +121,14 @@ This will log header mappings, row scans, and pagination triggers to help troubl
|
|
|
121
121
|
|
|
122
122
|
### Resetting Table State
|
|
123
123
|
|
|
124
|
-
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):
|
|
125
125
|
|
|
126
126
|
<!-- embed: advanced-reset -->
|
|
127
127
|
```typescript
|
|
128
128
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
129
129
|
// Navigate deep into the table by searching for a row on a later page
|
|
130
130
|
try {
|
|
131
|
-
await table.
|
|
131
|
+
await table.findRow({ Name: 'Angelica Ramos' });
|
|
132
132
|
} catch (e) { }
|
|
133
133
|
|
|
134
134
|
// Reset internal state (and potentially UI) to initial page
|
|
@@ -136,11 +136,28 @@ await table.reset();
|
|
|
136
136
|
await table.init(); // Re-init after reset
|
|
137
137
|
|
|
138
138
|
// Now subsequent searches start from the beginning
|
|
139
|
-
const currentPageRow = table.
|
|
139
|
+
const currentPageRow = table.getRow({ Name: 'Airi Satou' });
|
|
140
140
|
await expect(currentPageRow).toBeVisible();
|
|
141
141
|
```
|
|
142
142
|
<!-- /embed: advanced-reset -->
|
|
143
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
|
+
|
|
144
161
|
### Column Scanning
|
|
145
162
|
|
|
146
163
|
Efficiently extract all values from a specific column:
|
|
@@ -162,7 +179,7 @@ Use `smartFill()` to intelligently populate form fields in a table row. The meth
|
|
|
162
179
|
<!-- embed: fill-basic -->
|
|
163
180
|
```typescript
|
|
164
181
|
// Find a row and fill it with new data
|
|
165
|
-
const row = table.
|
|
182
|
+
const row = table.getRow({ ID: '1' });
|
|
166
183
|
|
|
167
184
|
await row.smartFill({
|
|
168
185
|
Name: 'John Updated',
|
|
@@ -254,11 +271,11 @@ expect(headers).toContain('Actions');
|
|
|
254
271
|
|
|
255
272
|
// Use the renamed column
|
|
256
273
|
// First check it's not on the current page
|
|
257
|
-
const currentPageRow = table.
|
|
274
|
+
const currentPageRow = table.getRow({ "Last name": "Melisandre" });
|
|
258
275
|
await expect(currentPageRow).not.toBeVisible();
|
|
259
276
|
|
|
260
277
|
// Then find it across pages
|
|
261
|
-
const row = await table.
|
|
278
|
+
const row = await table.findRow({ "Last name": "Melisandre" });
|
|
262
279
|
const actionsCell = row.getCell('Actions');
|
|
263
280
|
await actionsCell.getByLabel("Select row").click();
|
|
264
281
|
```
|
|
@@ -282,34 +299,152 @@ const table = useTable(page.locator('#table1'), {
|
|
|
282
299
|
await table.init();
|
|
283
300
|
|
|
284
301
|
// Now column names are consistent
|
|
285
|
-
const row = table.
|
|
302
|
+
const row = table.getRow({ "Last Name": "Doe" });
|
|
286
303
|
const emailCell = row.getCell("Email");
|
|
287
304
|
await expect(emailCell).toHaveText("jdoe@hotmail.com");
|
|
288
305
|
```
|
|
289
306
|
<!-- /embed: header-transformer-normalize -->
|
|
290
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
|
+
|
|
291
409
|
---
|
|
292
410
|
|
|
293
411
|
## 📖 API Reference
|
|
294
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
|
+
|
|
295
429
|
### Table Methods
|
|
296
430
|
|
|
297
|
-
#### <a name="
|
|
431
|
+
#### <a name="getrow"></a>`getRow(filters, options?)`
|
|
298
432
|
|
|
299
|
-
**Purpose:** Strict retrieval - finds exactly one row matching the filters
|
|
433
|
+
**Purpose:** Strict retrieval - finds exactly one row matching the filters on the **current page**.
|
|
300
434
|
|
|
301
435
|
**Behavior:**
|
|
302
436
|
- ✅ Returns `SmartRow` if exactly one match
|
|
303
437
|
- ❌ Throws error if multiple matches (ambiguous query)
|
|
304
438
|
- 👻 Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
305
|
-
-
|
|
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.
|
|
306
441
|
|
|
307
442
|
**Type Signature:**
|
|
308
443
|
```typescript
|
|
309
|
-
|
|
444
|
+
getRow: <T extends { asJSON?: boolean }>(
|
|
310
445
|
filters: Record<string, string | RegExp | number>,
|
|
311
|
-
options?: { exact?: boolean
|
|
312
|
-
) =>
|
|
446
|
+
options?: { exact?: boolean } & T
|
|
447
|
+
) => SmartRow;
|
|
313
448
|
```
|
|
314
449
|
|
|
315
450
|
<!-- embed: get-by-row -->
|
|
@@ -319,11 +454,11 @@ const table = useTable(page.locator('#example'), { headerSelector: 'thead th' })
|
|
|
319
454
|
await table.init();
|
|
320
455
|
|
|
321
456
|
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
322
|
-
const row = table.
|
|
457
|
+
const row = table.getRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
323
458
|
await expect(row).toBeVisible();
|
|
324
459
|
|
|
325
460
|
// Assert it does NOT exist
|
|
326
|
-
await expect(table.
|
|
461
|
+
await expect(table.getRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
327
462
|
```
|
|
328
463
|
<!-- /embed: get-by-row -->
|
|
329
464
|
|
|
@@ -331,7 +466,7 @@ Get row data as JSON:
|
|
|
331
466
|
<!-- embed: get-by-row-json -->
|
|
332
467
|
```typescript
|
|
333
468
|
// Get row data as JSON object
|
|
334
|
-
const row = table.
|
|
469
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
335
470
|
const data = await row.toJSON();
|
|
336
471
|
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
337
472
|
|
|
@@ -344,17 +479,33 @@ expect(partial).toEqual({ Name: 'Airi Satou' });
|
|
|
344
479
|
```
|
|
345
480
|
<!-- /embed: get-by-row-json -->
|
|
346
481
|
|
|
347
|
-
#### <a name="
|
|
482
|
+
#### <a name="findrow"></a>`findRow(filters, options?)`
|
|
348
483
|
|
|
349
|
-
**Purpose:**
|
|
484
|
+
**Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
|
|
350
485
|
|
|
351
|
-
**
|
|
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?)`
|
|
352
501
|
|
|
353
|
-
|
|
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.
|
|
354
505
|
|
|
355
506
|
**Type Signature:**
|
|
356
507
|
```typescript
|
|
357
|
-
|
|
508
|
+
getRows: <T extends { asJSON?: boolean }>(
|
|
358
509
|
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
359
510
|
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
360
511
|
```
|
|
@@ -363,17 +514,17 @@ getAllCurrentRows: <T extends { asJSON?: boolean }>(
|
|
|
363
514
|
```typescript
|
|
364
515
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
365
516
|
// 1. Get ALL rows on the current page
|
|
366
|
-
const allRows = await table.
|
|
517
|
+
const allRows = await table.getRows();
|
|
367
518
|
expect(allRows.length).toBeGreaterThan(0);
|
|
368
519
|
|
|
369
520
|
// 2. Get subset of rows (Filtering)
|
|
370
|
-
const tokyoUsers = await table.
|
|
521
|
+
const tokyoUsers = await table.getRows({
|
|
371
522
|
filter: { Office: 'Tokyo' }
|
|
372
523
|
});
|
|
373
524
|
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
374
525
|
|
|
375
526
|
// 3. Dump data to JSON
|
|
376
|
-
const data = await table.
|
|
527
|
+
const data = await table.getRows({ asJSON: true });
|
|
377
528
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
378
529
|
expect(data.length).toBeGreaterThan(0);
|
|
379
530
|
expect(data[0]).toHaveProperty('Name');
|
|
@@ -384,7 +535,7 @@ Filter rows with exact match:
|
|
|
384
535
|
<!-- embed: get-all-rows-exact -->
|
|
385
536
|
```typescript
|
|
386
537
|
// Get rows with exact match (default is fuzzy/contains match)
|
|
387
|
-
const exactMatches = await table.
|
|
538
|
+
const exactMatches = await table.getRows({
|
|
388
539
|
filter: { Office: 'Tokyo' },
|
|
389
540
|
exact: true // Requires exact string match
|
|
390
541
|
});
|
|
@@ -393,6 +544,22 @@ expect(exactMatches.length).toBeGreaterThan(0);
|
|
|
393
544
|
```
|
|
394
545
|
<!-- /embed: get-all-rows-exact -->
|
|
395
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
|
+
|
|
396
563
|
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
397
564
|
|
|
398
565
|
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
@@ -463,6 +630,44 @@ Resets table state (clears cache, pagination flags) and invokes the `onReset` st
|
|
|
463
630
|
reset: () => Promise<void>;
|
|
464
631
|
```
|
|
465
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
|
+
```
|
|
670
|
+
|
|
466
671
|
---
|
|
467
672
|
|
|
468
673
|
## 🧩 Pagination Strategies
|
|
@@ -495,12 +700,12 @@ strategies: {
|
|
|
495
700
|
|
|
496
701
|
#### <a name="tablestrategiesclickloadmore"></a>`Strategies.Pagination.clickLoadMore(selector)`
|
|
497
702
|
|
|
498
|
-
Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
703
|
+
Best for "Load More" buttons that may not be part of the table. Clicks and waits for row count to increase.
|
|
499
704
|
|
|
500
705
|
```typescript
|
|
501
706
|
strategies: {
|
|
502
|
-
pagination: Strategies.Pagination.clickLoadMore((
|
|
503
|
-
|
|
707
|
+
pagination: Strategies.Pagination.clickLoadMore((page) =>
|
|
708
|
+
page.getByRole('button', { name: 'Load More' })
|
|
504
709
|
)
|
|
505
710
|
}
|
|
506
711
|
```
|
|
@@ -573,22 +778,81 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
573
778
|
|
|
574
779
|
<!-- embed-type: SmartRow -->
|
|
575
780
|
```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
|
+
*/
|
|
576
805
|
export type SmartRow<T = any> = Locator & {
|
|
577
|
-
|
|
806
|
+
/** Optional row index (0-based) if known */
|
|
578
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
|
+
*/
|
|
579
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
|
+
*/
|
|
580
831
|
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
832
|
+
|
|
581
833
|
/**
|
|
582
834
|
* Scrolls/paginates to bring this row into view.
|
|
583
|
-
* Only works if rowIndex is known.
|
|
835
|
+
* Only works if rowIndex is known (e.g., from getRowByIndex).
|
|
836
|
+
* @throws Error if rowIndex is unknown
|
|
584
837
|
*/
|
|
585
838
|
bringIntoView(): Promise<void>;
|
|
839
|
+
|
|
586
840
|
/**
|
|
587
|
-
*
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
*
|
|
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
|
+
* );
|
|
592
856
|
*/
|
|
593
857
|
smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
594
858
|
};
|
|
@@ -598,10 +862,12 @@ export type SmartRow<T = any> = Locator & {
|
|
|
598
862
|
**Methods:**
|
|
599
863
|
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
600
864
|
- `toJSON()`: Extracts all cell data as a key-value object
|
|
601
|
-
- `smartFill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types or use `inputMappers` for custom control.
|
|
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.
|
|
602
866
|
|
|
603
867
|
All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
|
|
604
868
|
|
|
869
|
+
**Note**: `getRequestIndex()` is an internal method for row tracking - you typically won't need it.
|
|
870
|
+
|
|
605
871
|
#### <a name="tableconfig"></a>`TableConfig`
|
|
606
872
|
|
|
607
873
|
Configuration options for `useTable()`.
|
|
@@ -684,6 +950,15 @@ Flexible selector type supporting strings, functions, or existing locators.
|
|
|
684
950
|
|
|
685
951
|
<!-- embed-type: Selector -->
|
|
686
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
|
+
*/
|
|
687
962
|
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
688
963
|
|
|
689
964
|
/**
|
|
@@ -717,66 +992,6 @@ Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
|
717
992
|
|
|
718
993
|
---
|
|
719
994
|
|
|
720
|
-
## 🔄 Migration Guide
|
|
721
|
-
|
|
722
|
-
### Upgrading from v3.x to v4.0
|
|
723
|
-
|
|
724
|
-
**Breaking Change**: Strategy imports are now consolidated under the `Strategies` object.
|
|
725
|
-
|
|
726
|
-
#### Import Changes
|
|
727
|
-
```typescript
|
|
728
|
-
// ❌ Old (v3.x)
|
|
729
|
-
import { PaginationStrategies, SortingStrategies } from '../src/useTable';
|
|
730
|
-
|
|
731
|
-
// ✅ New (v4.0)
|
|
732
|
-
import { Strategies } from '../src/strategies';
|
|
733
|
-
// or
|
|
734
|
-
import { useTable, Strategies } from '../src/useTable';
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
#### Strategy Usage
|
|
738
|
-
```typescript
|
|
739
|
-
// ❌ Old (v3.x)
|
|
740
|
-
strategies: {
|
|
741
|
-
pagination: PaginationStrategies.clickNext(() => page.locator('#next')),
|
|
742
|
-
sorting: SortingStrategies.AriaSort(),
|
|
743
|
-
header: HeaderStrategies.scrollRight,
|
|
744
|
-
cellNavigation: ColumnStrategies.keyboard
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ✅ New (v4.0)
|
|
748
|
-
strategies: {
|
|
749
|
-
pagination: Strategies.Pagination.clickNext(() => page.locator('#next')),
|
|
750
|
-
sorting: Strategies.Sorting.AriaSort(),
|
|
751
|
-
header: Strategies.Header.scrollRight,
|
|
752
|
-
cellNavigation: Strategies.Column.keyboard
|
|
753
|
-
}
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
#### New Features (Optional)
|
|
757
|
-
|
|
758
|
-
**Generic Type Support:**
|
|
759
|
-
```typescript
|
|
760
|
-
interface User {
|
|
761
|
-
Name: string;
|
|
762
|
-
Email: string;
|
|
763
|
-
Office: string;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const table = useTable<User>(page.locator('#table'), config);
|
|
767
|
-
const data = await row.toJSON(); // Type: User (not Record<string, string>)
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
**Revalidate Method:**
|
|
771
|
-
```typescript
|
|
772
|
-
// Refresh column mappings when table structure changes
|
|
773
|
-
await table.revalidate();
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
For detailed migration instructions optimized for AI code transformation, see the [AI Migration Guide](./MIGRATION_v4.md).
|
|
777
|
-
|
|
778
|
-
---
|
|
779
|
-
|
|
780
995
|
## 🚀 Tips & Best Practices
|
|
781
996
|
|
|
782
997
|
1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
|
|
@@ -784,6 +999,8 @@ For detailed migration instructions optimized for AI code transformation, see th
|
|
|
784
999
|
3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
|
|
785
1000
|
4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
|
|
786
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.
|
|
787
1004
|
|
|
788
1005
|
---
|
|
789
1006
|
|