@rickcedwhat/playwright-smart-table 5.0.0 → 5.2.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
@@ -117,7 +117,7 @@ await expect(row).toBeVisible();
117
117
  ```
118
118
  <!-- /embed: advanced-debug -->
119
119
 
120
- This will log header mappings, row scans, and pagination triggers to help troubleshoot issues.
120
+ This will log header mappings, row scans, and pagination triggers to the console, and slow down operations to help you see what's happening.
121
121
 
122
122
  ### Resetting Table State
123
123
 
@@ -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
@@ -889,8 +922,8 @@ export interface TableConfig {
889
922
  headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
890
923
  /** Automatically scroll to table on init */
891
924
  autoScroll?: boolean;
892
- /** Enable debug logs */
893
- debug?: boolean;
925
+ /** Debug options for development and troubleshooting */
926
+ debug?: DebugConfig;
894
927
  /** Reset hook */
895
928
  onReset?: (context: TableContext) => Promise<void>;
896
929
  /** All interaction strategies */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FilterEngine = void 0;
4
+ const stringUtils_1 = require("./utils/stringUtils");
4
5
  class FilterEngine {
5
6
  constructor(config, resolve) {
6
7
  this.config = config;
@@ -17,7 +18,7 @@ class FilterEngine {
17
18
  const colIndex = map.get(colName);
18
19
  // TODO: Use ColumnStrategy for better resolution error handling
19
20
  if (colIndex === undefined) {
20
- throw new Error(`Filter Error: Column "${colName}" not found.`);
21
+ throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
21
22
  }
22
23
  const filterVal = typeof value === 'number' ? String(value) : value;
23
24
  // Use strategy if provided (For future: configured filter strategies)
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 debugUtils_1 = require("./utils/debugUtils");
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({
@@ -112,10 +113,13 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
112
113
  return result;
113
114
  });
114
115
  smart.smartFill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
116
+ (0, debugUtils_1.logDebug)(config, 'info', 'Filling row', data);
115
117
  for (const [colName, value] of Object.entries(data)) {
118
+ if (value === undefined)
119
+ continue;
116
120
  const colIdx = map.get(colName);
117
121
  if (colIdx === undefined) {
118
- throw new Error(`Column "${colName}" not found in fill data.`);
122
+ throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
119
123
  }
120
124
  yield config.strategies.cellNavigation({
121
125
  config: config,
@@ -128,6 +132,7 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
128
132
  rowIndex: rowIndex
129
133
  });
130
134
  const strategy = config.strategies.fill || fill_1.FillStrategies.default;
135
+ (0, debugUtils_1.logDebug)(config, 'verbose', `Filling cell "${colName}" with value`, value);
131
136
  yield strategy({
132
137
  row: smart,
133
138
  columnName: colName,
@@ -138,7 +143,10 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
138
143
  table: table,
139
144
  fillOptions
140
145
  });
146
+ // Delay after filling
147
+ yield (0, debugUtils_1.debugDelay)(config, 'getCell');
141
148
  }
149
+ (0, debugUtils_1.logDebug)(config, 'info', 'Row fill complete');
142
150
  });
143
151
  smart.bringIntoView = () => __awaiter(void 0, void 0, void 0, function* () {
144
152
  if (rowIndex === undefined) {
@@ -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\n/**\n * Debug configuration for development and troubleshooting\n */\nexport type DebugConfig = {\n /**\n * Slow down operations for debugging\n * - number: Apply same delay to all operations (ms)\n * - object: Granular delays per operation type\n */\n slow?: number | {\n pagination?: number;\n getCell?: number;\n findRow?: number;\n default?: number;\n };\n /**\n * Log level for debug output\n * - 'verbose': All logs (verbose, info, error)\n * - 'info': Info and error logs only\n * - 'error': Error logs only\n * - 'none': No logs\n */\n logLevel?: 'verbose' | 'info' | 'error' | '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 /** Debug options for development and troubleshooting */\n debug?: DebugConfig;\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?: TableConfig['debug'];\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";
@@ -133,6 +133,31 @@ export interface SortingStrategy {
133
133
  }): Promise<'asc' | 'desc' | 'none'>;
134
134
  }
135
135
 
136
+ /**
137
+ * Debug configuration for development and troubleshooting
138
+ */
139
+ export type DebugConfig = {
140
+ /**
141
+ * Slow down operations for debugging
142
+ * - number: Apply same delay to all operations (ms)
143
+ * - object: Granular delays per operation type
144
+ */
145
+ slow?: number | {
146
+ pagination?: number;
147
+ getCell?: number;
148
+ findRow?: number;
149
+ default?: number;
150
+ };
151
+ /**
152
+ * Log level for debug output
153
+ * - 'verbose': All logs (verbose, info, error)
154
+ * - 'info': Info and error logs only
155
+ * - 'error': Error logs only
156
+ * - 'none': No logs
157
+ */
158
+ logLevel?: 'verbose' | 'info' | 'error' | 'none';
159
+ };
160
+
136
161
  export interface TableContext {
137
162
  root: Locator;
138
163
  config: FinalTableConfig;
@@ -221,8 +246,8 @@ export interface TableConfig {
221
246
  headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
222
247
  /** Automatically scroll to table on init */
223
248
  autoScroll?: boolean;
224
- /** Enable debug logs */
225
- debug?: boolean;
249
+ /** Debug options for development and troubleshooting */
250
+ debug?: DebugConfig;
226
251
  /** Reset hook */
227
252
  onReset?: (context: TableContext) => Promise<void>;
228
253
  /** All interaction strategies */
@@ -235,7 +260,7 @@ export interface FinalTableConfig extends TableConfig {
235
260
  cellSelector: string;
236
261
  maxPages: number;
237
262
  autoScroll: boolean;
238
- debug: boolean;
263
+ debug?: TableConfig['debug'];
239
264
  headerTransformer: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
240
265
  onReset: (context: TableContext) => Promise<void>;
241
266
  strategies: TableStrategies;
@@ -368,15 +393,21 @@ export interface TableResult<T = any> {
368
393
  rows: SmartRow[];
369
394
  allData: T[];
370
395
  table: RestrictedTableResult;
396
+ batchInfo?: {
397
+ startIndex: number;
398
+ endIndex: number;
399
+ size: number;
400
+ };
371
401
  }) => T | Promise<T>,
372
402
  options?: {
373
403
  pagination?: PaginationStrategy;
374
404
  dedupeStrategy?: DedupeStrategy;
375
405
  maxIterations?: number;
406
+ batchSize?: number;
376
407
  getIsFirst?: (context: { index: number }) => boolean;
377
408
  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>;
409
+ beforeFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
410
+ afterLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
380
411
  }
381
412
  ) => Promise<T[]>;
382
413
  }
package/dist/types.d.ts CHANGED
@@ -118,6 +118,30 @@ export interface SortingStrategy {
118
118
  context: StrategyContext;
119
119
  }): Promise<'asc' | 'desc' | 'none'>;
120
120
  }
121
+ /**
122
+ * Debug configuration for development and troubleshooting
123
+ */
124
+ export type DebugConfig = {
125
+ /**
126
+ * Slow down operations for debugging
127
+ * - number: Apply same delay to all operations (ms)
128
+ * - object: Granular delays per operation type
129
+ */
130
+ slow?: number | {
131
+ pagination?: number;
132
+ getCell?: number;
133
+ findRow?: number;
134
+ default?: number;
135
+ };
136
+ /**
137
+ * Log level for debug output
138
+ * - 'verbose': All logs (verbose, info, error)
139
+ * - 'info': Info and error logs only
140
+ * - 'error': Error logs only
141
+ * - 'none': No logs
142
+ */
143
+ logLevel?: 'verbose' | 'info' | 'error' | 'none';
144
+ };
121
145
  export interface TableContext {
122
146
  root: Locator;
123
147
  config: FinalTableConfig;
@@ -206,8 +230,8 @@ export interface TableConfig {
206
230
  }) => string | Promise<string>;
207
231
  /** Automatically scroll to table on init */
208
232
  autoScroll?: boolean;
209
- /** Enable debug logs */
210
- debug?: boolean;
233
+ /** Debug options for development and troubleshooting */
234
+ debug?: DebugConfig;
211
235
  /** Reset hook */
212
236
  onReset?: (context: TableContext) => Promise<void>;
213
237
  /** All interaction strategies */
@@ -219,7 +243,7 @@ export interface FinalTableConfig extends TableConfig {
219
243
  cellSelector: string;
220
244
  maxPages: number;
221
245
  autoScroll: boolean;
222
- debug: boolean;
246
+ debug?: TableConfig['debug'];
223
247
  headerTransformer: (args: {
224
248
  text: string;
225
249
  index: number;
@@ -347,10 +371,16 @@ export interface TableResult<T = any> {
347
371
  rows: SmartRow[];
348
372
  allData: T[];
349
373
  table: RestrictedTableResult;
374
+ batchInfo?: {
375
+ startIndex: number;
376
+ endIndex: number;
377
+ size: number;
378
+ };
350
379
  }) => T | Promise<T>, options?: {
351
380
  pagination?: PaginationStrategy;
352
381
  dedupeStrategy?: DedupeStrategy;
353
382
  maxIterations?: number;
383
+ batchSize?: number;
354
384
  getIsFirst?: (context: {
355
385
  index: number;
356
386
  }) => boolean;
@@ -358,12 +388,12 @@ export interface TableResult<T = any> {
358
388
  index: number;
359
389
  paginationResult: boolean;
360
390
  }) => boolean;
361
- onFirst?: (context: {
391
+ beforeFirst?: (context: {
362
392
  index: number;
363
393
  rows: SmartRow[];
364
394
  allData: any[];
365
395
  }) => void | Promise<void>;
366
- onLast?: (context: {
396
+ afterLast?: (context: {
367
397
  index: number;
368
398
  rows: SmartRow[];
369
399
  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 debugUtils_1 = require("./utils/debugUtils");
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, 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);
@@ -53,9 +54,8 @@ const useTable = (rootLocator, configOptions = {}) => {
53
54
  let _hasPaginated = false;
54
55
  let _isInitialized = false;
55
56
  // Helpers
56
- const logDebug = (msg) => {
57
- if (config.debug)
58
- console.log(`🔎 [SmartTable Debug] ${msg}`);
57
+ const log = (msg) => {
58
+ (0, debugUtils_1.logDebug)(config, 'verbose', msg); // Legacy(`🔎 [SmartTable Debug] ${msg}`);
59
59
  };
60
60
  const _createColumnError = (colName, map, context) => {
61
61
  const availableColumns = Array.from(map.keys());
@@ -80,7 +80,7 @@ const useTable = (rootLocator, configOptions = {}) => {
80
80
  const _getMap = (timeout) => __awaiter(void 0, void 0, void 0, function* () {
81
81
  if (_headerMap)
82
82
  return _headerMap;
83
- logDebug('Mapping headers...');
83
+ log('Mapping headers...');
84
84
  const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
85
85
  if (config.autoScroll) {
86
86
  try {
@@ -112,8 +112,25 @@ const useTable = (rootLocator, configOptions = {}) => {
112
112
  }
113
113
  return [text, i];
114
114
  })));
115
+ // Validation: Check for empty table
116
+ if (entries.length === 0) {
117
+ throw new Error(`Initialization Error: No columns found using selector "${config.headerSelector}". Check your selector or ensure the table is visible.`);
118
+ }
119
+ // Validation: Check for duplicates
120
+ const seen = new Set();
121
+ const duplicates = new Set();
122
+ for (const [name] of entries) {
123
+ if (seen.has(name)) {
124
+ duplicates.add(name);
125
+ }
126
+ seen.add(name);
127
+ }
128
+ if (duplicates.size > 0) {
129
+ const dupList = Array.from(duplicates).map(d => `"${d}"`).join(', ');
130
+ throw new Error(`Initialization Error: Duplicate column names found: ${dupList}. Use 'headerTransformer' to rename duplicate columns.`);
131
+ }
115
132
  _headerMap = new Map(entries);
116
- logDebug(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
133
+ log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
117
134
  return _headerMap;
118
135
  });
119
136
  // Placeholder for the final table object
@@ -129,13 +146,13 @@ const useTable = (rootLocator, configOptions = {}) => {
129
146
  const map = yield _getMap();
130
147
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
131
148
  let currentPage = 1;
132
- logDebug(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
149
+ log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
133
150
  while (true) {
134
151
  const allRows = resolve(config.rowSelector, rootLocator);
135
152
  // Use FilterEngine
136
153
  const matchedRows = filterEngine.applyFilters(allRows, filters, map, options.exact || false, rootLocator.page());
137
154
  const count = yield matchedRows.count();
138
- logDebug(`Page ${currentPage}: Found ${count} matches.`);
155
+ log(`Page ${currentPage}: Found ${count} matches.`);
139
156
  if (count > 1) {
140
157
  // Sample data logic (simplified for refactor, kept inline or moved to util if needed)
141
158
  const sampleData = [];
@@ -155,7 +172,7 @@ const useTable = (rootLocator, configOptions = {}) => {
155
172
  if (count === 1)
156
173
  return matchedRows.first();
157
174
  if (currentPage < effectiveMaxPages) {
158
- logDebug(`Page ${currentPage}: Not found. Attempting pagination...`);
175
+ log(`Page ${currentPage}: Not found. Attempting pagination...`);
159
176
  const context = {
160
177
  root: rootLocator,
161
178
  config: config,
@@ -170,7 +187,7 @@ const useTable = (rootLocator, configOptions = {}) => {
170
187
  continue;
171
188
  }
172
189
  else {
173
- logDebug(`Page ${currentPage}: Pagination failed (end of data).`);
190
+ log(`Page ${currentPage}: Pagination failed (end of data).`);
174
191
  }
175
192
  }
176
193
  if (_hasPaginated) {
@@ -223,8 +240,15 @@ const useTable = (rootLocator, configOptions = {}) => {
223
240
  init: (options) => __awaiter(void 0, void 0, void 0, function* () {
224
241
  if (_isInitialized && _headerMap)
225
242
  return result;
243
+ (0, debugUtils_1.warnIfDebugInCI)(config);
244
+ (0, debugUtils_1.logDebug)(config, 'info', 'Initializing table');
226
245
  yield _getMap(options === null || options === void 0 ? void 0 : options.timeout);
227
246
  _isInitialized = true;
247
+ if (_headerMap) {
248
+ (0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${_headerMap.size} columns`, Array.from(_headerMap.keys()));
249
+ // Trace event removed - redundant with debug logging
250
+ }
251
+ yield (0, debugUtils_1.debugDelay)(config, 'default');
228
252
  return result;
