@rickcedwhat/playwright-smart-table 6.20.0 → 6.20.1-next.380efe3
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 +20 -16
- package/dist/engine/tableIteration.js +15 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/smartRow.js +63 -9
- package/dist/strategies/pagination.js +11 -2
- package/dist/strategies/viewport.js +26 -19
- package/dist/useTable.js +10 -1
- package/dist/utils/elementTracker.js +6 -3
- package/package.json +6 -1
package/dist/engine/rowFinder.js
CHANGED
|
@@ -104,6 +104,22 @@ class RowFinder {
|
|
|
104
104
|
smartRow[sentinel_1.SENTINEL_ROW] = true;
|
|
105
105
|
return smartRow;
|
|
106
106
|
}
|
|
107
|
+
async waitForTableReady() {
|
|
108
|
+
var _a;
|
|
109
|
+
const isTableLoading = (_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading;
|
|
110
|
+
if (!isTableLoading)
|
|
111
|
+
return;
|
|
112
|
+
const context = {
|
|
113
|
+
root: this.rootLocator,
|
|
114
|
+
config: this.config,
|
|
115
|
+
page: this.rootLocator.page(),
|
|
116
|
+
resolve: this.resolve
|
|
117
|
+
};
|
|
118
|
+
while (await isTableLoading(context)) {
|
|
119
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
|
|
120
|
+
await this.rootLocator.page().waitForTimeout(200);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
107
123
|
async findRows(filters = {}, options) {
|
|
108
124
|
var _a, _b, _c;
|
|
109
125
|
const filtersRecord = filters;
|
|
@@ -114,6 +130,7 @@ class RowFinder {
|
|
|
114
130
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
|
|
115
131
|
const tracker = new elementTracker_1.ElementTracker('findRows');
|
|
116
132
|
try {
|
|
133
|
+
await this.waitForTableReady();
|
|
117
134
|
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filtersRecord);
|
|
118
135
|
const hasOverrideFilters = Object.keys(overrideFilters).length > 0;
|
|
119
136
|
const hasSyntheticFilters = Object.keys(syntheticFilters).length > 0;
|
|
@@ -186,26 +203,13 @@ class RowFinder {
|
|
|
186
203
|
return (0, smartRowArray_1.createSmartRowArray)(allRows);
|
|
187
204
|
}
|
|
188
205
|
async findRowLocator(filters, options = {}) {
|
|
189
|
-
var _a, _b
|
|
206
|
+
var _a, _b;
|
|
190
207
|
const map = await this.tableMapper.getMap();
|
|
191
208
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
|
|
192
209
|
let pagesScanned = 1;
|
|
193
210
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
|
|
194
211
|
while (true) {
|
|
195
|
-
|
|
196
|
-
if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
|
|
197
|
-
const isLoading = await this.config.strategies.loading.isTableLoading({
|
|
198
|
-
root: this.rootLocator,
|
|
199
|
-
config: this.config,
|
|
200
|
-
page: this.rootLocator.page(),
|
|
201
|
-
resolve: this.resolve
|
|
202
|
-
});
|
|
203
|
-
if (isLoading) {
|
|
204
|
-
(0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
|
|
205
|
-
await this.rootLocator.page().waitForTimeout(200);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
212
|
+
await this.waitForTableReady();
|
|
209
213
|
const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
210
214
|
const { domFilters, overrideFilters, syntheticFilters } = this.splitFilters(filters);
|
|
211
215
|
const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
|
|
@@ -242,7 +246,7 @@ class RowFinder {
|
|
|
242
246
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
|
|
243
247
|
// Default to single-step goNext; bulk is opt-in via useBulkPagination: true (#349).
|
|
244
248
|
// Bulk-by-default made findRow jump past the page holding the target row.
|
|
245
|
-
const useBulk = options.useBulkPagination === true && !!((
|
|
249
|
+
const useBulk = options.useBulkPagination === true && !!((_b = this.config.strategies.pagination) === null || _b === void 0 ? void 0 : _b.goNextBulk);
|
|
246
250
|
const prevPage = this.tableState.currentPageIndex;
|
|
247
251
|
const didLoadMore = await this.advancePage(useBulk);
|
|
248
252
|
if (didLoadMore) {
|
|
@@ -37,6 +37,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
37
37
|
const tracker = new elementTracker_1.ElementTracker(label);
|
|
38
38
|
log(env.config, `${label}: starting (maxPages=${effectiveMaxPages}, mode=${concurrency}, dedupe=${!!dedupeStrategy})`);
|
|
39
39
|
const results = [];
|
|
40
|
+
const seenLogicalIndices = new Set();
|
|
40
41
|
try {
|
|
41
42
|
let rowIndex = 0;
|
|
42
43
|
let stopped = false;
|
|
@@ -63,9 +64,12 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
63
64
|
// #384: overscan rows ABOVE the visible range have already scrolled past and will never
|
|
64
65
|
// re-enter the viewport — collect them now. Only defer rows BELOW the visible range
|
|
65
66
|
// (they'll scroll into view on a later page).
|
|
67
|
+
//
|
|
68
|
+
// #417 follow-up: skip viewport filtering on the final scan — there are no more pages
|
|
69
|
+
// to defer to, so collect all remaining unseen rows regardless of visibility.
|
|
66
70
|
let candidateIndices = allIndices;
|
|
67
71
|
const getVisibleRowIndices = (_f = env.config.strategies.viewport) === null || _f === void 0 ? void 0 : _f.getVisibleRowIndices;
|
|
68
|
-
if (getVisibleRowIndices) {
|
|
72
|
+
if (getVisibleRowIndices && !reachedEnd) {
|
|
69
73
|
const visible = new Set(await getVisibleRowIndices(env.getContext()));
|
|
70
74
|
const minVisible = visible.size > 0 ? Math.min(...visible) : Infinity;
|
|
71
75
|
candidateIndices = allIndices.filter(idx => visible.has(idx) || idx < minVisible);
|
|
@@ -111,6 +115,13 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
111
115
|
if (stopped && enumIndex > stoppedIndex) {
|
|
112
116
|
return SKIP;
|
|
113
117
|
}
|
|
118
|
+
// When resolveRowIndex is configured, identical logical indices across pages
|
|
119
|
+
// indicate the same record (e.g. re-created DOM elements in recycling
|
|
120
|
+
// virtualizers whose WeakMap entry was GC'd). Skip without processing.
|
|
121
|
+
if (row.rowIndex !== undefined && seenLogicalIndices.has(row.rowIndex)) {
|
|
122
|
+
log(env.config, `${label}: skipping duplicate logical row ${row.rowIndex}`);
|
|
123
|
+
return SKIP;
|
|
124
|
+
}
|
|
114
125
|
// Wait for the row to finish loading BEFORE evaluating its dedupe key (Bug #355).
|
|
115
126
|
// map/forEach/filter process a still-loading row when no timeout is set (unlike
|
|
116
127
|
// findRows, which skips) — see resolveRowLoading's `noTimeoutAction`.
|
|
@@ -141,6 +152,9 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
141
152
|
if (dedupeKeys && dedupeKey !== undefined && result !== SKIP) {
|
|
142
153
|
dedupeKeys.add(dedupeKey);
|
|
143
154
|
}
|
|
155
|
+
if (row.rowIndex !== undefined && result !== SKIP) {
|
|
156
|
+
seenLogicalIndices.add(row.rowIndex);
|
|
157
|
+
}
|
|
144
158
|
return result;
|
|
145
159
|
}
|
|
146
160
|
finally {
|
package/dist/packageVersion.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Generated by scripts/embed-version.mjs from package.json
|
|
4
4
|
*/
|
|
5
5
|
/** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
|
|
6
|
-
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.20.
|
|
6
|
+
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.20.1-next.380efe3";
|
package/dist/packageVersion.js
CHANGED
|
@@ -6,4 +6,4 @@ exports.PLAYWRIGHT_SMART_TABLE_VERSION = void 0;
|
|
|
6
6
|
* Generated by scripts/embed-version.mjs from package.json
|
|
7
7
|
*/
|
|
8
8
|
/** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
|
|
9
|
-
exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.20.
|
|
9
|
+
exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.20.1-next.380efe3";
|
package/dist/smartRow.js
CHANGED
|
@@ -424,7 +424,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
424
424
|
return (await cell.innerText()).trim();
|
|
425
425
|
};
|
|
426
426
|
smart.toJSON = async (options) => {
|
|
427
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
427
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
428
428
|
// Atomic mode: snapshot the row in a single evaluate, then apply column overrides
|
|
429
429
|
// against a frozen reconstruction. Zero inter-column stagger — even in-place React
|
|
430
430
|
// re-renders can't affect it because overrides read from a detached clone.
|
|
@@ -434,10 +434,64 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
434
434
|
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
435
435
|
}
|
|
436
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
|
+
if (resolveRI && typeof rowIndex === 'number') {
|
|
444
|
+
const isSelfHealing = smart._selfHealing === true;
|
|
445
|
+
if (!isSelfHealing) {
|
|
446
|
+
let drifted = false;
|
|
447
|
+
try {
|
|
448
|
+
const curResult = await resolveRI(rowLocator);
|
|
449
|
+
const curIndex = curResult !== undefined
|
|
450
|
+
? (0, rowResolution_1.normalizeRowIndexResult)(curResult).index
|
|
451
|
+
: undefined;
|
|
452
|
+
drifted = curIndex !== rowIndex;
|
|
453
|
+
}
|
|
454
|
+
catch (_k) {
|
|
455
|
+
drifted = true;
|
|
456
|
+
}
|
|
457
|
+
if (drifted) {
|
|
458
|
+
const inBatch = smart._inBatch === true || !!smart._barrier;
|
|
459
|
+
const rescanForRow = async () => {
|
|
460
|
+
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
461
|
+
for (const r of rows) {
|
|
462
|
+
try {
|
|
463
|
+
const rResult = await resolveRI(r);
|
|
464
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
465
|
+
return r;
|
|
466
|
+
}
|
|
467
|
+
catch ( /* element may have detached during scan */_a) { /* element may have detached during scan */ }
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
};
|
|
471
|
+
let recovered = await rescanForRow();
|
|
472
|
+
if (!recovered && !inBatch && ((_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow)) {
|
|
473
|
+
await config.strategies.viewport.scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
|
|
474
|
+
const deadline = Date.now() + 500;
|
|
475
|
+
while (!recovered && Date.now() < deadline) {
|
|
476
|
+
recovered = await rescanForRow();
|
|
477
|
+
if (!recovered)
|
|
478
|
+
await page.waitForTimeout(50);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (!recovered) {
|
|
482
|
+
throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
|
|
483
|
+
(inBatch
|
|
484
|
+
? `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`
|
|
485
|
+
: `Ensure a viewport.scrollToRow strategy can restore the row.`));
|
|
486
|
+
}
|
|
487
|
+
cloneTarget = recovered;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
437
491
|
// Step 1: clone row in browser memory (NOT in the DOM), extract text + row HTML.
|
|
438
492
|
// Uses textContent (not innerText) because the clone is detached — innerText
|
|
439
493
|
// falls back to textContent on detached nodes per HTML spec, so be explicit.
|
|
440
|
-
const cloneHandle = await
|
|
494
|
+
const cloneHandle = await cloneTarget.evaluateHandle(el => el.cloneNode(true));
|
|
441
495
|
let snapshot;
|
|
442
496
|
try {
|
|
443
497
|
snapshot = await cloneHandle.evaluate((row, sel) => {
|
|
@@ -491,7 +545,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
491
545
|
await materializeRow();
|
|
492
546
|
const result = {};
|
|
493
547
|
for (const [col, idx] of columnsToProcess) {
|
|
494
|
-
const columnOverride = (
|
|
548
|
+
const columnOverride = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[col];
|
|
495
549
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
496
550
|
if (mapper) {
|
|
497
551
|
const cell = page.locator(`#${snapId}`).locator(cellSel).nth(idx);
|
|
@@ -508,7 +562,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
508
562
|
result[col] = snapshot.cells[idx].text;
|
|
509
563
|
}
|
|
510
564
|
}
|
|
511
|
-
for (const [name, def] of Object.entries((
|
|
565
|
+
for (const [name, def] of Object.entries((_c = config.syntheticColumns) !== null && _c !== void 0 ? _c : {})) {
|
|
512
566
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
513
567
|
continue;
|
|
514
568
|
const snapshotRow = Object.create(smart);
|
|
@@ -607,7 +661,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
607
661
|
// Re-pin to the correct logical row before reading this column (#366).
|
|
608
662
|
const stableRow = await resolveStableRow();
|
|
609
663
|
// Check if we have a column override for this column
|
|
610
|
-
const columnOverride = (
|
|
664
|
+
const columnOverride = (_d = config.columnOverrides) === null || _d === void 0 ? void 0 : _d[col];
|
|
611
665
|
const mapper = columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read;
|
|
612
666
|
// --- Navigation Logic Start ---
|
|
613
667
|
const cell = config.strategies.getCellLocator
|
|
@@ -654,13 +708,13 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
654
708
|
});
|
|
655
709
|
}
|
|
656
710
|
// Cell loading wait (if configured)
|
|
657
|
-
const isCellLoading = (
|
|
658
|
-
const rawCellTimeout = (
|
|
711
|
+
const isCellLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isCellLoading;
|
|
712
|
+
const rawCellTimeout = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.cellLoadingTimeout;
|
|
659
713
|
// undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms
|
|
660
714
|
const cellLoadingTimeout = rawCellTimeout !== undefined && Number.isFinite(rawCellTimeout) && rawCellTimeout >= 0
|
|
661
715
|
? rawCellTimeout
|
|
662
716
|
: undefined;
|
|
663
|
-
const onCellLoadingTimeout = (
|
|
717
|
+
const onCellLoadingTimeout = (_h = (_g = config.strategies.loading) === null || _g === void 0 ? void 0 : _g.onCellLoadingTimeout) !== null && _h !== void 0 ? _h : 'read-as-is';
|
|
664
718
|
if (isCellLoading && await isCellLoading(targetCell, col, smart)) {
|
|
665
719
|
if (cellLoadingTimeout !== undefined) {
|
|
666
720
|
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${cellLoadingTimeout}ms`);
|
|
@@ -699,7 +753,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
699
753
|
result[col] = (text || '').trim();
|
|
700
754
|
}
|
|
701
755
|
}
|
|
702
|
-
for (const [name, def] of Object.entries((
|
|
756
|
+
for (const [name, def] of Object.entries((_j = config.syntheticColumns) !== null && _j !== void 0 ? _j : {})) {
|
|
703
757
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(name))
|
|
704
758
|
continue;
|
|
705
759
|
const snapshotRow = Object.create(smart);
|
|
@@ -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;
|
package/dist/useTable.js
CHANGED
|
@@ -301,7 +301,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
301
301
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
302
302
|
},
|
|
303
303
|
countRows: async (filters, options) => {
|
|
304
|
-
var _a, _b, _c, _d;
|
|
304
|
+
var _a, _b, _c, _d, _e;
|
|
305
305
|
if (tableState.empty)
|
|
306
306
|
return 0;
|
|
307
307
|
await _autoInit();
|
|
@@ -359,6 +359,15 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
359
359
|
}
|
|
360
360
|
return count;
|
|
361
361
|
};
|
|
362
|
+
// Wait for table to finish loading before counting
|
|
363
|
+
const isTableLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isTableLoading;
|
|
364
|
+
if (isTableLoading) {
|
|
365
|
+
const ctx = createStrategyContext();
|
|
366
|
+
while (await isTableLoading(ctx)) {
|
|
367
|
+
log('countRows: table is loading... waiting');
|
|
368
|
+
await rootLocator.page().waitForTimeout(200);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
362
371
|
if (!hasPagination) {
|
|
363
372
|
log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
|
|
364
373
|
if (!hasPostFilters)
|
|
@@ -18,7 +18,8 @@ class ElementTracker {
|
|
|
18
18
|
const seenMap = win[trackerId];
|
|
19
19
|
const newIndices = [];
|
|
20
20
|
elements.forEach((el, index) => {
|
|
21
|
-
const
|
|
21
|
+
const htmlEl = el;
|
|
22
|
+
const signature = htmlEl.offsetTop + '|' + (el.textContent || '');
|
|
22
23
|
if (seenMap.get(el) !== signature) {
|
|
23
24
|
newIndices.push(index);
|
|
24
25
|
}
|
|
@@ -39,8 +40,10 @@ class ElementTracker {
|
|
|
39
40
|
const seenMap = win[trackerId];
|
|
40
41
|
for (const index of indicesToCommit) {
|
|
41
42
|
const el = elements[index];
|
|
42
|
-
if (el)
|
|
43
|
-
|
|
43
|
+
if (el) {
|
|
44
|
+
const htmlEl = el;
|
|
45
|
+
seenMap.set(el, htmlEl.offsetTop + '|' + (el.textContent || ''));
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
}, [this.id, indices]);
|
|
46
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "6.20.
|
|
3
|
+
"version": "6.20.1-next.380efe3",
|
|
4
4
|
"description": "Smart, column-aware table interactions for Playwright",
|
|
5
5
|
"author": "Cedrick Catalan",
|
|
6
6
|
"license": "MIT",
|
|
@@ -110,6 +110,11 @@
|
|
|
110
110
|
"peerDependencies": {
|
|
111
111
|
"@playwright/test": "^1.40.0"
|
|
112
112
|
},
|
|
113
|
+
"peerDependenciesMeta": {
|
|
114
|
+
"@playwright/test": {
|
|
115
|
+
"optional": true
|
|
116
|
+
}
|
|
117
|
+
},
|
|
113
118
|
"devDependencies": {
|
|
114
119
|
"@commitlint/cli": "^21.0.0",
|
|
115
120
|
"@commitlint/config-conventional": "^21.0.0",
|