@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.
- package/dist/engine/rowFinder.d.ts +1 -0
- package/dist/engine/rowFinder.js +75 -75
- package/dist/engine/scanPages.d.ts +58 -0
- package/dist/engine/scanPages.js +46 -0
- package/dist/engine/tableIteration.d.ts +1 -0
- package/dist/engine/tableIteration.js +38 -20
- package/dist/filterEngine.d.ts +4 -0
- package/dist/filterEngine.js +15 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +51 -10
- package/dist/plugins/index.js +14 -6
- package/dist/presets/glide/index.d.ts +1 -1
- package/dist/presets/glide/index.js +2 -2
- package/dist/presets/rdg.d.ts +1 -1
- package/dist/presets/rdg.js +4 -4
- package/dist/smartRow.d.ts +2 -2
- package/dist/smartRow.js +160 -49
- package/dist/strategies/columns.d.ts +5 -9
- package/dist/strategies/columns.js +0 -10
- package/dist/strategies/contentReady.d.ts +20 -0
- package/dist/strategies/contentReady.js +62 -0
- package/dist/strategies/filter.d.ts +0 -4
- package/dist/strategies/filter.js +0 -16
- package/dist/strategies/headers.d.ts +5 -0
- package/dist/strategies/headers.js +17 -9
- package/dist/strategies/index.d.ts +23 -15
- package/dist/strategies/index.js +9 -5
- package/dist/strategies/pagination.js +11 -2
- package/dist/strategies/viewport.d.ts +4 -1
- package/dist/strategies/viewport.js +62 -32
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +45 -1
- package/dist/types.d.ts +43 -1
- package/dist/useTable.js +105 -43
- package/dist/utils/elementTracker.js +6 -3
- package/dist/utils/resolveCellLocator.d.ts +35 -0
- package/dist/utils/resolveCellLocator.js +52 -0
- 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
|
-
//
|
|
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
|
-
|
|
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 ${
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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 (
|
|
629
|
-
|
|
630
|
-
if (!(yield __await(_advancePage(useBulk))))
|
|
631
|
-
break;
|
|
632
|
-
pagesScanned++;
|
|
691
|
+
if (runError)
|
|
692
|
+
throw runError;
|
|
633
693
|
}
|
|
634
694
|
}
|
|
635
695
|
finally {
|
|
636
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|