@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 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.getByRow({ Name: 'Airi Satou' });
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
- - `getByRow()` finds a specific row by column values
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.getByRow({ Name: 'Airi Satou' });
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
- pagination: PaginationStrategies.clickNext(() =>
85
- page.getByRole('link', { name: 'Next' })
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 searchForRow for pagination
95
- await expect(await table.searchForRow({ Name: "Colleen Hurst" })).toBeVisible();
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.getByRow({ Name: 'Airi Satou' });
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.searchForRow({ Name: 'Angelica Ramos' });
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.getByRow({ Name: 'Airi Satou' });
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.getByRow({ ID: '1' });
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
- pagination: PaginationStrategies.clickNext(
233
- (root) => root.getByRole("button", { name: "Go to next page" })
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.getByRow({ "Last name": "Melisandre" });
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.searchForRow({ "Last name": "Melisandre" });
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.getByRow({ "Last Name": "Doe" });
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="getbyrow"></a>`getByRow(filters, options?)`
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
- - 🔄 Auto-paginates if row isn't on current page (when `maxPages > 1` and pagination strategy is configured)
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
- getByRow: <T extends { asJSON?: boolean }>(
411
+ getRow: <T extends { asJSON?: boolean }>(
306
412
  filters: Record<string, string | RegExp | number>,
307
- options?: { exact?: boolean, maxPages?: number } & T
308
- ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
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.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
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.getByRow({ Name: "Ghost User" })).not.toBeVisible();
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.getByRow({ Name: 'Airi Satou' });
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="getallcurrentrows"></a>`getAllCurrentRows(options?)`
449
+ #### <a name="findrow"></a>`findRow(filters, options?)`
340
450
 
341
- **Purpose:** Inclusive retrieval - gets all rows on the current page matching optional filters.
451
+ **Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
342
452
 
343
- **Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
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
- > **Note:** `getAllRows` is deprecated and will be removed in a future major version. Use `getAllCurrentRows` instead. The deprecated method still works for backwards compatibility.
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
- getAllCurrentRows: <T extends { asJSON?: boolean }>(
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.getAllCurrentRows();
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.getAllCurrentRows({
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.getAllCurrentRows({ asJSON: true });
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.getAllCurrentRows({
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>`TableStrategies.clickNext(selector)`
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
- pagination: TableStrategies.clickNext((root) =>
472
- root.page().getByRole('button', { name: 'Next' })
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>`TableStrategies.infiniteScroll()`
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
- pagination: TableStrategies.infiniteScroll()
663
+ strategies: {
664
+ pagination: Strategies.Pagination.infiniteScroll()
665
+ }
482
666
  ```
483
667
 
484
- #### <a name="tablestrategiesclickloadmore"></a>`TableStrategies.clickLoadMore(selector)`
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
- pagination: TableStrategies.clickLoadMore((root) =>
490
- root.getByRole('button', { name: 'Load More' })
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
- export type SmartRow = Locator & {
563
- getRequestIndex(): number | undefined; // Helper to get the row index if known
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
- toJSON(): Promise<Record<string, string>>;
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
- * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
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
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
805
+ bringIntoView(): Promise<void>;
806
+
571
807
  /**
572
- * Alias for fill() to avoid conflict with Locator.fill()
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. Use Locator's standard `fill()` for single-input scenarios.
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
- * Output Strategy:
594
- * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).
595
- * - 'console': Standard console logs (Default).
596
- */
597
- output?: 'console' | 'error';
598
- includeTypes?: boolean;
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
- export type FillStrategy = (options: {
602
- row: SmartRow;
603
- columnName: string;
604
- value: any;
605
- index: number;
606
- page: Page;
607
- rootLocator: Locator;
608
- table: TableResult; // The parent table instance
609
- fillOptions?: FillOptions;
610
- }) => Promise<void>;
611
-
612
- export type { HeaderStrategy } from './strategies/headers';
613
- export type { ColumnStrategy } from './strategies/columns';
614
- import { HeaderStrategy } from './strategies/headers';
615
- import { ColumnStrategy } from './strategies/columns';
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
- * Custom resolver for finding a cell.
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
- - `pagination`: Strategy function for navigating pages (default: no pagination)
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