@rickcedwhat/playwright-smart-table 6.20.0 → 6.20.1-next.1d11eb1

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/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 {
@@ -301,7 +301,7 @@ const useTable = (rootLocator, configOptions = {}) => {
301
301
  return resolve(config.headerSelector, rootLocator).nth(idx);
302
302
  },
303
303
  countRows: async (filters, options) => {
304
- var _a, _b, _c, _d;
304
+ var _a, _b, _c, _d, _e;
305
305
  if (tableState.empty)
306
306
  return 0;
307
307
  await _autoInit();
@@ -359,6 +359,15 @@ const useTable = (rootLocator, configOptions = {}) => {
359
359
  }
360
360
  return count;
361
361
  };
362
+ // Wait for table to finish loading before counting
363
+ const isTableLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isTableLoading;
364
+ if (isTableLoading) {
365
+ const ctx = createStrategyContext();
366
+ while (await isTableLoading(ctx)) {
367
+ log('countRows: table is loading... waiting');
368
+ await rootLocator.page().waitForTimeout(200);
369
+ }
370
+ }
362
371
  if (!hasPagination) {
363
372
  log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
364
373
  if (!hasPostFilters)
@@ -372,19 +381,34 @@ const useTable = (rootLocator, configOptions = {}) => {
372
381
  let pagesScanned = 1;
373
382
  let paginationError;
374
383
  try {
375
- while (true) {
384
+ const scanned = await (0, scanPages_1.scanPages)({
385
+ advancePage: _advancePage,
386
+ getCurrentPageIndex: () => tableState.currentPageIndex,
387
+ config,
388
+ waitForReady: async () => {
389
+ if (!isTableLoading)
390
+ return;
391
+ const ctx = createStrategyContext();
392
+ while (await isTableLoading(ctx)) {
393
+ log('countRows: table is loading... waiting');
394
+ await rootLocator.page().waitForTimeout(200);
395
+ }
396
+ },
397
+ }, async ({ pagesScanned: pageNum }) => {
376
398
  const rowLocators = resolveRows();
377
399
  const newIndices = await tracker.getUnseenIndices(rowLocators);
378
400
  const candidates = await rowLocators.all();
379
401
  const matched = await countPostFilterMatches(candidates, newIndices);
380
402
  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
- }
403
+ log(`countRows: page ${pageNum} — ${matched} row(s) (running total: ${total})`);
404
+ return 'continue';
405
+ }, {
406
+ maxPages: effectiveMaxPages,
407
+ useBulk: false,
408
+ label: 'countRows',
409
+ finalScanOnEof: true,
410
+ });
411
+ pagesScanned = scanned.pagesScanned;
388
412
  }
389
413
  catch (e) {
390
414
  paginationError = e;
@@ -428,6 +452,10 @@ const useTable = (rootLocator, configOptions = {}) => {
428
452
  return (text || '').trim();
429
453
  }, options);
430
454
  },
455
+ /**
456
+ * @deprecated Use `mapColumn` (or `map`) instead. Will be removed in v7.0.0.
457
+ * Iterates over rows and extracts the value of a single column as strings.
458
+ */
431
459
  getColumnValues: async (columnName, options = {}) => {
432
460
  log(`getColumnValues: column="${columnName}" options=${safeStringify(options)}`);
433
461
  const values = await result.mapColumn(columnName, options);
@@ -463,6 +491,11 @@ const useTable = (rootLocator, configOptions = {}) => {
463
491
  throw new Error(`getRow() cannot filter by synthetic column(s): ${syntheticKeys.join(', ')}. ` +
464
492
  `Use findRow() instead — synthetic columns require async evaluation.`);
465
493
  }
494
+ 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; });
495
+ if (overrideKeys.length > 0) {
496
+ throw new Error(`getRow() cannot filter by columnOverrides.read column(s): ${overrideKeys.join(', ')}. ` +
497
+ `Use findRow() instead — override columns require async evaluation.`);
498
+ }
466
499
  const map = tableMapper.getMapSync();
467
500
  if (!map)
468
501
  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.');
@@ -597,43 +630,65 @@ const useTable = (rootLocator, configOptions = {}) => {
597
630
  }
598
631
  },
