@rickcedwhat/playwright-smart-table 6.9.0 → 6.10.1

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.
Files changed (42) hide show
  1. package/README.md +9 -6
  2. package/dist/engine/rowFinder.js +153 -168
  3. package/dist/engine/tableIteration.js +105 -120
  4. package/dist/engine/tableMapper.js +88 -103
  5. package/dist/minimalConfigContext.d.ts +1 -1
  6. package/dist/minimalConfigContext.js +1 -1
  7. package/dist/packageVersion.d.ts +1 -1
  8. package/dist/packageVersion.js +1 -1
  9. package/dist/presets/glide/columns.js +23 -32
  10. package/dist/presets/glide/headers.js +18 -27
  11. package/dist/presets/glide/index.d.ts +9 -9
  12. package/dist/presets/glide/index.js +24 -33
  13. package/dist/presets/mui.js +197 -124
  14. package/dist/presets/rdg.d.ts +1 -1
  15. package/dist/presets/rdg.js +32 -41
  16. package/dist/smartRow.js +209 -101
  17. package/dist/strategies/columns.js +2 -11
  18. package/dist/strategies/dedupe.js +3 -12
  19. package/dist/strategies/fill.js +20 -29
  20. package/dist/strategies/headers.js +22 -31
  21. package/dist/strategies/index.d.ts +7 -0
  22. package/dist/strategies/index.js +3 -0
  23. package/dist/strategies/loading.js +24 -33
  24. package/dist/strategies/pagination.d.ts +10 -0
  25. package/dist/strategies/pagination.js +49 -30
  26. package/dist/strategies/sorting.js +18 -31
  27. package/dist/strategies/stabilization.js +28 -37
  28. package/dist/strategies/viewport.d.ts +52 -0
  29. package/dist/strategies/viewport.js +123 -0
  30. package/dist/typeContext.d.ts +1 -1
  31. package/dist/typeContext.js +160 -5
  32. package/dist/types.d.ts +153 -5
  33. package/dist/useTable.js +142 -76
  34. package/dist/utils/debugUtils.js +5 -16
  35. package/dist/utils/elementTracker.js +27 -40
  36. package/dist/utils/mutex.js +5 -16
  37. package/dist/utils/navigationBarrier.js +25 -36
  38. package/dist/utils/paginationPath.d.ts +4 -1
  39. package/dist/utils/paginationPath.js +164 -141
  40. package/dist/utils/smartRowArray.js +2 -11
  41. package/dist/utils.js +4 -13
  42. package/package.json +13 -8
