@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.
package/dist/smartRow.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const rowResolution_1 = require("./engine/rowResolution");
4
+ const cellNavigation_1 = require("./engine/cellNavigation");
5
+ const rowSnapshot_1 = require("./engine/rowSnapshot");
4
6
  const fill_1 = require("./strategies/fill");
5
7
  const stringUtils_1 = require("./utils/stringUtils");
6
8
  const debugUtils_1 = require("./utils/debugUtils");
@@ -8,309 +10,6 @@ const paginationPath_1 = require("./utils/paginationPath");
8
10
  const sentinel_1 = require("./utils/sentinel");
9
11
  const pageIndex_1 = require("./utils/pageIndex");
10
12
  const locatorDescription_1 = require("./utils/locatorDescription");
11
- /**
12
- * Internal helper to navigate to a cell with active cell optimization.
13
- * Uses `strategies.navigation` primitives (goUp/goDown/goLeft/goRight, optional snap/seek/goHome).
14
- * Returns the target cell locator after navigation.
15
- */
16
- const _navigateToCell = async (params) => {
17
- var _a;
18
- const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, barrier, allowMissingCell } = params;
19
- // getRowByIndex supplies a position in the mounted row set. Resolve it while the
20
- // selected row is still mounted, before horizontal scrolling can evict it.
21
- const rowIndex = params.renderWindowPosition && config.strategies.viewport && config.strategies.resolveRowIndex
22
- ? (_a = (await (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, config, () => undefined))) === null || _a === void 0 ? void 0 : _a.index
23
- : params.rowIndex;
24
- const rowLabel = typeof rowIndex === 'number' ? `row ${rowIndex}` : 'row ?';
25
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: resolving column "${column}" (colIndex ${index}), ${rowLabel}`);
26
- // Get active cell if strategy is available
27
- let activeCell = null;
28
- if (config.strategies.getActiveCell) {
29
- activeCell = await config.strategies.getActiveCell({
30
- config,
31
- root: rootLocator,
32
- page,
33
- resolve
34
- });
35
- // Optimization: Check if we are ALREADY at the target cell
36
- if (activeCell && activeCell.rowIndex === rowIndex && activeCell.columnIndex === index) {
37
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: Already at target cell {row: ${rowIndex}, col: ${index}}`);
38
- // Still participate in the barrier (no-op) so peers are not left waiting.
39
- if (barrier)
40
- await barrier.sync(index);
41
- return activeCell.locator;
42
- }
43
- }
44
- const context = { config, root: rootLocator, page, resolve, getHeaders };
45
- // Shared helper: is the target cell currently in the DOM?
46
- const targetReached = async () => {
47
- if (config.strategies.getActiveCell) {
48
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
49
- if (ac && ac.rowIndex === rowIndex && ac.columnIndex === index)
50
- return true;
51
- }
52
- // Locator presence: virtualized grids (e.g. Glide) often attach the target `td` after scroll
53
- // while focus id lags; do not require getActiveCell to match for this to count as reached.
54
- const cell = config.strategies.getCellLocator
55
- ? config.strategies.getCellLocator({ row: rowLocator, root: rootLocator, columnName: column, columnIndex: index, rowIndex, page, config })
56
- : resolve(config.cellSelector, rowLocator).nth(index);
57
- return (await cell.count() > 0);
58
- };
59
- const getCellLocator = () => config.strategies.getCellLocator
60
- ? config.strategies.getCellLocator({ row: rowLocator, root: rootLocator, columnName: column, columnIndex: index, rowIndex, page, config })
61
- : resolve(config.cellSelector, rowLocator).nth(index);
62
- // Viewport-oracle phase: direct positioning for 2D virtualized tables.
63
- // Runs before navigation primitives; falls through to them if cell still not accessible.
64
- const viewport = config.strategies.viewport;
65
- if (viewport && typeof rowIndex === 'number') {
66
- // Fast path via range oracle: re-used below to avoid a second DOM round-trip.
67
- // Rows do NOT sync at the barrier here — only scroll boundaries need coordination.
68
- let knownColRange = null;
69
- if (viewport.getVisibleColumnRange) {
70
- knownColRange = await viewport.getVisibleColumnRange(context);
71
- if (knownColRange && index >= knownColRange.first && index <= knownColRange.last) {
72
- const rowRange = viewport.getVisibleRowRange
73
- ? await viewport.getVisibleRowRange(context)
74
- : null;
75
- const rowVisible = !rowRange || (rowIndex >= rowRange.first && rowIndex <= rowRange.last);
76
- let cellAttached = rowVisible && await targetReached();
77
- if (rowVisible && !cellAttached) {
78
- try {
79
- await getCellLocator().waitFor({ state: 'attached', timeout: 500 });
80
- cellAttached = true;
81
- }
82
- catch (error) {
83
- if ((error === null || error === void 0 ? void 0 : error.name) !== 'TimeoutError')
84
- throw error;
85
- }
86
- }
87
- if (cellAttached) {
88
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: col ${index} in visible range [${knownColRange.first}-${knownColRange.last}], reading directly`);
89
- // Check in to the barrier without a moveAction — peers that need the scroll
90
- // still supply their own action and will trigger it when all rows arrive.
91
- if (barrier)
92
- await barrier.sync(index);
93
- return getCellLocator();
94
- }
95
- }
96
- }
97
- else if (await targetReached()) {
98
- // No range oracle — fall back to DOM count() check.
99
- if (barrier)
100
- await barrier.sync(index);
101
- return getCellLocator();
102
- }
103
- // Column is out of view. Scroll to bring it in.
104
- // In synchronized mode, the last row to arrive triggers the scroll once for all rows.
105
- const scrollIntoViewport = async () => {
106
- // Always scroll — scrollToColumn is idempotent (bounds-checks before acting).
107
- // The trigger row may already have the cell via overscan, but other rows in the
108
- // batch might not; skipping the scroll here would silently starve them.
109
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: col ${index} out of view, scrolling`);
110
- if (viewport.scrollToColumn) {
111
- await viewport.scrollToColumn(context, index);
112
- }
113
- };
114
- if (barrier) {
115
- await barrier.sync(index, scrollIntoViewport);
116
- }
117
- else {
118
- await scrollIntoViewport();
119
- }
120
- // Per-row 2D recovery: horizontal scroll may evict rows from the vertical viewport
121
- // (scrollbar layout shift) or rows may simply be outside the virtual row window.
122
- // Recovery is serialized via the barrier's mutex so concurrent rows don't race on
123
- // scrollTop — adjacent rows often appear in view for free after the previous recovery.
124
- // scrollToRow must only adjust scrollTop (not scrollLeft) to preserve the column
125
- // position established by scrollToColumn.
126
- if (!await targetReached() && viewport.scrollToRow && typeof rowIndex === 'number') {
127
- const scrollToRow = viewport.scrollToRow;
128
- const recover = async () => {
129
- if (!await targetReached()) {
130
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: row ${rowIndex} evicted by col scroll, recovering`);
131
- await scrollToRow(context, rowIndex);
132
- }
133
- };
134
- if (barrier) {
135
- await barrier.runExclusiveRecovery(recover);
136
- }
137
- else {
138
- await recover();
139
- }
140
- }
141
- if (await targetReached()) {
142
- return getCellLocator();
143
- }
144
- // Cell still not accessible — fall through to navigation primitives if configured.
145
- if (!config.strategies.navigation) {
146
- if (allowMissingCell)
147
- return getCellLocator();
148
- const colRange = viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
149
- const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
150
- let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex}.\n`;
151
- if (colRange) {
152
- errMsg += ` Visible column range: [${colRange.first}–${colRange.last}]. Column is out of view and no navigation fallback is configured.\n`;
153
- }
154
- else {
155
- errMsg += ` Column is out of view and no navigation fallback is configured.\n`;
156
- }
157
- if (rowRange && rowIndex !== undefined) {
158
- const inView = rowIndex >= rowRange.first && rowIndex <= rowRange.last;
159
- errMsg += ` Visible row range: [${rowRange.first}–${rowRange.last}]. ${inView ? 'Row is in view.' : 'Row is out of view.'}\n`;
160
- }
161
- errMsg += ` → Add a \`strategies.navigation\` or \`strategies.viewport.scrollToColumn\` to handle off-screen columns.`;
162
- throw new Error(errMsg);
163
- }
164
- }
165
- // Use navigation primitives if available
166
- if (config.strategies.navigation) {
167
- const nav = config.strategies.navigation;
168
- if (await targetReached()) {
169
- // Already there. If we have a barrier, check-in to stay in lock-step.
170
- if (barrier)
171
- await barrier.sync(index);
172
- return getCellLocator();
173
- }
174
- if (typeof rowIndex !== 'number') {
175
- if (allowMissingCell)
176
- return getCellLocator();
177
- throw new Error('Row index is required for navigation');
178
- }
179
- // If getActiveCell is present but returns null (no focus), and cell is in DOM,
180
- // try to focus it once before entering navigation loop.
181
- if (config.strategies.getActiveCell) {
182
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
183
- if (!ac) {
184
- const cell = resolve(config.cellSelector, rowLocator).nth(index);
185
- if (await cell.count() > 0) {
186
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: cell "${column}" found but not focused; focusing.`);
187
- await cell.focus();
188
- // Re-check target
189
- if (await targetReached()) {
190
- if (barrier)
191
- await barrier.sync(index);
192
- return cell;
193
- }
194
- }
195
- }
196
- }
197
- const navigateUntilReached = async () => {
198
- let currRow = 0;
199
- let currCol = 0;
200
- if (config.strategies.getActiveCell) {
201
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
202
- if (ac) {
203
- currRow = ac.rowIndex;
204
- currCol = ac.columnIndex;
205
- }
206
- }
207
- const rDiff = rowIndex - currRow;
208
- // Navigate vertically (tight loop)
209
- for (let i = 0; i < Math.abs(rDiff); i++) {
210
- if (rDiff > 0 && nav.goDown)
211
- await nav.goDown(context);
212
- else if (rDiff < 0 && nav.goUp)
213
- await nav.goUp(context);
214
- }
215
- if (config.strategies.getActiveCell) {
216
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
217
- if (ac) {
218
- currRow = ac.rowIndex;
219
- currCol = ac.columnIndex;
220
- }
221
- }
222
- // Column-0 snap is owned by the navigation primitive (scrollLeft=0, Home, etc.).
223
- // Core must not special-case canvas/Home — that was pre-viewport Glide glue.
224
- if (index === 0 && nav.snapFirstColumnIntoView) {
225
- (0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: snapFirstColumnIntoView for column index 0');
226
- await nav.snapFirstColumnIntoView(context);
227
- if (config.strategies.getActiveCell) {
228
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
229
- if (ac) {
230
- currRow = ac.rowIndex;
231
- currCol = ac.columnIndex;
232
- }
233
- }
234
- }
235
- let cDiff = index - currCol;
236
- if (Math.abs(cDiff) > 12 && nav.seekColumnIndex) {
237
- (0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: seekColumnIndex approx for column ${index}`);
238
- await nav.seekColumnIndex(context, index);
239
- if (config.strategies.getActiveCell) {
240
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
241
- if (ac) {
242
- currRow = ac.rowIndex;
243
- currCol = ac.columnIndex;
244
- }
245
- }
246
- cDiff = index - currCol;
247
- }
248
- for (let i = 0; i < Math.abs(cDiff); i++) {
249
- if (cDiff > 0 && nav.goRight)
250
- await nav.goRight(context);
251
- else if (cDiff < 0 && nav.goLeft)
252
- await nav.goLeft(context);
253
- }
254
- const horizontalSteps = Math.abs(cDiff);
255
- if (horizontalSteps > 0) {
256
- const computed = Math.min(2500, 60 + horizontalSteps * 12);
257
- const settleMs = (Number.isFinite(nav.settleMs) && nav.settleMs >= 0) ? nav.settleMs : computed;
258
- await page.waitForTimeout(settleMs);
259
- }
260
- // After horizontal steps, poll until getActiveCell / DOM presence confirms the target
261
- // (a11y trees and virtual mounts often lag the scroll). Tune via navigation.maxWaitMs.
262
- const pollIntervalMs = 10;
263
- const computedMax = Math.min(6000, 250 + horizontalSteps * 25);
264
- const maxWaitMs = (Number.isFinite(nav.maxWaitMs) && nav.maxWaitMs >= 0) ? nav.maxWaitMs : computedMax;
265
- const start = Date.now();
266
- while (Date.now() - start < maxWaitMs) {
267
- if (config.strategies.getActiveCell) {
268
- const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
269
- if (ac && ac.rowIndex === rowIndex && ac.columnIndex === index) {
270
- return ac.locator;
271
- }
272
- }
273
- if (await targetReached()) {
274
- break;
275
- }
276
- await page.waitForTimeout(pollIntervalMs);
277
- }
278
- };
279
- // Not there, perform (coordinated) navigation
280
- let navResult;
281
- if (barrier) {
282
- navResult = await barrier.sync(index, navigateUntilReached);
283
- }
284
- else {
285
- navResult = await navigateUntilReached();
286
- }
287
- // navigateUntilReached may return a Locator it found mid-navigation
288
- if (navResult && typeof navResult.count === 'function') {
289
- const loc = navResult;
290
- if (await loc.count() > 0)
291
- return loc;
292
- }
293
- // Return final locator if reached
294
- const finalCell = getCellLocator();
295
- if (await finalCell.count() > 0)
296
- return finalCell;
297
- if (allowMissingCell)
298
- return finalCell;
299
- const colRange = viewport && viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
300
- const rowRange = viewport && viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
301
- let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex} after exhausting navigation strategies. Ensure navigation primitives are correctly implemented.\n`;
302
- if (colRange) {
303
- errMsg += ` Visible column range: [${colRange.first}–${colRange.last}].\n`;
304
- }
305
- if (rowRange && rowIndex !== undefined) {
306
- errMsg += ` Visible row range: [${rowRange.first}–${rowRange.last}].\n`;
307
- }
308
- throw new Error(errMsg);
309
- }
310
- // No horizontal strategies configured; assume table is 1D or fully rendered,
311
- // and let standard Playwright auto-waiting handle any lazy-loading delays.
312
- return getCellLocator();
313
- };
314
13
  /**
315
14
  * Factory to create a SmartRow by extending a Playwright Locator.
316
15
  * We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
@@ -356,7 +55,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
356
55
  const smartCell = (0, locatorDescription_1.describeLocator)(baseLocator, cellDescription, config);
357
56
  smartCell.bringIntoView = async () => {
358
57
  const page = rootLocator.page();
359
- const navigatedCell = await _navigateToCell({
58
+ const navigatedCell = await (0, cellNavigation_1.navigateToCell)({
360
59
  config,
361
60
  rootLocator,
362
61
  page,
@@ -371,14 +70,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
371
70
  });
372
71
  // Run beforeCellRead hook (same as toJSON does after navigation).
373
72
  if (config.strategies.beforeCellRead) {
374
- const getHeaderCell = (table === null || table === void 0 ? void 0 : table.getHeaderCell)
375
- ? table.getHeaderCell.bind(table)
376
- : async (colNameArg) => {
377
- const i = map.get(colNameArg);
378
- if (i === undefined)
379
- throw new Error(`Column "${colNameArg}" not found`);
380
- return resolve(config.headerSelector, rootLocator).nth(i);
381
- };
73
+ const getHeaderCell = (0, rowSnapshot_1.headerCellGetter)({ config, rootLocator, resolve, map, table });
382
74
  await config.strategies.beforeCellRead({
383
75
  cell: navigatedCell !== null && navigatedCell !== void 0 ? navigatedCell : baseLocator,
384
76
  columnName: colName,
@@ -419,7 +111,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
419
111
  const page = rootLocator.page();
420
112
  // Same nav pipeline as toJSON / getCell().bringIntoView() (#430) so virtualized
421
113
  // off-screen columns don't return empty/stale text.
422
- const cell = await _navigateToCell({
114
+ const cell = await (0, cellNavigation_1.navigateToCell)({
423
115
  config,
424
116
  rootLocator,
425
117
  page,
@@ -444,385 +136,13 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
444
136
  }
445
137
  return (await cell.innerText()).trim();
446
138
  };
139
+ const snapshotEnv = {
140
+ config, rootLocator, resolve, map, row: smart, rowLocator, rowIndex, table, renderWindowPosition,
141
+ };
447
142
  smart.toJSON = async (options) => {
448
- var _a, _b, _c, _d, _e, _f;
449
- var _g, _h, _j;
450
- // Atomic mode: snapshot the row in a single evaluate, then apply column overrides
451
- // against a frozen reconstruction. Zero inter-column stagger — even in-place React
452
- // re-renders can't affect it because overrides read from a detached clone.
453
- if (options === null || options === void 0 ? void 0 : options.atomic) {
454
- const cellSel = config.cellSelector;
455
- if (typeof cellSel !== 'string') {
456
- throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
457
- }
458
- const page = rootLocator.page();
459
- // Re-pin: the row locator is a positional nth-child from the iteration loop.
460
- // On recycling virtualizers, elements can shift between SmartRow creation and
461
- // this clone — the nth element may now show a different row's content.
462
- // Verify via resolveRowIndex and rescan if drifted.
463
- let cloneTarget = rowLocator;
464
- const resolveRI = config.strategies.resolveRowIndex;
465
- const isSelfHealing = smart._selfHealing === true;
466
- const rescanForRow = resolveRI && typeof rowIndex === 'number' && !isSelfHealing
467
- ? async () => {
468
- const rows = await resolve(config.rowSelector, rootLocator).all();
469
- for (const r of rows) {
470
- try {
471
- const rResult = await resolveRI(r);
472
- if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
473
- return r;
474
- }
475
- catch (_a) { /* element may have detached during scan */ }
476
- }
477
- return null;
478
- }
479
- : null;
480
- if (rescanForRow) {
481
- let drifted = false;
482
- try {
483
- const curResult = await resolveRI(rowLocator);
484
- const curIndex = curResult !== undefined
485
- ? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
486
- : undefined;
487
- drifted = curIndex !== rowIndex;
488
- }
489
- catch (_k) {
490
- drifted = true;
491
- }
492
- if (drifted) {
493
- const inBatch = smart._inBatch === true || !!smart._barrier;
494
- let recovered = await rescanForRow();
495
- if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
496
- await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
497
- const deadline = Date.now() + 500;
498
- while (!recovered && Date.now() < deadline) {
499
- recovered = await rescanForRow();
500
- if (!recovered)
501
- await page.waitForTimeout(50);
502
- }
503
- }
504
- if (!recovered) {
505
- throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
506
- (inBatch
507
- ? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
508
- : `Ensure a viewport.scrollToRow strategy can restore the row.`));
509
- }
510
- cloneTarget = recovered;
511
- }
512
- }
513
- // Wait for async content renders to settle before cloning.
514
- if (config.strategies.contentReady) {
515
- await config.strategies.contentReady(cloneTarget, page);
516
- // Revalidate: the element may have recycled during the await.
517
- if (rescanForRow) {
518
- let postIdx;
519
- try {
520
- const r = await resolveRI(cloneTarget);
521
- postIdx = r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
522
- }
523
- catch (_l) {
524
- postIdx = undefined;
525
- }
526
- if (postIdx !== rowIndex) {
527
- const reFound = await rescanForRow();
528
- if (reFound)
529
- cloneTarget = reFound;
530
- }
531
- }
532
- }
533
- // Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
534
- // Uses textContent (not innerText) because the clone is detached — innerText
535
- // falls back to textContent on detached nodes per HTML spec, so be explicit.
536
- const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
537
- let snapshot;
538
- try {
539
- snapshot = await cloneHandle.evaluate((row, sel) => {
540
- const cells = row.querySelectorAll(sel);
541
- return {
542
- rowHTML: row.outerHTML,
543
- cells: [...cells].map(c => ({
544
- text: (c.textContent || '').trim(),
545
- })),
546
- };
547
- }, cellSel);
548
- }
549
- finally {
550
- await cloneHandle.dispose();
551
- }
552
- // Determine which columns have overrides (need lazy materialization)
553
- const columnsToProcess = [...map.entries()]
554
- .filter(([col]) => !(options === null || options === void 0 ? void 0 : options.columns) || options.columns.includes(col));
555
- const hasAnyOverride = columnsToProcess.some(([col]) => { var _a, _b; return !!((_b = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b.read); });
556
- // Step 2: lazily materialize the row if any override needs a Locator.
557
- // Uses position:fixed off-screen (NOT display:none — that breaks innerText).
558
- const snapId = `__st_snap_${Math.random().toString(36).slice(2, 10)}`;
559
- let materialized = false;
560
- const materializeRow = async () => {
561
- if (materialized)
562
- return;
563
- await page.evaluate(([html, id]) => {
564
- const c = document.createElement('div');
565
- c.id = id;
566
- c.style.cssText = 'position:fixed;left:-99999px;top:-99999px;opacity:0;pointer-events:none';
567
- // Wrap in <table><tbody> so the browser's HTML parser keeps <tr>/<td> intact.
568
- c.innerHTML = `<table><tbody>${html}</tbody></table>`;
569
- document.body.appendChild(c);
570
- }, [snapshot.rowHTML, snapId]);
571
- materialized = true;
572
- };
573
- const cleanupSnapshot = async () => {
574
- if (!materialized)
575
- return;
576
- await page.evaluate(id => { var _a; return (_a = document.getElementById(id)) === null || _a === void 0 ? void 0 : _a.remove(); }, snapId);
577
- };
578
- // Step 3: build getCell helper — returns Locator to cell in materialized row
579
- const getCell = (colName) => {
580
- const colIdx = map.get(colName);
581
- if (colIdx === undefined)
582
- throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
583
- return page.locator(`#${snapId}`).locator(cellSel).nth(colIdx);
584
- };
585
- try {
586
- if (hasAnyOverride)
587
- await materializeRow();
588
- const result = {};
589
- for (const [col, idx] of columnsToProcess) {
590
- const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
591
- const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
592
- if (mapper) {
593
- const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
594
- try {
595
- result[col] = await mapper(cell, { row: smart, columnName: col, columnIndex: idx, getCell });
596
- }
597
- catch (err) {
598
- const wrapped = new Error(`[SmartTable] toJSON({ atomic: true }): column override for "${col}" failed — ${err.message}`);
599
- wrapped.cause = err;
600
- throw wrapped;
601
- }
602
- }
603
- else if (idx < snapshot.cells.length) {
604
- result[col] = snapshot.cells[idx].text;
605
- }
606
- }
607
- for (const [name, def] of Object.entries((_g = config.syntheticColumns) !== null && _g !== void 0 ? _g : {})) {
608
- if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
609
- continue;
610
- const snapshotRow = Object.create(smart);
611
- snapshotRow.getValue = async (col) => {
612
- if (col in result)
613
- return String(result[col]);
614
- return smart.getValue(col);
615
- };
616
- result[name] = String(await def.compute(snapshotRow));
617
- }
618
- return result;
619
- }
620
- finally {
621
- await cleanupSnapshot();
622
- }
623
- }
624
- const result = {};
625
- const page = rootLocator.page();
626
- // Build a getHeaderCell helper for the beforeCellRead context.
627
- // Uses the table reference if available, otherwise falls back to index-based lookup.
628
- const getHeaderCell = (table === null || table === void 0 ? void 0 : table.getHeaderCell)
629
- ? table.getHeaderCell.bind(table)
630
- : async (colName) => {
631
- const idx = map.get(colName);
632
- if (idx === undefined)
633
- throw new Error(`Column "${colName}" not found`);
634
- return resolve(config.headerSelector, rootLocator).nth(idx);
635
- };
636
- // #366 / #392: on virtualized tables the row's DOM node can recycle between the
637
- // per-column awaits below. When a self-healing locator is in use (the row locator was
638
- // rebuilt with a CSS selector, e.g. `[data-rowindex="47"]`), the locator automatically
639
- // re-queries the DOM — no rescan needed. For plain-number strategies (JS-only, no
640
- // selector), fall back to the original rescan-then-scrollToRow approach.
641
- const resolveRI = config.strategies.resolveRowIndex;
642
- const canPin = !!resolveRI && typeof rowIndex === 'number';
643
- const isSelfHealing = smart._selfHealing === true;
644
- const stableCtx = { root: rootLocator, config, page, resolve };
645
- const inBatch = smart._inBatch === true || !!smart._barrier;
646
- let lastGood = rowLocator;
647
- const resolveStableRow = async () => {
648
- var _a;
649
- // Self-healing locator: always re-queries the DOM by attribute — no rescan needed.
650
- if (isSelfHealing)
651
- return rowLocator;
652
- if (!canPin)
653
- return rowLocator;
654
- const result = await resolveRI(lastGood);
655
- const resolvedIndex = result !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(result).index : undefined;
656
- if (resolvedIndex === rowIndex)
657
- return lastGood; // still correct
658
- // Drifted (recycled). Re-locate the row carrying the expected logical index.
659
- const rescan = async () => {
660
- const rows = await resolve(config.rowSelector, rootLocator).all();
661
- for (const r of rows) {
662
- const rResult = await resolveRI(r);
663
- if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
664
- return r;
665
- }
666
- return null;
667
- };
668
- let recovered = await rescan();
669
- if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
670
- await config.strategies.viewport.scrollToRow(stableCtx, rowIndex);
671
- recovered = await rescan();
672
- }
673
- if (!recovered) {
674
- throw new Error(`[SmartTable] toJSON: row ${rowIndex} recycled out of the DOM mid-read and could not be recovered — ` +
675
- `the returned object would mix fields from different rows. ` +
676
- (inBatch
677
- ? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows; the row must stay mounted for the read.)`
678
- : `Ensure a viewport.scrollToRow can restore the row.`));
679
- }
680
- lastGood = recovered;
681
- return recovered;
682
- };
683
- // getCell for non-atomic mode: returns a live cell Locator via the column map.
684
- // Uses the last-known-good row from the re-pin so it's as fresh as possible,
685
- // but returns raw cell Locators, not override-processed values.
686
- const getCellLive = (colName) => {
687
- const colIdx = map.get(colName);
688
- if (colIdx === undefined)
689
- throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
690
- const row = lastGood;
691
- if (config.strategies.getCellLocator) {
692
- return config.strategies.getCellLocator({
693
- row, root: rootLocator, columnName: colName,
694
- columnIndex: colIdx, rowIndex, page, config,
695
- });
696
- }
697
- return resolve(config.cellSelector, row).nth(colIdx);
698
- };
699
- for (const [col, idx] of map.entries()) {
700
- if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(col)) {
701
- continue;
702
- }
703
- // Re-pin to the correct logical row before reading this column (#366).
704
- const stableRow = await resolveStableRow();
705
- // Check if we have a column override for this column
706
- const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[col];
707
- const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
708
- // --- Navigation Logic Start ---
709
- const cell = config.strategies.getCellLocator
710
- ? config.strategies.getCellLocator({
711
- row: stableRow,
712
- root: rootLocator,
713
- columnName: col,
714
- columnIndex: idx,
715
- rowIndex: rowIndex,
716
- page: page,
717
- config
718
- })
719
- : resolve(config.cellSelector, stableRow).nth(idx);
720
- let targetCell = cell;
721
- // Always call _navigateToCell to ensure Lock-Step synchronization
722
- // even if the cell is already present (it handles check-in inside)
723
- const navigatedCell = await _navigateToCell({
724
- config,
725
- rootLocator,
726
- page,
727
- resolve,
728
- getHeaders: table === null || table === void 0 ? void 0 : table.getHeaders,
729
- column: col,
730
- index: idx,
731
- rowLocator: stableRow,
732
- rowIndex,
733
- renderWindowPosition,
734
- barrier: smart._barrier,
735
- allowMissingCell: !!mapper,
736
- });
737
- if (navigatedCell) {
738
- targetCell = navigatedCell;
739
- }
740
- // --- Navigation Logic End ---
741
- // Call beforeCellRead hook if configured.
742
- // Fires for BOTH columnOverrides.read and the default innerText path.
743
- if (config.strategies.beforeCellRead) {
744
- await config.strategies.beforeCellRead({
745
- cell: targetCell,
746
- columnName: col,
747
- columnIndex: idx,
748
- row: stableRow,
749
- page,
750
- root: rootLocator,
751
- getHeaderCell,
752
- });
753
- }
754
- // Cell loading wait (if configured)
755
- const isCellLoading = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.isCellLoading;
756
- const rawCellTimeout = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.cellLoadingTimeout;
757
- // undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
758
- const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
759
- ? rawCellTimeout
760
- : undefined;
761
- const onCellLoadingTimeout = (_h = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
762
- if (isCellLoading) {
763
- let loading = await isCellLoading(targetCell, col, smart);
764
- // First-paint race: the cell node can attach a tick before the loading
765
- // indicator mounts. A single false-negative then reads "" and skips
766
- // onCellLoadingTimeout. Only grace when the cell still looks empty.
767
- if (!loading && cellLoadingTimeout !== undefined && cellLoadingTimeout > 0) {
768
- const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
769
- if (peek === '') {
770
- const graceDeadline = Date.now() + Math.min(250, cellLoadingTimeout);
771
- while (!loading && Date.now() < graceDeadline) {
772
- await page.waitForTimeout(25);
773
- loading = await isCellLoading(targetCell, col, smart);
774
- }
775
- }
776
- }
777
- if (loading && cellLoadingTimeout !== undefined) {
778
- (0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
779
- const deadline = Date.now() + cellLoadingTimeout;
780
- let cellResolved = !(await isCellLoading(targetCell, col, smart)); // immediate check (handles timeout=0)
781
- while (!cellResolved && Date.now() < deadline) {
782
- await page.waitForTimeout(100);
783
- cellResolved = !(await isCellLoading(targetCell, col, smart));
784
- }
785
- if (!cellResolved) {
786
- (0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — still loading after ${cellLoadingTimeout}ms, action: ${typeof onCellLoadingTimeout === 'function' ? 'callback' : onCellLoadingTimeout}`);
787
- if (typeof onCellLoadingTimeout === 'function') {
788
- result[col] = await onCellLoadingTimeout(targetCell, col, smart);
789
- continue;
790
- }
791
- if (onCellLoadingTimeout === 'skip') {
792
- result[col] = '';
793
- continue;
794
- }
795
- if (onCellLoadingTimeout === 'throw') {
796
- throw new Error(`[SmartTable] Cell "${col}" (row ${rowIndex}) did not finish loading within ${cellLoadingTimeout}ms`);
797
- }
798
- // 'read-as-is': fall through to normal read
799
- }
800
- }
801
- // If no timeout or resolved: fall through to normal read
802
- }
803
- if (mapper) {
804
- // Apply mapper
805
- const mappedValue = await mapper(targetCell, { row: smart, columnName: col, columnIndex: idx, getCell: getCellLive });
806
- result[col] = mappedValue;
807
- }
808
- else {
809
- // Default string extraction
810
- const text = await targetCell.innerText();
811
- result[col] = (text || '').trim();
812
- }
813
- }
814
- for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
815
- if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
816
- continue;
817
- const snapshotRow = Object.create(smart);
818
- snapshotRow.getValue = async (col) => {
819
- if (col in result)
820
- return String(result[col]);
821
- return smart.getValue(col);
822
- };
823
- result[name] = String(await def.compute(snapshotRow));
824
- }
825
- return result;
143
+ return (options === null || options === void 0 ? void 0 : options.atomic)
144
+ ? (0, rowSnapshot_1.readRowAtomic)(snapshotEnv, options.columns)
145
+ : (0, rowSnapshot_1.readRowLive)(snapshotEnv, options === null || options === void 0 ? void 0 : options.columns);
826
146
  };
827
147
  smart.smartFill = async (data, fillOptions) => {
828
148
  var _a, _b;
@@ -838,7 +158,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
838
158
  if (colIdx === undefined) {
839
159
  throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})]));
840
160
  }
841
- await _navigateToCell({
161
+ await (0, cellNavigation_1.navigateToCell)({
842
162
  config,
843
163
  rootLocator,
844
164
  page: rootLocator.page(),