@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 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 = await table.findRow({ Name: 'John Doe' });
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
- // Find row by column values
70
- const row = await table.findRow({ Name: 'John Doe' });
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, safe for interactions (parallel: false default)
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 within page, safe for reads (parallel: true default)
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 — async predicate across all pages, returns SmartRowArray
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 `parallel: true`. If your callback opens popovers,
105
- > fills inputs, or otherwise mutates UI state, pass `{ parallel: false }` to avoid overlapping interactions.
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
- * Shared row-iteration loop for map.
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
- * Shared row-iteration loop used by forEach, map, and filter.
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
- var _a, _b, _c, _d;
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
- * Shared row-iteration loop for map.
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, _d;
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 parallel = (_c = options.parallel) !== null && _c !== void 0 ? _c : true;
109
- const useBulk = (_d = options.useBulkPagination) !== null && _d !== void 0 ? _d : false;
110
- const tracker = new elementTracker_1.ElementTracker('map');
111
- log(env.config, `map: starting (maxPages=${effectiveMaxPages}, parallel=${parallel}, dedupe=${!!dedupeStrategy})`);
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
- log(env.config, `map: stop() called — halting after current page`);
120
- stopped = true;
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 smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i));
127
- log(env.config, `map: scanning page ${pagesScanned} — ${newIndices.length} new row(s)`);
128
- if (parallel) {
129
- const pageResults = yield Promise.all(smartRows.map((row) => __awaiter(this, void 0, void 0, function* () {
130
- if (dedupeKeys && dedupeStrategy) {
131
- const key = yield dedupeStrategy(row);
132
- if (dedupeKeys.has(key)) {
133
- log(env.config, `map: dedupe skip key="${key}"`);
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.add(key);
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
- else {
146
- for (const row of smartRows) {
147
- if (stopped)
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
- if (dedupeKeys && dedupeStrategy) {
150
- const key = yield dedupeStrategy(row);
151
- if (dedupeKeys.has(key)) {
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, `map: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
129
+ log(env.config, `${label}: advancing to next page (${pagesScanned} → ${pagesScanned + 1})`);
164
130
  if (!(yield env.advancePage(useBulk))) {
165
- log(env.config, `map: no more pages — done`);
131
+ log(env.config, `${label}: no more pages — done`);
166
132
  break;
167
133
  }
168
134
  pagesScanned++;
169
135
  }
170
- log(env.config, `map: complete — ${results.length} result(s) across ${pagesScanned} page(s)`);
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
- var _a, _b, _c, _d;
184
- const map = env.getMap();
185
- const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : env.config.maxPages;
186
- const dedupeStrategy = (_b = options.dedupe) !== null && _b !== void 0 ? _b : env.config.strategies.dedupe;
187
- const dedupeKeys = dedupeStrategy ? new Set() : null;
188
- const parallel = (_c = options.parallel) !== null && _c !== void 0 ? _c : false;
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
  }
@@ -109,6 +109,7 @@ class TableMapper {
109
109
  seenHeaders
110
110
  });
111
111
  }
112
+ this.log(`Header mapping index ${i}: "${text}"`);
112
113
  entries.push([text, i]);
113
114
  seenHeaders.add(text);
114
115
  }
@@ -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
- export declare const glideGoHome: (context: StrategyContext) => Promise<void>;
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.glideGoHome = exports.glideGoRight = exports.glideGoLeft = exports.glideGoDown = exports.glideGoUp = void 0;
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.keyboard.press('ArrowLeft');
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.keyboard.press('ArrowRight');
39
+ yield nudgePageScroller(context.page, 400);
32
40
  });
33
41
  exports.glideGoRight = glideGoRight;
34
- const glideGoHome = (context) => __awaiter(void 0, void 0, void 0, function* () {
35
- const { root, page } = context;
36
- // Glide renders to canvas - the accessibility table (root) is inside the canvas
37
- yield root.evaluate((el) => {
38
- var _a;
39
- const canvas = el.closest('canvas') || ((_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector('canvas'));
40
- if (canvas instanceof HTMLCanvasElement) {
41
- canvas.tabIndex = 0;
42
- canvas.focus();
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
- yield page.keyboard.press('Control+Home');
47
- yield page.keyboard.press('Meta+ArrowUp');
48
- yield page.keyboard.press('Home');
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.glideGoHome = glideGoHome;
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
- return row.locator('td').nth(columnIndex);
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.glideGoHome
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 });
@@ -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;