@rickcedwhat/playwright-smart-table 5.0.0 → 5.1.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
@@ -355,11 +355,11 @@ const allData = await table.iterateThroughTable(
355
355
  },
356
356
  {
357
357
  getIsLast: ({ paginationResult }) => !paginationResult,
358
- onFirst: async ({ allData }) => {
358
+ beforeFirst: async ({ allData }) => {
359
359
  console.log('Starting data collection...');
360
360
  // Could perform setup actions
361
361
  },
362
- onLast: async ({ allData }) => {
362
+ afterLast: async ({ allData }) => {
363
363
  console.log(`Collected ${allData.length} total items`);
364
364
  // Could perform cleanup or final actions
365
365
  }
@@ -369,10 +369,43 @@ const allData = await table.iterateThroughTable(
369
369
  <!-- /embed: iterate-through-table-hooks -->
370
370
 
371
371
  **Hook Timing:**
372
- - `onFirst`: Runs **before** your callback processes the first page
373
- - `onLast`: Runs **after** your callback processes the last page
372
+ - `beforeFirst`: Runs **before** your callback processes the first page
373
+ - `afterLast`: Runs **after** your callback processes the last page
374
374
  - Both are optional and receive `{ index, rows, allData }`
375
375
 
376
+ #### Batching (v5.1+)
377
+
378
+ Process multiple pages at once for better performance:
379
+
380
+ ```typescript
381
+ const results = await table.iterateThroughTable(
382
+ async ({ rows, batchInfo }) => {
383
+ // rows contains data from multiple pages
384
+ console.log(`Processing pages ${batchInfo.startIndex}-${batchInfo.endIndex}`);
385
+ console.log(`Batch has ${rows.length} total rows from ${batchInfo.size} pages`);
386
+
387
+ // Bulk process (e.g., batch database insert)
388
+ await bulkInsert(rows);
389
+ return rows.length;
390
+ },
391
+ {
392
+ batchSize: 3 // Process 3 pages at a time
393
+ }
394
+ );
395
+
396
+ // With 6 pages total:
397
+ // - Batch 1: pages 0,1,2 (batchInfo.size = 3)
398
+ // - Batch 2: pages 3,4,5 (batchInfo.size = 3)
399
+ // results.length === 2 (fewer callbacks than pages)
400
+ ```
401
+
402
+ **Key Points:**
403
+ - `batchSize` = number of **pages**, not rows
404
+ - `batchInfo` is undefined when not batching (`batchSize` undefined or `1`)
405
+ - Works with deduplication, pagination strategies, and hooks
406
+ - Reduces callback overhead for bulk operations
407
+ - Default: no batching (one callback per page)
408
+
376
409
  ---
377
410
 
378
411
  ## 📖 API Reference
package/dist/smartRow.js CHANGED
@@ -11,6 +11,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.createSmartRow = void 0;
13
13
  const fill_1 = require("./strategies/fill");
14
+ const stringUtils_1 = require("./utils/stringUtils");
15
+ const traceUtils_1 = require("./utils/traceUtils");
14
16
  /**
15
17
  * Factory to create a SmartRow by extending a Playwright Locator.
16
18
  * We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
@@ -23,8 +25,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
23
25
  smart.getCell = (colName) => {
24
26
  const idx = map.get(colName);
25
27
  if (idx === undefined) {
26
- const availableColumns = Array.from(map.keys());
27
- throw new Error(`Column "${colName}" not found. Available: ${availableColumns.join(', ')}`);
28
+ throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
28
29
  }
29
30
  if (config.strategies.getCellLocator) {
30
31
  return config.strategies.getCellLocator({
@@ -35,6 +36,8 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
35
36
  page: rootLocator.page()
36
37
  });
37
38
  }
39
+ // Add trace event
40
+ (0, traceUtils_1.addTraceEvent)(rootLocator.page(), 'getCell', { column: colName, columnIndex: idx, rowIndex }).catch(() => { });
38
41
  return resolve(config.cellSelector, rowLocator).nth(idx);
39
42
  };
40
43
  smart.toJSON = (options) => __awaiter(void 0, void 0, void 0, function* () {
@@ -3,4 +3,4 @@
3
3
  * This file is generated by scripts/embed-types.js
4
4
  * It contains the raw text of types.ts to provide context for LLM prompts.
5
5
  */
6
- export declare const TYPE_CONTEXT = "\n/**\n * Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.\n * @example\n * // String selector\n * rowSelector: 'tbody tr'\n * \n * // Function selector\n * rowSelector: (root) => root.locator('[role=\"row\"]')\n */\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\n/**\n * Function to get a cell locator given row, column info.\n * Replaces the old cellResolver.\n */\nexport type GetCellLocatorFn = (args: {\n row: Locator;\n columnName: string;\n columnIndex: number;\n rowIndex?: number;\n page: Page;\n}) => Locator;\n\n/**\n * Function to get the currently active/focused cell.\n * Returns null if no cell is active.\n */\nexport type GetActiveCellFn = (args: TableContext) => Promise<{\n rowIndex: number;\n columnIndex: number;\n columnName?: string;\n locator: Locator;\n} | null>;\n\n\n/**\n * SmartRow - A Playwright Locator with table-aware methods.\n * \n * Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.\n * \n * @example\n * const row = table.getRow({ Name: 'John Doe' });\n * await row.click(); // Standard Locator method\n * const email = row.getCell('Email'); // Table-aware method\n * const data = await row.toJSON(); // Extract all row data\n * await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields\n */\nexport type SmartRow<T = any> = Locator & {\n /** Optional row index (0-based) if known */\n rowIndex?: number;\n\n /**\n * Get a cell locator by column name.\n * @param column - Column name (case-sensitive)\n * @returns Locator for the cell\n * @example\n * const emailCell = row.getCell('Email');\n * await expect(emailCell).toHaveText('john@example.com');\n */\n getCell(column: string): Locator;\n\n /**\n * Extract all cell data as a key-value object.\n * @param options - Optional configuration\n * @param options.columns - Specific columns to extract (extracts all if not specified)\n * @returns Promise resolving to row data\n * @example\n * const data = await row.toJSON();\n * // { Name: 'John', Email: 'john@example.com', ... }\n * \n * const partial = await row.toJSON({ columns: ['Name', 'Email'] });\n * // { Name: 'John', Email: 'john@example.com' }\n */\n toJSON(options?: { columns?: string[] }): Promise<T>;\n\n /**\n * Scrolls/paginates to bring this row into view.\n * Only works if rowIndex is known (e.g., from getRowByIndex).\n * @throws Error if rowIndex is unknown\n */\n bringIntoView(): Promise<void>;\n\n /**\n * Intelligently fills form fields in the row.\n * Automatically detects input types (text, select, checkbox, contenteditable).\n * \n * @param data - Column-value pairs to fill\n * @param options - Optional configuration\n * @param options.inputMappers - Custom input selectors per column\n * @example\n * // Auto-detection\n * await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });\n * \n * // Custom input mappers\n * await row.smartFill(\n * { Name: 'John' },\n * { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }\n * );\n */\n smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext & { rowLocator?: Locator; rowIndex?: number };\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport type FillStrategy = (options: {\n row: SmartRow;\n columnName: string;\n value: any;\n index: number;\n page: Page;\n rootLocator: Locator;\n table: TableResult; // The parent table instance\n fillOptions?: FillOptions;\n}) => Promise<void>;\n\nexport type { HeaderStrategy } from './strategies/headers';\nexport type { CellNavigationStrategy } from './strategies/columns';\n\n/**\n * Strategy to resolve column names (string or regex) to their index.\n */\nexport type { ColumnResolutionStrategy } from './strategies/resolution';\n\n/**\n * Strategy to filter rows based on criteria.\n */\nexport interface FilterStrategy {\n apply(options: {\n rows: Locator;\n filter: { column: string, value: string | RegExp | number };\n colIndex: number;\n tableContext: TableContext;\n }): Locator;\n}\n\n/**\n * Organized container for all table interaction strategies.\n */\nexport interface TableStrategies {\n /** Strategy for discovering/scanning headers */\n header?: HeaderStrategy;\n /** Strategy for navigating to specific cells (row + column) */\n cellNavigation?: CellNavigationStrategy;\n /** Strategy for filling form inputs */\n fill?: FillStrategy;\n /** Strategy for paginating through data */\n pagination?: PaginationStrategy;\n /** Strategy for sorting columns */\n sorting?: SortingStrategy;\n /** Function to get a cell locator */\n getCellLocator?: GetCellLocatorFn;\n /** Function to get the currently active/focused cell */\n getActiveCell?: GetActiveCellFn;\n}\n\n/**\n * Configuration options for useTable.\n */\nexport interface TableConfig {\n /** Selector for the table headers */\n headerSelector?: string;\n /** Selector for the table rows */\n rowSelector?: string;\n /** Selector for the cells within a row */\n cellSelector?: string;\n /** Number of pages to scan for verification */\n maxPages?: number;\n /** Hook to rename columns dynamically */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n /** Automatically scroll to table on init */\n autoScroll?: boolean;\n /** Enable debug logs */\n debug?: boolean;\n /** Reset hook */\n onReset?: (context: TableContext) => Promise<void>;\n /** All interaction strategies */\n strategies?: TableStrategies;\n}\n\nexport interface FinalTableConfig extends TableConfig {\n headerSelector: string;\n rowSelector: string;\n cellSelector: string;\n maxPages: number;\n autoScroll: boolean;\n debug: boolean;\n headerTransformer: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n onReset: (context: TableContext) => Promise<void>;\n strategies: TableStrategies;\n}\n\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult<T = any> {\n /**\n * Initializes the table by resolving headers. Must be called before using sync methods.\n * @param options Optional timeout for header resolution (default: 3000ms)\n */\n init(options?: { timeout?: number }): Promise<TableResult>;\n\n /**\n * SYNC: Checks if the table has been initialized.\n * @returns true if init() has been called and completed, false otherwise\n */\n isInitialized(): boolean;\n\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row by filters on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean }\n ) => SmartRow;\n\n /**\n * Gets a row by 1-based index on the current page.\n * Throws error if table is not initialized.\n * @param index 1-based row index\n * @param options Optional settings including bringIntoView\n */\n getRowByIndex: (\n index: number,\n options?: { bringIntoView?: boolean }\n ) => SmartRow;\n\n /**\n * ASYNC: Searches for a single row across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match and max pages\n */\n findRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n /**\n * ASYNC: Searches for all matching rows across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match, max pages, and asJSON\n */\n findRows: <R extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * Navigates to a specific column using the configured CellNavigationStrategy.\n */\n scrollToColumn: (columnName: string) => Promise<void>;\n\n /**\n * ASYNC: Gets all rows on the current page only (does not paginate).\n * Auto-initializes the table if not already initialized.\n * @param options - Filter and formatting options\n */\n getRows: <R extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Revalidates the table's structure (headers, columns) without resetting pagination or state.\n * Useful when columns change visibility or order dynamically.\n */\n revalidate: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n\n /**\n * Iterates through paginated table data, calling the callback for each iteration.\n * Callback return values are automatically appended to allData, which is returned.\n */\n iterateThroughTable: <T = any>(\n callback: (context: {\n index: number;\n isFirst: boolean;\n isLast: boolean;\n rows: SmartRow[];\n allData: T[];\n table: RestrictedTableResult;\n }) => T | Promise<T>,\n options?: {\n pagination?: PaginationStrategy;\n dedupeStrategy?: DedupeStrategy;\n maxIterations?: number;\n getIsFirst?: (context: { index: number }) => boolean;\n getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;\n onFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n onLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n }\n ) => Promise<T[]>;\n}\n\n/**\n * Restricted table result that excludes methods that shouldn't be called during iteration.\n */\nexport type RestrictedTableResult<T = any> = Omit<TableResult<T>, 'searchForRow' | 'iterateThroughTable' | 'reset'>;\n";
6
+ export declare const TYPE_CONTEXT = "\n/**\n * Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.\n * @example\n * // String selector\n * rowSelector: 'tbody tr'\n * \n * // Function selector\n * rowSelector: (root) => root.locator('[role=\"row\"]')\n */\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\n/**\n * Function to get a cell locator given row, column info.\n * Replaces the old cellResolver.\n */\nexport type GetCellLocatorFn = (args: {\n row: Locator;\n columnName: string;\n columnIndex: number;\n rowIndex?: number;\n page: Page;\n}) => Locator;\n\n/**\n * Function to get the currently active/focused cell.\n * Returns null if no cell is active.\n */\nexport type GetActiveCellFn = (args: TableContext) => Promise<{\n rowIndex: number;\n columnIndex: number;\n columnName?: string;\n locator: Locator;\n} | null>;\n\n\n/**\n * SmartRow - A Playwright Locator with table-aware methods.\n * \n * Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.\n * \n * @example\n * const row = table.getRow({ Name: 'John Doe' });\n * await row.click(); // Standard Locator method\n * const email = row.getCell('Email'); // Table-aware method\n * const data = await row.toJSON(); // Extract all row data\n * await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields\n */\nexport type SmartRow<T = any> = Locator & {\n /** Optional row index (0-based) if known */\n rowIndex?: number;\n\n /**\n * Get a cell locator by column name.\n * @param column - Column name (case-sensitive)\n * @returns Locator for the cell\n * @example\n * const emailCell = row.getCell('Email');\n * await expect(emailCell).toHaveText('john@example.com');\n */\n getCell(column: string): Locator;\n\n /**\n * Extract all cell data as a key-value object.\n * @param options - Optional configuration\n * @param options.columns - Specific columns to extract (extracts all if not specified)\n * @returns Promise resolving to row data\n * @example\n * const data = await row.toJSON();\n * // { Name: 'John', Email: 'john@example.com', ... }\n * \n * const partial = await row.toJSON({ columns: ['Name', 'Email'] });\n * // { Name: 'John', Email: 'john@example.com' }\n */\n toJSON(options?: { columns?: string[] }): Promise<T>;\n\n /**\n * Scrolls/paginates to bring this row into view.\n * Only works if rowIndex is known (e.g., from getRowByIndex).\n * @throws Error if rowIndex is unknown\n */\n bringIntoView(): Promise<void>;\n\n /**\n * Intelligently fills form fields in the row.\n * Automatically detects input types (text, select, checkbox, contenteditable).\n * \n * @param data - Column-value pairs to fill\n * @param options - Optional configuration\n * @param options.inputMappers - Custom input selectors per column\n * @example\n * // Auto-detection\n * await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });\n * \n * // Custom input mappers\n * await row.smartFill(\n * { Name: 'John' },\n * { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }\n * );\n */\n smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext & { rowLocator?: Locator; rowIndex?: number };\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport type FillStrategy = (options: {\n row: SmartRow;\n columnName: string;\n value: any;\n index: number;\n page: Page;\n rootLocator: Locator;\n table: TableResult; // The parent table instance\n fillOptions?: FillOptions;\n}) => Promise<void>;\n\nexport type { HeaderStrategy } from './strategies/headers';\nexport type { CellNavigationStrategy } from './strategies/columns';\n\n/**\n * Strategy to resolve column names (string or regex) to their index.\n */\nexport type { ColumnResolutionStrategy } from './strategies/resolution';\n\n/**\n * Strategy to filter rows based on criteria.\n */\nexport interface FilterStrategy {\n apply(options: {\n rows: Locator;\n filter: { column: string, value: string | RegExp | number };\n colIndex: number;\n tableContext: TableContext;\n }): Locator;\n}\n\n/**\n * Organized container for all table interaction strategies.\n */\nexport interface TableStrategies {\n /** Strategy for discovering/scanning headers */\n header?: HeaderStrategy;\n /** Strategy for navigating to specific cells (row + column) */\n cellNavigation?: CellNavigationStrategy;\n /** Strategy for filling form inputs */\n fill?: FillStrategy;\n /** Strategy for paginating through data */\n pagination?: PaginationStrategy;\n /** Strategy for sorting columns */\n sorting?: SortingStrategy;\n /** Function to get a cell locator */\n getCellLocator?: GetCellLocatorFn;\n /** Function to get the currently active/focused cell */\n getActiveCell?: GetActiveCellFn;\n}\n\n/**\n * Configuration options for useTable.\n */\nexport interface TableConfig {\n /** Selector for the table headers */\n headerSelector?: string;\n /** Selector for the table rows */\n rowSelector?: string;\n /** Selector for the cells within a row */\n cellSelector?: string;\n /** Number of pages to scan for verification */\n maxPages?: number;\n /** Hook to rename columns dynamically */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n /** Automatically scroll to table on init */\n autoScroll?: boolean;\n /** Enable debug logs */\n debug?: boolean;\n /** Reset hook */\n onReset?: (context: TableContext) => Promise<void>;\n /** All interaction strategies */\n strategies?: TableStrategies;\n}\n\nexport interface FinalTableConfig extends TableConfig {\n headerSelector: string;\n rowSelector: string;\n cellSelector: string;\n maxPages: number;\n autoScroll: boolean;\n debug: boolean;\n headerTransformer: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n onReset: (context: TableContext) => Promise<void>;\n strategies: TableStrategies;\n}\n\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult<T = any> {\n /**\n * Initializes the table by resolving headers. Must be called before using sync methods.\n * @param options Optional timeout for header resolution (default: 3000ms)\n */\n init(options?: { timeout?: number }): Promise<TableResult>;\n\n /**\n * SYNC: Checks if the table has been initialized.\n * @returns true if init() has been called and completed, false otherwise\n */\n isInitialized(): boolean;\n\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row by filters on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean }\n ) => SmartRow;\n\n /**\n * Gets a row by 1-based index on the current page.\n * Throws error if table is not initialized.\n * @param index 1-based row index\n * @param options Optional settings including bringIntoView\n */\n getRowByIndex: (\n index: number,\n options?: { bringIntoView?: boolean }\n ) => SmartRow;\n\n /**\n * ASYNC: Searches for a single row across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match and max pages\n */\n findRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n /**\n * ASYNC: Searches for all matching rows across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match, max pages, and asJSON\n */\n findRows: <R extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * Navigates to a specific column using the configured CellNavigationStrategy.\n */\n scrollToColumn: (columnName: string) => Promise<void>;\n\n /**\n * ASYNC: Gets all rows on the current page only (does not paginate).\n * Auto-initializes the table if not already initialized.\n * @param options - Filter and formatting options\n */\n getRows: <R extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Revalidates the table's structure (headers, columns) without resetting pagination or state.\n * Useful when columns change visibility or order dynamically.\n */\n revalidate: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n\n /**\n * Iterates through paginated table data, calling the callback for each iteration.\n * Callback return values are automatically appended to allData, which is returned.\n */\n iterateThroughTable: <T = any>(\n callback: (context: {\n index: number;\n isFirst: boolean;\n isLast: boolean;\n rows: SmartRow[];\n allData: T[];\n table: RestrictedTableResult;\n batchInfo?: {\n startIndex: number;\n endIndex: number;\n size: number;\n };\n }) => T | Promise<T>,\n options?: {\n pagination?: PaginationStrategy;\n dedupeStrategy?: DedupeStrategy;\n maxIterations?: number;\n batchSize?: number;\n getIsFirst?: (context: { index: number }) => boolean;\n getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;\n beforeFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n afterLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n }\n ) => Promise<T[]>;\n}\n\n/**\n * Restricted table result that excludes methods that shouldn't be called during iteration.\n */\nexport type RestrictedTableResult<T = any> = Omit<TableResult<T>, 'searchForRow' | 'iterateThroughTable' | 'reset'>;\n";
@@ -368,15 +368,21 @@ export interface TableResult<T = any> {
368
368
  rows: SmartRow[];
369
369
  allData: T[];
370
370
  table: RestrictedTableResult;
371
+ batchInfo?: {
372
+ startIndex: number;
373
+ endIndex: number;
374
+ size: number;
375
+ };
371
376
  }) => T | Promise<T>,
372
377
  options?: {
373
378
  pagination?: PaginationStrategy;
374
379
  dedupeStrategy?: DedupeStrategy;
375
380
  maxIterations?: number;
381
+ batchSize?: number;
376
382
  getIsFirst?: (context: { index: number }) => boolean;
377
383
  getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;
378
- onFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
379
- onLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
384
+ beforeFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
385
+ afterLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
380
386
  }
381
387
  ) => Promise<T[]>;
382
388
  }
package/dist/types.d.ts CHANGED
@@ -347,10 +347,16 @@ export interface TableResult<T = any> {
347
347
  rows: SmartRow[];
348
348
  allData: T[];
349
349
  table: RestrictedTableResult;
350
+ batchInfo?: {
351
+ startIndex: number;
352
+ endIndex: number;
353
+ size: number;
354
+ };
350
355
  }) => T | Promise<T>, options?: {
351
356
  pagination?: PaginationStrategy;
352
357
  dedupeStrategy?: DedupeStrategy;
353
358
  maxIterations?: number;
359
+ batchSize?: number;
354
360
  getIsFirst?: (context: {
355
361
  index: number;
356
362
  }) => boolean;
@@ -358,12 +364,12 @@ export interface TableResult<T = any> {
358
364
  index: number;
359
365
  paginationResult: boolean;
360
366
  }) => boolean;
