@rickcedwhat/playwright-smart-table 6.18.0 → 6.20.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/dist/types.d.ts CHANGED
@@ -10,6 +10,15 @@ import type { SmartRowArray } from './utils/smartRowArray';
10
10
  * headerSelector: (root) => root.locator('[role="columnheader"]')
11
11
  */
12
12
  export type Selector = string | ((root: Locator | Page) => Locator) | ((root: Locator) => Locator);
13
+ /**
14
+ * Return type for `resolveRowIndex`. A plain number gives the logical index only;
15
+ * `{ index, selector }` additionally provides a CSS selector the library uses to
16
+ * build a self-healing row locator that survives virtual-scroll DOM recycling.
17
+ */
18
+ export type RowIndexResult = number | {
19
+ index: number;
20
+ selector: string;
21
+ };
13
22
  /**
14
23
  * Value used to filter rows.
15
24
  * - string/number/RegExp: filter by text content of the cell.
@@ -157,6 +166,15 @@ export interface ViewportStrategy {
157
166
  first: number;
158
167
  last: number;
159
168
  }>;
169
+ /**
170
+ * Returns the DOM positions (0-based, document order — indices into the resolved
171
+ * `rowSelector` set) of rows currently within the scroll container's visible bounds.
172
+ * Unlike `getVisibleRowRange` (a logical min/max), this identifies the exact rows, so
173
+ * `map`/`forEach`/`filter` can skip overscan rows during collection (see #353 / #357).
174
+ * Geometry-based and inclusive (any overlap counts as visible). When the container can't
175
+ * be measured, return all positions so no rows are filtered.
176
+ */
177
+ getVisibleRowIndices?: (context: TableContext) => Promise<number[]>;
160
178
  /**
161
179
  * Returns the 0-based index range of columns currently rendered in the DOM.
162
180
  * Used to detect when a target column is not yet mounted before reading.
@@ -236,9 +254,16 @@ export type SmartRow<T = any> = Locator & {
236
254
  *
237
255
  * const partial = await row.toJSON({ columns: ['Name', 'Email'] });
238
256
  * // { Name: 'John', Email: 'john@example.com' }
257
+ *
258
+ * // Atomic: snapshot all cell values in a single evaluate — zero inter-column stagger.
259
+ * // Requires cellSelector to be a CSS string (not a function).
260
+ * // Column overrides ARE supported (run against a frozen off-screen reconstruction).
261
+ * // Uses textContent (not layout-dependent innerText) for non-override columns.
262
+ * const coherent = await row.toJSON({ atomic: true });
239
263
  */
240
264
  toJSON(options?: {
241
265
  columns?: string[];
266
+ atomic?: boolean;
242
267
  }): Promise<T>;
243
268
  /**
244
269
  * Scrolls/paginates to bring this row into view.
@@ -265,6 +290,12 @@ export type SmartRow<T = any> = Locator & {
265
290
  * );
266
291
  */
267
292
  smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
293
+ /**
294
+ * Get the resolved value of any column — real, override, or synthetic.
295
+ * @param column - Column name (case-sensitive)
296
+ * @returns The column value as a string
297
+ */
298
+ getValue(column: string): Promise<string>;
268
299
  /**
269
300
  * Returns whether the row exists in the DOM (i.e. is not a sentinel row).
270
301
  */
@@ -383,12 +414,45 @@ export type FillStrategy = (options: {
383
414
  table: TableResult;
384
415
  fillOptions?: FillOptions;
385
416
  }) => Promise<void>;
417
+ /** Context passed as the second argument to {@link ColumnOverride.read}. */
418
+ export interface ColumnOverrideReadContext {
419
+ /**
420
+ * The parent row. Use for multi-cell or row-derived values — e.g. a synthetic column
421
+ * that reads an `a[href]` or a `data-*` attribute from the row rather than a cell:
422
+ * `read: (_cell, { row }) => row.evaluate(el => el.querySelector('a')?.href)`.
423
+ */
424
+ row: SmartRow;
425
+ /** The column being read. */
426
+ columnName: string;
427
+ /** The column's 0-based index in the resolved header map. */
428
+ columnIndex: number;
429
+ /**
430
+ * Get a Locator for another cell in the same row by column name.
431
+ * Returns raw cell Locators, not override-processed values.
432
+ *
433
+ * In atomic mode, the Locator points at the frozen reconstructed cell — coherent with
434
+ * every other cell from the same snapshot. In non-atomic mode, it points at the live cell.
435
+ *
436
+ * @example
437
+ * read: async (_cell, { getCell }) => {
438
+ * const name = (await getCell('Name').innerText()).trim();
439
+ * const href = await getCell('Name').locator('a').getAttribute('href') || '';
440
+ * return `${name} | ${href}`;
441
+ * }
442
+ */
443
+ getCell: (columnName: string) => Locator;
444
+ }
445
+ export interface SyntheticColumnDef<T = any> {
446
+ compute: (row: SmartRow<T>) => Promise<string | number> | string | number;
447
+ }
386
448
  export interface ColumnOverride<TValue = any> {
387
449
  /**
388
450
  * 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.
451
+ * The second `context` argument provides the parent `row` (for multi-cell or row-derived
452
+ * values), plus `columnName` and `columnIndex`. Backwards-compatible: existing
453
+ * single-argument `read(cell)` implementations keep working.
390
454
  */
391
- read?: (cell: Locator) => Promise<TValue> | TValue;
455
+ read?: (cell: Locator, context: ColumnOverrideReadContext) => Promise<TValue> | TValue;
392
456
  /**
393
457
  * How to fill the cell with a new value. (Replaces smartFill default logic)
394
458
  * Provides the current value (via `read`) if a `write` wants to check state first.
@@ -466,6 +530,12 @@ export interface LoadingStrategy {
466
530
  * Defaults to 'read-as-is' when cellLoadingTimeout is set.
467
531
  */
468
532
  onCellLoadingTimeout?: 'skip' | 'read-as-is' | 'throw' | ((cell: import('@playwright/test').Locator, columnName: string, row: SmartRow) => Promise<string>);
533
+ /** Max ms to wait for sort stabilization when isTableLoading is set. @default 10000 */
534
+ sortStabilizationTimeout?: number;
535
+ /** Polling interval (ms) while waiting for sort stabilization. @default 100 */
536
+ sortStabilizationPollInterval?: number;
537
+ /** Fallback delay (ms) after sort when no isTableLoading is configured. @default 200 */
538
+ sortStabilizationFallbackDelay?: number;
469
539
  }
