@rickcedwhat/playwright-smart-table 6.25.0-next.c784f2c → 6.25.0-next.d8860d0

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 (40) hide show
  1. package/dist/engine/cellNavigation.d.ts +24 -0
  2. package/dist/engine/cellNavigation.js +329 -0
  3. package/dist/engine/countRows.d.ts +28 -0
  4. package/dist/engine/countRows.js +149 -0
  5. package/dist/engine/rowByIndex.d.ts +20 -0
  6. package/dist/engine/rowByIndex.js +79 -0
  7. package/dist/engine/rowFinder.d.ts +3 -1
  8. package/dist/engine/rowFinder.js +5 -7
  9. package/dist/engine/rowResolution.d.ts +2 -2
  10. package/dist/engine/rowResolution.js +11 -7
  11. package/dist/engine/rowSnapshot.d.ts +28 -0
  12. package/dist/engine/rowSnapshot.js +422 -0
  13. package/dist/engine/scanPages.d.ts +7 -2
  14. package/dist/engine/scanPages.js +11 -0
  15. package/dist/engine/sorting.d.ts +11 -0
  16. package/dist/engine/sorting.js +51 -0
  17. package/dist/engine/tableIteration.js +1 -0
  18. package/dist/engine/tableMapper.js +16 -5
  19. package/dist/index.d.ts +1 -1
  20. package/dist/packageVersion.d.ts +1 -1
  21. package/dist/packageVersion.js +1 -1
  22. package/dist/smartRow.js +12 -692
  23. package/dist/strategies/columns.d.ts +1 -1
  24. package/dist/strategies/contentReady.d.ts +19 -1
  25. package/dist/strategies/contentReady.js +81 -0
  26. package/dist/strategies/dedupe.d.ts +5 -2
  27. package/dist/strategies/dedupe.js +5 -2
  28. package/dist/strategies/index.d.ts +5 -0
  29. package/dist/types.d.ts +36 -8
  30. package/dist/useTable.js +41 -289
  31. package/dist/utils/deadline.d.ts +2 -0
  32. package/dist/utils/deadline.js +19 -0
  33. package/dist/utils/memoizeViewport.d.ts +6 -0
  34. package/dist/utils/memoizeViewport.js +28 -0
  35. package/dist/utils/navigationBarrier.js +0 -1
  36. package/dist/utils/safeStringify.d.ts +2 -0
  37. package/dist/utils/safeStringify.js +18 -0
  38. package/dist/utils/smartRowArray.d.ts +1 -1
  39. package/dist/utils/smartRowArray.js +4 -1
  40. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import type { FinalTableConfig } from '../types';
