@rickcedwhat/playwright-smart-table 6.18.0 → 6.20.0
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 +16 -1
- package/dist/engine/rowFinder.js +156 -83
- package/dist/engine/rowResolution.d.ts +35 -0
- package/dist/engine/rowResolution.js +78 -0
- package/dist/engine/tableIteration.d.ts +4 -2
- package/dist/engine/tableIteration.js +72 -57
- 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 +9 -3
- package/dist/presets/glide/headers.js +6 -3
- package/dist/presets/mui.js +28 -1
- package/dist/smartRow.js +240 -48
- package/dist/strategies/fill.js +5 -2
- package/dist/strategies/headers.js +6 -3
- package/dist/strategies/viewport.js +46 -3
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +146 -16
- package/dist/types.d.ts +140 -14
- package/dist/useTable.js +202 -26
- package/dist/utils/mergeTableConfig.js +4 -0
- package/package.json +1 -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,145 @@ 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;
|
|
427
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
428
|
+
// Atomic mode: snapshot the row in a single evaluate, then apply column overrides
|
|
429
|
+
// against a frozen reconstruction. Zero inter-column stagger — even in-place React
|
|
430
|
+
// re-renders can't affect it because overrides read from a detached clone.
|
|
431
|
+
if (options === null || options === void 0 ? void 0 : options.atomic) {
|
|
432
|
+
const cellSel = config.cellSelector;
|
|
433
|
+
if (typeof cellSel !== 'string') {
|
|
434
|
+
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
435
|
+
}
|
|
436
|
+
const page = rootLocator.page();
|
|
437
|
+
// Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
|
|
438
|
+
// Uses textContent (not innerText) because the clone is detached — innerText
|
|
439
|
+
// falls back to textContent on detached nodes per HTML spec, so be explicit.
|
|
440
|
+
const cloneHandle = await rowLocator.evaluateHandle(el => el.cloneNode(true));
|
|
441
|
+
let snapshot;
|
|
442
|
+
try {
|
|
443
|
+
snapshot = await cloneHandle.evaluate((row, sel) => {
|
|
444
|
+
const cells = row.querySelectorAll(sel);
|
|
445
|
+
return {
|
|
446
|
+
rowHTML: row.outerHTML,
|
|
447
|
+
cells: [...cells].map(c => ({
|
|
448
|
+
text: (c.textContent || '').trim(),
|
|
449
|
+
})),
|
|
450
|
+
};
|
|
451
|
+
}, cellSel);
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
await cloneHandle.dispose();
|
|
455
|
+
}
|
|
456
|
+
// Determine which columns have overrides (need lazy materialization)
|
|
457
|
+
const columnsToProcess = [...map.entries()]
|
|
458
|
+
.filter(([col]) => !(options === null || options === void 0 ? void 0 : options.columns) || options.columns.includes(col));
|
|
459
|
+
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); });
|
|
460
|
+
// Step 2: lazily materialize the row if any override needs a Locator.
|
|
461
|
+
// Uses position:fixed off-screen (NOT display:none — that breaks innerText).
|
|
462
|
+
const snapId = `__st_snap_${Math.random().toString(36).slice(2, 10)}`;
|
|
463
|
+
let materialized = false;
|
|
464
|
+
const materializeRow = async () => {
|
|
465
|
+
if (materialized)
|
|
466
|
+
return;
|
|
467
|
+
await page.evaluate(([html, id]) => {
|
|
468
|
+
const c = document.createElement('div');
|
|
469
|
+
c.id = id;
|
|
470
|
+
c.style.cssText = 'position:fixed;left:-99999px;top:-99999px;opacity:0;pointer-events:none';
|
|
471
|
+
// Wrap in <table><tbody> so the browser's HTML parser keeps <tr>/<td> intact.
|
|
472
|
+
c.innerHTML = `<table><tbody>${html}</tbody></table>`;
|
|
473
|
+
document.body.appendChild(c);
|
|
474
|
+
}, [snapshot.rowHTML, snapId]);
|
|
475
|
+
materialized = true;
|
|
476
|
+
};
|
|
477
|
+
const cleanupSnapshot = async () => {
|
|
478
|
+
if (!materialized)
|
|
479
|
+
return;
|
|
480
|
+
await page.evaluate(id => { var _a; return (_a = document.getElementById(id)) === null || _a === void 0 ? void 0 : _a.remove(); }, snapId);
|
|
481
|
+
};
|
|
482
|
+
// Step 3: build getCell helper — returns Locator to cell in materialized row
|
|
483
|
+
const getCell = (colName) => {
|
|
484
|
+
const colIdx = map.get(colName);
|
|
485
|
+
if (colIdx === undefined)
|
|
486
|
+
throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
|
|
487
|
+
return page.locator(`#${snapId}`).locator(cellSel).nth(colIdx);
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
if (hasAnyOverride)
|
|
491
|
+
await materializeRow();
|
|
492
|
+
const result = {};
|
|
493
|
+
for (const [col, idx] of columnsToProcess) {
|
|
494
|
+
const columnOverride = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col];
|
|
495
|
+
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
496
|
+
if (mapper) {
|
|
497
|
+
const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
|
|
498
|
+
try {
|
|
499
|
+
result[col] = await mapper(cell, { row: smart, columnName: col, columnIndex: idx, getCell });
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
const wrapped = new Error(`[SmartTable] toJSON({ atomic: true }): column override for "${col}" failed — ${err.message}`);
|
|
503
|
+
wrapped.cause = err;
|
|
504
|
+
throw wrapped;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else if (idx < snapshot.cells.length) {
|
|
508
|
+
result[col] = snapshot.cells[idx].text;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
for (const [name, def] of Object.entries((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})) {
|
|
512
|
+
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
513
|
+
continue;
|
|
514
|
+
const snapshotRow = Object.create(smart);
|
|
515
|
+
snapshotRow.getValue = async (col) => {
|
|
516
|
+
if (col in result)
|
|
517
|
+
return String(result[col]);
|
|
518
|
+
return smart.getValue(col);
|
|
519
|
+
};
|
|
520
|
+
result[name] = String(await def.compute(snapshotRow));
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
await cleanupSnapshot();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
418
528
|
const result = {};
|
|
419
529
|
const page = rootLocator.page();
|
|
420
530
|
// Build a getHeaderCell helper for the beforeCellRead context.
|
|
@@ -427,17 +537,82 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
427
537
|
throw new Error(`Column "${colName}" not found`);
|
|
428
538
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
429
539
|
};
|
|
540
|
+
// #366 / #392: on virtualized tables the row's DOM node can recycle between the
|
|
541
|
+
// per-column awaits below. When a self-healing locator is in use (the row locator was
|
|
542
|
+
// rebuilt with a CSS selector, e.g. `[data-rowindex="47"]`), the locator automatically
|
|
543
|
+
// re-queries the DOM — no rescan needed. For plain-number strategies (JS-only, no
|
|
544
|
+
// selector), fall back to the original rescan-then-scrollToRow approach.
|
|
545
|
+
const resolveRI = config.strategies.resolveRowIndex;
|
|
546
|
+
const canPin = !!resolveRI && typeof rowIndex === 'number';
|
|
547
|
+
const isSelfHealing = smart._selfHealing === true;
|
|
548
|
+
const stableCtx = { root: rootLocator, config, page, resolve };
|
|
549
|
+
const inBatch = smart._inBatch === true || !!smart._barrier;
|
|
550
|
+
let lastGood = rowLocator;
|
|
551
|
+
const resolveStableRow = async () => {
|
|
552
|
+
var _a;
|
|
553
|
+
// Self-healing locator: always re-queries the DOM by attribute — no rescan needed.
|
|
554
|
+
if (isSelfHealing)
|
|
555
|
+
return rowLocator;
|
|
556
|
+
if (!canPin)
|
|
557
|
+
return rowLocator;
|
|
558
|
+
const result = await resolveRI(lastGood);
|
|
559
|
+
const resolvedIndex = result !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(result).index : undefined;
|
|
560
|
+
if (resolvedIndex === rowIndex)
|
|
561
|
+
return lastGood; // still correct
|
|
562
|
+
// Drifted (recycled). Re-locate the row carrying the expected logical index.
|
|
563
|
+
const rescan = async () => {
|
|
564
|
+
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
565
|
+
for (const r of rows) {
|
|
566
|
+
const rResult = await resolveRI(r);
|
|
567
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
568
|
+
return r;
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
};
|
|
572
|
+
let recovered = await rescan();
|
|
573
|
+
if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
|
|
574
|
+
await config.strategies.viewport.scrollToRow(stableCtx, rowIndex);
|
|
575
|
+
recovered = await rescan();
|
|
576
|
+
}
|
|
577
|
+
if (!recovered) {
|
|
578
|
+
throw new Error(`[SmartTable] toJSON: row ${rowIndex} recycled out of the DOM mid-read and could not be recovered — ` +
|
|
579
|
+
`the returned object would mix fields from different rows. ` +
|
|
580
|
+
(inBatch
|
|
581
|
+
? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows; the row must stay mounted for the read.)`
|
|
582
|
+
: `Ensure a viewport.scrollToRow can restore the row.`));
|
|
583
|
+
}
|
|
584
|
+
lastGood = recovered;
|
|
585
|
+
return recovered;
|
|
586
|
+
};
|
|
587
|
+
// getCell for non-atomic mode: returns a live cell Locator via the column map.
|
|
588
|
+
// Uses the last-known-good row from the re-pin so it's as fresh as possible,
|
|
589
|
+
// but returns raw cell Locators, not override-processed values.
|
|
590
|
+
const getCellLive = (colName) => {
|
|
591
|
+
const colIdx = map.get(colName);
|
|
592
|
+
if (colIdx === undefined)
|
|
593
|
+
throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
|
|
594
|
+
const row = lastGood;
|
|
595
|
+
if (config.strategies.getCellLocator) {
|
|
596
|
+
return config.strategies.getCellLocator({
|
|
597
|
+
row, root: rootLocator, columnName: colName,
|
|
598
|
+
columnIndex: colIdx, rowIndex, page, config,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return resolve(config.cellSelector, row).nth(colIdx);
|
|
602
|
+
};
|
|
430
603
|
for (const [col, idx] of map.entries()) {
|
|
431
604
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(col)) {
|
|
432
605
|
continue;
|
|
433
606
|
}
|
|
607
|
+
// Re-pin to the correct logical row before reading this column (#366).
|
|
608
|
+
const stableRow = await resolveStableRow();
|
|
434
609
|
// Check if we have a column override for this column
|
|
435
|
-
const columnOverride = (
|
|
610
|
+
const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[col];
|
|
436
611
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
437
612
|
// --- Navigation Logic Start ---
|
|
438
613
|
const cell = config.strategies.getCellLocator
|
|
439
614
|
? config.strategies.getCellLocator({
|
|
440
|
-
row:
|
|
615
|
+
row: stableRow,
|
|
441
616
|
root: rootLocator,
|
|
442
617
|
columnName: col,
|
|
443
618
|
columnIndex: idx,
|
|
@@ -445,7 +620,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
445
620
|
page: page,
|
|
446
621
|
config
|
|
447
622
|
})
|
|
448
|
-
: resolve(config.cellSelector,
|
|
623
|
+
: resolve(config.cellSelector, stableRow).nth(idx);
|
|
449
624
|
let targetCell = cell;
|
|
450
625
|
// Always call _navigateToCell to ensure Lock-Step synchronization
|
|
451
626
|
// even if the cell is already present (it handles check-in inside)
|
|
@@ -457,7 +632,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
457
632
|
getHeaders: table === null || table === void 0 ? void 0 : table.getHeaders,
|
|
458
633
|
column: col,
|
|
459
634
|
index: idx,
|
|
460
|
-
rowLocator,
|
|
635
|
+
rowLocator: stableRow,
|
|
461
636
|
rowIndex,
|
|
462
637
|
barrier: smart._barrier
|
|
463
638
|
});
|
|
@@ -472,20 +647,20 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
472
647
|
cell: targetCell,
|
|
473
648
|
columnName: col,
|
|
474
649
|
columnIndex: idx,
|
|
475
|
-
row:
|
|
650
|
+
row: stableRow,
|
|
476
651
|
page,
|
|
477
652
|
root: rootLocator,
|
|
478
653
|
getHeaderCell,
|
|
479
654
|
});
|
|
480
655
|
}
|
|
481
656
|
// Cell loading wait (if configured)
|
|
482
|
-
const isCellLoading = (
|
|
483
|
-
const rawCellTimeout = (
|
|
657
|
+
const isCellLoading = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.isCellLoading;
|
|
658
|
+
const rawCellTimeout = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.cellLoadingTimeout;
|
|
484
659
|
// undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
|
|
485
660
|
const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
|
|
486
661
|
? rawCellTimeout
|
|
487
662
|
: undefined;
|
|
488
|
-
const onCellLoadingTimeout = (
|
|
663
|
+
const onCellLoadingTimeout = (_g = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.onCellLoadingTimeout) !== null && _g !== void 0 ? _g : 'read-as-is';
|
|
489
664
|
if (isCellLoading && await isCellLoading(targetCell, col, smart)) {
|
|
490
665
|
if (cellLoadingTimeout !== undefined) {
|
|
491
666
|
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
|
|
@@ -515,7 +690,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
515
690
|
}
|
|
516
691
|
if (mapper) {
|
|
517
692
|
// Apply mapper
|
|
518
|
-
const mappedValue = await mapper(targetCell);
|
|
693
|
+
const mappedValue = await mapper(targetCell, { row: smart, columnName: col, columnIndex: idx, getCell: getCellLive });
|
|
519
694
|
result[col] = mappedValue;
|
|
520
695
|
}
|
|
521
696
|
else {
|
|
@@ -524,17 +699,31 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
524
699
|
result[col] = (text || '').trim();
|
|
525
700
|
}
|
|
526
701
|
}
|
|
702
|
+
for (const [name, def] of Object.entries((_h = config.syntheticColumns) !== null && _h !== void 0 ? _h : {})) {
|
|
703
|
+
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
704
|
+
continue;
|
|
705
|
+
const snapshotRow = Object.create(smart);
|
|
706
|
+
snapshotRow.getValue = async (col) => {
|
|
707
|
+
if (col in result)
|
|
708
|
+
return String(result[col]);
|
|
709
|
+
return smart.getValue(col);
|
|
710
|
+
};
|
|
711
|
+
result[name] = String(await def.compute(snapshotRow));
|
|
712
|
+
}
|
|
527
713
|
return result;
|
|
528
714
|
};
|
|
529
715
|
smart.smartFill = async (data, fillOptions) => {
|
|
530
|
-
var _a;
|
|
716
|
+
var _a, _b, _c;
|
|
531
717
|
(0, debugUtils_1.logDebug)(config, 'info', 'Filling row', data);
|
|
532
718
|
for (const [colName, value] of Object.entries(data)) {
|
|
533
719
|
if (value === undefined)
|
|
534
720
|
continue;
|
|
721
|
+
if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[colName]) {
|
|
722
|
+
throw new Error(`Cannot fill synthetic column "${colName}" — it has no DOM cell`);
|
|
723
|
+
}
|
|
535
724
|
const colIdx = map.get(colName);
|
|
536
725
|
if (colIdx === undefined) {
|
|
537
|
-
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName,
|
|
726
|
+
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, [...map.keys(), ...Object.keys((_b = config.syntheticColumns) !== null && _b !== void 0 ? _b : {})]));
|
|
538
727
|
}
|
|
539
728
|
await _navigateToCell({
|
|
540
729
|
config,
|
|
@@ -548,12 +737,15 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
548
737
|
rowIndex,
|
|
549
738
|
barrier: smart._barrier
|
|
550
739
|
});
|
|
551
|
-
const columnOverride = (
|
|
740
|
+
const columnOverride = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[colName];
|
|
552
741
|
if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.write) {
|
|
553
742
|
const cellLocator = smart.getCell(colName);
|
|
554
743
|
let currentValue;
|
|
555
744
|
if (columnOverride.read) {
|
|
556
|
-
currentValue = await columnOverride.read(cellLocator
|
|
745
|
+
currentValue = await columnOverride.read(cellLocator, {
|
|
746
|
+
row: smart, columnName: colName, columnIndex: colIdx,
|
|
747
|
+
getCell: (name) => smart.getCell(name),
|
|
748
|
+
});
|
|
557
749
|
}
|
|
558
750
|
await columnOverride.write({
|
|
559
751
|
cell: cellLocator,
|
package/dist/strategies/fill.js
CHANGED
|
@@ -14,7 +14,11 @@ exports.FillStrategies = {
|
|
|
14
14
|
let currentValue;
|
|
15
15
|
// Auto-sync: If read exists, get current state first
|
|
16
16
|
if (columnOverride.read) {
|
|
17
|
-
|
|
17
|
+
const columnIndex = table ? (await table.getHeaders()).indexOf(columnName) : -1;
|
|
18
|
+
currentValue = await columnOverride.read(cell, {
|
|
19
|
+
row, columnName, columnIndex,
|
|
20
|
+
getCell: (name) => row.getCell(name),
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
await columnOverride.write({
|
|
20
24
|
cell,
|
|
@@ -69,7 +73,6 @@ exports.FillStrategies = {
|
|
|
69
73
|
const inputTag = await inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
70
74
|
const inputType = await inputLocator.getAttribute('type').catch(() => null);
|
|
71
75
|
const isContentEditable = await inputLocator.getAttribute('contenteditable').catch(() => null);
|
|
72
|
-
// console.log(`[SmartTable] Filling "${columnName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
|
|
73
76
|
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
74
77
|
// Boolean value for checkbox/radio
|
|
75
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
|
}
|
|
@@ -46,16 +46,59 @@ const dataAttribute = (options) => {
|
|
|
46
46
|
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
47
47
|
}, { rowSel, cellSel, colAttr, colOffset });
|
|
48
48
|
},
|
|
49
|
+
getVisibleRowIndices: async ({ root, config }) => {
|
|
50
|
+
const rowSel = config.rowSelector;
|
|
51
|
+
// Geometry-based (mirrors getVisibleRowRange's inclusive intersection), but returns
|
|
52
|
+
// the rows' DOM positions so the iteration engine can drop overscan rows (#353/#357).
|
|
53
|
+
return root.evaluate((el, { rowSel, containerSel }) => {
|
|
54
|
+
const rows = Array.from(el.querySelectorAll(rowSel));
|
|
55
|
+
const container = el.closest(containerSel);
|
|
56
|
+
const containerRect = container ? container.getBoundingClientRect() : null;
|
|
57
|
+
const visible = [];
|
|
58
|
+
rows.forEach((r, i) => {
|
|
59
|
+
// Container not resolvable — can't measure, so keep every row (no filtering).
|
|
60
|
+
if (!containerRect) {
|
|
61
|
+
visible.push(i);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const rect = r.getBoundingClientRect();
|
|
65
|
+
if (rect.height === 0)
|
|
66
|
+
return; // unrendered / detached
|
|
67
|
+
if (rect.bottom > containerRect.top && rect.top < containerRect.bottom)
|
|
68
|
+
visible.push(i);
|
|
69
|
+
});
|
|
70
|
+
return visible;
|
|
71
|
+
}, { rowSel, containerSel });
|
|
72
|
+
},
|
|
49
73
|
getVisibleRowRange: async ({ root, config }) => {
|
|
50
74
|
const rowSel = config.rowSelector;
|
|
51
|
-
return root.evaluate((el, { rowSel, rowAttr, rowOffset }) => {
|
|
52
|
-
const
|
|
75
|
+
return root.evaluate((el, { rowSel, rowAttr, rowOffset, containerSel }) => {
|
|
76
|
+
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
|
+
const container = el.closest(containerSel);
|
|
84
|
+
const containerRect = container ? container.getBoundingClientRect() : null;
|
|
85
|
+
const indices = rows
|
|
86
|
+
.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
|
+
if (!containerRect)
|
|
90
|
+
return true;
|
|
91
|
+
const rect = r.getBoundingClientRect();
|
|
92
|
+
if (rect.height === 0)
|
|
93
|
+
return false; // unrendered / detached
|
|
94
|
+
return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
|
|
95
|
+
})
|
|
53
96
|
.map(r => Number(r.getAttribute(rowAttr)) - rowOffset)
|
|
54
97
|
.filter(n => !isNaN(n));
|
|
55
98
|
if (!indices.length)
|
|
56
99
|
return { first: 0, last: 0 };
|
|
57
100
|
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
58
|
-
}, { rowSel, rowAttr, rowOffset });
|
|
101
|
+
}, { rowSel, rowAttr, rowOffset, containerSel });
|
|
59
102
|
},
|
|
60
103
|
scrollToColumn: async ({ root, config }, colIndex) => {
|
|
61
104
|
const headerSel = typeof config.headerSelector === 'string' ? config.headerSelector : null;
|