@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/dist/types.d.ts CHANGED
@@ -329,6 +329,11 @@ export interface TableConfig<T = any> {
329
329
  cellSelector?: string | ((row: Locator) => Locator);
330
330
  /** Number of pages to scan for verification */
331
331
  maxPages?: number;
332
+ /**
333
+ * Default concurrency strategy for iteration methods.
334
+ * Can be overridden by the options passed to forEach, map, or filter.
335
+ */
336
+ concurrency?: RowIterationMode;
332
337
  /** Hook to rename columns dynamically */
333
338
  headerTransformer?: (args: {
334
339
  text: string;
@@ -356,6 +361,7 @@ export interface FinalTableConfig<T = any> extends TableConfig<T> {
356
361
  cellSelector: string | ((row: Locator) => Locator);
357
362
  maxPages: number;
358
363
  autoScroll: boolean;
364
+ concurrency?: RowIterationMode;
359
365
  debug?: TableConfig['debug'];
360
366
  headerTransformer: (args: {
361
367
  text: string;
@@ -379,15 +385,26 @@ export type RowIterationContext<T = any> = {
379
385
  rowIndex: number;
380
386
  stop: () => void;
381
387
  };
388
+ /** Concurrency modes for row iteration. */
389
+ export type RowIterationMode = 'parallel' | 'sequential' | 'synchronized';
382
390
  /** Shared options for forEach, map, and filter. */
383
391
  export type RowIterationOptions = {
384
392
  /** Maximum number of pages to iterate. Defaults to config.maxPages. */
385
393
  maxPages?: number;
386
394
  /**
387
- * Whether to process rows within a page concurrently.
388
- * @default false for forEach/filter, true for map
395
+ * @deprecated Use 'concurrency' mode instead.
396
+ * - parallel: true -> concurrency: 'parallel'
397
+ * - parallel: false -> concurrency: 'sequential'
389
398
  */
390
399
  parallel?: boolean;
400
+ /**
401
+ * Concurrency strategy for iteration.
402
+ * - 'parallel': Full parallel execution of both navigation and actions.
403
+ * - 'synchronized': Parallel navigation (lock-step) with serial actions.
404
+ * - 'sequential': Strictly serial one-at-a-time execution. No parallel navigation.
405
+ * @default 'parallel' (for map), 'sequential' (for forEach/filter)
406
+ */
407
+ concurrency?: RowIterationMode;
391
408
  /**
392
409
  * Deduplication strategy. Use when rows may repeat across iterations
393
410
  * (e.g. infinite scroll tables). Returns a unique key per row.
@@ -475,6 +492,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
475
492
  * Execution is sequential by default (safe for interactions like clicking/filling).
476
493
  * Call `stop()` in the callback to end iteration early.
477
494
  *
495
+ * @param callback - Function receiving { row, rowIndex, stop }
496
+ * @param options - maxPages, concurrency, dedupe, useBulkPagination (`parallel` is deprecated; use `concurrency`)
497
+ *
478
498
  * @example
479
499
  * await table.forEach(async ({ row, stop }) => {
480
500
  * if (await row.getCell('Status').innerText() === 'Done') stop();
@@ -487,22 +507,25 @@ export interface TableResult<T = any> extends AsyncIterable<{
487
507
  * Execution is parallel within each page by default (safe for reads).
488
508
  * Call `stop()` to halt after the current page finishes.
489
509
  *
490
- * > **⚠️ UI Interactions:** `map` defaults to `parallel: true`. If your callback opens popovers,
491
- * > fills inputs, or otherwise mutates UI state, pass `{ parallel: false }` to avoid concurrent
492
- * > interactions interfering with each other.
510
+ * > **⚠️ UI Interactions:** `map` defaults to `concurrency: 'parallel'`. If your callback opens popovers,
511
+ * > fills inputs, or otherwise mutates UI state, pass `concurrency: 'sequential'` (or `'synchronized'`
512
+ * > when navigation must stay lock-step) to avoid overlapping interactions.
513
+ *
514
+ * @param callback - Function receiving { row, rowIndex, stop }
515
+ * @param options - maxPages, concurrency, dedupe, useBulkPagination (`parallel` is deprecated; use `concurrency`)
493
516
  *
494
517
  * @example
495
518
  * // Data extraction — parallel is safe
496
519
  * const emails = await table.map(({ row }) => row.getCell('Email').innerText());
497
520
  *
498
521
  * @example
499
- * // UI interactions — must use parallel: false
522
+ * // UI interactions — use sequential (or synchronized) concurrency
500
523
  * const assignees = await table.map(async ({ row }) => {
501
524
  * await row.getCell('Assignee').locator('button').click();
502
525
  * const name = await page.locator('.popover .name').innerText();
503
526
  * await page.keyboard.press('Escape');
504
527
  * return name;
505
- * }, { parallel: false });
528
+ * }, { concurrency: 'sequential' });
506
529
  */
507
530
  map<R>(callback: (ctx: RowIterationContext<T>) => R | Promise<R>, options?: RowIterationOptions): Promise<R[]>;
508
531
  /**
@@ -510,6 +533,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
510
533
  * Rows are returned as-is — call `bringIntoView()` on each if needed.
511
534
  * Execution is sequential by default.
512
535
  *
536
+ * @param predicate - Function receiving { row, rowIndex, stop }
537
+ * @param options - maxPages, concurrency, dedupe, useBulkPagination (`parallel` is deprecated; use `concurrency`)
538
+ *
513
539
  * @example
514
540
  * const active = await table.filter(async ({ row }) =>
515
541
  * await row.getCell('Status').innerText() === 'Active'
package/dist/useTable.js CHANGED
@@ -39,6 +39,7 @@ const tableIteration_1 = require("./engine/tableIteration");
39
39
  const debugUtils_1 = require("./utils/debugUtils");
40
40
  const smartRowArray_1 = require("./utils/smartRowArray");
41
41
  const elementTracker_1 = require("./utils/elementTracker");
42
+ const navigationBarrier_1 = require("./utils/navigationBarrier");
42
43
  /**
43
44
  * Main hook to interact with a table.
44
45
  */
@@ -94,8 +95,8 @@ const useTable = (rootLocator, configOptions = {}) => {
94
95
  // Placeholder for the final table object
95
96
  let finalTable = null;
96
97
  // Helper factory
97
- const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex) => {
98
- return (0, smartRow_1.default)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex);
98
+ const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier) => {
99
+ return (0, smartRow_1.default)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
99
100
  };
100
101
  const tableState = { currentPageIndex: 0 };
101
102
  const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart, tableState);
@@ -299,8 +300,9 @@ const useTable = (rootLocator, configOptions = {}) => {
299
300
  const rowLocators = resolve(config.rowSelector, rootLocator);
300
301
  const newIndices = yield __await(tracker.getUnseenIndices(rowLocators));
301
302
  const pageRows = yield __await(rowLocators.all());
303
+ const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
302
304
  for (const idx of newIndices) {
303
- yield yield __await({ row: _makeSmart(pageRows[idx], map, rowIndex), rowIndex });
305
+ yield yield __await({ row: _makeSmart(pageRows[idx], map, rowIndex, pagesScanned - 1, barrier), rowIndex });
304
306
  rowIndex++;
305
307
  }
306
308
  if (pagesScanned >= effectiveMaxPages)
@@ -322,7 +324,7 @@ const useTable = (rootLocator, configOptions = {}) => {
322
324
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
323
325
  getMap: () => tableMapper.getMapSync(),
324
326
  advancePage: _advancePage,
325
- makeSmartRow: (loc, map, idx, pageIdx) => _makeSmart(loc, map, idx, pageIdx),
327
+ makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
326
328
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
327
329
  config,
328
330
  getPage: () => rootLocator.page(),
@@ -334,7 +336,7 @@ const useTable = (rootLocator, configOptions = {}) => {
334
336
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
335
337
  getMap: () => tableMapper.getMapSync(),
336
338
  advancePage: _advancePage,
337
- makeSmartRow: (loc, map, idx, pageIdx) => _makeSmart(loc, map, idx, pageIdx),
339
+ makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
338
340
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
339
341
  config,
340
342
  getPage: () => rootLocator.page(),
@@ -346,7 +348,7 @@ const useTable = (rootLocator, configOptions = {}) => {
346
348
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
347
349
  getMap: () => tableMapper.getMapSync(),
348
350
  advancePage: _advancePage,
349
- makeSmartRow: (loc, map, idx, pageIdx) => _makeSmart(loc, map, idx, pageIdx),
351
+ makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
350
352
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
351
353
  config,
352
354
  getPage: () => rootLocator.page(),
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A simple async mutex to handle sequential execution within
3
+ * a parallel batch.
4
+ */
5
+ export declare class Mutex {
6
+ private queue;
7
+ run<T>(action: () => T | Promise<T>): Promise<T>;
8
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Mutex = void 0;
13
+ /**
14
+ * A simple async mutex to handle sequential execution within
15
+ * a parallel batch.
16
+ */
17
+ class Mutex {
18
+ constructor() {
19
+ this.queue = Promise.resolve();
20
+ }
21
+ run(action) {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ const next = this.queue.then(() => __awaiter(this, void 0, void 0, function* () {
24
+ return yield action();
25
+ }));
26
+ this.queue = next.catch(() => { }); // prevent chain breakage
27
+ return yield next;
28
+ });
29
+ }
30
+ }
31
+ exports.Mutex = Mutex;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A synchronization barrier that allows multiple parallel row processors
3
+ * to coordinate their navigation actions.
4
+ */
5
+ export declare class NavigationBarrier {
6
+ private total;
7
+ private finishedCount;
8
+ private buckets;
9
+ constructor(total: number);
10
+ /**
11
+ * Synchronize all active rows at a specific column index.
12
+ * The last row to arrive will trigger the `moveAction`.
13
+ */
14
+ sync<T = void>(colIndex: number, moveAction?: () => Promise<T>): Promise<T | void>;
15
+ /**
16
+ * Mark a row as finished (no more cells to process).
17
+ * This ensures the barrier doesn't wait for rows that have exited the loop.
18
+ */
19
+ markFinished(): void;
20
+ private broadcast;
21
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.NavigationBarrier = void 0;
13
+ /**
14
+ * A synchronization barrier that allows multiple parallel row processors
15
+ * to coordinate their navigation actions.
16
+ */
17
+ class NavigationBarrier {
18
+ constructor(total) {
19
+ this.finishedCount = 0;
20
+ // Map of colIndex -> Set of waiting resolvers
21
+ this.buckets = new Map();
22
+ this.total = total;
23
+ }
24
+ /**
25
+ * Synchronize all active rows at a specific column index.
26
+ * The last row to arrive will trigger the `moveAction`.
27
+ */
28
+ sync(colIndex, moveAction) {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ if (this.total <= 1) {
31
+ if (moveAction)
32
+ return yield moveAction();
33
+ return;
34
+ }
35
+ return new Promise((resolve) => {
36
+ let bucket = this.buckets.get(colIndex);
37
+ if (!bucket) {
38
+ bucket = new Set();
39
+ this.buckets.set(colIndex, bucket);
40
+ }
41
+ bucket.add(resolve);
42
+ if (bucket.size + this.finishedCount >= this.total) {
43
+ // Use a self-executing async function to handle the moveAction synchronously in this context
44
+ (() => __awaiter(this, void 0, void 0, function* () {
45
+ let result;
46
+ try {
47
+ if (moveAction)
48
+ result = yield moveAction();
49
+ }
50
+ finally {
51
+ this.broadcast(colIndex, result);
52
+ }
53
+ }))();
54
+ }
55
+ });
56
+ });
57
+ }
58
+ /**
59
+ * Mark a row as finished (no more cells to process).
60
+ * This ensures the barrier doesn't wait for rows that have exited the loop.
61
+ */
62
+ markFinished() {
63
+ this.finishedCount++;
64
+ // Check all buckets - any bucket that was waiting for this row might now be ready
65
+ for (const colIndex of Array.from(this.buckets.keys())) {
66
+ const bucket = this.buckets.get(colIndex);
67
+ if (bucket.size + this.finishedCount >= this.total) {
68
+ this.broadcast(colIndex);
69
+ }
70
+ }
71
+ }
72
+ broadcast(colIndex, result) {
73
+ const bucket = this.buckets.get(colIndex);
74
+ if (bucket) {
75
+ for (const resolve of bucket) {
76
+ resolve(result);
77
+ }
78
+ this.buckets.delete(colIndex);
79
+ }
80
+ }
81
+ }
82
+ exports.NavigationBarrier = NavigationBarrier;
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "6.7.8",
3
+ "version": "6.8.0",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",
7
+ "homepage": "https://rickcedwhat.github.io/playwright-smart-table/",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/rickcedwhat/playwright-smart-table.git"
10
11
  },
12
+ "bugs": {
13
+ "url": "https://github.com/rickcedwhat/playwright-smart-table/issues"
14
+ },
11
15
  "main": "dist/index.js",
12
16
  "types": "dist/index.d.ts",
13
17
  "files": [
@@ -55,7 +59,15 @@
55
59
  "playwright",
56
60
  "testing",
57
61
  "table",
58
- "automation"
62
+ "automation",
63
+ "locators",
64
+ "e2e",
65
+ "qa",
66
+ "typescript",
67
+ "datagrid",
68
+ "smart-table",
69
+ "ag-grid",
70
+ "mui"
59
71
  ],
60
72
  "peerDependencies": {
61
73
  "@playwright/test": "^1.40.0"