@@ -14,7 +14,7 @@ exports.TYPE_CONTEXT = `
14
14
  * rowSelector: 'tbody tr'
15
15
  *
16
16
  * // Function selector
17
- * rowSelector: (root) => root.locator('[role="row"]')
17
+ * headerSelector: (root) => root.locator('[role="columnheader"]')
18
18
  */
19
19
  export type Selector = string | ((root: Locator | Page) => Locator) | ((root: Locator) => Locator);
20
20
 
@@ -37,10 +37,13 @@ export type FilterValue = string | RegExp | number | ((cell: Locator) => Locator
37
37
  */
38
38
  export type GetCellLocatorFn = (args: {
39
39
  row: Locator;
40
+ /** The root locator passed to useTable(). Useful for re-querying stale row locators. */
41
+ root: Locator;
40
42
  columnName: string;
41
43
  columnIndex: number;
42
44
  rowIndex?: number;
43
45
  page: Page;
46
+ config: FinalTableConfig<any>;
44
47
  }) => Locator;
45
48
 
46
49
  /**
@@ -80,6 +83,103 @@ export type GetActiveCellFn = (args: TableContext) => Promise<{
80
83
  locator: Locator;
81
84
  } | null>;
82
85
 
86
+ /**
87
+ * Viewport oracle strategies for virtualized tables.
88
+ *
89
+ * These tell the library *what is currently in the DOM* rather than *how to move*.
90
+ * When present, the cell-reading engine uses them to jump directly to a target
91
+ * row/column instead of stepping blindly with navigation primitives.
92
+ *
93
+ * This is the primary fix for the 2D scroll hazard: scrolling right to bring a
94
+ * column into view can knock rows out of the vertical viewport (and vice versa).
95
+ * With \`getVisibleRowRange\` the engine detects this and calls \`scrollToRow\` to
96
+ * restore the row before reading — eliminating the hazard without polling loops.
97
+ *
98
+ * All members are optional. Supply whichever the underlying grid exposes:
99
+ * - Range oracles alone enable hazard detection with graceful fallback to navigation primitives.
100
+ * - Scroll primitives alone enable direct jumps without range awareness.
101
+ * - Both together give the most reliable, fastest path.
102
+ *
103
+ * @example
104
+ * // MUI DataGrid via apiRef
105
+ * strategies: {
106
+ * viewport: {
107
+ * getVisibleRowRange: async ({ root }) =>
108
+ * root.evaluate(el => {
109
+ * const api = (el as any).__muiDataGrid__;
110
+ * return { first: api.getFirstVisibleRow(), last: api.getLastVisibleRow() };
111
+ * }),
112
+ * scrollToRow: async ({ root }, rowIndex) =>
113
+ * root.evaluate((el, idx) =>
114
+ * (el as any).__muiDataGrid__.scrollToIndexes({ rowIndex: idx }), rowIndex),
115
+ * scrollToColumn: async ({ root }, colIndex) =>
116
+ * root.evaluate((el, idx) =>
117
+ * (el as any).__muiDataGrid__.scrollToIndexes({ colIndex: idx }), colIndex),
118
+ * }
119
+ * }
120
+ *
121
+ * @example
122
+ * // aria-rowindex / aria-colindex DOM fallback (works without internal API access)
123
+ * strategies: {
124
+ * viewport: {
125
+ * getVisibleRowRange: async ({ root }) =>
126
+ * root.evaluate(el => {
127
+ * const rows = [...el.querySelectorAll('[role="row"][aria-rowindex]')];
128
+ * const idxs = rows.map(r => Number(r.getAttribute('aria-rowindex')) - 1);
129
+ * return { first: Math.min(...idxs), last: Math.max(...idxs) };
130
+ * }),
131
+ * }
132
+ * }
133
+ */
134
+ export interface ViewportStrategy {
135
+ /**
136
+ * Returns the 0-based index range of rows currently rendered in the DOM.
137
+ * Used to detect when a target row has been scrolled out of view and needs recovery.
138
+ */
139
+ getVisibleRowRange?: (context: TableContext) => Promise<{ first: number; last: number }>;
140
+
141
+ /**
142
+ * Returns the 0-based index range of columns currently rendered in the DOM.
143
+ * Used to detect when a target column is not yet mounted before reading.
144
+ */
145
+ getVisibleColumnRange?: (context: TableContext) => Promise<{ first: number; last: number }>;
146
+
147
+ /**
148
+ * Scrolls or jumps directly to make a row visible by 0-based index.
149
+ * Replaces blind goDown()/goUp() step loops for grids that expose a scroll API.
150
+ */
151
+ scrollToRow?: (context: TableContext, rowIndex: number) => Promise<void>;
152
+
153
+ /**
154
+ * Scrolls or jumps directly to make a column visible by 0-based index.
155
+ * Replaces blind goRight()/goLeft() step loops for grids that expose a scroll API.
156
+ */
157
+ scrollToColumn?: (context: TableContext, colIndex: number) => Promise<void>;
158
+
159
+ /**
160
+ * When true, disables the automatic memoization of getVisibleColumnRange and
161
+ * getVisibleRowRange results. By default the library caches each range value and
162
+ * invalidates it after the corresponding scroll call, eliminating redundant DOM
163
+ * evaluate() round-trips between scrolls.
164
+ */
165
+ disableCache?: boolean;
166
+ }
167
+
168
+
169
+ /**
170
+ * SmartCell - A Playwright Locator with table-aware methods for single-cell operations.
171
+ *
172
+ * Extends all standard Locator methods (click, isVisible, etc.).
173
+ */
174
+ export type SmartCell = Locator & {
175
+ /**
176
+ * Scrolls/paginates to bring this specific cell into view using the configured strategies.
177
+ * Useful when the grid is horizontally virtualized and the column must be scrolled
178
+ * into view before it can be interacted with or read.
179
+ */
180
+ bringIntoView(): Promise<void>;
181
+ };
182
+
83
183
 
84
184
  /**
85
185
  * SmartRow - A Playwright Locator with table-aware methods.
@@ -106,12 +206,13 @@ export type SmartRow<T = any> = Locator & {
106
206
  /**
107
207
  * Get a cell locator by column name.
108
208
  * @param column - Column name (case-sensitive)
109
- * @returns Locator for the cell
209
+ * @returns SmartCell (Locator + bringIntoView)
110
210
  * @example
111
211
  * const emailCell = row.getCell('Email');
212
+ * await emailCell.bringIntoView();
112
213
  * await expect(emailCell).toHaveText('john@example.com');
113
214
  */
114
- getCell(column: string): Locator;
215
+ getCell(column: string): SmartCell;
115
216
 
116
217
  /**
117
218
  * Extract all cell data as a key-value object.
@@ -129,8 +230,9 @@ export type SmartRow<T = any> = Locator & {
129
230
 
130
231
  /**
131
232
  * Scrolls/paginates to bring this row into view.
132
- * Only works if rowIndex is known (e.g., from getRowByIndex).
133
- * @throws Error if rowIndex is unknown
233
+ * Works when row position metadata is known (e.g., from getRowByIndex, findRow,
234
+ * findRows, filter, or async iteration).
235
+ * @throws Error if row position metadata is unknown
134
236
  */
135
237
  bringIntoView(): Promise<void>;
136
238
 
@@ -240,6 +342,15 @@ export interface PaginationPrimitives {
240
342
  /** Jump to first page / scroll to top */
241
343
  goToFirst?: (context: TableContext) => Promise<boolean>;
242
344
 
345
+ /** Jump to last page / scroll to bottom */
346
+ goToLast?: (context: TableContext) => Promise<boolean>;
347
+
348
+ /**
349
+ * Fetch the total number of pages currently available.
350
+ * Can be used to optimize pagination paths (e.g. jumping to last page and going backwards).
351
+ */
352
+ getTotalPages?: (context: TableContext) => Promise<number | null>;
353
+
243
354
  /**
244
355
  * Jump to specific page index (0-indexed).
245
356
  * Can be full-range (e.g. page number input: any page works) or windowed (e.g. only visible links 6–14).
@@ -252,6 +363,18 @@ export interface PaginationPrimitives {
252
363
 
253
364
  /** How many pages one goPreviousBulk() goes back. Used by navigation path planner for optimal bringIntoView. */
254
365
  previousBulkPages?: number;
366
+
367
+ /**
368
+ * Called once during init() to sync the library's page counter with the actual DOM state.
369
+ * Use when a table may open on a page other than the first (e.g. a deep-linked URL that
370
+ * lands on page 5). Returns a 0-indexed page number.
371
+ * @example
372
+ * detectCurrentPage: async (root) => {
373
+ * const text = await root.locator('[aria-current="page"]').textContent();
374
+ * return parseInt(text ?? '1') - 1;
375
+ * }
376
+ */
377
+ detectCurrentPage?: (root: import('@playwright/test').Locator) => number | Promise<number>;
255
378
  }
256
379
 
257
380
  export type PaginationStrategy = PaginationPrimitives;
@@ -358,6 +481,18 @@ export interface TableStrategies {
358
481
  * E.g. after sort/pagination, the engine uses loading.isTableLoading when present.
359
482
  */
360
483
  loading?: LoadingStrategy;
484
+
485
+ /**
486
+ * Viewport oracle strategies for 2D virtualized tables (e.g. MUI DataGrid, AG Grid,
487
+ * Braintrust-style grids where both rows and columns are virtualized simultaneously).
488
+ *
489
+ * When present, the cell-reading engine uses these to jump directly to the target
490
+ * row/column and to detect the 2D scroll hazard (scrolling to reveal a column can
491
+ * knock rows out of the vertical viewport, and vice versa).
492
+ *
493
+ * Completely optional and additive — existing \`navigation\` primitives remain the fallback.
494
+ */
495
+ viewport?: ViewportStrategy;
361
496
  }
362
497
 
363
498
 
@@ -520,6 +655,26 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
520
655
  */
521
656
  scrollToColumn: (columnName: string) => Promise<void>;
522
657
 
658
+ /**
659
+ * Counts the number of rows currently on the page.
660
+ * Does not paginate.
661
+ */
662
+ countRows: () => Promise<number>;
663
+
664
+ /**
665
+ * Iterates over rows and extracts the value of a single column.
666
+ * More efficient than map + toJSON for single-column extraction.
667
+ * @param columnName - The name of the column to extract
668
+ * @param options - Iteration options
669
+ */
670
+ mapColumn<R = string>(columnName: string, options?: RowIterationOptions): Promise<R[]>;
671
+
672
+ /**
673
+ * Iterates over rows and extracts the value of a single column as strings.
674
+ * @param columnName - The name of the column to extract
675
+ * @param options - Iteration options
676
+ */
677
+ getColumnValues(columnName: string, options?: RowIterationOptions): Promise<string[]>;
523
678
 
524
679
 
525
680
  /**
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { SmartRowArray } from './utils/smartRowArray';
7
7
  * rowSelector: 'tbody tr'
8
8
  *
9
9
  * // Function selector
10
- * rowSelector: (root) => root.locator('[role="row"]')
10
+ * headerSelector: (root) => root.locator('[role="columnheader"]')
11
11
  */
12
12
  export type Selector = string | ((root: Locator | Page) => Locator) | ((root: Locator) => Locator);
13
13
  /**
@@ -28,10 +28,13 @@ export type FilterValue = string | RegExp | number | ((cell: Locator) => Locator
28
28
  */
29
29
  export type GetCellLocatorFn = (args: {
30
30
  row: Locator;
31
+ /** The root locator passed to useTable(). Useful for re-querying stale row locators. */
32
+ root: Locator;
31
33
  columnName: string;
32
34
  columnIndex: number;
33
35
  rowIndex?: number;
34
36
  page: Page;
37
+ config: FinalTableConfig<any>;
35
38
  }) => Locator;
36
39
  /**
37
40
  * Hook called before each cell value is read in toJSON (and columnOverrides.read).
@@ -68,6 +71,102 @@ export type GetActiveCellFn = (args: TableContext) => Promise<{
68
71
  columnName?: string;
69
72
  locator: Locator;
70
73
  } | null>;
74
+ /**
75
+ * Viewport oracle strategies for virtualized tables.
76
+ *
77
+ * These tell the library *what is currently in the DOM* rather than *how to move*.
78
+ * When present, the cell-reading engine uses them to jump directly to a target
79
+ * row/column instead of stepping blindly with navigation primitives.
80
+ *
81
+ * This is the primary fix for the 2D scroll hazard: scrolling right to bring a
82
+ * column into view can knock rows out of the vertical viewport (and vice versa).
83
+ * With `getVisibleRowRange` the engine detects this and calls `scrollToRow` to
84
+ * restore the row before reading — eliminating the hazard without polling loops.
85
+ *
86
+ * All members are optional. Supply whichever the underlying grid exposes:
87
+ * - Range oracles alone enable hazard detection with graceful fallback to navigation primitives.
88
+ * - Scroll primitives alone enable direct jumps without range awareness.
89
+ * - Both together give the most reliable, fastest path.
90
+ *
91
+ * @example
92
+ * // MUI DataGrid via apiRef
93
+ * strategies: {
94
+ * viewport: {
95
+ * getVisibleRowRange: async ({ root }) =>
96
+ * root.evaluate(el => {
97
+ * const api = (el as any).__muiDataGrid__;
98
+ * return { first: api.getFirstVisibleRow(), last: api.getLastVisibleRow() };
99
+ * }),
100
+ * scrollToRow: async ({ root }, rowIndex) =>
101
+ * root.evaluate((el, idx) =>
102
+ * (el as any).__muiDataGrid__.scrollToIndexes({ rowIndex: idx }), rowIndex),
103
+ * scrollToColumn: async ({ root }, colIndex) =>
104
+ * root.evaluate((el, idx) =>
105
+ * (el as any).__muiDataGrid__.scrollToIndexes({ colIndex: idx }), colIndex),
106
+ * }
107
+ * }
108
+ *
109
+ * @example
110
+ * // aria-rowindex / aria-colindex DOM fallback (works without internal API access)
111
+ * strategies: {
112
+ * viewport: {
113
+ * getVisibleRowRange: async ({ root }) =>
114
+ * root.evaluate(el => {
115
+ * const rows = [...el.querySelectorAll('[role="row"][aria-rowindex]')];
116
+ * const idxs = rows.map(r => Number(r.getAttribute('aria-rowindex')) - 1);
117
+ * return { first: Math.min(...idxs), last: Math.max(...idxs) };
118
+ * }),
119
+ * }
120
+ * }
121
+ */
122
+ export interface ViewportStrategy {
123
+ /**
124
+ * Returns the 0-based index range of rows currently rendered in the DOM.
125
+ * Used to detect when a target row has been scrolled out of view and needs recovery.
126
+ */
127
+ getVisibleRowRange?: (context: TableContext) => Promise<{
128
+ first: number;
129
+ last: number;
130
+ }>;
131
+ /**
132
+ * Returns the 0-based index range of columns currently rendered in the DOM.
133
+ * Used to detect when a target column is not yet mounted before reading.
134
+ */
135
+ getVisibleColumnRange?: (context: TableContext) => Promise<{
136
+ first: number;
137
+ last: number;
138
+ }>;
139
+ /**
140
+ * Scrolls or jumps directly to make a row visible by 0-based index.
141
+ * Replaces blind goDown()/goUp() step loops for grids that expose a scroll API.
142
+ */
143
+ scrollToRow?: (context: TableContext, rowIndex: number) => Promise<void>;
144
+ /**
145
+ * Scrolls or jumps directly to make a column visible by 0-based index.
146
+ * Replaces blind goRight()/goLeft() step loops for grids that expose a scroll API.
147
+ */
148
+ scrollToColumn?: (context: TableContext, colIndex: number) => Promise<void>;
149
+ /**
150
+ * When true, disables the automatic memoization of getVisibleColumnRange and
151
+ * getVisibleRowRange results. By default the library caches each range value and
152
+ * invalidates it after the corresponding scroll call, eliminating redundant DOM
153
+ * evaluate() round-trips between scrolls.
154
+ */
155
+ disableCache?: boolean;
156
+ }
157
+ /**
158
+ * SmartCell - A Playwright Locator with table-aware methods for single-cell operations.
159
+ *
160
+ * Extends all standard Locator methods (click, isVisible, etc.).
161
+ */
162
+ export type SmartCell = Locator & {
163
+ /**
164
+ * Scrolls/paginates to bring this specific cell into view using the configured strategies.
165
+ * Useful when the grid is horizontally virtualized and the column must be scrolled
166
+ * into view before it can be interacted with or read.
167
+ */
168
+ bringIntoView(): Promise<void>;
169
+ };
71
170
  /**
72
171
  * SmartRow - A Playwright Locator with table-aware methods.
73
172
  *
@@ -90,12 +189,13 @@ export type SmartRow<T = any> = Locator & {
90
189
  /**
91
190
  * Get a cell locator by column name.
92
191
  * @param column - Column name (case-sensitive)
93
- * @returns Locator for the cell
192
+ * @returns SmartCell (Locator + bringIntoView)
94
193
  * @example
95
194
  * const emailCell = row.getCell('Email');
195
+ * await emailCell.bringIntoView();
96
196
  * await expect(emailCell).toHaveText('john@example.com');
97
197
  */
98
- getCell(column: string): Locator;
198
+ getCell(column: string): SmartCell;
99
199
  /**
100
200
  * Extract all cell data as a key-value object.
101
201
  * @param options - Optional configuration
@@ -113,8 +213,9 @@ export type SmartRow<T = any> = Locator & {
113
213
  }): Promise<T>;
114
214
  /**
115
215
  * Scrolls/paginates to bring this row into view.
116
- * Only works if rowIndex is known (e.g., from getRowByIndex).
117
- * @throws Error if rowIndex is unknown
216
+ * Works when row position metadata is known (e.g., from getRowByIndex, findRow,
217
+ * findRows, filter, or async iteration).
218
+ * @throws Error if row position metadata is unknown
118
219
  */
119
220
  bringIntoView(): Promise<void>;
120
221
  /**
@@ -211,6 +312,13 @@ export interface PaginationPrimitives {
211
312
  goPreviousBulk?: (context: TableContext) => Promise<boolean | number>;
212
313
  /** Jump to first page / scroll to top */
213
314
  goToFirst?: (context: TableContext) => Promise<boolean>;
315
+ /** Jump to last page / scroll to bottom */
316
+ goToLast?: (context: TableContext) => Promise<boolean>;
317
+ /**
318
+ * Fetch the total number of pages currently available.
319
+ * Can be used to optimize pagination paths (e.g. jumping to last page and going backwards).
320
+ */
321
+ getTotalPages?: (context: TableContext) => Promise<number | null>;
214
322
  /**
215
323
  * Jump to specific page index (0-indexed).
216
324
  * Can be full-range (e.g. page number input: any page works) or windowed (e.g. only visible links 6–14).
@@ -221,6 +329,17 @@ export interface PaginationPrimitives {
221
329
  nextBulkPages?: number;
222
330
  /** How many pages one goPreviousBulk() goes back. Used by navigation path planner for optimal bringIntoView. */
223
331
  previousBulkPages?: number;
332
+ /**
333
+ * Called once during init() to sync the library's page counter with the actual DOM state.
334
+ * Use when a table may open on a page other than the first (e.g. a deep-linked URL that
335
+ * lands on page 5). Returns a 0-indexed page number.
336
+ * @example
337
+ * detectCurrentPage: async (root) => {
338
+ * const text = await root.locator('[aria-current="page"]').textContent();
339
+ * return parseInt(text ?? '1') - 1;
340
+ * }
341
+ */
342
+ detectCurrentPage?: (root: import('@playwright/test').Locator) => number | Promise<number>;
224
343
  }
225
344
  export type PaginationStrategy = PaginationPrimitives;
226
345
  export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
@@ -319,6 +438,17 @@ export interface TableStrategies {
319
438
  * E.g. after sort/pagination, the engine uses loading.isTableLoading when present.
320
439
  */
321
440
  loading?: LoadingStrategy;
441
+ /**
442
+ * Viewport oracle strategies for 2D virtualized tables (e.g. MUI DataGrid, AG Grid,
443
+ * Braintrust-style grids where both rows and columns are virtualized simultaneously).
444
+ *
445
+ * When present, the cell-reading engine uses these to jump directly to the target
446
+ * row/column and to detect the 2D scroll hazard (scrolling to reveal a column can
447
+ * knock rows out of the vertical viewport, and vice versa).
448
+ *
449
+ * Completely optional and additive — existing `navigation` primitives remain the fallback.
450
+ */
451
+ viewport?: ViewportStrategy;
322
452
  }
323
453
  export interface TableConfig<T = any> {
324
454
  /** Selector for the table headers */
@@ -472,6 +602,24 @@ export interface TableResult<T = any> extends AsyncIterable<{
472
602
  * Navigates to a specific column using the configured CellNavigationStrategy.
473
603
  */
474
604
  scrollToColumn: (columnName: string) => Promise<void>;
605
+ /**
606
+ * Counts the number of rows currently on the page.
607
+ * Does not paginate.
608
+ */
609
+ countRows: () => Promise<number>;
610
+ /**
611
+ * Iterates over rows and extracts the value of a single column.
612
+ * More efficient than map + toJSON for single-column extraction.
613
+ * @param columnName - The name of the column to extract
614
+ * @param options - Iteration options
615
+ */
616
+ mapColumn<R = string>(columnName: string, options?: RowIterationOptions): Promise<R[]>;
617
+ /**
618
+ * Iterates over rows and extracts the value of a single column as strings.
619
+ * @param columnName - The name of the column to extract
620
+ * @param options - Iteration options
621
+ */
622
+ getColumnValues(columnName: string, options?: RowIterationOptions): Promise<string[]>;
475
623
  /**
476
624
  * Resets the table state (clears cache, flags) and invokes the onReset strategy.
477
625
  */