361
- onFirst?: (context: {
367
+ beforeFirst?: (context: {
362
368
  index: number;
363
369
  rows: SmartRow[];
364
370
  allData: any[];
365
371
  }) => void | Promise<void>;
366
- onLast?: (context: {
372
+ afterLast?: (context: {
367
373
  index: number;
368
374
  rows: SmartRow[];
369
375
  allData: any[];
package/dist/useTable.js CHANGED
@@ -26,6 +26,7 @@ Object.defineProperty(exports, "ResolutionStrategies", { enumerable: true, get:
26
26
  const strategies_1 = require("./strategies");
27
27
  Object.defineProperty(exports, "Strategies", { enumerable: true, get: function () { return strategies_1.Strategies; } });
28
28
  const validation_1 = require("./strategies/validation");
29
+ const traceUtils_1 = require("./utils/traceUtils");
29
30
  /**
30
31
  * Main hook to interact with a table.
31
32
  */
@@ -40,7 +41,7 @@ const useTable = (rootLocator, configOptions = {}) => {
40
41
  cellNavigation: columns_1.CellNavigationStrategies.default,
41
42
  pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
42
43
  };
43
- const config = Object.assign(Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, onReset: () => __awaiter(void 0, void 0, void 0, function* () { }) }, configOptions), { strategies: Object.assign(Object.assign({}, defaultStrategies), configOptions.strategies) });
44
+ const config = Object.assign(Object.assign({ rowSelector: "tbody tr", headerSelector: "thead th", cellSelector: "td", maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, onReset: () => __awaiter(void 0, void 0, void 0, function* () { }) }, configOptions), { strategies: Object.assign(Object.assign({}, defaultStrategies), configOptions.strategies) });
44
45
  const resolve = (item, parent) => {
45
46
  if (typeof item === 'string')
46
47
  return parent.locator(item);
@@ -225,6 +226,9 @@ const useTable = (rootLocator, configOptions = {}) => {
225
226
  return result;
226
227
  yield _getMap(options === null || options === void 0 ? void 0 : options.timeout);
227
228
  _isInitialized = true;
229
+ if (_headerMap) {
230
+ yield (0, traceUtils_1.addTraceEvent)(rootLocator.page(), 'init', { headers: Array.from(_headerMap.keys()), columnCount: _headerMap.size });
231
+ }
228
232
  return result;
229
233
  }),
230
234
  scrollToColumn: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
@@ -419,10 +423,14 @@ const useTable = (rootLocator, configOptions = {}) => {
419
423
  const getIsLast = (_c = options === null || options === void 0 ? void 0 : options.getIsLast) !== null && _c !== void 0 ? _c : (() => false);
420
424
  const allData = [];
421
425
  const effectiveMaxIterations = (_d = options === null || options === void 0 ? void 0 : options.maxIterations) !== null && _d !== void 0 ? _d : config.maxPages;
426
+ const batchSize = options === null || options === void 0 ? void 0 : options.batchSize;
427
+ const isBatching = batchSize !== undefined && batchSize > 1;
422
428
  let index = 0;
423
429
  let paginationResult = true;
424
430
  let seenKeys = null;
425
- logDebug(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations})`);
431
+ let batchRows = [];
432
+ let batchStartIndex = 0;
433
+ logDebug(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations}, batchSize: ${batchSize !== null && batchSize !== void 0 ? batchSize : 'none'})`);
426
434
  while (index < effectiveMaxIterations) {
427
435
  const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
428
436
  let rows = rowLocators.map((loc, i) => _makeSmart(loc, _headerMap, i));
@@ -440,21 +448,91 @@ const useTable = (rootLocator, configOptions = {}) => {
440
448
  rows = deduplicated;
441
449
  logDebug(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
442
450
  }
443
- const isFirst = getIsFirst({ index });
444
- let isLast = getIsLast({ index, paginationResult });
445
- const isLastDueToMax = index === effectiveMaxIterations - 1;
446
- if (isFirst && (options === null || options === void 0 ? void 0 : options.onFirst))
447
- yield options.onFirst({ index, rows, allData });
448
- const returnValue = yield callback({ index, isFirst, isLast, rows, allData, table: restrictedTable });
449
- allData.push(returnValue);
450
- const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
451
- paginationResult = yield paginationStrategy(context);
452
- isLast = getIsLast({ index, paginationResult }) || isLastDueToMax;
453
- if (isLast && (options === null || options === void 0 ? void 0 : options.onLast))
454
- yield options.onLast({ index, rows, allData });
455
- if (isLast || !paginationResult) {
456
- logDebug(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult})`);
457
- break;
451
+ // Add rows to batch if batching is enabled
452
+ if (isBatching) {
453
+ batchRows.push(...rows);
454
+ }
455
+ const isLastIteration = index === effectiveMaxIterations - 1;
456
+ // Determine if we should invoke the callback
457
+ const batchComplete = isBatching && (index - batchStartIndex + 1) >= batchSize;
458
+ const shouldInvokeCallback = !isBatching || batchComplete || isLastIteration;
459
+ if (shouldInvokeCallback) {
460
+ const callbackRows = isBatching ? batchRows : rows;
461
+ const callbackIndex = isBatching ? batchStartIndex : index;
462
+ const isFirst = getIsFirst({ index: callbackIndex });
463
+ let isLast = getIsLast({ index: callbackIndex, paginationResult });
464
+ const isLastDueToMax = index === effectiveMaxIterations - 1;
465
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
466
+ yield options.beforeFirst({ index: callbackIndex, rows: callbackRows, allData });
467
+ }
468
+ const batchInfo = isBatching ? {
469
+ startIndex: batchStartIndex,
470
+ endIndex: index,
471
+ size: index - batchStartIndex + 1
472
+ } : undefined;
473
+ const returnValue = yield callback({
474
+ index: callbackIndex,
475
+ isFirst,
476
+ isLast,
477
+ rows: callbackRows,
478
+ allData,
479
+ table: restrictedTable,
480
+ batchInfo
481
+ });
482
+ allData.push(returnValue);
483
+ // Determine if this is truly the last iteration
484
+ let finalIsLast = isLastDueToMax;
485
+ if (!isLastIteration) {
486
+ const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
487
+ paginationResult = yield paginationStrategy(context);
488
+ finalIsLast = getIsLast({ index: callbackIndex, paginationResult }) || !paginationResult;
489
+ }
490
+ if (finalIsLast && (options === null || options === void 0 ? void 0 : options.afterLast)) {
491
+ yield options.afterLast({ index: callbackIndex, rows: callbackRows, allData });
492
+ }
493
+ if (finalIsLast || !paginationResult) {
494
+ logDebug(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult})`);
495
+ break;
496
+ }
497
+ // Reset batch
498
+ if (isBatching) {
499
+ batchRows = [];
500
+ batchStartIndex = index + 1;
501
+ }
502
+ }
503
+ else {
504
+ // Continue paginating even when batching
505
+ const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
506
+ paginationResult = yield paginationStrategy(context);
507
+ if (!paginationResult) {
508
+ // Pagination failed, invoke callback with current batch
509
+ const callbackIndex = batchStartIndex;
510
+ const isFirst = getIsFirst({ index: callbackIndex });
511
+ const isLast = true;
512
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
513
+ yield options.beforeFirst({ index: callbackIndex, rows: batchRows, allData });
514
+ }
515
+ const batchInfo = {
516
+ startIndex: batchStartIndex,
517
+ endIndex: index,
518
+ size: index - batchStartIndex + 1
519
+ };
520
+ const returnValue = yield callback({
521
+ index: callbackIndex,
522
+ isFirst,
523
+ isLast,
524
+ rows: batchRows,
525
+ allData,
526
+ table: restrictedTable,
527
+ batchInfo
528
+ });
529
+ allData.push(returnValue);
530
+ if (options === null || options === void 0 ? void 0 : options.afterLast) {
531
+ yield options.afterLast({ index: callbackIndex, rows: batchRows, allData });
532
+ }
533
+ logDebug(`Pagination failed mid-batch (index: ${index})`);
534
+ break;
535
+ }
458
536
  }
459
537
  index++;
460
538
  logDebug(`Iteration ${index} completed, continuing...`);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Calculate Levenshtein distance between two strings
3
+ * Used for "did you mean?" suggestions
4
+ */
5
+ export declare function levenshteinDistance(a: string, b: string): number;
6
+ /**
7
+ * Calculate similarity score between two strings (0-1)
8
+ * 1 = identical, 0 = completely different
9
+ */
10
+ export declare function stringSimilarity(a: string, b: string): number;
11
+ /**
12
+ * Find similar strings from a list
13
+ * Returns matches above threshold, sorted by similarity
14
+ */
15
+ export declare function findSimilar(input: string, available: string[], threshold?: number): Array<{
16
+ value: string;
17
+ score: number;
18
+ }>;
19
+ /**
20
+ * Build a helpful error message for column not found
21
+ */
22
+ export declare function buildColumnNotFoundError(columnName: string, availableColumns: string[]): string;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.levenshteinDistance = levenshteinDistance;
4
+ exports.stringSimilarity = stringSimilarity;
5
+ exports.findSimilar = findSimilar;
6
+ exports.buildColumnNotFoundError = buildColumnNotFoundError;
7
+ /**
8
+ * Calculate Levenshtein distance between two strings
9
+ * Used for "did you mean?" suggestions
10
+ */
11
+ function levenshteinDistance(a, b) {
12
+ const matrix = [];
13
+ for (let i = 0; i <= b.length; i++) {
14
+ matrix[i] = [i];
15
+ }
16
+ for (let j = 0; j <= a.length; j++) {
17
+ matrix[0][j] = j;
18
+ }
19
+ for (let i = 1; i <= b.length; i++) {
20
+ for (let j = 1; j <= a.length; j++) {
21
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
22
+ matrix[i][j] = matrix[i - 1][j - 1];
23
+ }
24
+ else {
25
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
26
+ matrix[i][j - 1] + 1, // insertion
27
+ matrix[i - 1][j] + 1 // deletion
28
+ );
29
+ }
30
+ }
31
+ }
32
+ return matrix[b.length][a.length];
33
+ }
34
+ /**
35
+ * Calculate similarity score between two strings (0-1)
36
+ * 1 = identical, 0 = completely different
37
+ */
38
+ function stringSimilarity(a, b) {
39
+ const distance = levenshteinDistance(a.toLowerCase(), b.toLowerCase());
40
+ const maxLen = Math.max(a.length, b.length);
41
+ return maxLen === 0 ? 1 : 1 - (distance / maxLen);
42
+ }
43
+ /**
44
+ * Find similar strings from a list
45
+ * Returns matches above threshold, sorted by similarity
46
+ */
47
+ function findSimilar(input, available, threshold = 0.5) {
48
+ return available
49
+ .map(value => ({
50
+ value,
51
+ score: stringSimilarity(input, value)
52
+ }))
53
+ .filter(x => x.score >= threshold)
54
+ .sort((a, b) => b.score - a.score)
55
+ .slice(0, 3); // Top 3 suggestions
56
+ }
57
+ /**
58
+ * Build a helpful error message for column not found
59
+ */
60
+ function buildColumnNotFoundError(columnName, availableColumns) {
61
+ const suggestions = findSimilar(columnName, availableColumns);
62
+ let message = `Column '${columnName}' not found`;
63
+ if (suggestions.length > 0) {
64
+ message += `\n\nDid you mean:`;
65
+ suggestions.forEach(({ value, score }) => {
66
+ const percentage = Math.round(score * 100);
67
+ message += `\n • ${value} (${percentage}% match)`;
68
+ });
69
+ }
70
+ message += `\n\nAvailable columns: ${availableColumns.join(', ')}`;
71
+ message += `\n\nTip: Column names are case-sensitive`;
72
+ return message;
73
+ }
@@ -0,0 +1,11 @@
1
+ import type { Page } from '@playwright/test';
2
+ /**
3
+ * Add a custom trace event to Playwright's trace viewer
4
+ * Uses page.evaluate to log events that appear in the trace
5
+ */
6
+ export declare function addTraceEvent(page: Page, type: string, data?: Record<string, any>): Promise<void>;
7
+ /**
8
+ * Check if tracing is currently enabled
9
+ * Used for conditional trace logic
10
+ */
11
+ export declare function isTracingEnabled(page: Page): Promise<boolean>;
@@ -0,0 +1,47 @@
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.addTraceEvent = addTraceEvent;
13
+ exports.isTracingEnabled = isTracingEnabled;
14
+ /**
15
+ * Add a custom trace event to Playwright's trace viewer
16
+ * Uses page.evaluate to log events that appear in the trace
17
+ */
18
+ function addTraceEvent(page_1, type_1) {
19
+ return __awaiter(this, arguments, void 0, function* (page, type, data = {}) {
20
+ try {
21
+ // Add a console log that will appear in the trace viewer
22
+ // Prefix with [SmartTable] for easy filtering
23
+ const message = `[SmartTable:${type}] ${JSON.stringify(data)}`;
24
+ yield page.evaluate((msg) => console.log(msg), message);
25
+ }
26
+ catch (_a) {
27
+ // Silently ignore if page is not available
28
+ // This ensures zero overhead when tracing is off
29
+ }
30
+ });
31
+ }
32
+ /**
33
+ * Check if tracing is currently enabled
34
+ * Used for conditional trace logic
35
+ */
36
+ function isTracingEnabled(page) {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ try {
39
+ // We can't directly check if tracing is enabled
40
+ // But we can safely call addTraceEvent - it will just be a no-op if not tracing
41
+ return true;
42
+ }
43
+ catch (_a) {
44
+ return false;
45
+ }
46
+ });
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Production-ready table testing for Playwright with smart column-aware locators. Core library with plugin support for custom table implementations.",
5
5
  "repository": {
6
6
  "type": "git",