@rickcedwhat/playwright-smart-table 6.20.1 → 6.21.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.
Files changed (41) hide show
  1. package/dist/engine/rowFinder.d.ts +1 -0
  2. package/dist/engine/rowFinder.js +75 -75
  3. package/dist/engine/scanPages.d.ts +58 -0
  4. package/dist/engine/scanPages.js +46 -0
  5. package/dist/engine/tableIteration.d.ts +1 -0
  6. package/dist/engine/tableIteration.js +38 -20
  7. package/dist/filterEngine.d.ts +4 -0
  8. package/dist/filterEngine.js +15 -4
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.js +5 -1
  11. package/dist/packageVersion.d.ts +1 -1
  12. package/dist/packageVersion.js +1 -1
  13. package/dist/plugins/index.d.ts +51 -10
  14. package/dist/plugins/index.js +14 -6
  15. package/dist/presets/glide/index.d.ts +1 -1
  16. package/dist/presets/glide/index.js +2 -2
  17. package/dist/presets/rdg.d.ts +1 -1
  18. package/dist/presets/rdg.js +4 -4
  19. package/dist/smartRow.d.ts +2 -2
  20. package/dist/smartRow.js +160 -49
  21. package/dist/strategies/columns.d.ts +5 -9
  22. package/dist/strategies/columns.js +0 -10
  23. package/dist/strategies/contentReady.d.ts +20 -0
  24. package/dist/strategies/contentReady.js +62 -0
  25. package/dist/strategies/filter.d.ts +0 -4
  26. package/dist/strategies/filter.js +0 -16
  27. package/dist/strategies/headers.d.ts +5 -0
  28. package/dist/strategies/headers.js +17 -9
  29. package/dist/strategies/index.d.ts +23 -15
  30. package/dist/strategies/index.js +9 -5
  31. package/dist/strategies/pagination.js +11 -2
  32. package/dist/strategies/viewport.d.ts +4 -1
  33. package/dist/strategies/viewport.js +62 -32
  34. package/dist/typeContext.d.ts +1 -1
  35. package/dist/typeContext.js +45 -1
  36. package/dist/types.d.ts +43 -1
  37. package/dist/useTable.js +105 -43
  38. package/dist/utils/elementTracker.js +6 -3
  39. package/dist/utils/resolveCellLocator.d.ts +35 -0
  40. package/dist/utils/resolveCellLocator.js +52 -0
  41. package/package.json +1 -1
@@ -1,13 +1,32 @@
1
1
  /**
2
2
  * @deprecated Use `presets` instead. Plugins will be removed in v7.0.0.
3
- * Presets for specific table/grid libraries. Each plugin exposes:
4
- * - Plugins.X — full preset (selectors + headerTransformer if any + strategies). Spread: useTable(loc, { ...Presets.MUI, maxPages: 5 }).
5
- * - Plugins.X.Strategies — strategies only. Use with your own selectors: useTable(loc, { rowSelector: '...', strategies: Presets.MUI.Strategies }).
3
+ *
4
+ * Compatibility shim over official presets:
5
+ * - `Plugins.MUI` → `presets.muiDataGrid` (**DataGrid only** — not `presets.muiTable`)
6
+ * - `Plugins.RDG` → `presets.rdg`
7
+ * - `Plugins.Glide` → `presets.glide`
8
+ *
9
+ * Prefer: `useTable(loc, { ...presets.muiDataGrid, maxPages: 5 })`.
10
+ * Strategies only: `useTable(loc, { rowSelector: '...', strategies: presets.muiDataGrid.strategies })`.
6
11
  */
