@rickcedwhat/playwright-smart-table 6.20.1 → 6.21.0-next.3ab7a18

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 (46) hide show
  1. package/dist/engine/rowFinder.d.ts +1 -0
  2. package/dist/engine/rowFinder.js +73 -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 +40 -22
  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/mui.js +15 -29
  18. package/dist/presets/rdg.d.ts +1 -1
  19. package/dist/presets/rdg.js +4 -4
  20. package/dist/smartRow.d.ts +2 -2
  21. package/dist/smartRow.js +163 -51
  22. package/dist/strategies/columns.d.ts +5 -9
  23. package/dist/strategies/columns.js +0 -10
  24. package/dist/strategies/contentReady.d.ts +20 -0
  25. package/dist/strategies/contentReady.js +62 -0
  26. package/dist/strategies/filter.d.ts +0 -4
  27. package/dist/strategies/filter.js +0 -16
  28. package/dist/strategies/headers.d.ts +5 -0
  29. package/dist/strategies/headers.js +17 -9
  30. package/dist/strategies/index.d.ts +23 -15
  31. package/dist/strategies/index.js +9 -5
  32. package/dist/strategies/pagination.js +11 -2
  33. package/dist/strategies/viewport.d.ts +4 -1
  34. package/dist/strategies/viewport.js +62 -32
  35. package/dist/typeContext.d.ts +1 -1
  36. package/dist/typeContext.js +77 -17
  37. package/dist/types.d.ts +74 -17
  38. package/dist/useTable.js +125 -56
  39. package/dist/utils/elementTracker.js +6 -3
  40. package/dist/utils/loadingWait.d.ts +15 -0
  41. package/dist/utils/loadingWait.js +43 -0
  42. package/dist/utils/pageIndex.d.ts +12 -0
  43. package/dist/utils/pageIndex.js +19 -0
  44. package/dist/utils/resolveCellLocator.d.ts +35 -0
  45. package/dist/utils/resolveCellLocator.js +52 -0
  46. package/package.json +1 -1
package/dist/smartRow.js CHANGED
@@ -1,18 +1,20 @@
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");
6
7
  const debugUtils_1 = require("./utils/debugUtils");
7
8
  const paginationPath_1 = require("./utils/paginationPath");
8
9
  const sentinel_1 = require("./utils/sentinel");
10
+ const pageIndex_1 = require("./utils/pageIndex");
9
11
  /**
10
12
  * Internal helper to navigate to a cell with active cell optimization.
11
13
  * Uses `strategies.navigation` primitives (goUp/goDown/goLeft/goRight, optional snap/seek/goHome).
12
14
  * Returns the target cell locator after navigation.
13
15
  */
14
16
  const _navigateToCell = async (params) => {
15
- const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier } = params;
17
+ const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier, allowMissingCell } = params;
16
18
  const rowLabel = typeof rowIndex === 'number' ? `row ${rowIndex}` : 'row ?';
17
19
  (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: resolving column "${column}" (colIndex ${index}), ${rowLabel}`);
18
20
  // Get active cell if strategy is available
@@ -65,7 +67,18 @@ const _navigateToCell = async (params) => {
65
67
  ? await viewport.getVisibleRowRange(context)
66
68
  : null;
67
69
  const rowVisible = !rowRange || (rowIndex >= rowRange.first && rowIndex <= rowRange.last);
68
- if (rowVisible && await targetReached()) {
70
+ let cellAttached = rowVisible && await targetReached();
71
+ if (rowVisible && !cellAttached) {
72
+ try {
73
+ await getCellLocator().waitFor({ state: 'attached', timeout: 500 });
74
+ cellAttached = true;
75
+ }
76
+ catch (error) {
77
+ if (!(error instanceof test_1.errors.TimeoutError))
78
+ throw error;
79
+ }
80
+ }
81
+ if (cellAttached) {
69
82
  (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: col ${index} in visible range [${knownColRange.first}-${knownColRange.last}], reading directly`);
