@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.
@@ -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
  }
@@ -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 rowIndex = await this.resolveRowIndex(rowLocator);
29
- return this.makeSmartRow(rowLocator, map, rowIndex, this.tableState.currentPageIndex);
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, _e, _f;
121
+ var _a, _b, _c, _d;
51
122
  let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
52
- // Only apply filters if we have them
53
- if (Object.keys(filtersRecord).length > 0) {
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
- // Use the configured strategy (O(1), e.g. MUI data-rowindex) when available.
73
- // Without a strategy, fall back to sequential position to avoid the O(n)
74
- // elementHandle scan that resolveRowIndex() would otherwise perform per row.
75
- const rowIndex = this.config.strategies.resolveRowIndex
76
- ? ((_f = await this.config.strategies.resolveRowIndex(currentRows[idx])) !== null && _f !== void 0 ? _f : allRows.length)
77
- : allRows.length;
78
- const smartRow = this.makeSmartRow(currentRows[idx], map, rowIndex, this.tableState.currentPageIndex, barrier);
79
- if (isRowLoading && await isRowLoading(smartRow)) {
80
- if (rowLoadingTimeout === undefined) {
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 — end of data`);
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 matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
166
- const count = await matchedRows.count();
167
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
168
- if (count > 1) {
169
- const sampleData = [];
170
- try {
171
- const firstFewRows = await matchedRows.all();
172
- const sampleCount = Math.min(firstFewRows.length, 3);
173
- for (let i = 0; i < sampleCount; i++) {
174
- const rowData = await this.makeSmartRow(firstFewRows[i], map, 0, this.tableState.currentPageIndex).toJSON();
175
- sampleData.push(JSON.stringify(rowData));
176
- }
177
- }
178
- catch (e) { }
179
- const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
180
- throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${this.tableState.currentPageIndex}. ` +
181
- `Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
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}: Pagination failed (end of data).`);
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
- async resolveRowIndex(rowLocator) {
207
- if (this.config.strategies.resolveRowIndex) {
208
- const result = await this.config.strategies.resolveRowIndex(rowLocator);
209
- if (result !== undefined)
210
- return result;
211
- // Strategy returned undefined — fall through to DOM scan below.
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
- // Fallback: find the row's position in the current DOM order in a SINGLE roundtrip.
214
- // Previously this looped over every row calling elementHandle() + evaluate() per row
215
- // (O(n) CDP roundtrips, and elementHandle() is soft-deprecated). We now resolve the
216
- // target once and let evaluateAll compare it against the full row set in the browser.
217
- // The row set is resolved through the same selector/scope as everywhere else, so a
218
- // string, function, or Locator rowSelector all behave identically. (#350)
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>;