@rickcedwhat/playwright-smart-table 4.0.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 +286 -102
- package/dist/smartRow.js +2 -5
- 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 +94 -24
- package/dist/types.d.ts +89 -24
- package/dist/useTable.d.ts +2 -9
- package/dist/useTable.js +54 -32
- 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,119 @@ 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
|
+
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
|
+
|
|
291
376
|
---
|
|
292
377
|
|
|
293
378
|
## 📖 API Reference
|
|
294
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
|
+
|
|
295
396
|
### Table Methods
|
|
296
397
|
|
|
297
|
-
#### <a name="
|
|
398
|
+
#### <a name="getrow"></a>`getRow(filters, options?)`
|
|
298
399
|
|
|
299
|
-
**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**.
|
|
300
401
|
|
|
301
402
|
**Behavior:**
|
|
302
403
|
- ✅ Returns `SmartRow` if exactly one match
|
|
303
404
|
- ❌ Throws error if multiple matches (ambiguous query)
|
|
304
405
|
- 👻 Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
305
|
-
-
|
|
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.
|
|
306
408
|
|
|
307
409
|
**Type Signature:**
|
|
308
410
|
```typescript
|
|
309
|
-
|
|
411
|
+
getRow: <T extends { asJSON?: boolean }>(
|
|
310
412
|
filters: Record<string, string | RegExp | number>,
|
|
311
|
-
options?: { exact?: boolean
|
|
312
|
-
) =>
|
|
413
|
+
options?: { exact?: boolean } & T
|
|
414
|
+
) => SmartRow;
|
|
313
415
|
```
|
|
314
416
|
|
|
315
417
|
<!-- embed: get-by-row -->
|
|
@@ -319,11 +421,11 @@ const table = useTable(page.locator('#example'), { headerSelector: 'thead th' })
|
|
|
319
421
|
await table.init();
|
|
320
422
|
|
|
321
423
|
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
322
|
-
const row = table.
|
|
424
|
+
const row = table.getRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
323
425
|
await expect(row).toBeVisible();
|
|
324
426
|
|
|
325
427
|
// Assert it does NOT exist
|
|
326
|
-
await expect(table.
|
|
428
|
+
await expect(table.getRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
327
429
|
```
|
|
328
430
|
<!-- /embed: get-by-row -->
|
|
329
431
|
|
|
@@ -331,7 +433,7 @@ Get row data as JSON:
|
|
|
331
433
|
<!-- embed: get-by-row-json -->
|
|
332
434
|
```typescript
|
|
333
435
|
// Get row data as JSON object
|
|
334
|
-
const row = table.
|
|
436
|
+
const row = table.getRow({ Name: 'Airi Satou' });
|
|
335
437
|
const data = await row.toJSON();
|
|
336
438
|
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
337
439
|
|
|
@@ -344,17 +446,33 @@ expect(partial).toEqual({ Name: 'Airi Satou' });
|
|
|
344
446
|
```
|
|
345
447
|
<!-- /embed: get-by-row-json -->
|
|
346
448
|
|
|
347
|
-
#### <a name="
|
|
449
|
+
#### <a name="findrow"></a>`findRow(filters, options?)`
|
|
348
450
|
|
|
349
|
-
**Purpose:**
|
|
451
|
+
**Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
|
|
350
452
|
|
|
351
|
-
**
|
|
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
|
|
352
458
|
|
|
353
|
-
|
|
459
|
+
**Type Signature:**
|
|
460
|
+
```typescript
|
|
461
|
+
findRow: (
|
|
462
|
+
filters: Record<string, string | RegExp | number>,
|
|
463
|
+
options?: { exact?: boolean, maxPages?: number }
|
|
464
|
+
) => Promise<SmartRow>;
|
|
465
|
+
```
|
|
466
|
+
|
|
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.
|
|
354
472
|
|
|
355
473
|
**Type Signature:**
|
|
356
474
|
```typescript
|
|
357
|
-
|
|
475
|
+
getRows: <T extends { asJSON?: boolean }>(
|
|
358
476
|
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
359
477
|
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
360
478
|
```
|
|
@@ -363,17 +481,17 @@ getAllCurrentRows: <T extends { asJSON?: boolean }>(
|
|
|
363
481
|
```typescript
|
|
364
482
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
365
483
|
// 1. Get ALL rows on the current page
|
|
366
|
-
const allRows = await table.
|
|
484
|
+
const allRows = await table.getRows();
|
|
367
485
|
expect(allRows.length).toBeGreaterThan(0);
|
|
368
486
|
|
|
369
487
|
// 2. Get subset of rows (Filtering)
|
|
370
|
-
const tokyoUsers = await table.
|
|
488
|
+
const tokyoUsers = await table.getRows({
|
|
371
489
|
filter: { Office: 'Tokyo' }
|
|
372
490
|
});
|
|
373
491
|
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
374
492
|
|
|
375
493
|
// 3. Dump data to JSON
|
|
376
|
-
const data = await table.
|
|
494
|
+
const data = await table.getRows({ asJSON: true });
|
|
377
495
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
378
496
|
expect(data.length).toBeGreaterThan(0);
|
|
379
497
|
expect(data[0]).toHaveProperty('Name');
|
|
@@ -384,7 +502,7 @@ Filter rows with exact match:
|
|
|
384
502
|
<!-- embed: get-all-rows-exact -->
|
|
385
503
|
```typescript
|
|
386
504
|
// Get rows with exact match (default is fuzzy/contains match)
|
|
387
|
-
const exactMatches = await table.
|
|
505
|
+
const exactMatches = await table.getRows({
|
|
388
506
|
filter: { Office: 'Tokyo' },
|
|
389
507
|
exact: true // Requires exact string match
|
|
390
508
|
});
|
|
@@ -393,6 +511,22 @@ expect(exactMatches.length).toBeGreaterThan(0);
|
|
|
393
511
|
```
|
|
394
512
|
<!-- /embed: get-all-rows-exact -->
|
|
395
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
|
+
|
|
396
530
|
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
397
531
|
|
|
398
532
|
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
@@ -463,6 +597,44 @@ Resets table state (clears cache, pagination flags) and invokes the `onReset` st
|
|
|
463
597
|
reset: () => Promise<void>;
|
|
464
598
|
```
|
|
465
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
|
+
|
|
466
638
|
---
|
|
467
639
|
|
|
468
640
|
## 🧩 Pagination Strategies
|
|
@@ -495,12 +667,12 @@ strategies: {
|
|
|
495
667
|
|
|
496
668
|
#### <a name="tablestrategiesclickloadmore"></a>`Strategies.Pagination.clickLoadMore(selector)`
|
|
497
669
|
|
|
498
|
-
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.
|
|
499
671
|
|
|
500
672
|
```typescript
|
|
501
673
|
strategies: {
|
|
502
|
-
pagination: Strategies.Pagination.clickLoadMore((
|
|
503
|
-
|
|
674
|
+
pagination: Strategies.Pagination.clickLoadMore((page) =>
|
|
675
|
+
page.getByRole('button', { name: 'Load More' })
|
|
504
676
|
)
|
|
505
677
|
}
|
|
506
678
|
```
|
|
@@ -573,22 +745,81 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
573
745
|
|
|
574
746
|
<!-- embed-type: SmartRow -->
|
|
575
747
|
```typescript
|
|
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
|
+
*/
|
|
576
772
|
export type SmartRow<T = any> = Locator & {
|
|
577
|
-
|
|
773
|
+
/** Optional row index (0-based) if known */
|
|
578
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
|
+
*/
|
|
579
784
|
getCell(column: string): Locator;
|
|
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
|
+
*/
|
|
580
798
|
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
799
|
+
|
|
581
800
|
/**
|
|
582
801
|
* Scrolls/paginates to bring this row into view.
|
|
583
|
-
* Only works if rowIndex is known.
|
|
802
|
+
* Only works if rowIndex is known (e.g., from getRowByIndex).
|
|
803
|
+
* @throws Error if rowIndex is unknown
|
|
584
804
|
*/
|
|
585
805
|
bringIntoView(): Promise<void>;
|
|
806
|
+
|
|
586
807
|
/**
|
|
587
|
-
*
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
*
|
|
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
|
+
* );
|
|
592
823
|
*/
|
|
593
824
|
smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
594
825
|
};
|
|
@@ -598,10 +829,12 @@ export type SmartRow<T = any> = Locator & {
|
|
|
598
829
|
**Methods:**
|
|
599
830
|
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
600
831
|
- `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.
|
|
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.
|
|
602
833
|
|
|
603
834
|
All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
|
|
604
835
|
|
|
836
|
+
**Note**: `getRequestIndex()` is an internal method for row tracking - you typically won't need it.
|
|
837
|
+
|
|
605
838
|
#### <a name="tableconfig"></a>`TableConfig`
|
|
606
839
|
|
|
607
840
|
Configuration options for `useTable()`.
|
|
@@ -684,6 +917,15 @@ Flexible selector type supporting strings, functions, or existing locators.
|
|
|
684
917
|
|
|
685
918
|
<!-- embed-type: Selector -->
|
|
686
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
|
+
*/
|
|
687
929
|
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
688
930
|
|
|
689
931
|
/**
|
|
@@ -717,66 +959,6 @@ Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
|
717
959
|
|
|
718
960
|
---
|
|
719
961
|
|
|
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
962
|
## 🚀 Tips & Best Practices
|
|
781
963
|
|
|
782
964
|
1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
|
|
@@ -784,6 +966,8 @@ For detailed migration instructions optimized for AI code transformation, see th
|
|
|
784
966
|
3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
|
|
785
967
|
4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
|
|
786
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.
|
|
787
971
|
|
|
788
972
|
---
|
|
789
973
|
|
package/dist/smartRow.js
CHANGED
|
@@ -19,7 +19,6 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
19
19
|
const smart = rowLocator;
|
|
20
20
|
// Attach State
|
|
21
21
|
smart.rowIndex = rowIndex;
|
|
22
|
-
smart.getRequestIndex = () => rowIndex;
|
|
23
22
|
// Attach Methods
|
|
24
23
|
smart.getCell = (colName) => {
|
|
25
24
|
const idx = map.get(colName);
|
|
@@ -112,8 +111,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
112
111
|
}
|
|
113
112
|
return result;
|
|
114
113
|
});
|
|
115
|
-
|
|
116
|
-
smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
smart.smartFill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
115
|
for (const [colName, value] of Object.entries(data)) {
|
|
118
116
|
const colIdx = map.get(colName);
|
|
119
117
|
if (colIdx === undefined) {
|
|
@@ -144,12 +142,11 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
144
142
|
});
|
|
145
143
|
smart.bringIntoView = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
144
|
if (rowIndex === undefined) {
|
|
147
|
-
throw new Error('Cannot bring row into view - row index is unknown. Use
|
|
145
|
+
throw new Error('Cannot bring row into view - row index is unknown. Use getRowByIndex() instead of getRow().');
|
|
148
146
|
}
|
|
149
147
|
// Scroll row into view using Playwright's built-in method
|
|
150
148
|
yield rowLocator.scrollIntoViewIfNeeded();
|
|
151
149
|
});
|
|
152
|
-
smart.smartFill = smart.fill;
|
|
153
150
|
return smart;
|
|
154
151
|
};
|
|
155
152
|
exports.createSmartRow = createSmartRow;
|
|
@@ -9,44 +9,10 @@ export type CellNavigationStrategy = (context: StrategyContext & {
|
|
|
9
9
|
index: number;
|
|
10
10
|
rowIndex?: number;
|
|
11
11
|
}) => Promise<void>;
|
|
12
|
-
/** @deprecated Use CellNavigationStrategy instead */
|
|
13
|
-
export type ColumnStrategy = CellNavigationStrategy;
|
|
14
12
|
export declare const CellNavigationStrategies: {
|
|
15
13
|
/**
|
|
16
14
|
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
17
15
|
* No specific action taken other than what Playwright's default locator handling does.
|
|
18
16
|
*/
|
|
19
17
|
default: () => Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
22
|
-
* to navigate to the target CELL (navigates down to the row, then right to the column).
|
|
23
|
-
*
|
|
24
|
-
* Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
|
|
25
|
-
* or where keyboard navigation is the primary way to move focus.
|
|
26
|
-
*/
|
|
27
|
-
keyboard: (context: StrategyContext & {
|
|
28
|
-
column: string;
|
|
29
|
-
index: number;
|
|
30
|
-
rowIndex?: number;
|
|
31
|
-
}) => Promise<void>;
|
|
32
|
-
};
|
|
33
|
-
/** @deprecated Use CellNavigationStrategies instead */
|
|
34
|
-
export declare const ColumnStrategies: {
|
|
35
|
-
/**
|
|
36
|
-
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
37
|
-
* No specific action taken other than what Playwright's default locator handling does.
|
|
38
|
-
*/
|
|
39
|
-
default: () => Promise<void>;
|
|
40
|
-
/**
|
|
41
|
-
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
42
|
-
* to navigate to the target CELL (navigates down to the row, then right to the column).
|
|
43
|
-
*
|
|
44
|
-
* Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
|
|
45
|
-
* or where keyboard navigation is the primary way to move focus.
|
|
46
|
-
*/
|
|
47
|
-
keyboard: (context: StrategyContext & {
|
|
48
|
-
column: string;
|
|
49
|
-
index: number;
|
|
50
|
-
rowIndex?: number;
|
|
51
|
-
}) => Promise<void>;
|
|
52
18
|
};
|