@rickcedwhat/playwright-smart-table 6.18.0 → 6.20.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 +16 -1
- package/dist/engine/rowFinder.js +156 -83
- package/dist/engine/rowResolution.d.ts +35 -0
- package/dist/engine/rowResolution.js +78 -0
- package/dist/engine/tableIteration.d.ts +4 -2
- package/dist/engine/tableIteration.js +72 -57
- package/dist/engine/tableMapper.js +10 -3
- package/dist/index.d.ts +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +9 -3
- package/dist/presets/glide/headers.js +6 -3
- package/dist/presets/mui.js +28 -1
- package/dist/smartRow.js +240 -48
- package/dist/strategies/fill.js +5 -2
- package/dist/strategies/headers.js +6 -3
- package/dist/strategies/viewport.js +46 -3
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +146 -16
- package/dist/types.d.ts +140 -14
- package/dist/useTable.js +202 -26
- package/dist/utils/mergeTableConfig.js +4 -0
- package/package.json +1 -1
|
@@ -13,9 +13,13 @@ export declare class RowFinder<T = any> {
|
|
|
13
13
|
private tableState;
|
|
14
14
|
private advancePage;
|
|
15
15
|
private resolve;
|
|
16
|
-
constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number | undefined, tablePageIndex?: number, barrier?: NavigationBarrier) => SmartRow<T>, tableState?: {
|
|
16
|
+
constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number | undefined, tablePageIndex?: number, barrier?: NavigationBarrier, rowSelector?: string) => SmartRow<T>, tableState?: {
|
|
17
17
|
currentPageIndex: number;
|
|
18
18
|
}, advancePage?: (useBulk: boolean) => Promise<boolean>);
|
|
19
|
+
private splitFilters;
|
|
20
|
+
static matchReadValue(readValue: string, filterValue: FilterValue, exact: boolean): boolean;
|
|
21
|
+
matchesOverrideFilters(rowLocator: Locator, overrideFilters: Record<string, FilterValue>, map: Map<string, number>, exact: boolean): Promise<boolean>;
|
|
22
|
+
matchesSyntheticFilters(rowLocator: Locator, syntheticFilters: Record<string, FilterValue>, map: Map<string, number>, exact: boolean): Promise<boolean>;
|
|
19
23
|
findRow(filters: Record<string, FilterValue>, options?: {
|
|
20
24
|
exact?: boolean;
|
|
21
25
|
maxPages?: number;
|
|
@@ -27,4 +31,15 @@ export declare class RowFinder<T = any> {
|
|
|
27
31
|
}): Promise<SmartRowArray<T>>;
|
|
28
32
|
private findRowLocator;
|
|
29
33
|
private resolveRowIndex;
|
|
34
|
+
/**
|
|
35
|
+
* Fallback for findRow when no resolveRowIndex strategy is configured: find the row's
|
|
36
|
+
* position in the current DOM order in a SINGLE roundtrip. Previously this looped over
|
|
37
|
+
* every row calling elementHandle() + evaluate() per row (O(n) CDP roundtrips, and
|
|
38
|
+
* elementHandle() is soft-deprecated). We resolve the target once and let evaluateAll
|
|
39
|
+
* compare it against the full row set in the browser. The row set is resolved through the
|
|
40
|
+
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
41
|
+
* all behave identically. (#350)
|
|
42
|
+
*/
|
|
43
|
+
private throwIfAmbiguous;
|
|
44
|
+
private scanDomPosition;
|
|
30
45
|
}
|
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;
|
|
@@ -17,6 +18,73 @@ class RowFinder {
|
|
|
17
18
|
this.advancePage = advancePage;
|
|
18
19
|
this.resolve = resolve;
|
|
19
20
|
}
|
|
21
|
+
splitFilters(filters) {
|
|
22
|
+
var _a, _b, _c;
|
|
23
|
+
const domFilters = {};
|
|
24
|
+
const overrideFilters = {};
|
|
25
|
+
const syntheticFilters = {};
|
|
26
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
27
|
+
if ((_a = this.config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[key]) {
|
|
28
|
+
syntheticFilters[key] = value;
|
|
29
|
+
}
|
|
30
|
+
else if ((_c = (_b = this.config.columnOverrides) === null || _b === void 0 ? void 0 : _b[key]) === null || _c === void 0 ? void 0 : _c.read) {
|
|
31
|
+
overrideFilters[key] = value;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
domFilters[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { domFilters, overrideFilters, syntheticFilters };
|
|
38
|
+
}
|
|
39
|
+
static matchReadValue(readValue, filterValue, exact) {
|
|
40
|
+
if (typeof filterValue === 'function') {
|
|
41
|
+
throw new Error(`[SmartTable] Function filters are not supported for columns with columnOverrides.read. ` +
|
|
42
|
+
`Use a string, number, or RegExp filter instead.`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof filterValue === 'string' || typeof filterValue === 'number') {
|
|
45
|
+
const target = String(filterValue);
|
|
46
|
+
return exact ? readValue === target : readValue.includes(target);
|
|
47
|
+
}
|
|
48
|
+
if (filterValue instanceof RegExp) {
|
|
49
|
+
return filterValue.test(readValue);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async matchesOverrideFilters(rowLocator, overrideFilters, map, exact) {
|
|
54
|
+
for (const [colName, filterValue] of Object.entries(overrideFilters)) {
|
|
55
|
+
const colIndex = map.get(colName);
|
|
56
|
+
if (colIndex === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
const override = this.config.columnOverrides[colName];
|
|
59
|
+
const cell = this.resolve(this.config.cellSelector, rowLocator).nth(colIndex);
|
|
60
|
+
const getCell = (name) => {
|
|
61
|
+
const idx = map.get(name);
|
|
62
|
+
if (idx === undefined)
|
|
63
|
+
throw new Error(`Column "${name}" not found`);
|
|
64
|
+
return this.resolve(this.config.cellSelector, rowLocator).nth(idx);
|
|
65
|
+
};
|
|
66
|
+
const context = {
|
|
67
|
+
row: this.makeSmartRow(rowLocator, map, undefined),
|
|
68
|
+
columnName: colName,
|
|
69
|
+
columnIndex: colIndex,
|
|
70
|
+
getCell,
|
|
71
|
+
};
|
|
72
|
+
const readValue = String(await override.read(cell, context));
|
|
73
|
+
if (!RowFinder.matchReadValue(readValue, filterValue, exact))
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
async matchesSyntheticFilters(rowLocator, syntheticFilters, map, exact) {
|
|
79
|
+
const smartRow = this.makeSmartRow(rowLocator, map, undefined);
|
|
80
|
+
for (const [colName, filterValue] of Object.entries(syntheticFilters)) {
|
|
81
|
+
const def = this.config.syntheticColumns[colName];
|
|
82
|
+
const computedValue = String(await def.compute(smartRow));
|
|
83
|
+
if (!RowFinder.matchReadValue(computedValue, filterValue, exact))
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
20
88
|
async findRow(filters, options = {}) {
|
|
21
89
|
(0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
|
|
22
90
|
await this.tableMapper.getMap();
|
|
@@ -25,8 +93,8 @@ class RowFinder {
|
|
|
25
93
|
(0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
|
|
26
94
|
await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
27
95
|
const map = await this.tableMapper.getMap();
|
|
28
|
-
const
|
|
29
|
-
return this.makeSmartRow(rowLocator, map,
|
|
96
|
+
const resolved = await this.resolveRowIndex(rowLocator);
|
|
97
|
+
return 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);
|
|
30
98
|
}
|
|
31
99
|
(0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
|
|
32
100
|
await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
@@ -46,66 +114,42 @@ class RowFinder {
|
|
|
46
114
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
|
|
47
115
|
const tracker = new elementTracker_1.ElementTracker('findRows');
|
|
48
116
|
try {
|
|
117
|
+
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filtersRecord);
|
|
118
|
+
const hasOverrideFilters = Object.keys(overrideFilters).length > 0;
|
|
119
|
+
const hasSyntheticFilters = Object.keys(syntheticFilters).length > 0;
|
|
49
120
|
const collectMatches = async () => {
|
|
50
|
-
var _a, _b, _c, _d
|
|
121
|
+
var _a, _b, _c, _d;
|
|
51
122
|
let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rowLocators = this.filterEngine.applyFilters(rowLocators, filtersRecord, map, (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false, this.rootLocator.page(), this.rootLocator);
|
|
123
|
+
if (Object.keys(domFilters).length > 0) {
|
|
124
|
+
rowLocators = this.filterEngine.applyFilters(rowLocators, domFilters, map, (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false, this.rootLocator.page(), this.rootLocator);
|
|
55
125
|
}
|
|
56
126
|
// Get only newly seen matched rows
|
|
57
127
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
58
128
|
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
129
|
let added = 0;
|
|
68
130
|
// One barrier per batch — synchronizes cell navigation across all rows in this page's results
|
|
69
131
|
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
70
132
|
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
71
133
|
for (const idx of newIndices) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// No timeout configured (or invalid value) — legacy skip behavior
|
|
82
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — row skipped (isRowLoading=true, no timeout)`);
|
|
83
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
84
|
-
continue;
|
|
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`);
|
|
134
|
+
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
|
|
135
|
+
const smartRow = this.makeSmartRow(currentRows[idx], map, resolved === null || resolved === void 0 ? void 0 : resolved.index, this.tableState.currentPageIndex, barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
|
|
136
|
+
// findRows skips a still-loading row when no timeout is configured (legacy
|
|
137
|
+
// behavior) — see resolveRowLoading's `noTimeoutAction: 'skip'`.
|
|
138
|
+
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
|
|
139
|
+
if (loadingOutcome !== 'process') {
|
|
140
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
141
|
+
if (loadingOutcome === 'throw') {
|
|
142
|
+
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
143
|
}
|
|
144
|
+
continue; // 'skip'
|
|
145
|
+
}
|
|
146
|
+
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)) {
|
|
147
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
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)) {
|
|
151
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
152
|
+
continue;
|
|
109
153
|
}
|
|
110
154
|
allRows.push(smartRow);
|
|
111
155
|
added++;
|
|
@@ -124,7 +168,8 @@ class RowFinder {
|
|
|
124
168
|
const prevPage = this.tableState.currentPageIndex;
|
|
125
169
|
const didPaginate = await this.advancePage(useBulk);
|
|
126
170
|
if (!didPaginate) {
|
|
127
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false —
|
|
171
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false — final scan`);
|
|
172
|
+
await collectMatches();
|
|
128
173
|
break;
|
|
129
174
|
}
|
|
130
175
|
const pagesJumped = this.tableState.currentPageIndex - prevPage;
|
|
@@ -162,26 +207,37 @@ class RowFinder {
|
|
|
162
207
|
}
|
|
163
208
|
}
|
|
164
209
|
const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filters);
|
|
211
|
+
const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
|
|
212
|
+
let matchedRows = allRows;
|
|
213
|
+
if (Object.keys(domFilters).length > 0) {
|
|
214
|
+
matchedRows = this.filterEngine.applyFilters(allRows, domFilters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
|
|
215
|
+
}
|
|
216
|
+
if (!hasPostFilters) {
|
|
217
|
+
const count = await matchedRows.count();
|
|
218
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
|
|
219
|
+
if (count > 1)
|
|
220
|
+
await this.throwIfAmbiguous(await matchedRows.all(), filters, map);
|
|
221
|
+
if (count === 1)
|
|
222
|
+
return matchedRows.first();
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const candidates = await matchedRows.all();
|
|
226
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: ${candidates.length} DOM candidate(s), post-filtering with override/synthetic columns`);
|
|
227
|
+
const results = await Promise.all(candidates.map(async (c) => {
|
|
228
|
+
if (Object.keys(overrideFilters).length > 0 && !await this.matchesOverrideFilters(c, overrideFilters, map, options.exact || false))
|
|
229
|
+
return false;
|
|
230
|
+
if (Object.keys(syntheticFilters).length > 0 && !await this.matchesSyntheticFilters(c, syntheticFilters, map, options.exact || false))
|
|
231
|
+
return false;
|
|
232
|
+
return true;
|
|
233
|
+
}));
|
|
234
|
+
const postFilterMatches = candidates.filter((_, i) => results[i]);
|
|
235
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: ${postFilterMatches.length} match(es) after post-filter`);
|
|
236
|
+
if (postFilterMatches.length > 1)
|
|
237
|
+
await this.throwIfAmbiguous(postFilterMatches, filters, map);
|
|
238
|
+
if (postFilterMatches.length === 1)
|
|
239
|
+
return postFilterMatches[0];
|
|
182
240
|
}
|
|
183
|
-
if (count === 1)
|
|
184
|
-
return matchedRows.first();
|
|
185
241
|
if (pagesScanned < effectiveMaxPages) {
|
|
186
242
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
|
|
187
243
|
// Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
|
|
@@ -197,25 +253,42 @@ class RowFinder {
|
|
|
197
253
|
continue;
|
|
198
254
|
}
|
|
199
255
|
else {
|
|
200
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}:
|
|
256
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: pagination returned false — final scan`);
|
|
257
|
+
pagesScanned = effectiveMaxPages;
|
|
258
|
+
continue;
|
|
201
259
|
}
|
|
202
260
|
}
|
|
203
261
|
return null;
|
|
204
262
|
}
|
|
205
263
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
264
|
+
resolveRowIndex(rowLocator) {
|
|
265
|
+
// Shared resolver: resolveRowIndex strategy first, else the DOM-position fallback.
|
|
266
|
+
return (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, this.config, () => this.scanDomPosition(rowLocator));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Fallback for findRow when no resolveRowIndex strategy is configured: find the row's
|
|
270
|
+
* position in the current DOM order in a SINGLE roundtrip. Previously this looped over
|
|
271
|
+
* every row calling elementHandle() + evaluate() per row (O(n) CDP roundtrips, and
|
|
272
|
+
* elementHandle() is soft-deprecated). We resolve the target once and let evaluateAll
|
|
273
|
+
* compare it against the full row set in the browser. The row set is resolved through the
|
|
274
|
+
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
275
|
+
* all behave identically. (#350)
|
|
276
|
+
*/
|
|
277
|
+
async throwIfAmbiguous(rows, filters, map) {
|
|
278
|
+
const sampleData = [];
|
|
279
|
+
try {
|
|
280
|
+
const sampleCount = Math.min(rows.length, 3);
|
|
281
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
282
|
+
const rowData = await this.makeSmartRow(rows[i], map, 0, this.tableState.currentPageIndex).toJSON();
|
|
283
|
+
sampleData.push(JSON.stringify(rowData));
|
|
284
|
+
}
|
|
212
285
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
catch (e) { }
|
|
287
|
+
const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
|
|
288
|
+
throw new Error(`Ambiguous Row: Found ${rows.length} rows matching ${JSON.stringify(filters)} on page ${this.tableState.currentPageIndex}. ` +
|
|
289
|
+
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
290
|
+
}
|
|
291
|
+
async scanDomPosition(rowLocator) {
|
|
219
292
|
const targetHandle = await rowLocator.elementHandle();
|
|
220
293
|
if (!targetHandle)
|
|
221
294
|
return undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Locator } from '@playwright/test';
|
|
2
|
+
import type { FinalTableConfig, LoadingStrategy, RowIndexResult, SmartRow } from '../types';
|
|
3
|
+
/** Normalized result from resolveRowIndex — always has `.index`, optionally `.selector`. */
|
|
4
|
+
export interface ResolvedRowIndex {
|
|
5
|
+
index: number;
|
|
6
|
+
selector?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Extract the numeric index from a {@link RowIndexResult}. */
|
|
9
|
+
export declare function normalizeRowIndexResult(result: RowIndexResult): ResolvedRowIndex;
|
|
10
|
+
/**
|
|
11
|
+
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
12
|
+
*
|
|
13
|
+
* Uses the configured `resolveRowIndex` strategy when present (e.g. MUI's `data-rowindex`);
|
|
14
|
+
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
15
|
+
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
16
|
+
*
|
|
17
|
+
* Returns a {@link ResolvedRowIndex} with the numeric index and, when the strategy provides
|
|
18
|
+
* one, a CSS selector for building a self-healing row locator.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveLogicalRowIndex(row: Locator, config: Pick<FinalTableConfig, 'strategies'>, fallback: () => number | undefined | Promise<number | undefined>): Promise<ResolvedRowIndex | undefined>;
|
|
21
|
+
/** What the caller should do with a row after checking its loading state. */
|
|
22
|
+
export type RowLoadingOutcome = 'process' | 'skip' | 'throw';
|
|
23
|
+
/**
|
|
24
|
+
* Single source of truth for the per-row loading wait (#362 consolidation).
|
|
25
|
+
*
|
|
26
|
+
* Collapses the two near-identical blocks in `findRows` (RowFinder) and `runMap`
|
|
27
|
+
* (tableIteration). The only real difference between the callers is the behavior when no
|
|
28
|
+
* `rowLoadingTimeout` is configured: `findRows` skips a still-loading row (`noTimeoutAction:
|
|
29
|
+
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
30
|
+
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
31
|
+
*
|
|
32
|
+
* The helper never throws and never touches the navigation barrier; it returns a decision so
|
|
33
|
+
* each caller keeps its own barrier bookkeeping and error message.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveRowLoading(row: SmartRow, loading: LoadingStrategy | undefined, noTimeoutAction: 'skip' | 'process', log?: (msg: string) => void): Promise<RowLoadingOutcome>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRowIndexResult = normalizeRowIndexResult;
|
|
4
|
+
exports.resolveLogicalRowIndex = resolveLogicalRowIndex;
|
|
5
|
+
exports.resolveRowLoading = resolveRowLoading;
|
|
6
|
+
/** Extract the numeric index from a {@link RowIndexResult}. */
|
|
7
|
+
function normalizeRowIndexResult(result) {
|
|
8
|
+
return typeof result === 'number' ? { index: result } : result;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
12
|
+
*
|
|
13
|
+
* Uses the configured `resolveRowIndex` strategy when present (e.g. MUI's `data-rowindex`);
|
|
14
|
+
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
15
|
+
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
16
|
+
*
|
|
17
|
+
* Returns a {@link ResolvedRowIndex} with the numeric index and, when the strategy provides
|
|
18
|
+
* one, a CSS selector for building a self-healing row locator.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveLogicalRowIndex(row, config, fallback) {
|
|
21
|
+
const strategy = config.strategies.resolveRowIndex;
|
|
22
|
+
if (strategy) {
|
|
23
|
+
const resolved = await strategy(row);
|
|
24
|
+
if (resolved !== undefined)
|
|
25
|
+
return normalizeRowIndexResult(resolved);
|
|
26
|
+
}
|
|
27
|
+
const fb = await fallback();
|
|
28
|
+
return fb !== undefined ? { index: fb } : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Single source of truth for the per-row loading wait (#362 consolidation).
|
|
32
|
+
*
|
|
33
|
+
* Collapses the two near-identical blocks in `findRows` (RowFinder) and `runMap`
|
|
34
|
+
* (tableIteration). The only real difference between the callers is the behavior when no
|
|
35
|
+
* `rowLoadingTimeout` is configured: `findRows` skips a still-loading row (`noTimeoutAction:
|
|
36
|
+
* 'skip'`), while `map`/`forEach`/`filter` process it as-is (`'process'`) — and map, in that
|
|
37
|
+
* case, never even probes `isRowLoading`. Both are preserved exactly.
|
|
38
|
+
*
|
|
39
|
+
* The helper never throws and never touches the navigation barrier; it returns a decision so
|
|
40
|
+
* each caller keeps its own barrier bookkeeping and error message.
|
|
41
|
+
*/
|
|
42
|
+
async function resolveRowLoading(row, loading, noTimeoutAction, log) {
|
|
43
|
+
var _a;
|
|
44
|
+
const isRowLoading = loading === null || loading === void 0 ? void 0 : loading.isRowLoading;
|
|
45
|
+
if (!isRowLoading)
|
|
46
|
+
return 'process';
|
|
47
|
+
const rawTimeout = loading === null || loading === void 0 ? void 0 : loading.rowLoadingTimeout;
|
|
48
|
+
// undefined = unset (legacy skip / process-as-is); 0 = one immediate re-check; >0 = poll up to N ms.
|
|
49
|
+
// Non-finite values are treated as unset (defensive guard against Infinity/NaN).
|
|
50
|
+
const timeout = rawTimeout !== undefined && Number.isFinite(rawTimeout) && rawTimeout >= 0
|
|
51
|
+
? rawTimeout
|
|
52
|
+
: undefined;
|
|
53
|
+
// map's fast path: with no timeout and a process-default, it never probes isRowLoading.
|
|
54
|
+
if (timeout === undefined && noTimeoutAction === 'process')
|
|
55
|
+
return 'process';
|
|
56
|
+
if (!(await isRowLoading(row)))
|
|
57
|
+
return 'process';
|
|
58
|
+
// Row is loading with no timeout configured → legacy skip (findRows).
|
|
59
|
+
if (timeout === undefined)
|
|
60
|
+
return 'skip';
|
|
61
|
+
log === null || log === void 0 ? void 0 : log(`row ${row.rowIndex} — waiting up to ${timeout}ms for row to load`);
|
|
62
|
+
const deadline = Date.now() + timeout;
|
|
63
|
+
let resolved = !(await isRowLoading(row)); // immediate check (handles timeout=0)
|
|
64
|
+
while (!resolved && Date.now() < deadline) {
|
|
65
|
+
// Page accessed lazily (only while actually polling), matching the pre-refactor code.
|
|
66
|
+
await row.page().waitForTimeout(100);
|
|
67
|
+
resolved = !(await isRowLoading(row));
|
|
68
|
+
}
|
|
69
|
+
if (resolved)
|
|
70
|
+
return 'process';
|
|
71
|
+
const onTimeout = (_a = loading === null || loading === void 0 ? void 0 : loading.onRowLoadingTimeout) !== null && _a !== void 0 ? _a : 'read-as-is';
|
|
72
|
+
log === null || log === void 0 ? void 0 : log(`row ${row.rowIndex} — still loading after ${timeout}ms, action: ${onTimeout}`);
|
|
73
|
+
if (onTimeout === 'skip')
|
|
74
|
+
return 'skip';
|
|
75
|
+
if (onTimeout === 'throw')
|
|
76
|
+
return 'throw';
|
|
77
|
+
return 'process'; // 'read-as-is'
|
|
78
|
+
}
|
|
@@ -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';
|
|
@@ -7,11 +7,13 @@ export interface TableIterationEnv<T = any> {
|
|
|
7
7
|
getRowLocators: () => Locator;
|
|
8
8
|
getMap: () => Map<string, number>;
|
|
9
9
|
advancePage: (useBulk: boolean) => Promise<boolean>;
|
|
10
|
-
makeSmartRow: (rowLocator: Locator, map: Map<string, number>, rowIndex: number, tablePageIndex?: number, barrier?: NavigationBarrier) => SmartRow<T>;
|
|
10
|
+
makeSmartRow: (rowLocator: Locator, map: Map<string, number>, rowIndex: number, tablePageIndex?: number, barrier?: NavigationBarrier, rowSelector?: string) => SmartRow<T>;
|
|
11
11
|
createSmartRowArray: (rows: SmartRow<T>[]) => SmartRowArray<T>;
|
|
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>;
|