470
540
  /**
471
541
  * Organized container for all table interaction strategies.
@@ -513,14 +583,20 @@ export interface TableStrategies {
513
583
  *
514
584
  * Return `undefined` to fall back to DOM position.
515
585
  *
586
+ * When the index maps to a DOM attribute, return `{ index, selector }` instead of
587
+ * a plain number. The library uses the CSS selector to build a **self-healing row
588
+ * locator** that re-queries the DOM on every action — surviving virtual-scroll
589
+ * recycling for `getCell`, `smartFill`, and `toJSON` without manual re-pinning.
590
+ *
516
591
  * @example
517
- * // MUI DataGrid: read the global monotone counter from the attribute
592
+ * // MUI DataGrid: self-healing via data-rowindex attribute
518
593
  * resolveRowIndex: async (row) => {
519
594
  * const v = await row.getAttribute('data-rowindex').catch(() => null);
520
- * return v !== null && !isNaN(Number(v)) ? Number(v) : undefined;
595
+ * if (v === null || isNaN(Number(v))) return undefined;
596
+ * return { index: Number(v), selector: `[data-rowindex="${v}"]` };
521
597
  * }
522
598
  */
523
- resolveRowIndex?: (row: Locator) => Promise<number | undefined>;
599
+ resolveRowIndex?: (row: Locator) => Promise<RowIndexResult | undefined>;
524
600
  /**
525
601
  * Viewport oracle strategies for 2D virtualized tables (e.g. MUI DataGrid, AG Grid,
526
602
  * Braintrust-style grids where both rows and columns are virtualized simultaneously).
@@ -558,7 +634,7 @@ export interface TableConfig<T = any> {
558
634
  autoScroll?: boolean;
559
635
  /** Debug options for development and troubleshooting */
560
636
  debug?: DebugConfig;
561
- /** Reset hook */
637
+ /** Hook called after reset completes (after goToFirst, cache clear, and autoInit). */
562
638
  onReset?: (context: TableContext) => Promise<void>;
563
639
  /** All interaction strategies */
564
640
  strategies?: TableStrategies;
@@ -567,6 +643,19 @@ export interface TableConfig<T = any> {
567
643
  * Overrides both default extraction (toJSON) and filling (smartFill) logic.
568
644
  */
569
645
  columnOverrides?: Partial<Record<keyof T, ColumnOverride<T[keyof T]>>>;
646
+ /**
647
+ * Computed columns with no DOM presence. Each key becomes a virtual column name
648
+ * available in `toJSON()`, `getValue()`, and `findRow()`/`findRows()` filters.
649
+ * The `compute` function receives the full SmartRow and must only read real or
650
+ * override columns (no chaining between synthetics).
651
+ */
652
+ syntheticColumns?: Record<string, SyntheticColumnDef<T>>;
653
+ /**
654
+ * Locator for an empty-state element that replaces the table when there are no results.
655
+ * If header resolution fails during init() and this locator is visible, init() succeeds
656
+ * and isEmpty() returns true. All row operations still throw normally.
657
+ */
658
+ emptyState?: Locator;
570
659
  }
