@rickcedwhat/playwright-smart-table 6.20.1 → 6.21.0-next.3ab7a18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/rowFinder.d.ts +1 -0
- package/dist/engine/rowFinder.js +73 -75
- package/dist/engine/scanPages.d.ts +58 -0
- package/dist/engine/scanPages.js +46 -0
- package/dist/engine/tableIteration.d.ts +1 -0
- package/dist/engine/tableIteration.js +40 -22
- package/dist/filterEngine.d.ts +4 -0
- package/dist/filterEngine.js +15 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +51 -10
- package/dist/plugins/index.js +14 -6
- package/dist/presets/glide/index.d.ts +1 -1
- package/dist/presets/glide/index.js +2 -2
- package/dist/presets/mui.js +15 -29
- package/dist/presets/rdg.d.ts +1 -1
- package/dist/presets/rdg.js +4 -4
- package/dist/smartRow.d.ts +2 -2
- package/dist/smartRow.js +163 -51
- package/dist/strategies/columns.d.ts +5 -9
- package/dist/strategies/columns.js +0 -10
- package/dist/strategies/contentReady.d.ts +20 -0
- package/dist/strategies/contentReady.js +62 -0
- package/dist/strategies/filter.d.ts +0 -4
- package/dist/strategies/filter.js +0 -16
- package/dist/strategies/headers.d.ts +5 -0
- package/dist/strategies/headers.js +17 -9
- package/dist/strategies/index.d.ts +23 -15
- package/dist/strategies/index.js +9 -5
- package/dist/strategies/pagination.js +11 -2
- package/dist/strategies/viewport.d.ts +4 -1
- package/dist/strategies/viewport.js +62 -32
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +77 -17
- package/dist/types.d.ts +74 -17
- package/dist/useTable.js +125 -56
- package/dist/utils/elementTracker.js +6 -3
- package/dist/utils/loadingWait.d.ts +15 -0
- package/dist/utils/loadingWait.js +43 -0
- package/dist/utils/pageIndex.d.ts +12 -0
- package/dist/utils/pageIndex.js +19 -0
- package/dist/utils/resolveCellLocator.d.ts +35 -0
- package/dist/utils/resolveCellLocator.js +52 -0
- package/package.json +1 -1
package/dist/smartRow.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const test_1 = require("@playwright/test");
|
|
3
4
|
const rowResolution_1 = require("./engine/rowResolution");
|
|
4
5
|
const fill_1 = require("./strategies/fill");
|
|
5
6
|
const stringUtils_1 = require("./utils/stringUtils");
|
|
6
7
|
const debugUtils_1 = require("./utils/debugUtils");
|
|
7
8
|
const paginationPath_1 = require("./utils/paginationPath");
|
|
8
9
|
const sentinel_1 = require("./utils/sentinel");
|
|
10
|
+
const pageIndex_1 = require("./utils/pageIndex");
|
|
9
11
|
/**
|
|
10
12
|
* Internal helper to navigate to a cell with active cell optimization.
|
|
11
13
|
* Uses `strategies.navigation` primitives (goUp/goDown/goLeft/goRight, optional snap/seek/goHome).
|
|
12
14
|
* Returns the target cell locator after navigation.
|
|
13
15
|
*/
|
|
14
16
|
const _navigateToCell = async (params) => {
|
|
15
|
-
const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier } = params;
|
|
17
|
+
const { config, rootLocator, page, resolve, getHeaders, column, index, rowLocator, rowIndex, barrier, allowMissingCell } = params;
|
|
16
18
|
const rowLabel = typeof rowIndex === 'number' ? `row ${rowIndex}` : 'row ?';
|
|
17
19
|
(0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: resolving column "${column}" (colIndex ${index}), ${rowLabel}`);
|
|
18
20
|
// Get active cell if strategy is available
|
|
@@ -65,7 +67,18 @@ const _navigateToCell = async (params) => {
|
|
|
65
67
|
? await viewport.getVisibleRowRange(context)
|
|
66
68
|
: null;
|
|
67
69
|
const rowVisible = !rowRange || (rowIndex >= rowRange.first && rowIndex <= rowRange.last);
|
|
68
|
-
|
|
70
|
+
let cellAttached = rowVisible && await targetReached();
|
|
71
|
+
if (rowVisible && !cellAttached) {
|
|
72
|
+
try {
|
|
73
|
+
await getCellLocator().waitFor({ state: 'attached', timeout: 500 });
|
|
74
|
+
cellAttached = true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof test_1.errors.TimeoutError))
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (cellAttached) {
|
|
69
82
|
(0, debugUtils_1.logDebug)(config, 'verbose', `_navigateToCell: col ${index} in visible range [${knownColRange.first}-${knownColRange.last}], reading directly`);
|
|
70
83
|
// Check in to the barrier without a moveAction — peers that need the scroll
|
|
71
84
|
// still supply their own action and will trigger it when all rows arrive.
|
|
@@ -124,6 +137,8 @@ const _navigateToCell = async (params) => {
|
|
|
124
137
|
}
|
|
125
138
|
// Cell still not accessible — fall through to navigation primitives if configured.
|
|
126
139
|
if (!config.strategies.navigation) {
|
|
140
|
+
if (allowMissingCell)
|
|
141
|
+
return getCellLocator();
|
|
127
142
|
const colRange = viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
|
|
128
143
|
const rowRange = viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
|
|
129
144
|
let errMsg = `SmartTable: could not reach cell for column "${column}" (colIndex ${index}) at row ${rowIndex}.\n`;
|
|
@@ -144,15 +159,17 @@ const _navigateToCell = async (params) => {
|
|
|
144
159
|
// Use navigation primitives if available
|
|
145
160
|
if (config.strategies.navigation) {
|
|
146
161
|
const nav = config.strategies.navigation;
|
|
147
|
-
if (typeof rowIndex !== 'number') {
|
|
148
|
-
throw new Error('Row index is required for navigation');
|
|
149
|
-
}
|
|
150
162
|
if (await targetReached()) {
|
|
151
163
|
// Already there. If we have a barrier, check-in to stay in lock-step.
|
|
152
164
|
if (barrier)
|
|
153
165
|
await barrier.sync(index);
|
|
154
166
|
return getCellLocator();
|
|
155
167
|
}
|
|
168
|
+
if (typeof rowIndex !== 'number') {
|
|
169
|
+
if (allowMissingCell)
|
|
170
|
+
return getCellLocator();
|
|
171
|
+
throw new Error('Row index is required for navigation');
|
|
172
|
+
}
|
|
156
173
|
// If getActiveCell is present but returns null (no focus), and cell is in DOM,
|
|
157
174
|
// try to focus it once before entering navigation loop.
|
|
158
175
|
if (config.strategies.getActiveCell) {
|
|
@@ -196,6 +213,8 @@ const _navigateToCell = async (params) => {
|
|
|
196
213
|
currCol = ac.columnIndex;
|
|
197
214
|
}
|
|
198
215
|
}
|
|
216
|
+
// Column-0 snap is owned by the navigation primitive (scrollLeft=0, Home, etc.).
|
|
217
|
+
// Core must not special-case canvas/Home — that was pre-viewport Glide glue.
|
|
199
218
|
if (index === 0 && nav.snapFirstColumnIntoView) {
|
|
200
219
|
(0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: snapFirstColumnIntoView for column index 0');
|
|
201
220
|
await nav.snapFirstColumnIntoView(context);
|
|
@@ -206,27 +225,6 @@ const _navigateToCell = async (params) => {
|
|
|
206
225
|
currCol = ac.columnIndex;
|
|
207
226
|
}
|
|
208
227
|
}
|
|
209
|
-
// Home moves a11y focus within the current row to column 0. If focus row !== target row
|
|
210
|
-
// (e.g. still on previous row), Home jumps to grid origin and breaks `tr.nth(k)` reads.
|
|
211
|
-
if (typeof rowIndex === 'number' && currRow === rowIndex) {
|
|
212
|
-
await rootLocator.evaluate((el) => {
|
|
213
|
-
var _a;
|
|
214
|
-
const canvas = el.closest('canvas') || ((_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('canvas'));
|
|
215
|
-
if (canvas instanceof HTMLCanvasElement) {
|
|
216
|
-
canvas.tabIndex = 0;
|
|
217
|
-
canvas.focus();
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
await page.keyboard.press('Home');
|
|
221
|
-
await page.waitForTimeout(120);
|
|
222
|
-
if (config.strategies.getActiveCell) {
|
|
223
|
-
const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
|
|
224
|
-
if (ac) {
|
|
225
|
-
currRow = ac.rowIndex;
|
|
226
|
-
currCol = ac.columnIndex;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
228
|
}
|
|
231
229
|
let cDiff = index - currCol;
|
|
232
230
|
if (Math.abs(cDiff) > 12 && nav.seekColumnIndex) {
|
|
@@ -253,8 +251,8 @@ const _navigateToCell = async (params) => {
|
|
|
253
251
|
const settleMs = (Number.isFinite(nav.settleMs) && nav.settleMs >= 0) ? nav.settleMs : computed;
|
|
254
252
|
await page.waitForTimeout(settleMs);
|
|
255
253
|
}
|
|
256
|
-
//
|
|
257
|
-
//
|
|
254
|
+
// After horizontal steps, poll until getActiveCell / DOM presence confirms the target
|
|
255
|
+
// (a11y trees and virtual mounts often lag the scroll). Tune via navigation.maxWaitMs.
|
|
258
256
|
const pollIntervalMs = 10;
|
|
259
257
|
const computedMax = Math.min(6000, 250 + horizontalSteps * 25);
|
|
260
258
|
const maxWaitMs = (Number.isFinite(nav.maxWaitMs) && nav.maxWaitMs >= 0) ? nav.maxWaitMs : computedMax;
|
|
@@ -290,6 +288,8 @@ const _navigateToCell = async (params) => {
|
|
|
290
288
|
const finalCell = getCellLocator();
|
|
291
289
|
if (await finalCell.count() > 0)
|
|
292
290
|
return finalCell;
|
|
291
|
+
if (allowMissingCell)
|
|
292
|
+
return finalCell;
|
|
293
293
|
const colRange = viewport && viewport.getVisibleColumnRange ? await viewport.getVisibleColumnRange(context) : null;
|
|
294
294
|
const rowRange = viewport && viewport.getVisibleRowRange ? await viewport.getVisibleRowRange(context) : null;
|
|
295
295
|
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`;
|
|
@@ -313,7 +313,7 @@ const _navigateToCell = async (params) => {
|
|
|
313
313
|
* @internal Internal factory for creating SmartRow objects.
|
|
314
314
|
* Not part of the public package surface; tests and consumers should use public APIs.
|
|
315
315
|
*/
|
|
316
|
-
const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier) => {
|
|
316
|
+
const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table, tablePageIndex, barrier, renderWindowPosition = false) => {
|
|
317
317
|
const smart = rowLocator;
|
|
318
318
|
// Attach State
|
|
319
319
|
smart.rowIndex = rowIndex;
|
|
@@ -406,12 +406,22 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
406
406
|
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
|
|
407
407
|
}
|
|
408
408
|
const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[colName];
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
409
|
+
const page = rootLocator.page();
|
|
410
|
+
// Same nav pipeline as toJSON / getCell().bringIntoView() (#430) so virtualized
|
|
411
|
+
// off-screen columns don't return empty/stale text.
|
|
412
|
+
const cell = await _navigateToCell({
|
|
413
|
+
config,
|
|
414
|
+
rootLocator,
|
|
415
|
+
page,
|
|
416
|
+
resolve,
|
|
417
|
+
getHeaders: table === null || table === void 0 ? void 0 : table.getHeaders,
|
|
418
|
+
column: colName,
|
|
419
|
+
index: idx,
|
|
420
|
+
rowLocator,
|
|
421
|
+
rowIndex,
|
|
422
|
+
barrier: smart._barrier,
|
|
423
|
+
allowMissingCell: !!(columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read),
|
|
424
|
+
});
|
|
415
425
|
if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
|
|
416
426
|
const getCell = (name) => {
|
|
417
427
|
const ci = map.get(name);
|
|
@@ -424,7 +434,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
424
434
|
return (await cell.innerText()).trim();
|
|
425
435
|
};
|
|
426
436
|
smart.toJSON = async (options) => {
|
|
427
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
437
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
428
438
|
// Atomic mode: snapshot the row in a single evaluate, then apply column overrides
|
|
429
439
|
// against a frozen reconstruction. Zero inter-column stagger — even in-place React
|
|
430
440
|
// re-renders can't affect it because overrides read from a detached clone.
|
|
@@ -434,10 +444,84 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
434
444
|
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
435
445
|
}
|
|
436
446
|
const page = rootLocator.page();
|
|
447
|
+
// Re-pin: the row locator is a positional nth-child from the iteration loop.
|
|
448
|
+
// On recycling virtualizers, elements can shift between SmartRow creation and
|
|
449
|
+
// this clone — the nth element may now show a different row's content.
|
|
450
|
+
// Verify via resolveRowIndex and rescan if drifted.
|
|
451
|
+
let cloneTarget = rowLocator;
|
|
452
|
+
const resolveRI = config.strategies.resolveRowIndex;
|
|
453
|
+
const isSelfHealing = smart._selfHealing === true;
|
|
454
|
+
const rescanForRow = resolveRI && typeof rowIndex === 'number' && !isSelfHealing
|
|
455
|
+
? async () => {
|
|
456
|
+
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
457
|
+
for (const r of rows) {
|
|
458
|
+
try {
|
|
459
|
+
const rResult = await resolveRI(r);
|
|
460
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
461
|
+
return r;
|
|
462
|
+
}
|
|
463
|
+
catch ( /* element may have detached during scan */_a) { /* element may have detached during scan */ }
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
: null;
|
|
468
|
+
if (rescanForRow) {
|
|
469
|
+
let drifted = false;
|
|
470
|
+
try {
|
|
471
|
+
const curResult = await resolveRI(rowLocator);
|
|
472
|
+
const curIndex = curResult !== undefined
|
|
473
|
+
? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
|
|
474
|
+
: undefined;
|
|
475
|
+
drifted = curIndex !== rowIndex;
|
|
476
|
+
}
|
|
477
|
+
catch (_k) {
|
|
478
|
+
drifted = true;
|
|
479
|
+
}
|
|
480
|
+
if (drifted) {
|
|
481
|
+
const inBatch = smart._inBatch === true || !!smart._barrier;
|
|
482
|
+
let recovered = await rescanForRow();
|
|
483
|
+
if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
|
|
484
|
+
await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
|
|
485
|
+
const deadline = Date.now() + 500;
|
|
486
|
+
while (!recovered && Date.now() < deadline) {
|
|
487
|
+
recovered = await rescanForRow();
|
|
488
|
+
if (!recovered)
|
|
489
|
+
await page.waitForTimeout(50);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!recovered) {
|
|
493
|
+
throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
|
|
494
|
+
(inBatch
|
|
495
|
+
? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
|
|
496
|
+
: `Ensure a viewport.scrollToRow strategy can restore the row.`));
|
|
497
|
+
}
|
|
498
|
+
cloneTarget = recovered;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Wait for async content renders to settle before cloning.
|
|
502
|
+
if (config.strategies.contentReady) {
|
|
503
|
+
await config.strategies.contentReady(cloneTarget, page);
|
|
504
|
+
// Revalidate: the element may have recycled during the await.
|
|
505
|
+
if (rescanForRow) {
|
|
506
|
+
let postIdx;
|
|
507
|
+
try {
|
|
508
|
+
const r = await resolveRI(cloneTarget);
|
|
509
|
+
postIdx = r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
|
|
510
|
+
}
|
|
511
|
+
catch (_l) {
|
|
512
|
+
postIdx = undefined;
|
|
513
|
+
}
|
|
514
|
+
if (postIdx !== rowIndex) {
|
|
515
|
+
const reFound = await rescanForRow();
|
|
516
|
+
if (reFound)
|
|
517
|
+
cloneTarget = reFound;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
437
521
|
// Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
|
|
438
522
|
// Uses textContent (not innerText) because the clone is detached — innerText
|
|
439
523
|
// falls back to textContent on detached nodes per HTML spec, so be explicit.
|
|
440
|
-
const cloneHandle = await
|
|
524
|
+
const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
|
|
441
525
|
let snapshot;
|
|
442
526
|
try {
|
|
443
527
|
snapshot = await cloneHandle.evaluate((row, sel) => {
|
|
@@ -491,7 +575,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
491
575
|
await materializeRow();
|
|
492
576
|
const result = {};
|
|
493
577
|
for (const [col, idx] of columnsToProcess) {
|
|
494
|
-
const columnOverride = (
|
|
578
|
+
const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
|
|
495
579
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
496
580
|
if (mapper) {
|
|
497
581
|
const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
|
|
@@ -508,7 +592,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
508
592
|
result[col] = snapshot.cells[idx].text;
|
|
509
593
|
}
|
|
510
594
|
}
|
|
511
|
-
for (const [name, def] of Object.entries((
|
|
595
|
+
for (const [name, def] of Object.entries((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})) {
|
|
512
596
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
513
597
|
continue;
|
|
514
598
|
const snapshotRow = Object.create(smart);
|
|
@@ -607,7 +691,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
607
691
|
// Re-pin to the correct logical row before reading this column (#366).
|
|
608
692
|
const stableRow = await resolveStableRow();
|
|
609
693
|
// Check if we have a column override for this column
|
|
610
|
-
const columnOverride = (
|
|
694
|
+
const columnOverride = (_d = config.columnOverrides) === null || _d === void 0 ? void 0 : _d[col];
|
|
611
695
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
612
696
|
// --- Navigation Logic Start ---
|
|
613
697
|
const cell = config.strategies.getCellLocator
|
|
@@ -634,7 +718,8 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
634
718
|
index: idx,
|
|
635
719
|
rowLocator: stableRow,
|
|
636
720
|
rowIndex,
|
|
637
|
-
barrier: smart._barrier
|
|
721
|
+
barrier: smart._barrier,
|
|
722
|
+
allowMissingCell: !!mapper,
|
|
638
723
|
});
|
|
639
724
|
if (navigatedCell) {
|
|
640
725
|
targetCell = navigatedCell;
|
|
@@ -654,15 +739,29 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
654
739
|
});
|
|
655
740
|
}
|
|
656
741
|
// Cell loading wait (if configured)
|
|
657
|
-
const isCellLoading = (
|
|
658
|
-
const rawCellTimeout = (
|
|
742
|
+
const isCellLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isCellLoading;
|
|
743
|
+
const rawCellTimeout = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.cellLoadingTimeout;
|
|
659
744
|
// undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
|
|
660
745
|
const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
|
|
661
746
|
? rawCellTimeout
|
|
662
747
|
: undefined;
|
|
663
|
-
const onCellLoadingTimeout = (
|
|
664
|
-
if (isCellLoading
|
|
665
|
-
|
|
748
|
+
const onCellLoadingTimeout = (_h = (_g = config.strategies.loading) === null || _g === void 0 ? void 0 : _g.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
|
|
749
|
+
if (isCellLoading) {
|
|
750
|
+
let loading = await isCellLoading(targetCell, col, smart);
|
|
751
|
+
// First-paint race: the cell node can attach a tick before the loading
|
|
752
|
+
// indicator mounts. A single false-negative then reads "" and skips
|
|
753
|
+
// onCellLoadingTimeout. Only grace when the cell still looks empty.
|
|
754
|
+
if (!loading && cellLoadingTimeout !== undefined && cellLoadingTimeout > 0) {
|
|
755
|
+
const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
|
|
756
|
+
if (peek === '') {
|
|
757
|
+
const graceDeadline = Date.now() + Math.min(250, cellLoadingTimeout);
|
|
758
|
+
while (!loading && Date.now() < graceDeadline) {
|
|
759
|
+
await page.waitForTimeout(25);
|
|
760
|
+
loading = await isCellLoading(targetCell, col, smart);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (loading && cellLoadingTimeout !== undefined) {
|
|
666
765
|
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
|
|
667
766
|
const deadline = Date.now() + cellLoadingTimeout;
|
|
668
767
|
let cellResolved = !(await isCellLoading(targetCell, col, smart)); // immediate check (handles timeout=0)
|
|
@@ -699,7 +798,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
699
798
|
result[col] = (text || '').trim();
|
|
700
799
|
}
|
|
701
800
|
}
|
|
702
|
-
for (const [name, def] of Object.entries((
|
|
801
|
+
for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
|
|
703
802
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
704
803
|
continue;
|
|
705
804
|
const snapshotRow = Object.create(smart);
|
|
@@ -775,6 +874,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
775
874
|
(0, debugUtils_1.logDebug)(config, 'info', 'Row fill complete');
|
|
776
875
|
};
|
|
777
876
|
smart.bringIntoView = async () => {
|
|
877
|
+
var _a;
|
|
778
878
|
if (rowIndex === undefined) {
|
|
779
879
|
(0, debugUtils_1.logDebug)(config, 'info', 'bringIntoView called on a row with unknown rowIndex (e.g. from getRow()). ' +
|
|
780
880
|
'Page navigation and scrolling will proceed, but virtual-scroll keyboard navigation will be skipped. ' +
|
|
@@ -786,7 +886,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
786
886
|
const primitives = config.strategies.pagination;
|
|
787
887
|
const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
|
|
788
888
|
const getCurrent = () => parentTable.currentPageIndex;
|
|
789
|
-
const setCurrent = (n) => { parentTable
|
|
889
|
+
const setCurrent = (n) => { (0, pageIndex_1.setCurrentPageIndex)(parentTable, n); };
|
|
790
890
|
if (primitives.goToPage) {
|
|
791
891
|
(0, debugUtils_1.logDebug)(config, 'info', `bringIntoView: Navigating to page ${tablePageIndex} (goToPage retry loop)`);
|
|
792
892
|
await (0, paginationPath_1.executeNavigationWithGoToPageRetry)(tablePageIndex, primitives, context, getCurrent, setCurrent);
|
|
@@ -811,7 +911,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
811
911
|
if (!ok)
|
|
812
912
|
throw new Error(`bringIntoView: goNext failed before reaching page ${tablePageIndex}.`);
|
|
813
913
|
}
|
|
814
|
-
parentTable
|
|
914
|
+
(0, pageIndex_1.setCurrentPageIndex)(parentTable, tablePageIndex);
|
|
815
915
|
}
|
|
816
916
|
else {
|
|
817
917
|
(0, debugUtils_1.logDebug)(config, 'error', `Cannot bring row on page ${tablePageIndex} into view. No backwards pagination strategies (goToPage, goPrevious, goPreviousBulk, or goToFirst+goNext) provided.`);
|
|
@@ -821,8 +921,20 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
821
921
|
}
|
|
822
922
|
// Delay after pagination/finding before scrolling
|
|
823
923
|
await (0, debugUtils_1.debugDelay)(config, 'findRow');
|
|
824
|
-
//
|
|
825
|
-
|
|
924
|
+
// Prefer viewport.scrollToRow when configured (#430). scrollIntoViewIfNeeded adjusts
|
|
925
|
+
// both axes and can evict sibling rows on recycling virtualizers.
|
|
926
|
+
const viewport = config.strategies.viewport;
|
|
927
|
+
const logicalRowIndex = (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && renderWindowPosition
|
|
928
|
+
? (_a = (await (0, rowResolution_1.resolveLogicalRowIndex)(rowLocator, config, () => undefined))) === null || _a === void 0 ? void 0 : _a.index
|
|
929
|
+
: rowIndex;
|
|
930
|
+
if ((viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow) && typeof logicalRowIndex === 'number') {
|
|
931
|
+
const page = rootLocator.page();
|
|
932
|
+
(0, debugUtils_1.logDebug)(config, 'verbose', `bringIntoView: viewport.scrollToRow(${logicalRowIndex})`);
|
|
933
|
+
await viewport.scrollToRow({ root: rootLocator, config, page, resolve, getHeaders: parentTable === null || parentTable === void 0 ? void 0 : parentTable.getHeaders }, logicalRowIndex);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
await rowLocator.scrollIntoViewIfNeeded();
|
|
937
|
+
}
|
|
826
938
|
};
|
|
827
939
|
return smart;
|
|
828
940
|
};
|
|
@@ -12,7 +12,9 @@ export interface NavigationPrimitives {
|
|
|
12
12
|
goHome?: (context: StrategyContext) => Promise<void>;
|
|
13
13
|
/**
|
|
14
14
|
* After vertical moves, run before horizontal steps when the target column index is 0.
|
|
15
|
-
* Use for horizontally virtualized
|
|
15
|
+
* Use for horizontally virtualized tables so the first column exists in the DOM again
|
|
16
|
+
* (e.g. set scrollLeft = 0). If the table needs keyboard `Home` or canvas focus, put that
|
|
17
|
+
* here — the core orchestrator does not special-case those behaviors.
|
|
16
18
|
* Do not reset vertical scroll here — RDG-style `goHome` is often unsuitable.
|
|
17
19
|
*/
|
|
18
20
|
snapFirstColumnIntoView?: (context: StrategyContext) => Promise<void>;
|
|
@@ -33,7 +35,8 @@ export interface NavigationPrimitives {
|
|
|
33
35
|
maxWaitMs?: number;
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
|
-
* @deprecated Use NavigationPrimitives instead.
|
|
38
|
+
* @deprecated Use NavigationPrimitives (`strategies.navigation`) instead. Removed from
|
|
39
|
+
* the public `Strategies` namespace in #428; this type remains for transitional typings.
|
|
37
40
|
* Defines the contract for a cell navigation strategy.
|
|
38
41
|
*/
|
|
39
42
|
export type CellNavigationStrategy = (context: StrategyContext & {
|
|
@@ -46,10 +49,3 @@ export type CellNavigationStrategy = (context: StrategyContext & {
|
|
|
46
49
|
locator: Locator;
|
|
47
50
|
} | null;
|
|
48
51
|
}) => Promise<void>;
|
|
49
|
-
export declare const CellNavigationStrategies: {
|
|
50
|
-
/**
|
|
51
|
-
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
52
|
-
* No specific action taken other than what Playwright's default locator handling does.
|
|
53
|
-
*/
|
|
54
|
-
default: () => Promise<void>;
|
|
55
|
-
};
|
|
@@ -1,12 +1,2 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CellNavigationStrategies = void 0;
|
|
4
|
-
exports.CellNavigationStrategies = {
|
|
5
|
-
/**
|
|
6
|
-
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
7
|
-
* No specific action taken other than what Playwright's default locator handling does.
|
|
8
|
-
*/
|
|
9
|
-
default: async () => {
|
|
10
|
-
// No-op
|
|
11
|
-
}
|
|
12
|
-
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ContentReadyStrategy } from '../types';
|
|
2
|
+
export type { ContentReadyStrategy };
|
|
3
|
+
export declare const ContentReadyStrategies: {
|
|
4
|
+
/**
|
|
5
|
+
* Polls the row's text content until two consecutive reads match.
|
|
6
|
+
*/
|
|
7
|
+
textStable: (options?: {
|
|
8
|
+
timeout?: number;
|
|
9
|
+
interval?: number;
|
|
10
|
+
}) => ContentReadyStrategy;
|
|
11
|
+
/**
|
|
12
|
+
* Uses MutationObserver to wait for DOM mutations on the row's subtree
|
|
13
|
+
* to settle. Resolves when no mutations occur for `quietPeriod` ms, or
|
|
14
|
+
* when `timeout` expires.
|
|
15
|
+
*/
|
|
16
|
+
mutationSettled: (options?: {
|
|
17
|
+
timeout?: number;
|
|
18
|
+
quietPeriod?: number;
|
|
19
|
+
}) => ContentReadyStrategy;
|
|
20
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContentReadyStrategies = void 0;
|
|
4
|
+
exports.ContentReadyStrategies = {
|
|
5
|
+
/**
|
|
6
|
+
* Polls the row's text content until two consecutive reads match.
|
|
7
|
+
*/
|
|
8
|
+
textStable: (options = {}) => {
|
|
9
|
+
var _a, _b;
|
|
10
|
+
const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
|
|
11
|
+
const interval = (_b = options.interval) !== null && _b !== void 0 ? _b : 50;
|
|
12
|
+
return async (row, page) => {
|
|
13
|
+
const deadline = Date.now() + timeout;
|
|
14
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
15
|
+
let prev = await row.innerText({ timeout: remaining() || timeout });
|
|
16
|
+
while (remaining() > 0) {
|
|
17
|
+
await page.waitForTimeout(Math.min(interval, remaining()));
|
|
18
|
+
if (remaining() <= 0)
|
|
19
|
+
break;
|
|
20
|
+
const cur = await row.innerText({ timeout: remaining() });
|
|
21
|
+
if (cur === prev)
|
|
22
|
+
return;
|
|
23
|
+
prev = cur;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Uses MutationObserver to wait for DOM mutations on the row's subtree
|
|
29
|
+
* to settle. Resolves when no mutations occur for `quietPeriod` ms, or
|
|
30
|
+
* when `timeout` expires.
|
|
31
|
+
*/
|
|
32
|
+
mutationSettled: (options = {}) => {
|
|
33
|
+
var _a, _b;
|
|
34
|
+
const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
|
|
35
|
+
const quietPeriod = (_b = options.quietPeriod) !== null && _b !== void 0 ? _b : 100;
|
|
36
|
+
return async (row) => {
|
|
37
|
+
await row.evaluate((el, opts) => {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let quietTimer;
|
|
40
|
+
let deadlineTimer;
|
|
41
|
+
const done = () => {
|
|
42
|
+
clearTimeout(quietTimer);
|
|
43
|
+
clearTimeout(deadlineTimer);
|
|
44
|
+
observer.disconnect();
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
const observer = new MutationObserver(() => {
|
|
48
|
+
clearTimeout(quietTimer);
|
|
49
|
+
quietTimer = setTimeout(done, opts.quietPeriod);
|
|
50
|
+
});
|
|
51
|
+
observer.observe(el, {
|
|
52
|
+
childList: true,
|
|
53
|
+
subtree: true,
|
|
54
|
+
characterData: true,
|
|
55
|
+
});
|
|
56
|
+
quietTimer = setTimeout(done, opts.quietPeriod);
|
|
57
|
+
deadlineTimer = setTimeout(done, opts.timeout);
|
|
58
|
+
});
|
|
59
|
+
}, { timeout, quietPeriod });
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -2,12 +2,8 @@ import type { FilterStrategy } from '../types';
|
|
|
2
2
|
/**
|
|
3
3
|
* Example filter strategies.
|
|
4
4
|
* - default: small convenience wrapper that mirrors the engine's default behavior.
|
|
5
|
-
* - spy(factory): returns a strategy that marks `calledRef.called = true` when invoked.
|
|
6
5
|
*/
|
|
7
6
|
export declare const FilterStrategies: {
|
|
8
7
|
default: FilterStrategy;
|
|
9
|
-
spy: (calledRef?: {
|
|
10
|
-
called?: boolean;
|
|
11
|
-
}) => FilterStrategy;
|
|
12
8
|
};
|
|
13
9
|
export type { FilterStrategy } from '../types';
|
|
@@ -4,7 +4,6 @@ exports.FilterStrategies = void 0;
|
|
|
4
4
|
/**
|
|
5
5
|
* Example filter strategies.
|
|
6
6
|
* - default: small convenience wrapper that mirrors the engine's default behavior.
|
|
7
|
-
* - spy(factory): returns a strategy that marks `calledRef.called = true` when invoked.
|
|
8
7
|
*/
|
|
9
8
|
exports.FilterStrategies = {
|
|
10
9
|
default: {
|
|
@@ -20,19 +19,4 @@ exports.FilterStrategies = {
|
|
|
20
19
|
return rows.filter({ has: targetCell.getByText(textVal, { exact: true }) });
|
|
21
20
|
}
|
|
22
21
|
},
|
|
23
|
-
spy: (calledRef = {}) => ({
|
|
24
|
-
apply({ rows, filter, colIndex, tableContext }) {
|
|
25
|
-
calledRef.called = true;
|
|
26
|
-
// Delegate to default behaviour for actual filtering
|
|
27
|
-
const page = tableContext.page;
|
|
28
|
-
const resolve = tableContext.resolve;
|
|
29
|
-
const cellTemplate = resolve(tableContext.config.cellSelector, page);
|
|
30
|
-
const targetCell = cellTemplate.nth(colIndex);
|
|
31
|
-
if (typeof filter.value === 'function') {
|
|
32
|
-
return rows.filter({ has: filter.value(targetCell) });
|
|
33
|
-
}
|
|
34
|
-
const textVal = typeof filter.value === 'number' ? String(filter.value) : filter.value;
|
|
35
|
-
return rows.filter({ has: targetCell.getByText(textVal, { exact: true }) });
|
|
36
|
-
}
|
|
37
|
-
}),
|
|
38
22
|
};
|
|
@@ -13,6 +13,11 @@ export declare const HeaderStrategies: {
|
|
|
13
13
|
/**
|
|
14
14
|
* Physically scrolls the table horizontally to force virtualized columns to mount,
|
|
15
15
|
* collecting their names along the way.
|
|
16
|
+
*
|
|
17
|
+
* **Requires `selector`** — the CSS selector for the horizontal scroll container
|
|
18
|
+
* (e.g. `.dvn-scroller` for Glide, `.rdg-viewport` for RDG). Framework class names
|
|
19
|
+
* belong in presets / your config, not in this generic factory. Without `selector`,
|
|
20
|
+
* only currently visible headers are returned (no scrolling).
|
|
16
21
|
*/
|
|
17
22
|
horizontalScroll: (options?: {
|
|
18
23
|
limit?: number;
|
|
@@ -22,6 +22,11 @@ exports.HeaderStrategies = {
|
|
|
22
22
|
/**
|
|
23
23
|
* Physically scrolls the table horizontally to force virtualized columns to mount,
|
|
24
24
|
* collecting their names along the way.
|
|
25
|
+
*
|
|
26
|
+
* **Requires `selector`** — the CSS selector for the horizontal scroll container
|
|
27
|
+
* (e.g. `.dvn-scroller` for Glide, `.rdg-viewport` for RDG). Framework class names
|
|
28
|
+
* belong in presets / your config, not in this generic factory. Without `selector`,
|
|
29
|
+
* only currently visible headers are returned (no scrolling).
|
|
25
30
|
*/
|
|
26
31
|
horizontalScroll: (options) => {
|
|
27
32
|
return async (context) => {
|
|
@@ -37,19 +42,22 @@ exports.HeaderStrategies = {
|
|
|
37
42
|
};
|
|
38
43
|
let currentHeaders = await getVisible();
|
|
39
44
|
currentHeaders.forEach(h => collectedHeaders.add(h));
|
|
40
|
-
const
|
|
41
|
-
|
|
45
|
+
const selector = options === null || options === void 0 ? void 0 : options.selector;
|
|
46
|
+
if (!selector) {
|
|
47
|
+
(0, debugUtils_1.logDebug)(config, 'info', 'HeaderStrategies.horizontalScroll: no selector provided — returning visible headers only. Pass { selector } for the scroll container (e.g. ".dvn-scroller").');
|
|
48
|
+
return Array.from(collectedHeaders);
|
|
49
|
+
}
|
|
50
|
+
const scrollerHandle = await root.evaluateHandle((el, sel) => {
|
|
51
|
+
if (el.matches(sel))
|
|
42
52
|
return el;
|
|
43
|
-
|
|
44
|
-
const effectiveSelector = selector || '.dvn-scroller, .rdg-viewport, [role="grid"]';
|
|
45
|
-
const ancestor = el.closest(effectiveSelector);
|
|
53
|
+
const ancestor = el.closest(sel);
|
|
46
54
|
if (ancestor)
|
|
47
55
|
return ancestor;
|
|
48
|
-
const child = el.querySelector(
|
|
56
|
+
const child = el.querySelector(sel);
|
|
49
57
|
if (child)
|
|
50
58
|
return child;
|
|
51
|
-
return
|
|
52
|
-
},
|
|
59
|
+
return null;
|
|
60
|
+
}, selector);
|
|
53
61
|
const isScrollerFound = await scrollerHandle.evaluate(el => !!el);
|
|
54
62
|
if (isScrollerFound) {
|
|
55
63
|
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
@@ -71,7 +79,7 @@ exports.HeaderStrategies = {
|
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
else {
|
|
74
|
-
(0, debugUtils_1.logDebug)(config, 'info',
|
|
82
|
+
(0, debugUtils_1.logDebug)(config, 'info', `HeaderStrategies.horizontalScroll: Could not find scroller matching "${selector}". Returning visible headers.`);
|
|
75
83
|
}
|
|
76
84
|
if (isScrollerFound) {
|
|
77
85
|
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|