@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 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');
@@ -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 searchForRow for pagination
97
- await expect(await table.searchForRow({ Name: "Colleen Hurst" })).toBeVisible();
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.getByRow({ Name: 'Airi Satou' });
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.searchForRow({ Name: 'Angelica Ramos' });
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.getByRow({ Name: 'Airi Satou' });
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.getByRow({ ID: '1' });
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.getByRow({ "Last name": "Melisandre" });
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.searchForRow({ "Last name": "Melisandre" });
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.getByRow({ "Last Name": "Doe" });
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="getbyrow"></a>`getByRow(filters, options?)`
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
- - 🔄 Auto-paginates if row isn't on current page (when `maxPages > 1` and pagination strategy is configured)
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
- getByRow: <T extends { asJSON?: boolean }>(
444
+ getRow: <T extends { asJSON?: boolean }>(
310
445
  filters: Record<string, string | RegExp | number>,
311
- options?: { exact?: boolean, maxPages?: number } & T
312
- ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
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.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
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.getByRow({ Name: "Ghost User" })).not.toBeVisible();
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.getByRow({ Name: 'Airi Satou' });
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="getallcurrentrows"></a>`getAllCurrentRows(options?)`
482
+ #### <a name="findrow"></a>`findRow(filters, options?)`
348
483
 
349
- **Purpose:** Inclusive retrieval - gets all rows on the current page matching optional filters.
484
+ **Purpose:** Async retrieval - finds exactly one row matching the filters **across multiple pages** (pagination).
350
485
 
351
- **Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
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
- > **Note:** `getAllRows` is deprecated and will be removed in a future major version. Use `getAllCurrentRows` instead. The deprecated method still works for backwards compatibility.
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
- getAllCurrentRows: <T extends { asJSON?: boolean }>(
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.getAllCurrentRows();
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.getAllCurrentRows({
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.getAllCurrentRows({ asJSON: true });
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.getAllCurrentRows({
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((root) =>
503
- root.getByRole('button', { name: 'Load More' })
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
- getRequestIndex(): number | undefined;
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
- * Fills the row with data. Automatically detects input types.
588
- */
589
- fill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
590
- /**
591
- * Alias for fill() to avoid conflict with Locator.fill()
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. Use Locator's standard `fill()` for single-input scenarios.
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