@rickcedwhat/playwright-smart-table 6.20.1-next.720a2e5 → 6.20.1-next.a5f5df2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -269,6 +269,9 @@ export type SmartRow<T = any> = Locator & {
269
269
  * Scrolls/paginates to bring this row into view.
270
270
  * Works when row position metadata is known (e.g., from getRowByIndex, findRow,
271
271
  * findRows, filter, or async iteration).
272
+ *
273
+ * Prefers `strategies.viewport.scrollToRow` when configured; otherwise falls back to
274
+ * Playwright `scrollIntoViewIfNeeded()`.
272
275
  * @throws Error if row position metadata is unknown
273
276
  */
274
277
  bringIntoView(): Promise<void>;
@@ -292,6 +295,12 @@ export type SmartRow<T = any> = Locator & {
292
295
  smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
293
296
  /**
294
297
  * Get the resolved value of any column — real, override, or synthetic.
298
+ *
299
+ * When `strategies.viewport` or `strategies.navigation` is configured, runs the same
300
+ * cell-navigation pipeline as `toJSON` / `getCell().bringIntoView()` so off-screen
301
+ * virtualized cells are mounted before reading. Without those strategies, reads the
302
+ * current DOM cell (Playwright auto-wait).
303
+ *
295
304
  * @param column - Column name (case-sensitive)
296
305
  * @returns The column value as a string
297
306
  */
@@ -403,6 +412,7 @@ export interface PaginationPrimitives {
403
412
  }
404
413
  export type PaginationStrategy = PaginationPrimitives;
405
414
  export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
415
+ export type ContentReadyStrategy = (row: Locator, page: Page) => Promise<void>;
406
416
  export type FillStrategy = (options: {
407
417
  row: SmartRow;
408
418
  columnName: string;
@@ -467,6 +477,7 @@ export interface ColumnOverride<TValue = any> {
467
477
  import { HeaderStrategy } from './strategies/headers';
468
478
  export type { HeaderStrategy } from './strategies/headers';
469
479
  import { NavigationPrimitives } from './strategies/columns';
480
+ export type { NavigationPrimitives, CellNavigationStrategy } from './strategies/columns';
470
481
  /**
471
482
  * Strategy to resolve column names (string or regex) to their index.
472
483
  */
@@ -597,6 +608,23 @@ export interface TableStrategies {
597
608
  * }
598
609
  */
599
610
  resolveRowIndex?: (row: Locator) => Promise<RowIndexResult | undefined>;
611
+ /**
612
+ * Waits until a row's content has stabilized before cloning in
613
+ * `toJSON({ atomic: true })`. Needed for recycling virtualizers where the
614
+ * framework updates element positions synchronously but renders cell content
615
+ * asynchronously (e.g. react-window, react-virtuoso with React concurrent mode).
616
+ *
617
+ * Without this, the clone captures stale content from the previous occupant of the
618
+ * DOM slot — the element is at the correct position but React hasn't re-rendered yet.
619
+ *
620
+ * Use `Strategies.ContentReady.textStable()` for the built-in text-polling strategy.
621
+ *
622
+ * @example
623
+ * strategies: {
624
+ * contentReady: Strategies.ContentReady.textStable({ timeout: 500 }),
625
+ * }
626
+ */
627
+ contentReady?: ContentReadyStrategy;
600
628
  /**
601
629
  * Viewport oracle strategies for 2D virtualized tables (e.g. MUI DataGrid, AG Grid,
602
630
  * Braintrust-style grids where both rows and columns are virtualized simultaneously).
@@ -657,6 +685,9 @@ export interface TableConfig<T = any> {
657
685
  */
658
686
  emptyState?: Locator;
659
687
  }
688
+ /**
689
+ * @internal Resolved config after defaults are applied. Prefer {@link TableConfig} in public code.
690
+ */
660
691
  export interface FinalTableConfig<T = any> extends TableConfig<T> {
661
692
  headerSelector: string | ((root: Locator) => Locator);
662
693
  rowSelector: string;
@@ -725,6 +756,11 @@ export type RowIterationOptions = {
725
756
  */
726
757
  useBulkPagination?: boolean;
727
758
  };
759
+ /**
760
+ * Result of {@link useTable}. Implements `AsyncIterable` — `for await (const { row } of table)`
761
+ * uses the same page-walk as `map` / `forEach` (`scanPages` + runMap): overscan, loading,
762
+ * dedupe, and EOF final scan (#427).
763
+ */
728
764
  export interface TableResult<T = any> extends AsyncIterable<{
729
765
  row: SmartRow<T>;
730
766
  rowIndex: number;
@@ -762,6 +798,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
762
798
  * @note The sync path cannot compute a real `rowIndex`, so the returned SmartRow's
763
799
  * `rowIndex` is `undefined` (virtual-scroll positioning via `bringIntoView()` is limited).
764
800
  * Use `findRow()` (async) when you need a row with an accurate `rowIndex`.
801
+ * @note Cannot filter by `syntheticColumns` or `columnOverrides.read` keys — those need
802
+ * async evaluation via `findRow()` / `findRows()`. DOM filters use `strategies.getCellLocator`
803
+ * when configured (column-virtualized grids), otherwise `cellSelector` + column index.
765
804
  */
766
805
  getRow: (filters: Record<string, FilterValue>, options?: {
767
806
  exact?: boolean;
@@ -823,7 +862,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
823
862
  useBulkPagination?: boolean;
824
863
  }) => Promise<SmartRowArray<T>>;
825
864
  /**
826
- * Navigates to a specific column using the configured CellNavigationStrategy.
865
+ * Resolves the named column and scrolls it into view.
866
+ * Prefers `strategies.viewport.scrollToColumn` when configured; otherwise scrolls the
867
+ * header cell via Playwright `scrollIntoViewIfNeeded()`.
827
868
  */
828
869
  scrollToColumn: (columnName: string) => Promise<void>;
829
870
  /**
@@ -843,6 +884,7 @@ export interface TableResult<T = any> extends AsyncIterable<{
843
884
  */
844
885
  mapColumn<R = string>(columnName: string, options?: RowIterationOptions): Promise<R[]>;
845
886
  /**
887
+ * @deprecated Use `mapColumn` (or `map`) instead. Will be removed in v7.0.0.
846
888
  * Iterates over rows and extracts the value of a single column as strings.
847
889
  * @param columnName - The name of the column to extract
848
890
  * @param options - Iteration options
package/dist/useTable.js CHANGED
@@ -28,10 +28,10 @@ const tableMapper_1 = require("./engine/tableMapper");
28
28
  const rowFinder_1 = require("./engine/rowFinder");
29
29
  const tableIteration_1 = require("./engine/tableIteration");
30
30
  const rowResolution_1 = require("./engine/rowResolution");
31
+ const scanPages_1 = require("./engine/scanPages");
31
32
  const debugUtils_1 = require("./utils/debugUtils");
32
33
  const smartRowArray_1 = require("./utils/smartRowArray");
33
34
  const elementTracker_1 = require("./utils/elementTracker");
34
- const navigationBarrier_1 = require("./utils/navigationBarrier");
35
35
  // Helper to safely serialize objects containing functions for logging
36
36
  const safeStringify = (obj) => {
37
37
  try {
@@ -145,11 +145,11 @@ const useTable = (rootLocator, configOptions = {}) => {
145
145
  // Placeholder for the final table object
146
146
  let finalTable = null;
147
147
  // Helper factory
148
- const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier, rowSelector) => {
148
+ const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier, rowSelector, renderWindowPosition = false) => {
149
149
  const effectiveLocator = rowSelector
150
150
  ? rootLocator.locator(rowSelector)
151
151
  : rowLocator;
152
- const sr = (0, smartRow_1.default)(effectiveLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
152
+ const sr = (0, smartRow_1.default)(effectiveLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier, renderWindowPosition);
153
153
  if (rowSelector)
154
154
  sr._selfHealing = true;
155
155
  return sr;
@@ -277,12 +277,19 @@ const useTable = (rootLocator, configOptions = {}) => {
277
277
  return result;
278
278
  },
279
279
  scrollToColumn: async (columnName) => {
280
+ var _a;
280
281
  log(`scrollToColumn: column="${columnName}"`);
281
282
  const map = await tableMapper.getMap();
282
283
  const idx = map.get(columnName);
283
284
  if (idx === undefined)
284
285
  throw _createColumnError(columnName, map);
285
- // Use header cell for scrolling
286
+ // Prefer viewport strategy when configured (#430). Raw scrollIntoViewIfNeeded on a
287
+ // header can shift Y and evict virtualized rows; viewport.scrollToColumn is X-aware.
288
+ const viewportScroll = (_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToColumn;
289
+ if (viewportScroll) {
290
+ await viewportScroll(createStrategyContext(), idx);
291
+ return;
292
+ }
286
293
  const headerCell = resolve(config.headerSelector, rootLocator).nth(idx);
287
294
  await headerCell.scrollIntoViewIfNeeded();
288
295
  },
@@ -381,19 +388,34 @@ const useTable = (rootLocator, configOptions = {}) => {
381
388
  let pagesScanned = 1;
382
389
  let paginationError;
383
390
  try {
384
- while (true) {
391
+ const scanned = await (0, scanPages_1.scanPages)({
392
+ advancePage: _advancePage,
393
+ getCurrentPageIndex: () => tableState.currentPageIndex,
394
+ config,
395
+ waitForReady: async () => {
396
+ if (!isTableLoading)
397
+ return;
398
+ const ctx = createStrategyContext();
399
+ while (await isTableLoading(ctx)) {
400
+ log('countRows: table is loading... waiting');
401
+ await rootLocator.page().waitForTimeout(200);
402
+ }
403
+ },
404
+ }, async ({ pagesScanned: pageNum }) => {
385
405
  const rowLocators = resolveRows();
386
406
  const newIndices = await tracker.getUnseenIndices(rowLocators);
387
407
  const candidates = await rowLocators.all();
388
408
  const matched = await countPostFilterMatches(candidates, newIndices);
389
409
  total += matched;
390
- log(`countRows: page ${pagesScanned} — ${matched} row(s) (running total: ${total})`);
391
- if (pagesScanned >= effectiveMaxPages)
392
- break;
393
- if (!await _advancePage(false))
394
- break;
395
- pagesScanned++;
396
- }
410
+ log(`countRows: page ${pageNum} — ${matched} row(s) (running total: ${total})`);
411
+ return 'continue';
412
+ }, {
413
+ maxPages: effectiveMaxPages,
414
+ useBulk: false,
415
+ label: 'countRows',
416
+ finalScanOnEof: true,
417
+ });
418
+ pagesScanned = scanned.pagesScanned;
397
419
  }
398
420
  catch (e) {
399
421
  paginationError = e;
@@ -437,6 +459,10 @@ const useTable = (rootLocator, configOptions = {}) => {
437
459
  return (text || '').trim();
438
460
  }, options);
439
461
  },
462
+ /**
463
+ * @deprecated Use `mapColumn` (or `map`) instead. Will be removed in v7.0.0.
464
+ * Iterates over rows and extracts the value of a single column as strings.
465
+ */
440
466
  getColumnValues: async (columnName, options = {}) => {
441
467
  log(`getColumnValues: column="${columnName}" options=${safeStringify(options)}`);
442
468
  const values = await result.mapColumn(columnName, options);
@@ -472,6 +498,11 @@ const useTable = (rootLocator, configOptions = {}) => {
472
498
  throw new Error(`getRow() cannot filter by synthetic column(s): ${syntheticKeys.join(', ')}. ` +
473
499
  `Use findRow() instead — synthetic columns require async evaluation.`);
474
500
  }
501
+ const overrideKeys = Object.keys(filters).filter(k => { var _a, _b; return (_b = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[k]) === null || _b === void 0 ? void 0 : _b.read; });
502
+ if (overrideKeys.length > 0) {
503
+ throw new Error(`getRow() cannot filter by columnOverrides.read column(s): ${overrideKeys.join(', ')}. ` +
504
+ `Use findRow() instead — override columns require async evaluation.`);
505
+ }
475
506
  const map = tableMapper.getMapSync();
476
507
  if (!map)
477
508
  throw new Error('Initialization Error: You attempted to access a row before the table structure was mapped. Please call "await table.init()" once before using synchronous row access.');
@@ -485,7 +516,7 @@ const useTable = (rootLocator, configOptions = {}) => {
485
516
  if (!map)
486
517
  throw new Error('Initialization Error: You attempted to access a row before the table structure was mapped. Please call "await table.init()" once before using synchronous row access.');
487
518
  const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
488
- return _makeSmart(rowLocator, map, index);
519
+ return _makeSmart(rowLocator, map, index, undefined, undefined, undefined, true);
489
520
  },
490
521
  findRowByIndex: async (index, options) => {
491
522
  var _a, _b;
@@ -606,43 +637,65 @@ const useTable = (rootLocator, configOptions = {}) => {
606
637
  }
607
638
  },
608
639
  // ─── Shared async row iterator ───────────────────────────────────────────
640
+ /**
641
+ * Streams rows using the same page-walk + collection path as `map` / `forEach`
642
+ * (`scanPages` + runMap): overscan filtering, loading-before-dedupe, EOF final
643
+ * scan, and config dedupe. Concurrency is sequential so yields stay ordered. (#427)
644
+ */
609
645
  [Symbol.asyncIterator]() {
610
646
  return __asyncGenerator(this, arguments, function* _a() {
611
- var _b;
612
647
  yield __await(_autoInit());
613
- const map = tableMapper.getMapSync();
614
- const effectiveMaxPages = config.maxPages;
615
- const tracker = new elementTracker_1.ElementTracker('iterator');
616
- const useBulk = false; // iterator has no options; default goNext
617
- log(`iterator: starting (maxPages=${effectiveMaxPages})`);
648
+ log(`iterator: starting via runMap (maxPages=${config.maxPages})`);
649
+ const queue = [];
650
+ let notify;
651
+ let release;
652
+ let finished = false;
653
+ let cancelled = false;
654
+ let runError;
655
+ const wake = () => {
656
+ notify === null || notify === void 0 ? void 0 : notify();
657
+ notify = undefined;
658
+ };
659
+ const runPromise = (0, tableIteration_1.runMap)({
660
+ getRowLocators: () => resolve(config.rowSelector, rootLocator),
661
+ getMap: () => tableMapper.getMapSync(),
662
+ advancePage: _advancePage,
663
+ makeSmartRow: (loc, map, idx, pageIdx, barrier, sel) => _makeSmart(loc, map, idx, pageIdx, barrier, sel),
664
+ createSmartRowArray: smartRowArray_1.createSmartRowArray,
665
+ config,
666
+ getPage: () => rootLocator.page(),
667
+ getCurrentPageIndex: () => tableState.currentPageIndex,
668
+ getContext: () => createStrategyContext(),
669
+ }, async (ctx) => {
670
+ queue.push({
671
+ row: ctx.row,
672
+ index: ctx.index,
673
+ rowIndex: ctx.rowIndex,
674
+ pageIndex: ctx.pageIndex,
675
+ });
676
+ wake();
677
+ await new Promise((resolve) => { release = resolve; });
678
+ if (cancelled)
679
+ ctx.stop();
680
+ }, { concurrency: 'sequential' }, 'iterator').then(() => { finished = true; wake(); }, (err) => { runError = err; finished = true; wake(); });
618
681
  try {
619
- let rowIndex = 0;
620
- let pagesScanned = 1;
621
- while (true) {
622
- const rowLocators = resolve(config.rowSelector, rootLocator);
623
- const newIndices = yield __await(tracker.getUnseenIndices(rowLocators));
624
- const pageRows = yield __await(rowLocators.all());
625
- const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
626
- for (const idx of newIndices) {
627
- // index = visit-order counter; rowIndex = logical/data index (B-hybrid, #362).
628
- const resolved = yield __await((0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], config, () => rowIndex));
629
- const logical = (_b = resolved === null || resolved === void 0 ? void 0 : resolved.index) !== null && _b !== void 0 ? _b : rowIndex;
630
- const sr = _makeSmart(pageRows[idx], map, logical, tableState.currentPageIndex, barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
631
- // Iterator rows share one per-page DOM snapshot (positional locators); disable
632
- // toJSON's scroll-back recovery so reading one row can't invalidate the others (#366).
633
- sr._inBatch = true;
634
- yield yield __await({ row: sr, index: rowIndex, rowIndex: logical, pageIndex: tableState.currentPageIndex });
635
- rowIndex++;
682
+ while (!finished || queue.length > 0) {
683
+ if (queue.length === 0) {
684
+ yield __await(new Promise((resolve) => { notify = resolve; }));
685
+ }
686
+ while (queue.length > 0) {
687
+ yield yield __await(queue.shift());
688
+ release === null || release === void 0 ? void 0 : release();
689
+ release = undefined;
636
690
  }
637
- if (pagesScanned >= effectiveMaxPages)
638
- break;
639
- if (!(yield __await(_advancePage(useBulk))))
640
- break;
641
- pagesScanned++;
691
+ if (runError)
692
+ throw runError;
642
693
  }
643
694
  }
644
695
  finally {
645
- yield __await(tracker.cleanup(rootLocator.page()));
696
+ cancelled = true;
697
+ release === null || release === void 0 ? void 0 : release();
698
+ yield __await(runPromise.catch(() => { }));
646
699
  }
647
700
  });
648
701
  },
@@ -0,0 +1,35 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import type { FinalTableConfig, Selector } from '../types';
3
+ /**
4
+ * Resolve a cell locator for a concrete row — prefers `strategies.getCellLocator`,
5
+ * otherwise `cellSelector` + `.nth(columnIndex)`.
6
+ */
7
+ export declare function resolveCellLocator(args: {
8
+ config: FinalTableConfig;
9
+ resolve: (selector: Selector, parent: Locator | Page) => Locator;
10
+ row: Locator;
11
+ root: Locator;
12
+ columnName: string;
13
+ columnIndex: number;
14
+ rowIndex?: number;
15
+ page?: Page;
16
+ }): Locator;
17
+ /**
18
+ * Cell locator for Playwright `rows.filter({ has })`.
19
+ *
20
+ * When `getCellLocator` is set, pass `page.locator(':scope')` as the row so the
21
+ * strategy's `row.locator(...)` chain stays relative to each candidate row under
22
+ * `filter({ has })`. Using the rows collection or a document-rooted parent nests
23
+ * the row selector and matches nothing.
24
+ *
25
+ * Without `getCellLocator`, keeps the historical page-scoped `cellSelector.nth(i)`
26
+ * template that Playwright re-bases into each row.
27
+ */
28
+ export declare function resolveCellLocatorForFilter(args: {
29
+ config: FinalTableConfig;
30
+ resolve: (selector: Selector, parent: Locator | Page) => Locator;
31
+ page: Page;
32
+ root?: Locator;
33
+ columnName: string;
34
+ columnIndex: number;
35
+ }): Locator;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCellLocator = resolveCellLocator;
4
+ exports.resolveCellLocatorForFilter = resolveCellLocatorForFilter;
5
+ /**
6
+ * Resolve a cell locator for a concrete row — prefers `strategies.getCellLocator`,
7
+ * otherwise `cellSelector` + `.nth(columnIndex)`.
8
+ */
9
+ function resolveCellLocator(args) {
10
+ var _a, _b;
11
+ const { config, resolve, row, root, columnName, columnIndex, rowIndex } = args;
12
+ const page = (_a = args.page) !== null && _a !== void 0 ? _a : root.page();
13
+ if ((_b = config.strategies) === null || _b === void 0 ? void 0 : _b.getCellLocator) {
14
+ return config.strategies.getCellLocator({
15
+ row,
16
+ root,
17
+ columnName,
18
+ columnIndex,
19
+ rowIndex,
20
+ page,
21
+ config,
22
+ });
23
+ }
24
+ return resolve(config.cellSelector, row).nth(columnIndex);
25
+ }
26
+ /**
27
+ * Cell locator for Playwright `rows.filter({ has })`.
28
+ *
29
+ * When `getCellLocator` is set, pass `page.locator(':scope')` as the row so the
30
+ * strategy's `row.locator(...)` chain stays relative to each candidate row under
31
+ * `filter({ has })`. Using the rows collection or a document-rooted parent nests
32
+ * the row selector and matches nothing.
33
+ *
34
+ * Without `getCellLocator`, keeps the historical page-scoped `cellSelector.nth(i)`
35
+ * template that Playwright re-bases into each row.
36
+ */
37
+ function resolveCellLocatorForFilter(args) {
38
+ var _a;
39
+ const { config, resolve, page, root, columnName, columnIndex } = args;
40
+ if ((_a = config.strategies) === null || _a === void 0 ? void 0 : _a.getCellLocator) {
41
+ const scopeRow = page.locator(':scope');
42
+ return config.strategies.getCellLocator({
43
+ row: scopeRow,
44
+ root: root !== null && root !== void 0 ? root : scopeRow,
45
+ columnName,
46
+ columnIndex,
47
+ page,
48
+ config,
49
+ });
50
+ }
51
+ return resolve(config.cellSelector, page).nth(columnIndex);
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "6.20.1-next.720a2e5",
3
+ "version": "6.20.1-next.a5f5df2",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",