3
+ import type { NavigationBarrier } from '../utils/navigationBarrier';
4
+ export interface NavigateToCellParams {
5
+ config: FinalTableConfig<any>;
6
+ rootLocator: Locator;
7
+ page: Page;
8
+ resolve: (item: any, parent: Locator | Page) => Locator;
9
+ getHeaders?: () => Promise<string[]>;
10
+ column: string;
11
+ index: number;
12
+ rowLocator: Locator;
13
+ rowIndex?: number;
14
+ renderWindowPosition?: boolean;
15
+ barrier?: NavigationBarrier;
16
+ allowMissingCell?: boolean;
17
+ }
18
+ /**
19
+ * Navigates to a cell with active cell optimization.
20
+ * Uses `strategies.viewport` first (2D virtualized tables), then `strategies.navigation`
21
+ * primitives (goUp/goDown/goLeft/goRight, optional snap/seek/goHome).
22
+ * Returns the target cell locator after navigation.
23
+ */
24
+ export declare const navigateToCell: (params: NavigateToCellParams) => Promise<Locator>;
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.navigateToCell = void 0;
4
+ const rowResolution_1 = require("./rowResolution");
5
+ const debugUtils_1 = require("../utils/debugUtils");
6
+ const memoizeViewport_1 = require("../utils/memoizeViewport");
7
+ const log = (config, msg) => (0, debugUtils_1.logDebug)(config, 'verbose', msg);
8
+ /**
9
+ * Navigates to a cell with active cell optimization.
10
+ * Uses `strategies.viewport` first (2D virtualized tables), then `strategies.navigation`
11
+ * primitives (goUp/goDown/goLeft/goRight, optional snap/seek/goHome).
12
+ * Returns the target cell locator after navigation.
13
+ */
14
+ const navigateToCell = async (params) => {
15
+ var _a;
16
+ const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, barrier, allowMissingCell } = params;
17
+ // getRowByIndex supplies a position in the mounted row set. Resolve it while the
18
+ // selected row is still mounted, before horizontal scrolling can evict it.
19
+ const rowIndex = params.renderWindowPosition && config.strategies.viewport && config.strategies.resolveRowIndex
20
+ ? (_a = (await (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, config, () => undefined))) === null || _a === void 0 ? void 0 : _a.index
21
+ : params.rowIndex;
22
+ const rowLabel = typeof rowIndex === 'number' ? `row ${rowIndex}` : 'row ?';
23
+ log(config, `_navigateToCell: resolving column "${column}" (colIndex ${index}), ${rowLabel}`);
24
+ const getActiveCell = config.strategies.getActiveCell;
25
+ const readActiveCell = async () => getActiveCell ? getActiveCell({ config, root: rootLocator, page, resolve }) : null;
26
+ const isActiveTarget = (ac) => !!ac && ac.rowIndex === rowIndex && ac.columnIndex === index;
27
+ // Optimization: Check if we are ALREADY at the target cell
28
+ if (getActiveCell) {
29
+ const activeCell = await readActiveCell();
30
+ if (activeCell && isActiveTarget(activeCell)) {
31
+ log(config, `_navigateToCell: Already at target cell {row: ${rowIndex}, col: ${index}}`);
32
+ // Still participate in the barrier (no-op) so peers are not left waiting.
33
+ if (barrier)
34
+ await barrier.sync(index);
35
+ return activeCell.locator;
36
+ }
37
+ }
38
+ const getCellLocator = () => config.strategies.getCellLocator
39
+ ? config.strategies.getCellLocator({ row: rowLocator, root: rootLocator, columnName: column, columnIndex: index, rowIndex, page, config })
40
+ : resolve(config.cellSelector, rowLocator).nth(index);
41
+ const nav = {
42
+ config, page, column, index, rowLocator, rowIndex, barrier, allowMissingCell, resolve,
43
+ context: { config, root: rootLocator, page, resolve, getHeaders },
44
+ readActiveCell,
45
+ isActiveTarget,
46
+ // Locator presence: virtualized grids (e.g. Glide) often attach the target `td` after scroll
47
+ // while focus id lags; do not require getActiveCell to match for this to count as reached.
48
+ targetReached: async () => isActiveTarget(await readActiveCell()) || (await getCellLocator().count() > 0),
49
+ getCellLocator,
50
+ };
51
+ // Viewport-oracle phase: direct positioning for 2D virtualized tables.
52
+ // Runs before navigation primitives; falls through to them if cell still not accessible.
53
+ const cache = config.strategies.viewport && (0, memoizeViewport_1.memoizeViewport)(config.strategies.viewport);
54
+ if (cache && typeof rowIndex === 'number') {
55
+ const viaViewport = await navigateViaViewport(nav, cache.viewport, rowIndex, cache.clear);
56
+ if (viaViewport)
57
+ return viaViewport;
58
+ }
59
+ if (config.strategies.navigation) {
60
+ return navigateViaPrimitives(nav, config.strategies.navigation);
61
+ }
62
+ // No horizontal strategies configured; assume table is 1D or fully rendered,
63
+ // and let standard Playwright auto-waiting handle any lazy-loading delays.
64
+ return getCellLocator();
65
+ };
66
+ exports.navigateToCell = navigateToCell;
67
+ /** Checks in to the barrier without a move action so peers aren't left waiting. */
68
+ async function checkIn(nav) {
69
+ if (nav.barrier)
70
+ await nav.barrier.sync(nav.index);
71
+ }
72
+ /** Runs `action` once for the whole batch when a barrier is present, otherwise directly. */
73
+ async function coordinated(nav, action) {
74
+ return nav.barrier ? nav.barrier.sync(nav.index, action) : action();
75
+ }
76
+ const inRange = (value, range) => value >= range.first && value <= range.last;
77
+ /** True when the row is in the visible row range and the target cell is (or soon becomes) attached. */
78
+ async function isCellAttachedInView(nav, viewport, rowIndex) {
79
+ const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(nav.context) : null;
80
+ if (rowRange && !inRange(rowIndex, rowRange))
81
+ return false;
82
+ if (await nav.targetReached())
83
+ return true;
84
+ try {
85
+ await nav.getCellLocator().waitFor({ state: 'attached', timeout: 500 });
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ if ((error === null || error === void 0 ? void 0 : error.name) !== 'TimeoutError')
90
+ throw error;
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Fast path: is the cell already readable without scrolling? Uses the column range oracle
96
+ * when available, else DOM presence. Rows do NOT sync at the barrier with a move action
97
+ * here — only scroll boundaries need coordination.
98
+ */
99
+ async function readIfInView(nav, viewport, rowIndex) {
100
+ const { config, context, index } = nav;
101
+ if (!viewport.getVisibleColumnRange) {
102
+ // No range oracle — fall back to DOM count() check.
103
+ if (!await nav.targetReached())
104
+ return null;
105
+ await checkIn(nav);
106
+ return nav.getCellLocator();
107
+ }
108
+ const knownColRange = await viewport.getVisibleColumnRange(context);
109
+ if (!knownColRange || !inRange(index, knownColRange))
110
+ return null;
111
+ if (!await isCellAttachedInView(nav, viewport, rowIndex))
112
+ return null;
113
+ log(config, `_navigateToCell: col ${index} in visible range [${knownColRange.first}-${knownColRange.last}], reading directly`);
114
+ // Check in to the barrier without a moveAction — peers that need the scroll
115
+ // still supply their own action and will trigger it when all rows arrive.
116
+ await checkIn(nav);
117
+ return nav.getCellLocator();
118
+ }
119
+ /**
120
+ * Viewport phase. Returns the cell locator, or null to fall through to navigation primitives
121
+ * (only when `strategies.navigation` is configured; otherwise throws on a miss).
122
+ */
123
+ async function navigateViaViewport(nav, viewport, rowIndex, clearCache) {
124
+ const { config, context, index } = nav;
125
+ const inView = await readIfInView(nav, viewport, rowIndex);
126
+ if (inView)
127
+ return inView;
128
+ // Column is out of view. Scroll to bring it in.
129
+ // In synchronized mode, the last row to arrive triggers the scroll once for all rows.
130
+ await coordinated(nav, async () => {
131
+ // Always scroll — scrollToColumn is idempotent (bounds-checks before acting).
132
+ // The trigger row may already have the cell via overscan, but other rows in the
133
+ // batch might not; skipping the scroll here would silently starve them.
134
+ log(config, `_navigateToCell: col ${index} out of view, scrolling`);
135
+ if (viewport.scrollToColumn) {
136
+ await viewport.scrollToColumn(context, index);
137
+ }
138
+ });
139
+ // A peer may have performed the coordinated scroll. Drop this row's pre-scroll geometry too.
140
+ clearCache();
141
+ await recoverEvictedRow(nav, viewport, rowIndex);
142
+ if (await nav.targetReached()) {
143
+ return nav.getCellLocator();
144
+ }
145
+ // Cell still not accessible — fall through to navigation primitives if configured.
146
+ if (config.strategies.navigation)
147
+ return null;
148
+ if (nav.allowMissingCell)
149
+ return nav.getCellLocator();
150
+ throw new Error(await describeViewportMiss(nav, viewport, rowIndex));
151
+ }
152
+ /**
153
+ * Per-row 2D recovery: horizontal scroll may evict rows from the vertical viewport
154
+ * (scrollbar layout shift) or rows may simply be outside the virtual row window.
155
+ * Recovery is serialized via the barrier's mutex so concurrent rows don't race on
156
+ * scrollTop — adjacent rows often appear in view for free after the previous recovery.
157
+ * scrollToRow must only adjust scrollTop (not scrollLeft) to preserve the column
158
+ * position established by scrollToColumn.
159
+ */
160
+ async function recoverEvictedRow(nav, viewport, rowIndex) {
161
+ if (await nav.targetReached())
162
+ return;
163
+ const scrollToRow = viewport.scrollToRow;
164
+ if (!scrollToRow)
165
+ return;
166
+ const recover = async () => {
167
+ if (!await nav.targetReached()) {
168
+ log(nav.config, `_navigateToCell: row ${rowIndex} evicted by col scroll, recovering`);
169
+ await scrollToRow(nav.context, rowIndex);
170
+ }
171
+ };
172
+ if (nav.barrier) {
173
+ await nav.barrier.runExclusiveRecovery(recover);
174
+ }
175
+ else {
176
+ await recover();
177
+ }
178
+ }
179
+ async function visibleRanges(nav) {
180
+ const viewport = nav.config.strategies.viewport;
181
+ const colRange = (viewport === null || viewport === void 0 ? void 0 : viewport.getVisibleColumnRange) ? await viewport.getVisibleColumnRange(nav.context) : null;
182
+ const rowRange = (viewport === null || viewport === void 0 ? void 0 : viewport.getVisibleRowRange) ? await viewport.getVisibleRowRange(nav.context) : null;
183
+ return { colRange, rowRange };
184
+ }
185
+ async function describeViewportMiss(nav, viewport, rowIndex) {
186
+ const colRange = viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(nav.context) : null;
187
+ const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(nav.context) : null;
188
+ let errMsg = `SmartTable: could not reach cell for column "${nav.column}" (colIndex ${nav.index}) at row ${rowIndex}.\n`;
189
+ errMsg += colRange
190
+ ? ` Visible column range: [${colRange.first}–${colRange.last}]. Column is out of view and no navigation fallback is configured.\n`
191
+ : ` Column is out of view and no navigation fallback is configured.\n`;
192
+ if (rowRange) {
193
+ errMsg += ` Visible row range: [${rowRange.first}–${rowRange.last}]. ${inRange(rowIndex, rowRange) ? 'Row is in view.' : 'Row is out of view.'}\n`;
194
+ }
195
+ errMsg += ` → Add a \`strategies.navigation\` or \`strategies.viewport.scrollToColumn\` to handle off-screen columns.`;
196
+ return errMsg;
197
+ }
198
+ /** Navigation-primitive phase (goUp/goDown/goLeft/goRight, optional snap/seek). */
199
+ async function navigateViaPrimitives(nav, primitives) {
200
+ const { rowIndex } = nav;
201
+ if (await nav.targetReached()) {
202
+ // Already there. If we have a barrier, check-in to stay in lock-step.
203
+ await checkIn(nav);
204
+ return nav.getCellLocator();
205
+ }
206
+ if (typeof rowIndex !== 'number') {
207
+ if (nav.allowMissingCell)
208
+ return nav.getCellLocator();
209
+ throw new Error('Row index is required for navigation');
210
+ }
211
+ const focused = await focusUnfocusedCell(nav);
212
+ if (focused)
213
+ return focused;
214
+ // Not there, perform (coordinated) navigation
215
+ const navResult = await coordinated(nav, () => stepToCell(nav, primitives, rowIndex));
216
+ return finishNavigation(nav, navResult, rowIndex);
217
+ }
218
+ /** Picks the cell after stepping: the one stepToCell found mid-navigation, else the final locator. */
219
+ async function finishNavigation(nav, navResult, rowIndex) {
220
+ if (navResult && typeof navResult.count === 'function' && await navResult.count() > 0) {
221
+ return navResult;
222
+ }
223
+ const finalCell = nav.getCellLocator();
224
+ if (await finalCell.count() > 0 || nav.allowMissingCell)
225
+ return finalCell;
226
+ const { colRange, rowRange } = await visibleRanges(nav);
227
+ let errMsg = `SmartTable: could not reach cell for column "${nav.column}" (colIndex ${nav.index}) at row ${rowIndex} after exhausting navigation strategies. Ensure navigation primitives are correctly implemented.\n`;
228
+ if (colRange)
229
+ errMsg += ` Visible column range: [${colRange.first}–${colRange.last}].\n`;
230
+ if (rowRange)
231
+ errMsg += ` Visible row range: [${rowRange.first}–${rowRange.last}].\n`;
232
+ throw new Error(errMsg);
233
+ }
234
+ /**
235
+ * If getActiveCell is present but returns null (no focus), and the cell is in the DOM,
236
+ * focus it once before entering the navigation loop.
237
+ */
238
+ async function focusUnfocusedCell(nav) {
239
+ if (!nav.config.strategies.getActiveCell)
240
+ return null;
241
+ if (await nav.readActiveCell())
242
+ return null;
243
+ const cell = nav.resolve(nav.config.cellSelector, nav.rowLocator).nth(nav.index);
244
+ if (await cell.count() === 0)
245
+ return null;
246
+ log(nav.config, `_navigateToCell: cell "${nav.column}" found but not focused; focusing.`);
247
+ await cell.focus();
248
+ // Re-check target
249
+ if (!await nav.targetReached())
250
+ return null;
251
+ await checkIn(nav);
252
+ return cell;
253
+ }
254
+ /** Tracks the active cell position while stepping (stays at the last known value when focus is unknown). */
255
+ class Position {
256
+ constructor(nav) {
257
+ this.nav = nav;
258
+ this.row = 0;
259
+ this.col = 0;
260
+ }
261
+ async sync() {
262
+ const ac = await this.nav.readActiveCell();
263
+ if (ac) {
264
+ this.row = ac.rowIndex;
265
+ this.col = ac.columnIndex;
266
+ }
267
+ }
268
+ }
269
+ /** A configured non-negative wait, or the computed default. */
270
+ const waitOrDefault = (configured, computed) => (Number.isFinite(configured) && configured >= 0) ? configured : computed;
271
+ async function repeatStep(times, step) {
272
+ if (!step)
273
+ return;
274
+ for (let i = 0; i < times; i++)
275
+ await step();
276
+ }
277
+ /** Column-0 snap and long-distance seek, both owned by the navigation primitive. */
278
+ async function alignColumn(nav, primitives, pos) {
279
+ const { config, context, index } = nav;
280
+ // Core must not special-case canvas/Home — that was pre-viewport Glide glue.
281
+ if (index === 0 && primitives.snapFirstColumnIntoView) {
282
+ log(config, '_navigateToCell: snapFirstColumnIntoView for column index 0');
283
+ await primitives.snapFirstColumnIntoView(context);
284
+ await pos.sync();
285
+ }
286
+ if (Math.abs(index - pos.col) > 12 && primitives.seekColumnIndex) {
287
+ log(config, `_navigateToCell: seekColumnIndex approx for column ${index}`);
288
+ await primitives.seekColumnIndex(context, index);
289
+ await pos.sync();
290
+ }
291
+ }
292
+ /**
293
+ * After horizontal steps, poll until getActiveCell / DOM presence confirms the target
294
+ * (a11y trees and virtual mounts often lag the scroll). Tune via navigation.maxWaitMs.
295
+ */
296
+ async function pollForTarget(nav, primitives, horizontalSteps) {
297
+ const pollIntervalMs = 10;
298
+ const maxWaitMs = waitOrDefault(primitives.maxWaitMs, Math.min(6000, 250 + horizontalSteps * 25));
299
+ const start = Date.now();
300
+ while (Date.now() - start < maxWaitMs) {
301
+ const ac = await nav.readActiveCell();
302
+ if (ac && nav.isActiveTarget(ac))
303
+ return ac.locator;
304
+ if (await nav.targetReached())
305
+ break;
306
+ await nav.page.waitForTimeout(pollIntervalMs);
307
+ }
308
+ return undefined;
309
+ }
310
+ /** Steps with the navigation primitives from the active cell to the target, then waits for it. */
311
+ async function stepToCell(nav, primitives, rowIndex) {
312
+ const { context, index } = nav;
313
+ const pos = new Position(nav);
314
+ await pos.sync();
315
+ // Navigate vertically (tight loop)
316
+ const rDiff = rowIndex - pos.row;
317
+ const goVertical = rDiff > 0 ? primitives.goDown : primitives.goUp;
318
+ await repeatStep(Math.abs(rDiff), goVertical && (() => goVertical(context)));
319
+ await pos.sync();
320
+ await alignColumn(nav, primitives, pos);
321
+ const cDiff = index - pos.col;
322
+ const goHorizontal = cDiff > 0 ? primitives.goRight : primitives.goLeft;
323
+ await repeatStep(Math.abs(cDiff), goHorizontal && (() => goHorizontal(context)));
324
+ const horizontalSteps = Math.abs(cDiff);
325
+ if (horizontalSteps > 0) {
326
+ await nav.page.waitForTimeout(waitOrDefault(primitives.settleMs, Math.min(2500, 60 + horizontalSteps * 12)));
327
+ }
328
+ return pollForTarget(nav, primitives, horizontalSteps);
329
+ }
@@ -0,0 +1,28 @@
1
+ import type { Locator } from '@playwright/test';
2
+ import type { FilterValue, FinalTableConfig, TableContext } from '../types';
3
+ import type { FilterEngine } from '../filterEngine';
4
+ import type { TableMapper } from './tableMapper';
5
+ import type { RowFinder } from './rowFinder';
6
+ export interface CountRowsEnv<T = any> {
7
+ config: FinalTableConfig<T>;
8
+ rootLocator: Locator;
9
+ getRowLocators: () => Locator;
10
+ tableMapper: TableMapper;
11
+ filterEngine: FilterEngine;
12
+ rowFinder: RowFinder<T>;
13
+ advancePage: (useBulk: boolean) => Promise<boolean>;
14
+ getCurrentPageIndex: () => number;
15
+ getContext: () => TableContext;
16
+ /** Restores page 1 after a paginated count. */
17
+ reset: () => Promise<void>;
18
+ }
19
+ /**
20
+ * Counts matching rows on the current page, or across pages when pagination and
21
+ * `maxPages > 1` allow it. A paginated count always resets to page 1 afterwards (#348).
22
+ * Callers must have initialized the table.
23
+ */
24
+ export declare function countRows<T>(env: CountRowsEnv<T>, filters?: Record<string, FilterValue>, options?: {
25
+ exact?: boolean;
26
+ maxPages?: number;
27
+ signal?: AbortSignal;
28
+ }): Promise<number>;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.countRows = countRows;
4
+ const scanPages_1 = require("./scanPages");
5
+ const elementTracker_1 = require("../utils/elementTracker");
6
+ const debugUtils_1 = require("../utils/debugUtils");
7
+ const loadingWait_1 = require("../utils/loadingWait");
8
+ const safeStringify_1 = require("../utils/safeStringify");
9
+ function log(config, msg) {
10
+ (0, debugUtils_1.logDebug)(config, 'verbose', msg);
11
+ }
12
+ /** Splits filters by how they're evaluated: DOM text, `columnOverrides.read`, or synthetic columns. */
13
+ function partitionFilters(config, filters) {
14
+ var _a, _b, _c;
15
+ const domFilters = {};
16
+ const overrideFilters = {};
17
+ const syntheticFilters = {};
18
+ for (const [key, value] of Object.entries(filters)) {
19
+ if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[key]) {
20
+ syntheticFilters[key] = value;
21
+ }
22
+ else if ((_c = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[key]) === null || _c === void 0 ? void 0 : _c.read) {
23
+ overrideFilters[key] = value;
24
+ }
25
+ else {
26
+ domFilters[key] = value;
27
+ }
28
+ }
29
+ return { domFilters, overrideFilters, syntheticFilters };
30
+ }
31
+ function requireMap(tableMapper) {
32
+ const map = tableMapper.getMapSync();
33
+ if (!map)
34
+ throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
35
+ return map;
36
+ }
37
+ /**
38
+ * Counts matching rows on the current page, or across pages when pagination and
39
+ * `maxPages > 1` allow it. A paginated count always resets to page 1 afterwards (#348).
40
+ * Callers must have initialized the table.
41
+ */
42
+ async function countRows(env, filters, options) {
43
+ var _a, _b, _c, _d, _e;
44
+ var _f, _g;
45
+ const { config, rootLocator, tableMapper, rowFinder } = env;
46
+ const filtersRecord = filters !== null && filters !== void 0 ? filters : {};
47
+ const hasFilters = Object.keys(filtersRecord).length > 0;
48
+ const effectiveMaxPages = (_f = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _f !== void 0 ? _f : config.maxPages;
49
+ const pag = config.strategies.pagination;
50
+ const hasPagination = effectiveMaxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
51
+ const exact = (_g = options === null || options === void 0 ? void 0 : options.exact) !== null && _g !== void 0 ? _g : false;
52
+ const { domFilters, overrideFilters, syntheticFilters } = partitionFilters(config, filtersRecord);
53
+ const hasOverrides = Object.keys(overrideFilters).length > 0;
54
+ const hasSynthetics = Object.keys(syntheticFilters).length > 0;
55
+ const hasPostFilters = hasOverrides || hasSynthetics;
56
+ const resolveRows = () => {
57
+ const rows = env.getRowLocators();
58
+ if (Object.keys(domFilters).length === 0)
59
+ return rows;
60
+ return env.filterEngine.applyFilters(rows, domFilters, requireMap(tableMapper), exact, rootLocator.page(), rootLocator);
61
+ };
62
+ const countPostFilterMatches = async (candidates, indices) => {
63
+ if (!hasPostFilters)
64
+ return indices.length;
65
+ const map = requireMap(tableMapper);
66
+ let count = 0;
67
+ for (const idx of indices) {
68
+ const row = candidates[idx];
69
+ if (hasOverrides && !await rowFinder.matchesOverrideFilters(row, overrideFilters, map, exact))
70
+ continue;
71
+ if (hasSynthetics && !await rowFinder.matchesSyntheticFilters(row, syntheticFilters, map, exact))
72
+ continue;
73
+ count++;
74
+ }
75
+ return count;
76
+ };
77
+ const isTableLoading = (_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading;
78
+ const waitForReady = async () => {
79
+ if (!isTableLoading)
80
+ return;
81
+ await (0, loadingWait_1.waitWhileTableLoading)(config, env.getContext(), rootLocator.page(), 'countRows');
82
+ };
83
+ (_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
84
+ await waitForReady();
85
+ (_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.throwIfAborted();
86
+ const filterSuffix = hasFilters ? ` filters=${(0, safeStringify_1.safeStringify)(filtersRecord)}` : '';
87
+ if (!hasPagination) {
88
+ log(config, `countRows: counting rows in current viewport (no pagination)${filterSuffix}`);
89
+ let count;
90
+ if (!hasPostFilters) {
91
+ count = await resolveRows().count();
92
+ }
93
+ else {
94
+ const all = await resolveRows().all();
95
+ (_d = options === null || options === void 0 ? void 0 : options.signal) === null || _d === void 0 ? void 0 : _d.throwIfAborted();
96
+ count = await countPostFilterMatches(all, all.map((_, i) => i));
97
+ }
98
+ (_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.throwIfAborted();
99
+ return count;
100
+ }
101
+ log(config, `countRows: paginating up to ${effectiveMaxPages} page(s)${filterSuffix}`);
102
+ const tracker = new elementTracker_1.ElementTracker('countRows');
103
+ let total = 0;
104
+ let pagesScanned = 1;
105
+ let paginationError;
106
+ try {
107
+ const scanned = await (0, scanPages_1.scanPages)({
108
+ advancePage: env.advancePage,
109
+ getCurrentPageIndex: env.getCurrentPageIndex,
110
+ config,
111
+ waitForReady,
112
+ }, async ({ pagesScanned: pageNum }) => {
113
+ const rowLocators = resolveRows();
114
+ const newIndices = await tracker.getUnseenIndices(rowLocators);
115
+ const candidates = await rowLocators.all();
116
+ const matched = await countPostFilterMatches(candidates, newIndices);
117
+ total += matched;
118
+ log(config, `countRows: page ${pageNum} — ${matched} row(s) (running total: ${total})`);
119
+ return 'continue';
120
+ }, {
121
+ maxPages: effectiveMaxPages,
122
+ useBulk: false,
123
+ label: 'countRows',
124
+ finalScanOnEof: true,
125
+ signal: options === null || options === void 0 ? void 0 : options.signal,
126
+ });
127
+ pagesScanned = scanned.pagesScanned;
128
+ }
129
+ catch (e) {
130
+ paginationError = e;
131
+ throw e;
132
+ }
133
+ finally {
134
+ await tracker.cleanup(rootLocator.page());
135
+ // Always restore page 1, even if pagination threw mid-count (#348), so the table
136
+ // stays usable for callers that catch the error. If reset() itself fails, only let
137
+ // it surface when pagination succeeded — otherwise preserve the original error.
138
+ log(config, `countRows: ${total} total row(s) across ${pagesScanned} page(s) — resetting`);
139
+ try {
140
+ await env.reset();
141
+ }
142
+ catch (resetError) {
143
+ if (paginationError === undefined)
144
+ throw resetError;
145
+ (0, debugUtils_1.logDebug)(config, 'error', 'countRows: reset() failed after pagination error', resetError);
146
+ }
147
+ }
148
+ return total;
149
+ }
@@ -0,0 +1,20 @@
1
+ import type { Locator } from '@playwright/test';
2
+ import type { FinalTableConfig, SmartRow, TableContext } from '../types';
3
+ export interface RowByIndexEnv<T = any> {
4
+ config: FinalTableConfig<T>;
5
+ getRowLocators: () => Locator;
6
+ getMap: () => Map<string, number>;
7
+ advancePage: (useBulk: boolean) => Promise<boolean>;
8
+ getCurrentPageIndex: () => number;
9
+ getContext: () => TableContext;
10
+ makeSmartRow: (rowLocator: Locator, map: Map<string, number>, rowIndex: number, tablePageIndex: number, healSelector?: string) => SmartRow<T>;
11
+ }
12
+ /**
13
+ * Finds the row whose logical index (per `strategies.resolveRowIndex`) is `index`:
14
+ * already mounted, else via `viewport.scrollToRow`, else by advancing pages until it
15
+ * mounts (bounded by `maxPages`). Callers must have initialized the table.
16
+ */
17
+ export declare function findRowByIndex<T>(env: RowByIndexEnv<T>, index: number, options?: {
18
+ maxPages?: number;
19
+ signal?: AbortSignal;
20
+ }): Promise<SmartRow<T>>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findRowByIndex = findRowByIndex;
4
+ const rowResolution_1 = require("./rowResolution");
5
+ const scanPages_1 = require("./scanPages");
6
+ const debugUtils_1 = require("../utils/debugUtils");
7
+ const safeStringify_1 = require("../utils/safeStringify");
8
+ /**
9
+ * Finds the row whose logical index (per `strategies.resolveRowIndex`) is `index`:
10
+ * already mounted, else via `viewport.scrollToRow`, else by advancing pages until it
11
+ * mounts (bounded by `maxPages`). Callers must have initialized the table.
12
+ */
13
+ async function findRowByIndex(env, index, options) {
14
+ var _a, _b, _c, _d, _e;
15
+ var _f;
16
+ (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
17
+ const { config } = env;
18
+ (0, debugUtils_1.logDebug)(config, 'verbose', `findRowByIndex: index=${index} options=${(0, safeStringify_1.safeStringify)(options)}`);
19
+ const resolveRI = config.strategies.resolveRowIndex;
20
+ const viewport = config.strategies.viewport;
21
+ // Identifying a row by its logical index requires a resolveRowIndex strategy. Without one
22
+ // the library can only offer render-window-relative getRowByIndex, or filter-based findRow.
23
+ if (!resolveRI) {
24
+ throw new Error(`findRowByIndex(${index}) requires a strategies.resolveRowIndex to identify rows by their logical index. ` +
25
+ `Configure one (e.g. read the grid's row-index attribute), or use getRowByIndex() (current render window) / findRow(filters).`);
26
+ }
27
+ let foundSelector;
28
+ // The currently-mounted row whose logical index === index, if any.
29
+ const findMounted = async () => {
30
+ var _a, _b;
31
+ const rows = await env.getRowLocators().all();
32
+ (_a = options === null || options === void 0 ? void 0 : options.signal) === null || _a === void 0 ? void 0 : _a.throwIfAborted();
33
+ for (const r of rows) {
34
+ const result = await resolveRI(r);
35
+ (_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
36
+ if (result === undefined)
37
+ continue;
38
+ const normalized = (0, rowResolution_1.normalizeRowIndexResult)(result);
39
+ if (normalized.index === index) {
40
+ foundSelector = normalized.selector;
41
+ return r;
42
+ }
43
+ }
44
+ return null;
45
+ };
46
+ // 1. Already mounted (no navigation needed)?
47
+ let loc = await findMounted();
48
+ (_b = options === null || options === void 0 ? void 0 : options.signal) === null || _b === void 0 ? void 0 : _b.throwIfAborted();
49
+ // 2. Fast path: jump straight to it via the viewport's random-access scroll.
50
+ if (!loc && (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow)) {
51
+ (0, debugUtils_1.logDebug)(config, 'verbose', `findRowByIndex: scrolling to row ${index}`);
52
+ await viewport.scrollToRow(env.getContext(), index);
53
+ (_c = options === null || options === void 0 ? void 0 : options.signal) === null || _c === void 0 ? void 0 : _c.throwIfAborted();
54
+ loc = await findMounted();
55
+ (_d = options === null || options === void 0 ? void 0 : options.signal) === null || _d === void 0 ? void 0 : _d.throwIfAborted();
56
+ }
57
+ // 3. Fallback: advance pages (on infinite-scroll tables, a "page" is a scroll step) until
58
+ // the row is mounted. Page 1 was already searched above.
59
+ const maxPages = (_f = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _f !== void 0 ? _f : config.maxPages;
60
+ const scanForMounted = async () => {
61
+ let hit = null;
62
+ await (0, scanPages_1.scanPages)({ advancePage: env.advancePage, getCurrentPageIndex: env.getCurrentPageIndex, config }, async ({ pagesScanned }) => {
63
+ if (pagesScanned > 1)
64
+ hit = await findMounted();
65
+ return hit ? 'stop' : 'continue';
66
+ }, { maxPages, useBulk: false, label: 'findRowByIndex', finalScanOnEof: false, signal: options === null || options === void 0 ? void 0 : options.signal });
67
+ return hit;
68
+ };
69
+ if (!loc && config.strategies.pagination) {
70
+ loc = await scanForMounted();
71
+ }
72
+ if (!loc) {
73
+ throw new Error(`findRowByIndex(${index}): could not reach row ${index}. ` +
74
+ `Ensure a viewport.scrollToRow or pagination can bring it into view (searched up to maxPages=${maxPages}).`);
75
+ }
76
+ await (0, debugUtils_1.debugDelay)(config, 'findRow');
77
+ (_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.throwIfAborted();
78
+ return env.makeSmartRow(loc, env.getMap(), index, env.getCurrentPageIndex(), foundSelector);
79
+ }
@@ -3,7 +3,7 @@ import { FinalTableConfig, Selector, SmartRow, FilterValue } from '../types';
3
3
  import { FilterEngine } from '../filterEngine';
4
4
  import { TableMapper } from './tableMapper';
5
5
  import { SmartRowArray } from '../utils/smartRowArray';
6
- import { NavigationBarrier } from '../utils/navigationBarrier';
6
+ import type { NavigationBarrier } from '../utils/navigationBarrier';
7
7
  export declare class RowFinder<T = any> {
8
8
  private rootLocator;
9
9
  private config;
@@ -23,6 +23,7 @@ export declare class RowFinder<T = any> {
23
23
  findRow(filters: Record<string, FilterValue>, options?: {
24
24
  exact?: boolean;
25
25
  maxPages?: number;
26
+ signal?: AbortSignal;
26
27
  }): Promise<SmartRow<T>>;
27
28
  private waitForTableReady;
28
29
  /** Unfiltered rows keep the plain `SmartRow #n` name that `_makeSmart` applies. */
@@ -31,6 +32,7 @@ export declare class RowFinder<T = any> {
31
32
  exact?: boolean;
32
33
  maxPages?: number;
33
34
  useBulkPagination?: boolean;
35
+ signal?: AbortSignal;
34
36
  }): Promise<SmartRowArray<T>>;
35
37
  private findRowLocator;
36
38
  private resolveRowIndex;