@rickcedwhat/playwright-smart-table 6.19.0 → 6.20.1-next.19d3cd3
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 +7 -1
- package/dist/engine/rowFinder.js +154 -45
- package/dist/engine/rowResolution.d.ts +11 -4
- package/dist/engine/rowResolution.js +10 -5
- package/dist/engine/tableIteration.d.ts +1 -1
- package/dist/engine/tableIteration.js +38 -15
- package/dist/engine/tableMapper.js +10 -3
- package/dist/index.d.ts +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +6 -3
- package/dist/presets/glide/headers.js +6 -3
- package/dist/presets/mui.js +3 -1
- package/dist/smartRow.js +167 -62
- package/dist/strategies/contentReady.d.ts +13 -0
- package/dist/strategies/contentReady.js +29 -0
- package/dist/strategies/fill.js +0 -1
- package/dist/strategies/headers.js +6 -3
- package/dist/strategies/index.d.ts +7 -0
- package/dist/strategies/index.js +3 -0
- package/dist/strategies/pagination.js +11 -2
- package/dist/strategies/viewport.js +26 -19
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +68 -8
- package/dist/types.d.ts +67 -8
- package/dist/useTable.js +115 -24
- package/dist/utils/elementTracker.js +6 -3
- package/dist/utils/mergeTableConfig.js +4 -0
- package/package.json +6 -1
package/dist/smartRow.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rowResolution_1 = require("./engine/rowResolution");
|
|
3
4
|
const fill_1 = require("./strategies/fill");
|
|
4
5
|
const stringUtils_1 = require("./utils/stringUtils");
|
|
5
6
|
const debugUtils_1 = require("./utils/debugUtils");
|
|
@@ -146,38 +147,6 @@ const _navigateToCell = async (params) => {
|
|
|
146
147
|
if (typeof rowIndex !== 'number') {
|
|
147
148
|
throw new Error('Row index is required for navigation');
|
|
148
149
|
}
|
|
149
|
-
const navigateOnce = async () => {
|
|
150
|
-
// Get current position again to be sure
|
|
151
|
-
let currRow = 0;
|
|
152
|
-
let currCol = 0;
|
|
153
|
-
if (config.strategies.getActiveCell) {
|
|
154
|
-
const ac = await config.strategies.getActiveCell({ config, root: rootLocator, page, resolve });
|
|
155
|
-
if (ac) {
|
|
156
|
-
currRow = ac.rowIndex;
|
|
157
|
-
currCol = ac.columnIndex;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const rDiff = rowIndex - currRow;
|
|
161
|
-
const cDiff = index - currCol;
|
|
162
|
-
// Move one step vertically
|
|
163
|
-
if (rDiff > 0 && nav.goDown) {
|
|
164
|
-
(0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: moving down');
|
|
165
|
-
await nav.goDown(context);
|
|
166
|
-
}
|
|
167
|
-
else if (rDiff < 0 && nav.goUp) {
|
|
168
|
-
(0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: moving up');
|
|
169
|
-
await nav.goUp(context);
|
|
170
|
-
}
|
|
171
|
-
// Move one step horizontally
|
|
172
|
-
if (cDiff > 0 && nav.goRight) {
|
|
173
|
-
(0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: moving right');
|
|
174
|
-
await nav.goRight(context);
|
|
175
|
-
}
|
|
176
|
-
else if (cDiff < 0 && nav.goLeft) {
|
|
177
|
-
(0, debugUtils_1.logDebug)(config, 'verbose', '_navigateToCell: moving left');
|
|
178
|
-
await nav.goLeft(context);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
150
|
if (await targetReached()) {
|
|
182
151
|
// Already there. If we have a barrier, check-in to stay in lock-step.
|
|
183
152
|
if (barrier)
|
|
@@ -311,7 +280,7 @@ const _navigateToCell = async (params) => {
|
|
|
311
280
|
else {
|
|
312
281
|
navResult = await navigateUntilReached();
|
|
313
282
|
}
|
|
314
|
-
// navigateUntilReached may return a Locator it found mid-navigation
|
|
283
|
+
// navigateUntilReached may return a Locator it found mid-navigation
|
|
315
284
|
if (navResult && typeof navResult.count === 'function') {
|
|
316
285
|
const loc = navResult;
|
|
317
286
|
if (await loc.count() > 0)
|
|
@@ -353,9 +322,13 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
353
322
|
smart._barrier = barrier;
|
|
354
323
|
// Attach Methods
|
|
355
324
|
smart.getCell = (colName) => {
|
|
325
|
+
var _a, _b;
|
|
326
|
+
if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[colName]) {
|
|
327
|
+
throw new Error(`Column "${colName}" is synthetic (no DOM cell) — use getValue("${colName}") or toJSON() instead`);
|
|
328
|
+
}
|
|
356
329
|
const idx = map.get(colName);
|
|
357
330
|
if (idx === undefined) {
|
|
358
|
-
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName,
|
|
331
|
+
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
|
|
359
332
|
}
|
|
360
333
|
let baseLocator;
|
|
361
334
|
if (config.strategies.getCellLocator) {
|
|
@@ -413,8 +386,45 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
413
386
|
smart.wasFound = () => {
|
|
414
387
|
return !smart[sentinel_1.SENTINEL_ROW];
|
|
415
388
|
};
|
|
389
|
+
smart.getValue = async (colName) => {
|
|
390
|
+
var _a, _b, _c;
|
|
391
|
+
const syntheticDef = (_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[colName];
|
|
392
|
+
if (syntheticDef) {
|
|
393
|
+
const guardedRow = Object.create(smart);
|
|
394
|
+
guardedRow.getValue = async (innerCol) => {
|
|
395
|
+
var _a;
|
|
396
|
+
if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[innerCol]) {
|
|
397
|
+
throw new Error(`[SmartTable] Synthetic column "${innerCol}" cannot be read from inside another synthetic column's compute(). ` +
|
|
398
|
+
`Synthetic columns may only reference real or override columns.`);
|
|
399
|
+
}
|
|
400
|
+
return smart.getValue(innerCol);
|
|
401
|
+
};
|
|
402
|
+
return String(await syntheticDef.compute(guardedRow));
|
|
403
|
+
}
|
|
404
|
+
const idx = map.get(colName);
|
|
405
|
+
if (idx === undefined) {
|
|
406
|
+
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
|
|
407
|
+
}
|
|
408
|
+
const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[colName];
|
|
409
|
+
const cell = config.strategies.getCellLocator
|
|
410
|
+
? config.strategies.getCellLocator({
|
|
411
|
+
row: rowLocator, root: rootLocator, columnName: colName,
|
|
412
|
+
columnIndex: idx, rowIndex, page: rootLocator.page(), config,
|
|
413
|
+
})
|
|
414
|
+
: resolve(config.cellSelector, rowLocator).nth(idx);
|
|
415
|
+
if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
|
|
416
|
+
const getCell = (name) => {
|
|
417
|
+
const ci = map.get(name);
|
|
418
|
+
if (ci === undefined)
|
|
419
|
+
throw new Error(`Column "${name}" not found`);
|
|
420
|
+
return resolve(config.cellSelector, rowLocator).nth(ci);
|
|
421
|
+
};
|
|
422
|
+
return String(await columnOverride.read(cell, { row: smart, columnName: colName, columnIndex: idx, getCell }));
|
|
423
|
+
}
|
|
424
|
+
return (await cell.innerText()).trim();
|
|
425
|
+
};
|
|
416
426
|
smart.toJSON = async (options) => {
|
|
417
|
-
var _a, _b, _c, _d, _e, _f;
|
|
427
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
418
428
|
// Atomic mode: snapshot the row in a single evaluate, then apply column overrides
|
|
419
429
|
// against a frozen reconstruction. Zero inter-column stagger — even in-place React
|
|
420
430
|
// re-renders can't affect it because overrides read from a detached clone.
|
|
@@ -424,10 +434,84 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
424
434
|
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
425
435
|
}
|
|
426
436
|
const page = rootLocator.page();
|
|
437
|
+
// Re-pin: the row locator is a positional nth-child from the iteration loop.
|
|
438
|
+
// On recycling virtualizers, elements can shift between SmartRow creation and
|
|
439
|
+
// this clone — the nth element may now show a different row's content.
|
|
440
|
+
// Verify via resolveRowIndex and rescan if drifted.
|
|
441
|
+
let cloneTarget = rowLocator;
|
|
442
|
+
const resolveRI = config.strategies.resolveRowIndex;
|
|
443
|
+
const isSelfHealing = smart._selfHealing === true;
|
|
444
|
+
const rescanForRow = resolveRI && typeof rowIndex === 'number' && !isSelfHealing
|
|
445
|
+
? async () => {
|
|
446
|
+
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
447
|
+
for (const r of rows) {
|
|
448
|
+
try {
|
|
449
|
+
const rResult = await resolveRI(r);
|
|
450
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
451
|
+
return r;
|
|
452
|
+
}
|
|
453
|
+
catch ( /* element may have detached during scan */_a) { /* element may have detached during scan */ }
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
: null;
|
|
458
|
+
if (rescanForRow) {
|
|
459
|
+
let drifted = false;
|
|
460
|
+
try {
|
|
461
|
+
const curResult = await resolveRI(rowLocator);
|
|
462
|
+
const curIndex = curResult !== undefined
|
|
463
|
+
? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
|
|
464
|
+
: undefined;
|
|
465
|
+
drifted = curIndex !== rowIndex;
|
|
466
|
+
}
|
|
467
|
+
catch (_k) {
|
|
468
|
+
drifted = true;
|
|
469
|
+
}
|
|
470
|
+
if (drifted) {
|
|
471
|
+
const inBatch = smart._inBatch === true || !!smart._barrier;
|
|
472
|
+
let recovered = await rescanForRow();
|
|
473
|
+
if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
|
|
474
|
+
await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
|
|
475
|
+
const deadline = Date.now() + 500;
|
|
476
|
+
while (!recovered && Date.now() < deadline) {
|
|
477
|
+
recovered = await rescanForRow();
|
|
478
|
+
if (!recovered)
|
|
479
|
+
await page.waitForTimeout(50);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!recovered) {
|
|
483
|
+
throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
|
|
484
|
+
(inBatch
|
|
485
|
+
? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
|
|
486
|
+
: `Ensure a viewport.scrollToRow strategy can restore the row.`));
|
|
487
|
+
}
|
|
488
|
+
cloneTarget = recovered;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Wait for async content renders to settle before cloning.
|
|
492
|
+
if (config.strategies.contentReady) {
|
|
493
|
+
await config.strategies.contentReady(cloneTarget, page);
|
|
494
|
+
// Revalidate: the element may have recycled during the await.
|
|
495
|
+
if (rescanForRow) {
|
|
496
|
+
let postIdx;
|
|
497
|
+
try {
|
|
498
|
+
const r = await resolveRI(cloneTarget);
|
|
499
|
+
postIdx = r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
|
|
500
|
+
}
|
|
501
|
+
catch (_l) {
|
|
502
|
+
postIdx = undefined;
|
|
503
|
+
}
|
|
504
|
+
if (postIdx !== rowIndex) {
|
|
505
|
+
const reFound = await rescanForRow();
|
|
506
|
+
if (reFound)
|
|
507
|
+
cloneTarget = reFound;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
427
511
|
// Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
|
|
428
512
|
// Uses textContent (not innerText) because the clone is detached — innerText
|
|
429
513
|
// falls back to textContent on detached nodes per HTML spec, so be explicit.
|
|
430
|
-
const cloneHandle = await
|
|
514
|
+
const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
|
|
431
515
|
let snapshot;
|
|
432
516
|
try {
|
|
433
517
|
snapshot = await cloneHandle.evaluate((row, sel) => {
|
|
@@ -481,7 +565,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
481
565
|
await materializeRow();
|
|
482
566
|
const result = {};
|
|
483
567
|
for (const [col, idx] of columnsToProcess) {
|
|
484
|
-
const columnOverride = (
|
|
568
|
+
const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
|
|
485
569
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
486
570
|
if (mapper) {
|
|
487
571
|
const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
|
|
@@ -498,6 +582,17 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
498
582
|
result[col] = snapshot.cells[idx].text;
|
|
499
583
|
}
|
|
500
584
|
}
|
|
585
|
+
for (const [name, def] of Object.entries((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})) {
|
|
586
|
+
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
587
|
+
continue;
|
|
588
|
+
const snapshotRow = Object.create(smart);
|
|
589
|
+
snapshotRow.getValue = async (col) => {
|
|
590
|
+
if (col in result)
|
|
591
|
+
return String(result[col]);
|
|
592
|
+
return smart.getValue(col);
|
|
593
|
+
};
|
|
594
|
+
result[name] = String(await def.compute(snapshotRow));
|
|
595
|
+
}
|
|
501
596
|
return result;
|
|
502
597
|
}
|
|
503
598
|
finally {
|
|
@@ -516,43 +611,39 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
516
611
|
throw new Error(`Column "${colName}" not found`);
|
|
517
612
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
518
613
|
};
|
|
519
|
-
// #366: on virtualized tables the row's DOM node can recycle between the
|
|
520
|
-
// awaits below
|
|
521
|
-
//
|
|
522
|
-
// re-
|
|
523
|
-
//
|
|
524
|
-
// rather than silently returning a mixed-row object. No-op without resolveRowIndex/rowIndex.
|
|
614
|
+
// #366 / #392: on virtualized tables the row's DOM node can recycle between the
|
|
615
|
+
// per-column awaits below. When a self-healing locator is in use (the row locator was
|
|
616
|
+
// rebuilt with a CSS selector, e.g. `[data-rowindex="47"]`), the locator automatically
|
|
617
|
+
// re-queries the DOM — no rescan needed. For plain-number strategies (JS-only, no
|
|
618
|
+
// selector), fall back to the original rescan-then-scrollToRow approach.
|
|
525
619
|
const resolveRI = config.strategies.resolveRowIndex;
|
|
526
620
|
const canPin = !!resolveRI && typeof rowIndex === 'number';
|
|
621
|
+
const isSelfHealing = smart._selfHealing === true;
|
|
527
622
|
const stableCtx = { root: rootLocator, config, page, resolve };
|
|
528
|
-
// scrollToRow recovery is only safe for standalone reads (findRow/findRowByIndex/getRow*).
|
|
529
|
-
// Inside a map/forEach/filter or async-iterator batch, the other rows are positional
|
|
530
|
-
// locators from one DOM snapshot (and, in synchronized mode, coordinated by a barrier),
|
|
531
|
-
// so an independent scroll here would fight peers / invalidate their locators. In a batch
|
|
532
|
-
// we recover by rescanning the currently-mounted rows only, and throw if that fails.
|
|
533
623
|
const inBatch = smart._inBatch === true || !!smart._barrier;
|
|
534
|
-
// The last locator we confirmed points at the expected logical row. After a recovery the
|
|
535
|
-
// node the original `rowLocator` referenced stays drifted, so re-checking it every column
|
|
536
|
-
// would re-run the full rescan needlessly; validate the last-known-good locator first and
|
|
537
|
-
// only fall back to a rescan when it too has drifted.
|
|
538
624
|
let lastGood = rowLocator;
|
|
539
625
|
const resolveStableRow = async () => {
|
|
540
626
|
var _a;
|
|
627
|
+
// Self-healing locator: always re-queries the DOM by attribute — no rescan needed.
|
|
628
|
+
if (isSelfHealing)
|
|
629
|
+
return rowLocator;
|
|
541
630
|
if (!canPin)
|
|
542
631
|
return rowLocator;
|
|
543
|
-
|
|
632
|
+
const result = await resolveRI(lastGood);
|
|
633
|
+
const resolvedIndex = result !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(result).index : undefined;
|
|
634
|
+
if (resolvedIndex === rowIndex)
|
|
544
635
|
return lastGood; // still correct
|
|
545
636
|
// Drifted (recycled). Re-locate the row carrying the expected logical index.
|
|
546
637
|
const rescan = async () => {
|
|
547
638
|
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
548
639
|
for (const r of rows) {
|
|
549
|
-
|
|
640
|
+
const rResult = await resolveRI(r);
|
|
641
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
550
642
|
return r;
|
|
551
643
|
}
|
|
552
644
|
return null;
|
|
553
645
|
};
|
|
554
646
|
let recovered = await rescan();
|
|
555
|
-
// Scroll the row back only when it's safe to move the viewport (not mid-batch).
|
|
556
647
|
if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
|
|
557
648
|
await config.strategies.viewport.scrollToRow(stableCtx, rowIndex);
|
|
558
649
|
recovered = await rescan();
|
|
@@ -590,7 +681,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
590
681
|
// Re-pin to the correct logical row before reading this column (#366).
|
|
591
682
|
const stableRow = await resolveStableRow();
|
|
592
683
|
// Check if we have a column override for this column
|
|
593
|
-
const columnOverride = (
|
|
684
|
+
const columnOverride = (_d = config.columnOverrides) === null || _d === void 0 ? void 0 : _d[col];
|
|
594
685
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
595
686
|
// --- Navigation Logic Start ---
|
|
596
687
|
const cell = config.strategies.getCellLocator
|
|
@@ -637,13 +728,13 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
637
728
|
});
|
|
638
729
|
}
|
|
639
730
|
// Cell loading wait (if configured)
|
|
640
|
-
const isCellLoading = (
|
|
641
|
-
const rawCellTimeout = (
|
|
731
|
+
const isCellLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isCellLoading;
|
|
732
|
+
const rawCellTimeout = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.cellLoadingTimeout;
|
|
642
733
|
// undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
|
|
643
734
|
const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
|
|
644
735
|
? rawCellTimeout
|
|
645
736
|
: undefined;
|
|
646
|
-
const onCellLoadingTimeout = (
|
|
737
|
+
const onCellLoadingTimeout = (_h = (_g = config.strategies.loading) === null || _g === void 0 ? void 0 : _g.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
|
|
647
738
|
if (isCellLoading && await isCellLoading(targetCell, col, smart)) {
|
|
648
739
|
if (cellLoadingTimeout !== undefined) {
|
|
649
740
|
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
|
|
@@ -682,17 +773,31 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
682
773
|
result[col] = (text || '').trim();
|
|
683
774
|
}
|
|
684
775
|
}
|
|
776
|
+
for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
|
|
777
|
+
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
778
|
+
continue;
|
|
779
|
+
const snapshotRow = Object.create(smart);
|
|
780
|
+
snapshotRow.getValue = async (col) => {
|
|
781
|
+
if (col in result)
|
|
782
|
+
return String(result[col]);
|
|
783
|
+
return smart.getValue(col);
|
|
784
|
+
};
|
|
785
|
+
result[name] = String(await def.compute(snapshotRow));
|
|
786
|
+
}
|
|
685
787
|
return result;
|
|
686
788
|
};
|
|
687
789
|
smart.smartFill = async (data, fillOptions) => {
|
|
688
|
-
var _a;
|
|
790
|
+
var _a, _b, _c;
|
|
689
791
|
(0, debugUtils_1.logDebug)(config, 'info', 'Filling row', data);
|
|
690
792
|
for (const [colName, value] of Object.entries(data)) {
|
|
691
793
|
if (value === undefined)
|
|
692
794
|
continue;
|
|
795
|
+
if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[colName]) {
|
|
796
|
+
throw new Error(`Cannot fill synthetic column "${colName}" — it has no DOM cell`);
|
|
797
|
+
}
|
|
693
798
|
const colIdx = map.get(colName);
|
|
694
799
|
if (colIdx === undefined) {
|
|
695
|
-
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName,
|
|
800
|
+
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
|
|
696
801
|
}
|
|
697
802
|
await _navigateToCell({
|
|
698
803
|
config,
|
|
@@ -706,7 +811,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
706
811
|
rowIndex,
|
|
707
812
|
barrier: smart._barrier
|
|
708
813
|
});
|
|
709
|
-
const columnOverride = (
|
|
814
|
+
const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[colName];
|
|
710
815
|
if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.write) {
|
|
711
816
|
const cellLocator = smart.getCell(colName);
|
|
712
817
|
let currentValue;
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
* Handles recycling virtualizers where position updates synchronously
|
|
7
|
+
* but React renders cell content asynchronously.
|
|
8
|
+
*/
|
|
9
|
+
textStable: (options?: {
|
|
10
|
+
timeout?: number;
|
|
11
|
+
interval?: number;
|
|
12
|
+
}) => ContentReadyStrategy;
|
|
13
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
* Handles recycling virtualizers where position updates synchronously
|
|
8
|
+
* but React renders cell content asynchronously.
|
|
9
|
+
*/
|
|
10
|
+
textStable: (options = {}) => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
|
|
13
|
+
const interval = (_b = options.interval) !== null && _b !== void 0 ? _b : 50;
|
|
14
|
+
return async (row, page) => {
|
|
15
|
+
const deadline = Date.now() + timeout;
|
|
16
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
17
|
+
let prev = await row.innerText({ timeout: remaining() || timeout });
|
|
18
|
+
while (remaining() > 0) {
|
|
19
|
+
await page.waitForTimeout(Math.min(interval, remaining()));
|
|
20
|
+
if (remaining() <= 0)
|
|
21
|
+
break;
|
|
22
|
+
const cur = await row.innerText({ timeout: remaining() });
|
|
23
|
+
if (cur === prev)
|
|
24
|
+
return;
|
|
25
|
+
prev = cur;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
package/dist/strategies/fill.js
CHANGED
|
@@ -73,7 +73,6 @@ exports.FillStrategies = {
|
|
|
73
73
|
const inputTag = await inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
74
74
|
const inputType = await inputLocator.getAttribute('type').catch(() => null);
|
|
75
75
|
const isContentEditable = await inputLocator.getAttribute('contenteditable').catch(() => null);
|
|
76
|
-
// console.log(`[SmartTable] Filling "${columnName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
|
|
77
76
|
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
78
77
|
// Boolean value for checkbox/radio
|
|
79
78
|
const shouldBeChecked = Boolean(value);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HeaderStrategies = void 0;
|
|
4
|
+
const debugUtils_1 = require("../utils/debugUtils");
|
|
4
5
|
exports.HeaderStrategies = {
|
|
5
6
|
/**
|
|
6
7
|
* Default strategy: Returns only the headers currently visible in the DOM.
|
|
@@ -70,10 +71,12 @@ exports.HeaderStrategies = {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
else {
|
|
73
|
-
|
|
74
|
+
(0, debugUtils_1.logDebug)(config, 'info', "HeaderStrategies.horizontalScroll: Could not find scroller. Returning visible headers.");
|
|
75
|
+
}
|
|
76
|
+
if (isScrollerFound) {
|
|
77
|
+
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
78
|
+
await page.waitForTimeout(200);
|
|
74
79
|
}
|
|
75
|
-
await scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
76
|
-
await page.waitForTimeout(200);
|
|
77
80
|
return Array.from(collectedHeaders);
|
|
78
81
|
};
|
|
79
82
|
}
|
|
@@ -9,6 +9,7 @@ export * from './loading';
|
|
|
9
9
|
export * from './stabilization';
|
|
10
10
|
export * from './filter';
|
|
11
11
|
export * from './viewport';
|
|
12
|
+
export * from './contentReady';
|
|
12
13
|
export declare const Strategies: {
|
|
13
14
|
Pagination: {
|
|
14
15
|
click: (selectors: {
|
|
@@ -100,4 +101,10 @@ export declare const Strategies: {
|
|
|
100
101
|
Viewport: {
|
|
101
102
|
dataAttribute: (options?: import("./viewport").DataAttributeViewportOptions) => import("../types").ViewportStrategy;
|
|
102
103
|
};
|
|
104
|
+
ContentReady: {
|
|
105
|
+
textStable: (options?: {
|
|
106
|
+
timeout?: number;
|
|
107
|
+
interval?: number;
|
|
108
|
+
}) => import("./contentReady").ContentReadyStrategy;
|
|
109
|
+
};
|
|
103
110
|
};
|
package/dist/strategies/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const loading_1 = require("./loading");
|
|
|
26
26
|
const stabilization_1 = require("./stabilization");
|
|
27
27
|
const filter_1 = require("./filter");
|
|
28
28
|
const viewport_1 = require("./viewport");
|
|
29
|
+
const contentReady_1 = require("./contentReady");
|
|
29
30
|
__exportStar(require("./pagination"), exports);
|
|
30
31
|
__exportStar(require("./sorting"), exports);
|
|
31
32
|
__exportStar(require("./columns"), exports);
|
|
@@ -37,6 +38,7 @@ __exportStar(require("./loading"), exports);
|
|
|
37
38
|
__exportStar(require("./stabilization"), exports);
|
|
38
39
|
__exportStar(require("./filter"), exports);
|
|
39
40
|
__exportStar(require("./viewport"), exports);
|
|
41
|
+
__exportStar(require("./contentReady"), exports);
|
|
40
42
|
exports.Strategies = {
|
|
41
43
|
Pagination: pagination_1.PaginationStrategies,
|
|
42
44
|
Sorting: sorting_1.SortingStrategies,
|
|
@@ -49,4 +51,5 @@ exports.Strategies = {
|
|
|
49
51
|
Stabilization: stabilization_1.StabilizationStrategies,
|
|
50
52
|
Filter: filter_1.FilterStrategies,
|
|
51
53
|
Viewport: viewport_1.ViewportStrategies,
|
|
54
|
+
ContentReady: contentReady_1.ContentReadyStrategies,
|
|
52
55
|
};
|
|
@@ -83,6 +83,7 @@ exports.PaginationStrategies = {
|
|
|
83
83
|
const scrollTarget = options.scrollTarget
|
|
84
84
|
? resolve(options.scrollTarget, root)
|
|
85
85
|
: root;
|
|
86
|
+
const beforeScrollTop = await scrollTarget.evaluate((el) => el.scrollTop);
|
|
86
87
|
const doScroll = async () => {
|
|
87
88
|
const box = await scrollTarget.boundingBox();
|
|
88
89
|
const scrollValue = amount * directionMultiplier;
|
|
@@ -98,8 +99,16 @@ exports.PaginationStrategies = {
|
|
|
98
99
|
await page.mouse.wheel(0, scrollValue);
|
|
99
100
|
}
|
|
100
101
|
};
|
|
101
|
-
// Stabilization: Wait
|
|
102
|
-
|
|
102
|
+
// Stabilization: Wait for content to settle
|
|
103
|
+
const stabilizationResult = await stabilization(context, doScroll);
|
|
104
|
+
// EOF detection: use scroll position, not stabilization result.
|
|
105
|
+
// Stabilization may time out for recycling virtualizers when the scroll
|
|
106
|
+
// amount is small enough to stay within the overscan window (no new rows
|
|
107
|
+
// rendered), but the scroll position still changed — we haven't reached
|
|
108
|
+
// the end of the data.
|
|
109
|
+
const afterScrollTop = await scrollTarget.evaluate((el) => el.scrollTop);
|
|
110
|
+
const scrollMoved = Math.abs(afterScrollTop - beforeScrollTop) >= 1;
|
|
111
|
+
return scrollMoved || stabilizationResult;
|
|
103
112
|
};
|
|
104
113
|
};
|
|
105
114
|
const createGoToFirst = () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ViewportStrategies = void 0;
|
|
4
|
+
const debugUtils_1 = require("../utils/debugUtils");
|
|
4
5
|
/**
|
|
5
6
|
* Viewport strategies for tables where row and cell elements carry a DOM attribute
|
|
6
7
|
* equal to their index (e.g. Braintrust logs table, TanStack-style grids).
|
|
@@ -11,6 +12,10 @@ exports.ViewportStrategies = void 0;
|
|
|
11
12
|
* - scrollToColumn — aligns header's left edge to container left, waits for cell mount
|
|
12
13
|
* - scrollToRow — scrolls row into view, waits for row mount
|
|
13
14
|
*
|
|
15
|
+
* **Important:** `rowAttribute` must be present on the elements matched by `rowSelector`,
|
|
16
|
+
* not on child cells. If row elements lack the attribute, `getVisibleRowRange` returns
|
|
17
|
+
* `{first:0, last:0}` and logs a warning at the `error` log level.
|
|
18
|
+
*
|
|
14
19
|
* @example
|
|
15
20
|
* // TanStack / Braintrust (data-index, 0-based — default)
|
|
16
21
|
* viewport: Strategies.Viewport.dataAttribute()
|
|
@@ -34,17 +39,22 @@ const dataAttribute = (options) => {
|
|
|
34
39
|
getVisibleColumnRange: async ({ root, config }) => {
|
|
35
40
|
const rowSel = config.rowSelector;
|
|
36
41
|
const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : `[${colAttr}]`;
|
|
37
|
-
|
|
42
|
+
const result = await root.evaluate((el, { rowSel, cellSel, colAttr, colOffset }) => {
|
|
38
43
|
const firstRow = el.querySelector(rowSel);
|
|
39
44
|
if (!firstRow)
|
|
40
|
-
return { first: 0, last: 0 };
|
|
41
|
-
const
|
|
45
|
+
return { first: 0, last: 0, _cellCount: 0, _validCount: 0 };
|
|
46
|
+
const cells = Array.from(firstRow.querySelectorAll(cellSel));
|
|
47
|
+
const indices = cells
|
|
42
48
|
.map(c => Number(c.getAttribute(colAttr)) - colOffset)
|
|
43
49
|
.filter(n => !isNaN(n));
|
|
44
50
|
if (!indices.length)
|
|
45
|
-
return { first: 0, last: 0 };
|
|
46
|
-
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
51
|
+
return { first: 0, last: 0, _cellCount: cells.length, _validCount: 0 };
|
|
52
|
+
return { first: Math.min(...indices), last: Math.max(...indices), _cellCount: cells.length, _validCount: indices.length };
|
|
47
53
|
}, { rowSel, cellSel, colAttr, colOffset });
|
|
54
|
+
if (result._cellCount > 0 && result._validCount === 0) {
|
|
55
|
+
(0, debugUtils_1.logDebug)(config, 'error', `dataAttribute viewport: ${result._cellCount} cell(s) found but none have attribute "${colAttr}". Check that columnAttribute targets cell elements matched by cellSelector.`);
|
|
56
|
+
}
|
|
57
|
+
return { first: result.first, last: result.last };
|
|
48
58
|
},
|
|
49
59
|
getVisibleRowIndices: async ({ root, config }) => {
|
|
50
60
|
const rowSel = config.rowSelector;
|
|
@@ -72,33 +82,30 @@ const dataAttribute = (options) => {
|
|
|
72
82
|
},
|
|
73
83
|
getVisibleRowRange: async ({ root, config }) => {
|
|
74
84
|
const rowSel = config.rowSelector;
|
|
75
|
-
|
|
85
|
+
const result = await root.evaluate((el, { rowSel, rowAttr, rowOffset, containerSel }) => {
|
|
76
86
|
const rows = Array.from(el.querySelectorAll(rowSel));
|
|
77
|
-
// #353: report only rows actually within the scroll container's vertical
|
|
78
|
-
// bounds, not every mounted row. Previously overscan rows (kept mounted by
|
|
79
|
-
// the virtual scroller above/below the fold) were counted as "visible".
|
|
80
|
-
// Inclusive: a row with ANY vertical overlap counts as visible; only rows
|
|
81
|
-
// entirely above or below the container are dropped — so we never drop a
|
|
82
|
-
// partially-visible real row.
|
|
83
87
|
const container = el.closest(containerSel);
|
|
84
88
|
const containerRect = container ? container.getBoundingClientRect() : null;
|
|
85
|
-
const
|
|
89
|
+
const visibleRows = rows
|
|
86
90
|
.filter(r => {
|
|
87
|
-
// Container not resolvable — can't measure, so keep all rows (safe
|
|
88
|
-
// fallback, preserves pre-#353 behavior rather than risk dropping real rows).
|
|
89
91
|
if (!containerRect)
|
|
90
92
|
return true;
|
|
91
93
|
const rect = r.getBoundingClientRect();
|
|
92
94
|
if (rect.height === 0)
|
|
93
|
-
return false;
|
|
95
|
+
return false;
|
|
94
96
|
return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
|
|
95
|
-
})
|
|
97
|
+
});
|
|
98
|
+
const indices = visibleRows
|
|
96
99
|
.map(r => Number(r.getAttribute(rowAttr)) - rowOffset)
|
|
97
100
|
.filter(n => !isNaN(n));
|
|
98
101
|
if (!indices.length)
|
|
99
|
-
return { first: 0, last: 0 };
|
|
100
|
-
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
102
|
+
return { first: 0, last: 0, _rowCount: visibleRows.length, _validCount: 0 };
|
|
103
|
+
return { first: Math.min(...indices), last: Math.max(...indices), _rowCount: visibleRows.length, _validCount: indices.length };
|
|
101
104
|
}, { rowSel, rowAttr, rowOffset, containerSel });
|
|
105
|
+
if (result._rowCount > 0 && result._validCount === 0) {
|
|
106
|
+
(0, debugUtils_1.logDebug)(config, 'error', `dataAttribute viewport: ${result._rowCount} row(s) found but none have attribute "${rowAttr}". Check that rowAttribute targets row elements matched by rowSelector, not child cells. getVisibleRowRange returning {first:0, last:0}.`);
|
|
107
|
+
}
|
|
108
|
+
return { first: result.first, last: result.last };
|
|
102
109
|
},
|
|
103
110
|
scrollToColumn: async ({ root, config }, colIndex) => {
|
|
104
111
|
const headerSel = typeof config.headerSelector === 'string' ? config.headerSelector : null;
|