@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.
@@ -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
  }
@@ -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 rowIndex = await this.resolveRowIndex(rowLocator);
30
- 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);
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
- // Only apply filters if we have them
54
- if (Object.keys(filtersRecord).length > 0) {
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 rowIndex = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
66
- const smartRow = this.makeSmartRow(currentRows[idx], map, rowIndex, this.tableState.currentPageIndex, barrier);
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 — end of data`);
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 matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
133
- const count = await matchedRows.count();
134
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
135
- if (count > 1) {
136
- const sampleData = [];
137
- try {
138
- const firstFewRows = await matchedRows.all();
139
- const sampleCount = Math.min(firstFewRows.length, 3);
140
- for (let i = 0; i < sampleCount; i++) {
141
- const rowData = await this.makeSmartRow(firstFewRows[i], map, 0, this.tableState.currentPageIndex).toJSON();
142
- sampleData.push(JSON.stringify(rowData));
143
- }
144
- }
145
- catch (e) { }
146
- const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
147
- throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${this.tableState.currentPageIndex}. ` +
148
- `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];
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}: 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;
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
- * This replaces the three inline variants that previously answered the index question
11
- * (findRow's private method, findRows' inline expression, and — later — map's counter).
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<number | undefined>;
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
- * This replaces the three inline variants that previously answered the index question
13
- * (findRow's private method, findRows' inline expression, and — later — map's counter).
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
- return fallback();
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
- candidateIndices = allIndices.filter(idx => visible.has(idx));
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 logicalIndex = (_a = await (0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], env.config, () => positionBase + i)) !== null && _a !== void 0 ? _a : (positionBase + i);
94
- const sr = env.makeSmartRow(pageRows[idx], map, logicalIndex, env.getCurrentPageIndex(), barrier);
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
- const key = await dedupeStrategy(row);
118
- if (dedupeKeys.has(key)) {
119
- log(env.config, `${label}: dedupe skip key="${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
- if (actionMutex) {
132
- return await actionMutex.run(runCallback);
133
- }
134
- else {
135
- return await runCallback();
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}: no more pages — done`);
167
- break;
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
- this._headerMap = new Map(entries);
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. */
@@ -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.19.0";
6
+ export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.20.1-next.230dedf";
@@ -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.19.0";
9
+ exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.20.1-next.230dedf";
@@ -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("../types").ColumnOverride<any>>> | undefined;
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("../types").ColumnOverride<any>>> | undefined;
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("../types").ColumnOverride<any>>> | undefined;
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
- console.warn("HeaderStrategies.scrollRight: Could not find scroller. Returning visible headers.");
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;
@@ -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
- return v !== null && !isNaN(Number(v)) ? Number(v) : undefined;
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) => {