@rickcedwhat/playwright-smart-table 6.7.8 → 6.8.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/README.md +13 -9
- package/dist/engine/tableIteration.d.ts +7 -9
- package/dist/engine/tableIteration.js +89 -191
- package/dist/engine/tableMapper.js +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/presets/glide/columns.d.ts +4 -1
- package/dist/presets/glide/columns.js +29 -18
- package/dist/presets/glide/index.d.ts +2 -0
- package/dist/presets/glide/index.js +26 -3
- package/dist/smartRow.d.ts +2 -1
- package/dist/smartRow.js +191 -63
- package/dist/strategies/columns.d.ts +11 -0
- package/dist/strategies/fill.js +1 -1
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +34 -7
- package/dist/types.d.ts +33 -7
- package/dist/useTable.js +8 -6
- package/dist/utils/mutex.d.ts +8 -0
- package/dist/utils/mutex.js +31 -0
- package/dist/utils/navigationBarrier.d.ts +21 -0
- package/dist/utils/navigationBarrier.js +82 -0
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -40,10 +40,10 @@ const email = await row.locator('td').nth(emailIndex).textContent();
|
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
// ✅ Column-aware - survives column reordering
|
|
43
|
-
const row =
|
|
43
|
+
const row = table.getRow({ Name: 'John Doe' });
|
|
44
44
|
const email = await row.getCell('Email').textContent();
|
|
45
45
|
|
|
46
|
-
// ✅ Auto-pagination
|
|
46
|
+
// ✅ Auto-pagination across all pages
|
|
47
47
|
const allEngineers = await table.findRows({ Department: 'Engineering' });
|
|
48
48
|
|
|
49
49
|
// ✅ Type-safe
|
|
@@ -66,8 +66,8 @@ import { useTable } from '@rickcedwhat/playwright-smart-table';
|
|
|
66
66
|
|
|
67
67
|
const table = await useTable(page.locator('#my-table')).init();
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
const row =
|
|
69
|
+
// Get row by column values (current page)
|
|
70
|
+
const row = table.getRow({ Name: 'John Doe' });
|
|
71
71
|
|
|
72
72
|
// Access cells by column name
|
|
73
73
|
const email = await row.getCell('Email').innerText();
|
|
@@ -79,16 +79,16 @@ const allActive = await table.findRows({ Status: 'Active' });
|
|
|
79
79
|
### Iterating Across Pages
|
|
80
80
|
|
|
81
81
|
```typescript
|
|
82
|
-
// forEach — sequential
|
|
82
|
+
// forEach — sequential by default (concurrency: 'sequential') — safe for interactions
|
|
83
83
|
await table.forEach(async ({ row, rowIndex, stop }) => {
|
|
84
84
|
if (await row.getCell('Status').innerText() === 'Done') stop();
|
|
85
85
|
await row.getCell('Checkbox').click();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
// map — parallel
|
|
88
|
+
// map — parallel by default (concurrency: 'parallel') — safe for reads
|
|
89
89
|
const emails = await table.map(({ row }) => row.getCell('Email').innerText());
|
|
90
90
|
|
|
91
|
-
// filter —
|
|
91
|
+
// filter — sequential by default; returns SmartRowArray
|
|
92
92
|
const active = await table.filter(async ({ row }) =>
|
|
93
93
|
await row.getCell('Status').innerText() === 'Active'
|
|
94
94
|
);
|
|
@@ -99,10 +99,14 @@ for await (const { row, rowIndex } of table) {
|
|
|
99
99
|
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
Set a default for all iteration calls with `useTable(..., { concurrency: 'sequential' })`, or per call: `table.map(fn, { concurrency: 'synchronized' })`. Modes: **`parallel`** (full parallelism), **`sequential`** (strictly one row at a time), **`synchronized`** (parallel navigation with serialized callbacks — useful for virtualized grids).
|
|
103
|
+
|
|
102
104
|
When your pagination strategy supports bulk jumps (`goNextBulk`), pass `{ useBulkPagination: true }` to `map`/`forEach`/`filter` to advance by multiple pages at once.
|
|
103
105
|
|
|
104
|
-
> **`map` + UI interactions:** `map` defaults to `
|
|
105
|
-
> fills inputs, or otherwise mutates UI state, pass `{
|
|
106
|
+
> **`map` + UI interactions:** `map` defaults to `concurrency: 'parallel'`. If your callback opens popovers,
|
|
107
|
+
> fills inputs, or otherwise mutates UI state, pass `{ concurrency: 'sequential' }` (or `'synchronized'` if you need lock-step navigation with serialized work).
|
|
108
|
+
|
|
109
|
+
The legacy `{ parallel: true | false }` option still works but is deprecated; prefer `concurrency`.
|
|
106
110
|
|
|
107
111
|
### `filter` vs `findRows`
|
|
108
112
|
|
|
@@ -2,24 +2,22 @@ import type { Locator, Page } from '@playwright/test';
|
|
|
2
2
|
import type { SmartRow, RowIterationContext, RowIterationOptions } from '../types';
|
|
3
3
|
import type { FinalTableConfig } from '../types';
|
|
4
4
|
import type { SmartRowArray } from '../utils/smartRowArray';
|
|
5
|
+
import { NavigationBarrier } from '../utils/navigationBarrier';
|
|
5
6
|
export interface TableIterationEnv<T = any> {
|
|
6
7
|
getRowLocators: () => Locator;
|
|
7
8
|
getMap: () => Map<string, number>;
|
|
8
9
|
advancePage: (useBulk: boolean) => Promise<boolean>;
|
|
9
|
-
makeSmartRow: (rowLocator: Locator, map: Map<string, number>, rowIndex: number, tablePageIndex?: number) => SmartRow<T>;
|
|
10
|
+
makeSmartRow: (rowLocator: Locator, map: Map<string, number>, rowIndex: number, tablePageIndex?: number, barrier?: NavigationBarrier) => SmartRow<T>;
|
|
10
11
|
createSmartRowArray: (rows: SmartRow<T>[]) => SmartRowArray<T>;
|
|
11
12
|
config: FinalTableConfig<T>;
|
|
12
13
|
getPage: () => Page;
|
|
13
14
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Shared row-iteration loop used by forEach, map, and filter.
|
|
16
|
-
*/
|
|
15
|
+
/** Delegates to {@link runMap}; void results are discarded. */
|
|
17
16
|
export declare function runForEach<T>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => void | Promise<void>, options?: RowIterationOptions): Promise<void>;
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
export declare function runMap<T, R>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => R | Promise<R>, options?: RowIterationOptions): Promise<R[]>;
|
|
22
|
-
/**
|
|
23
|
-
* Shared row-iteration loop for filter.
|
|
18
|
+
* Row iteration for map (and forEach/filter via label).
|
|
19
|
+
* Concurrency: `parallel` | `synchronized` | `sequential` (see RowIterationOptions).
|
|
24
20
|
*/
|
|
21
|
+
export declare function runMap<T, R>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => R | Promise<R>, options?: RowIterationOptions, label?: string): Promise<R[]>;
|
|
22
|
+
/** Filter via map; returns matched rows as {@link SmartRowArray}. */
|
|
25
23
|
export declare function runFilter<T>(env: TableIterationEnv<T>, predicate: (ctx: RowIterationContext<T>) => boolean | Promise<boolean>, options?: RowIterationOptions): Promise<SmartRowArray<T>>;
|
|
@@ -14,160 +14,126 @@ exports.runMap = runMap;
|
|
|
14
14
|
exports.runFilter = runFilter;
|
|
15
15
|
const elementTracker_1 = require("../utils/elementTracker");
|
|
16
16
|
const debugUtils_1 = require("../utils/debugUtils");
|
|
17
|
+
const navigationBarrier_1 = require("../utils/navigationBarrier");
|
|
18
|
+
const mutex_1 = require("../utils/mutex");
|
|
17
19
|
function log(config, msg) {
|
|
18
20
|
(0, debugUtils_1.logDebug)(config, 'verbose', msg);
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
22
|
+
const SKIP = Symbol('skip');
|
|
23
|
+
/** Delegates to {@link runMap}; void results are discarded. */
|
|
23
24
|
function runForEach(env_1, callback_1) {
|
|
24
25
|
return __awaiter(this, arguments, void 0, function* (env, callback, options = {}) {
|
|
25
|
-
|
|
26
|
-
const map = env.getMap();
|
|
27
|
-
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : env.config.maxPages;
|
|
28
|
-
const dedupeStrategy = (_b = options.dedupe) !== null && _b !== void 0 ? _b : env.config.strategies.dedupe;
|
|
29
|
-
const dedupeKeys = dedupeStrategy ? new Set() : null;
|
|
30
|
-
const parallel = (_c = options.parallel) !== null && _c !== void 0 ? _c : false;
|
|
31
|
-
const useBulk = (_d = options.useBulkPagination) !== null && _d !== void 0 ? _d : false;
|
|
32
|
-
const tracker = new elementTracker_1.ElementTracker('forEach');
|
|
33
|
-
log(env.config, `forEach: starting (maxPages=${effectiveMaxPages}, parallel=${parallel}, dedupe=${!!dedupeStrategy})`);
|
|
34
|
-
try {
|
|
35
|
-
let rowIndex = 0;
|
|
36
|
-
let stopped = false;
|
|
37
|
-
let pagesScanned = 1;
|
|
38
|
-
let totalProcessed = 0;
|
|
39
|
-
const stop = () => {
|
|
40
|
-
log(env.config, `forEach: stop() called — halting after current page`);
|
|
41
|
-
stopped = true;
|
|
42
|
-
};
|
|
43
|
-
while (!stopped) {
|
|
44
|
-
const rowLocators = env.getRowLocators();
|
|
45
|
-
const newIndices = yield tracker.getUnseenIndices(rowLocators);
|
|
46
|
-
const pageRows = yield rowLocators.all();
|
|
47
|
-
const smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i));
|
|
48
|
-
log(env.config, `forEach: page ${pagesScanned} — ${newIndices.length} new row(s) found`);
|
|
49
|
-
if (parallel) {
|
|
50
|
-
yield Promise.all(smartRows.map((row) => __awaiter(this, void 0, void 0, function* () {
|
|
51
|
-
if (stopped)
|
|
52
|
-
return;
|
|
53
|
-
if (dedupeKeys && dedupeStrategy) {
|
|
54
|
-
const key = yield dedupeStrategy(row);
|
|
55
|
-
if (dedupeKeys.has(key)) {
|
|
56
|
-
log(env.config, `forEach: dedupe skip key="${key}"`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
dedupeKeys.add(key);
|
|
60
|
-
}
|
|
61
|
-
yield callback({ row, rowIndex: row.rowIndex, stop });
|
|
62
|
-
totalProcessed++;
|
|
63
|
-
})));
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
for (const row of smartRows) {
|
|
67
|
-
if (stopped)
|
|
68
|
-
break;
|
|
69
|
-
if (dedupeKeys && dedupeStrategy) {
|
|
70
|
-
const key = yield dedupeStrategy(row);
|
|
71
|
-
if (dedupeKeys.has(key)) {
|
|
72
|
-
log(env.config, `forEach: dedupe skip key="${key}"`);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
dedupeKeys.add(key);
|
|
76
|
-
}
|
|
77
|
-
yield callback({ row, rowIndex: row.rowIndex, stop });
|
|
78
|
-
totalProcessed++;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
rowIndex += smartRows.length;
|
|
82
|
-
if (stopped || pagesScanned >= effectiveMaxPages)
|
|
83
|
-
break;
|
|
84
|
-
log(env.config, `forEach: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
|
|
85
|
-
if (!(yield env.advancePage(useBulk))) {
|
|
86
|
-
log(env.config, `forEach: no more pages — done`);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
pagesScanned++;
|
|
90
|
-
}
|
|
91
|
-
log(env.config, `forEach: complete — ${totalProcessed} row(s) processed across ${pagesScanned} page(s)`);
|
|
92
|
-
}
|
|
93
|
-
finally {
|
|
94
|
-
yield tracker.cleanup(env.getPage());
|
|
95
|
-
}
|
|
26
|
+
yield runMap(env, callback, options, 'forEach');
|
|
96
27
|
});
|
|
97
28
|
}
|
|
98
29
|
/**
|
|
99
|
-
*
|
|
30
|
+
* Row iteration for map (and forEach/filter via label).
|
|
31
|
+
* Concurrency: `parallel` | `synchronized` | `sequential` (see RowIterationOptions).
|
|
100
32
|
*/
|
|
101
33
|
function runMap(env_1, callback_1) {
|
|
102
|
-
return __awaiter(this, arguments, void 0, function* (env, callback, options = {}) {
|
|
103
|
-
var _a, _b, _c
|
|
34
|
+
return __awaiter(this, arguments, void 0, function* (env, callback, options = {}, label = 'map') {
|
|
35
|
+
var _a, _b, _c;
|
|
104
36
|
const map = env.getMap();
|
|
105
37
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : env.config.maxPages;
|
|
106
38
|
const dedupeStrategy = (_b = options.dedupe) !== null && _b !== void 0 ? _b : env.config.strategies.dedupe;
|
|
107
39
|
const dedupeKeys = dedupeStrategy ? new Set() : null;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
40
|
+
const defaultMode = label === 'map' ? 'parallel' : 'sequential';
|
|
41
|
+
const concurrency = options.concurrency ||
|
|
42
|
+
(options.parallel === true ? 'parallel' :
|
|
43
|
+
options.parallel === false ? 'sequential' :
|
|
44
|
+
env.config.concurrency || defaultMode);
|
|
45
|
+
const useBarrier = concurrency !== 'sequential';
|
|
46
|
+
const useMutex = concurrency !== 'parallel';
|
|
47
|
+
const useBulk = (_c = options.useBulkPagination) !== null && _c !== void 0 ? _c : false;
|
|
48
|
+
const tracker = new elementTracker_1.ElementTracker(label);
|
|
49
|
+
log(env.config, `${label}: starting (maxPages=${effectiveMaxPages}, mode=${concurrency}, dedupe=${!!dedupeStrategy})`);
|
|
112
50
|
const results = [];
|
|
113
|
-
const SKIP = Symbol('skip');
|
|
114
51
|
try {
|
|
115
52
|
let rowIndex = 0;
|
|
116
53
|
let stopped = false;
|
|
54
|
+
let stoppedIndex = Infinity;
|
|
117
55
|
let pagesScanned = 1;
|
|
118
|
-
const stop = () => {
|
|
119
|
-
|
|
120
|
-
|
|
56
|
+
const stop = (idx) => {
|
|
57
|
+
if (!stopped) {
|
|
58
|
+
log(env.config, `${label}: stop() called at row ${idx} — halting`);
|
|
59
|
+
stopped = true;
|
|
60
|
+
stoppedIndex = idx;
|
|
61
|
+
}
|
|
121
62
|
};
|
|
122
63
|
while (!stopped) {
|
|
123
64
|
const rowLocators = env.getRowLocators();
|
|
124
65
|
const newIndices = yield tracker.getUnseenIndices(rowLocators);
|
|
125
66
|
const pageRows = yield rowLocators.all();
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
67
|
+
const batchSize = newIndices.length;
|
|
68
|
+
if (batchSize === 0) {
|
|
69
|
+
log(env.config, `${label}: page ${pagesScanned} — no new row(s) found`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
log(env.config, `${label}: scanning page ${pagesScanned} — ${batchSize} new row(s)`);
|
|
73
|
+
const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(batchSize) : undefined;
|
|
74
|
+
const actionMutex = useMutex ? new mutex_1.Mutex() : null;
|
|
75
|
+
const smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i, pagesScanned - 1, barrier));
|
|
76
|
+
const processRow = (row) => __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
try {
|
|
78
|
+
if (stopped && row.rowIndex > stoppedIndex) {
|
|
134
79
|
return SKIP;
|
|
135
80
|
}
|
|
136
|
-
dedupeKeys
|
|
81
|
+
if (dedupeKeys && dedupeStrategy) {
|
|
82
|
+
const key = yield dedupeStrategy(row);
|
|
83
|
+
if (dedupeKeys.has(key)) {
|
|
84
|
+
log(env.config, `${label}: dedupe skip key="${key}"`);
|
|
85
|
+
return SKIP;
|
|
86
|
+
}
|
|
87
|
+
dedupeKeys.add(key);
|
|
88
|
+
}
|
|
89
|
+
// Execute callback (optionally serialized via mutex)
|
|
90
|
+
const runCallback = () => __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
if (stopped && row.rowIndex > stoppedIndex)
|
|
92
|
+
return SKIP;
|
|
93
|
+
log(env.config, `${label}: processing row ${row.rowIndex}`);
|
|
94
|
+
return yield callback({ row, rowIndex: row.rowIndex, stop: () => stop(row.rowIndex) });
|
|
95
|
+
});
|
|
96
|
+
if (actionMutex) {
|
|
97
|
+
return yield actionMutex.run(runCallback);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return yield runCallback();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
// Ensure the barrier is notified once per row, even on error or skip.
|
|
105
|
+
barrier === null || barrier === void 0 ? void 0 : barrier.markFinished();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const pageResults = [];
|
|
109
|
+
if (concurrency === 'sequential') {
|
|
110
|
+
for (const row of smartRows) {
|
|
111
|
+
pageResults.push(yield processRow(row));
|
|
137
112
|
}
|
|
138
|
-
return callback({ row, rowIndex: row.rowIndex, stop });
|
|
139
|
-
})));
|
|
140
|
-
for (const r of pageResults) {
|
|
141
|
-
if (r !== SKIP)
|
|
142
|
-
results.push(r);
|
|
143
113
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
114
|
+
else {
|
|
115
|
+
const results = yield Promise.all(smartRows.map(processRow));
|
|
116
|
+
pageResults.push(...results);
|
|
117
|
+
}
|
|
118
|
+
for (let i = 0; i < pageResults.length; i++) {
|
|
119
|
+
if (smartRows[i].rowIndex > stoppedIndex)
|
|
148
120
|
break;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
log(env.config, `map: dedupe skip key="${key}"`);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
dedupeKeys.add(key);
|
|
156
|
-
}
|
|
157
|
-
results.push(yield callback({ row, rowIndex: row.rowIndex, stop }));
|
|
121
|
+
const r = pageResults[i];
|
|
122
|
+
if (r !== SKIP)
|
|
123
|
+
results.push(r);
|
|
158
124
|
}
|
|
125
|
+
rowIndex += batchSize;
|
|
159
126
|
}
|
|
160
|
-
rowIndex += smartRows.length;
|
|
161
127
|
if (stopped || pagesScanned >= effectiveMaxPages)
|
|
162
128
|
break;
|
|
163
|
-
log(env.config,
|
|
129
|
+
log(env.config, `${label}: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
|
|
164
130
|
if (!(yield env.advancePage(useBulk))) {
|
|
165
|
-
log(env.config,
|
|
131
|
+
log(env.config, `${label}: no more pages — done`);
|
|
166
132
|
break;
|
|
167
133
|
}
|
|
168
134
|
pagesScanned++;
|
|
169
135
|
}
|
|
170
|
-
log(env.config,
|
|
136
|
+
log(env.config, `${label}: complete — ${results.length} result(s) across ${pagesScanned} page(s)`);
|
|
171
137
|
}
|
|
172
138
|
finally {
|
|
173
139
|
yield tracker.cleanup(env.getPage());
|
|
@@ -175,82 +141,14 @@ function runMap(env_1, callback_1) {
|
|
|
175
141
|
return results;
|
|
176
142
|
});
|
|
177
143
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Shared row-iteration loop for filter.
|
|
180
|
-
*/
|
|
144
|
+
/** Filter via map; returns matched rows as {@link SmartRowArray}. */
|
|
181
145
|
function runFilter(env_1, predicate_1) {
|
|
182
146
|
return __awaiter(this, arguments, void 0, function* (env, predicate, options = {}) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
const useBulk = (_d = options.useBulkPagination) !== null && _d !== void 0 ? _d : false;
|
|
190
|
-
const tracker = new elementTracker_1.ElementTracker('filter');
|
|
191
|
-
log(env.config, `filter: starting (maxPages=${effectiveMaxPages}, parallel=${parallel}, dedupe=${!!dedupeStrategy})`);
|
|
192
|
-
const matched = [];
|
|
193
|
-
try {
|
|
194
|
-
let rowIndex = 0;
|
|
195
|
-
let stopped = false;
|
|
196
|
-
let pagesScanned = 1;
|
|
197
|
-
const stop = () => {
|
|
198
|
-
log(env.config, `filter: stop() called — halting after current page`);
|
|
199
|
-
stopped = true;
|
|
200
|
-
};
|
|
201
|
-
while (!stopped) {
|
|
202
|
-
const rowLocators = env.getRowLocators();
|
|
203
|
-
const newIndices = yield tracker.getUnseenIndices(rowLocators);
|
|
204
|
-
const pageRows = yield rowLocators.all();
|
|
205
|
-
const smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i, pagesScanned - 1));
|
|
206
|
-
log(env.config, `filter: scanning page ${pagesScanned} — ${newIndices.length} new row(s)`);
|
|
207
|
-
if (parallel) {
|
|
208
|
-
const flags = yield Promise.all(smartRows.map((row) => __awaiter(this, void 0, void 0, function* () {
|
|
209
|
-
if (dedupeKeys && dedupeStrategy) {
|
|
210
|
-
const key = yield dedupeStrategy(row);
|
|
211
|
-
if (dedupeKeys.has(key)) {
|
|
212
|
-
log(env.config, `filter: dedupe skip key="${key}"`);
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
dedupeKeys.add(key);
|
|
216
|
-
}
|
|
217
|
-
return predicate({ row, rowIndex: row.rowIndex, stop });
|
|
218
|
-
})));
|
|
219
|
-
smartRows.forEach((row, i) => { if (flags[i])
|
|
220
|
-
matched.push(row); });
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
for (const row of smartRows) {
|
|
224
|
-
if (stopped)
|
|
225
|
-
break;
|
|
226
|
-
if (dedupeKeys && dedupeStrategy) {
|
|
227
|
-
const key = yield dedupeStrategy(row);
|
|
228
|
-
if (dedupeKeys.has(key)) {
|
|
229
|
-
log(env.config, `filter: dedupe skip key="${key}"`);
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
dedupeKeys.add(key);
|
|
233
|
-
}
|
|
234
|
-
if (yield predicate({ row, rowIndex: row.rowIndex, stop })) {
|
|
235
|
-
matched.push(row);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
rowIndex += smartRows.length;
|
|
240
|
-
if (stopped || pagesScanned >= effectiveMaxPages)
|
|
241
|
-
break;
|
|
242
|
-
log(env.config, `filter: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
|
|
243
|
-
if (!(yield env.advancePage(useBulk))) {
|
|
244
|
-
log(env.config, `filter: no more pages — done`);
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
pagesScanned++;
|
|
248
|
-
}
|
|
249
|
-
log(env.config, `filter: complete — ${matched.length} match(es) across ${pagesScanned} page(s)`);
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
yield tracker.cleanup(env.getPage());
|
|
253
|
-
}
|
|
254
|
-
return env.createSmartRowArray(matched);
|
|
147
|
+
const mapCallback = (ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
const matched = yield predicate(ctx);
|
|
149
|
+
return matched ? ctx.row : SKIP;
|
|
150
|
+
});
|
|
151
|
+
const results = yield runMap(env, mapCallback, options, 'filter');
|
|
152
|
+
return env.createSmartRowArray(results);
|
|
255
153
|
});
|
|
256
154
|
}
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const Plugins: {
|
|
|
12
12
|
rowSelector?: string | undefined;
|
|
13
13
|
cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
|
|
14
14
|
maxPages?: number | undefined;
|
|
15
|
+
concurrency?: import("../types").RowIterationMode | undefined;
|
|
15
16
|
headerTransformer?: ((args: {
|
|
16
17
|
text: string;
|
|
17
18
|
index: number;
|
|
@@ -31,6 +32,7 @@ export declare const Plugins: {
|
|
|
31
32
|
rowSelector?: string | undefined;
|
|
32
33
|
cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
|
|
33
34
|
maxPages?: number | undefined;
|
|
35
|
+
concurrency?: import("../types").RowIterationMode | undefined;
|
|
34
36
|
headerTransformer?: ((args: {
|
|
35
37
|
text: string;
|
|
36
38
|
index: number;
|
|
@@ -50,6 +52,7 @@ export declare const Plugins: {
|
|
|
50
52
|
rowSelector?: string | undefined;
|
|
51
53
|
cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
|
|
52
54
|
maxPages?: number | undefined;
|
|
55
|
+
concurrency?: import("../types").RowIterationMode | undefined;
|
|
53
56
|
headerTransformer?: ((args: {
|
|
54
57
|
text: string;
|
|
55
58
|
index: number;
|
|
@@ -8,4 +8,7 @@ export declare const glideGoUp: (context: StrategyContext) => Promise<void>;
|
|
|
8
8
|
export declare const glideGoDown: (context: StrategyContext) => Promise<void>;
|
|
9
9
|
export declare const glideGoLeft: (context: StrategyContext) => Promise<void>;
|
|
10
10
|
export declare const glideGoRight: (context: StrategyContext) => Promise<void>;
|
|
11
|
-
|
|
11
|
+
/** Coarse `scrollLeft` toward `columnIndex`; `goLeft`/`goRight` correct the remainder. */
|
|
12
|
+
export declare const glideSeekColumnIndex: (context: StrategyContext, columnIndex: number) => Promise<void>;
|
|
13
|
+
/** `scrollLeft = 0` so low `aria-colindex` cells exist again (see smartRow for optional `Home`). */
|
|
14
|
+
export declare const glideSnapFirstColumnIntoView: (context: StrategyContext) => Promise<void>;
|
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.
|
|
12
|
+
exports.glideSnapFirstColumnIntoView = exports.glideSeekColumnIndex = exports.glideGoRight = exports.glideGoLeft = exports.glideGoDown = exports.glideGoUp = void 0;
|
|
13
13
|
/**
|
|
14
14
|
* Primitive navigation functions for Glide Data Grid.
|
|
15
15
|
* These define HOW to move, not WHEN to move.
|
|
@@ -23,29 +23,40 @@ const glideGoDown = (context) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
23
23
|
yield context.page.keyboard.press('ArrowDown');
|
|
24
24
|
});
|
|
25
25
|
exports.glideGoDown = glideGoDown;
|
|
26
|
+
// Horizontal: use page `.dvn-scroller` (canvas root is not its ancestor). Virtualized rows expose
|
|
27
|
+
// a small window of `td[aria-colindex]` that shifts with scrollLeft.
|
|
28
|
+
const nudgePageScroller = (page, delta) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
const scroller = page.locator('.dvn-scroller').first();
|
|
30
|
+
yield scroller.evaluate((el, d) => {
|
|
31
|
+
el.scrollLeft += d;
|
|
32
|
+
}, delta);
|
|
33
|
+
});
|
|
26
34
|
const glideGoLeft = (context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
27
|
-
yield context.page
|
|
35
|
+
yield nudgePageScroller(context.page, -400);
|
|
28
36
|
});
|
|
29
37
|
exports.glideGoLeft = glideGoLeft;
|
|
30
38
|
const glideGoRight = (context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
-
yield context.page
|
|
39
|
+
yield nudgePageScroller(context.page, 400);
|
|
32
40
|
});
|
|
33
41
|
exports.glideGoRight = glideGoRight;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
});
|
|
42
|
+
/** Coarse `scrollLeft` toward `columnIndex`; `goLeft`/`goRight` correct the remainder. */
|
|
43
|
+
const glideSeekColumnIndex = (context, columnIndex) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
const { page } = context;
|
|
45
|
+
const scroller = page.locator('.dvn-scroller').first();
|
|
46
|
+
yield scroller.evaluate((el, colIdx) => {
|
|
47
|
+
const max = Math.max(0, el.scrollWidth - el.clientWidth);
|
|
48
|
+
const ratio = Math.min(1, Math.max(0, (colIdx + 0.5) / 64));
|
|
49
|
+
el.scrollLeft = Math.floor(ratio * max);
|
|
50
|
+
}, columnIndex);
|
|
45
51
|
yield page.waitForTimeout(100);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
});
|
|
53
|
+
exports.glideSeekColumnIndex = glideSeekColumnIndex;
|
|
54
|
+
/** `scrollLeft = 0` so low `aria-colindex` cells exist again (see smartRow for optional `Home`). */
|
|
55
|
+
const glideSnapFirstColumnIntoView = (context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
const { page } = context;
|
|
57
|
+
yield page.locator('.dvn-scroller').first().evaluate((el) => {
|
|
58
|
+
el.scrollLeft = 0;
|
|
59
|
+
});
|
|
49
60
|
yield page.waitForTimeout(150);
|
|
50
61
|
});
|
|
51
|
-
exports.
|
|
62
|
+
exports.glideSnapFirstColumnIntoView = glideSnapFirstColumnIntoView;
|
|
@@ -15,6 +15,8 @@ export declare const GlideStrategies: {
|
|
|
15
15
|
goLeft: (context: import("../../types").StrategyContext) => Promise<void>;
|
|
16
16
|
goRight: (context: import("../../types").StrategyContext) => Promise<void>;
|
|
17
17
|
goHome: (context: import("../../types").StrategyContext) => Promise<void>;
|
|
18
|
+
snapFirstColumnIntoView: (context: import("../../types").StrategyContext) => Promise<void>;
|
|
19
|
+
seekColumnIndex: (context: import("../../types").StrategyContext, columnIndex: number) => Promise<void>;
|
|
18
20
|
};
|
|
19
21
|
loading: {
|
|
20
22
|
isHeaderLoading: () => Promise<boolean>;
|
|
@@ -14,7 +14,25 @@ const columns_1 = require("./columns");
|
|
|
14
14
|
const headers_1 = require("./headers");
|
|
15
15
|
const pagination_1 = require("../../strategies/pagination");
|
|
16
16
|
const stabilization_1 = require("../../strategies/stabilization");
|
|
17
|
-
const glideFillStrategy = (_a) => __awaiter(void 0, [_a], void 0, function* ({ value, page }) {
|
|
17
|
+
const glideFillStrategy = (_a) => __awaiter(void 0, [_a], void 0, function* ({ row, columnName, value, page }) {
|
|
18
|
+
// Canvas-aware click: Glide is a canvas grid. The accessibility 'td' elements
|
|
19
|
+
// may not trigger internal focus on a simple click. We click the canvas at the cell's location.
|
|
20
|
+
const cell = row.getCell(columnName);
|
|
21
|
+
const box = yield cell.boundingBox();
|
|
22
|
+
const canvas = page.locator('canvas').first();
|
|
23
|
+
const cBox = yield canvas.boundingBox();
|
|
24
|
+
if (box && cBox) {
|
|
25
|
+
yield canvas.click({
|
|
26
|
+
position: {
|
|
27
|
+
x: box.x - cBox.x + box.width / 2,
|
|
28
|
+
y: box.y - cBox.y + box.height / 2
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Fallback
|
|
34
|
+
yield cell.focus();
|
|
35
|
+
}
|
|
18
36
|
yield page.keyboard.press('Enter');
|
|
19
37
|
const textarea = page.locator('textarea.gdg-input');
|
|
20
38
|
yield textarea.waitFor({ state: 'visible', timeout: 2000 });
|
|
@@ -50,7 +68,9 @@ const glidePaginationStrategy = pagination_1.PaginationStrategies.infiniteScroll
|
|
|
50
68
|
timeout: 5000
|
|
51
69
|
});
|
|
52
70
|
const glideGetCellLocator = ({ row, columnIndex }) => {
|
|
53
|
-
|
|
71
|
+
// WAI-ARIA: aria-colindex is 1-based; header map index 0 → "1", index 59 → "60".
|
|
72
|
+
// Rows are virtualized: only a window of `td`s exists; navigation must scroll `.dvn-scroller`.
|
|
73
|
+
return row.locator(`td[aria-colindex="${columnIndex + 1}"]`);
|
|
54
74
|
};
|
|
55
75
|
const glideGetActiveCell = (_a) => __awaiter(void 0, [_a], void 0, function* ({ page }) {
|
|
56
76
|
const focused = page.locator('*:focus').first();
|
|
@@ -80,7 +100,9 @@ const GlideDefaultStrategies = {
|
|
|
80
100
|
goDown: columns_1.glideGoDown,
|
|
81
101
|
goLeft: columns_1.glideGoLeft,
|
|
82
102
|
goRight: columns_1.glideGoRight,
|
|
83
|
-
goHome: columns_1.
|
|
103
|
+
goHome: columns_1.glideSnapFirstColumnIntoView,
|
|
104
|
+
snapFirstColumnIntoView: columns_1.glideSnapFirstColumnIntoView,
|
|
105
|
+
seekColumnIndex: columns_1.glideSeekColumnIndex
|
|
84
106
|
},
|
|
85
107
|
loading: {
|
|
86
108
|
isHeaderLoading: () => __awaiter(void 0, void 0, void 0, function* () { return false; })
|
|
@@ -99,6 +121,7 @@ const GlidePreset = {
|
|
|
99
121
|
headerSelector: 'table[role="grid"] thead tr th',
|
|
100
122
|
rowSelector: 'table[role="grid"] tbody tr',
|
|
101
123
|
cellSelector: 'td',
|
|
124
|
+
concurrency: 'sequential',
|
|
102
125
|
strategies: GlideDefaultStrategies
|
|
103
126
|
};
|
|
104
127
|
exports.Glide = Object.defineProperty(GlidePreset, 'Strategies', { get: () => exports.GlideStrategies, enumerable: false });
|
package/dist/smartRow.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Locator, Page } from '@playwright/test';
|
|
2
2
|
import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types';
|
|
3
|
+
import { NavigationBarrier } from './utils/navigationBarrier';
|
|
3
4
|
/**
|
|
4
5
|
* Factory to create a SmartRow by extending a Playwright Locator.
|
|
5
6
|
* We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
|
|
@@ -8,5 +9,5 @@ import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types
|
|
|
8
9
|
* @internal Internal factory for creating SmartRow objects.
|
|
9
10
|
* Not part of the public package surface; tests and consumers should use public APIs.
|
|
10
11
|
*/
|
|
11
|
-
declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null, tablePageIndex?: number) => SmartRowType<T>;
|
|
12
|
+
declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null, tablePageIndex?: number, barrier?: NavigationBarrier) => SmartRowType<T>;
|
|
12
13
|
export default createSmartRow;
|