70
83
  // Check in to the barrier without a moveAction — peers that need the scroll
71
84
  // still supply their own action and will trigger it when all rows arrive.
@@ -124,6 +137,8 @@ const _navigateToCell = async (params) => {
124
137
  }
125
138
  // Cell still not accessible — fall through to navigation primitives if configured.
126
139
  if (!config.strategies.navigation) {
140
+ if (allowMissingCell)
141
+ return getCellLocator();
127
142
  const colRange = viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
128
143
  const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
129
144
  let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex}.\n`;
@@ -144,15 +159,17 @@ const _navigateToCell = async (params) => {
144
159
  // Use navigation primitives if available
145
160
  if (config.strategies.navigation) {
146
161
  const nav = config.strategies.navigation;
147
- if (typeof rowIndex !== 'number') {
148
- throw new Error('Row index is required for navigation');
149
- }
150
162
  if (await targetReached()) {
151
163
  // Already there. If we have a barrier, check-in to stay in lock-step.
152
164
  if (barrier)
153
165
  await barrier.sync(index);
154
166
  return getCellLocator();
155
167
  }
168
+ if (typeof rowIndex !== 'number') {
169
+ if (allowMissingCell)
170
+ return getCellLocator();
171
+ throw new Error('Row index is required for navigation');
172
+ }
156
173
  // If getActiveCell is present but returns null (no focus), and cell is in DOM,
157
174
  // try to focus it once before entering navigation loop.
158
175
  if (config.strategies.getActiveCell) {
@@ -196,6 +213,8 @@ const _navigateToCell = async (params) => {
196
213
  currCol = ac.columnIndex;
197
214
  }
198
215
  }
216
+ // Column-0 snap is owned by the navigation primitive (scrollLeft=0, Home, etc.).
217
+ // Core must not special-case canvas/Home — that was pre-viewport Glide glue.
199
218
  if (index === 0 && nav.snapFirstColumnIntoView) {
200
219
  (0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: snapFirstColumnIntoView for column index 0');
201
220
  await nav.snapFirstColumnIntoView(context);
@@ -206,27 +225,6 @@ const _navigateToCell = async (params) => {
206
225
  currCol = ac.columnIndex;
207
226
  }
208
227
  }
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
228
  }
231
229
  let cDiff = index - currCol;
232
230
  if (Math.abs(cDiff) > 12 && nav.seekColumnIndex) {
@@ -253,8 +251,8 @@ const _navigateToCell = async (params) => {
253
251
  const settleMs = (Number.isFinite(nav.settleMs) && nav.settleMs >= 0) ? nav.settleMs : computed;
254
252
  await page.waitForTimeout(settleMs);
255
253
  }
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.
254
+ // After horizontal steps, poll until getActiveCell / DOM presence confirms the target
255
+ // (a11y trees and virtual mounts often lag the scroll). Tune via navigation.maxWaitMs.
258
256
  const pollIntervalMs = 10;
259
257
  const computedMax = Math.min(6000, 250 + horizontalSteps * 25);
260
258
  const maxWaitMs = (Number.isFinite(nav.maxWaitMs) && nav.maxWaitMs >= 0) ? nav.maxWaitMs : computedMax;
@@ -290,6 +288,8 @@ const _navigateToCell = async (params) => {
290
288
  const finalCell = getCellLocator();
291
289
  if (await finalCell.count() > 0)
292
290
  return finalCell;
291
+ if (allowMissingCell)
292
+ return finalCell;
293
293
  const colRange = viewport && viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
294
294
  const rowRange = viewport && viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
295
295
  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 +313,7 @@ const _navigateToCell = async (params) => {
313
313
  * @internal Internal factory for creating SmartRow objects.
314
314
  * Not part of the public package surface; tests and consumers should use public APIs.
315
315
  */
316
- const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier) => {
316
+ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier, renderWindowPosition = false) => {
317
317
  const smart = rowLocator;
318
318
  // Attach State
319
319
  smart.rowIndex = rowIndex;
@@ -406,12 +406,22 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
406
406
  throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
407
407
  }
408
408
  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);
409
+ const page = rootLocator.page();
410
+ // Same nav pipeline as toJSON / getCell().bringIntoView() (#430) so virtualized
411
+ // off-screen columns don't return empty/stale text.
412
+ const cell = await _navigateToCell({
413
+ config,
414
+ rootLocator,
415
+ page,
416
+ resolve,
417
+ getHeaders: table === null || table === void 0 ? void 0 : table.getHeaders,
418
+ column: colName,
419
+ index: idx,
420
+ rowLocator,
421
+ rowIndex,
422
+ barrier: smart._barrier,
423
+ allowMissingCell: !!(columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read),
424
+ });
415
425
  if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
416
426
  const getCell = (name) => {
417
427
  const ci = map.get(name);
@@ -424,7 +434,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
424
434
  return (await cell.innerText()).trim();
425
435
  };
426
436
  smart.toJSON = async (options) => {
427
- var _a, _b, _c, _d, _e, _f, _g, _h;
437
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
428
438
  // Atomic mode: snapshot the row in a single evaluate, then apply column overrides
429
439
  // against a frozen reconstruction. Zero inter-column stagger — even in-place React
430
440
  // re-renders can't affect it because overrides read from a detached clone.
@@ -434,10 +444,84 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
434
444
  throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
435
445
  }
436
446
  const page = rootLocator.page();
447
+ // Re-pin: the row locator is a positional nth-child from the iteration loop.
448
+ // On recycling virtualizers, elements can shift between SmartRow creation and
449
+ // this clone — the nth element may now show a different row's content.
450
+ // Verify via resolveRowIndex and rescan if drifted.
451
+ let cloneTarget = rowLocator;
452
+ const resolveRI = config.strategies.resolveRowIndex;
453
+ const isSelfHealing = smart._selfHealing === true;
454
+ const rescanForRow = resolveRI && typeof rowIndex === 'number' && !isSelfHealing
455
+ ? async () => {
456
+ const rows = await resolve(config.rowSelector, rootLocator).all();
457
+ for (const r of rows) {
458
+ try {
459
+ const rResult = await resolveRI(r);
460
+ if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
461
+ return r;
462
+ }
463
+ catch ( /* element may have detached during scan */_a) { /* element may have detached during scan */ }
464
+ }
465
+ return null;
466
+ }
467
+ : null;
468
+ if (rescanForRow) {
469
+ let drifted = false;
470
+ try {
471
+ const curResult = await resolveRI(rowLocator);
472
+ const curIndex = curResult !== undefined
473
+ ? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
474
+ : undefined;
475
+ drifted = curIndex !== rowIndex;
476
+ }
477
+ catch (_k) {
478
+ drifted = true;
479
+ }
480
+ if (drifted) {
481
+ const inBatch = smart._inBatch === true || !!smart._barrier;
482
+ let recovered = await rescanForRow();
483
+ if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
484
+ await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
485
+ const deadline = Date.now() + 500;
486
+ while (!recovered && Date.now() < deadline) {
487
+ recovered = await rescanForRow();
488
+ if (!recovered)
489
+ await page.waitForTimeout(50);
490
+ }
491
+ }
492
+ if (!recovered) {
493
+ throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
494
+ (inBatch
495
+ ? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
496
+ : `Ensure a viewport.scrollToRow strategy can restore the row.`));
497
+ }
498
+ cloneTarget = recovered;
499
+ }
500
+ }
501
+ // Wait for async content renders to settle before cloning.
502
+ if (config.strategies.contentReady) {
503
+ await config.strategies.contentReady(cloneTarget, page);
504
+ // Revalidate: the element may have recycled during the await.
505
+ if (rescanForRow) {
506
+ let postIdx;
507
+ try {
508
+ const r = await resolveRI(cloneTarget);
509
+ postIdx = r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
510
+ }
511
+ catch (_l) {
512
+ postIdx = undefined;
513
+ }
514
+ if (postIdx !== rowIndex) {
515
+ const reFound = await rescanForRow();
516
+ if (reFound)
517
+ cloneTarget = reFound;
518
+ }
519
+ }
520
+ }
437
521
  // Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
438
522
  // Uses textContent (not innerText) because the clone is detached — innerText
439
523
  // falls back to textContent on detached nodes per HTML spec, so be explicit.
440
- const cloneHandle = await rowLocator.evaluateHandle(el => el.cloneNode(true));
524
+ const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
441
525
  let snapshot;
442
526
  try {
443
527
  snapshot = await cloneHandle.evaluate((row, sel) => {
@@ -491,7 +575,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
491
575
  await materializeRow();
492
576
  const result = {};
493
577
  for (const [col, idx] of columnsToProcess) {
494
- const columnOverride = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col];
578
+ const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
495
579
  const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
496
580
  if (mapper) {
497
581
  const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
@@ -508,7 +592,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
508
592
  result[col] = snapshot.cells[idx].text;
509
593
  }
510
594
  }
511
- for (const [name, def] of Object.entries((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})) {
595
+ for (const [name, def] of Object.entries((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})) {
512
596
  if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
513
597
  continue;
514
598
  const snapshotRow = Object.create(smart);
@@ -607,7 +691,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
607
691
  // Re-pin to the correct logical row before reading this column (#366).
608
692
  const stableRow = await resolveStableRow();
609
693
  // Check if we have a column override for this column
610
- const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[col];
694
+ const columnOverride = (_d = config.columnOverrides) === null || _d === void 0 ? void 0 : _d[col];
611
695
  const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
612
696
  // --- Navigation Logic Start ---
613
697
  const cell = config.strategies.getCellLocator
@@ -634,7 +718,8 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
634
718
  index: idx,
635
719
  rowLocator: stableRow,
636
720
  rowIndex,
637
- barrier: smart._barrier
721
+ barrier: smart._barrier,
722
+ allowMissingCell: !!mapper,
638
723
  });
639
724
  if (navigatedCell) {
640
725
  targetCell = navigatedCell;
@@ -654,15 +739,29 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
654
739
  });
655
740
  }
656
741
  // 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;
742
+ const isCellLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isCellLoading;
743
+ const rawCellTimeout = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.cellLoadingTimeout;
659
744
  // undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
660
745
  const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
661
746
  ? rawCellTimeout
662
747
  : 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) {
748
+ const onCellLoadingTimeout = (_h = (_g = config.strategies.loading) === null || _g === void 0 ? void 0 : _g.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
749
+ if (isCellLoading) {
750
+ let loading = await isCellLoading(targetCell, col, smart);
751
+ // First-paint race: the cell node can attach a tick before the loading
752
+ // indicator mounts. A single false-negative then reads "" and skips
753
+ // onCellLoadingTimeout. Only grace when the cell still looks empty.
754
+ if (!loading && cellLoadingTimeout !== undefined && cellLoadingTimeout > 0) {
755
+ const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
756
+ if (peek === '') {
757
+ const graceDeadline = Date.now() + Math.min(250, cellLoadingTimeout);
758
+ while (!loading && Date.now() < graceDeadline) {
759
+ await page.waitForTimeout(25);
760
+ loading = await isCellLoading(targetCell, col, smart);
761
+ }
762
+ }
763
+ }
764
+ if (loading && cellLoadingTimeout !== undefined) {
666
765
  (0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
667
766
  const deadline = Date.now() + cellLoadingTimeout;
668
767
  let cellResolved = !(await isCellLoading(targetCell, col, smart)); // immediate check (handles timeout=0)
@@ -699,7 +798,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
699
798
  result[col] = (text || '').trim();
700
799
  }
701
800
  }
702
- for (const [name, def] of Object.entries((_h = config.syntheticColumns) !== null && _h !== void 0 ? _h : {})) {
801
+ for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
703
802
  if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
704
803
  continue;
705
804
  const snapshotRow = Object.create(smart);
@@ -775,6 +874,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
775
874
  (0, debugUtils_1.logDebug)(config, 'info', 'Row fill complete');
776
875
  };
777
876
  smart.bringIntoView = async () => {
877
+ var _a;
778
878
  if (rowIndex === undefined) {
779
879
  (0, debugUtils_1.logDebug)(config, 'info', 'bringIntoView called on a row with unknown rowIndex (e.g. from getRow()). ' +
780
880
  'Page navigation and scrolling will proceed, but virtual-scroll keyboard navigation will be skipped. ' +
@@ -786,7 +886,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
786
886
  const primitives = config.strategies.pagination;
787
887
  const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
788
888
  const getCurrent = () => parentTable.currentPageIndex;
789
- const setCurrent = (n) => { parentTable.currentPageIndex = n; };
889
+ const setCurrent = (n) => { (0, pageIndex_1.setCurrentPageIndex)(parentTable, n); };
790
890
  if (primitives.goToPage) {
791
891
  (0, debugUtils_1.logDebug)(config, 'info', `bringIntoView: Navigating to page ${tablePageIndex} (goToPage retry loop)`);
792
892
  await (0, paginationPath_1.executeNavigationWithGoToPageRetry)(tablePageIndex, primitives, context, getCurrent, setCurrent);
@@ -811,7 +911,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
811
911
  if (!ok)
812
912
  throw new Error(`bringIntoView: goNext failed before reaching page ${tablePageIndex}.`);
813
913
  }
814
- parentTable.currentPageIndex = tablePageIndex;
914
+ (0, pageIndex_1.setCurrentPageIndex)(parentTable, tablePageIndex);
815
915
  }
816
916
  else {
817
917
  (0, debugUtils_1.logDebug)(config, 'error', `Cannot bring row on page ${tablePageIndex} into view. No backwards pagination strategies (goToPage, goPrevious, goPreviousBulk, or goToFirst+goNext) provided.`);
@@ -821,8 +921,20 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
821
921
  }
822
922
  // Delay after pagination/finding before scrolling
823
923
  await (0, debugUtils_1.debugDelay)(config, 'findRow');
824
- // Scroll row into view using Playwright's built-in method
825
- await rowLocator.scrollIntoViewIfNeeded();
924
+ // Prefer viewport.scrollToRow when configured (#430). scrollIntoViewIfNeeded adjusts
925
+ // both axes and can evict sibling rows on recycling virtualizers.
926
+ const viewport = config.strategies.viewport;
927
+ const logicalRowIndex = (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && renderWindowPosition
928
+ ? (_a = (await (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, config, () => undefined))) === null || _a === void 0 ? void 0 : _a.index
929
+ : rowIndex;
930
+ if ((viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && typeof logicalRowIndex === 'number') {
931
+ const page = rootLocator.page();
932
+ (0, debugUtils_1.logDebug)(config, 'verbose', `bringIntoView: viewport.scrollToRow(${logicalRowIndex})`);
933
+ await viewport.scrollToRow({ root: rootLocator, config, page, resolve, getHeaders: parentTable === null || parentTable === void 0 ? void 0 : parentTable.getHeaders }, logicalRowIndex);
934
+ }
935
+ else {
936
+ await rowLocator.scrollIntoViewIfNeeded();
937
+ }
826
938
  };
827
939
  return smart;
828
940
  };
@@ -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
+ };
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContentReadyStrategies = void 0;
4
+ exports.ContentReadyStrategies = {
5
+ /**
6
+ * Polls the row's text content until two consecutive reads match.
7
+ */
8
+ textStable: (options = {}) => {
9
+ var _a, _b;
10
+ const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
11
+ const interval = (_b = options.interval) !== null && _b !== void 0 ? _b : 50;
12
+ return async (row, page) => {
13
+ const deadline = Date.now() + timeout;
14
+ const remaining = () => Math.max(0, deadline - Date.now());
15
+ let prev = await row.innerText({ timeout: remaining() || timeout });
16
+ while (remaining() > 0) {
17
+ await page.waitForTimeout(Math.min(interval, remaining()));
18
+ if (remaining() <= 0)
19
+ break;
20
+ const cur = await row.innerText({ timeout: remaining() });
21
+ if (cur === prev)
22
+ return;
23
+ prev = cur;
24
+ }
25
+ };
26
+ },
27
+ /**
28
+ * Uses MutationObserver to wait for DOM mutations on the row's subtree
29
+ * to settle. Resolves when no mutations occur for `quietPeriod` ms, or
30
+ * when `timeout` expires.
31
+ */
32
+ mutationSettled: (options = {}) => {
33
+ var _a, _b;
34
+ const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
35
+ const quietPeriod = (_b = options.quietPeriod) !== null && _b !== void 0 ? _b : 100;
36
+ return async (row) => {
37
+ await row.evaluate((el, opts) => {
38
+ return new Promise((resolve) => {
39
+ let quietTimer;
40
+ let deadlineTimer;
41
+ const done = () => {
42
+ clearTimeout(quietTimer);
43
+ clearTimeout(deadlineTimer);
44
+ observer.disconnect();
45
+ resolve();
46
+ };
47
+ const observer = new MutationObserver(() => {
48
+ clearTimeout(quietTimer);
49
+ quietTimer = setTimeout(done, opts.quietPeriod);
50
+ });
51
+ observer.observe(el, {
52
+ childList: true,
53
+ subtree: true,
54
+ characterData: true,
55
+ });
56
+ quietTimer = setTimeout(done, opts.quietPeriod);
57
+ deadlineTimer = setTimeout(done, opts.timeout);
58
+ });
59
+ }, { timeout, quietPeriod });
60
+ };
61
+ },
62
+ };
@@ -2,12 +2,8 @@ import type { FilterStrategy } from '../types';
2
2
  /**
3
3
  * Example filter strategies.
4
4
  * - default: small convenience wrapper that mirrors the engine's default behavior.
5
- * - spy(factory): returns a strategy that marks `calledRef.called = true` when invoked.
6
5
  */
7
6
  export declare const FilterStrategies: {
8
7
  default: FilterStrategy;
9
- spy: (calledRef?: {
10
- called?: boolean;
11
- }) => FilterStrategy;
12
8
  };
13
9
  export type { FilterStrategy } from '../types';
@@ -4,7 +4,6 @@ exports.FilterStrategies = void 0;
4
4
  /**
5
5
  * Example filter strategies.
6
6
  * - default: small convenience wrapper that mirrors the engine's default behavior.
7
- * - spy(factory): returns a strategy that marks `calledRef.called = true` when invoked.
8
7
  */
9
8
  exports.FilterStrategies = {
10
9
  default: {
@@ -20,19 +19,4 @@ exports.FilterStrategies = {
20
19
  return rows.filter({ has: targetCell.getByText(textVal, { exact: true }) });
21
20
  }
22
21
  },
23
- spy: (calledRef = {}) => ({
24
- apply({ rows, filter, colIndex, tableContext }) {
25
- calledRef.called = true;
26
- // Delegate to default behaviour for actual filtering
27
- const page = tableContext.page;
28
- const resolve = tableContext.resolve;
29
- const cellTemplate = resolve(tableContext.config.cellSelector, page);
30
- const targetCell = cellTemplate.nth(colIndex);
31
- if (typeof filter.value === 'function') {
32
- return rows.filter({ has: filter.value(targetCell) });
33
- }
34
- const textVal = typeof filter.value === 'number' ? String(filter.value) : filter.value;
35
- return rows.filter({ has: targetCell.getByText(textVal, { exact: true }) });
36
- }
37
- }),
38
22
  };
@@ -13,6 +13,11 @@ export declare const HeaderStrategies: {
13
13
  /**
14
14
  * Physically scrolls the table horizontally to force virtualized columns to mount,
15
15
  * collecting their names along the way.
16
+ *
17
+ * **Requires `selector`** — the CSS selector for the horizontal scroll container
18
+ * (e.g. `.dvn-scroller` for Glide, `.rdg-viewport` for RDG). Framework class names
19
+ * belong in presets / your config, not in this generic factory. Without `selector`,
20
+ * only currently visible headers are returned (no scrolling).
16
21
  */
17
22
  horizontalScroll: (options?: {
18
23
  limit?: number;
@@ -22,6 +22,11 @@ exports.HeaderStrategies = {
22
22
  /**
23
23
  * Physically scrolls the table horizontally to force virtualized columns to mount,
24
24
  * collecting their names along the way.
25
+ *
26
+ * **Requires `selector`** — the CSS selector for the horizontal scroll container
27
+ * (e.g. `.dvn-scroller` for Glide, `.rdg-viewport` for RDG). Framework class names
28
+ * belong in presets / your config, not in this generic factory. Without `selector`,
29
+ * only currently visible headers are returned (no scrolling).
25
30
  */
26
31
  horizontalScroll: (options) => {
27
32
  return async (context) => {
@@ -37,19 +42,22 @@ exports.HeaderStrategies = {
37
42
  };
38
43
  let currentHeaders = await getVisible();
39
44
  currentHeaders.forEach(h => collectedHeaders.add(h));
40
- const scrollerHandle = await root.evaluateHandle((el, selector) => {
41
- if (selector && el.matches(selector))
45
+ const selector = options === null || options === void 0 ? void 0 : options.selector;
46
+ if (!selector) {
47
+ (0, debugUtils_1.logDebug)(config, 'info', 'HeaderStrategies.horizontalScroll: no selector provided — returning visible headers only. Pass { selector } for the scroll container (e.g. ".dvn-scroller").');
48
+ return Array.from(collectedHeaders);
49
+ }
50
+ const scrollerHandle = await root.evaluateHandle((el, sel) => {
51
+ if (el.matches(sel))
42
52
  return el;
43
- // Try finding common scrollable containers or fallback to root
44
- const effectiveSelector = selector || '.dvn-scroller, .rdg-viewport, [role="grid"]';
45
- const ancestor = el.closest(effectiveSelector);
53
+ const ancestor = el.closest(sel);
46
54
  if (ancestor)
47
55
  return ancestor;
48
- const child = el.querySelector(effectiveSelector);
56
+ const child = el.querySelector(sel);
49
57
  if (child)
50
58
  return child;
51
- return el;
52
- }, options === null || options === void 0 ? void 0 : options.selector);
59
+ return null;
60
+ }, selector);
53
61
  const isScrollerFound = await scrollerHandle.evaluate(el => !!el);
54
62
  if (isScrollerFound) {
55
63
  await scrollerHandle.evaluate(el => el.scrollLeft = 0);
@@ -71,7 +79,7 @@ exports.HeaderStrategies = {
71
79
  }
72
80
  }
73
81
  else {
74
- (0, debugUtils_1.logDebug)(config, 'info', "HeaderStrategies.horizontalScroll: Could not find scroller. Returning visible headers.");
82
+ (0, debugUtils_1.logDebug)(config, 'info', `HeaderStrategies.horizontalScroll: Could not find scroller matching "${selector}". Returning visible headers.`);
75
83
  }
76
84
  if (isScrollerFound) {
77
85
  await scrollerHandle.evaluate(el => el.scrollLeft = 0);