@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
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
  },
@@ -301,7 +308,7 @@ const useTable = (rootLocator, configOptions = {}) => {
301
308
  return resolve(config.headerSelector, rootLocator).nth(idx);
302
309
  },
303
310
  countRows: async (filters, options) => {
304
- var _a, _b, _c, _d;
311
+ var _a, _b, _c, _d, _e;
305
312
  if (tableState.empty)
306
313
  return 0;
307
314
  await _autoInit();
@@ -359,6 +366,15 @@ const useTable = (rootLocator, configOptions = {}) => {
359
366
  }
360
367
  return count;
361
368
  };
369
+ // Wait for table to finish loading before counting
370
+ const isTableLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isTableLoading;
371
+ if (isTableLoading) {
372
+ const ctx = createStrategyContext();
373
+ while (await isTableLoading(ctx)) {
374
+ log('countRows: table is loading... waiting');
375
+ await rootLocator.page().waitForTimeout(200);
376
+ }
377
+ }
362
378
  if (!hasPagination) {
363
379
  log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
364
380
  if (!hasPostFilters)
@@ -372,19 +388,34 @@ const useTable = (rootLocator, configOptions = {}) => {
372
388
  let pagesScanned = 1;
373
389
  let paginationError;
374
390
  try {
375
- 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 }) => {
376
405
  const rowLocators = resolveRows();
377
406
  const newIndices = await tracker.getUnseenIndices(rowLocators);
378
407
  const candidates = await rowLocators.all();
379
408
  const matched = await countPostFilterMatches(candidates, newIndices);
380
409
  total += matched;
381
- log(`countRows: page ${pagesScanned} — ${matched} row(s) (running total: ${total})`);
382
- if (pagesScanned >= effectiveMaxPages)
383
- break;
384
- if (!await _advancePage(false))
385
- break;
386
- pagesScanned++;
387
- }
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;
388
419
  }
389
420
  catch (e) {
390
421
  paginationError = e;
@@ -428,6 +459,10 @@ const useTable = (rootLocator, configOptions = {}) => {
428
459
  return (text || '').trim();
429
460
  }, options);
430
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
+ */
431
466
  getColumnValues: async (columnName, options = {}) => {
432
467
  log(`getColumnValues: column="${columnName}" options=${safeStringify(options)}`);
433
468
  const values = await result.mapColumn(columnName, options);
@@ -463,6 +498,11 @@ const useTable = (rootLocator, configOptions = {}) => {
463
498
  throw new Error(`getRow() cannot filter by synthetic column(s): ${syntheticKeys.join(', ')}. ` +
464
499
  `Use findRow() instead — synthetic columns require async evaluation.`);
465
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
+ }
466
506
  const map = tableMapper.getMapSync();
467
507
  if (!map)
468
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.');
@@ -476,7 +516,7 @@ const useTable = (rootLocator, configOptions = {}) => {
476
516
  if (!map)
477
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.');
478
518
  const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
479
- return _makeSmart(rowLocator, map, index);
519
+ return _makeSmart(rowLocator, map, index, undefined, undefined, undefined, true);
480
520
  },
481
521
  findRowByIndex: async (index, options) => {
482
522
  var _a, _b;
@@ -597,43 +637,65 @@ const useTable = (rootLocator, configOptions = {}) => {
597
637
  }
598
638
  },
599
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
+ */
600
645
  [Symbol.asyncIterator]() {
601
646
  return __asyncGenerator(this, arguments, function* _a() {
602
- var _b;
603
647
  yield __await(_autoInit());
604
- const map = tableMapper.getMapSync();
605
- const effectiveMaxPages = config.maxPages;
606
- const tracker = new elementTracker_1.ElementTracker('iterator');
607
- const useBulk = false; // iterator has no options; default goNext
608
- 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(); });
609
681
  try {
610
- let rowIndex = 0;
611
- let pagesScanned = 1;
612
- while (true) {
613
- const rowLocators = resolve(config.rowSelector, rootLocator);
614
- const newIndices = yield __await(tracker.getUnseenIndices(rowLocators));
615
- const pageRows = yield __await(rowLocators.all());
616
- const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
617
- for (const idx of newIndices) {
618
- // index = visit-order counter; rowIndex = logical/data index (B-hybrid, #362).
619
- const resolved = yield __await((0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], config, () => rowIndex));
620
- const logical = (_b = resolved === null || resolved === void 0 ? void 0 : resolved.index) !== null && _b !== void 0 ? _b : rowIndex;
621
- const sr = _makeSmart(pageRows[idx], map, logical, tableState.currentPageIndex, barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
622
- // Iterator rows share one per-page DOM snapshot (positional locators); disable
623
- // toJSON's scroll-back recovery so reading one row can't invalidate the others (#366).
624
- sr._inBatch = true;
625
- yield yield __await({ row: sr, index: rowIndex, rowIndex: logical, pageIndex: tableState.currentPageIndex });
626
- 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;
627
690
  }
628
- if (pagesScanned >= effectiveMaxPages)
629
- break;
630
- if (!(yield __await(_advancePage(useBulk))))
631
- break;
632
- pagesScanned++;
691
+ if (runError)
692
+ throw runError;
633
693
  }
634
694
  }
635
695
  finally {
636
- yield __await(tracker.cleanup(rootLocator.page()));
696
+ cancelled = true;
697
+ release === null || release === void 0 ? void 0 : release();
698
+ yield __await(runPromise.catch(() => { }));
637
699
  }
638
700
  });
639
701
  },
@@ -18,7 +18,8 @@ class ElementTracker {
18
18
  const seenMap = win[trackerId];
19
19
  const newIndices = [];
20
20
  elements.forEach((el, index) => {
21
- const signature = el.textContent || '';
21
+ const htmlEl = el;
22
+ const signature = htmlEl.offsetTop + '|' + (el.textContent || '');
22
23
  if (seenMap.get(el) !== signature) {
23
24
  newIndices.push(index);
24
25
  }
@@ -39,8 +40,10 @@ class ElementTracker {
39
40
  const seenMap = win[trackerId];
40
41
  for (const index of indicesToCommit) {
41
42
  const el = elements[index];
42
- if (el)
43
- seenMap.set(el, el.textContent || '');
43
+ if (el) {
44
+ const htmlEl = el;
45
+ seenMap.set(el, htmlEl.offsetTop + '|' + (el.textContent || ''));
46
+ }
44
47
  }
45
48
  }, [this.id, indices]);
46
49
  }
@@ -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",
3
+ "version": "6.21.0",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",