@rickcedwhat/playwright-smart-table 6.20.1 → 6.21.0-next.3ab7a18

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.
Files changed (46) hide show
  1. package/dist/engine/rowFinder.d.ts +1 -0
  2. package/dist/engine/rowFinder.js +73 -75
  3. package/dist/engine/scanPages.d.ts +58 -0
  4. package/dist/engine/scanPages.js +46 -0
  5. package/dist/engine/tableIteration.d.ts +1 -0
  6. package/dist/engine/tableIteration.js +40 -22
  7. package/dist/filterEngine.d.ts +4 -0
  8. package/dist/filterEngine.js +15 -4
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.js +5 -1
  11. package/dist/packageVersion.d.ts +1 -1
  12. package/dist/packageVersion.js +1 -1
  13. package/dist/plugins/index.d.ts +51 -10
  14. package/dist/plugins/index.js +14 -6
  15. package/dist/presets/glide/index.d.ts +1 -1
  16. package/dist/presets/glide/index.js +2 -2
  17. package/dist/presets/mui.js +15 -29
  18. package/dist/presets/rdg.d.ts +1 -1
  19. package/dist/presets/rdg.js +4 -4
  20. package/dist/smartRow.d.ts +2 -2
  21. package/dist/smartRow.js +163 -51
  22. package/dist/strategies/columns.d.ts +5 -9
  23. package/dist/strategies/columns.js +0 -10
  24. package/dist/strategies/contentReady.d.ts +20 -0
  25. package/dist/strategies/contentReady.js +62 -0
  26. package/dist/strategies/filter.d.ts +0 -4
  27. package/dist/strategies/filter.js +0 -16
  28. package/dist/strategies/headers.d.ts +5 -0
  29. package/dist/strategies/headers.js +17 -9
  30. package/dist/strategies/index.d.ts +23 -15
  31. package/dist/strategies/index.js +9 -5
  32. package/dist/strategies/pagination.js +11 -2
  33. package/dist/strategies/viewport.d.ts +4 -1
  34. package/dist/strategies/viewport.js +62 -32
  35. package/dist/typeContext.d.ts +1 -1
  36. package/dist/typeContext.js +77 -17
  37. package/dist/types.d.ts +74 -17
  38. package/dist/useTable.js +125 -56
  39. package/dist/utils/elementTracker.js +6 -3
  40. package/dist/utils/loadingWait.d.ts +15 -0
  41. package/dist/utils/loadingWait.js +43 -0
  42. package/dist/utils/pageIndex.d.ts +12 -0
  43. package/dist/utils/pageIndex.js +19 -0
  44. package/dist/utils/resolveCellLocator.d.ts +35 -0
  45. package/dist/utils/resolveCellLocator.js +52 -0
  46. package/package.json +1 -1
@@ -24,6 +24,7 @@ export declare class RowFinder<T = any> {
24
24
  exact?: boolean;
25
25
  maxPages?: number;
26
26
  }): Promise<SmartRow<T>>;
