@rickcedwhat/playwright-smart-table 6.25.0-next.ec8d20a → 6.25.0-next.f72ee4c
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/cellNavigation.d.ts +24 -0
- package/dist/engine/cellNavigation.js +326 -0
- package/dist/engine/countRows.d.ts +28 -0
- package/dist/engine/countRows.js +149 -0
- package/dist/engine/rowByIndex.d.ts +20 -0
- package/dist/engine/rowByIndex.js +79 -0
- package/dist/engine/rowFinder.d.ts +3 -1
- package/dist/engine/rowFinder.js +5 -7
- package/dist/engine/rowSnapshot.d.ts +28 -0
- package/dist/engine/rowSnapshot.js +416 -0
- package/dist/engine/scanPages.d.ts +7 -2
- package/dist/engine/scanPages.js +11 -0
- package/dist/engine/sorting.d.ts +11 -0
- package/dist/engine/sorting.js +51 -0
- package/dist/engine/tableIteration.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/smartRow.js +12 -692
- package/dist/strategies/columns.d.ts +1 -1
- package/dist/strategies/contentReady.d.ts +19 -1
- package/dist/strategies/contentReady.js +81 -0
- package/dist/strategies/index.d.ts +5 -0
- package/dist/types.d.ts +26 -5
- package/dist/useTable.js +41 -239
- package/dist/utils/navigationBarrier.js +0 -1
- package/dist/utils/safeStringify.d.ts +2 -0
- package/dist/utils/safeStringify.js +18 -0
- package/dist/utils/smartRowArray.d.ts +1 -1
- package/dist/utils/smartRowArray.js +4 -1
- package/package.json +1 -1
package/dist/engine/rowFinder.js
CHANGED
|
@@ -5,7 +5,6 @@ const debugUtils_1 = require("../utils/debugUtils");
|
|
|
5
5
|
const smartRowArray_1 = require("../utils/smartRowArray");
|
|
6
6
|
const elementTracker_1 = require("../utils/elementTracker");
|
|
7
7
|
const sentinel_1 = require("../utils/sentinel");
|
|
8
|
-
const navigationBarrier_1 = require("../utils/navigationBarrier");
|
|
9
8
|
const rowResolution_1 = require("./rowResolution");
|
|
10
9
|
const resolveCellLocator_1 = require("../utils/resolveCellLocator");
|
|
11
10
|
const scanPages_1 = require("./scanPages");
|
|
@@ -172,26 +171,23 @@ class RowFinder {
|
|
|
172
171
|
const newIndices = await tracker.getUnseenIndices(rowLocators);
|
|
173
172
|
const currentRows = await rowLocators.all();
|
|
174
173
|
let added = 0;
|
|
175
|
-
const useBarrier = this.config.concurrency === 'synchronized' && newIndices.length > 1;
|
|
176
|
-
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(newIndices.length) : undefined;
|
|
177
174
|
for (const idx of newIndices) {
|
|
178
175
|
const resolved = await (0, rowResolution_1.resolveLogicalRowIndex)(currentRows[idx], this.config, () => allRows.length);
|
|
179
176
|
const rowLocator = this.nameFilteredRow(currentRows[idx], resolved, filtersRecord);
|
|
180
|
-
|
|
177
|
+
// These rows outlive the scan and may be consumed individually. Only
|
|
178
|
+
// iteration engines that own every callback can attach a batch barrier.
|
|
179
|
+
const smartRow = this.makeSmartRow(rowLocator, map, resolved === null || resolved === void 0 ? void 0 : resolved.index, this.tableState.currentPageIndex, undefined, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
|
|
181
180
|
const loadingOutcome = await (0, rowResolution_1.resolveRowLoading)(smartRow, this.config.strategies.loading, 'skip', (msg) => (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: ${msg}`));
|
|
182
181
|
if (loadingOutcome !== 'process') {
|
|
183
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
184
182
|
if (loadingOutcome === 'throw') {
|
|
185
183
|
throw new Error(`[SmartTable] Row ${allRows.length} did not finish loading within ${(_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.rowLoadingTimeout}ms`);
|
|
186
184
|
}
|
|
187
185
|
continue;
|
|
188
186
|
}
|
|
189
187
|
if (hasOverrideFilters && !await this.matchesOverrideFilters(currentRows[idx], overrideFilters, map, (_c = options === null || options === void 0 ? void 0 : options.exact) !== null && _c !== void 0 ? _c : false)) {
|
|
190
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
191
188
|
continue;
|
|
192
189
|
}
|
|
193
190
|
if (hasSyntheticFilters && !await this.matchesSyntheticFilters(currentRows[idx], syntheticFilters, map, (_d = options === null || options === void 0 ? void 0 : options.exact) !== null && _d !== void 0 ? _d : false)) {
|
|
194
|
-
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
195
191
|
continue;
|
|
196
192
|
}
|
|
197
193
|
allRows.push(smartRow);
|
|
@@ -204,6 +200,7 @@ class RowFinder {
|
|
|
204
200
|
useBulk,
|
|
205
201
|
label: 'findRows',
|
|
206
202
|
finalScanOnEof: hasPagination,
|
|
203
|
+
signal: options === null || options === void 0 ? void 0 : options.signal,
|
|
207
204
|
});
|
|
208
205
|
(0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
|
|
209
206
|
}
|
|
@@ -268,6 +265,7 @@ class RowFinder {
|
|
|
268
265
|
useBulk,
|
|
269
266
|
label: 'findRow',
|
|
270
267
|
finalScanOnEof: true,
|
|
268
|
+
signal: options.signal,
|
|
271
269
|
});
|
|
272
270
|
return found;
|
|
273
271
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
|
+
import type { FinalTableConfig, SmartRow, TableResult } from '../types';
|
|
3
|
+
import type { SmartRowInternals } from '../utils/smartRowInternals';
|
|
4
|
+
/** Everything `toJSON` needs from the SmartRow being read. */
|
|
5
|
+
export interface RowSnapshotEnv<T = any> {
|
|
6
|
+
config: FinalTableConfig<T>;
|
|
7
|
+
rootLocator: Locator;
|
|
8
|
+
resolve: (item: any, parent: Locator | Page) => Locator;
|
|
9
|
+
map: Map<string, number>;
|
|
10
|
+
row: SmartRow<T> & SmartRowInternals;
|
|
11
|
+
rowLocator: Locator;
|
|
12
|
+
rowIndex: number | undefined;
|
|
13
|
+
table: TableResult<T> | null;
|
|
14
|
+
renderWindowPosition: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** `getHeaderCell` for strategy hooks: the table's when available, else an index lookup. */
|
|
17
|
+
export declare function headerCellGetter<T>(env: Pick<RowSnapshotEnv<T>, 'config' | 'rootLocator' | 'resolve' | 'map' | 'table'>): (columnName: string) => Promise<Locator>;
|
|
18
|
+
/**
|
|
19
|
+
* `toJSON({ atomic: true })`: snapshot the row in a single evaluate, then apply column
|
|
20
|
+
* overrides against a frozen reconstruction. Zero inter-column stagger — even in-place
|
|
21
|
+
* React re-renders can't affect it because overrides read from a detached clone.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readRowAtomic<T>(env: RowSnapshotEnv<T>, columns?: string[]): Promise<T>;
|
|
24
|
+
/**
|
|
25
|
+
* Default `toJSON()`: reads each column from the live DOM, navigating to it first
|
|
26
|
+
* (keeps synchronized batches in lock-step) and re-pinning the row before each read.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readRowLive<T>(env: RowSnapshotEnv<T>, columns?: string[]): Promise<T>;
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.headerCellGetter = headerCellGetter;
|
|
4
|
+
exports.readRowAtomic = readRowAtomic;
|
|
5
|
+
exports.readRowLive = readRowLive;
|
|
6
|
+
const rowResolution_1 = require("./rowResolution");
|
|
7
|
+
const cellNavigation_1 = require("./cellNavigation");
|
|
8
|
+
const debugUtils_1 = require("../utils/debugUtils");
|
|
9
|
+
/** `getHeaderCell` for strategy hooks: the table's when available, else an index lookup. */
|
|
10
|
+
function headerCellGetter(env) {
|
|
11
|
+
const { table } = env;
|
|
12
|
+
return (table === null || table === void 0 ? void 0 : table.getHeaderCell)
|
|
13
|
+
? table.getHeaderCell.bind(table)
|
|
14
|
+
: async (colName) => {
|
|
15
|
+
const idx = env.map.get(colName);
|
|
16
|
+
if (idx === undefined)
|
|
17
|
+
throw new Error(`Column "${colName}" not found`);
|
|
18
|
+
return env.resolve(env.config.headerSelector, env.rootLocator).nth(idx);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Adds synthetic columns to `result`; their compute() reads already-read columns from `result`. */
|
|
22
|
+
async function computeSyntheticColumns(env, result, columns) {
|
|
23
|
+
var _a;
|
|
24
|
+
const smart = env.row;
|
|
25
|
+
for (const [name, def] of Object.entries((_a = env.config.syntheticColumns) !== null && _a !== void 0 ? _a : {})) {
|
|
26
|
+
if (columns && !columns.includes(name))
|
|
27
|
+
continue;
|
|
28
|
+
const snapshotRow = Object.create(smart);
|
|
29
|
+
snapshotRow.getValue = async (col) => {
|
|
30
|
+
if (col in result)
|
|
31
|
+
return String(result[col]);
|
|
32
|
+
return smart.getValue(col);
|
|
33
|
+
};
|
|
34
|
+
result[name] = String(await def.compute(snapshotRow));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const isBatchRow = (row) => row._inBatch === true || !!row._barrier;
|
|
38
|
+
/** Capture a mounted position's identity once, before readiness or navigation can recycle it. */
|
|
39
|
+
async function resolveSnapshotIdentity(env) {
|
|
40
|
+
if (!env.renderWindowPosition || !env.config.strategies.resolveRowIndex)
|
|
41
|
+
return env;
|
|
42
|
+
const identity = await (0, rowResolution_1.resolveLogicalRowIndex)(env.rowLocator, env.config, () => undefined);
|
|
43
|
+
// Keep the public SmartRow's mounted position unchanged; all snapshot recovery and
|
|
44
|
+
// navigation use this read's logical identity. An unknown identity is not position 0.
|
|
45
|
+
return Object.assign(Object.assign({}, env), { rowIndex: identity === null || identity === void 0 ? void 0 : identity.index, renderWindowPosition: false });
|
|
46
|
+
}
|
|
47
|
+
// ─── Atomic snapshot ─────────────────────────────────────────────────────────
|
|
48
|
+
/** Mounted row whose logical index (per resolveRowIndex) is `rowIndex`; tolerates rows detaching mid-scan. */
|
|
49
|
+
async function rescanForRow(env, rowIndex) {
|
|
50
|
+
const resolveRI = env.config.strategies.resolveRowIndex;
|
|
51
|
+
const rows = await env.resolve(env.config.rowSelector, env.rootLocator).all();
|
|
52
|
+
for (const r of rows) {
|
|
53
|
+
try {
|
|
54
|
+
const rResult = await resolveRI(r);
|
|
55
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
56
|
+
return r;
|
|
57
|
+
}
|
|
58
|
+
catch (_a) { /* element may have detached during scan */ }
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function logicalIndexOf(env, loc) {
|
|
63
|
+
try {
|
|
64
|
+
const r = await env.config.strategies.resolveRowIndex(loc);
|
|
65
|
+
return r !== undefined ? (0, rowResolution_1.normalizeRowIndexResult)(r).index : undefined;
|
|
66
|
+
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const recycledRowHint = (inBatch, batchNote, otherwise) => inBatch ? batchNote : otherwise;
|
|
72
|
+
/** Scrolls the row back into view and rescans for it for up to 500ms. */
|
|
73
|
+
async function scrollBackAndRescan(env, rowIndex) {
|
|
74
|
+
var _a;
|
|
75
|
+
const { config, rootLocator, resolve } = env;
|
|
76
|
+
const scrollToRow = (_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow;
|
|
77
|
+
if (!scrollToRow)
|
|
78
|
+
return null;
|
|
79
|
+
const page = rootLocator.page();
|
|
80
|
+
await scrollToRow({ root: rootLocator, config, page, resolve }, rowIndex);
|
|
81
|
+
const deadline = Date.now() + 500;
|
|
82
|
+
let recovered = null;
|
|
83
|
+
while (!recovered && Date.now() < deadline) {
|
|
84
|
+
recovered = await rescanForRow(env, rowIndex);
|
|
85
|
+
if (!recovered)
|
|
86
|
+
await page.waitForTimeout(50);
|
|
87
|
+
}
|
|
88
|
+
return recovered;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Re-pin: the row locator is a positional nth-child from the iteration loop.
|
|
92
|
+
* On recycling virtualizers, elements can shift between SmartRow creation and
|
|
93
|
+
* the clone — the nth element may now show a different row's content.
|
|
94
|
+
* Verify via resolveRowIndex and rescan if drifted.
|
|
95
|
+
*/
|
|
96
|
+
async function recoverDriftedRow(env, rowIndex) {
|
|
97
|
+
const inBatch = isBatchRow(env.row);
|
|
98
|
+
let recovered = await rescanForRow(env, rowIndex);
|
|
99
|
+
if (!recovered && !inBatch)
|
|
100
|
+
recovered = await scrollBackAndRescan(env, rowIndex);
|
|
101
|
+
if (recovered)
|
|
102
|
+
return recovered;
|
|
103
|
+
throw new Error(`[SmartTable] toJSON({ atomic: true }): row ${rowIndex} recycled out of the DOM and could not be recovered. ` +
|
|
104
|
+
recycledRowHint(inBatch, `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows.)`, `Ensure a viewport.scrollToRow strategy can restore the row.`));
|
|
105
|
+
}
|
|
106
|
+
/** The element to clone: the row itself, re-pinned if it drifted, after `contentReady` settles. */
|
|
107
|
+
async function resolveCloneTarget(env) {
|
|
108
|
+
const { config, rowIndex } = env;
|
|
109
|
+
const canRepin = !!config.strategies.resolveRowIndex && typeof rowIndex === 'number' && env.row._selfHealing !== true;
|
|
110
|
+
let cloneTarget = env.rowLocator;
|
|
111
|
+
if (canRepin && await logicalIndexOf(env, cloneTarget) !== rowIndex) {
|
|
112
|
+
cloneTarget = await recoverDriftedRow(env, rowIndex);
|
|
113
|
+
}
|
|
114
|
+
// Wait for async content renders to settle before cloning.
|
|
115
|
+
if (config.strategies.contentReady) {
|
|
116
|
+
await config.strategies.contentReady(cloneTarget, env.rootLocator.page());
|
|
117
|
+
// Revalidate: the element may have recycled during the await.
|
|
118
|
+
if (canRepin && await logicalIndexOf(env, cloneTarget) !== rowIndex) {
|
|
119
|
+
cloneTarget = await recoverDriftedRow(env, rowIndex);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return cloneTarget;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clones the row in browser memory (NOT in the DOM) and extracts text + row HTML.
|
|
126
|
+
* Uses textContent (not innerText) because the clone is detached — innerText
|
|
127
|
+
* falls back to textContent on detached nodes per HTML spec, so be explicit.
|
|
128
|
+
*/
|
|
129
|
+
async function cloneRow(target, cellSel) {
|
|
130
|
+
const cloneHandle = await target.evaluateHandle(el => el.cloneNode(true));
|
|
131
|
+
try {
|
|
132
|
+
return await cloneHandle.evaluate((row, sel) => {
|
|
133
|
+
const cells = row.querySelectorAll(sel);
|
|
134
|
+
return {
|
|
135
|
+
rowHTML: row.outerHTML,
|
|
136
|
+
cells: [...cells].map(c => ({
|
|
137
|
+
text: (c.textContent || '').trim(),
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
}, cellSel);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await cloneHandle.dispose();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `toJSON({ atomic: true })`: snapshot the row in a single evaluate, then apply column
|
|
148
|
+
* overrides against a frozen reconstruction. Zero inter-column stagger — even in-place
|
|
149
|
+
* React re-renders can't affect it because overrides read from a detached clone.
|
|
150
|
+
*/
|
|
151
|
+
async function readRowAtomic(env, columns) {
|
|
152
|
+
var _a, _b;
|
|
153
|
+
const { config, map, rootLocator } = env;
|
|
154
|
+
const cellSel = config.cellSelector;
|
|
155
|
+
if (typeof cellSel !== 'string') {
|
|
156
|
+
throw new Error('[SmartTable] toJSON({ atomic: true }) requires cellSelector to be a CSS string, not a function.');
|
|
157
|
+
}
|
|
158
|
+
env = await resolveSnapshotIdentity(env);
|
|
159
|
+
const page = rootLocator.page();
|
|
160
|
+
const snapshot = await cloneRow(await resolveCloneTarget(env), cellSel);
|
|
161
|
+
// Determine which columns have overrides (need lazy materialization)
|
|
162
|
+
const columnsToProcess = [...map.entries()].filter(([col]) => !columns || columns.includes(col));
|
|
163
|
+
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); });
|
|
164
|
+
// Lazily materialize the row if any override needs a Locator.
|
|
165
|
+
// Uses position:fixed off-screen (NOT display:none — that breaks innerText).
|
|
166
|
+
const snapId = `__st_snap_${Math.random().toString(36).slice(2, 10)}`;
|
|
167
|
+
const snapCell = (colIdx) => page.locator(`#${snapId}`).locator(cellSel).nth(colIdx);
|
|
168
|
+
const getCell = (colName) => {
|
|
169
|
+
const colIdx = map.get(colName);
|
|
170
|
+
if (colIdx === undefined)
|
|
171
|
+
throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
|
|
172
|
+
return snapCell(colIdx);
|
|
173
|
+
};
|
|
174
|
+
if (hasAnyOverride) {
|
|
175
|
+
await page.evaluate(([html, id]) => {
|
|
176
|
+
const c = document.createElement('div');
|
|
177
|
+
c.id = id;
|
|
178
|
+
c.style.cssText = 'position:fixed;left:-99999px;top:-99999px;opacity:0;pointer-events:none';
|
|
179
|
+
// Wrap in <table><tbody> so the browser's HTML parser keeps <tr>/<td> intact.
|
|
180
|
+
c.innerHTML = `<table><tbody>${html}</tbody></table>`;
|
|
181
|
+
document.body.appendChild(c);
|
|
182
|
+
}, [snapshot.rowHTML, snapId]);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const [col, idx] of columnsToProcess) {
|
|
187
|
+
const mapper = (_b = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b.read;
|
|
188
|
+
if (mapper) {
|
|
189
|
+
try {
|
|
190
|
+
result[col] = await mapper(snapCell(idx), { row: env.row, columnName: col, columnIndex: idx, getCell });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const wrapped = new Error(`[SmartTable] toJSON({ atomic: true }): column override for "${col}" failed — ${err.message}`);
|
|
194
|
+
wrapped.cause = err;
|
|
195
|
+
throw wrapped;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (idx < snapshot.cells.length) {
|
|
199
|
+
result[col] = snapshot.cells[idx].text;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
await computeSyntheticColumns(env, result, columns);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (hasAnyOverride)
|
|
207
|
+
await page.evaluate(id => { var _a; return (_a = document.getElementById(id)) === null || _a === void 0 ? void 0 : _a.remove(); }, snapId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// ─── Live read ───────────────────────────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* #366 / #392: on virtualized tables the row's DOM node can recycle between the
|
|
213
|
+
* per-column awaits of a live read. When a self-healing locator is in use (the row locator was
|
|
214
|
+
* rebuilt with a CSS selector, e.g. `[data-rowindex="47"]`), the locator automatically
|
|
215
|
+
* re-queries the DOM — no rescan needed. For plain-number strategies (JS-only, no
|
|
216
|
+
* selector), fall back to rescan-then-scrollToRow.
|
|
217
|
+
*
|
|
218
|
+
* Returns `resolveStableRow` (call before each column) and `current` (last known-good row).
|
|
219
|
+
*/
|
|
220
|
+
function createRowPinner(env) {
|
|
221
|
+
const { config, rowLocator, rowIndex } = env;
|
|
222
|
+
const resolveRI = config.strategies.resolveRowIndex;
|
|
223
|
+
const canPin = !!resolveRI && typeof rowIndex === 'number' && env.row._selfHealing !== true;
|
|
224
|
+
let lastGood = rowLocator;
|
|
225
|
+
const resolveStableRow = async () => {
|
|
226
|
+
// Self-healing locator: always re-queries the DOM by attribute — no rescan needed.
|
|
227
|
+
if (!canPin)
|
|
228
|
+
return rowLocator;
|
|
229
|
+
const result = await resolveRI(lastGood);
|
|
230
|
+
if (result !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(result).index === rowIndex)
|
|
231
|
+
return lastGood; // still correct
|
|
232
|
+
// Drifted (recycled). Re-locate the row carrying the expected logical index.
|
|
233
|
+
lastGood = await relocateRecycledRow(env, rowIndex);
|
|
234
|
+
return lastGood;
|
|
235
|
+
};
|
|
236
|
+
return { resolveStableRow, current: () => lastGood };
|
|
237
|
+
}
|
|
238
|
+
/** Mounted row whose logical index is `rowIndex` (strict: resolveRowIndex errors propagate). */
|
|
239
|
+
async function rescanStrict(env, rowIndex) {
|
|
240
|
+
const resolveRI = env.config.strategies.resolveRowIndex;
|
|
241
|
+
const rows = await env.resolve(env.config.rowSelector, env.rootLocator).all();
|
|
242
|
+
for (const r of rows) {
|
|
243
|
+
const rResult = await resolveRI(r);
|
|
244
|
+
if (rResult !== undefined && (0, rowResolution_1.normalizeRowIndexResult)(rResult).index === rowIndex)
|
|
245
|
+
return r;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
/** Live-read recovery: rescan, then (outside batches) scrollToRow and rescan once more. */
|
|
250
|
+
async function relocateRecycledRow(env, rowIndex) {
|
|
251
|
+
var _a;
|
|
252
|
+
const { config, rootLocator, resolve } = env;
|
|
253
|
+
const inBatch = isBatchRow(env.row);
|
|
254
|
+
let recovered = await rescanStrict(env, rowIndex);
|
|
255
|
+
const scrollToRow = (_a = config.strategies.viewport) === null || _a === void 0 ? void 0 : _a.scrollToRow;
|
|
256
|
+
if (!recovered && !inBatch && scrollToRow) {
|
|
257
|
+
await scrollToRow({ root: rootLocator, config, page: rootLocator.page(), resolve }, rowIndex);
|
|
258
|
+
recovered = await rescanStrict(env, rowIndex);
|
|
259
|
+
}
|
|
260
|
+
if (recovered)
|
|
261
|
+
return recovered;
|
|
262
|
+
throw new Error(`[SmartTable] toJSON: row ${rowIndex} recycled out of the DOM mid-read and could not be recovered — ` +
|
|
263
|
+
`the returned object would mix fields from different rows. ` +
|
|
264
|
+
recycledRowHint(inBatch, `(During a map/forEach batch, scroll-back recovery is disabled to avoid disrupting sibling rows; the row must stay mounted for the read.)`, `Ensure a viewport.scrollToRow can restore the row.`));
|
|
265
|
+
}
|
|
266
|
+
function cellIn(env, row, colName, colIdx) {
|
|
267
|
+
const { config, rootLocator } = env;
|
|
268
|
+
if (config.strategies.getCellLocator) {
|
|
269
|
+
return config.strategies.getCellLocator({
|
|
270
|
+
row, root: rootLocator, columnName: colName,
|
|
271
|
+
columnIndex: colIdx, rowIndex: env.rowIndex, page: rootLocator.page(), config,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return env.resolve(config.cellSelector, row).nth(colIdx);
|
|
275
|
+
}
|
|
276
|
+
/** undefined = unset (read as-is immediately); 0 = no wait (immediate check); >0 = wait up to N ms */
|
|
277
|
+
function cellLoadingTimeoutOf(config) {
|
|
278
|
+
var _a;
|
|
279
|
+
const raw = (_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.cellLoadingTimeout;
|
|
280
|
+
return raw !== undefined && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
|
|
281
|
+
}
|
|
282
|
+
/** Polls `check` every `intervalMs` until it returns true or `deadline` passes. */
|
|
283
|
+
async function pollUntil(page, deadline, intervalMs, check, initial) {
|
|
284
|
+
let done = initial;
|
|
285
|
+
while (!done && Date.now() < deadline) {
|
|
286
|
+
await page.waitForTimeout(intervalMs);
|
|
287
|
+
done = await check();
|
|
288
|
+
}
|
|
289
|
+
return done;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* First-paint race: the cell node can attach a tick before the loading
|
|
293
|
+
* indicator mounts. A single false-negative then reads "" and skips
|
|
294
|
+
* onCellLoadingTimeout. Only grace when the cell still looks empty.
|
|
295
|
+
*/
|
|
296
|
+
async function graceForLoadingIndicator(page, targetCell, isLoading, timeout) {
|
|
297
|
+
const peek = await targetCell.evaluate((el) => (el.textContent || '').trim()).catch(() => '');
|
|
298
|
+
if (peek !== '')
|
|
299
|
+
return false;
|
|
300
|
+
return pollUntil(page, Date.now() + Math.min(250, timeout), 25, isLoading, false);
|
|
301
|
+
}
|
|
302
|
+
/** What to do when a cell is still loading after `cellLoadingTimeout`. */
|
|
303
|
+
async function onStillLoading(env, targetCell, col, timeout) {
|
|
304
|
+
var _a;
|
|
305
|
+
var _b;
|
|
306
|
+
const { config, row: smart } = env;
|
|
307
|
+
const onCellLoadingTimeout = (_b = (_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.onCellLoadingTimeout) !== null && _b !== void 0 ? _b : 'read-as-is';
|
|
308
|
+
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — still loading after ${timeout}ms, action: ${typeof onCellLoadingTimeout === 'function' ? 'callback' : onCellLoadingTimeout}`);
|
|
309
|
+
if (typeof onCellLoadingTimeout === 'function') {
|
|
310
|
+
return { read: false, value: await onCellLoadingTimeout(targetCell, col, smart) };
|
|
311
|
+
}
|
|
312
|
+
if (onCellLoadingTimeout === 'skip')
|
|
313
|
+
return { read: false, value: '' };
|
|
314
|
+
if (onCellLoadingTimeout === 'throw') {
|
|
315
|
+
throw new Error(`[SmartTable] Cell "${col}" (row ${env.rowIndex}) did not finish loading within ${timeout}ms`);
|
|
316
|
+
}
|
|
317
|
+
// 'read-as-is': fall through to normal read
|
|
318
|
+
return { read: true };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Applies `loading.isCellLoading` / `cellLoadingTimeout` / `onCellLoadingTimeout` to one cell.
|
|
322
|
+
* Returns `{ read: true }` to read the cell normally, or the value to use instead.
|
|
323
|
+
*/
|
|
324
|
+
async function waitForCellLoaded(env, targetCell, col) {
|
|
325
|
+
var _a;
|
|
326
|
+
const { config, row: smart } = env;
|
|
327
|
+
const isCellLoading = (_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isCellLoading;
|
|
328
|
+
if (!isCellLoading)
|
|
329
|
+
return { read: true };
|
|
330
|
+
const page = env.rootLocator.page();
|
|
331
|
+
const isLoading = () => isCellLoading(targetCell, col, smart);
|
|
332
|
+
const timeout = cellLoadingTimeoutOf(config);
|
|
333
|
+
let loading = await isLoading();
|
|
334
|
+
if (!loading && timeout !== undefined && timeout > 0) {
|
|
335
|
+
loading = await graceForLoadingIndicator(page, targetCell, isLoading, timeout);
|
|
336
|
+
}
|
|
337
|
+
if (!loading || timeout === undefined)
|
|
338
|
+
return { read: true };
|
|
339
|
+
(0, debugUtils_1.logDebug)(config, 'verbose', `toJSON: cell "${col}" — waiting up to ${timeout}ms`);
|
|
340
|
+
const deadline = Date.now() + timeout;
|
|
341
|
+
const isLoaded = async () => !(await isLoading());
|
|
342
|
+
const cellResolved = await pollUntil(page, deadline, 100, isLoaded, await isLoaded()); // immediate check (handles timeout=0)
|
|
343
|
+
return cellResolved ? { read: true } : onStillLoading(env, targetCell, col, timeout);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Default `toJSON()`: reads each column from the live DOM, navigating to it first
|
|
347
|
+
* (keeps synchronized batches in lock-step) and re-pinning the row before each read.
|
|
348
|
+
*/
|
|
349
|
+
async function readRowLive(env, columns) {
|
|
350
|
+
var _a, _b, _c;
|
|
351
|
+
env = await resolveSnapshotIdentity(env);
|
|
352
|
+
const { config, map, rootLocator, rowIndex } = env;
|
|
353
|
+
const result = {};
|
|
354
|
+
const page = rootLocator.page();
|
|
355
|
+
const getHeaderCell = headerCellGetter(env);
|
|
356
|
+
const pinner = createRowPinner(env);
|
|
357
|
+
// getCell for column overrides: a live cell Locator on the last-known-good row,
|
|
358
|
+
// as fresh as possible, but raw (not override-processed).
|
|
359
|
+
const getCellLive = (colName) => {
|
|
360
|
+
const colIdx = map.get(colName);
|
|
361
|
+
if (colIdx === undefined)
|
|
362
|
+
throw new Error(`[SmartTable] getCell: column "${colName}" not found`);
|
|
363
|
+
return cellIn(env, pinner.current(), colName, colIdx);
|
|
364
|
+
};
|
|
365
|
+
for (const [col, idx] of map.entries()) {
|
|
366
|
+
if (columns && !columns.includes(col))
|
|
367
|
+
continue;
|
|
368
|
+
// Re-pin to the correct logical row before reading this column (#366).
|
|
369
|
+
const stableRow = await pinner.resolveStableRow();
|
|
370
|
+
const mapper = (_b = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[col]) === null || _b === void 0 ? void 0 : _b.read;
|
|
371
|
+
const cell = cellIn(env, stableRow, col, idx);
|
|
372
|
+
// Always navigate to keep Lock-Step synchronization even if the cell is
|
|
373
|
+
// already present (navigateToCell handles the barrier check-in).
|
|
374
|
+
const navigatedCell = await (0, cellNavigation_1.navigateToCell)({
|
|
375
|
+
config,
|
|
376
|
+
rootLocator,
|
|
377
|
+
page,
|
|
378
|
+
resolve: env.resolve,
|
|
379
|
+
getHeaders: (_c = env.table) === null || _c === void 0 ? void 0 : _c.getHeaders,
|
|
380
|
+
column: col,
|
|
381
|
+
index: idx,
|
|
382
|
+
rowLocator: stableRow,
|
|
383
|
+
rowIndex,
|
|
384
|
+
renderWindowPosition: env.renderWindowPosition,
|
|
385
|
+
barrier: env.row._barrier,
|
|
386
|
+
allowMissingCell: !!mapper,
|
|
387
|
+
});
|
|
388
|
+
const targetCell = navigatedCell !== null && navigatedCell !== void 0 ? navigatedCell : cell;
|
|
389
|
+
// Fires for BOTH columnOverrides.read and the default innerText path.
|
|
390
|
+
if (config.strategies.beforeCellRead) {
|
|
391
|
+
await config.strategies.beforeCellRead({
|
|
392
|
+
cell: targetCell,
|
|
393
|
+
columnName: col,
|
|
394
|
+
columnIndex: idx,
|
|
395
|
+
row: stableRow,
|
|
396
|
+
page,
|
|
397
|
+
root: rootLocator,
|
|
398
|
+
getHeaderCell,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const loadOutcome = await waitForCellLoaded(env, targetCell, col);
|
|
402
|
+
if (!loadOutcome.read) {
|
|
403
|
+
result[col] = loadOutcome.value;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (mapper) {
|
|
407
|
+
result[col] = await mapper(targetCell, { row: env.row, columnName: col, columnIndex: idx, getCell: getCellLive });
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const text = await targetCell.innerText();
|
|
411
|
+
result[col] = (text || '').trim();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
await computeSyntheticColumns(env, result, columns);
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
@@ -21,10 +21,10 @@ import type { FinalTableConfig } from '../types';
|
|
|
21
21
|
* | findRow | yes | options.useBulkPagination | yes | DOM/post filters only |
|
|
22
22
|
* | countRows | yes | no (single-step) | yes | count-only / post filters |
|
|
23
23
|
* | async iterator | yes | no | via runMap path | same as map |
|
|
24
|
-
* | findRowByIndex | no
|
|
24
|
+
* | findRowByIndex | no | no | no | stops once the target index is mounted* |
|
|
25
25
|
* | bringIntoView | n/a | path planner | n/a | not a page scan |
|
|
26
26
|
*
|
|
27
|
-
* \* findRowByIndex
|
|
27
|
+
* \* findRowByIndex only falls back to scanPages after the mounted / viewport.scrollToRow paths miss.
|
|
28
28
|
*/
|
|
29
29
|
export type ScanDecision = 'continue' | 'stop';
|
|
30
30
|
export interface ScanPageContext {
|
|
@@ -52,6 +52,11 @@ export interface ScanPagesOptions {
|
|
|
52
52
|
* Matches runMap / findRows EOF behavior. @default true
|
|
53
53
|
*/
|
|
54
54
|
finalScanOnEof?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Checked before each page and before each advance. When aborted, the scan throws
|
|
57
|
+
* `signal.reason` instead of returning partial results (#488).
|
|
58
|
+
*/
|
|
59
|
+
signal?: AbortSignal;
|
|
55
60
|
}
|
|
56
61
|
export declare function scanPages(env: ScanPagesEnv, onPage: (ctx: ScanPageContext) => Promise<ScanDecision>, options: ScanPagesOptions): Promise<{
|
|
57
62
|
pagesScanned: number;
|
package/dist/engine/scanPages.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scanPages = scanPages;
|
|
4
4
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
5
|
+
function throwIfAborted(env, label, signal) {
|
|
6
|
+
if (!(signal === null || signal === void 0 ? void 0 : signal.aborted))
|
|
7
|
+
return;
|
|
8
|
+
(0, debugUtils_1.logDebug)(env.config, 'info', `${label}: aborted via AbortSignal at page ${env.getCurrentPageIndex()}`);
|
|
9
|
+
throw signal.reason;
|
|
10
|
+
}
|
|
5
11
|
async function scanPages(env, onPage, options) {
|
|
6
12
|
var _a, _b;
|
|
7
13
|
const maxPages = options.maxPages;
|
|
@@ -12,14 +18,17 @@ async function scanPages(env, onPage, options) {
|
|
|
12
18
|
let reachedEnd = false;
|
|
13
19
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: starting (maxPages=${maxPages}, useBulk=${useBulk}, finalScanOnEof=${finalScanOnEof})`);
|
|
14
20
|
while (true) {
|
|
21
|
+
throwIfAborted(env, label, options.signal);
|
|
15
22
|
if (env.waitForReady) {
|
|
16
23
|
await env.waitForReady();
|
|
24
|
+
throwIfAborted(env, label, options.signal);
|
|
17
25
|
}
|
|
18
26
|
const decision = await onPage({
|
|
19
27
|
pagesScanned,
|
|
20
28
|
pageIndex: env.getCurrentPageIndex(),
|
|
21
29
|
isFinalScan: reachedEnd,
|
|
22
30
|
});
|
|
31
|
+
throwIfAborted(env, label, options.signal);
|
|
23
32
|
if (decision === 'stop') {
|
|
24
33
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: onPage returned stop at page ${env.getCurrentPageIndex()}`);
|
|
25
34
|
break;
|
|
@@ -27,8 +36,10 @@ async function scanPages(env, onPage, options) {
|
|
|
27
36
|
if (pagesScanned >= maxPages || reachedEnd) {
|
|
28
37
|
break;
|
|
29
38
|
}
|
|
39
|
+
throwIfAborted(env, label, options.signal);
|
|
30
40
|
const prevPage = env.getCurrentPageIndex();
|
|
31
41
|
const didAdvance = await env.advancePage(useBulk);
|
|
42
|
+
throwIfAborted(env, label, options.signal);
|
|
32
43
|
if (!didAdvance) {
|
|
33
44
|
if (finalScanOnEof) {
|
|
34
45
|
(0, debugUtils_1.logDebug)(env.config, 'verbose', `${label}: pagination returned false — final scan`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
import type { FinalTableConfig, StrategyContext } from '../types';
|
|
3
|
+
export interface SortingEnv<T = any> {
|
|
4
|
+
config: FinalTableConfig<T>;
|
|
5
|
+
getPage: () => Page;
|
|
6
|
+
/** Strategy context including `getHeaderCell`. */
|
|
7
|
+
getContext: () => StrategyContext;
|
|
8
|
+
}
|
|
9
|
+
/** Drives the sorting strategy until `columnName` reports `direction`, retrying up to 3 times. */
|
|
10
|
+
export declare function applySort<T>(env: SortingEnv<T>, columnName: string, direction: 'asc' | 'desc'): Promise<void>;
|
|
11
|
+
export declare function getSortState<T>(env: SortingEnv<T>, columnName: string): Promise<'asc' | 'desc' | 'none'>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applySort = applySort;
|
|
4
|
+
exports.getSortState = getSortState;
|
|
5
|
+
const debugUtils_1 = require("../utils/debugUtils");
|
|
6
|
+
const loadingWait_1 = require("../utils/loadingWait");
|
|
7
|
+
const MAX_SORT_ATTEMPTS = 3;
|
|
8
|
+
function requireSorting(config) {
|
|
9
|
+
if (!config.strategies.sorting)
|
|
10
|
+
throw new Error('No sorting strategy has been configured.');
|
|
11
|
+
return config.strategies.sorting;
|
|
12
|
+
}
|
|
13
|
+
/** Waits for the table to settle after `doSort`: `isTableLoading` when configured, otherwise a fixed delay. */
|
|
14
|
+
async function waitForSortToSettle(env, context) {
|
|
15
|
+
var _a, _b, _c, _d;
|
|
16
|
+
const loading = env.config.strategies.loading;
|
|
17
|
+
if (loading === null || loading === void 0 ? void 0 : loading.isTableLoading) {
|
|
18
|
+
const timeout = (_b = (_a = loading.sortStabilizationTimeout) !== null && _a !== void 0 ? _a : loading.loadingTimeout) !== null && _b !== void 0 ? _b : 10000;
|
|
19
|
+
const pollInterval = (_c = loading.sortStabilizationPollInterval) !== null && _c !== void 0 ? _c : 100;
|
|
20
|
+
await (0, loadingWait_1.waitWhileTableLoading)(env.config, context, env.getPage(), 'sorting.apply', { timeout, pollInterval });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
await env.getPage().waitForTimeout((_d = loading === null || loading === void 0 ? void 0 : loading.sortStabilizationFallbackDelay) !== null && _d !== void 0 ? _d : 200);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Drives the sorting strategy until `columnName` reports `direction`, retrying up to 3 times. */
|
|
27
|
+
async function applySort(env, columnName, direction) {
|
|
28
|
+
const sorting = requireSorting(env.config);
|
|
29
|
+
(0, debugUtils_1.logDebug)(env.config, 'verbose', `Applying sort for column "${columnName}" (${direction})`);
|
|
30
|
+
const context = env.getContext();
|
|
31
|
+
for (let i = 0; i < MAX_SORT_ATTEMPTS; i++) {
|
|
32
|
+
const currentState = await sorting.getSortState({ columnName, context });
|
|
33
|
+
if (currentState === direction) {
|
|
34
|
+
(0, debugUtils_1.logDebug)(env.config, 'verbose', `Sort for "${columnName}" is already "${direction}".`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await sorting.doSort({ columnName, direction, context });
|
|
38
|
+
await waitForSortToSettle(env, context);
|
|
39
|
+
await (0, debugUtils_1.debugDelay)(env.config, 'default');
|
|
40
|
+
const newState = await sorting.getSortState({ columnName, context });
|
|
41
|
+
if (newState === direction) {
|
|
42
|
+
(0, debugUtils_1.logDebug)(env.config, 'verbose', `Successfully sorted "${columnName}" to "${direction}".`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Failed to sort column "${columnName}" to "${direction}" after ${MAX_SORT_ATTEMPTS} attempts.`);
|
|
47
|
+
}
|
|
48
|
+
async function getSortState(env, columnName) {
|
|
49
|
+
const sorting = requireSorting(env.config);
|
|
50
|
+
return sorting.getSortState({ columnName, context: env.getContext() });
|
|
51
|
+
}
|
|
@@ -196,6 +196,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
|
|
|
196
196
|
useBulk,
|
|
197
197
|
label,
|
|
198
198
|
finalScanOnEof: true,
|
|
199
|
+
signal: options.signal,
|
|
199
200
|
});
|
|
200
201
|
log(env.config, `${label}: complete — ${results.length} result(s) across ${pagesScanned} page(s)`);
|
|
201
202
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { PLAYWRIGHT_SMART_TABLE_VERSION } from './packageVersion';
|
|
2
2
|
export { useTable } from './useTable';
|
|
3
|
-
export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, TableSelector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, TableStrategies, LoadingStrategy, ViewportStrategy, FilterStrategy, NavigationPrimitives, DedupeStrategy, ContentReadyStrategy, FillStrategy, PaginationStrategy, HeaderStrategy, } from './types';
|
|
3
|
+
export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, TableSelector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, SyntheticColumnDef, ColumnOverride, ColumnOverrideReadContext, TableStrategies, LoadingStrategy, ViewportStrategy, FilterStrategy, NavigationPrimitives, DedupeStrategy, ContentReadyStrategy, RowReadyPredicate, FillStrategy, PaginationStrategy, HeaderStrategy, } from './types';
|
|
4
4
|
export { Strategies } from './strategies';
|
|
5
5
|
export * as presets from './presets';
|
|
6
6
|
/**
|
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.25.0-next.
|
|
6
|
+
export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.25.0-next.f72ee4c";
|