229
253
  }),
230
254
  scrollToColumn: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
@@ -255,19 +279,19 @@ const useTable = (rootLocator, configOptions = {}) => {
255
279
  return resolve(config.headerSelector, rootLocator).nth(idx);
256
280
  }),
257
281
  reset: () => __awaiter(void 0, void 0, void 0, function* () {
258
- logDebug("Resetting table...");
282
+ log("Resetting table...");
259
283
  const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
260
284
  yield config.onReset(context);
261
285
  _hasPaginated = false;
262
286
  _headerMap = null;
263
287
  _isInitialized = false;
264
- logDebug("Table reset complete.");
288
+ log("Table reset complete.");
265
289
  }),
266
290
  revalidate: () => __awaiter(void 0, void 0, void 0, function* () {
267
- logDebug("Revalidating table structure...");
291
+ log("Revalidating table structure...");
268
292
  _headerMap = null; // Clear the map to force re-scanning
269
293
  yield _getMap(); // Re-scan headers
270
- logDebug("Table revalidated.");
294
+ log("Table revalidated.");
271
295
  }),
272
296
  getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
273
297
  var _a, _b;
@@ -279,7 +303,7 @@ const useTable = (rootLocator, configOptions = {}) => {
279
303
  const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
280
304
  let currentPage = 1;
281
305
  const results = [];
282
- logDebug(`Getting column values for '${column}' (Pages: ${effectiveMaxPages})`);
306
+ log(`Getting column values for '${column}' (Pages: ${effectiveMaxPages})`);
283
307
  while (true) {
284
308
  const rows = yield resolve(config.rowSelector, rootLocator).all();
285
309
  for (const row of rows) {
@@ -316,11 +340,18 @@ const useTable = (rootLocator, configOptions = {}) => {
316
340
  return _makeSmart(rowLocator, _headerMap, rowIndex);
317
341
  },
318
342
  findRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
343
+ (0, debugUtils_1.logDebug)(config, 'info', 'Searching for row', filters);
319
344
  yield _ensureInitialized();
320
345
  let row = yield _findRowLocator(filters, options);
321
- if (!row) {
322
- row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
346
+ if (row) {
347
+ (0, debugUtils_1.logDebug)(config, 'info', 'Row found');
348
+ yield (0, debugUtils_1.debugDelay)(config, 'findRow');
349
+ return _makeSmart(row, _headerMap, 0);
323
350
  }
351
+ (0, debugUtils_1.logDebug)(config, 'error', 'Row not found', filters);
352
+ yield (0, debugUtils_1.debugDelay)(config, 'findRow');
353
+ // Return sentinel row
354
+ row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
324
355
  return _makeSmart(row, _headerMap, 0);
325
356
  }),
326
357
  getRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
@@ -379,7 +410,7 @@ const useTable = (rootLocator, configOptions = {}) => {
379
410
  yield _ensureInitialized();
380
411
  if (!config.strategies.sorting)
381
412
  throw new Error('No sorting strategy has been configured.');
382
- logDebug(`Applying sort for column "${columnName}" (${direction})`);
413
+ log(`Applying sort for column "${columnName}" (${direction})`);
383
414
  const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
384
415
  yield config.strategies.sorting.doSort({ columnName, direction, context });
385
416
  }),
@@ -419,10 +450,14 @@ const useTable = (rootLocator, configOptions = {}) => {
419
450
  const getIsLast = (_c = options === null || options === void 0 ? void 0 : options.getIsLast) !== null && _c !== void 0 ? _c : (() => false);
420
451
  const allData = [];
421
452
  const effectiveMaxIterations = (_d = options === null || options === void 0 ? void 0 : options.maxIterations) !== null && _d !== void 0 ? _d : config.maxPages;
453
+ const batchSize = options === null || options === void 0 ? void 0 : options.batchSize;
454
+ const isBatching = batchSize !== undefined && batchSize > 1;
422
455
  let index = 0;
423
456
  let paginationResult = true;
424
457
  let seenKeys = null;
425
- logDebug(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations})`);
458
+ let batchRows = [];
459
+ let batchStartIndex = 0;
460
+ log(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations}, batchSize: ${batchSize !== null && batchSize !== void 0 ? batchSize : 'none'})`);
426
461
  while (index < effectiveMaxIterations) {
427
462
  const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
428
463
  let rows = rowLocators.map((loc, i) => _makeSmart(loc, _headerMap, i));
@@ -438,28 +473,102 @@ const useTable = (rootLocator, configOptions = {}) => {
438
473
  }
439
474
  }