27
+ private waitForTableReady;
27
28
  findRows(filters?: Record<string, FilterValue>, options?: {
28
29
  exact?: boolean;
29
30
  maxPages?: number;
@@ -7,6 +7,9 @@ const elementTracker_1 = require("../utils/elementTracker");
7
7
  const sentinel_1 = require("../utils/sentinel");
8
8
  const navigationBarrier_1 = require("../utils/navigationBarrier");
9
9
  const rowResolution_1 = require("./rowResolution");
10
+ const resolveCellLocator_1 = require("../utils/resolveCellLocator");
11
+ const scanPages_1 = require("./scanPages");
12
+ const loadingWait_1 = require("../utils/loadingWait");
10
13
  class RowFinder {
11
14
  constructor(rootLocator, config, resolve, filterEngine, tableMapper, makeSmartRow, tableState = { currentPageIndex: 0 }, advancePage = async () => false) {
12
15
  this.rootLocator = rootLocator;
@@ -56,12 +59,26 @@ class RowFinder {
56
59
  if (colIndex === undefined)
57
60
  continue;
58
61
  const override = this.config.columnOverrides[colName];
59
- const cell = this.resolve(this.config.cellSelector, rowLocator).nth(colIndex);
62
+ const cell = (0, resolveCellLocator_1.resolveCellLocator)({
63
+ config: this.config,
64
+ resolve: this.resolve,
65
+ row: rowLocator,
66
+ root: this.rootLocator,
67
+ columnName: colName,
68
+ columnIndex: colIndex,
69
+ });
60
70
  const getCell = (name) => {
61
71
  const idx = map.get(name);
62
72
  if (idx === undefined)
63
73
  throw new Error(`Column "${name}" not found`);
64
- return this.resolve(this.config.cellSelector, rowLocator).nth(idx);
74
+ return (0, resolveCellLocator_1.resolveCellLocator)({
75
+ config: this.config,
76
+ resolve: this.resolve,
77
+ row: rowLocator,
78
+ root: this.rootLocator,
79
+ columnName: name,
80
+ columnIndex: idx,
81
+ });
65
82
  };
66
83
  const context = {
67
84
  row: this.makeSmartRow(rowLocator, map, undefined),
@@ -104,44 +121,59 @@ class RowFinder {
104
121
  smartRow[sentinel_1.SENTINEL_ROW] = true;
105
122
  return smartRow;
106
123
  }
124
+ async waitForTableReady() {
125
+ var _a;
126
+ const isTableLoading = (_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading;
127
+ if (!isTableLoading)
128
+ return;
129
+ const context = {
130
+ root: this.rootLocator,
131
+ config: this.config,
132
+ page: this.rootLocator.page(),
133
+ resolve: this.resolve
134
+ };
135
+ await (0, loadingWait_1.waitWhileTableLoading)(this.config, context, this.rootLocator.page(), 'waitForTableReady');
136
+ }
107
137
  async findRows(filters = {}, options) {
108
138
  var _a, _b, _c;
109
139
  const filtersRecord = filters;
110
140
  const map = await this.tableMapper.getMap();
111
141
  const allRows = [];
112
142
  const effectiveMaxPages = (_b = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
113
- let pagesScanned = 1;
143
+ const useBulk = (options === null || options === void 0 ? void 0 : options.useBulkPagination) === true && !!((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk);
144
+ const hasPagination = !!this.config.strategies.pagination;
114
145
  (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
115
146
  const tracker = new elementTracker_1.ElementTracker('findRows');
116
147
  try {
117
148
  const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filtersRecord);
118
149
  const hasOverrideFilters = Object.keys(overrideFilters).length > 0;
119
150
  const hasSyntheticFilters = Object.keys(syntheticFilters).length > 0;
120
- const collectMatches = async () => {
151
+ const { pagesScanned } = await (0, scanPages_1.scanPages)({
152
+ advancePage: this.advancePage,
153
+ getCurrentPageIndex: () => this.tableState.currentPageIndex,
154
+ config: this.config,
155
+ waitForReady: () => this.waitForTableReady(),
156
+ }, async () => {
121
157
  var _a, _b, _c, _d;
122
158
  let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
123
159
  if (Object.keys(domFilters).length > 0) {
124
160
  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);
125
161
  }
126
- // Get only newly seen matched rows
127
162
  const newIndices = await tracker.getUnseenIndices(rowLocators);
128
163
  const currentRows = await rowLocators.all();
129
164
  let added = 0;
130
- // One barrier per batch — synchronizes cell navigation across all rows in this page's results
131
165
  const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
132
166
  const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
133
167
  for (const idx of newIndices) {
134
168
  const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
135
169
  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
170
  const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
139
171
  if (loadingOutcome !== 'process') {
140
172
  barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
141
173
  if (loadingOutcome === 'throw') {
142
174
  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`);
143
175
  }
144
- continue; // 'skip'
176
+ continue;
145
177
  }
146
178
  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
179
  barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
@@ -155,57 +187,33 @@ class RowFinder {
155
187
  added++;
156
188
  }
157
189
  (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — ${added} new match(es) (total: ${allRows.length})`);
158
- };
159
- // Scan first page
160
- await collectMatches();
161
- // Pagination Loop
162
- while (pagesScanned < effectiveMaxPages && this.config.strategies.pagination) {
163
- // Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
164
- // Bulk-by-default made findRows jump N pages per advance and silently skip the
165
- // rows on intermediate pages. When only a bulk primitive exists, _advancePage
166
- // still falls back to it.
167
- const useBulk = (options === null || options === void 0 ? void 0 : options.useBulkPagination) === true && !!((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk);
168
- const prevPage = this.tableState.currentPageIndex;
169
- const didPaginate = await this.advancePage(useBulk);
170
- if (!didPaginate) {
171
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false — final scan`);
172
- await collectMatches();
173
- break;
174
- }
175
- const pagesJumped = this.tableState.currentPageIndex - prevPage;
176
- pagesScanned += pagesJumped;
177
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
178
- await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
179
- await collectMatches();
180
- }
190
+ return 'continue';
191
+ }, {
192
+ maxPages: hasPagination ? effectiveMaxPages : 1,
193
+ useBulk,
194
+ label: 'findRows',
195
+ finalScanOnEof: hasPagination,
196
+ });
197
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
181
198
  }
182
199
  finally {
183
200
  await tracker.cleanup(this.rootLocator.page());
184
201
  }
185
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
186
202
  return (0, smartRowArray_1.createSmartRowArray)(allRows);
187
203
  }
188
204
  async findRowLocator(filters, options = {}) {
189
- var _a, _b, _c;
205
+ var _a, _b;
190
206
  const map = await this.tableMapper.getMap();
191
207
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
192
- let pagesScanned = 1;
208
+ const useBulk = options.useBulkPagination === true && !!((_b = this.config.strategies.pagination) === null || _b === void 0 ? void 0 : _b.goNextBulk);
209
+ let found = null;
193
210
  (0, debugUtils_1.logDebug)(this.config, 'verbose', `Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
194
- while (true) {
195
- // Check Loading
196
- if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
197
- const isLoading = await this.config.strategies.loading.isTableLoading({
198
- root: this.rootLocator,
199
- config: this.config,
200
- page: this.rootLocator.page(),
201
- resolve: this.resolve
202
- });
203
- if (isLoading) {
204
- (0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
205
- await this.rootLocator.page().waitForTimeout(200);
206
- continue;
207
- }
208
- }
211
+ await (0, scanPages_1.scanPages)({
212
+ advancePage: this.advancePage,
213
+ getCurrentPageIndex: () => this.tableState.currentPageIndex,
214
+ config: this.config,
215
+ waitForReady: () => this.waitForTableReady(),
216
+ }, async () => {
209
217
  const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
210
218
  const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filters);
211
219
  const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
@@ -218,8 +226,10 @@ class RowFinder {
218
226
  (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
219
227
  if (count > 1)
220
228
  await this.throwIfAmbiguous(await matchedRows.all(), filters, map);
221
- if (count === 1)
222
- return matchedRows.first();
229
+ if (count === 1) {
230
+ found = matchedRows.first();
231
+ return 'stop';
232
+ }
223
233
  }
224
234
  else {
225
235
  const candidates = await matchedRows.all();
@@ -235,31 +245,19 @@ class RowFinder {
235
245
  (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: ${postFilterMatches.length} match(es) after post-filter`);
236
246
  if (postFilterMatches.length > 1)
237
247
  await this.throwIfAmbiguous(postFilterMatches, filters, map);
238
- if (postFilterMatches.length === 1)
239
- return postFilterMatches[0];
240
- }
241
- if (pagesScanned < effectiveMaxPages) {
242
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
243
- // Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
244
- // Bulk-by-default made findRow jump past the page holding the target row.
245
- const useBulk = options.useBulkPagination === true && !!((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk);
246
- const prevPage = this.tableState.currentPageIndex;
247
- const didLoadMore = await this.advancePage(useBulk);
248
- if (didLoadMore) {
249
- const pagesJumped = this.tableState.currentPageIndex - prevPage;
250
- pagesScanned += pagesJumped;
251
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRowLocator: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
252
- await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
253
- continue;
254
- }
255
- else {
256
- (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: pagination returned false — final scan`);
257
- pagesScanned = effectiveMaxPages;
258
- continue;
248
+ if (postFilterMatches.length === 1) {
249
+ found = postFilterMatches[0];
250
+ return 'stop';
259
251
  }
260
252
  }
261
- return null;
262
- }
253
+ return 'continue';
254
+ }, {
255
+ maxPages: effectiveMaxPages,
256
+ useBulk,
257
+ label: 'findRow',
258
+ finalScanOnEof: true,
259
+ });
260
+ return found;
263
261
  }
264
262
  resolveRowIndex(rowLocator) {
265
263
  // Shared resolver: resolveRowIndex strategy first, else the DOM-position fallback.
@@ -0,0 +1,58 @@
1
+ import type { FinalTableConfig } from '../types';
2
+ /**
3
+ * Page-walk primitive (#427).
4
+ *
5
+ * Owns the shared pagination shell used by map/forEach/filter, findRow(s),
6
+ * countRows, and the public async iterator:
7
+ * - `maxPages` budget
8
+ * - `advancePage(useBulk)` with bulk page-jump accounting
9
+ * - EOF final scan (one more `onPage` after `advancePage` returns false)
10
+ * - optional `waitForReady` before each page
11
+ *
12
+ * Per-page collection (overscan, dedupe, loading, filters) stays in the caller
13
+ * so find vs map can keep their different row policies.
14
+ *
15
+ * ## Feature matrix (public APIs → this shell)
16
+ *
17
+ * | API | final-scan | bulk opt-in | waitForReady | overscan/dedupe/loading |
18
+ * |-----|------------|-------------|--------------|-------------------------|
19
+ * | map / forEach / filter | yes | options.useBulkPagination | via caller | in runMap |
20
+ * | findRows | yes | options.useBulkPagination | yes | findRows policy (skip loading) |
21
+ * | findRow | yes | options.useBulkPagination | yes | DOM/post filters only |
22
+ * | countRows | yes | no (single-step) | yes | count-only / post filters |
23
+ * | async iterator | yes | no | via runMap path | same as map |
24
+ * | findRowByIndex | no* | no | n/a | scroll/search, not a full scan |
25
+ * | bringIntoView | n/a | path planner | n/a | not a page scan |
26
+ *
27
+ * \* findRowByIndex stops when the target index is mounted; it does not collect rows.
28
+ */
29
+ export type ScanDecision = 'continue' | 'stop';
30
+ export interface ScanPageContext {
31
+ /** 1-based count of pages visited so far (includes the current page). */
32
+ pagesScanned: number;
33
+ /** `tableState.currentPageIndex` at the start of this page callback. */
34
+ pageIndex: number;
35
+ /** True when this is the post-EOF extra pass after `advancePage` returned false. */
36
+ isFinalScan: boolean;
37
+ }
38
+ export interface ScanPagesEnv {
39
+ advancePage: (useBulk: boolean) => Promise<boolean>;
40
+ getCurrentPageIndex: () => number;
41
+ config: FinalTableConfig;
42
+ /** Optional gate before each page (e.g. `isTableLoading` poll). */
43
+ waitForReady?: () => Promise<void>;
44
+ }
45
+ export interface ScanPagesOptions {
46
+ maxPages: number;
47
+ /** Prefer goNextBulk when the advance helper supports it. @default false */
48
+ useBulk?: boolean;
49
+ label?: string;
50
+ /**
51
+ * When `advancePage` returns false, invoke `onPage` once more before stopping.
52
+ * Matches runMap / findRows EOF behavior. @default true
53
+ */
54
+ finalScanOnEof?: boolean;
55
+ }
56
+ export declare function scanPages(env: ScanPagesEnv, onPage: (ctx: ScanPageContext) => Promise<ScanDecision>, options: ScanPagesOptions): Promise<{
57
+ pagesScanned: number;
58
+ }>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanPages = scanPages;
4
+ const debugUtils_1 = require("../utils/debugUtils");
5
+ async function scanPages(env, onPage, options) {
6
+ var _a, _b;
7
+ const maxPages = options.maxPages;
8
+ const useBulk = (_a = options.useBulk) !== null && _a !== void 0 ? _a : false;
9
+ const finalScanOnEof = options.finalScanOnEof !== false;
10
+ const label = (_b = options.label) !== null && _b !== void 0 ? _b : 'scanPages';
11
+ let pagesScanned = 1;
12
+ let reachedEnd = false;
13
+ (0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: starting (maxPages=${maxPages}, useBulk=${useBulk}, finalScanOnEof=${finalScanOnEof})`);
14
+ while (true) {
15
+ if (env.waitForReady) {
16
+ await env.waitForReady();
17
+ }
18
+ const decision = await onPage({
19
+ pagesScanned,
20
+ pageIndex: env.getCurrentPageIndex(),
21
+ isFinalScan: reachedEnd,
22
+ });
23
+ if (decision === 'stop') {
24
+ (0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: onPage returned stop at page ${env.getCurrentPageIndex()}`);
25
+ break;
26
+ }
27
+ if (pagesScanned >= maxPages || reachedEnd) {
28
+ break;
29
+ }
30
+ const prevPage = env.getCurrentPageIndex();
31
+ const didAdvance = await env.advancePage(useBulk);
32
+ if (!didAdvance) {
33
+ if (finalScanOnEof) {
34
+ (0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: pagination returned false — final scan`);
35
+ reachedEnd = true;
36
+ continue;
37
+ }
38
+ break;
39
+ }
40
+ const pagesJumped = env.getCurrentPageIndex() - prevPage;
41
+ pagesScanned += pagesJumped > 0 ? pagesJumped : 1;
42
+ (0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: advanced ${pagesJumped > 0 ? pagesJumped : 1} page(s), now at page ${env.getCurrentPageIndex()} (scanned=${pagesScanned})`);
43
+ }
44
+ (0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: done — ${pagesScanned} page(s) scanned`);
45
+ return { pagesScanned };
46
+ }
@@ -20,6 +20,7 @@ export declare function runForEach<T>(env: TableIterationEnv<T>, callback: (ctx:
20
20
  /**
21
21
  * Row iteration for map (and forEach/filter via label).
22
22
  * Concurrency: `parallel` | `synchronized` | `sequential` (see RowIterationOptions).
23
+ * Page walking is owned by {@link scanPages} (#427).
23
24
  */
24
25
  export declare function runMap<T, R>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => R | Promise<R>, options?: RowIterationOptions, label?: string): Promise<R[]>;
25
26
  /** Filter via map; returns matched rows as {@link SmartRowArray}. */
@@ -8,6 +8,7 @@ const debugUtils_1 = require("../utils/debugUtils");
8
8
  const navigationBarrier_1 = require("../utils/navigationBarrier");
9
9
  const mutex_1 = require("../utils/mutex");
10
10
  const rowResolution_1 = require("./rowResolution");
11
+ const scanPages_1 = require("./scanPages");
11
12
  function log(config, msg) {
12
13
  (0, debugUtils_1.logDebug)(config, 'verbose', msg);
13
14
  }
@@ -19,15 +20,16 @@ async function runForEach(env, callback, options = {}) {
19
20
  /**
20
21
  * Row iteration for map (and forEach/filter via label).
21
22
  * Concurrency: `parallel` | `synchronized` | `sequential` (see RowIterationOptions).
23
+ * Page walking is owned by {@link scanPages} (#427).
22
24
  */
23
25
  async function runMap(env, callback, options = {}, label = 'map') {
24
- var _a, _b, _c, _d, _e, _f;
26
+ var _a, _b, _c, _d, _e;
25
27
  const map = env.getMap();
26
28
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : env.config.maxPages;
27
29
  const dedupeStrategy = (_b = options.dedupe) !== null && _b !== void 0 ? _b : env.config.strategies.dedupe;
28
30
  const dedupeKeys = dedupeStrategy ? new Set() : null;
29
- const defaultMode = label === 'map' ? 'parallel' : 'sequential';
30
- const concurrency = (_d = (_c = options.concurrency) !== null && _c !== void 0 ? _c : env.config.concurrency) !== null && _d !== void 0 ? _d : defaultMode;
31
+ // Sequential by default for all iteration labels (#434) — parallel is opt-in for read-only maps.
32
+ const concurrency = (_d = (_c = options.concurrency) !== null && _c !== void 0 ? _c : env.config.concurrency) !== null && _d !== void 0 ? _d : 'sequential';
31
33
  const useBarrier = concurrency === 'synchronized';
32
34
  // Mutex must not pair with the navigation barrier: synchronized mode needs every row
33
35
  // to enter barrier.sync concurrently; serializing callbacks here deadlocks (first row waits
@@ -37,12 +39,11 @@ async function runMap(env, callback, options = {}, label = 'map') {
37
39
  const tracker = new elementTracker_1.ElementTracker(label);
38
40
  log(env.config, `${label}: starting (maxPages=${effectiveMaxPages}, mode=${concurrency}, dedupe=${!!dedupeStrategy})`);
39
41
  const results = [];
42
+ const seenLogicalIndices = new Set();
40
43
  try {
41
44
  let rowIndex = 0;
42
45
  let stopped = false;
43
46
  let stoppedIndex = Infinity;
44
- let pagesScanned = 1;
45
- let reachedEnd = false;
46
47
  const stop = (idx) => {
47
48
  if (!stopped) {
48
49
  log(env.config, `${label}: stop() called at row ${idx} — halting`);
@@ -50,7 +51,14 @@ async function runMap(env, callback, options = {}, label = 'map') {
50
51
  stoppedIndex = idx;
51
52
  }
52
53
  };
53
- while (!stopped) {
54
+ const { pagesScanned } = await (0, scanPages_1.scanPages)({
55
+ advancePage: env.advancePage,
56
+ getCurrentPageIndex: env.getCurrentPageIndex,
57
+ config: env.config,
58
+ }, async ({ pagesScanned: pageNum, isFinalScan }) => {
59
+ var _a;
60
+ if (stopped)
61
+ return 'stop';
54
62
  const rowLocators = env.getRowLocators();
55
63
  const allIndices = await tracker.peekUnseenIndices(rowLocators);
56
64
  const pageRows = await rowLocators.all();
@@ -63,9 +71,12 @@ async function runMap(env, callback, options = {}, label = 'map') {
63
71
  // #384: overscan rows ABOVE the visible range have already scrolled past and will never
64
72
  // re-enter the viewport — collect them now. Only defer rows BELOW the visible range
65
73
  // (they'll scroll into view on a later page).
74
+ //
75
+ // #417 follow-up: skip viewport filtering on the final scan — there are no more pages
76
+ // to defer to, so collect all remaining unseen rows regardless of visibility.
66
77
  let candidateIndices = allIndices;
67
- const getVisibleRowIndices = (_f = env.config.strategies.viewport) === null || _f === void 0 ? void 0 : _f.getVisibleRowIndices;
68
- if (getVisibleRowIndices) {
78
+ const getVisibleRowIndices = (_a = env.config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.getVisibleRowIndices;
79
+ if (getVisibleRowIndices && !isFinalScan) {
69
80
  const visible = new Set(await getVisibleRowIndices(env.getContext()));
70
81
  const minVisible = visible.size > 0 ? Math.min(...visible) : Infinity;
71
82
  candidateIndices = allIndices.filter(idx => visible.has(idx) || idx < minVisible);
@@ -82,10 +93,10 @@ async function runMap(env, callback, options = {}, label = 'map') {
82
93
  await tracker.commitIndices(rowLocators, newIndices);
83
94
  const batchSize = newIndices.length;
84
95
  if (batchSize === 0) {
85
- log(env.config, `${label}: page ${pagesScanned} — no new row(s) found`);
96
+ log(env.config, `${label}: page ${pageNum} — no new row(s) found`);
86
97
  }
87
98
  else {
88
- log(env.config, `${label}: scanning page ${pagesScanned} — ${batchSize} new row(s)`);
99
+ log(env.config, `${label}: scanning page ${pageNum} — ${batchSize} new row(s)`);
89
100
  const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(batchSize) : undefined;
90
101
  const actionMutex = useMutex ? new mutex_1.Mutex() : null;
91
102
  // `index` (ctx.index) is the enumeration counter — the order rows are visited,
@@ -111,6 +122,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
111
122
  if (stopped && enumIndex > stoppedIndex) {
112
123
  return SKIP;
113
124
  }
125
+ // When resolveRowIndex is configured, identical logical indices across pages
126
+ // indicate the same record (e.g. re-created DOM elements in recycling
127
+ // virtualizers whose WeakMap entry was GC'd). Skip without processing.
128
+ if (row.rowIndex !== undefined && seenLogicalIndices.has(row.rowIndex)) {
129
+ log(env.config, `${label}: skipping duplicate logical row ${row.rowIndex}`);
130
+ return SKIP;
131
+ }
114
132
  // Wait for the row to finish loading BEFORE evaluating its dedupe key (Bug #355).
115
133
  // map/forEach/filter process a still-loading row when no timeout is set (unlike
116
134
  // findRows, which skips) — see resolveRowLoading's `noTimeoutAction`.
@@ -141,6 +159,9 @@ async function runMap(env, callback, options = {}, label = 'map') {
141
159
  if (dedupeKeys && dedupeKey !== undefined && result !== SKIP) {
142
160
  dedupeKeys.add(dedupeKey);
143
161
  }
162
+ if (row.rowIndex !== undefined && result !== SKIP) {
163
+ seenLogicalIndices.add(row.rowIndex);
164
+ }
144
165
  return result;
145
166
  }
146
167
  finally {
@@ -155,8 +176,8 @@ async function runMap(env, callback, options = {}, label = 'map') {
155
176
  }
156
177
  }
157
178
  else {
158
- const results = await Promise.all(smartRows.map((row, i) => processRow(row, positionBase + i)));
159
- pageResults.push(...results);
179
+ const batchResults = await Promise.all(smartRows.map((row, i) => processRow(row, positionBase + i)));
180
+ pageResults.push(...batchResults);
160
181
  }
161
182
  for (let i = 0; i < pageResults.length; i++) {
162
183
  if (positionBase + i > stoppedIndex)
@@ -167,16 +188,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
167
188
  }
168
189
  rowIndex += batchSize;
169
190
  }
170
- if (stopped || pagesScanned >= effectiveMaxPages || reachedEnd)
171
- break;
172
- log(env.config, `${label}: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
173
- if (!await env.advancePage(useBulk)) {
174
- log(env.config, `${label}: pagination returned false — final scan`);
175
- reachedEnd = true;
176
- continue;
177
- }
178
- pagesScanned++;
179
- }
191
+ return stopped ? 'stop' : 'continue';
192
+ }, {
193
+ maxPages: effectiveMaxPages,
194
+ useBulk,
195
+ label,
196
+ finalScanOnEof: true,
197
+ });
180
198
  log(env.config, `${label}: complete — ${results.length} result(s) across ${pagesScanned} page(s)`);
181
199
  }
182
200
  finally {
@@ -10,6 +10,10 @@ export declare class FilterEngine {
10
10
  * Note: `rootLocator` is optional for backward compatibility in call sites that already
11
11
  * pass only the page. When strategies.filter is present we construct a TableContext
12
12
  * using the provided `rootLocator`.
13
+ *
14
+ * When `strategies.getCellLocator` is set, cells are resolved through that strategy
15
+ * (via a `:scope` row stub) so column-virtualized grids that use `aria-colindex` / etc.
16
+ * are not filtered with fragile `.nth(colIndex)` DOM order.
13
17
  */
14
18
  applyFilters(baseRows: Locator, filters: Record<string, FilterValue>, map: Map<string, number>, exact: boolean, page: Page, rootLocator?: Locator): Locator;
15
19
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FilterEngine = void 0;
4
4
  const stringUtils_1 = require("./utils/stringUtils");
5
+ const resolveCellLocator_1 = require("./utils/resolveCellLocator");
5
6
  class FilterEngine {
6
7
  constructor(config, resolve) {
7
8
  this.config = config;
@@ -13,6 +14,10 @@ class FilterEngine {
13
14
  * Note: `rootLocator` is optional for backward compatibility in call sites that already
14
15
  * pass only the page. When strategies.filter is present we construct a TableContext
15
16
  * using the provided `rootLocator`.
17
+ *
18
+ * When `strategies.getCellLocator` is set, cells are resolved through that strategy
19
+ * (via a `:scope` row stub) so column-virtualized grids that use `aria-colindex` / etc.
20
+ * are not filtered with fragile `.nth(colIndex)` DOM order.
16
21
  */
17
22
  applyFilters(baseRows, filters, map, exact, page, rootLocator) {
18
23
  var _a;
@@ -41,10 +46,16 @@ class FilterEngine {
41
46
  });
42
47
  continue;
43
48
  }
44
- // Default Filter Logic
45
- const cellTemplate = this.resolve(this.config.cellSelector, page);
46
- // Playwright scoping: `cellTemplate.nth(colIndex)` will be re-based when used in filtered.filter({ has: ... })
47
- const targetCell = cellTemplate.nth(colIndex);
49
+ // Prefer getCellLocator (column-virtualized presets); else cellSelector.nth.
50
+ // Playwright re-bases the `has` locator into each candidate row.
51
+ const targetCell = (0, resolveCellLocator_1.resolveCellLocatorForFilter)({
52
+ config: this.config,
53
+ resolve: this.resolve,
54
+ page,
55
+ root: rootLocator,
56
+ columnName: colName,
57
+ columnIndex: colIndex,
58
+ });
48
59
  if (typeof filterVal === 'function') {
49
60
  // Locator-based filter: (cell) => cell.locator(...)
50
61
  filtered = filtered.filter({
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
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, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, } 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, TableStrategies, LoadingStrategy, ViewportStrategy, FilterStrategy, NavigationPrimitives, DedupeStrategy, ContentReadyStrategy, FillStrategy, PaginationStrategy, HeaderStrategy, } from './types';
4
4
  export { Strategies } from './strategies';
5
5
  export * as presets from './presets';
6
- /** @deprecated Use `presets` instead. Plugins will be removed in v7.0.0. */
6
+ /**
7
+ * @deprecated Use `presets` instead (`presets.muiDataGrid`, `presets.rdg`, `presets.glide`, …).
8
+ * `Plugins` will be removed in v7.0.0.
9
+ * Note: `Plugins.MUI` is the **DataGrid** preset only — for MUI Table use `presets.muiTable`.
10
+ */
7
11
  export { Plugins } from './plugins';
8
12
  export { mergeTableConfig } from './utils/mergeTableConfig';
package/dist/index.js CHANGED
@@ -42,7 +42,11 @@ Object.defineProperty(exports, "useTable", { enumerable: true, get: function ()
42
42
  var strategies_1 = require("./strategies");
43
43
  Object.defineProperty(exports, "Strategies", { enumerable: true, get: function () { return strategies_1.Strategies; } });
44
44
  exports.presets = __importStar(require("./presets"));
45
- /** @deprecated Use `presets` instead. Plugins will be removed in v7.0.0. */
45
+ /**
46
+ * @deprecated Use `presets` instead (`presets.muiDataGrid`, `presets.rdg`, `presets.glide`, …).
47
+ * `Plugins` will be removed in v7.0.0.
48
+ * Note: `Plugins.MUI` is the **DataGrid** preset only — for MUI Table use `presets.muiTable`.
49
+ */
46
50
  var plugins_1 = require("./plugins");
47
51
  Object.defineProperty(exports, "Plugins", { enumerable: true, get: function () { return plugins_1.Plugins; } });
48
52
  var mergeTableConfig_1 = require("./utils/mergeTableConfig");
@@ -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.20.1";
6
+ export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.21.0-next.3ab7a18";
@@ -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.20.1";
9
+ exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.21.0-next.3ab7a18";