599
632
  // ─── Shared async row iterator ───────────────────────────────────────────
633
+ /**
634
+ * Streams rows using the same page-walk + collection path as `map` / `forEach`
635
+ * (`scanPages` + runMap): overscan filtering, loading-before-dedupe, EOF final
636
+ * scan, and config dedupe. Concurrency is sequential so yields stay ordered. (#427)
637
+ */
600
638
  [Symbol.asyncIterator]() {
601
639
  return __asyncGenerator(this, arguments, function* _a() {
602
- var _b;
603
640
  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})`);
641
+ log(`iterator: starting via runMap (maxPages=${config.maxPages})`);
642
+ const queue = [];
643
+ let notify;
644
+ let release;
645
+ let finished = false;
646
+ let cancelled = false;
647
+ let runError;
648
+ const wake = () => {
649
+ notify === null || notify === void 0 ? void 0 : notify();
650
+ notify = undefined;
651
+ };
652
+ const runPromise = (0, tableIteration_1.runMap)({
653
+ getRowLocators: () => resolve(config.rowSelector, rootLocator),
654
+ getMap: () => tableMapper.getMapSync(),
655
+ advancePage: _advancePage,
656
+ makeSmartRow: (loc, map, idx, pageIdx, barrier, sel) => _makeSmart(loc, map, idx, pageIdx, barrier, sel),
657
+ createSmartRowArray: smartRowArray_1.createSmartRowArray,
658
+ config,
659
+ getPage: () => rootLocator.page(),
660
+ getCurrentPageIndex: () => tableState.currentPageIndex,
661
+ getContext: () => createStrategyContext(),
662
+ }, async (ctx) => {
663
+ queue.push({
664
+ row: ctx.row,
665
+ index: ctx.index,
666
+ rowIndex: ctx.rowIndex,
667
+ pageIndex: ctx.pageIndex,
668
+ });
669
+ wake();
670
+ await new Promise((resolve) => { release = resolve; });
671
+ if (cancelled)
672
+ ctx.stop();
673
+ }, { concurrency: 'sequential' }, 'iterator').then(() => { finished = true; wake(); }, (err) => { runError = err; finished = true; wake(); });
609
674
  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++;
675
+ while (!finished || queue.length > 0) {
676
+ if (queue.length === 0) {
677
+ yield __await(new Promise((resolve) => { notify = resolve; }));
678
+ }
679
+ while (queue.length > 0) {
680
+ yield yield __await(queue.shift());
681
+ release === null || release === void 0 ? void 0 : release();
682
+ release = undefined;
627
683
  }
628
- if (pagesScanned >= effectiveMaxPages)
629
- break;
630
- if (!(yield __await(_advancePage(useBulk))))
631
- break;
632
- pagesScanned++;
684
+ if (runError)
685
+ throw runError;
633
686
  }
634
687
  }
635
688
  finally {
636
- yield __await(tracker.cleanup(rootLocator.page()));
689
+ cancelled = true;
690
+ release === null || release === void 0 ? void 0 : release();
691
+ yield __await(runPromise.catch(() => { }));
637
692
  }
638
693
  });
639
694
  },
@@ -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.0",
3
+ "version": "6.20.1-next.1d11eb1",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",
@@ -110,6 +110,11 @@
110
110
  "peerDependencies": {
111
111
  "@playwright/test": "^1.40.0"
112
112
  },
113
+ "peerDependenciesMeta": {
114
+ "@playwright/test": {
115
+ "optional": true
116
+ }
117
+ },
113
118
  "devDependencies": {
114
119
  "@commitlint/cli": "^21.0.0",
115
120
  "@commitlint/config-conventional": "^21.0.0",