@rickcedwhat/playwright-smart-table 6.18.0 → 6.19.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 +10 -0
- package/dist/engine/rowFinder.js +25 -57
- package/dist/engine/rowResolution.d.ts +28 -0
- package/dist/engine/rowResolution.js +73 -0
- package/dist/engine/tableIteration.d.ts +3 -1
- package/dist/engine/tableIteration.js +51 -45
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +3 -0
- package/dist/presets/mui.js +25 -0
- package/dist/smartRow.js +172 -11
- package/dist/strategies/fill.js +5 -1
- package/dist/strategies/viewport.js +46 -3
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +98 -8
- package/dist/types.d.ts +91 -6
- package/dist/useTable.js +100 -6
- package/package.json +1 -1
|
@@ -27,4 +27,14 @@ export declare class RowFinder<T = any> {
|
|
|
27
27
|
}): Promise<SmartRowArray<T>>;
|
|
28
28
|
private findRowLocator;
|
|
29
29
|
private resolveRowIndex;
|
|
30
|
+
/**
|
|
31
|
+
* Fallback for findRow when no resolveRowIndex strategy is configured: find the row's
|
|
32
|
+
* position in the current DOM order in a SINGLE roundtrip. Previously this looped over
|
|
33
|
+
* every row calling elementHandle() + evaluate() per row (O(n) CDP roundtrips, and
|
|
34
|
+
* elementHandle() is soft-deprecated). We resolve the target once and let evaluateAll
|
|
35
|
+
* compare it against the full row set in the browser. The row set is resolved through the
|
|
36
|
+
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
37
|
+
* all behave identically. (#350)
|
|
38
|
+
*/
|
|
39
|
+
private scanDomPosition;
|
|
30
40
|
}
|
package/dist/engine/rowFinder.js
CHANGED
|
@@ -6,6 +6,7 @@ const smartRowArray_1 = require("../utils/smartRowArray");
|
|
|
6
6
|
const elementTracker_1 = require("../utils/elementTracker");
|
|
7
7
|
const sentinel_1 = require("../utils/sentinel");
|
|
8
8
|
const navigationBarrier_1 = require("../utils/navigationBarrier");
|
|
9
|
+
const rowResolution_1 = require("./rowResolution");
|
|
9
10
|
class RowFinder {
|
|
10
11
|
constructor(rootLocator, config, resolve, filterEngine, tableMapper, makeSmartRow, tableState = { currentPageIndex: 0 }, advancePage = async () => false) {
|
|
11
12
|
this.rootLocator = rootLocator;
|
|
@@ -47,7 +48,7 @@ class RowFinder {
|
|
|
47
48
|
const tracker = new elementTracker_1.ElementTracker('findRows');
|
|
48
49
|
try {
|
|
49
50
|
const collectMatches = async () => {
|
|
50
|
-
var _a, _b
|
|
51
|
+
var _a, _b;
|
|
51
52
|
let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
52
53
|
// Only apply filters if we have them
|
|
53
54
|
if (Object.keys(filtersRecord).length > 0) {
|
|
@@ -56,56 +57,22 @@ class RowFinder {
|
|
|
56
57
|
// Get only newly seen matched rows
|
|
57
58
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
58
59
|
const currentRows = await rowLocators.all();
|
|
59
|
-
const isRowLoading = (_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isRowLoading;
|
|
60
|
-
const rawRowTimeout = (_c = this.config.strategies.loading) === null || _c === void 0 ? void 0 : _c.rowLoadingTimeout;
|
|
61
|
-
// undefined = unset (legacy skip); 0 = no wait (immediate check); >0 = wait up to N ms
|
|
62
|
-
// Non-finite values are treated as unset (defensive guard against Infinity/NaN)
|
|
63
|
-
const rowLoadingTimeout = rawRowTimeout !== undefined && Number.isFinite(rawRowTimeout) && rawRowTimeout >= 0
|
|
64
|
-
? rawRowTimeout
|
|
65
|
-
: undefined;
|
|
66
|
-
const onRowLoadingTimeout = (_e = (_d = this.config.strategies.loading) === null || _d === void 0 ? void 0 : _d.onRowLoadingTimeout) !== null && _e !== void 0 ? _e : 'read-as-is';
|
|
67
60
|
let added = 0;
|
|
68
61
|
// One barrier per batch — synchronizes cell navigation across all rows in this page's results
|
|
69
62
|
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
70
63
|
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
71
64
|
for (const idx of newIndices) {
|
|
72
|
-
|
|
73
|
-
// Without a strategy, fall back to sequential position to avoid the O(n)
|
|
74
|
-
// elementHandle scan that resolveRowIndex() would otherwise perform per row.
|
|
75
|
-
const rowIndex = this.config.strategies.resolveRowIndex
|
|
76
|
-
? ((_f = await this.config.strategies.resolveRowIndex(currentRows[idx])) !== null && _f !== void 0 ? _f : allRows.length)
|
|
77
|
-
: allRows.length;
|
|
65
|
+
const rowIndex = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
|
|
78
66
|
const smartRow = this.makeSmartRow(currentRows[idx], map, rowIndex, this.tableState.currentPageIndex, barrier);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Wait up to rowLoadingTimeout ms (0 = one immediate re-check, no polling)
|
|
87
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — waiting up to ${rowLoadingTimeout}ms for row to load`);
|
|
88
|
-
const deadline = Date.now() + rowLoadingTimeout;
|
|
89
|
-
let resolved = !(await isRowLoading(smartRow)); // immediate check (handles timeout=0)
|
|
90
|
-
while (!resolved && Date.now() < deadline) {
|
|
91
|
-
await currentRows[idx].page().waitForTimeout(100);
|
|
92
|
-
resolved = !(await isRowLoading(smartRow));
|
|
93
|
-
}
|
|
94
|
-
if (!resolved) {
|
|
95
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — still loading after ${rowLoadingTimeout}ms, action: ${onRowLoadingTimeout}`);
|
|
96
|
-
if (onRowLoadingTimeout === 'skip') {
|
|
97
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (onRowLoadingTimeout === 'throw') {
|
|
101
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
102
|
-
throw new Error(`[SmartTable] Row ${allRows.length} did not finish loading within ${rowLoadingTimeout}ms`);
|
|
103
|
-
}
|
|
104
|
-
// 'read-as-is': fall through and add the row
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — finished loading`);
|
|
67
|
+
// findRows skips a still-loading row when no timeout is configured (legacy
|
|
68
|
+
// behavior) — see resolveRowLoading's `noTimeoutAction: 'skip'`.
|
|
69
|
+
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
|
|
70
|
+
if (loadingOutcome !== 'process') {
|
|
71
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
72
|
+
if (loadingOutcome === 'throw') {
|
|
73
|
+
throw new Error(`[SmartTable] Row ${allRows.length} did not finish loading within ${(_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.rowLoadingTimeout}ms`);
|
|
108
74
|
}
|
|
75
|
+
continue; // 'skip'
|
|
109
76
|
}
|
|
110
77
|
allRows.push(smartRow);
|
|
111
78
|
added++;
|
|
@@ -203,19 +170,20 @@ class RowFinder {
|
|
|
203
170
|
return null;
|
|
204
171
|
}
|
|
205
172
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
173
|
+
resolveRowIndex(rowLocator) {
|
|
174
|
+
// Shared resolver: resolveRowIndex strategy first, else the DOM-position fallback.
|
|
175
|
+
return (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, this.config, () => this.scanDomPosition(rowLocator));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Fallback for findRow when no resolveRowIndex strategy is configured: find the row's
|
|
179
|
+
* position in the current DOM order in a SINGLE roundtrip. Previously this looped over
|
|
180
|
+
* every row calling elementHandle() + evaluate() per row (O(n) CDP roundtrips, and
|
|
181
|
+
* elementHandle() is soft-deprecated). We resolve the target once and let evaluateAll
|
|
182
|
+
* compare it against the full row set in the browser. The row set is resolved through the
|
|
183
|
+
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
184
|
+
* all behave identically. (#350)
|
|
185
|
+
*/
|
|
186
|
+
async scanDomPosition(rowLocator) {
|
|
219
187
|
const targetHandle = await rowLocator.elementHandle();
|
|
220
188
|
if (!targetHandle)
|
|
221
189
|
return undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Locator } from '@playwright/test';
|
|
2
|
+
import type { FinalTableConfig, LoadingStrategy, SmartRow } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
5
|
+
*
|
|
6
|
+
* Uses the configured `resolveRowIndex` strategy when present (e.g. MUI's `data-rowindex`);
|
|
7
|
+
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
8
|
+
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
9
|
+
*
|
|
10
|
+
* This replaces the three inline variants that previously answered the index question
|
|
11
|
+
* (findRow's private method, findRows' inline expression, and — later — map's counter).
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveLogicalRowIndex(row: Locator, config: Pick<FinalTableConfig, 'strategies'>, fallback: () => number | undefined | Promise<number | undefined>): Promise<number | undefined>;
|
|
14
|
+
/** What the caller should do with a row after checking its loading state. */
|
|
15
|
+
export type RowLoadingOutcome = 'process' | 'skip' | 'throw';
|
|
16
|
+
/**
|
|
17
|
+
* Single source of truth for the per-row loading wait (#362 consolidation).
|
|
18
|
+
*
|
|
19
|
+
* Collapses the two near-identical blocks in `findRows` (RowFinder) and `runMap`
|
|
20
|
+
* (tableIteration). The only real difference between the callers is the behavior when no
|
|
21
|
+
* `rowLoadingTimeout` is configured: `findRows` skips a still-loading row (`noTimeoutAction:
|
|
22
|
+
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
23
|
+
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
24
|
+
*
|
|
25
|
+
* The helper never throws and never touches the navigation barrier; it returns a decision so
|
|
26
|
+
* each caller keeps its own barrier bookkeeping and error message.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRowLoading(row: SmartRow, loading: LoadingStrategy | undefined, noTimeoutAction: 'skip' | 'process', log?: (msg: string) => void): Promise<RowLoadingOutcome>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveLogicalRowIndex = resolveLogicalRowIndex;
|
|
4
|
+
exports.resolveRowLoading = resolveRowLoading;
|
|
5
|
+
/**
|
|
6
|
+
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
7
|
+
*
|
|
8
|
+
* Uses the configured `resolveRowIndex` strategy when present (e.g. MUI's `data-rowindex`);
|
|
9
|
+
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
10
|
+
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
11
|
+
*
|
|
12
|
+
* This replaces the three inline variants that previously answered the index question
|
|
13
|
+
* (findRow's private method, findRows' inline expression, and — later — map's counter).
|
|
14
|
+
*/
|
|
15
|
+
async function resolveLogicalRowIndex(row, config, fallback) {
|
|
16
|
+
const strategy = config.strategies.resolveRowIndex;
|
|
17
|
+
if (strategy) {
|
|
18
|
+
const resolved = await strategy(row);
|
|
19
|
+
if (resolved !== undefined)
|
|
20
|
+
return resolved;
|
|
21
|
+
// Strategy returned undefined — fall through to the caller's fallback.
|
|
22
|
+
}
|
|
23
|
+
return fallback();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Single source of truth for the per-row loading wait (#362 consolidation).
|
|
27
|
+
*
|
|
28
|
+
* Collapses the two near-identical blocks in `findRows` (RowFinder) and `runMap`
|
|
29
|
+
* (tableIteration). The only real difference between the callers is the behavior when no
|
|
30
|
+
* `rowLoadingTimeout` is configured: `findRows` skips a still-loading row (`noTimeoutAction:
|
|
31
|
+
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
32
|
+
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
33
|
+
*
|
|
34
|
+
* The helper never throws and never touches the navigation barrier; it returns a decision so
|
|
35
|
+
* each caller keeps its own barrier bookkeeping and error message.
|
|
36
|
+
*/
|
|
37
|
+
async function resolveRowLoading(row, loading, noTimeoutAction, log) {
|
|
38
|
+
var _a;
|
|
39
|
+
const isRowLoading = loading === null || loading === void 0 ? void 0 : loading.isRowLoading;
|
|
40
|
+
if (!isRowLoading)
|
|
41
|
+
return 'process';
|
|
42
|
+
const rawTimeout = loading === null || loading === void 0 ? void 0 : loading.rowLoadingTimeout;
|
|
43
|
+
// undefined = unset (legacy skip / process-as-is); 0 = one immediate re-check; >0 = poll up to N ms.
|
|
44
|
+
// Non-finite values are treated as unset (defensive guard against Infinity/NaN).
|
|
45
|
+
const timeout = rawTimeout !== undefined && Number.isFinite(rawTimeout) && rawTimeout >= 0
|
|
46
|
+
? rawTimeout
|
|
47
|
+
: undefined;
|
|
48
|
+
// map's fast path: with no timeout and a process-default, it never probes isRowLoading.
|
|
49
|
+
if (timeout === undefined && noTimeoutAction === 'process')
|
|
50
|
+
return 'process';
|
|
51
|
+
if (!(await isRowLoading(row)))
|
|
52
|
+
return 'process';
|
|
53
|
+
// Row is loading with no timeout configured → legacy skip (findRows).
|
|
54
|
+
if (timeout === undefined)
|
|
55
|
+
return 'skip';
|
|
56
|
+
log === null || log === void 0 ? void 0 : log(`row ${row.rowIndex} — waiting up to ${timeout}ms for row to load`);
|
|
57
|
+
const deadline = Date.now() + timeout;
|
|
58
|
+
let resolved = !(await isRowLoading(row)); // immediate check (handles timeout=0)
|
|
59
|
+
while (!resolved && Date.now() < deadline) {
|
|
60
|
+
// Page accessed lazily (only while actually polling), matching the pre-refactor code.
|
|
61
|
+
await row.page().waitForTimeout(100);
|
|
62
|
+
resolved = !(await isRowLoading(row));
|
|
63
|
+
}
|
|
64
|
+
if (resolved)
|
|
65
|
+
return 'process';
|
|
66
|
+
const onTimeout = (_a = loading === null || loading === void 0 ? void 0 : loading.onRowLoadingTimeout) !== null && _a !== void 0 ? _a : 'read-as-is';
|
|
67
|
+
log === null || log === void 0 ? void 0 : log(`row ${row.rowIndex} — still loading after ${timeout}ms, action: ${onTimeout}`);
|
|
68
|
+
if (onTimeout === 'skip')
|
|
69
|
+
return 'skip';
|
|
70
|
+
if (onTimeout === 'throw')
|
|
71
|
+
return 'throw';
|
|
72
|
+
return 'process'; // 'read-as-is'
|
|
73
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Locator, Page } from '@playwright/test';
|
|
2
|
-
import type { SmartRow, RowIterationContext, RowIterationOptions } from '../types';
|
|
2
|
+
import type { SmartRow, RowIterationContext, RowIterationOptions, TableContext } from '../types';
|
|
3
3
|
import type { FinalTableConfig } from '../types';
|
|
4
4
|
import type { SmartRowArray } from '../utils/smartRowArray';
|
|
5
5
|
import { NavigationBarrier } from '../utils/navigationBarrier';
|
|
@@ -12,6 +12,8 @@ export interface TableIterationEnv<T = any> {
|
|
|
12
12
|
config: FinalTableConfig<T>;
|
|
13
13
|
getPage: () => Page;
|
|
14
14
|
getCurrentPageIndex: () => number;
|
|
15
|
+
/** Builds a TableContext for strategy calls (e.g. the viewport visible-row oracle). */
|
|
16
|
+
getContext: () => TableContext;
|
|
15
17
|
}
|
|
16
18
|
/** Delegates to {@link runMap}; void results are discarded. */
|
|
17
19
|
export declare function runForEach<T>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => void | Promise<void>, options?: RowIterationOptions): Promise<void>;
|
|
@@ -7,6 +7,7 @@ const elementTracker_1 = require("../utils/elementTracker");
|
|
|
7
7
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
8
8
|
const navigationBarrier_1 = require("../utils/navigationBarrier");
|
|
9
9
|
const mutex_1 = require("../utils/mutex");
|
|
10
|
+
const rowResolution_1 = require("./rowResolution");
|
|
10
11
|
function log(config, msg) {
|
|
11
12
|
(0, debugUtils_1.logDebug)(config, 'verbose', msg);
|
|
12
13
|
}
|
|
@@ -20,7 +21,7 @@ async function runForEach(env, callback, options = {}) {
|
|
|
20
21
|
* Concurrency: `parallel` | `synchronized` | `sequential` (see RowIterationOptions).
|
|
21
22
|
*/
|
|
22
23
|
async function runMap(env, callback, options = {}, label = 'map') {
|
|
23
|
-
var _a, _b, _c, _d, _e, _f
|
|
24
|
+
var _a, _b, _c, _d, _e, _f;
|
|
24
25
|
const map = env.getMap();
|
|
25
26
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : env.config.maxPages;
|
|
26
27
|
const dedupeStrategy = (_b = options.dedupe) !== null && _b !== void 0 ? _b : env.config.strategies.dedupe;
|
|
@@ -34,21 +35,6 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
34
35
|
const useMutex = concurrency === 'sequential';
|
|
35
36
|
const useBulk = (_e = options.useBulkPagination) !== null && _e !== void 0 ? _e : false;
|
|
36
37
|
const tracker = new elementTracker_1.ElementTracker(label);
|
|
37
|
-
// Row-level loading gate (Bug #355). Must run BEFORE the dedupe strategy so
|
|
38
|
-
// content-based dedupe keys are computed on the row's final loaded state — a key
|
|
39
|
-
// computed on a skeleton changes once the row loads, and the ElementTracker
|
|
40
|
-
// re-flags the row (text signature changed), producing duplicates.
|
|
41
|
-
// Semantics mirror findRows (rowFinder.ts), with one deliberate difference:
|
|
42
|
-
// findRows drops loading rows when no timeout is set (legacy skip), but map/forEach
|
|
43
|
-
// historically visited every row — so without a timeout we process the row as-is.
|
|
44
|
-
const isRowLoading = (_f = env.config.strategies.loading) === null || _f === void 0 ? void 0 : _f.isRowLoading;
|
|
45
|
-
const rawRowTimeout = (_g = env.config.strategies.loading) === null || _g === void 0 ? void 0 : _g.rowLoadingTimeout;
|
|
46
|
-
// undefined = unset (process as-is); 0 = no wait (immediate re-check); >0 = wait up to N ms
|
|
47
|
-
// Non-finite values are treated as unset (defensive guard against Infinity/NaN)
|
|
48
|
-
const rowLoadingTimeout = rawRowTimeout !== undefined && Number.isFinite(rawRowTimeout) && rawRowTimeout >= 0
|
|
49
|
-
? rawRowTimeout
|
|
50
|
-
: undefined;
|
|
51
|
-
const onRowLoadingTimeout = (_j = (_h = env.config.strategies.loading) === null || _h === void 0 ? void 0 : _h.onRowLoadingTimeout) !== null && _j !== void 0 ? _j : 'read-as-is';
|
|
52
38
|
log(env.config, `${label}: starting (maxPages=${effectiveMaxPages}, mode=${concurrency}, dedupe=${!!dedupeStrategy})`);
|
|
53
39
|
const results = [];
|
|
54
40
|
try {
|
|
@@ -67,12 +53,26 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
67
53
|
const rowLocators = env.getRowLocators();
|
|
68
54
|
const allIndices = await tracker.peekUnseenIndices(rowLocators);
|
|
69
55
|
const pageRows = await rowLocators.all();
|
|
56
|
+
// A2 (#353 part 2 / #357): when the viewport can report which rows are within the visible
|
|
57
|
+
// bounds, drop overscan rows from collection. They are left uncommitted (still unseen), so
|
|
58
|
+
// they're picked up on a later page once scrolled into view — preventing overscan from
|
|
59
|
+
// being ingested (#353) or double-collected via node recycling (#357). No-op when no
|
|
60
|
+
// viewport reports visible rows.
|
|
61
|
+
let candidateIndices = allIndices;
|
|
62
|
+
const getVisibleRowIndices = (_f = env.config.strategies.viewport) === null || _f === void 0 ? void 0 : _f.getVisibleRowIndices;
|
|
63
|
+
if (getVisibleRowIndices) {
|
|
64
|
+
const visible = new Set(await getVisibleRowIndices(env.getContext()));
|
|
65
|
+
candidateIndices = allIndices.filter(idx => visible.has(idx));
|
|
66
|
+
if (candidateIndices.length !== allIndices.length) {
|
|
67
|
+
log(env.config, `${label}: viewport visible filter — ${candidateIndices.length}/${allIndices.length} row(s) in view`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
70
|
// In synchronized mode, overscan rows rendered beyond the visible viewport by virtual
|
|
71
71
|
// scrollers can be evicted when the horizontal barrier fires snapFirstColumnIntoView.
|
|
72
72
|
// Filter them out before committing so they stay unseen and are picked up on the next page.
|
|
73
73
|
const newIndices = concurrency === 'synchronized'
|
|
74
|
-
? (await Promise.all(
|
|
75
|
-
:
|
|
74
|
+
? (await Promise.all(candidateIndices.map(async (idx) => ({ idx, present: pageRows[idx] != null && (await pageRows[idx].count()) > 0 })))).filter(r => r.present).map(r => r.idx)
|
|
75
|
+
: candidateIndices;
|
|
76
76
|
await tracker.commitIndices(rowLocators, newIndices);
|
|
77
77
|
const batchSize = newIndices.length;
|
|
78
78
|
if (batchSize === 0) {
|
|
@@ -82,30 +82,36 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
82
82
|
log(env.config, `${label}: scanning page ${pagesScanned} — ${batchSize} new row(s)`);
|
|
83
83
|
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(batchSize) : undefined;
|
|
84
84
|
const actionMutex = useMutex ? new mutex_1.Mutex() : null;
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// `index` (ctx.index) is the enumeration counter — the order rows are visited,
|
|
86
|
+
// contiguous and monotonic. It drives the internal stop()/ordering logic.
|
|
87
|
+
const positionBase = rowIndex;
|
|
88
|
+
// B-hybrid (#362): the row's rowIndex is its logical/data-model index when a
|
|
89
|
+
// resolveRowIndex strategy is configured (so bringIntoView and position math are
|
|
90
|
+
// correct on virtualized tables); otherwise it equals the enumeration counter.
|
|
91
|
+
const smartRows = await Promise.all(newIndices.map(async (idx, i) => {
|
|
92
|
+
var _a;
|
|
93
|
+
const logicalIndex = (_a = await (0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], env.config, () => positionBase + i)) !== null && _a !== void 0 ? _a : (positionBase + i);
|
|
94
|
+
const sr = env.makeSmartRow(pageRows[idx], map, logicalIndex, env.getCurrentPageIndex(), barrier);
|
|
95
|
+
// Mark as part of an iteration batch so toJSON's #366 re-pin uses rescan-only recovery
|
|
96
|
+
// (no scroll-back) — scrolling here would disrupt sibling rows' positional locators.
|
|
97
|
+
sr._inBatch = true;
|
|
98
|
+
return sr;
|
|
99
|
+
}));
|
|
100
|
+
// enumIndex = visit-order counter (ctx.index); row.rowIndex = logical (ctx.rowIndex).
|
|
101
|
+
const processRow = async (row, enumIndex) => {
|
|
102
|
+
var _a;
|
|
87
103
|
try {
|
|
88
|
-
if (stopped &&
|
|
104
|
+
if (stopped && enumIndex > stoppedIndex) {
|
|
89
105
|
return SKIP;
|
|
90
106
|
}
|
|
91
107
|
// Wait for the row to finish loading BEFORE evaluating its dedupe key (Bug #355).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
if (!resolved) {
|
|
101
|
-
log(env.config, `${label}: row ${row.rowIndex} — still loading after ${rowLoadingTimeout}ms, action: ${onRowLoadingTimeout}`);
|
|
102
|
-
if (onRowLoadingTimeout === 'skip')
|
|
103
|
-
return SKIP;
|
|
104
|
-
if (onRowLoadingTimeout === 'throw') {
|
|
105
|
-
throw new Error(`[SmartTable] Row ${row.rowIndex} did not finish loading within ${rowLoadingTimeout}ms`);
|
|
106
|
-
}
|
|
107
|
-
// 'read-as-is': fall through and process the row
|
|
108
|
-
}
|
|
108
|
+
// map/forEach/filter process a still-loading row when no timeout is set (unlike
|
|
109
|
+
// findRows, which skips) — see resolveRowLoading's `noTimeoutAction`.
|
|
110
|
+
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(row, env.config.strategies.loading, 'process', (msg) => log(env.config, `${label}: ${msg}`));
|
|
111
|
+
if (loadingOutcome === 'skip')
|
|
112
|
+
return SKIP;
|
|
113
|
+
if (loadingOutcome === 'throw') {
|
|
114
|
+
throw new Error(`[SmartTable] Row ${row.rowIndex} did not finish loading within ${(_a = env.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.rowLoadingTimeout}ms`);
|
|
109
115
|
}
|
|
110
116
|
if (dedupeKeys && dedupeStrategy) {
|
|
111
117
|
const key = await dedupeStrategy(row);
|
|
@@ -117,10 +123,10 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
117
123
|
}
|
|
118
124
|
// Execute callback (optionally serialized via mutex)
|
|
119
125
|
const runCallback = async () => {
|
|
120
|
-
if (stopped &&
|
|
126
|
+
if (stopped && enumIndex > stoppedIndex)
|
|
121
127
|
return SKIP;
|
|
122
|
-
log(env.config, `${label}: processing row ${row.rowIndex}`);
|
|
123
|
-
return await callback({ row, index:
|
|
128
|
+
log(env.config, `${label}: processing row (index ${enumIndex}, rowIndex ${row.rowIndex})`);
|
|
129
|
+
return await callback({ row, index: enumIndex, rowIndex: row.rowIndex, pageIndex: env.getCurrentPageIndex(), stop: () => stop(enumIndex) });
|
|
124
130
|
};
|
|
125
131
|
if (actionMutex) {
|
|
126
132
|
return await actionMutex.run(runCallback);
|
|
@@ -136,16 +142,16 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
136
142
|
};
|
|
137
143
|
const pageResults = [];
|
|
138
144
|
if (concurrency === 'sequential') {
|
|
139
|
-
for (
|
|
140
|
-
pageResults.push(await processRow(
|
|
145
|
+
for (let i = 0; i < smartRows.length; i++) {
|
|
146
|
+
pageResults.push(await processRow(smartRows[i], positionBase + i));
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
else {
|
|
144
|
-
const results = await Promise.all(smartRows.map(processRow));
|
|
150
|
+
const results = await Promise.all(smartRows.map((row, i) => processRow(row, positionBase + i)));
|
|
145
151
|
pageResults.push(...results);
|
|
146
152
|
}
|
|
147
153
|
for (let i = 0; i < pageResults.length; i++) {
|
|
148
|
-
if (
|
|
154
|
+
if (positionBase + i > stoppedIndex)
|
|
149
155
|
break;
|
|
150
156
|
const r = pageResults[i];
|
|
151
157
|
if (r !== SKIP)
|
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.
|
|
6
|
+
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.19.0";
|
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.
|
|
9
|
+
exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.19.0";
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare const Plugins: {
|
|
|
24
24
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
25
25
|
strategies?: import("../types").TableStrategies | undefined;
|
|
26
26
|
columnOverrides?: Partial<Record<string | number | symbol, import("../types").ColumnOverride<any>>> | undefined;
|
|
27
|
+
emptyState?: import("@playwright/test").Locator | undefined;
|
|
27
28
|
};
|
|
28
29
|
/** @deprecated Use `presets.glide` */
|
|
29
30
|
Glide: {
|
|
@@ -44,6 +45,7 @@ export declare const Plugins: {
|
|
|
44
45
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
45
46
|
strategies?: import("../types").TableStrategies | undefined;
|
|
46
47
|
columnOverrides?: Partial<Record<string | number | symbol, import("../types").ColumnOverride<any>>> | undefined;
|
|
48
|
+
emptyState?: import("@playwright/test").Locator | undefined;
|
|
47
49
|
};
|
|
48
50
|
/** @deprecated Use `presets.muiDataGrid` */
|
|
49
51
|
MUI: {
|
|
@@ -64,5 +66,6 @@ export declare const Plugins: {
|
|
|
64
66
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
65
67
|
strategies?: import("../types").TableStrategies | undefined;
|
|
66
68
|
columnOverrides?: Partial<Record<string | number | symbol, import("../types").ColumnOverride<any>>> | undefined;
|
|
69
|
+
emptyState?: import("@playwright/test").Locator | undefined;
|
|
67
70
|
};
|
|
68
71
|
};
|
package/dist/presets/mui.js
CHANGED
|
@@ -353,6 +353,31 @@ function createMuiDataGrid(opts) {
|
|
|
353
353
|
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
354
354
|
}, rowSel);
|
|
355
355
|
},
|
|
356
|
+
// Geometry-based visible-row set for the iteration engine (A2, #353/#357). Returns
|
|
357
|
+
// the DOM positions of rows overlapping the virtualScroller's bounds so map/forEach
|
|
358
|
+
// skip the overscan rows MUI keeps mounted above/below the fold. The scroller is a
|
|
359
|
+
// descendant of root, so querySelector (not closest).
|
|
360
|
+
getVisibleRowIndices: async ({ root, config }) => {
|
|
361
|
+
const rowSel = config.rowSelector;
|
|
362
|
+
return root.evaluate((el, rowSel) => {
|
|
363
|
+
const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
|
|
364
|
+
const rows = Array.from(el.querySelectorAll(rowSel));
|
|
365
|
+
const scrollerRect = scroller ? scroller.getBoundingClientRect() : null;
|
|
366
|
+
const visible = [];
|
|
367
|
+
rows.forEach((r, i) => {
|
|
368
|
+
if (!scrollerRect) {
|
|
369
|
+
visible.push(i);
|
|
370
|
+
return;
|
|
371
|
+
} // can't measure → keep all
|
|
372
|
+
const rect = r.getBoundingClientRect();
|
|
373
|
+
if (rect.height === 0)
|
|
374
|
+
return;
|
|
375
|
+
if (rect.bottom > scrollerRect.top && rect.top < scrollerRect.bottom)
|
|
376
|
+
visible.push(i);
|
|
377
|
+
});
|
|
378
|
+
return visible;
|
|
379
|
+
}, rowSel);
|
|
380
|
+
},
|
|
356
381
|
getVisibleColumnRange: async ({ root, config }) => {
|
|
357
382
|
const rowSel = config.rowSelector;
|
|
358
383
|
const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : '[aria-colindex]';
|