571
660
  export interface FinalTableConfig<T = any> extends TableConfig<T> {
572
661
  headerSelector: string | ((root: Locator) => Locator);
@@ -595,9 +684,17 @@ export interface FillOptions {
595
684
  /** Callback context passed to forEach, map, and filter. */
596
685
  export type RowIterationContext<T = any> = {
597
686
  row: SmartRow<T>;
598
- /** 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. */
687
+ /**
688
+ * The row's logical/data-model index. When a `resolveRowIndex` strategy is configured
689
+ * (e.g. MUI DataGrid's `data-rowindex`) this is the grid's true row index; otherwise it
690
+ * equals `index`. Use this for `row.bringIntoView()` and position math on virtualized
691
+ * tables — it is stable across scrolling/dedupe, unlike the visit-order `index`.
692
+ */
599
693
  rowIndex: number;
600
- /** 0-based iteration counter — the order this row was visited, not its DOM position or grid identity. */
694
+ /**
695
+ * 0-based enumeration counter — the order this row was visited (contiguous within the run).
696
+ * Not a DOM position or grid identity. Use `rowIndex` for the row's data-model index.
697
+ */
601
698
  index: number;
602
699
  /** 0-based page index — which page this row was collected from. */
603
700
  pageIndex: number;
@@ -651,6 +748,12 @@ export interface TableResult<T = any> extends AsyncIterable<{
651
748
  * @returns true if init() has been called and completed, false otherwise
652
749
  */
653
750
  isInitialized(): boolean;
751
+ /**
752
+ * SYNC: Returns true if init() resolved via the emptyState path — the table's
753
+ * empty-state locator was visible when header resolution failed.
754
+ * Row operations still throw normally; use this to branch before calling them.
755
+ */
756
+ isEmpty(): boolean;
654
757
  getHeaders: () => Promise<string[]>;
655
758
  getHeaderCell: (columnName: string) => Promise<Locator>;
656
759
  /**
@@ -670,8 +773,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
670
773
  * For button-paginated tables this is the i-th row on the current page. For virtualized
671
774
  * tables the render window shifts as you scroll, so `getRowByIndex(i)` returns whatever
672
775
  * row currently sits at DOM position `i` — NOT the logical/absolute row `i` in the dataset
673
- * once the list has scrolled. To iterate virtualized rows by logical identity, use `map` /
674
- * `findRows` (with a `dedupe` strategy) instead.
776
+ * once the list has scrolled. For the row with a specific logical/data-model index on a
777
+ * virtualized table, use the async `findRowByIndex(i)` instead (it scrolls to the row);
778
+ * to iterate by logical identity, use `map` / `findRows` (with a `dedupe` strategy).
675
779
  *
676
780
  * Resolution is lazy: an out-of-range index yields a SmartRow whose operations fail when
677
781
  * it is used, rather than throwing here.
@@ -679,6 +783,24 @@ export interface TableResult<T = any> extends AsyncIterable<{
679
783
  * @param index 0-based position within the current render window
680
784
  */
681
785
  getRowByIndex: (index: number) => SmartRow<T>;
786
+ /**
787
+ * ASYNC: Returns the row with a specific logical/data-model index, scrolling/paginating to
788
+ * reach it. Unlike the sync `getRowByIndex` (render-window position), this resolves the true
789
+ * data-model row `index` on virtualized tables.
790
+ *
791
+ * Reaches the row via, in order: a currently-mounted match, the viewport's random-access
792
+ * `scrollToRow` fast path, then advancing pages (on infinite-scroll tables a "page" is a
793
+ * scroll step) up to `maxPages`.
794
+ *
795
+ * Requires a `strategies.resolveRowIndex` to identify rows by logical index — throws if one
796
+ * is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
797
+ *
798
+ * @param index 0-based logical/data-model row index
799
+ * @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages)
800
+ */
801
+ findRowByIndex: (index: number, options?: {
802
+ maxPages?: number;
803
+ }) => Promise<SmartRow<T>>;
682
804
  /**
683
805
  * ASYNC: Searches for a single row across pages using pagination.
684
806
  * Auto-initializes the table if not already initialized.
@@ -705,10 +827,14 @@ export interface TableResult<T = any> extends AsyncIterable<{
705
827
  */
706
828
  scrollToColumn: (columnName: string) => Promise<void>;
707
829
  /**
708
- * Counts the number of rows currently on the page.
709
- * Does not paginate.
830
+ * Counts the number of rows, optionally filtered.
831
+ * Without arguments, counts all rows. With filters, counts only matching rows.
832
+ * Paginates when pagination is configured.
710
833
  */
711
- countRows: () => Promise<number>;
834
+ countRows: (filters?: Record<string, FilterValue>, options?: {
835
+ exact?: boolean;
836
+ maxPages?: number;
837
+ }) => Promise<number>;
712
838
  /**
713
839
  * Iterates over rows and extracts the value of a single column.
714
840
  * More efficient than map + toJSON for single-column extraction.
@@ -723,7 +849,7 @@ export interface TableResult<T = any> extends AsyncIterable<{
723
849
  */
724
850
  getColumnValues(columnName: string, options?: RowIterationOptions): Promise<string[]>;
725
851
  /**
726
- * Resets the table state (clears cache, flags) and invokes the onReset strategy.
852
+ * Resets the table: calls goToFirst (if configured), clears cache, re-inits headers, then calls onReset.
727
853
  */
728
854
  reset: () => Promise<void>;
729
855
  /**