@rickcedwhat/playwright-smart-table 6.19.0 → 6.20.1-next.19d3cd3
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 +7 -1
- package/dist/engine/rowFinder.js +154 -45
- package/dist/engine/rowResolution.d.ts +11 -4
- package/dist/engine/rowResolution.js +10 -5
- package/dist/engine/tableIteration.d.ts +1 -1
- package/dist/engine/tableIteration.js +38 -15
- 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 +6 -3
- package/dist/presets/glide/headers.js +6 -3
- package/dist/presets/mui.js +3 -1
- package/dist/smartRow.js +167 -62
- package/dist/strategies/contentReady.d.ts +13 -0
- package/dist/strategies/contentReady.js +29 -0
- package/dist/strategies/fill.js +0 -1
- package/dist/strategies/headers.js +6 -3
- package/dist/strategies/index.d.ts +7 -0
- package/dist/strategies/index.js +3 -0
- package/dist/strategies/pagination.js +11 -2
- package/dist/strategies/viewport.js +26 -19
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +68 -8
- package/dist/types.d.ts +67 -8
- package/dist/useTable.js +115 -24
- package/dist/utils/elementTracker.js +6 -3
- package/dist/utils/mergeTableConfig.js +4 -0
- package/package.json +6 -1
|
@@ -13,13 +13,18 @@ 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;
|
|
22
26
|
}): Promise<SmartRow<T>>;
|
|
27
|
+
private waitForTableReady;
|
|
23
28
|
findRows(filters?: Record<string, FilterValue>, options?: {
|
|
24
29
|
exact?: boolean;
|
|
25
30
|
maxPages?: number;
|
|
@@ -36,5 +41,6 @@ export declare class RowFinder<T = any> {
|
|
|
36
41
|
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
37
42
|
* all behave identically. (#350)
|
|
38
43
|
*/
|
|
44
|
+
private throwIfAmbiguous;
|
|
39
45
|
private scanDomPosition;
|
|
40
46
|
}
|
package/dist/engine/rowFinder.js
CHANGED
|
@@ -18,6 +18,73 @@ class RowFinder {
|
|
|
18
18
|
this.advancePage = advancePage;
|
|
19
19
|
this.resolve = resolve;
|
|
20
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
|
+
}
|
|
21
88
|
async findRow(filters, options = {}) {
|
|
22
89
|
(0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
|
|
23
90
|
await this.tableMapper.getMap();
|
|
@@ -26,8 +93,8 @@ class RowFinder {
|
|
|
26
93
|
(0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
|
|
27
94
|
await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
28
95
|
const map = await this.tableMapper.getMap();
|
|
29
|
-
const
|
|
30
|
-
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);
|
|
31
98
|
}
|
|
32
99
|
(0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
|
|
33
100
|
await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
@@ -37,6 +104,22 @@ class RowFinder {
|
|
|
37
104
|
smartRow[sentinel_1.SENTINEL_ROW] = true;
|
|
38
105
|
return smartRow;
|
|
39
106
|
}
|
|
107
|
+
async waitForTableReady() {
|
|
108
|
+
var _a;
|
|
109
|
+
const isTableLoading = (_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading;
|
|
110
|
+
if (!isTableLoading)
|
|
111
|
+
return;
|
|
112
|
+
const context = {
|
|
113
|
+
root: this.rootLocator,
|
|
114
|
+
config: this.config,
|
|
115
|
+
page: this.rootLocator.page(),
|
|
116
|
+
resolve: this.resolve
|
|
117
|
+
};
|
|
118
|
+
while (await isTableLoading(context)) {
|
|
119
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
|
|
120
|
+
await this.rootLocator.page().waitForTimeout(200);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
40
123
|
async findRows(filters = {}, options) {
|
|
41
124
|
var _a, _b, _c;
|
|
42
125
|
const filtersRecord = filters;
|
|
@@ -47,12 +130,15 @@ class RowFinder {
|
|
|
47
130
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
|
|
48
131
|
const tracker = new elementTracker_1.ElementTracker('findRows');
|
|
49
132
|
try {
|
|
133
|
+
await this.waitForTableReady();
|
|
134
|
+
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filtersRecord);
|
|
135
|
+
const hasOverrideFilters = Object.keys(overrideFilters).length > 0;
|
|
136
|
+
const hasSyntheticFilters = Object.keys(syntheticFilters).length > 0;
|
|
50
137
|
const collectMatches = async () => {
|
|
51
|
-
var _a, _b;
|
|
138
|
+
var _a, _b, _c, _d;
|
|
52
139
|
let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
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);
|
|
140
|
+
if (Object.keys(domFilters).length > 0) {
|
|
141
|
+
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);
|
|
56
142
|
}
|
|
57
143
|
// Get only newly seen matched rows
|
|
58
144
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
@@ -62,8 +148,8 @@ class RowFinder {
|
|
|
62
148
|
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
63
149
|
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
64
150
|
for (const idx of newIndices) {
|
|
65
|
-
const
|
|
66
|
-
const smartRow = this.makeSmartRow(currentRows[idx], map,
|
|
151
|
+
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
|
|
152
|
+
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);
|
|
67
153
|
// findRows skips a still-loading row when no timeout is configured (legacy
|
|
68
154
|
// behavior) — see resolveRowLoading's `noTimeoutAction: 'skip'`.
|
|
69
155
|
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
|
|
@@ -74,6 +160,14 @@ class RowFinder {
|
|
|
74
160
|
}
|
|
75
161
|
continue; // 'skip'
|
|
76
162
|
}
|
|
163
|
+
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)) {
|
|
164
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
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)) {
|
|
168
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
77
171
|
allRows.push(smartRow);
|
|
78
172
|
added++;
|
|
79
173
|
}
|
|
@@ -91,7 +185,8 @@ class RowFinder {
|
|
|
91
185
|
const prevPage = this.tableState.currentPageIndex;
|
|
92
186
|
const didPaginate = await this.advancePage(useBulk);
|
|
93
187
|
if (!didPaginate) {
|
|
94
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false —
|
|
188
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false — final scan`);
|
|
189
|
+
await collectMatches();
|
|
95
190
|
break;
|
|
96
191
|
}
|
|
97
192
|
const pagesJumped = this.tableState.currentPageIndex - prevPage;
|
|
@@ -108,52 +203,50 @@ class RowFinder {
|
|
|
108
203
|
return (0, smartRowArray_1.createSmartRowArray)(allRows);
|
|
109
204
|
}
|
|
110
205
|
async findRowLocator(filters, options = {}) {
|
|
111
|
-
var _a, _b
|
|
206
|
+
var _a, _b;
|
|
112
207
|
const map = await this.tableMapper.getMap();
|
|
113
208
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
|
|
114
209
|
let pagesScanned = 1;
|
|
115
210
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
|
|
116
211
|
while (true) {
|
|
117
|
-
|
|
118
|
-
if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
|
|
119
|
-
const isLoading = await this.config.strategies.loading.isTableLoading({
|
|
120
|
-
root: this.rootLocator,
|
|
121
|
-
config: this.config,
|
|
122
|
-
page: this.rootLocator.page(),
|
|
123
|
-
resolve: this.resolve
|
|
124
|
-
});
|
|
125
|
-
if (isLoading) {
|
|
126
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
|
|
127
|
-
await this.rootLocator.page().waitForTimeout(200);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
212
|
+
await this.waitForTableReady();
|
|
131
213
|
const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
214
|
+
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filters);
|
|
215
|
+
const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
|
|
216
|
+
let matchedRows = allRows;
|
|
217
|
+
if (Object.keys(domFilters).length > 0) {
|
|
218
|
+
matchedRows = this.filterEngine.applyFilters(allRows, domFilters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
|
|
219
|
+
}
|
|
220
|
+
if (!hasPostFilters) {
|
|
221
|
+
const count = await matchedRows.count();
|
|
222
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
|
|
223
|
+
if (count > 1)
|
|
224
|
+
await this.throwIfAmbiguous(await matchedRows.all(), filters, map);
|
|
225
|
+
if (count === 1)
|
|
226
|
+
return matchedRows.first();
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const candidates = await matchedRows.all();
|
|
230
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: ${candidates.length} DOM candidate(s), post-filtering with override/synthetic columns`);
|
|
231
|
+
const results = await Promise.all(candidates.map(async (c) => {
|
|
232
|
+
if (Object.keys(overrideFilters).length > 0 && !await this.matchesOverrideFilters(c, overrideFilters, map, options.exact || false))
|
|
233
|
+
return false;
|
|
234
|
+
if (Object.keys(syntheticFilters).length > 0 && !await this.matchesSyntheticFilters(c, syntheticFilters, map, options.exact || false))
|
|
235
|
+
return false;
|
|
236
|
+
return true;
|
|
237
|
+
}));
|
|
238
|
+
const postFilterMatches = candidates.filter((_, i) => results[i]);
|
|
239
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: ${postFilterMatches.length} match(es) after post-filter`);
|
|
240
|
+
if (postFilterMatches.length > 1)
|
|
241
|
+
await this.throwIfAmbiguous(postFilterMatches, filters, map);
|
|
242
|
+
if (postFilterMatches.length === 1)
|
|
243
|
+
return postFilterMatches[0];
|
|
149
244
|
}
|
|
150
|
-
if (count === 1)
|
|
151
|
-
return matchedRows.first();
|
|
152
245
|
if (pagesScanned < effectiveMaxPages) {
|
|
153
246
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
|
|
154
247
|
// Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
|
|
155
248
|
// Bulk-by-default made findRow jump past the page holding the target row.
|
|
156
|
-
const useBulk = options.useBulkPagination === true && !!((
|
|
249
|
+
const useBulk = options.useBulkPagination === true && !!((_b = this.config.strategies.pagination) === null || _b === void 0 ? void 0 : _b.goNextBulk);
|
|
157
250
|
const prevPage = this.tableState.currentPageIndex;
|
|
158
251
|
const didLoadMore = await this.advancePage(useBulk);
|
|
159
252
|
if (didLoadMore) {
|
|
@@ -164,7 +257,9 @@ class RowFinder {
|
|
|
164
257
|
continue;
|
|
165
258
|
}
|
|
166
259
|
else {
|
|
167
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}:
|
|
260
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: pagination returned false — final scan`);
|
|
261
|
+
pagesScanned = effectiveMaxPages;
|
|
262
|
+
continue;
|
|
168
263
|
}
|
|
169
264
|
}
|
|
170
265
|
return null;
|
|
@@ -183,6 +278,20 @@ class RowFinder {
|
|
|
183
278
|
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
184
279
|
* all behave identically. (#350)
|
|
185
280
|
*/
|
|
281
|
+
async throwIfAmbiguous(rows, filters, map) {
|
|
282
|
+
const sampleData = [];
|
|
283
|
+
try {
|
|
284
|
+
const sampleCount = Math.min(rows.length, 3);
|
|
285
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
286
|
+
const rowData = await this.makeSmartRow(rows[i], map, 0, this.tableState.currentPageIndex).toJSON();
|
|
287
|
+
sampleData.push(JSON.stringify(rowData));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e) { }
|
|
291
|
+
const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
|
|
292
|
+
throw new Error(`Ambiguous Row: Found ${rows.length} rows matching ${JSON.stringify(filters)} on page ${this.tableState.currentPageIndex}. ` +
|
|
293
|
+
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
294
|
+
}
|
|
186
295
|
async scanDomPosition(rowLocator) {
|
|
187
296
|
const targetHandle = await rowLocator.elementHandle();
|
|
188
297
|
if (!targetHandle)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Locator } from '@playwright/test';
|
|
2
|
-
import type { FinalTableConfig, LoadingStrategy, SmartRow } from '../types';
|
|
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;
|
|
3
10
|
/**
|
|
4
11
|
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
5
12
|
*
|
|
@@ -7,10 +14,10 @@ import type { FinalTableConfig, LoadingStrategy, SmartRow } from '../types';
|
|
|
7
14
|
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
8
15
|
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
19
|
*/
|
|
13
|
-
export declare function resolveLogicalRowIndex(row: Locator, config: Pick<FinalTableConfig, 'strategies'>, fallback: () => number | undefined | Promise<number | undefined>): Promise<
|
|
20
|
+
export declare function resolveLogicalRowIndex(row: Locator, config: Pick<FinalTableConfig, 'strategies'>, fallback: () => number | undefined | Promise<number | undefined>): Promise<ResolvedRowIndex | undefined>;
|
|
14
21
|
/** What the caller should do with a row after checking its loading state. */
|
|
15
22
|
export type RowLoadingOutcome = 'process' | 'skip' | 'throw';
|
|
16
23
|
/**
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRowIndexResult = normalizeRowIndexResult;
|
|
3
4
|
exports.resolveLogicalRowIndex = resolveLogicalRowIndex;
|
|
4
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
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* Single source of truth for "what is this row's logical index?" (#362 consolidation).
|
|
7
12
|
*
|
|
@@ -9,18 +14,18 @@ exports.resolveRowLoading = resolveRowLoading;
|
|
|
9
14
|
* otherwise, and when the strategy returns `undefined`, defers to the caller-provided
|
|
10
15
|
* `fallback` — a running counter for iteration/`findRows`, or a DOM-position scan for `findRow`.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
19
|
*/
|
|
15
20
|
async function resolveLogicalRowIndex(row, config, fallback) {
|
|
16
21
|
const strategy = config.strategies.resolveRowIndex;
|
|
17
22
|
if (strategy) {
|
|
18
23
|
const resolved = await strategy(row);
|
|
19
24
|
if (resolved !== undefined)
|
|
20
|
-
return resolved;
|
|
21
|
-
// Strategy returned undefined — fall through to the caller's fallback.
|
|
25
|
+
return normalizeRowIndexResult(resolved);
|
|
22
26
|
}
|
|
23
|
-
|
|
27
|
+
const fb = await fallback();
|
|
28
|
+
return fb !== undefined ? { index: fb } : undefined;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Single source of truth for the per-row loading wait (#362 consolidation).
|
|
@@ -7,7 +7,7 @@ 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;
|
|
@@ -37,11 +37,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
37
37
|
const tracker = new elementTracker_1.ElementTracker(label);
|
|
38
38
|
log(env.config, `${label}: starting (maxPages=${effectiveMaxPages}, mode=${concurrency}, dedupe=${!!dedupeStrategy})`);
|
|
39
39
|
const results = [];
|
|
40
|
+
const seenLogicalIndices = new Set();
|
|
40
41
|
try {
|
|
41
42
|
let rowIndex = 0;
|
|
42
43
|
let stopped = false;
|
|
43
44
|
let stoppedIndex = Infinity;
|
|
44
45
|
let pagesScanned = 1;
|
|
46
|
+
let reachedEnd = false;
|
|
45
47
|
const stop = (idx) => {
|
|
46
48
|
if (!stopped) {
|
|
47
49
|
log(env.config, `${label}: stop() called at row ${idx} — halting`);
|
|
@@ -58,11 +60,19 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
58
60
|
// they're picked up on a later page once scrolled into view — preventing overscan from
|
|
59
61
|
// being ingested (#353) or double-collected via node recycling (#357). No-op when no
|
|
60
62
|
// viewport reports visible rows.
|
|
63
|
+
//
|
|
64
|
+
// #384: overscan rows ABOVE the visible range have already scrolled past and will never
|
|
65
|
+
// re-enter the viewport — collect them now. Only defer rows BELOW the visible range
|
|
66
|
+
// (they'll scroll into view on a later page).
|
|
67
|
+
//
|
|
68
|
+
// #417 follow-up: skip viewport filtering on the final scan — there are no more pages
|
|
69
|
+
// to defer to, so collect all remaining unseen rows regardless of visibility.
|
|
61
70
|
let candidateIndices = allIndices;
|
|
62
71
|
const getVisibleRowIndices = (_f = env.config.strategies.viewport) === null || _f === void 0 ? void 0 : _f.getVisibleRowIndices;
|
|
63
|
-
if (getVisibleRowIndices) {
|
|
72
|
+
if (getVisibleRowIndices && !reachedEnd) {
|
|
64
73
|
const visible = new Set(await getVisibleRowIndices(env.getContext()));
|
|
65
|
-
|
|
74
|
+
const minVisible = visible.size > 0 ? Math.min(...visible) : Infinity;
|
|
75
|
+
candidateIndices = allIndices.filter(idx => visible.has(idx) || idx < minVisible);
|
|
66
76
|
if (candidateIndices.length !== allIndices.length) {
|
|
67
77
|
log(env.config, `${label}: viewport visible filter — ${candidateIndices.length}/${allIndices.length} row(s) in view`);
|
|
68
78
|
}
|
|
@@ -90,8 +100,9 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
90
100
|
// correct on virtualized tables); otherwise it equals the enumeration counter.
|
|
91
101
|
const smartRows = await Promise.all(newIndices.map(async (idx, i) => {
|
|
92
102
|
var _a;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
103
|
+
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], env.config, () => positionBase + i);
|
|
104
|
+
const logicalIndex = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.index) !== null && _a !== void 0 ? _a : (positionBase + i);
|
|
105
|
+
const sr = env.makeSmartRow(pageRows[idx], map, logicalIndex, env.getCurrentPageIndex(), barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
|
|
95
106
|
// Mark as part of an iteration batch so toJSON's #366 re-pin uses rescan-only recovery
|
|
96
107
|
// (no scroll-back) — scrolling here would disrupt sibling rows' positional locators.
|
|
97
108
|
sr._inBatch = true;
|
|
@@ -104,6 +115,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
104
115
|
if (stopped && enumIndex > stoppedIndex) {
|
|
105
116
|
return SKIP;
|
|
106
117
|
}
|
|
118
|
+
// When resolveRowIndex is configured, identical logical indices across pages
|
|
119
|
+
// indicate the same record (e.g. re-created DOM elements in recycling
|
|
120
|
+
// virtualizers whose WeakMap entry was GC'd). Skip without processing.
|
|
121
|
+
if (row.rowIndex !== undefined && seenLogicalIndices.has(row.rowIndex)) {
|
|
122
|
+
log(env.config, `${label}: skipping duplicate logical row ${row.rowIndex}`);
|
|
123
|
+
return SKIP;
|
|
124
|
+
}
|
|
107
125
|
// Wait for the row to finish loading BEFORE evaluating its dedupe key (Bug #355).
|
|
108
126
|
// map/forEach/filter process a still-loading row when no timeout is set (unlike
|
|
109
127
|
// findRows, which skips) — see resolveRowLoading's `noTimeoutAction`.
|
|
@@ -113,13 +131,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
113
131
|
if (loadingOutcome === 'throw') {
|
|
114
132
|
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`);
|
|
115
133
|
}
|
|
134
|
+
let dedupeKey;
|
|
116
135
|
if (dedupeKeys && dedupeStrategy) {
|
|
117
|
-
|
|
118
|
-
if (dedupeKeys.has(
|
|
119
|
-
log(env.config, `${label}: dedupe skip key="${
|
|
136
|
+
dedupeKey = await dedupeStrategy(row);
|
|
137
|
+
if (dedupeKeys.has(dedupeKey)) {
|
|
138
|
+
log(env.config, `${label}: dedupe skip key="${dedupeKey}"`);
|
|
120
139
|
return SKIP;
|
|
121
140
|
}
|
|
122
|
-
dedupeKeys.add(key);
|
|
123
141
|
}
|
|
124
142
|
// Execute callback (optionally serialized via mutex)
|
|
125
143
|
const runCallback = async () => {
|
|
@@ -128,12 +146,16 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
128
146
|
log(env.config, `${label}: processing row (index ${enumIndex}, rowIndex ${row.rowIndex})`);
|
|
129
147
|
return await callback({ row, index: enumIndex, rowIndex: row.rowIndex, pageIndex: env.getCurrentPageIndex(), stop: () => stop(enumIndex) });
|
|
130
148
|
};
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
const result = actionMutex
|
|
150
|
+
? await actionMutex.run(runCallback)
|
|
151
|
+
: await runCallback();
|
|
152
|
+
if (dedupeKeys && dedupeKey !== undefined && result !== SKIP) {
|
|
153
|
+
dedupeKeys.add(dedupeKey);
|
|
133
154
|
}
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
if (row.rowIndex !== undefined && result !== SKIP) {
|
|
156
|
+
seenLogicalIndices.add(row.rowIndex);
|
|
136
157
|
}
|
|
158
|
+
return result;
|
|
137
159
|
}
|
|
138
160
|
finally {
|
|
139
161
|
// Ensure the barrier is notified once per row, even on error or skip.
|
|
@@ -159,12 +181,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
159
181
|
}
|
|
160
182
|
rowIndex += batchSize;
|
|
161
183
|
}
|
|
162
|
-
if (stopped || pagesScanned >= effectiveMaxPages)
|
|
184
|
+
if (stopped || pagesScanned >= effectiveMaxPages || reachedEnd)
|
|
163
185
|
break;
|
|
164
186
|
log(env.config, `${label}: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
|
|
165
187
|
if (!await env.advancePage(useBulk)) {
|
|
166
|
-
log(env.config, `${label}:
|
|
167
|
-
|
|
188
|
+
log(env.config, `${label}: pagination returned false — final scan`);
|
|
189
|
+
reachedEnd = true;
|
|
190
|
+
continue;
|
|
168
191
|
}
|
|
169
192
|
pagesScanned++;
|
|
170
193
|
}
|
|
@@ -14,7 +14,7 @@ class TableMapper {
|
|
|
14
14
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
|
|
15
15
|
}
|
|
16
16
|
async getMap(timeout) {
|
|
17
|
-
var _a;
|
|
17
|
+
var _a, _b;
|
|
18
18
|
if (this._headerMap)
|
|
19
19
|
return this._headerMap;
|
|
20
20
|
this.log('Mapping headers...');
|
|
@@ -56,8 +56,15 @@ class TableMapper {
|
|
|
56
56
|
try {
|
|
57
57
|
const rawHeaders = await strategy(context);
|
|
58
58
|
const entries = await this.processHeaders(rawHeaders);
|
|
59
|
-
// Success
|
|
60
|
-
|
|
59
|
+
// Success — validate before caching so a collision doesn't leave stale state
|
|
60
|
+
const headerMap = new Map(entries);
|
|
61
|
+
const syntheticNames = Object.keys((_b = this.config.syntheticColumns) !== null && _b !== void 0 ? _b : {});
|
|
62
|
+
const collisions = syntheticNames.filter(name => headerMap.has(name));
|
|
63
|
+
if (collisions.length > 0) {
|
|
64
|
+
throw new Error(`Synthetic column name(s) collide with real header(s): ${collisions.join(', ')}. ` +
|
|
65
|
+
`Rename the synthetic column or use columnOverrides for columns that exist in the DOM.`);
|
|
66
|
+
}
|
|
67
|
+
this._headerMap = headerMap;
|
|
61
68
|
this.log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
|
|
62
69
|
return this._headerMap;
|
|
63
70
|
}
|
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, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, } from './types';
|
|
3
|
+
export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, } from './types';
|
|
4
4
|
export { Strategies } from './strategies';
|
|
5
5
|
export * as presets from './presets';
|
|
6
6
|
/** @deprecated Use `presets` instead. Plugins will be removed in v7.0.0. */
|
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.20.1-next.19d3cd3";
|
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.20.1-next.19d3cd3";
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ export declare const Plugins: {
|
|
|
23
23
|
debug?: import("..").DebugConfig | undefined;
|
|
24
24
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
25
25
|
strategies?: import("../types").TableStrategies | undefined;
|
|
26
|
-
columnOverrides?: Partial<Record<string | number | symbol, import("
|
|
26
|
+
columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
|
|
27
|
+
syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
|
|
27
28
|
emptyState?: import("@playwright/test").Locator | undefined;
|
|
28
29
|
};
|
|
29
30
|
/** @deprecated Use `presets.glide` */
|
|
@@ -44,7 +45,8 @@ export declare const Plugins: {
|
|
|
44
45
|
debug?: import("..").DebugConfig | undefined;
|
|
45
46
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
46
47
|
strategies?: import("../types").TableStrategies | undefined;
|
|
47
|
-
columnOverrides?: Partial<Record<string | number | symbol, import("
|
|
48
|
+
columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
|
|
49
|
+
syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
|
|
48
50
|
emptyState?: import("@playwright/test").Locator | undefined;
|
|
49
51
|
};
|
|
50
52
|
/** @deprecated Use `presets.muiDataGrid` */
|
|
@@ -65,7 +67,8 @@ export declare const Plugins: {
|
|
|
65
67
|
debug?: import("..").DebugConfig | undefined;
|
|
66
68
|
onReset?: ((context: import("..").TableContext) => Promise<void>) | undefined;
|
|
67
69
|
strategies?: import("../types").TableStrategies | undefined;
|
|
68
|
-
columnOverrides?: Partial<Record<string | number | symbol, import("
|
|
70
|
+
columnOverrides?: Partial<Record<string | number | symbol, import("..").ColumnOverride<any>>> | undefined;
|
|
71
|
+
syntheticColumns?: Record<string, import("..").SyntheticColumnDef<any>> | undefined;
|
|
69
72
|
emptyState?: import("@playwright/test").Locator | undefined;
|
|
70
73
|
};
|
|
71
74
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scrollRightHeader = void 0;
|
|
4
|
+
const debugUtils_1 = require("../../utils/debugUtils");
|
|
4
5
|
/**
|
|
5
6
|
* Scans for headers by finding a scrollable container and setting scrollLeft.
|
|
6
7
|
*/
|
|
@@ -47,10 +48,12 @@ const scrollRightHeader = async (context, options) => {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
else {
|
|
50
|
-
|
|
51
|
+
(0, debugUtils_1.logDebug)(config, 'info', "HeaderStrategies.scrollRight: Could not find scroller. Returning visible headers.");
|
|
52
|
+
}
|
|
53
|
+
if (isScrollerFound) {
|
|
54
|
+
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
55
|
+
await page.waitForTimeout(200);
|
|
51
56
|
}
|
|
52
|
-
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
53
|
-
await page.waitForTimeout(200);
|
|
54
57
|
return Array.from(collectedHeaders);
|
|
55
58
|
};
|
|
56
59
|
exports.scrollRightHeader = scrollRightHeader;
|
package/dist/presets/mui.js
CHANGED
|
@@ -200,7 +200,9 @@ function createMuiDataGrid(opts) {
|
|
|
200
200
|
strategies: {
|
|
201
201
|
resolveRowIndex: async (row) => {
|
|
202
202
|
const v = await row.getAttribute('data-rowindex').catch(() => null);
|
|
203
|
-
|
|
203
|
+
if (v === null || isNaN(Number(v)))
|
|
204
|
+
return undefined;
|
|
205
|
+
return { index: Number(v), selector: `[data-rowindex="${v}"]` };
|
|
204
206
|
},
|
|
205
207
|
pagination: {
|
|
206
208
|
goNext: async (context) => {
|