@rickcedwhat/playwright-smart-table 6.25.0-next.49ce2de → 6.25.0-next.4d4c252

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