440
475
  rows = deduplicated;
441
- logDebug(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
476
+ log(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
442
477
  }
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;
478
+ // Add rows to batch if batching is enabled
479
+ if (isBatching) {
480
+ batchRows.push(...rows);
481
+ }
482
+ const isLastIteration = index === effectiveMaxIterations - 1;
483
+ // Determine if we should invoke the callback
484
+ const batchComplete = isBatching && (index - batchStartIndex + 1) >= batchSize;
485
+ const shouldInvokeCallback = !isBatching || batchComplete || isLastIteration;
486
+ if (shouldInvokeCallback) {
487
+ const callbackRows = isBatching ? batchRows : rows;
488
+ const callbackIndex = isBatching ? batchStartIndex : index;
489
+ const isFirst = getIsFirst({ index: callbackIndex });
490
+ let isLast = getIsLast({ index: callbackIndex, paginationResult });
491
+ const isLastDueToMax = index === effectiveMaxIterations - 1;
492
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
493
+ yield options.beforeFirst({ index: callbackIndex, rows: callbackRows, allData });
494
+ }
495
+ const batchInfo = isBatching ? {
496
+ startIndex: batchStartIndex,
497
+ endIndex: index,
498
+ size: index - batchStartIndex + 1
499
+ } : undefined;
500
+ const returnValue = yield callback({
501
+ index: callbackIndex,
502
+ isFirst,
503
+ isLast,
504
+ rows: callbackRows,
505
+ allData,
506
+ table: restrictedTable,
507
+ batchInfo
508
+ });
509
+ allData.push(returnValue);
510
+ // Determine if this is truly the last iteration
511
+ let finalIsLast = isLastDueToMax;
512
+ if (!isLastIteration) {
513
+ const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
514
+ paginationResult = yield paginationStrategy(context);
515
+ (0, debugUtils_1.logDebug)(config, 'info', `Pagination ${paginationResult ? 'succeeded' : 'failed'}`);
516
+ yield (0, debugUtils_1.debugDelay)(config, 'pagination');
517
+ finalIsLast = getIsLast({ index: callbackIndex, paginationResult }) || !paginationResult;
518
+ }
519
+ if (finalIsLast && (options === null || options === void 0 ? void 0 : options.afterLast)) {
520
+ yield options.afterLast({ index: callbackIndex, rows: callbackRows, allData });
521
+ }
522
+ if (finalIsLast || !paginationResult) {
523
+ log(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult})`);
524
+ break;
525
+ }
526
+ // Reset batch
527
+ if (isBatching) {
528
+ batchRows = [];
529
+ batchStartIndex = index + 1;
530
+ }
531
+ }
532
+ else {
533
+ // Continue paginating even when batching
534
+ const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
535
+ paginationResult = yield paginationStrategy(context);
536
+ (0, debugUtils_1.logDebug)(config, 'info', `Pagination ${paginationResult ? 'succeeded' : 'failed'} (batching mode)`);
537
+ yield (0, debugUtils_1.debugDelay)(config, 'pagination');
538
+ if (!paginationResult) {
539
+ // Pagination failed, invoke callback with current batch
540
+ const callbackIndex = batchStartIndex;
541
+ const isFirst = getIsFirst({ index: callbackIndex });
542
+ const isLast = true;
543
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
544
+ yield options.beforeFirst({ index: callbackIndex, rows: batchRows, allData });
545
+ }
546
+ const batchInfo = {
547
+ startIndex: batchStartIndex,
548
+ endIndex: index,
549
+ size: index - batchStartIndex + 1
550
+ };
551
+ const returnValue = yield callback({
552
+ index: callbackIndex,
553
+ isFirst,
554
+ isLast,
555
+ rows: batchRows,
556
+ allData,
557
+ table: restrictedTable,
558
+ batchInfo
559
+ });
560
+ allData.push(returnValue);
561
+ if (options === null || options === void 0 ? void 0 : options.afterLast) {
562
+ yield options.afterLast({ index: callbackIndex, rows: batchRows, allData });
563
+ }
564
+ log(`Pagination failed mid-batch (index: ${index})`);
565
+ break;
566
+ }
458
567
  }
459
568
  index++;
460
- logDebug(`Iteration ${index} completed, continuing...`);
569
+ log(`Iteration ${index} completed, continuing...`);
461
570
  }
462
- logDebug(`iterateThroughTable completed after ${index + 1} iterations, collected ${allData.length} items`);
571
+ log(`iterateThroughTable completed after ${index + 1} iterations, collected ${allData.length} items`);
463
572
  return allData;
464
573
  }),
465
574
  };
@@ -0,0 +1,17 @@
1
+ import type { FinalTableConfig } from '../types';
2
+ /**
3
+ * Get delay for specific action type
4
+ */
5
+ export declare function getDebugDelay(config: FinalTableConfig, actionType: 'pagination' | 'getCell' | 'findRow' | 'default'): number;
6
+ /**
7
+ * Add debug delay for specific action type
8
+ */
9
+ export declare function debugDelay(config: FinalTableConfig, actionType: 'pagination' | 'getCell' | 'findRow' | 'default'): Promise<void>;
10
+ /**
11
+ * Log debug message based on log level
12
+ */
13
+ export declare function logDebug(config: FinalTableConfig, level: 'error' | 'info' | 'verbose', message: string, data?: any): void;
14
+ /**
15
+ * Warn if debug.slow is enabled in CI environment
16
+ */
17
+ export declare function warnIfDebugInCI(config: FinalTableConfig): void;
@@ -0,0 +1,62 @@
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.getDebugDelay = getDebugDelay;
13
+ exports.debugDelay = debugDelay;
14
+ exports.logDebug = logDebug;
15
+ exports.warnIfDebugInCI = warnIfDebugInCI;
16
+ /**
17
+ * Get delay for specific action type
18
+ */
19
+ function getDebugDelay(config, actionType) {
20
+ var _a, _b, _c;
21
+ if (!((_a = config.debug) === null || _a === void 0 ? void 0 : _a.slow))
22
+ return 0;
23
+ if (typeof config.debug.slow === 'number') {
24
+ return config.debug.slow;
25
+ }
26
+ return (_c = (_b = config.debug.slow[actionType]) !== null && _b !== void 0 ? _b : config.debug.slow.default) !== null && _c !== void 0 ? _c : 0;
27
+ }
28
+ /**
29
+ * Add debug delay for specific action type
30
+ */
31
+ function debugDelay(config, actionType) {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ const delay = getDebugDelay(config, actionType);
34
+ if (delay > 0) {
35
+ yield new Promise(resolve => setTimeout(resolve, delay));
36
+ }
37
+ });
38
+ }
39
+ /**
40
+ * Log debug message based on log level
41
+ */
42
+ function logDebug(config, level, message, data) {
43
+ var _a, _b;
44
+ const logLevel = (_b = (_a = config.debug) === null || _a === void 0 ? void 0 : _a.logLevel) !== null && _b !== void 0 ? _b : 'none';
45
+ const levels = { none: 0, error: 1, info: 2, verbose: 3 };
46
+ if (levels[logLevel] >= levels[level]) {
47
+ const prefix = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
48
+ const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
49
+ console.log(`${prefix} [${timestamp}] [SmartTable] ${message}`, data !== null && data !== void 0 ? data : '');
50
+ }
51
+ }
52
+ /**
53
+ * Warn if debug.slow is enabled in CI environment
54
+ */
55
+ function warnIfDebugInCI(config) {
56
+ var _a;
57
+ if (process.env.CI === 'true' && ((_a = config.debug) === null || _a === void 0 ? void 0 : _a.slow)) {
58
+ console.warn('⚠️ [SmartTable] Warning: debug.slow is enabled in CI environment.\n' +
59
+ ' This will significantly slow down test execution.\n' +
60
+ ' Consider disabling debug mode in CI.');
61
+ }
62
+ }
@@ -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.2.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",