@rickcedwhat/playwright-smart-table 6.17.1 → 6.19.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.
@@ -167,6 +167,16 @@ export interface ViewportStrategy {
167
167
  */
168
168
  getVisibleRowRange?: (context: TableContext) => Promise<{ first: number; last: number }>;
169
169
 
170
+ /**
171
+ * Returns the DOM positions (0-based, document order — indices into the resolved
172
+ * \`rowSelector\` set) of rows currently within the scroll container's visible bounds.
173
+ * Unlike \`getVisibleRowRange\` (a logical min/max), this identifies the exact rows, so
174
+ * \`map\`/\`forEach\`/\`filter\` can skip overscan rows during collection (see #353 / #357).
175
+ * Geometry-based and inclusive (any overlap counts as visible). When the container can't
176
+ * be measured, return all positions so no rows are filtered.
177
+ */
178
+ getVisibleRowIndices?: (context: TableContext) => Promise<number[]>;
179
+
170
180
  /**
171
181
  * Returns the 0-based index range of columns currently rendered in the DOM.
172
182
  * Used to detect when a target column is not yet mounted before reading.
@@ -254,8 +264,14 @@ export type SmartRow<T = any> = Locator & {
254
264
  *
255
265
  * const partial = await row.toJSON({ columns: ['Name', 'Email'] });
256
266
  * // { Name: 'John', Email: 'john@example.com' }
267
+ *
268
+ * // Atomic: snapshot all cell values in a single evaluate — zero inter-column stagger.
269
+ * // Requires cellSelector to be a CSS string (not a function).
270
+ * // Column overrides ARE supported (run against a frozen off-screen reconstruction).
271
+ * // Uses textContent (not layout-dependent innerText) for non-override columns.
272
+ * const coherent = await row.toJSON({ atomic: true });
257
273
  */
258
- toJSON(options?: { columns?: string[] }): Promise<T>;
274
+ toJSON(options?: { columns?: string[]; atomic?: boolean }): Promise<T>;
259
275
 
260
276
  /**
261
277
  * Scrolls/paginates to bring this row into view.
@@ -424,12 +440,43 @@ export type FillStrategy = (options: {
424
440
  fillOptions?: FillOptions;
425
441
  }) => Promise<void>;
426
442
 
443
+ /** Context passed as the second argument to {@link ColumnOverride.read}. */
444
+ export interface ColumnOverrideReadContext {
445
+ /**
446
+ * The parent row. Use for multi-cell or row-derived values — e.g. a synthetic column
447
+ * that reads an \`a[href]\` or a \`data-*\` attribute from the row rather than a cell:
448
+ * \`read: (_cell, { row }) => row.evaluate(el => el.querySelector('a')?.href)\`.
449
+ */
450
+ row: SmartRow;
451
+ /** The column being read. */
452
+ columnName: string;
453
+ /** The column's 0-based index in the resolved header map. */
454
+ columnIndex: number;
455
+ /**
456
+ * Get a Locator for another cell in the same row by column name.
457
+ * Returns raw cell Locators, not override-processed values.
458
+ *
459
+ * In atomic mode, the Locator points at the frozen reconstructed cell — coherent with
460
+ * every other cell from the same snapshot. In non-atomic mode, it points at the live cell.
461
+ *
462
+ * @example
463
+ * read: async (_cell, { getCell }) => {
464
+ * const name = (await getCell('Name').innerText()).trim();
465
+ * const href = await getCell('Name').locator('a').getAttribute('href') || '';
466
+ * return \`\${name} | \${href}\`;
467
+ * }
468
+ */
469
+ getCell: (columnName: string) => Locator;
470
+ }
471
+
427
472
  export interface ColumnOverride<TValue = any> {
428
- /**
473
+ /**
429
474
  * How to extract the value from the cell.
430
- * \`context\` provides access to the parent row, permitting multi-cell logic or bypassing the default cell locator.
475
+ * The second \`context\` argument provides the parent \`row\` (for multi-cell or row-derived
476
+ * values), plus \`columnName\` and \`columnIndex\`. Backwards-compatible: existing
477
+ * single-argument \`read(cell)\` implementations keep working.
431
478
  */
432
- read?: (cell: Locator) => Promise<TValue> | TValue;
479
+ read?: (cell: Locator, context: ColumnOverrideReadContext) => Promise<TValue> | TValue;
433
480
 
434
481
  /**
435
482
  * How to fill the cell with a new value. (Replaces smartFill default logic)
@@ -475,7 +522,10 @@ export interface LoadingStrategy {
475
522
 
476
523
  /**
477
524
  * How long (ms) to wait for a loading row to resolve before applying onRowLoadingTimeout.
478
- * When not set, loading rows are immediately skipped (backward-compatible behavior).
525
+ * Honored by findRows AND by map/forEach/filter, where the wait runs before the dedupe
526
+ * strategy so content-based dedupe keys see the row's final loaded state.
527
+ * When not set (backward-compatible behavior): findRows skips loading rows immediately;
528
+ * map/forEach/filter process them as-is.
479
529
  */
480
530
  rowLoadingTimeout?: number;
481
531
 
@@ -617,6 +667,13 @@ export interface TableConfig<T = any> {
617
667
  * Overrides both default extraction (toJSON) and filling (smartFill) logic.
618
668
  */
619
669
  columnOverrides?: Partial<Record<keyof T, ColumnOverride<T[keyof T]>>>;
670
+
671
+ /**
672
+ * Locator for an empty-state element that replaces the table when there are no results.
673
+ * If header resolution fails during init() and this locator is visible, init() succeeds
674
+ * and isEmpty() returns true. All row operations still throw normally.
675
+ */
676
+ emptyState?: Locator;
620
677
  }
621
678
 
622
679
  export interface FinalTableConfig<T = any> extends TableConfig<T> {
@@ -646,9 +703,17 @@ export interface FillOptions {
646
703
  /** Callback context passed to forEach, map, and filter. */
647
704
  export type RowIterationContext<T = any> = {
648
705
  row: SmartRow<T>;
649
- /** 0-based iteration counter — the order this row was visited, not its DOM position or grid identity. @deprecated Use \`index\` instead. \`rowIndex\` will be removed in v7.0.0. */
706
+ /**
707
+ * The row's logical/data-model index. When a \`resolveRowIndex\` strategy is configured
708
+ * (e.g. MUI DataGrid's \`data-rowindex\`) this is the grid's true row index; otherwise it
709
+ * equals \`index\`. Use this for \`row.bringIntoView()\` and position math on virtualized
710
+ * tables — it is stable across scrolling/dedupe, unlike the visit-order \`index\`.
711
+ */
650
712
  rowIndex: number;
651
- /** 0-based iteration counter — the order this row was visited, not its DOM position or grid identity. */
713
+ /**
714
+ * 0-based enumeration counter — the order this row was visited (contiguous within the run).
715
+ * Not a DOM position or grid identity. Use \`rowIndex\` for the row's data-model index.
716
+ */
652
717
  index: number;
653
718
  /** 0-based page index — which page this row was collected from. */
654
719
  pageIndex: number;
@@ -701,14 +766,22 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
701
766
  */
702
767
  isInitialized(): boolean;
703
768
 
769
+ /**
770
+ * SYNC: Returns true if init() resolved via the emptyState path — the table's
771
+ * empty-state locator was visible when header resolution failed.
772
+ * Row operations still throw normally; use this to branch before calling them.
773
+ */
774
+ isEmpty(): boolean;
775
+
704
776
  getHeaders: () => Promise<string[]>;
705
777
  getHeaderCell: (columnName: string) => Promise<Locator>;
706
778
 
707
779
  /**
708
780
  * Finds a row by filters on the current page only. Returns immediately (sync).
709
781
  * Throws error if table is not initialized.
710
- * @note The returned SmartRow may have \`rowIndex\` as 0 when the match is not the first row.
711
- * Use getRowByIndex(index) when you need a known index (e.g. for bringIntoView()).
782
+ * @note The sync path cannot compute a real \`rowIndex\`, so the returned SmartRow's
783
+ * \`rowIndex\` is \`undefined\` (virtual-scroll positioning via \`bringIntoView()\` is limited).
784
+ * Use \`findRow()\` (async) when you need a row with an accurate \`rowIndex\`.
712
785
  */
713
786
  getRow: (
714
787
  filters: Record<string, FilterValue>,
@@ -716,14 +789,45 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
716
789
  ) => SmartRow<T>;
717
790
 
718
791
  /**
719
- * Gets a row by 0-based index on the current page.
720
- * Throws error if table is not initialized.
721
- * @param index 0-based row index
792
+ * Gets a row by its 0-based position in the currently-rendered DOM (sync, no scroll).
793
+ * Throws if the table is not initialized.
794
+ *
795
+ * For button-paginated tables this is the i-th row on the current page. For virtualized
796
+ * tables the render window shifts as you scroll, so \`getRowByIndex(i)\` returns whatever
797
+ * row currently sits at DOM position \`i\` — NOT the logical/absolute row \`i\` in the dataset
798
+ * once the list has scrolled. For the row with a specific logical/data-model index on a
799
+ * virtualized table, use the async \`findRowByIndex(i)\` instead (it scrolls to the row);
800
+ * to iterate by logical identity, use \`map\` / \`findRows\` (with a \`dedupe\` strategy).
801
+ *
802
+ * Resolution is lazy: an out-of-range index yields a SmartRow whose operations fail when
803
+ * it is used, rather than throwing here.
804
+ *
805
+ * @param index 0-based position within the current render window
722
806
  */
723
807
  getRowByIndex: (
724
808
  index: number
725
809
  ) => SmartRow<T>;
726
810
 
811
+ /**
812
+ * ASYNC: Returns the row with a specific logical/data-model index, scrolling/paginating to
813
+ * reach it. Unlike the sync \`getRowByIndex\` (render-window position), this resolves the true
814
+ * data-model row \`index\` on virtualized tables.
815
+ *
816
+ * Reaches the row via, in order: a currently-mounted match, the viewport's random-access
817
+ * \`scrollToRow\` fast path, then advancing pages (on infinite-scroll tables a "page" is a
818
+ * scroll step) up to \`maxPages\`.
819
+ *
820
+ * Requires a \`strategies.resolveRowIndex\` to identify rows by logical index — throws if one
821
+ * is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
822
+ *
823
+ * @param index 0-based logical/data-model row index
824
+ * @param options - \`maxPages\` bounds how far to scroll/paginate (defaults to config.maxPages)
825
+ */
826
+ findRowByIndex: (
827
+ index: number,
828
+ options?: { maxPages?: number }
829
+ ) => Promise<SmartRow<T>>;
830
+
727
831
  /**
728
832
  * ASYNC: Searches for a single row across pages using pagination.
729
833
  * Auto-initializes the table if not already initialized.
@@ -739,7 +843,7 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
739
843
  * ASYNC: Searches for all matching rows across pages using pagination.
740
844
  * Auto-initializes the table if not already initialized.
741
845
  * @param filters - The filter criteria to match (omit or pass {} for all rows)
742
- * @param options - Search options including exact match and max pages
846
+ * @param options - Search options (exact, maxPages). \`useBulkPagination\` defaults to \`false\`: pages advance one at a time via \`goNext\` so no intermediate page is skipped. Set it to \`true\` to opt into \`goNextBulk\` (faster, but skips the rows on jumped-over pages).
743
847
  */
744
848
  findRows: (
745
849
  filters?: Record<string, FilterValue>,
package/dist/types.d.ts CHANGED
@@ -157,6 +157,15 @@ export interface ViewportStrategy {
157
157
  first: number;
158
158
  last: number;
159
159
  }>;
160
+ /**
161
+ * Returns the DOM positions (0-based, document order — indices into the resolved
162
+ * `rowSelector` set) of rows currently within the scroll container's visible bounds.
163
+ * Unlike `getVisibleRowRange` (a logical min/max), this identifies the exact rows, so
164
+ * `map`/`forEach`/`filter` can skip overscan rows during collection (see #353 / #357).
165
+ * Geometry-based and inclusive (any overlap counts as visible). When the container can't
166
+ * be measured, return all positions so no rows are filtered.
167
+ */
168
+ getVisibleRowIndices?: (context: TableContext) => Promise<number[]>;
160
169
  /**
161
170
  * Returns the 0-based index range of columns currently rendered in the DOM.
162
171
  * Used to detect when a target column is not yet mounted before reading.
@@ -236,9 +245,16 @@ export type SmartRow<T = any> = Locator & {
236
245
  *
237
246
  * const partial = await row.toJSON({ columns: ['Name', 'Email'] });
238
247
  * // { Name: 'John', Email: 'john@example.com' }
248
+ *
249
+ * // Atomic: snapshot all cell values in a single evaluate — zero inter-column stagger.
250
+ * // Requires cellSelector to be a CSS string (not a function).
251
+ * // Column overrides ARE supported (run against a frozen off-screen reconstruction).
252
+ * // Uses textContent (not layout-dependent innerText) for non-override columns.
253
+ * const coherent = await row.toJSON({ atomic: true });
239
254
  */
240
255
  toJSON(options?: {
241
256
  columns?: string[];
257
+ atomic?: boolean;
242
258
  }): Promise<T>;
243
259
  /**
244
260
  * Scrolls/paginates to bring this row into view.
@@ -383,12 +399,42 @@ export type FillStrategy = (options: {
383
399
  table: TableResult;
384
400
  fillOptions?: FillOptions;
385
401
  }) => Promise<void>;
402
+ /** Context passed as the second argument to {@link ColumnOverride.read}. */
403
+ export interface ColumnOverrideReadContext {
404
+ /**
405
+ * The parent row. Use for multi-cell or row-derived values — e.g. a synthetic column
406
+ * that reads an `a[href]` or a `data-*` attribute from the row rather than a cell:
407
+ * `read: (_cell, { row }) => row.evaluate(el => el.querySelector('a')?.href)`.
408
+ */
409
+ row: SmartRow;
410
+ /** The column being read. */
411
+ columnName: string;
412
+ /** The column's 0-based index in the resolved header map. */
413
+ columnIndex: number;
414
+ /**
415
+ * Get a Locator for another cell in the same row by column name.
416
+ * Returns raw cell Locators, not override-processed values.
417
+ *
418
+ * In atomic mode, the Locator points at the frozen reconstructed cell — coherent with
419
+ * every other cell from the same snapshot. In non-atomic mode, it points at the live cell.
420
+ *
421
+ * @example
422
+ * read: async (_cell, { getCell }) => {
423
+ * const name = (await getCell('Name').innerText()).trim();
424
+ * const href = await getCell('Name').locator('a').getAttribute('href') || '';
425
+ * return `${name} | ${href}`;
426
+ * }
427
+ */
428
+ getCell: (columnName: string) => Locator;
429
+ }
386
430
  export interface ColumnOverride<TValue = any> {
387
431
  /**
388
432
  * How to extract the value from the cell.
389
- * `context` provides access to the parent row, permitting multi-cell logic or bypassing the default cell locator.
433
+ * The second `context` argument provides the parent `row` (for multi-cell or row-derived
434
+ * values), plus `columnName` and `columnIndex`. Backwards-compatible: existing
435
+ * single-argument `read(cell)` implementations keep working.
390
436
  */
391
- read?: (cell: Locator) => Promise<TValue> | TValue;
437
+ read?: (cell: Locator, context: ColumnOverrideReadContext) => Promise<TValue> | TValue;
392
438
  /**
393
439
  * How to fill the cell with a new value. (Replaces smartFill default logic)
394
440
  * Provides the current value (via `read`) if a `write` wants to check state first.
@@ -432,7 +478,10 @@ export interface LoadingStrategy {
432
478
  isHeaderLoading?: (context: TableContext) => Promise<boolean>;
433
479
  /**
434
480
  * How long (ms) to wait for a loading row to resolve before applying onRowLoadingTimeout.
435
- * When not set, loading rows are immediately skipped (backward-compatible behavior).
481
+ * Honored by findRows AND by map/forEach/filter, where the wait runs before the dedupe
482
+ * strategy so content-based dedupe keys see the row's final loaded state.
483
+ * When not set (backward-compatible behavior): findRows skips loading rows immediately;
484
+ * map/forEach/filter process them as-is.
436
485
  */
437
486
  rowLoadingTimeout?: number;
438
487
  /**
@@ -564,6 +613,12 @@ export interface TableConfig<T = any> {
564
613
  * Overrides both default extraction (toJSON) and filling (smartFill) logic.
565
614
  */
566
615
  columnOverrides?: Partial<Record<keyof T, ColumnOverride<T[keyof T]>>>;
616
+ /**
617
+ * Locator for an empty-state element that replaces the table when there are no results.
618
+ * If header resolution fails during init() and this locator is visible, init() succeeds
619
+ * and isEmpty() returns true. All row operations still throw normally.
620
+ */
621
+ emptyState?: Locator;
567
622
  }
568
623
  export interface FinalTableConfig<T = any> extends TableConfig<T> {
569
624
  headerSelector: string | ((root: Locator) => Locator);
@@ -592,9 +647,17 @@ export interface FillOptions {
592
647
  /** Callback context passed to forEach, map, and filter. */
593
648
  export type RowIterationContext<T = any> = {
594
649
  row: SmartRow<T>;
595
- /** 0-based iteration counter — the order this row was visited, not its DOM position or grid identity. @deprecated Use `index` instead. `rowIndex` will be removed in v7.0.0. */
650
+ /**
651
+ * The row's logical/data-model index. When a `resolveRowIndex` strategy is configured
652
+ * (e.g. MUI DataGrid's `data-rowindex`) this is the grid's true row index; otherwise it
653
+ * equals `index`. Use this for `row.bringIntoView()` and position math on virtualized
654
+ * tables — it is stable across scrolling/dedupe, unlike the visit-order `index`.
655
+ */
596
656
  rowIndex: number;
597
- /** 0-based iteration counter — the order this row was visited, not its DOM position or grid identity. */
657
+ /**
658
+ * 0-based enumeration counter — the order this row was visited (contiguous within the run).
659
+ * Not a DOM position or grid identity. Use `rowIndex` for the row's data-model index.
660
+ */
598
661
  index: number;
599
662
  /** 0-based page index — which page this row was collected from. */
600
663
  pageIndex: number;
@@ -648,23 +711,59 @@ export interface TableResult<T = any> extends AsyncIterable<{
648
711
  * @returns true if init() has been called and completed, false otherwise
649
712
  */
650
713
  isInitialized(): boolean;
714
+ /**
715
+ * SYNC: Returns true if init() resolved via the emptyState path — the table's
716
+ * empty-state locator was visible when header resolution failed.
717
+ * Row operations still throw normally; use this to branch before calling them.
718
+ */
719
+ isEmpty(): boolean;
651
720
  getHeaders: () => Promise<string[]>;
652
721
  getHeaderCell: (columnName: string) => Promise<Locator>;
653
722
  /**
654
723
  * Finds a row by filters on the current page only. Returns immediately (sync).
655
724
  * Throws error if table is not initialized.
656
- * @note The returned SmartRow may have `rowIndex` as 0 when the match is not the first row.
657
- * Use getRowByIndex(index) when you need a known index (e.g. for bringIntoView()).
725
+ * @note The sync path cannot compute a real `rowIndex`, so the returned SmartRow's
726
+ * `rowIndex` is `undefined` (virtual-scroll positioning via `bringIntoView()` is limited).
727
+ * Use `findRow()` (async) when you need a row with an accurate `rowIndex`.
658
728
  */
659
729
  getRow: (filters: Record<string, FilterValue>, options?: {
660
730
  exact?: boolean;
661
731
  }) => SmartRow<T>;
662
732
  /**
663
- * Gets a row by 0-based index on the current page.
664
- * Throws error if table is not initialized.
665
- * @param index 0-based row index
733
+ * Gets a row by its 0-based position in the currently-rendered DOM (sync, no scroll).
734
+ * Throws if the table is not initialized.
735
+ *
736
+ * For button-paginated tables this is the i-th row on the current page. For virtualized
737
+ * tables the render window shifts as you scroll, so `getRowByIndex(i)` returns whatever
738
+ * row currently sits at DOM position `i` — NOT the logical/absolute row `i` in the dataset
739
+ * once the list has scrolled. For the row with a specific logical/data-model index on a
740
+ * virtualized table, use the async `findRowByIndex(i)` instead (it scrolls to the row);
741
+ * to iterate by logical identity, use `map` / `findRows` (with a `dedupe` strategy).
742
+ *
743
+ * Resolution is lazy: an out-of-range index yields a SmartRow whose operations fail when
744
+ * it is used, rather than throwing here.
745
+ *
746
+ * @param index 0-based position within the current render window
666
747
  */
667
748
  getRowByIndex: (index: number) => SmartRow<T>;
749
+ /**
750
+ * ASYNC: Returns the row with a specific logical/data-model index, scrolling/paginating to
751
+ * reach it. Unlike the sync `getRowByIndex` (render-window position), this resolves the true
752
+ * data-model row `index` on virtualized tables.
753
+ *
754
+ * Reaches the row via, in order: a currently-mounted match, the viewport's random-access
755
+ * `scrollToRow` fast path, then advancing pages (on infinite-scroll tables a "page" is a
756
+ * scroll step) up to `maxPages`.
757
+ *
758
+ * Requires a `strategies.resolveRowIndex` to identify rows by logical index — throws if one
759
+ * is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
760
+ *
761
+ * @param index 0-based logical/data-model row index
762
+ * @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages)
763
+ */
764
+ findRowByIndex: (index: number, options?: {
765
+ maxPages?: number;
766
+ }) => Promise<SmartRow<T>>;
668
767
  /**
669
768
  * ASYNC: Searches for a single row across pages using pagination.
670
769
  * Auto-initializes the table if not already initialized.
@@ -679,7 +778,7 @@ export interface TableResult<T = any> extends AsyncIterable<{
679
778
  * ASYNC: Searches for all matching rows across pages using pagination.
680
779
  * Auto-initializes the table if not already initialized.
681
780
  * @param filters - The filter criteria to match (omit or pass {} for all rows)
682
- * @param options - Search options including exact match and max pages
781
+ * @param options - Search options (exact, maxPages). `useBulkPagination` defaults to `false`: pages advance one at a time via `goNext` so no intermediate page is skipped. Set it to `true` to opt into `goNextBulk` (faster, but skips the rows on jumped-over pages).
683
782
  */
684
783
  findRows: (filters?: Record<string, FilterValue>, options?: {
685
784
  exact?: boolean;