@rickcedwhat/playwright-smart-table 6.19.0 → 6.20.1-next.230dedf
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 +6 -1
- package/dist/engine/rowFinder.js +134 -29
- 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 +24 -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 +91 -60
- package/dist/strategies/fill.js +0 -1
- package/dist/strategies/headers.js +6 -3
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +48 -8
- package/dist/types.d.ts +49 -8
- package/dist/useTable.js +106 -24
- package/dist/utils/mergeTableConfig.js +4 -0
- package/package.json +6 -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;
|
|
@@ -36,5 +40,6 @@ export declare class RowFinder<T = any> {
|
|
|
36
40
|
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
37
41
|
* all behave identically. (#350)
|
|
38
42
|
*/
|
|
43
|
+
private throwIfAmbiguous;
|
|
39
44
|
private scanDomPosition;
|
|
40
45
|
}
|
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');
|
|
@@ -47,12 +114,14 @@ class RowFinder {
|
|
|
47
114
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
|
|
48
115
|
const tracker = new elementTracker_1.ElementTracker('findRows');
|
|
49
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;
|
|
50
120
|
const collectMatches = async () => {
|
|
51
|
-
var _a, _b;
|
|
121
|
+
var _a, _b, _c, _d;
|
|
52
122
|
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);
|
|
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);
|
|
56
125
|
}
|
|
57
126
|
// Get only newly seen matched rows
|
|
58
127
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
@@ -62,8 +131,8 @@ class RowFinder {
|
|
|
62
131
|
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
63
132
|
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
64
133
|
for (const idx of newIndices) {
|
|
65
|
-
const
|
|
66
|
-
const smartRow = this.makeSmartRow(currentRows[idx], map,
|
|
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);
|
|
67
136
|
// findRows skips a still-loading row when no timeout is configured (legacy
|
|
68
137
|
// behavior) — see resolveRowLoading's `noTimeoutAction: 'skip'`.
|
|
69
138
|
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 +143,14 @@ class RowFinder {
|
|
|
74
143
|
}
|
|
75
144
|
continue; // 'skip'
|
|
76
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;
|
|
153
|
+
}
|
|
77
154
|
allRows.push(smartRow);
|
|
78
155
|
added++;
|
|
79
156
|
}
|
|
@@ -91,7 +168,8 @@ class RowFinder {
|
|
|
91
168
|
const prevPage = this.tableState.currentPageIndex;
|
|
92
169
|
const didPaginate = await this.advancePage(useBulk);
|
|
93
170
|
if (!didPaginate) {
|
|
94
|
-
(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();
|
|
95
173
|
break;
|
|
96
174
|
}
|
|
97
175
|
const pagesJumped = this.tableState.currentPageIndex - prevPage;
|
|
@@ -129,26 +207,37 @@ class RowFinder {
|
|
|
129
207
|
}
|
|
130
208
|
}
|
|
131
209
|
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
|
-
|
|
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];
|
|
149
240
|
}
|
|
150
|
-
if (count === 1)
|
|
151
|
-
return matchedRows.first();
|
|
152
241
|
if (pagesScanned < effectiveMaxPages) {
|
|
153
242
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
|
|
154
243
|
// Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
|
|
@@ -164,7 +253,9 @@ class RowFinder {
|
|
|
164
253
|
continue;
|
|
165
254
|
}
|
|
166
255
|
else {
|
|
167
|
-
(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;
|
|
168
259
|
}
|
|
169
260
|
}
|
|
170
261
|
return null;
|
|
@@ -183,6 +274,20 @@ class RowFinder {
|
|
|
183
274
|
* same selector/scope as everywhere else, so a string, function, or Locator rowSelector
|
|
184
275
|
* all behave identically. (#350)
|
|
185
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
|
+
}
|
|
285
|
+
}
|
|
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
|
+
}
|
|
186
291
|
async scanDomPosition(rowLocator) {
|
|
187
292
|
const targetHandle = await rowLocator.elementHandle();
|
|
188
293
|
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;
|
|
@@ -42,6 +42,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
42
42
|
let stopped = false;
|
|
43
43
|
let stoppedIndex = Infinity;
|
|
44
44
|
let pagesScanned = 1;
|
|
45
|
+
let reachedEnd = false;
|
|
45
46
|
const stop = (idx) => {
|
|
46
47
|
if (!stopped) {
|
|
47
48
|
log(env.config, `${label}: stop() called at row ${idx} — halting`);
|
|
@@ -58,11 +59,16 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
58
59
|
// they're picked up on a later page once scrolled into view — preventing overscan from
|
|
59
60
|
// being ingested (#353) or double-collected via node recycling (#357). No-op when no
|
|
60
61
|
// viewport reports visible rows.
|
|
62
|
+
//
|
|
63
|
+
// #384: overscan rows ABOVE the visible range have already scrolled past and will never
|
|
64
|
+
// re-enter the viewport — collect them now. Only defer rows BELOW the visible range
|
|
65
|
+
// (they'll scroll into view on a later page).
|
|
61
66
|
let candidateIndices = allIndices;
|
|
62
67
|
const getVisibleRowIndices = (_f = env.config.strategies.viewport) === null || _f === void 0 ? void 0 : _f.getVisibleRowIndices;
|
|
63
68
|
if (getVisibleRowIndices) {
|
|
64
69
|
const visible = new Set(await getVisibleRowIndices(env.getContext()));
|
|
65
|
-
|
|
70
|
+
const minVisible = visible.size > 0 ? Math.min(...visible) : Infinity;
|
|
71
|
+
candidateIndices = allIndices.filter(idx => visible.has(idx) || idx < minVisible);
|
|
66
72
|
if (candidateIndices.length !== allIndices.length) {
|
|
67
73
|
log(env.config, `${label}: viewport visible filter — ${candidateIndices.length}/${allIndices.length} row(s) in view`);
|
|
68
74
|
}
|
|
@@ -90,8 +96,9 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
90
96
|
// correct on virtualized tables); otherwise it equals the enumeration counter.
|
|
91
97
|
const smartRows = await Promise.all(newIndices.map(async (idx, i) => {
|
|
92
98
|
var _a;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
99
|
+
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], env.config, () => positionBase + i);
|
|
100
|
+
const logicalIndex = (_a = resolved === null || resolved === void 0 ? void 0 : resolved.index) !== null && _a !== void 0 ? _a : (positionBase + i);
|
|
101
|
+
const sr = env.makeSmartRow(pageRows[idx], map, logicalIndex, env.getCurrentPageIndex(), barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
|
|
95
102
|
// Mark as part of an iteration batch so toJSON's #366 re-pin uses rescan-only recovery
|
|
96
103
|
// (no scroll-back) — scrolling here would disrupt sibling rows' positional locators.
|
|
97
104
|
sr._inBatch = true;
|
|
@@ -113,13 +120,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
113
120
|
if (loadingOutcome === 'throw') {
|
|
114
121
|
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
122
|
}
|
|
123
|
+
let dedupeKey;
|
|
116
124
|
if (dedupeKeys && dedupeStrategy) {
|
|
117
|
-
|
|
118
|
-
if (dedupeKeys.has(
|
|
119
|
-
log(env.config, `${label}: dedupe skip key="${
|
|
125
|
+
dedupeKey = await dedupeStrategy(row);
|
|
126
|
+
if (dedupeKeys.has(dedupeKey)) {
|
|
127
|
+
log(env.config, `${label}: dedupe skip key="${dedupeKey}"`);
|
|
120
128
|
return SKIP;
|
|
121
129
|
}
|
|
122
|
-
dedupeKeys.add(key);
|
|
123
130
|
}
|
|
124
131
|
// Execute callback (optionally serialized via mutex)
|
|
125
132
|
const runCallback = async () => {
|
|
@@ -128,12 +135,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
128
135
|
log(env.config, `${label}: processing row (index ${enumIndex}, rowIndex ${row.rowIndex})`);
|
|
129
136
|
return await callback({ row, index: enumIndex, rowIndex: row.rowIndex, pageIndex: env.getCurrentPageIndex(), stop: () => stop(enumIndex) });
|
|
130
137
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
const result = actionMutex
|
|
139
|
+
? await actionMutex.run(runCallback)
|
|
140
|
+
: await runCallback();
|
|
141
|
+
if (dedupeKeys && dedupeKey !== undefined && result !== SKIP) {
|
|
142
|
+
dedupeKeys.add(dedupeKey);
|
|
136
143
|
}
|
|
144
|
+
return result;
|
|
137
145
|
}
|
|
138
146
|
finally {
|
|
139
147
|
// Ensure the barrier is notified once per row, even on error or skip.
|
|
@@ -159,12 +167,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
159
167
|
}
|
|
160
168
|
rowIndex += batchSize;
|
|
161
169
|
}
|
|
162
|
-
if (stopped || pagesScanned >= effectiveMaxPages)
|
|
170
|
+
if (stopped || pagesScanned >= effectiveMaxPages || reachedEnd)
|
|
163
171
|
break;
|
|
164
172
|
log(env.config, `${label}: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
|
|
165
173
|
if (!await env.advancePage(useBulk)) {
|
|
166
|
-
log(env.config, `${label}:
|
|
167
|
-
|
|
174
|
+
log(env.config, `${label}: pagination returned false — final scan`);
|
|
175
|
+
reachedEnd = true;
|
|
176
|
+
continue;
|
|
168
177
|
}
|
|
169
178
|
pagesScanned++;
|
|
170
179
|
}
|
|
@@ -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.230dedf";
|
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.230dedf";
|
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) => {
|