7
12
  export declare const Plugins: {
8
13
  /** @deprecated Use `presets.rdg` */
9
14
  RDG: {
10
- Strategies: import("../types").TableStrategies | undefined;
15
+ Strategies: {
16
+ header: (context: import("..").TableContext) => Promise<string[]>;
17
+ getCellLocator: ({ row, columnIndex }: {
18
+ row: import("@playwright/test").Locator;
19
+ columnIndex: number;
20
+ }) => import("@playwright/test").Locator;
21
+ navigation: {
22
+ goRight: ({ root }: import("..").StrategyContext) => Promise<void>;
23
+ goLeft: ({ root }: import("..").StrategyContext) => Promise<void>;
24
+ goDown: ({ root }: import("..").StrategyContext) => Promise<void>;
25
+ goUp: ({ root }: import("..").StrategyContext) => Promise<void>;
26
+ goHome: ({ root }: import("..").StrategyContext) => Promise<void>;
27
+ };
28
+ pagination: import("..").PaginationPrimitives;
29
+ };
11
30
  headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
12
31
  rowSelector?: string | undefined;
13
32
  cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
@@ -22,14 +41,33 @@ export declare const Plugins: {
22
41
  autoScroll?: boolean | undefined;
23
42
  debug?: import("..").DebugConfig | undefined;
24
43
  onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
25
- strategies?: import("../types").TableStrategies | undefined;
44
+ strategies?: import("..").TableStrategies | undefined;
26
45
  columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
27
46
  syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
28
47
  emptyState?: import("@playwright/test").Locator | undefined;
29
48
  };
30
49
  /** @deprecated Use `presets.glide` */
31
50
  Glide: {
32
- Strategies: import("../types").TableStrategies | undefined;
51
+ Strategies: {
52
+ fillSimple: import("..").FillStrategy;
53
+ fill: import("..").FillStrategy;
54
+ pagination: import("..").PaginationPrimitives;
55
+ header: (context: import("..").StrategyContext, options?: {
56
+ limit?: number;
57
+ selector?: string;
58
+ scrollAmount?: number;
59
+ }) => Promise<string[]>;
60
+ viewport: import("..").ViewportStrategy;
61
+ loading: {
62
+ isHeaderLoading: () => Promise<boolean>;
63
+ };
64
+ getCellLocator: ({ row, columnIndex }: any) => any;
65
+ getActiveCell: ({ page }: any) => Promise<{
66
+ rowIndex: number;
67
+ columnIndex: number;
68
+ locator: any;
69
+ } | null>;
70
+ };
33
71
  headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
34
72
  rowSelector?: string | undefined;
35
73
  cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
@@ -44,14 +82,17 @@ export declare const Plugins: {
44
82
  autoScroll?: boolean | undefined;
45
83
  debug?: import("..").DebugConfig | undefined;
46
84
  onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
47
- strategies?: import("../types").TableStrategies | undefined;
85
+ strategies?: import("..").TableStrategies | undefined;
48
86
  columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
49
87
  syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
50
88
  emptyState?: import("@playwright/test").Locator | undefined;
51
89
  };
52
- /** @deprecated Use `presets.muiDataGrid` */
90
+ /**
91
+ * @deprecated Use `presets.muiDataGrid`.
92
+ * DataGrid only — for MUI `<Table>` use `presets.muiTable`.
93
+ */
53
94
  MUI: {
54
- Strategies: import("../types").TableStrategies | undefined;
95
+ Strategies: import("..").TableStrategies | undefined;
55
96
  headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
56
97
  rowSelector?: string | undefined;
57
98
  cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
@@ -66,7 +107,7 @@ export declare const Plugins: {
66
107
  autoScroll?: boolean | undefined;
67
108
  debug?: import("..").DebugConfig | undefined;
68
109
  onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
69
- strategies?: import("../types").TableStrategies | undefined;
110
+ strategies?: import("..").TableStrategies | undefined;
70
111
  columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
71
112
  syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
72
113
  emptyState?: import("@playwright/test").Locator | undefined;
@@ -4,15 +4,23 @@ exports.Plugins = void 0;
4
4
  const presets_1 = require("../presets");
5
5
  /**
6
6
  * @deprecated Use `presets` instead. Plugins will be removed in v7.0.0.
7
- * Presets for specific table/grid libraries. Each plugin exposes:
8
- * - Plugins.X — full preset (selectors + headerTransformer if any + strategies). Spread: useTable(loc, { ...Presets.MUI, maxPages: 5 }).
9
- * - Plugins.X.Strategies — strategies only. Use with your own selectors: useTable(loc, { rowSelector: '...', strategies: Presets.MUI.Strategies }).
7
+ *
8
+ * Compatibility shim over official presets:
9
+ * - `Plugins.MUI` → `presets.muiDataGrid` (**DataGrid only** — not `presets.muiTable`)
10
+ * - `Plugins.RDG` → `presets.rdg`
11
+ * - `Plugins.Glide` → `presets.glide`
12
+ *
13
+ * Prefer: `useTable(loc, { ...presets.muiDataGrid, maxPages: 5 })`.
14
+ * Strategies only: `useTable(loc, { rowSelector: '...', strategies: presets.muiDataGrid.strategies })`.
10
15
  */
11
16
  exports.Plugins = {
12
17
  /** @deprecated Use `presets.rdg` */
13
- RDG: Object.assign(Object.assign({}, presets_1.rdg), { Strategies: presets_1.rdg.strategies }),
18
+ RDG: Object.assign(Object.assign({}, presets_1.rdg), { Strategies: presets_1.rdg.Strategies }),
14
19
  /** @deprecated Use `presets.glide` */
15
- Glide: Object.assign(Object.assign({}, presets_1.glide), { Strategies: presets_1.glide.strategies }),
16
- /** @deprecated Use `presets.muiDataGrid` */
20
+ Glide: Object.assign(Object.assign({}, presets_1.glide), { Strategies: presets_1.glide.Strategies }),
21
+ /**
22
+ * @deprecated Use `presets.muiDataGrid`.
23
+ * DataGrid only — for MUI `<Table>` use `presets.muiTable`.
24
+ */
17
25
  MUI: Object.assign(Object.assign({}, presets_1.muiDataGrid), { Strategies: presets_1.muiDataGrid.strategies }),
18
26
  };
@@ -12,7 +12,7 @@ export declare const GlideStrategies: {
12
12
  selector?: string;
13
13
  scrollAmount?: number;
14
14
  }) => Promise<string[]>;
15
- viewport: import("../../types").ViewportStrategy;
15
+ viewport: import("../..").ViewportStrategy;
16
16
  loading: {
17
17
  isHeaderLoading: () => Promise<boolean>;
18
18
  };
@@ -123,8 +123,8 @@ function createGlide(options = {}) {
123
123
  }
124
124
  /**
125
125
  * Full preset for Glide Data Grid (selectors + default strategies only).
126
- * Spread: useTable(loc, { ...Plugins.Glide, maxPages: 5 }).
127
- * Strategies only (including fillSimple): useTable(loc, { rowSelector: '...', strategies: Plugins.Glide.Strategies }).
126
+ * Spread: `useTable(loc, { ...presets.glide, maxPages: 5 })`.
127
+ * Strategies only (including fillSimple): `useTable(loc, { rowSelector: '...', strategies: presets.glide.Strategies })`.
128
128
  * For a non-64-column grid, use `createGlide({ columnCount })` instead.
129
129
  */
130
130
  const GlidePreset = {
@@ -1,6 +1,6 @@
1
1
  import type { Locator } from '@playwright/test';
2
2
  import { TableContext, TableConfig, ViewportStrategy, StrategyContext } from '../types';
3
- /** Full strategies for React Data Grid. Use when you want to supply your own selectors: strategies: Plugins.RDG.Strategies */
3
+ /** Full strategies for React Data Grid. Use when you want to supply your own selectors: `strategies: presets.rdg.Strategies` */
4
4
  export declare const RDGStrategies: {
5
5
  header: (context: TableContext) => Promise<string[]>;
6
6
  getCellLocator: ({ row, columnIndex }: {
@@ -92,20 +92,20 @@ const rdgNavigation = {
92
92
  });
93
93
  }
94
94
  };
95
- /** Default strategies for the RDG preset (used when you spread Plugins.RDG). */
95
+ /** Default strategies for the RDG preset (used when you spread `presets.rdg`). */
96
96
  const RDGDefaultStrategies = {
97
97
  header: scrollRightHeaderRDG,
98
98
  getCellLocator: rdgGetCellLocator,
99
99
  navigation: rdgNavigation,
100
100
  pagination: rdgPaginationStrategy
101
101
  };
102
- /** Full strategies for React Data Grid. Use when you want to supply your own selectors: strategies: Plugins.RDG.Strategies */
102
+ /** Full strategies for React Data Grid. Use when you want to supply your own selectors: `strategies: presets.rdg.Strategies` */
103
103
  // fallow-ignore-next-line unused-export
104
104
  exports.RDGStrategies = RDGDefaultStrategies;
105
105
  /**
106
106
  * Full preset for React Data Grid (selectors + default strategies).
107
- * Spread: useTable(loc, { ...Plugins.RDG, maxPages: 5 }).
108
- * Strategies only: useTable(loc, { rowSelector: '...', strategies: Plugins.RDG.Strategies }).
107
+ * Spread: `useTable(loc, { ...presets.rdg, maxPages: 5 })`.
108
+ * Strategies only: `useTable(loc, { rowSelector: '...', strategies: presets.rdg.Strategies })`.
109
109
  */
110
110
  const RDGPreset = {
111
111
  rowSelector: '[role="row"].rdg-row',
@@ -1,4 +1,4 @@
1
- import type { Locator, Page } from '@playwright/test';
1
+ import { type Locator, type Page } from '@playwright/test';
2
2
  import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types';
3
3
  import { NavigationBarrier } from './utils/navigationBarrier';
4
4
  /**
@@ -9,5 +9,5 @@ import { NavigationBarrier } from './utils/navigationBarrier';
9
9
  * @internal Internal factory for creating SmartRow objects.
10
10
  * Not part of the public package surface; tests and consumers should use public APIs.
11
11
  */
12
- declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null, tablePageIndex?: number, barrier?: NavigationBarrier) => SmartRowType<T>;
12
+ declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null, tablePageIndex?: number, barrier?: NavigationBarrier, renderWindowPosition?: boolean) => SmartRowType<T>;
13
13
  export default createSmartRow;
package/dist/smartRow.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const test_1 = require("@playwright/test");
3
4
  const rowResolution_1 = require("./engine/rowResolution");
4
5
  const fill_1 = require("./strategies/fill");
5
6
  const stringUtils_1 = require("./utils/stringUtils");
@@ -12,7 +13,7 @@ const sentinel_1 = require("./utils/sentinel");
12
13
  * Returns the target cell locator after navigation.
13
14
  */
14
15
  const _navigateToCell = async (params) => {
15
- const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier } = params;
16
+ const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier, allowMissingCell } = params;
16
17
  const rowLabel = typeof rowIndex === 'number' ? `row ${rowIndex}` : 'row ?';
17
18
  (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: resolving column "${column}" (colIndex ${index}), ${rowLabel}`);
18
19
  // Get active cell if strategy is available
@@ -65,7 +66,18 @@ const _navigateToCell = async (params) => {
65
66
  ? await viewport.getVisibleRowRange(context)
66
67
  : null;
67
68
  const rowVisible = !rowRange || (rowIndex >= rowRange.first && rowIndex <= rowRange.last);
68
- if (rowVisible && await targetReached()) {
69
+ let cellAttached = rowVisible && await targetReached();
70
+ if (rowVisible && !cellAttached) {
71
+ try {
72
+ await getCellLocator().waitFor({ state: 'attached', timeout: 500 });
73
+ cellAttached = true;
74
+ }
75
+ catch (error) {
76
+ if (!(error instanceof test_1.errors.TimeoutError))
77
+ throw error;
78
+ }
79
+ }
80
+ if (cellAttached) {
69
81
  (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: col ${index} in visible range [${knownColRange.first}-${knownColRange.last}], reading directly`);
70
82
  // Check in to the barrier without a moveAction — peers that need the scroll
71
83
  // still supply their own action and will trigger it when all rows arrive.
@@ -124,6 +136,8 @@ const _navigateToCell = async (params) => {
124
136
  }
125
137
  // Cell still not accessible — fall through to navigation primitives if configured.
126
138
  if (!config.strategies.navigation) {
139
+ if (allowMissingCell)
140
+ return getCellLocator();
127
141
  const colRange = viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
128
142
  const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
129
143
  let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex}.\n`;
@@ -144,15 +158,17 @@ const _navigateToCell = async (params) => {
144
158
  // Use navigation primitives if available
145
159
  if (config.strategies.navigation) {
146
160
  const nav = config.strategies.navigation;
147
- if (typeof rowIndex !== 'number') {
148
- throw new Error('Row index is required for navigation');
149
- }
150
161
  if (await targetReached()) {
151
162
  // Already there. If we have a barrier, check-in to stay in lock-step.
152
163
  if (barrier)
153
164
  await barrier.sync(index);
154
165
  return getCellLocator();
155
166
  }
167
+ if (typeof rowIndex !== 'number') {
168
+ if (allowMissingCell)
169
+ return getCellLocator();
170
+ throw new Error('Row index is required for navigation');
171
+ }
156
172
  // If getActiveCell is present but returns null (no focus), and cell is in DOM,
157
173
  // try to focus it once before entering navigation loop.
158
174
  if (config.strategies.getActiveCell) {
@@ -196,6 +212,8 @@ const _navigateToCell = async (params) => {
196
212
  currCol = ac.columnIndex;
197
213
  }
198
214
  }
215
+ // Column-0 snap is owned by the navigation primitive (scrollLeft=0, Home, etc.).
216
+ // Core must not special-case canvas/Home — that was pre-viewport Glide glue.
199
217
  if (index === 0 && nav.snapFirstColumnIntoView) {
200
218
  (0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: snapFirstColumnIntoView for column index 0');
201
219
  await nav.snapFirstColumnIntoView(context);
@@ -206,27 +224,6 @@ const _navigateToCell = async (params) => {
206
224
  currCol = ac.columnIndex;
207
225
  }
208
226
  }
209
- // Home moves a11y focus within the current row to column 0. If focus row !== target row
210
- // (e.g. still on previous row), Home jumps to grid origin and breaks `tr.nth(k)` reads.
211
- if (typeof rowIndex === 'number' && currRow === rowIndex) {
212
- await rootLocator.evaluate((el) => {
213
- var _a;
214
- const canvas = el.closest('canvas') || ((_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('canvas'));
215
- if (canvas instanceof HTMLCanvasElement) {
216
- canvas.tabIndex = 0;
217
- canvas.focus();
218
- }
219
- });
220
- await page.keyboard.press('Home');
221
- await page.waitForTimeout(120);
222
- if (config.strategies.getActiveCell) {
223
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
224
- if (ac) {
225
- currRow = ac.rowIndex;
226
- currCol = ac.columnIndex;
227
- }
228
- }
229
- }
230
227
  }
231
228
  let cDiff = index - currCol;
232
229
  if (Math.abs(cDiff) > 12 && nav.seekColumnIndex) {
@@ -253,8 +250,8 @@ const _navigateToCell = async (params) => {
253
250
  const settleMs = (Number.isFinite(nav.settleMs) && nav.settleMs >= 0) ? nav.settleMs : computed;
254
251
  await page.waitForTimeout(settleMs);
255
252
  }
256
- // Wait for active cell to match target: poll getActiveCell or fallback to fixed delay
257
- // This is the "Midas Touch" buffer needed for Glide's async accessibility updates.
253
+ // After horizontal steps, poll until getActiveCell / DOM presence confirms the target
254
+ // (a11y trees and virtual mounts often lag the scroll). Tune via navigation.maxWaitMs.
258
255
  const pollIntervalMs = 10;
259
256
  const computedMax = Math.min(6000, 250 + horizontalSteps * 25);
260
257
  const maxWaitMs = (Number.isFinite(nav.maxWaitMs) && nav.maxWaitMs >= 0) ? nav.maxWaitMs : computedMax;
@@ -290,6 +287,8 @@ const _navigateToCell = async (params) => {
290
287
  const finalCell = getCellLocator();
291
288
  if (await finalCell.count() > 0)
292
289
  return finalCell;
290
+ if (allowMissingCell)
291
+ return finalCell;
293
292
  const colRange = viewport && viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
294
293
  const rowRange = viewport && viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
295
294
  let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex} after exhausting navigation strategies. Ensure navigation primitives are correctly implemented.\n`;
@@ -313,7 +312,7 @@ const _navigateToCell = async (params) => {
313
312
  * @internal Internal factory for creating SmartRow objects.
314
313
  * Not part of the public package surface; tests and consumers should use public APIs.
315
314
  */
316
- const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier) => {
315
+ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier, renderWindowPosition = false) => {
317
316
  const smart = rowLocator;
318
317
  // Attach State
319
318
  smart.rowIndex = rowIndex;
@@ -406,12 +405,22 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
406
405
  throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
407
406
  }
408
407
  const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[colName];
409
- const cell = config.strategies.getCellLocator
410
- ? config.strategies.getCellLocator({
411
- row: rowLocator, root: rootLocator, columnName: colName,
412
- columnIndex: idx, rowIndex, page: rootLocator.page(), config,
413
- })
414
- : resolve(config.cellSelector, rowLocator).nth(idx);
408
+ const page = rootLocator.page();
409
+ // Same nav pipeline as toJSON / getCell().bringIntoView() (#430) so virtualized
410
+ // off-screen columns don't return empty/stale text.
411
+ const cell = await _navigateToCell({
412
+ config,
413
+ rootLocator,
414
+ page,
415
+ resolve,
416
+ getHeaders: table === null || table === void 0 ? void 0 : table.getHeaders,
417
+ column: colName,
418
+ index: idx,
419
+ rowLocator,
420
+ rowIndex,
421
+ barrier: smart._barrier,
422
+ allowMissingCell: !!(columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read),
423
+ });
415
424
  if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
416
425
  const getCell = (name) => {
417
426
  const ci = map.get(name);
@@ -424,7 +433,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
424
433
  return (await cell.innerText()).trim();
425
434
  };
426
435
  smart.toJSON = async (options) => {
427
- var _a, _b, _c, _d, _e, _f, _g, _h;
436
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
428
437
  // Atomic mode: snapshot the row in a single evaluate, then apply column overrides
429
438
  // against a frozen reconstruction. Zero inter-column stagger — even in-place React
430
439
  // re-renders can't affect it because overrides read from a detached clone.
@@ -434,10 +443,84 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
434
443
  throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
435
444
  }
436
445
  const page = rootLocator.page();
446
+ // Re-pin: the row locator is a positional nth-child from the iteration loop.
447
+ // On recycling virtualizers, elements can shift between SmartRow creation and
448
+ // this clone — the nth element may now show a different row's content.
449
+ // Verify via resolveRowIndex and rescan if drifted.
450
+ let cloneTarget = rowLocator;
451
+ const resolveRI = config.strategies.resolveRowIndex;
452
+ const isSelfHealing = smart._selfHealing === true;
453
+ const rescanForRow = resolveRI && typeof rowIndex === 'number' && !isSelfHealing
454
+ ? async () => {
455
+ const rows = await resolve(config.rowSelector, rootLocator).all();
456
+ for (const r of rows) {
457
+ try {
458
+ const rResult = await resolveRI(r);
459
+ if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
460
+ return r;
461
+ }
462
+ catch ( /* element may have detached during scan */_a) { /* element may have detached during scan */ }
463
+ }
464
+ return null;
465
+ }
466
+ : null;
467
+ if (rescanForRow) {
468
+ let drifted = false;
469
+ try {
470
+ const curResult = await resolveRI(rowLocator);
471
+ const curIndex = curResult !== undefined
472
+ ? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
473
+ : undefined;
474
+ drifted = curIndex !== rowIndex;
475
+ }
476
+ catch (_k) {
477
+ drifted = true;
478
+ }
479
+ if (drifted) {
480
+ const inBatch = smart._inBatch === true || !!smart._barrier;
481
+ let recovered = await rescanForRow();
482
+ if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
483
+ await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
484
+ const deadline = Date.now() + 500;
485
+ while (!recovered && Date.now() < deadline) {
486
+ recovered = await rescanForRow();
487
+ if (!recovered)
488
+ await page.waitForTimeout(50);
489
+ }
490
+ }
491
+ if (!recovered) {
492
+ throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
493
+ (inBatch
494
+ ? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
495
+ : `Ensure a viewport.scrollToRow strategy can restore the row.`));
496
+ }
497
+ cloneTarget = recovered;
498
+ }
499
+ }
500
+ // Wait for async content renders to settle before cloning.
501
+ if (config.strategies.contentReady) {
502
+ await config.strategies.contentReady(cloneTarget, page);
503
+ // Revalidate: the element may have recycled during the await.
504
+ if (rescanForRow) {
505
+ let postIdx;
506
+ try {
507
+ const r = await resolveRI(cloneTarget);
508
+ postIdx = r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
509
+ }
510
+ catch (_l) {
511
+ postIdx = undefined;
512
+ }
513
+ if (postIdx !== rowIndex) {
514
+ const reFound = await rescanForRow();
515
+ if (reFound)
516
+ cloneTarget = reFound;
517
+ }
518
+ }
519
+ }
437
520
  // Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
438
521
  // Uses textContent (not innerText) because the clone is detached — innerText
439
522
  // falls back to textContent on detached nodes per HTML spec, so be explicit.
440
- const cloneHandle = await rowLocator.evaluateHandle(el => el.cloneNode(true));
523
+ const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
441
524
  let snapshot;
442
525
  try {
443
526
  snapshot = await cloneHandle.evaluate((row, sel) => {
@@ -491,7 +574,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
491
574
  await materializeRow();
492
575
  const result = {};
493
576
  for (const [col, idx] of columnsToProcess) {
494
- const columnOverride = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col];
577
+ const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
495
578
  const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
496
579
  if (mapper) {
497
580
  const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
@@ -508,7 +591,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
508
591
  result[col] = snapshot.cells[idx].text;
509
592
  }
510
593
  }
511
- for (const [name, def] of Object.entries((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})) {
594
+ for (const [name, def] of Object.entries((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})) {
512
595
  if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
513
596
  continue;
514
597
  const snapshotRow = Object.create(smart);
@@ -607,7 +690,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
607
690
  // Re-pin to the correct logical row before reading this column (#366).
608
691
  const stableRow = await resolveStableRow();
609
692
  // Check if we have a column override for this column
610
- const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[col];
693
+ const columnOverride = (_d = config.columnOverrides) === null || _d === void 0 ? void 0 : _d[col];
611
694
  const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
612
695
  // --- Navigation Logic Start ---
613
696
  const cell = config.strategies.getCellLocator
@@ -634,7 +717,8 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
634
717
  index: idx,
635
718
  rowLocator: stableRow,
636
719
  rowIndex,
637
- barrier: smart._barrier
720
+ barrier: smart._barrier,
721
+ allowMissingCell: !!mapper,
638
722
  });
639
723
  if (navigatedCell) {
640
724
  targetCell = navigatedCell;
@@ -654,15 +738,29 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
654
738
  });
655
739
  }
656
740
  // Cell loading wait (if configured)
657
- const isCellLoading = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.isCellLoading;
658
- const rawCellTimeout = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.cellLoadingTimeout;
741
+ const isCellLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isCellLoading;
742
+ const rawCellTimeout = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.cellLoadingTimeout;
659
743
  // undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
660
744
  const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
661
745
  ? rawCellTimeout
662
746
  : undefined;
663
- const onCellLoadingTimeout = (_g = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.onCellLoadingTimeout) !== null && _g !== void 0 ? _g : 'read-as-is';
664
- if (isCellLoading && await isCellLoading(targetCell, col, smart)) {
665
- if (cellLoadingTimeout !== undefined) {
747
+ const onCellLoadingTimeout = (_h = (_g = config.strategies.loading) === null || _g === void 0 ? void 0 : _g.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
748
+ if (isCellLoading) {
749
+ let loading = await isCellLoading(targetCell, col, smart);
750
+ // First-paint race: the cell node can attach a tick before the loading
751
+ // indicator mounts. A single false-negative then reads "" and skips
752
+ // onCellLoadingTimeout. Only grace when the cell still looks empty.
753
+ if (!loading && cellLoadingTimeout !== undefined && cellLoadingTimeout > 0) {
754
+ const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
755
+ if (peek === '') {
756
+ const graceDeadline = Date.now() + Math.min(250, cellLoadingTimeout);
757
+ while (!loading && Date.now() < graceDeadline) {
758
+ await page.waitForTimeout(25);
759
+ loading = await isCellLoading(targetCell, col, smart);
760
+ }
761
+ }
762
+ }
763
+ if (loading && cellLoadingTimeout !== undefined) {
666
764
  (0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
667
765
  const deadline = Date.now() + cellLoadingTimeout;
668
766
  let cellResolved = !(await isCellLoading(targetCell, col, smart)); // immediate check (handles timeout=0)
@@ -699,7 +797,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
699
797
  result[col] = (text || '').trim();
700
798
  }
701
799
  }
702
- for (const [name, def] of Object.entries((_h = config.syntheticColumns) !== null && _h !== void 0 ? _h : {})) {
800
+ for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
703
801
  if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
704
802
  continue;
705
803
  const snapshotRow = Object.create(smart);
@@ -775,6 +873,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
775
873
  (0, debugUtils_1.logDebug)(config, 'info', 'Row fill complete');
776
874
  };
777
875
  smart.bringIntoView = async () => {
876
+ var _a;
778
877
  if (rowIndex === undefined) {
779
878
  (0, debugUtils_1.logDebug)(config, 'info', 'bringIntoView called on a row with unknown rowIndex (e.g. from getRow()). ' +
780
879
  'Page navigation and scrolling will proceed, but virtual-scroll keyboard navigation will be skipped. ' +
@@ -821,8 +920,20 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
821
920
  }
822
921
  // Delay after pagination/finding before scrolling
823
922
  await (0, debugUtils_1.debugDelay)(config, 'findRow');
824
- // Scroll row into view using Playwright's built-in method
825
- await rowLocator.scrollIntoViewIfNeeded();
923
+ // Prefer viewport.scrollToRow when configured (#430). scrollIntoViewIfNeeded adjusts
924
+ // both axes and can evict sibling rows on recycling virtualizers.
925
+ const viewport = config.strategies.viewport;
926
+ const logicalRowIndex = (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && renderWindowPosition
927
+ ? (_a = (await (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, config, () => undefined))) === null || _a === void 0 ? void 0 : _a.index
928
+ : rowIndex;
929
+ if ((viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && typeof logicalRowIndex === 'number') {
930
+ const page = rootLocator.page();
931
+ (0, debugUtils_1.logDebug)(config, 'verbose', `bringIntoView: viewport.scrollToRow(${logicalRowIndex})`);
932
+ await viewport.scrollToRow({ root: rootLocator, config, page, resolve, getHeaders: parentTable === null || parentTable === void 0 ? void 0 : parentTable.getHeaders }, logicalRowIndex);
933
+ }
934
+ else {
935
+ await rowLocator.scrollIntoViewIfNeeded();
936
+ }
826
937
  };
827
938
  return smart;
828
939
  };
@@ -12,7 +12,9 @@ export interface NavigationPrimitives {
12
12
  goHome?: (context: StrategyContext) => Promise<void>;
13
13
  /**
14
14
  * After vertical moves, run before horizontal steps when the target column index is 0.
15
- * Use for horizontally virtualized a11y tables (e.g. Glide) so `td[aria-colindex="1"]` exists again.
15
+ * Use for horizontally virtualized tables so the first column exists in the DOM again
16
+ * (e.g. set scrollLeft = 0). If the table needs keyboard `Home` or canvas focus, put that
17
+ * here — the core orchestrator does not special-case those behaviors.
16
18
  * Do not reset vertical scroll here — RDG-style `goHome` is often unsuitable.
17
19
  */
18
20
  snapFirstColumnIntoView?: (context: StrategyContext) => Promise<void>;
@@ -33,7 +35,8 @@ export interface NavigationPrimitives {
33
35
  maxWaitMs?: number;
34
36
  }
35
37
  /**
36
- * @deprecated Use NavigationPrimitives instead. This will be removed in a future version.
38
+ * @deprecated Use NavigationPrimitives (`strategies.navigation`) instead. Removed from
39
+ * the public `Strategies` namespace in #428; this type remains for transitional typings.
37
40
  * Defines the contract for a cell navigation strategy.
38
41
  */
39
42
  export type CellNavigationStrategy = (context: StrategyContext & {
@@ -46,10 +49,3 @@ export type CellNavigationStrategy = (context: StrategyContext & {
46
49
  locator: Locator;
47
50
  } | null;
48
51
  }) => Promise<void>;
49
- export declare const CellNavigationStrategies: {
50
- /**
51
- * Default strategy: Assumes column is accessible or standard scrolling works.
52
- * No specific action taken other than what Playwright's default locator handling does.
53
- */
54
- default: () => Promise<void>;
55
- };
@@ -1,12 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CellNavigationStrategies = void 0;
4
- exports.CellNavigationStrategies = {
5
- /**
6
- * Default strategy: Assumes column is accessible or standard scrolling works.
7
- * No specific action taken other than what Playwright's default locator handling does.
8
- */
9
- default: async () => {
10
- // No-op
11
- }
12
- };
@@ -0,0 +1,20 @@
1
+ import type { ContentReadyStrategy } from '../types';
2
+ export type { ContentReadyStrategy };
3
+ export declare const ContentReadyStrategies: {
4
+ /**
5
+ * Polls the row's text content until two consecutive reads match.
6
+ */
7
+ textStable: (options?: {
8
+ timeout?: number;
9
+ interval?: number;
10
+ }) => ContentReadyStrategy;
11
+ /**
12
+ * Uses MutationObserver to wait for DOM mutations on the row's subtree
13
+ * to settle. Resolves when no mutations occur for `quietPeriod` ms, or
14
+ * when `timeout` expires.
15
+ */
16
+ mutationSettled: (options?: {
17
+ timeout?: number;
18
+ quietPeriod?: number;
19
+ }) => ContentReadyStrategy;
20
+ };