@rickcedwhat/playwright-smart-table 6.25.0-next.c81cb3d → 6.25.0-next.d8860d0
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/cellNavigation.js +7 -4
- package/dist/engine/countRows.d.ts +1 -0
- package/dist/engine/countRows.js +18 -8
- package/dist/engine/rowByIndex.d.ts +1 -0
- package/dist/engine/rowByIndex.js +12 -3
- package/dist/engine/rowFinder.d.ts +3 -1
- package/dist/engine/rowFinder.js +5 -7
- package/dist/engine/rowResolution.d.ts +2 -2
- package/dist/engine/rowResolution.js +11 -7
- package/dist/engine/rowSnapshot.js +26 -11
- package/dist/engine/scanPages.d.ts +5 -0
- package/dist/engine/scanPages.js +11 -0
- package/dist/engine/tableIteration.js +1 -0
- package/dist/engine/tableMapper.js +16 -5
- package/dist/index.d.ts +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/strategies/contentReady.d.ts +19 -1
- package/dist/strategies/contentReady.js +81 -0
- package/dist/strategies/dedupe.d.ts +5 -2
- package/dist/strategies/dedupe.js +5 -2
- package/dist/strategies/index.d.ts +5 -0
- package/dist/types.d.ts +36 -8
- package/dist/useTable.js +0 -50
- package/dist/utils/deadline.d.ts +2 -0
- package/dist/utils/deadline.js +19 -0
- package/dist/utils/memoizeViewport.d.ts +6 -0
- package/dist/utils/memoizeViewport.js +28 -0
- package/dist/utils/smartRowArray.d.ts +1 -1
- package/dist/utils/smartRowArray.js +4 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.navigateToCell = void 0;
|
|
4
4
|
const rowResolution_1 = require("./rowResolution");
|
|
5
5
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
6
|
+
const memoizeViewport_1 = require("../utils/memoizeViewport");
|
|
6
7
|
const log = (config, msg) => (0, debugUtils_1.logDebug)(config, 'verbose', msg);
|
|
7
8
|
/**
|
|
8
9
|
* Navigates to a cell with active cell optimization.
|
|
@@ -49,9 +50,9 @@ const navigateToCell = async (params) => {
|
|
|
49
50
|
};
|
|
50
51
|
// Viewport-oracle phase: direct positioning for 2D virtualized tables.
|
|
51
52
|
// Runs before navigation primitives; falls through to them if cell still not accessible.
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
const viaViewport = await navigateViaViewport(nav, viewport, rowIndex);
|
|
53
|
+
const cache = config.strategies.viewport && (0, memoizeViewport_1.memoizeViewport)(config.strategies.viewport);
|
|
54
|
+
if (cache && typeof rowIndex === 'number') {
|
|
55
|
+
const viaViewport = await navigateViaViewport(nav, cache.viewport, rowIndex, cache.clear);
|
|
55
56
|
if (viaViewport)
|
|
56
57
|
return viaViewport;
|
|
57
58
|
}
|
|
@@ -119,7 +120,7 @@ async function readIfInView(nav, viewport, rowIndex) {
|
|
|
119
120
|
* Viewport phase. Returns the cell locator, or null to fall through to navigation primitives
|
|
120
121
|
* (only when `strategies.navigation` is configured; otherwise throws on a miss).
|
|
121
122
|
*/
|
|
122
|
-
async function navigateViaViewport(nav, viewport, rowIndex) {
|
|
123
|
+
async function navigateViaViewport(nav, viewport, rowIndex, clearCache) {
|
|
123
124
|
const { config, context, index } = nav;
|
|
124
125
|
const inView = await readIfInView(nav, viewport, rowIndex);
|
|
125
126
|
if (inView)
|
|
@@ -135,6 +136,8 @@ async function navigateViaViewport(nav, viewport, rowIndex) {
|
|
|
135
136
|
await viewport.scrollToColumn(context, index);
|
|
136
137
|
}
|
|
137
138
|
});
|
|
139
|
+
// A peer may have performed the coordinated scroll. Drop this row's pre-scroll geometry too.
|
|
140
|
+
clearCache();
|
|
138
141
|
await recoverEvictedRow(nav, viewport, rowIndex);
|
|
139
142
|
if (await nav.targetReached()) {
|
|
140
143
|
return nav.getCellLocator();
|
package/dist/engine/countRows.js
CHANGED
|
@@ -40,15 +40,15 @@ function requireMap(tableMapper) {
|
|
|
40
40
|
* Callers must have initialized the table.
|
|
41
41
|
*/
|
|
42
42
|
async function countRows(env, filters, options) {
|
|
43
|
-
var _a;
|
|
44
|
-
var
|
|
43
|
+
var _a, _b, _c, _d, _e;
|
|
44
|
+
var _f, _g;
|
|
45
45
|
const { config, rootLocator, tableMapper, rowFinder } = env;
|
|
46
46
|
const filtersRecord = filters !== null && filters !== void 0 ? filters : {};
|
|
47
47
|
const hasFilters = Object.keys(filtersRecord).length > 0;
|
|
48
|
-
const effectiveMaxPages = (
|
|
48
|
+
const effectiveMaxPages = (_f = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _f !== void 0 ? _f : config.maxPages;
|
|
49
49
|
const pag = config.strategies.pagination;
|
|
50
50
|
const hasPagination = effectiveMaxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
|
|
51
|
-
const exact = (
|
|
51
|
+
const exact = (_g = options === null || options === void 0 ? void 0 : options.exact) !== null && _g !== void 0 ? _g : false;
|
|
52
52
|
const { domFilters, overrideFilters, syntheticFilters } = partitionFilters(config, filtersRecord);
|
|
53
53
|
const hasOverrides = Object.keys(overrideFilters).length > 0;
|
|
54
54
|
const hasSynthetics = Object.keys(syntheticFilters).length > 0;
|
|
@@ -80,14 +80,23 @@ async function countRows(env, filters, options) {
|
|
|
80
80
|
return;
|
|
81
81
|
await (0, loadingWait_1.waitWhileTableLoading)(config, env.getContext(), rootLocator.page(), 'countRows');
|
|
82
82
|
};
|
|
83
|
+
(_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
|
|
83
84
|
await waitForReady();
|
|
85
|
+
(_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.throwIfAborted();
|
|
84
86
|
const filterSuffix = hasFilters ? ` filters=${(0, safeStringify_1.safeStringify)(filtersRecord)}` : '';
|
|
85
87
|
if (!hasPagination) {
|
|
86
88
|
log(config, `countRows: counting rows in current viewport (no pagination)${filterSuffix}`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
let count;
|
|
90
|
+
if (!hasPostFilters) {
|
|
91
|
+
count = await resolveRows().count();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const all = await resolveRows().all();
|
|
95
|
+
(_d = options === null || options === void 0 ? void 0 : options.signal) === null || _d === void 0 ? void 0 : _d.throwIfAborted();
|
|
96
|
+
count = await countPostFilterMatches(all, all.map((_, i) => i));
|
|
97
|
+
}
|
|
98
|
+
(_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.throwIfAborted();
|
|
99
|
+
return count;
|
|
91
100
|
}
|
|
92
101
|
log(config, `countRows: paginating up to ${effectiveMaxPages} page(s)${filterSuffix}`);
|
|
93
102
|
const tracker = new elementTracker_1.ElementTracker('countRows');
|
|
@@ -113,6 +122,7 @@ async function countRows(env, filters, options) {
|
|
|
113
122
|
useBulk: false,
|
|
114
123
|
label: 'countRows',
|
|
115
124
|
finalScanOnEof: true,
|
|
125
|
+
signal: options === null || options === void 0 ? void 0 : options.signal,
|
|
116
126
|
});
|
|
117
127
|
pagesScanned = scanned.pagesScanned;
|
|
118
128
|
}
|
|
@@ -11,7 +11,9 @@ const safeStringify_1 = require("../utils/safeStringify");
|
|
|
11
11
|
* mounts (bounded by `maxPages`). Callers must have initialized the table.
|
|
12
12
|
*/
|
|
13
13
|
async function findRowByIndex(env, index, options) {
|
|
14
|
-
var _a;
|
|
14
|
+
var _a, _b, _c, _d, _e;
|
|
15
|
+
var _f;
|
|
16
|
+
(_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
|
|
15
17
|
const { config } = env;
|
|
16
18
|
(0, debugUtils_1.logDebug)(config, 'verbose', `findRowByIndex: index=${index} options=${(0, safeStringify_1.safeStringify)(options)}`);
|
|
17
19
|
const resolveRI = config.strategies.resolveRowIndex;
|
|
@@ -25,9 +27,12 @@ async function findRowByIndex(env, index, options) {
|
|
|
25
27
|
let foundSelector;
|
|
26
28
|
// The currently-mounted row whose logical index === index, if any.
|
|
27
29
|
const findMounted = async () => {
|
|
30
|
+
var _a, _b;
|
|
28
31
|
const rows = await env.getRowLocators().all();
|
|
32
|
+
(_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
|
|
29
33
|
for (const r of rows) {
|
|
30
34
|
const result = await resolveRI(r);
|
|
35
|
+
(_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
|
|
31
36
|
if (result === undefined)
|
|
32
37
|
continue;
|
|
33
38
|
const normalized = (0, rowResolution_1.normalizeRowIndexResult)(result);
|
|
@@ -40,22 +45,25 @@ async function findRowByIndex(env, index, options) {
|
|
|
40
45
|
};
|
|
41
46
|
// 1. Already mounted (no navigation needed)?
|
|
42
47
|
let loc = await findMounted();
|
|
48
|
+
(_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
|
|
43
49
|
// 2. Fast path: jump straight to it via the viewport's random-access scroll.
|
|
44
50
|
if (!loc && (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow)) {
|
|
45
51
|
(0, debugUtils_1.logDebug)(config, 'verbose', `findRowByIndex: scrolling to row ${index}`);
|
|
46
52
|
await viewport.scrollToRow(env.getContext(), index);
|
|
53
|
+
(_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.throwIfAborted();
|
|
47
54
|
loc = await findMounted();
|
|
55
|
+
(_d = options === null || options === void 0 ? void 0 : options.signal) === null || _d === void 0 ? void 0 : _d.throwIfAborted();
|
|
48
56
|
}
|
|
49
57
|
// 3. Fallback: advance pages (on infinite-scroll tables, a "page" is a scroll step) until
|
|
50
58
|
// the row is mounted. Page 1 was already searched above.
|
|
51
|
-
const maxPages = (
|
|
59
|
+
const maxPages = (_f = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _f !== void 0 ? _f : config.maxPages;
|
|
52
60
|
const scanForMounted = async () => {
|
|
53
61
|
let hit = null;
|
|
54
62
|
await (0, scanPages_1.scanPages)({ advancePage: env.advancePage, getCurrentPageIndex: env.getCurrentPageIndex, config }, async ({ pagesScanned }) => {
|
|
55
63
|
if (pagesScanned > 1)
|
|
56
64
|
hit = await findMounted();
|
|
57
65
|
return hit ? 'stop' : 'continue';
|
|
58
|
-
}, { maxPages, useBulk: false, label: 'findRowByIndex', finalScanOnEof: false });
|
|
66
|
+
}, { maxPages, useBulk: false, label: 'findRowByIndex', finalScanOnEof: false, signal: options === null || options === void 0 ? void 0 : options.signal });
|
|
59
67
|
return hit;
|
|
60
68
|
};
|
|
61
69
|
if (!loc && config.strategies.pagination) {
|
|
@@ -66,5 +74,6 @@ async function findRowByIndex(env, index, options) {
|
|
|
66
74
|
`Ensure a viewport.scrollToRow or pagination can bring it into view (searched up to maxPages=${maxPages}).`);
|
|
67
75
|
}
|
|
68
76
|
await (0, debugUtils_1.debugDelay)(config, 'findRow');
|
|
77
|
+
(_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.throwIfAborted();
|
|
69
78
|
return env.makeSmartRow(loc, env.getMap(), index, env.getCurrentPageIndex(), foundSelector);
|
|
70
79
|
}
|
|
@@ -3,7 +3,7 @@ import { FinalTableConfig, Selector, SmartRow, FilterValue } from '../types';
|
|
|
3
3
|
import { FilterEngine } from '../filterEngine';
|
|
4
4
|
import { TableMapper } from './tableMapper';
|
|
5
5
|
import { SmartRowArray } from '../utils/smartRowArray';
|
|
6
|
-
import { NavigationBarrier } from '../utils/navigationBarrier';
|
|
6
|
+
import type { NavigationBarrier } from '../utils/navigationBarrier';
|
|
7
7
|
export declare class RowFinder<T = any> {
|
|
8
8
|
private rootLocator;
|
|
9
9
|
private config;
|
|
@@ -23,6 +23,7 @@ export declare class RowFinder<T = any> {
|
|
|
23
23
|
findRow(filters: Record<string, FilterValue>, options?: {
|
|
24
24
|
exact?: boolean;
|
|
25
25
|
maxPages?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
26
27
|
}): Promise<SmartRow<T>>;
|
|
27
28
|
private waitForTableReady;
|
|
28
29
|
/** Unfiltered rows keep the plain `SmartRow #n` name that `_makeSmart` applies. */
|
|
@@ -31,6 +32,7 @@ export declare class RowFinder<T = any> {
|
|
|
31
32
|
exact?: boolean;
|
|
32
33
|
maxPages?: number;
|
|
33
34
|
useBulkPagination?: boolean;
|
|
35
|
+
signal?: AbortSignal;
|
|
34
36
|
}): Promise<SmartRowArray<T>>;
|
|
35
37
|
private findRowLocator;
|
|
36
38
|
private resolveRowIndex;
|
package/dist/engine/rowFinder.js
CHANGED
|
@@ -5,7 +5,6 @@ const debugUtils_1 = require("../utils/debugUtils");
|
|
|
5
5
|
const smartRowArray_1 = require("../utils/smartRowArray");
|
|
6
6
|
const elementTracker_1 = require("../utils/elementTracker");
|
|
7
7
|
const sentinel_1 = require("../utils/sentinel");
|
|
8
|
-
const navigationBarrier_1 = require("../utils/navigationBarrier");
|
|
9
8
|
const rowResolution_1 = require("./rowResolution");
|
|
10
9
|
const resolveCellLocator_1 = require("../utils/resolveCellLocator");
|
|
11
10
|
const scanPages_1 = require("./scanPages");
|
|
@@ -172,26 +171,23 @@ class RowFinder {
|
|
|
172
171
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
173
172
|
const currentRows = await rowLocators.all();
|
|
174
173
|
let added = 0;
|
|
175
|
-
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
176
|
-
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
177
174
|
for (const idx of newIndices) {
|
|
178
175
|
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
|
|
179
176
|
const rowLocator = this.nameFilteredRow(currentRows[idx], resolved, filtersRecord);
|
|
180
|
-
|
|
177
|
+
// These rows outlive the scan and may be consumed individually. Only
|
|
178
|
+
// iteration engines that own every callback can attach a batch barrier.
|
|
179
|
+
const smartRow = this.makeSmartRow(rowLocator, map, resolved === null || resolved === void 0 ? void 0 : resolved.index, this.tableState.currentPageIndex, undefined, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
|
|
181
180
|
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
|
|
182
181
|
if (loadingOutcome !== 'process') {
|
|
183
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
184
182
|
if (loadingOutcome === 'throw') {
|
|
185
183
|
throw new Error(`[SmartTable] Row ${allRows.length} did not finish loading within ${(_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.rowLoadingTimeout}ms`);
|
|
186
184
|
}
|
|
187
185
|
continue;
|
|
188
186
|
}
|
|
189
187
|
if (hasOverrideFilters && !await this.matchesOverrideFilters(currentRows[idx], overrideFilters, map, (_c = options === null || options === void 0 ? void 0 : options.exact) !== null && _c !== void 0 ? _c : false)) {
|
|
190
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
191
188
|
continue;
|
|
192
189
|
}
|
|
193
190
|
if (hasSyntheticFilters && !await this.matchesSyntheticFilters(currentRows[idx], syntheticFilters, map, (_d = options === null || options === void 0 ? void 0 : options.exact) !== null && _d !== void 0 ? _d : false)) {
|
|
194
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
195
191
|
continue;
|
|
196
192
|
}
|
|
197
193
|
allRows.push(smartRow);
|
|
@@ -204,6 +200,7 @@ class RowFinder {
|
|
|
204
200
|
useBulk,
|
|
205
201
|
label: 'findRows',
|
|
206
202
|
finalScanOnEof: hasPagination,
|
|
203
|
+
signal: options === null || options === void 0 ? void 0 : options.signal,
|
|
207
204
|
});
|
|
208
205
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
|
|
209
206
|
}
|
|
@@ -268,6 +265,7 @@ class RowFinder {
|
|
|
268
265
|
useBulk,
|
|
269
266
|
label: 'findRow',
|
|
270
267
|
finalScanOnEof: true,
|
|
268
|
+
signal: options.signal,
|
|
271
269
|
});
|
|
272
270
|
return found;
|
|
273
271
|
}
|
|
@@ -29,7 +29,7 @@ export type RowLoadingOutcome = 'process' | 'skip' | 'throw';
|
|
|
29
29
|
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
30
30
|
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Timeout returns a decision so each caller keeps its own barrier bookkeeping and error
|
|
33
|
+
* message. Predicate errors propagate unchanged.
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveRowLoading(row: SmartRow, loading: LoadingStrategy | undefined, noTimeoutAction: 'skip' | 'process', log?: (msg: string) => void): Promise<RowLoadingOutcome>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.normalizeRowIndexResult = normalizeRowIndexResult;
|
|
4
4
|
exports.resolveLogicalRowIndex = resolveLogicalRowIndex;
|
|
5
5
|
exports.resolveRowLoading = resolveRowLoading;
|
|
6
|
+
const deadline_1 = require("../utils/deadline");
|
|
6
7
|
/** Extract the numeric index from a {@link RowIndexResult}. */
|
|
7
8
|
function normalizeRowIndexResult(result) {
|
|
8
9
|
return typeof result === 'number' ? { index: result } : result;
|
|
@@ -36,8 +37,8 @@ async function resolveLogicalRowIndex(row, config, fallback) {
|
|
|
36
37
|
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
37
38
|
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* Timeout returns a decision so each caller keeps its own barrier bookkeeping and error
|
|
41
|
+
* message. Predicate errors propagate unchanged.
|
|
41
42
|
*/
|
|
42
43
|
async function resolveRowLoading(row, loading, noTimeoutAction, log) {
|
|
43
44
|
var _a;
|
|
@@ -53,18 +54,21 @@ async function resolveRowLoading(row, loading, noTimeoutAction, log) {
|
|
|
53
54
|
// map's fast path: with no timeout and a process-default, it never probes isRowLoading.
|
|
54
55
|
if (timeout === undefined && noTimeoutAction === 'process')
|
|
55
56
|
return 'process';
|
|
56
|
-
|
|
57
|
+
const deadline = Date.now() + (timeout !== null && timeout !== void 0 ? timeout : Infinity);
|
|
58
|
+
const check = () => (0, deadline_1.withinDeadline)(() => isRowLoading(row), deadline, true);
|
|
59
|
+
if (!(await check()))
|
|
57
60
|
return 'process';
|
|
58
61
|
// Row is loading with no timeout configured → legacy skip (findRows).
|
|
59
62
|
if (timeout === undefined)
|
|
60
63
|
return 'skip';
|
|
61
64
|
log === null || log === void 0 ? void 0 : log(`row ${row.rowIndex} — waiting up to ${timeout}ms for row to load`);
|
|
62
|
-
|
|
63
|
-
let resolved = !(await isRowLoading(row)); // immediate check (handles timeout=0)
|
|
65
|
+
let resolved = (timeout === 0 || Date.now() < deadline) && !(await check());
|
|
64
66
|
while (!resolved && Date.now() < deadline) {
|
|
65
67
|
// Page accessed lazily (only while actually polling), matching the pre-refactor code.
|
|
66
|
-
await row.page().waitForTimeout(100);
|
|
67
|
-
|
|
68
|
+
await row.page().waitForTimeout(Math.min(100, deadline - Date.now()));
|
|
69
|
+
if (Date.now() >= deadline)
|
|
70
|
+
break;
|
|
71
|
+
resolved = !(await check());
|
|
68
72
|
}
|
|
69
73
|
if (resolved)
|
|
70
74
|
return 'process';
|
|
@@ -6,6 +6,7 @@ exports.readRowLive = readRowLive;
|
|
|
6
6
|
const rowResolution_1 = require("./rowResolution");
|
|
7
7
|
const cellNavigation_1 = require("./cellNavigation");
|
|
8
8
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
9
|
+
const deadline_1 = require("../utils/deadline");
|
|
9
10
|
/** `getHeaderCell` for strategy hooks: the table's when available, else an index lookup. */
|
|
10
11
|
function headerCellGetter(env) {
|
|
11
12
|
const { table } = env;
|
|
@@ -35,6 +36,15 @@ async function computeSyntheticColumns(env, result, columns) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
const isBatchRow = (row) => row._inBatch === true || !!row._barrier;
|
|
39
|
+
/** Capture a mounted position's identity once, before readiness or navigation can recycle it. */
|
|
40
|
+
async function resolveSnapshotIdentity(env) {
|
|
41
|
+
if (!env.renderWindowPosition || !env.config.strategies.resolveRowIndex)
|
|
42
|
+
return env;
|
|
43
|
+
const identity = await (0, rowResolution_1.resolveLogicalRowIndex)(env.rowLocator, env.config, () => undefined);
|
|
44
|
+
// Keep the public SmartRow's mounted position unchanged; all snapshot recovery and
|
|
45
|
+
// navigation use this read's logical identity. An unknown identity is not position 0.
|
|
46
|
+
return Object.assign(Object.assign({}, env), { rowIndex: identity === null || identity === void 0 ? void 0 : identity.index, renderWindowPosition: false });
|
|
47
|
+
}
|
|
38
48
|
// ─── Atomic snapshot ─────────────────────────────────────────────────────────
|
|
39
49
|
/** Mounted row whose logical index (per resolveRowIndex) is `rowIndex`; tolerates rows detaching mid-scan. */
|
|
40
50
|
async function rescanForRow(env, rowIndex) {
|
|
@@ -107,9 +117,7 @@ async function resolveCloneTarget(env) {
|
|
|
107
117
|
await config.strategies.contentReady(cloneTarget, env.rootLocator.page());
|
|
108
118
|
// Revalidate: the element may have recycled during the await.
|
|
109
119
|
if (canRepin && await logicalIndexOf(env, cloneTarget) !== rowIndex) {
|
|
110
|
-
|
|
111
|
-
if (reFound)
|
|
112
|
-
cloneTarget = reFound;
|
|
120
|
+
cloneTarget = await recoverDriftedRow(env, rowIndex);
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
return cloneTarget;
|
|
@@ -148,6 +156,7 @@ async function readRowAtomic(env, columns) {
|
|
|
148
156
|
if (typeof cellSel !== 'string') {
|
|
149
157
|
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
150
158
|
}
|
|
159
|
+
env = await resolveSnapshotIdentity(env);
|
|
151
160
|
const page = rootLocator.page();
|
|
152
161
|
const snapshot = await cloneRow(await resolveCloneTarget(env), cellSel);
|
|
153
162
|
// Determine which columns have overrides (need lazy materialization)
|
|
@@ -275,7 +284,9 @@ function cellLoadingTimeoutOf(config) {
|
|
|
275
284
|
async function pollUntil(page, deadline, intervalMs, check, initial) {
|
|
276
285
|
let done = initial;
|
|
277
286
|
while (!done && Date.now() < deadline) {
|
|
278
|
-
await page.waitForTimeout(intervalMs);
|
|
287
|
+
await page.waitForTimeout(Math.min(intervalMs, deadline - Date.now()));
|
|
288
|
+
if (Date.now() >= deadline)
|
|
289
|
+
break;
|
|
279
290
|
done = await check();
|
|
280
291
|
}
|
|
281
292
|
return done;
|
|
@@ -285,11 +296,13 @@ async function pollUntil(page, deadline, intervalMs, check, initial) {
|
|
|
285
296
|
* indicator mounts. A single false-negative then reads "" and skips
|
|
286
297
|
* onCellLoadingTimeout. Only grace when the cell still looks empty.
|
|
287
298
|
*/
|
|
288
|
-
async function graceForLoadingIndicator(page, targetCell, isLoading,
|
|
289
|
-
const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
|
|
299
|
+
async function graceForLoadingIndicator(page, targetCell, isLoading, deadline) {
|
|
300
|
+
const peek = await (0, deadline_1.withinDeadline)(() => targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => ''), deadline, null);
|
|
301
|
+
if (peek === null)
|
|
302
|
+
return true; // Deadline expired: apply the cell loading timeout policy.
|
|
290
303
|
if (peek !== '')
|
|
291
304
|
return false;
|
|
292
|
-
return pollUntil(page, Date.now() +
|
|
305
|
+
return pollUntil(page, Math.min(Date.now() + 250, deadline), 25, isLoading, false);
|
|
293
306
|
}
|
|
294
307
|
/** What to do when a cell is still loading after `cellLoadingTimeout`. */
|
|
295
308
|
async function onStillLoading(env, targetCell, col, timeout) {
|
|
@@ -320,18 +333,19 @@ async function waitForCellLoaded(env, targetCell, col) {
|
|
|
320
333
|
if (!isCellLoading)
|
|
321
334
|
return { read: true };
|
|
322
335
|
const page = env.rootLocator.page();
|
|
323
|
-
const isLoading = () => isCellLoading(targetCell, col, smart);
|
|
324
336
|
const timeout = cellLoadingTimeoutOf(config);
|
|
337
|
+
const deadline = Date.now() + (timeout !== null && timeout !== void 0 ? timeout : Infinity);
|
|
338
|
+
const isLoading = () => (0, deadline_1.withinDeadline)(() => isCellLoading(targetCell, col, smart), deadline, true);
|
|
325
339
|
let loading = await isLoading();
|
|
326
340
|
if (!loading && timeout !== undefined && timeout > 0) {
|
|
327
|
-
loading = await graceForLoadingIndicator(page, targetCell, isLoading,
|
|
341
|
+
loading = await graceForLoadingIndicator(page, targetCell, isLoading, deadline);
|
|
328
342
|
}
|
|
329
343
|
if (!loading || timeout === undefined)
|
|
330
344
|
return { read: true };
|
|
331
345
|
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${timeout}ms`);
|
|
332
|
-
const deadline = Date.now() + timeout;
|
|
333
346
|
const isLoaded = async () => !(await isLoading());
|
|
334
|
-
const
|
|
347
|
+
const initial = (timeout === 0 || Date.now() < deadline) && await isLoaded();
|
|
348
|
+
const cellResolved = await pollUntil(page, deadline, 100, isLoaded, initial);
|
|
335
349
|
return cellResolved ? { read: true } : onStillLoading(env, targetCell, col, timeout);
|
|
336
350
|
}
|
|
337
351
|
/**
|
|
@@ -340,6 +354,7 @@ async function waitForCellLoaded(env, targetCell, col) {
|
|
|
340
354
|
*/
|
|
341
355
|
async function readRowLive(env, columns) {
|
|
342
356
|
var _a, _b, _c;
|
|
357
|
+
env = await resolveSnapshotIdentity(env);
|
|
343
358
|
const { config, map, rootLocator, rowIndex } = env;
|
|
344
359
|
const result = {};
|
|
345
360
|
const page = rootLocator.page();
|
|
@@ -52,6 +52,11 @@ export interface ScanPagesOptions {
|
|
|
52
52
|
* Matches runMap / findRows EOF behavior. @default true
|
|
53
53
|
*/
|
|
54
54
|
finalScanOnEof?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Checked before each page and before each advance. When aborted, the scan throws
|
|
57
|
+
* `signal.reason` instead of returning partial results (#488).
|
|
58
|
+
*/
|
|
59
|
+
signal?: AbortSignal;
|
|
55
60
|
}
|
|
56
61
|
export declare function scanPages(env: ScanPagesEnv, onPage: (ctx: ScanPageContext) => Promise<ScanDecision>, options: ScanPagesOptions): Promise<{
|
|
57
62
|
pagesScanned: number;
|
package/dist/engine/scanPages.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scanPages = scanPages;
|
|
4
4
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
5
|
+
function throwIfAborted(env, label, signal) {
|
|
6
|
+
if (!(signal === null || signal === void 0 ? void 0 : signal.aborted))
|
|
7
|
+
return;
|
|
8
|
+
(0, debugUtils_1.logDebug)(env.config, 'info', `${label}: aborted via AbortSignal at page ${env.getCurrentPageIndex()}`);
|
|
9
|
+
throw signal.reason;
|
|
10
|
+
}
|
|
5
11
|
async function scanPages(env, onPage, options) {
|
|
6
12
|
var _a, _b;
|
|
7
13
|
const maxPages = options.maxPages;
|
|
@@ -12,14 +18,17 @@ async function scanPages(env, onPage, options) {
|
|
|
12
18
|
let reachedEnd = false;
|
|
13
19
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: starting (maxPages=${maxPages}, useBulk=${useBulk}, finalScanOnEof=${finalScanOnEof})`);
|
|
14
20
|
while (true) {
|
|
21
|
+
throwIfAborted(env, label, options.signal);
|
|
15
22
|
if (env.waitForReady) {
|
|
16
23
|
await env.waitForReady();
|
|
24
|
+
throwIfAborted(env, label, options.signal);
|
|
17
25
|
}
|
|
18
26
|
const decision = await onPage({
|
|
19
27
|
pagesScanned,
|
|
20
28
|
pageIndex: env.getCurrentPageIndex(),
|
|
21
29
|
isFinalScan: reachedEnd,
|
|
22
30
|
});
|
|
31
|
+
throwIfAborted(env, label, options.signal);
|
|
23
32
|
if (decision === 'stop') {
|
|
24
33
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: onPage returned stop at page ${env.getCurrentPageIndex()}`);
|
|
25
34
|
break;
|
|
@@ -27,8 +36,10 @@ async function scanPages(env, onPage, options) {
|
|
|
27
36
|
if (pagesScanned >= maxPages || reachedEnd) {
|
|
28
37
|
break;
|
|
29
38
|
}
|
|
39
|
+
throwIfAborted(env, label, options.signal);
|
|
30
40
|
const prevPage = env.getCurrentPageIndex();
|
|
31
41
|
const didAdvance = await env.advancePage(useBulk);
|
|
42
|
+
throwIfAborted(env, label, options.signal);
|
|
32
43
|
if (!didAdvance) {
|
|
33
44
|
if (finalScanOnEof) {
|
|
34
45
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: pagination returned false — final scan`);
|
|
@@ -196,6 +196,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
196
196
|
useBulk,
|
|
197
197
|
label,
|
|
198
198
|
finalScanOnEof: true,
|
|
199
|
+
signal: options.signal,
|
|
199
200
|
});
|
|
200
201
|
log(env.config, `${label}: complete — ${results.length} result(s) across ${pagesScanned} page(s)`);
|
|
201
202
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TableMapper = void 0;
|
|
4
4
|
const headers_1 = require("../strategies/headers");
|
|
5
5
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
6
|
+
const deadline_1 = require("../utils/deadline");
|
|
6
7
|
class TableMapper {
|
|
7
8
|
constructor(rootLocator, config, resolve) {
|
|
8
9
|
this._headerMap = null;
|
|
@@ -21,9 +22,11 @@ class TableMapper {
|
|
|
21
22
|
this.log('Mapping headers...');
|
|
22
23
|
const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
|
|
23
24
|
const startTime = Date.now();
|
|
25
|
+
const deadline = startTime + headerTimeout;
|
|
26
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
24
27
|
if (this.config.autoScroll) {
|
|
25
28
|
try {
|
|
26
|
-
await this.rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
|
|
29
|
+
await (0, deadline_1.withinDeadline)(() => this.rootLocator.scrollIntoViewIfNeeded({ timeout: Math.max(1, Math.min(1000, remaining())) }), deadline, undefined);
|
|
27
30
|
}
|
|
28
31
|
catch (e) { }
|
|
29
32
|
}
|
|
@@ -39,22 +42,29 @@ class TableMapper {
|
|
|
39
42
|
while (Date.now() - startTime < headerTimeout) {
|
|
40
43
|
// 1. Wait for visibility
|
|
41
44
|
try {
|
|
42
|
-
await headerLoc.first().waitFor({ state: 'visible', timeout: 200 });
|
|
45
|
+
await (0, deadline_1.withinDeadline)(() => headerLoc.first().waitFor({ state: 'visible', timeout: Math.max(1, Math.min(200, remaining())) }), deadline, undefined);
|
|
43
46
|
}
|
|
44
47
|
catch (e) {
|
|
45
48
|
// Continue to check existing/loading state even if not strictly "visible" yet
|
|
46
49
|
}
|
|
50
|
+
if (remaining() <= 0)
|
|
51
|
+
break;
|
|
47
52
|
// 2. Check Smart Loading State
|
|
48
53
|
if ((_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isHeaderLoading) {
|
|
49
|
-
const isStable = !(await this.config.strategies.loading.isHeaderLoading(context));
|
|
54
|
+
const isStable = !(await (0, deadline_1.withinDeadline)(() => this.config.strategies.loading.isHeaderLoading(context), deadline, true));
|
|
50
55
|
if (!isStable) {
|
|
51
56
|
this.log('Headers are loading/unstable... waiting');
|
|
52
|
-
|
|
57
|
+
if (remaining() > 0)
|
|
58
|
+
await new Promise(r => setTimeout(r, Math.min(100, remaining())));
|
|
53
59
|
continue;
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
// 3. Attempt Scan
|
|
63
|
+
if (remaining() <= 0)
|
|
64
|
+
break;
|
|
57
65
|
try {
|
|
66
|
+
// Preserve long-running discovery strategies (e.g. horizontal header scans).
|
|
67
|
+
// The deadline bounds readiness and retries, not a strategy's own operation.
|
|
58
68
|
const rawHeaders = await strategy(context);
|
|
59
69
|
const entries = await this.processHeaders(rawHeaders);
|
|
60
70
|
// Success — validate before caching so a collision doesn't leave stale state
|
|
@@ -72,7 +82,8 @@ class TableMapper {
|
|
|
72
82
|
catch (e) {
|
|
73
83
|
lastError = e;
|
|
74
84
|
this.log(`Header mapping failed. Attempt ${Math.floor((Date.now() - startTime) / 100)}: ${e.message}`);
|
|
75
|
-
|
|
85
|
+
if (remaining() > 0)
|
|
86
|
+
await new Promise(r => setTimeout(r, Math.min(100, remaining())));
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
throw lastError || new Error(`Timed out waiting for headers after ${headerTimeout}ms`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { PLAYWRIGHT_SMART_TABLE_VERSION } from './packageVersion';
|
|
2
2
|
export { useTable } from './useTable';
|
|
3
|
-
export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, TableSelector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, TableStrategies, LoadingStrategy, ViewportStrategy, FilterStrategy, NavigationPrimitives, DedupeStrategy, ContentReadyStrategy, FillStrategy, PaginationStrategy, HeaderStrategy, } from './types';
|
|
3
|
+
export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, TableSelector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, TableStrategies, LoadingStrategy, ViewportStrategy, FilterStrategy, NavigationPrimitives, DedupeStrategy, ContentReadyStrategy, RowReadyPredicate, FillStrategy, PaginationStrategy, HeaderStrategy, } from './types';
|
|
4
4
|
export { Strategies } from './strategies';
|
|
5
5
|
export * as presets from './presets';
|
|
6
6
|
/**
|
package/dist/packageVersion.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Generated by scripts/embed-version.mjs from package.json
|
|
4
4
|
*/
|
|
5
5
|
/** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
|
|
6
|
-
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.25.0-next.
|
|
6
|
+
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.25.0-next.d8860d0";
|
package/dist/packageVersion.js
CHANGED
|
@@ -6,4 +6,4 @@ exports.PLAYWRIGHT_SMART_TABLE_VERSION = void 0;
|
|
|
6
6
|
* Generated by scripts/embed-version.mjs from package.json
|
|
7
7
|
*/
|
|
8
8
|
/** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
|
|
9
|
-
exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.25.0-next.
|
|
9
|
+
exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.25.0-next.d8860d0";
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
import type { ContentReadyStrategy } from '../types';
|
|
1
|
+
import type { ContentReadyStrategy, RowReadyPredicate } from '../types';
|
|
2
2
|
export declare const ContentReadyStrategies: {
|
|
3
|
+
/**
|
|
4
|
+
* Waits in the browser until `predicate(rowElement, arg)` returns a truthy value, e.g. until
|
|
5
|
+
* a skeleton placeholder is gone or every cell has rendered. Resolves when `timeout` expires
|
|
6
|
+
* even if the predicate never passed, like the other content-ready strategies.
|
|
7
|
+
*
|
|
8
|
+
* Uses `locator.waitForFunction` (Playwright 1.62+), which re-resolves the row on each retry
|
|
9
|
+
* and polls without a round-trip per check. Older versions poll via `locator.evaluate`.
|
|
10
|
+
* `timeout` and `interval` must be positive finite milliseconds. `interval` only controls
|
|
11
|
+
* the older-version fallback; native polling is controlled by Playwright.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton'))
|
|
15
|
+
*/
|
|
16
|
+
until: <Arg = undefined>(predicate: RowReadyPredicate<Arg>, options?: {
|
|
17
|
+
arg?: Arg;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
interval?: number;
|
|
20
|
+
}) => ContentReadyStrategy;
|
|
3
21
|
/**
|
|
4
22
|
* Polls the row's text content until two consecutive reads match.
|
|
5
23
|
*/
|
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ContentReadyStrategies = void 0;
|
|
4
|
+
function isTimeout(error) {
|
|
5
|
+
return (error === null || error === void 0 ? void 0 : error.name) === 'TimeoutError';
|
|
6
|
+
}
|
|
7
|
+
/** Playwright < 1.62 has no `locator.waitForFunction`: poll with one `evaluate` per check. */
|
|
8
|
+
async function pollPredicate(row, page, predicate, arg, timeout, interval) {
|
|
9
|
+
const deadline = Date.now() + timeout;
|
|
10
|
+
let source = predicate.toString();
|
|
11
|
+
// Build a serializable callback; the user predicate executes only inside the browser.
|
|
12
|
+
const wrap = (expression) => new Function(`return async (row, arg) => Boolean(await (${expression})(row, arg))`)();
|
|
13
|
+
let evaluateBoolean;
|
|
14
|
+
try {
|
|
15
|
+
evaluateBoolean = wrap(source);
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
// Like Playwright's serializer, normalize method shorthand into a function expression.
|
|
19
|
+
source = source.startsWith('async ') ? source.replace(/^async\s+/, 'async function ') : `function ${source}`;
|
|
20
|
+
evaluateBoolean = wrap(source);
|
|
21
|
+
}
|
|
22
|
+
let expired = false;
|
|
23
|
+
let deadlineTimer;
|
|
24
|
+
const timeoutReached = new Promise(resolve => {
|
|
25
|
+
deadlineTimer = setTimeout(() => { expired = true; resolve(); }, timeout);
|
|
26
|
+
});
|
|
27
|
+
const poll = async () => {
|
|
28
|
+
while (!expired && Date.now() < deadline) {
|
|
29
|
+
// Convert truthiness before serialization, including async DOM-element results.
|
|
30
|
+
if (await row.evaluate(evaluateBoolean, arg, { timeout: Math.max(1, deadline - Date.now()) }))
|
|
31
|
+
return;
|
|
32
|
+
if (expired || Date.now() >= deadline)
|
|
33
|
+
return;
|
|
34
|
+
await page.waitForTimeout(Math.min(interval, Math.max(0, deadline - Date.now())));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
// evaluate's timeout only bounds locator resolution, not an async predicate's promise.
|
|
39
|
+
await Promise.race([poll(), timeoutReached]);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
clearTimeout(deadlineTimer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
4
45
|
exports.ContentReadyStrategies = {
|
|
46
|
+
/**
|
|
47
|
+
* Waits in the browser until `predicate(rowElement, arg)` returns a truthy value, e.g. until
|
|
48
|
+
* a skeleton placeholder is gone or every cell has rendered. Resolves when `timeout` expires
|
|
49
|
+
* even if the predicate never passed, like the other content-ready strategies.
|
|
50
|
+
*
|
|
51
|
+
* Uses `locator.waitForFunction` (Playwright 1.62+), which re-resolves the row on each retry
|
|
52
|
+
* and polls without a round-trip per check. Older versions poll via `locator.evaluate`.
|
|
53
|
+
* `timeout` and `interval` must be positive finite milliseconds. `interval` only controls
|
|
54
|
+
* the older-version fallback; native polling is controlled by Playwright.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton'))
|
|
58
|
+
*/
|
|
59
|
+
until: (predicate, options = {}) => {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
|
|
62
|
+
const interval = (_b = options.interval) !== null && _b !== void 0 ? _b : 50;
|
|
63
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
64
|
+
throw new Error('ContentReady.until: timeout must be a positive finite number');
|
|
65
|
+
}
|
|
66
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
67
|
+
throw new Error('ContentReady.until: interval must be a positive finite number');
|
|
68
|
+
}
|
|
69
|
+
const fn = predicate;
|
|
70
|
+
const arg = options.arg;
|
|
71
|
+
return async (row, page) => {
|
|
72
|
+
try {
|
|
73
|
+
if (typeof row.waitForFunction === 'function') {
|
|
74
|
+
await row.waitForFunction(fn, arg, { timeout });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await pollPredicate(row, page, fn, arg, timeout, interval);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isTimeout(error))
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
},
|
|
5
86
|
/**
|
|
6
87
|
* Polls the row's text content until two consecutive reads match.
|
|
7
88
|
*/
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DedupeStrategy } from '../types';
|
|
2
2
|
export declare const DedupeStrategies: {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Groups rows by their viewport Y coordinate, rounded to a pixel bucket.
|
|
5
|
+
* Only applicable when each screen position represents the same record for the entire scan.
|
|
6
|
+
* Scrolling or pagination can place different records at the same position and silently drop them.
|
|
7
|
+
* @deprecated Prefer a stable record key, e.g. `async row => row.getCell('ID').innerText()`.
|
|
8
|
+
* Do not use for recycled virtualized rows or paginated tables.
|
|
6
9
|
* @param tolerance Pixel tolerance for position comparison (default: 2)
|
|
7
10
|
*/
|
|
8
11
|
byTopPosition: (tolerance?: number) => DedupeStrategy;
|
|
@@ -3,8 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DedupeStrategies = void 0;
|
|
4
4
|
exports.DedupeStrategies = {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Groups rows by their viewport Y coordinate, rounded to a pixel bucket.
|
|
7
|
+
* Only applicable when each screen position represents the same record for the entire scan.
|
|
8
|
+
* Scrolling or pagination can place different records at the same position and silently drop them.
|
|
9
|
+
* @deprecated Prefer a stable record key, e.g. `async row => row.getCell('ID').innerText()`.
|
|
10
|
+
* Do not use for recycled virtualized rows or paginated tables.
|
|
8
11
|
* @param tolerance Pixel tolerance for position comparison (default: 2)
|
|
9
12
|
*/
|
|
10
13
|
byTopPosition: (tolerance = 2) => async (row) => {
|
|
@@ -98,6 +98,11 @@ export declare const Strategies: {
|
|
|
98
98
|
dataAttribute: (options?: import("./viewport").DataAttributeViewportOptions) => import("..").ViewportStrategy;
|
|
99
99
|
};
|
|
100
100
|
ContentReady: {
|
|
101
|
+
until: <Arg = undefined>(predicate: import("..").RowReadyPredicate<Arg>, options?: {
|
|
102
|
+
arg?: Arg;
|
|
103
|
+
timeout?: number;
|
|
104
|
+
interval?: number;
|
|
105
|
+
}) => import("..").ContentReadyStrategy;
|
|
101
106
|
textStable: (options?: {
|
|
102
107
|
timeout?: number;
|
|
103
108
|
interval?: number;
|
package/dist/types.d.ts
CHANGED
|
@@ -210,9 +210,9 @@ export interface ViewportStrategy {
|
|
|
210
210
|
scrollToColumn?: (context: TableContext, colIndex: number) => Promise<void>;
|
|
211
211
|
/**
|
|
212
212
|
* When true, disables the automatic memoization of getVisibleColumnRange and
|
|
213
|
-
* getVisibleRowRange results
|
|
214
|
-
*
|
|
215
|
-
*
|
|
213
|
+
* getVisibleRowRange results within a single cell-navigation operation. Geometry
|
|
214
|
+
* is refreshed for each new read and around scroll calls. Row membership is not
|
|
215
|
+
* memoized by the library. Caches never persist across scans, resets, or revalidation.
|
|
216
216
|
*/
|
|
217
217
|
disableCache?: boolean;
|
|
218
218
|
}
|
|
@@ -426,8 +426,15 @@ export interface PaginationPrimitives {
|
|
|
426
426
|
detectCurrentPage?: (root: import('@playwright/test').Locator) => number | Promise<number>;
|
|
427
427
|
}
|
|
428
428
|
export type PaginationStrategy = PaginationPrimitives;
|
|
429
|
+
/**
|
|
430
|
+
* Returns a stable record key across the entire scan, including page advances.
|
|
431
|
+
* Different records must have different keys; viewport coordinates and mounted DOM indices
|
|
432
|
+
* are not stable identities in recycled or paginated tables.
|
|
433
|
+
*/
|
|
429
434
|
export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
|
|
430
435
|
export type ContentReadyStrategy = (row: Locator, page: Page) => Promise<void>;
|
|
436
|
+
/** Predicate for `Strategies.ContentReady.until`. Runs in the browser with the row element, so it must not close over test-side variables; pass values via `arg`. */
|
|
437
|
+
export type RowReadyPredicate<Arg = undefined> = (row: HTMLElement | SVGElement, arg: Arg) => unknown;
|
|
431
438
|
export type FillStrategy = (options: {
|
|
432
439
|
row: SmartRow;
|
|
433
440
|
columnName: string;
|
|
@@ -524,6 +531,7 @@ export interface LoadingStrategy {
|
|
|
524
531
|
* How long (ms) to wait for a loading row to resolve before applying onRowLoadingTimeout.
|
|
525
532
|
* Honored by findRows AND by map/forEach/filter, where the wait runs before the dedupe
|
|
526
533
|
* strategy so content-based dedupe keys see the row's final loaded state.
|
|
534
|
+
* Includes time spent awaiting the loading predicate, even if it never settles.
|
|
527
535
|
* When not set (backward-compatible behavior): findRows skips loading rows immediately;
|
|
528
536
|
* map/forEach/filter process them as-is.
|
|
529
537
|
*/
|
|
@@ -544,6 +552,7 @@ export interface LoadingStrategy {
|
|
|
544
552
|
isCellLoading?: (cell: import('@playwright/test').Locator, columnName: string, row: SmartRow) => Promise<boolean>;
|
|
545
553
|
/**
|
|
546
554
|
* How long (ms) to wait for a loading cell to resolve before applying onCellLoadingTimeout.
|
|
555
|
+
* Includes predicate evaluation and any initial rendering grace period.
|
|
547
556
|
* When not set and isCellLoading returns true, the cell is read as-is immediately.
|
|
548
557
|
*/
|
|
549
558
|
cellLoadingTimeout?: number;
|
|
@@ -638,12 +647,18 @@ export interface TableStrategies {
|
|
|
638
647
|
* Without this, the clone captures stale content from the previous occupant of the
|
|
639
648
|
* DOM slot — the element is at the correct position but React hasn't re-rendered yet.
|
|
640
649
|
*
|
|
641
|
-
*
|
|
650
|
+
* Built-ins: `Strategies.ContentReady.textStable()` (text stops changing),
|
|
651
|
+
* `mutationSettled()` (no DOM mutations for a quiet period), and `until(predicate)`
|
|
652
|
+
* (an in-browser condition holds, e.g. no skeleton placeholder).
|
|
642
653
|
*
|
|
643
654
|
* @example
|
|
644
655
|
* strategies: {
|
|
645
656
|
* contentReady: Strategies.ContentReady.textStable({ timeout: 500 }),
|
|
646
657
|
* }
|
|
658
|
+
* @example
|
|
659
|
+
* strategies: {
|
|
660
|
+
* contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton')),
|
|
661
|
+
* }
|
|
647
662
|
*/
|
|
648
663
|
contentReady?: ContentReadyStrategy;
|
|
649
664
|
/**
|
|
@@ -794,6 +809,11 @@ export type RowIterationOptions = {
|
|
|
794
809
|
* @default false — uses goNext for one-page-at-a-time advancement
|
|
795
810
|
*/
|
|
796
811
|
useBulkPagination?: boolean;
|
|
812
|
+
/**
|
|
813
|
+
* Cancels the iteration between pages. When aborted, the call rejects with
|
|
814
|
+
* `signal.reason` (rows already processed are not returned).
|
|
815
|
+
*/
|
|
816
|
+
signal?: AbortSignal;
|
|
797
817
|
};
|
|
798
818
|
/**
|
|
799
819
|
* Result of {@link useTable}. Implements `AsyncIterable` — `for await (const { row } of table)`
|
|
@@ -876,31 +896,37 @@ export interface TableResult<T = any> extends AsyncIterable<{
|
|
|
876
896
|
* is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
|
|
877
897
|
*
|
|
878
898
|
* @param index 0-based logical/data-model row index
|
|
879
|
-
* @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages)
|
|
899
|
+
* @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages).
|
|
900
|
+
* `signal` cancels the page scan; the call rejects with `signal.reason`.
|
|
880
901
|
*/
|
|
881
902
|
findRowByIndex: (index: number, options?: {
|
|
882
903
|
maxPages?: number;
|
|
904
|
+
signal?: AbortSignal;
|
|
883
905
|
}) => Promise<SmartRow<T>>;
|
|
884
906
|
/**
|
|
885
907
|
* ASYNC: Searches for a single row across pages using pagination.
|
|
886
908
|
* Auto-initializes the table if not already initialized.
|
|
887
909
|
* @param filters - The filter criteria to match
|
|
888
|
-
* @param options - Search options including exact match and max pages
|
|
910
|
+
* @param options - Search options including exact match and max pages. `signal` cancels the page scan; the call rejects with `signal.reason`.
|
|
889
911
|
*/
|
|
890
912
|
findRow: (filters: Record<string, FilterValue>, options?: {
|
|
891
913
|
exact?: boolean;
|
|
892
914
|
maxPages?: number;
|
|
915
|
+
signal?: AbortSignal;
|
|
893
916
|
}) => Promise<SmartRow<T>>;
|
|
894
917
|
/**
|
|
895
918
|
* ASYNC: Searches for all matching rows across pages using pagination.
|
|
896
919
|
* Auto-initializes the table if not already initialized.
|
|
920
|
+
* Returned rows can be read independently, including with `concurrency: 'synchronized'`.
|
|
921
|
+
* Use synchronized `map()` for coordinated column scrolling across virtualized rows.
|
|
897
922
|
* @param filters - The filter criteria to match (omit or pass {} for all rows)
|
|
898
|
-
* @param options - Search options (exact, maxPages). `useBulkPagination` defaults to `false`: pages advance one at a time via `goNext` so no intermediate page is skipped. Set it to `true` to opt into `goNextBulk` (faster, but skips the rows on jumped-over pages).
|
|
923
|
+
* @param options - Search options (exact, maxPages). `useBulkPagination` defaults to `false`: pages advance one at a time via `goNext` so no intermediate page is skipped. Set it to `true` to opt into `goNextBulk` (faster, but skips the rows on jumped-over pages). `signal` cancels the page scan; the call rejects with `signal.reason`.
|
|
899
924
|
*/
|
|
900
925
|
findRows: (filters?: Record<string, FilterValue>, options?: {
|
|
901
926
|
exact?: boolean;
|
|
902
927
|
maxPages?: number;
|
|
903
928
|
useBulkPagination?: boolean;
|
|
929
|
+
signal?: AbortSignal;
|
|
904
930
|
}) => Promise<SmartRowArray<T>>;
|
|
905
931
|
/**
|
|
906
932
|
* Resolves the named column and scrolls it into view.
|
|
@@ -911,11 +937,13 @@ export interface TableResult<T = any> extends AsyncIterable<{
|
|
|
911
937
|
/**
|
|
912
938
|
* Counts the number of rows, optionally filtered.
|
|
913
939
|
* Without arguments, counts all rows. With filters, counts only matching rows.
|
|
914
|
-
* Paginates when pagination is configured.
|
|
940
|
+
* Paginates when pagination is configured. `signal` cancels the page scan; the call
|
|
941
|
+
* rejects with `signal.reason` (the table is still reset to page 1).
|
|
915
942
|
*/
|
|
916
943
|
countRows: (filters?: Record<string, FilterValue>, options?: {
|
|
917
944
|
exact?: boolean;
|
|
918
945
|
maxPages?: number;
|
|
946
|
+
signal?: AbortSignal;
|
|
919
947
|
}) => Promise<number>;
|
|
920
948
|
/**
|
|
921
949
|
* Iterates over rows and extracts the value of a single column.
|
package/dist/useTable.js
CHANGED
|
@@ -53,52 +53,6 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
const config = Object.assign(Object.assign({ rowSelector: "tbody tr", headerSelector: "thead th", cellSelector: "td", maxPages: 1, headerTransformer: ({ text }) => text, autoScroll: true, onReset: async () => { } }, configOptions), { strategies: Object.assign(Object.assign({}, defaultStrategies), configOptions.strategies) });
|
|
56
|
-
// Automatically memoize viewport range oracles so user-provided functions are
|
|
57
|
-
// pure DOM queries with no cache bookkeeping. Invalidated after each scroll call.
|
|
58
|
-
let _clearViewportCache = null;
|
|
59
|
-
if (config.strategies.viewport && !config.strategies.viewport.disableCache) {
|
|
60
|
-
const vp = config.strategies.viewport;
|
|
61
|
-
let colRangeCache = null;
|
|
62
|
-
let rowRangeCache = null;
|
|
63
|
-
let rowIndicesCache = null;
|
|
64
|
-
_clearViewportCache = () => {
|
|
65
|
-
colRangeCache = null;
|
|
66
|
-
rowRangeCache = null;
|
|
67
|
-
rowIndicesCache = null;
|
|
68
|
-
};
|
|
69
|
-
config.strategies.viewport = Object.assign(Object.assign({}, vp), { getVisibleColumnRange: vp.getVisibleColumnRange
|
|
70
|
-
? async (ctx) => {
|
|
71
|
-
if (!colRangeCache)
|
|
72
|
-
colRangeCache = await vp.getVisibleColumnRange(ctx);
|
|
73
|
-
return colRangeCache;
|
|
74
|
-
}
|
|
75
|
-
: undefined, getVisibleRowRange: vp.getVisibleRowRange
|
|
76
|
-
? async (ctx) => {
|
|
77
|
-
if (!rowRangeCache)
|
|
78
|
-
rowRangeCache = await vp.getVisibleRowRange(ctx);
|
|
79
|
-
return rowRangeCache;
|
|
80
|
-
}
|
|
81
|
-
: undefined, getVisibleRowIndices: vp.getVisibleRowIndices
|
|
82
|
-
? async (ctx) => {
|
|
83
|
-
if (!rowIndicesCache)
|
|
84
|
-
rowIndicesCache = await vp.getVisibleRowIndices(ctx);
|
|
85
|
-
return rowIndicesCache;
|
|
86
|
-
}
|
|
87
|
-
: undefined, scrollToColumn: vp.scrollToColumn
|
|
88
|
-
? async (ctx, colIndex) => {
|
|
89
|
-
colRangeCache = null;
|
|
90
|
-
rowRangeCache = null;
|
|
91
|
-
rowIndicesCache = null;
|
|
92
|
-
await vp.scrollToColumn(ctx, colIndex);
|
|
93
|
-
}
|
|
94
|
-
: undefined, scrollToRow: vp.scrollToRow
|
|
95
|
-
? async (ctx, rowIndex) => {
|
|
96
|
-
rowRangeCache = null;
|
|
97
|
-
rowIndicesCache = null;
|
|
98
|
-
await vp.scrollToRow(ctx, rowIndex);
|
|
99
|
-
}
|
|
100
|
-
: undefined });
|
|
101
|
-
}
|
|
102
56
|
const resolve = (item, parent) => {
|
|
103
57
|
if (typeof item === 'string')
|
|
104
58
|
return parent.locator(item);
|
|
@@ -213,10 +167,6 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
213
167
|
if (pagesJumped > 0) {
|
|
214
168
|
tableState.currentPageIndex += pagesJumped;
|
|
215
169
|
log(`_advancePage: ${primitive} advanced ${pagesJumped} page(s) — now at page ${tableState.currentPageIndex}`);
|
|
216
|
-
// Invalidate viewport caches: after a page advance the row set changes and the
|
|
217
|
-
// column renderer may recalculate (e.g. scrollbar appearance changes available width).
|
|
218
|
-
if (_clearViewportCache)
|
|
219
|
-
_clearViewportCache();
|
|
220
170
|
await (0, debugUtils_1.debugDelay)(config, 'pagination');
|
|
221
171
|
}
|
|
222
172
|
else {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withinDeadline = withinDeadline;
|
|
4
|
+
/** Bound one asynchronous check by an existing deadline, without cancelling its underlying work. */
|
|
5
|
+
async function withinDeadline(check, deadline, expiredValue) {
|
|
6
|
+
if (!Number.isFinite(deadline))
|
|
7
|
+
return check();
|
|
8
|
+
let timer;
|
|
9
|
+
try {
|
|
10
|
+
const expired = new Promise(resolve => {
|
|
11
|
+
timer = setTimeout(() => resolve(expiredValue), Math.max(0, deadline - Date.now()));
|
|
12
|
+
});
|
|
13
|
+
// A late rejection is handled by the race even after the deadline wins.
|
|
14
|
+
return await Promise.race([check(), expired]);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ViewportStrategy } from '../types';
|
|
2
|
+
/** Cache geometry only for one navigation; scans and later reads use the original oracles. */
|
|
3
|
+
export declare function memoizeViewport(viewport: ViewportStrategy): {
|
|
4
|
+
viewport: ViewportStrategy;
|
|
5
|
+
clear: () => void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoizeViewport = memoizeViewport;
|
|
4
|
+
/** Cache geometry only for one navigation; scans and later reads use the original oracles. */
|
|
5
|
+
function memoizeViewport(viewport) {
|
|
6
|
+
if (viewport.disableCache)
|
|
7
|
+
return { viewport, clear: () => { } };
|
|
8
|
+
let columns;
|
|
9
|
+
let rows;
|
|
10
|
+
const clear = () => { columns = undefined; rows = undefined; };
|
|
11
|
+
const wrapScroll = (scroll) => scroll && (async (context, index) => {
|
|
12
|
+
clear();
|
|
13
|
+
try {
|
|
14
|
+
await scroll.call(viewport, context, index);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
clear();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
clear,
|
|
22
|
+
viewport: Object.assign(Object.assign({}, viewport), { getVisibleColumnRange: viewport.getVisibleColumnRange
|
|
23
|
+
? context => columns !== null && columns !== void 0 ? columns : (columns = viewport.getVisibleColumnRange(context))
|
|
24
|
+
: undefined, getVisibleRowRange: viewport.getVisibleRowRange
|
|
25
|
+
? context => rows !== null && rows !== void 0 ? rows : (rows = viewport.getVisibleRowRange(context))
|
|
26
|
+
: undefined, scrollToColumn: wrapScroll(viewport.scrollToColumn), scrollToRow: wrapScroll(viewport.scrollToRow) }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -4,7 +4,7 @@ import { SmartRow } from '../types';
|
|
|
4
4
|
*/
|
|
5
5
|
export interface SmartRowArray<T = any> extends Array<SmartRow<T>> {
|
|
6
6
|
/**
|
|
7
|
-
* Converts all rows to JSON objects
|
|
7
|
+
* Converts all rows to JSON objects, reading sequentially to avoid competing viewport moves.
|
|
8
8
|
*/
|
|
9
9
|
toJSON(): Promise<T[]>;
|
|
10
10
|
}
|
|
@@ -7,7 +7,10 @@ exports.createSmartRowArray = createSmartRowArray;
|
|
|
7
7
|
function createSmartRowArray(rows) {
|
|
8
8
|
const arr = rows;
|
|
9
9
|
arr.toJSON = async () => {
|
|
10
|
-
|
|
10
|
+
const results = [];
|
|
11
|
+
for (const row of rows)
|
|
12
|
+
results.push(await row.toJSON());
|
|
13
|
+
return results;
|
|
11
14
|
};
|
|
12
15
|
return arr;
|
|
13
16
|
}
|
package/package.json
CHANGED