@rickcedwhat/playwright-smart-table 2.3.1 → 3.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
@@ -21,14 +21,15 @@ For standard HTML tables (`<table>`, `<tr>`, `<td>`), the library works out of t
21
21
  <!-- embed: quick-start -->
22
22
  ```typescript
23
23
  // Example from: https://datatables.net/examples/data_sources/dom
24
- const table = useTable(page.locator('#example'), {
24
+ const table = await useTable(page.locator('#example'), {
25
25
  headerSelector: 'thead th' // Override for this specific site
26
- });
26
+ }).init();
27
27
 
28
28
  // Find the row with Name="Airi Satou", then get the Position cell
29
- const row = await table.getByRow({ Name: 'Airi Satou' });
29
+ const row = table.getByRow({ Name: 'Airi Satou' });
30
30
 
31
- await expect(row.getCell('Position')).toHaveText('Accountant');
31
+ const positionCell = row.getCell('Position');
32
+ await expect(positionCell).toHaveText('Accountant');
32
33
  ```
33
34
  <!-- /embed: quick-start -->
34
35
 
@@ -46,10 +47,11 @@ The `SmartRow` is the core power of this library. Unlike a standard Playwright `
46
47
  // Example from: https://datatables.net/examples/data_sources/dom
47
48
 
48
49
  // Get SmartRow via getByRow
49
- const row = await table.getByRow({ Name: 'Airi Satou' });
50
+ const row = table.getByRow({ Name: 'Airi Satou' });
50
51
 
51
52
  // Interact with cell using column name (resilient to column reordering)
52
- await row.getCell('Position').click();
53
+ const positionCell = row.getCell('Position');
54
+ await positionCell.click();
53
55
 
54
56
  // Extract row data as JSON
55
57
  const data = await row.toJSON();
@@ -84,11 +86,13 @@ const table = useTable(page.locator('#example'), {
84
86
  ),
85
87
  maxPages: 5 // Allow scanning up to 5 pages
86
88
  });
89
+ await table.init();
87
90
 
88
91
  // ✅ Verify Colleen is NOT visible initially
89
92
  await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
90
93
 
91
- await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
94
+ // Use searchForRow for pagination
95
+ await expect(await table.searchForRow({ Name: "Colleen Hurst" })).toBeVisible();
92
96
  // NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
93
97
  ```
94
98
  <!-- /embed: pagination -->
@@ -104,8 +108,9 @@ const table = useTable(page.locator('#example'), {
104
108
  headerSelector: 'thead th',
105
109
  debug: true // Enables verbose logging of internal operations
106
110
  });
111
+ await table.init();
107
112
 
108
- const row = await table.getByRow({ Name: 'Airi Satou' });
113
+ const row = table.getByRow({ Name: 'Airi Satou' });
109
114
  await expect(row).toBeVisible();
110
115
  ```
111
116
  <!-- /embed: advanced-debug -->
@@ -121,15 +126,16 @@ If your tests navigate deep into a paginated table, use `.reset()` to return to
121
126
  // Example from: https://datatables.net/examples/data_sources/dom
122
127
  // Navigate deep into the table by searching for a row on a later page
123
128
  try {
124
- await table.getByRow({ Name: 'Angelica Ramos' });
129
+ await table.searchForRow({ Name: 'Angelica Ramos' });
125
130
  } catch (e) {}
126
131
 
127
- // Reset internal state (and potentially UI) to Page 1
128
- await table.reset();
132
+ // Reset internal state (and potentially UI) to initial page
133
+ await table.reset();
134
+ await table.init(); // Re-init after reset
129
135
 
130
136
  // Now subsequent searches start from the beginning
131
- const firstPageRow = await table.getByRow({ Name: 'Airi Satou' });
132
- await expect(firstPageRow).toBeVisible();
137
+ const currentPageRow = table.getByRow({ Name: 'Airi Satou' });
138
+ await expect(currentPageRow).toBeVisible();
133
139
  ```
134
140
  <!-- /embed: advanced-reset -->
135
141
 
@@ -149,14 +155,14 @@ expect(offices.length).toBeGreaterThan(0);
149
155
 
150
156
  ### Filling Row Data
151
157
 
152
- Use `fill()` to intelligently populate form fields in a table row. The method automatically detects input types (text inputs, selects, checkboxes, contenteditable divs) and fills them appropriately.
158
+ Use `smartFill()` to intelligently populate form fields in a table row. The method automatically detects input types (text inputs, selects, checkboxes, contenteditable divs) and fills them appropriately. You can still use Locator's standard `fill()` method for single-input scenarios.
153
159
 
154
160
  <!-- embed: fill-basic -->
155
161
  ```typescript
156
162
  // Find a row and fill it with new data
157
- const row = await table.getByRow({ ID: '1' });
163
+ const row = table.getByRow({ ID: '1' });
158
164
 
159
- await row.fill({
165
+ await row.smartFill({
160
166
  Name: 'John Updated',
161
167
  Status: 'Inactive',
162
168
  Active: false,
@@ -164,10 +170,14 @@ await row.fill({
164
170
  });
165
171
 
166
172
  // Verify the values were filled correctly
167
- await expect(row.getCell('Name').locator('input')).toHaveValue('John Updated');
168
- await expect(row.getCell('Status').locator('select')).toHaveValue('Inactive');
169
- await expect(row.getCell('Active').locator('input[type="checkbox"]')).not.toBeChecked();
170
- await expect(row.getCell('Notes').locator('textarea')).toHaveValue('Updated notes here');
173
+ const nameCell = row.getCell('Name');
174
+ const statusCell = row.getCell('Status');
175
+ const activeCell = row.getCell('Active');
176
+ const notesCell = row.getCell('Notes');
177
+ await expect(nameCell.locator('input')).toHaveValue('John Updated');
178
+ await expect(statusCell.locator('select')).toHaveValue('Inactive');
179
+ await expect(activeCell.locator('input[type="checkbox"]')).not.toBeChecked();
180
+ await expect(notesCell.locator('textarea')).toHaveValue('Updated notes here');
171
181
  ```
172
182
  <!-- /embed: fill-basic -->
173
183
 
@@ -183,10 +193,10 @@ For edge cases where auto-detection doesn't work (e.g., custom components, multi
183
193
 
184
194
  <!-- embed: fill-custom-mappers -->
185
195
  ```typescript
186
- const row = await table.getByRow({ ID: '1' });
196
+ const row = table.getByRow({ ID: '1' });
187
197
 
188
198
  // Use custom input mappers for specific columns
189
- await row.fill({
199
+ await row.smartFill({
190
200
  Name: 'John Updated',
191
201
  Status: 'Inactive'
192
202
  }, {
@@ -199,8 +209,10 @@ await row.fill({
199
209
  });
200
210
 
201
211
  // Verify the values
202
- await expect(row.getCell('Name').locator('.primary-input')).toHaveValue('John Updated');
203
- await expect(row.getCell('Status').locator('select')).toHaveValue('Inactive');
212
+ const nameCell = row.getCell('Name');
213
+ const statusCell = row.getCell('Status');
214
+ await expect(nameCell.locator('.primary-input')).toHaveValue('John Updated');
215
+ await expect(statusCell.locator('select')).toHaveValue('Inactive');
204
216
  ```
205
217
  <!-- /embed: fill-custom-mappers -->
206
218
 
@@ -232,14 +244,21 @@ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
232
244
  return text;
233
245
  }
234
246
  });
247
+ await table.init();
235
248
 
236
249
  const headers = await table.getHeaders();
237
250
  // Now we can reference the "Actions" column even if it has no header text
238
251
  expect(headers).toContain('Actions');
239
252
 
240
253
  // Use the renamed column
241
- const row = await table.getByRow({ "Last name": "Melisandre" });
242
- await row.getCell('Actions').getByLabel("Select row").click();
254
+ // First check it's not on the current page
255
+ const currentPageRow = table.getByRow({ "Last name": "Melisandre" });
256
+ await expect(currentPageRow).not.toBeVisible();
257
+
258
+ // Then find it across pages
259
+ const row = await table.searchForRow({ "Last name": "Melisandre" });
260
+ const actionsCell = row.getCell('Actions');
261
+ await actionsCell.getByLabel("Select row").click();
243
262
  ```
244
263
  <!-- /embed: header-transformer -->
245
264
 
@@ -258,10 +277,12 @@ const table = useTable(page.locator('#table1'), {
258
277
  .replace(/^\s*|\s*$/g, ''); // Remove leading/trailing spaces
259
278
  }
260
279
  });
280
+ await table.init();
261
281
 
262
282
  // Now column names are consistent
263
- const row = await table.getByRow({ "Last Name": "Doe" });
264
- await expect(row.getCell("Email")).toHaveText("jdoe@hotmail.com");
283
+ const row = table.getByRow({ "Last Name": "Doe" });
284
+ const emailCell = row.getCell("Email");
285
+ await expect(emailCell).toHaveText("jdoe@hotmail.com");
265
286
  ```
266
287
  <!-- /embed: header-transformer-normalize -->
267
288
 
@@ -292,20 +313,24 @@ getByRow: <T extends { asJSON?: boolean }>(
292
313
  <!-- embed: get-by-row -->
293
314
  ```typescript
294
315
  // Example from: https://datatables.net/examples/data_sources/dom
316
+ const table = useTable(page.locator('#example'), { headerSelector: 'thead th' });
317
+ await table.init();
318
+
295
319
  // Find a row where Name is "Airi Satou" AND Office is "Tokyo"
296
- const row = await table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
320
+ const row = table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
297
321
  await expect(row).toBeVisible();
298
322
 
299
323
  // Assert it does NOT exist
300
- await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
324
+ await expect(table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
301
325
  ```
302
326
  <!-- /embed: get-by-row -->
303
327
 
304
328
  Get row data as JSON:
305
329
  <!-- embed: get-by-row-json -->
306
330
  ```typescript
307
- // Get row data directly as JSON object
308
- const data = await table.getByRow({ Name: 'Airi Satou' }, { asJSON: true });
331
+ // Get row data as JSON object
332
+ const row = table.getByRow({ Name: 'Airi Satou' });
333
+ const data = await row.toJSON();
309
334
  // Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
310
335
 
311
336
  expect(data).toHaveProperty('Name', 'Airi Satou');
@@ -534,13 +559,13 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
534
559
 
535
560
  <!-- embed-type: SmartRow -->
536
561
  ```typescript
537
- export type SmartRow = Omit<Locator, 'fill'> & {
562
+ export type SmartRow = Locator & {
538
563
  getCell(column: string): Locator;
539
564
  toJSON(): Promise<Record<string, string>>;
540
565
  /**
541
566
  * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
542
567
  */
543
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
568
+ smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
544
569
  };
545
570
  ```
546
571
  <!-- /embed-type: SmartRow -->
@@ -548,7 +573,7 @@ export type SmartRow = Omit<Locator, 'fill'> & {
548
573
  **Methods:**
549
574
  - `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
550
575
  - `toJSON()`: Extracts all cell data as a key-value object
551
- - `fill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types or use `inputMappers` for custom control
576
+ - `smartFill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types or use `inputMappers` for custom control. Use Locator's standard `fill()` for single-input scenarios.
552
577
 
553
578
  All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
554
579
 
@@ -578,7 +603,7 @@ export interface TableConfig {
578
603
  */
579
604
  debug?: boolean;
580
605
  /**
581
- * Strategy to reset the table to the first page.
606
+ * Strategy to reset the table to the initial page.
582
607
  * Called when table.reset() is invoked.
583
608
  */
584
609
  onReset?: (context: TableContext) => Promise<void>;
@@ -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 = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Omit<Locator, 'fill'> & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext;\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 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 interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n sorting?: SortingStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n /**\n * Enable debug mode to log internal state to console.\n */\n debug?: boolean;\n /**\n * Strategy to reset the table to the first page.\n * Called when table.reset() is invoked.\n */\n onReset?: (context: TableContext) => Promise<void>;\n}\n\n/**\n * Represents the final, resolved table configuration after default values have been applied.\n * All optional properties from TableConfig are now required, except for `sorting`.\n */\nexport type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {\n sorting?: SortingStrategy;\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 * Columns not specified here will use auto-detection.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n getByRow: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\n\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => 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";
6
+ export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext;\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 interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n sorting?: SortingStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n /**\n * Enable debug mode to log internal state to console.\n */\n debug?: boolean;\n /**\n * Strategy to reset the table to the initial page.\n * Called when table.reset() is invoked.\n */\n onReset?: (context: TableContext) => Promise<void>;\n}\n\n/**\n * Represents the final, resolved table configuration after default values have been applied.\n * All optional properties from TableConfig are now required, except for `sorting`.\n */\nexport type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {\n sorting?: SortingStrategy;\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 * Columns not specified here will use auto-detection.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult {\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 getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getByRow: (\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean }\n ) => SmartRow;\n\n /**\n * Searches for a row across all available data using the configured strategy (pagination, scroll, etc.).\n * Auto-initializes if needed.\n */\n searchForRow: (\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => 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 = Omit<TableResult, 'searchForRow' | 'iterateThroughTable' | 'reset'>;\n";
@@ -9,13 +9,13 @@ exports.TYPE_CONTEXT = void 0;
9
9
  exports.TYPE_CONTEXT = `
10
10
  export type Selector = string | ((root: Locator | Page) => Locator);
11
11
 
12
- export type SmartRow = Omit<Locator, 'fill'> & {
12
+ export type SmartRow = Locator & {
13
13
  getCell(column: string): Locator;
14
14
  toJSON(): Promise<Record<string, string>>;
15
15
  /**
16
16
  * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
17
17
  */
18
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
18
+ smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
19
19
  };
20
20
 
21
21
  export type StrategyContext = TableContext;
@@ -51,6 +51,8 @@ export interface TableContext {
51
51
 
52
52
  export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
53
53
 
54
+ export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
55
+
54
56
  export interface PromptOptions {
55
57
  /**
56
58
  * Output Strategy:
@@ -81,7 +83,7 @@ export interface TableConfig {
81
83
  */
82
84
  debug?: boolean;
83
85
  /**
84
- * Strategy to reset the table to the first page.
86
+ * Strategy to reset the table to the initial page.
85
87
  * Called when table.reset() is invoked.
86
88
  */
87
89
  onReset?: (context: TableContext) => Promise<void>;
@@ -105,13 +107,32 @@ export interface FillOptions {
105
107
  }
106
108
 
107
109
  export interface TableResult {
110
+ /**
111
+ * Initializes the table by resolving headers. Must be called before using sync methods.
112
+ * @param options Optional timeout for header resolution (default: 3000ms)
113
+ */
114
+ init(options?: { timeout?: number }): Promise<TableResult>;
115
+
108
116
  getHeaders: () => Promise<string[]>;
109
117
  getHeaderCell: (columnName: string) => Promise<Locator>;
110
118
 
111
- getByRow: <T extends { asJSON?: boolean }>(
119
+ /**
120
+ * Finds a row on the current page only. Returns immediately (sync).
121
+ * Throws error if table is not initialized.
122
+ */
123
+ getByRow: (
124
+ filters: Record<string, string | RegExp | number>,
125
+ options?: { exact?: boolean }
126
+ ) => SmartRow;
127
+
128
+ /**
129
+ * Searches for a row across all available data using the configured strategy (pagination, scroll, etc.).
130
+ * Auto-initializes if needed.
131
+ */
132
+ searchForRow: (
112
133
  filters: Record<string, string | RegExp | number>,
113
- options?: { exact?: boolean, maxPages?: number } & T
114
- ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
134
+ options?: { exact?: boolean, maxPages?: number }
135
+ ) => Promise<SmartRow>;
115
136
 
116
137
  getAllRows: <T extends { asJSON?: boolean }>(
117
138
  options?: { filter?: Record<string, any>, exact?: boolean } & T
@@ -147,5 +168,34 @@ export interface TableResult {
147
168
  */
148
169
  getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
149
170
  };
171
+
172
+ /**
173
+ * Iterates through paginated table data, calling the callback for each iteration.
174
+ * Callback return values are automatically appended to allData, which is returned.
175
+ */
176
+ iterateThroughTable: <T = any>(
177
+ callback: (context: {
178
+ index: number;
179
+ isFirst: boolean;
180
+ isLast: boolean;
181
+ rows: SmartRow[];
182
+ allData: T[];
183
+ table: RestrictedTableResult;
184
+ }) => T | Promise<T>,
185
+ options?: {
186
+ pagination?: PaginationStrategy;
187
+ dedupeStrategy?: DedupeStrategy;
188
+ maxIterations?: number;
189
+ getIsFirst?: (context: { index: number }) => boolean;
190
+ getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;
191
+ onFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
192
+ onLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
193
+ }
194
+ ) => Promise<T[]>;
150
195
  }
196
+
197
+ /**
198
+ * Restricted table result that excludes methods that shouldn't be called during iteration.
199
+ */
200
+ export type RestrictedTableResult = Omit<TableResult, 'searchForRow' | 'iterateThroughTable' | 'reset'>;
151
201
  `;
package/dist/types.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type { Locator, Page } from '@playwright/test';
2
2
  export type Selector = string | ((root: Locator | Page) => Locator);
3
- export type SmartRow = Omit<Locator, 'fill'> & {
3
+ export type SmartRow = Locator & {
4
4
  getCell(column: string): Locator;
5
5
  toJSON(): Promise<Record<string, string>>;
6
6
  /**
7
7
  * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
8
8
  */
9
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
9
+ smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
10
10
  };
11
11
  export type StrategyContext = TableContext;
12
12
  /**
@@ -36,6 +36,7 @@ export interface TableContext {
36
36
  resolve: (selector: Selector, parent: Locator | Page) => Locator;
37
37
  }
38
38
  export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
39
+ export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
39
40
  export interface PromptOptions {
40
41
  /**
41
42
  * Output Strategy:
@@ -69,7 +70,7 @@ export interface TableConfig {
69
70
  */
70
71
  debug?: boolean;
71
72
  /**
72
- * Strategy to reset the table to the first page.
73
+ * Strategy to reset the table to the initial page.
73
74
  * Called when table.reset() is invoked.
74
75
  */
75
76
  onReset?: (context: TableContext) => Promise<void>;
@@ -90,14 +91,30 @@ export interface FillOptions {
90
91
  inputMappers?: Record<string, (cell: Locator) => Locator>;
91
92
  }
92
93
  export interface TableResult {
94
+ /**
95
+ * Initializes the table by resolving headers. Must be called before using sync methods.
96
+ * @param options Optional timeout for header resolution (default: 3000ms)
97
+ */
98
+ init(options?: {
99
+ timeout?: number;
100
+ }): Promise<TableResult>;
93
101
  getHeaders: () => Promise<string[]>;
94
102
  getHeaderCell: (columnName: string) => Promise<Locator>;
95
- getByRow: <T extends {
96
- asJSON?: boolean;
97
- }>(filters: Record<string, string | RegExp | number>, options?: {
103
+ /**
104
+ * Finds a row on the current page only. Returns immediately (sync).
105
+ * Throws error if table is not initialized.
106
+ */
107
+ getByRow: (filters: Record<string, string | RegExp | number>, options?: {
108
+ exact?: boolean;
109
+ }) => SmartRow;
110
+ /**
111
+ * Searches for a row across all available data using the configured strategy (pagination, scroll, etc.).
112
+ * Auto-initializes if needed.
113
+ */
114
+ searchForRow: (filters: Record<string, string | RegExp | number>, options?: {
98
115
  exact?: boolean;
99
116
  maxPages?: number;
100
- } & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
117
+ }) => Promise<SmartRow>;
101
118
  getAllRows: <T extends {
102
119
  asJSON?: boolean;
103
120
  }>(options?: {
@@ -134,4 +151,41 @@ export interface TableResult {
134
151
  */
135
152
  getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
136
153
  };
154
+ /**
155
+ * Iterates through paginated table data, calling the callback for each iteration.
156
+ * Callback return values are automatically appended to allData, which is returned.
157
+ */
158
+ iterateThroughTable: <T = any>(callback: (context: {
159
+ index: number;
160
+ isFirst: boolean;
161
+ isLast: boolean;
162
+ rows: SmartRow[];
163
+ allData: T[];
164
+ table: RestrictedTableResult;
165
+ }) => T | Promise<T>, options?: {
166
+ pagination?: PaginationStrategy;
167
+ dedupeStrategy?: DedupeStrategy;
168
+ maxIterations?: number;
169
+ getIsFirst?: (context: {
170
+ index: number;
171
+ }) => boolean;
172
+ getIsLast?: (context: {
173
+ index: number;
174
+ paginationResult: boolean;
175
+ }) => boolean;
176
+ onFirst?: (context: {
177
+ index: number;
178
+ rows: SmartRow[];
179
+ allData: any[];
180
+ }) => void | Promise<void>;
181
+ onLast?: (context: {
182
+ index: number;
183
+ rows: SmartRow[];
184
+ allData: any[];
185
+ }) => void | Promise<void>;
186
+ }) => Promise<T[]>;
137
187
  }
188
+ /**
189
+ * Restricted table result that excludes methods that shouldn't be called during iteration.
190
+ */
191
+ export type RestrictedTableResult = Omit<TableResult, 'searchForRow' | 'iterateThroughTable' | 'reset'>;
@@ -1,20 +1,20 @@
1
1
  import type { Locator } from '@playwright/test';
2
- import { TableConfig, Selector, TableResult } from './types';
2
+ import { TableConfig, Selector, TableResult, PaginationStrategy } from './types';
3
3
  /**
4
4
  * A collection of pre-built pagination strategies.
5
5
  */
6
6
  export declare const PaginationStrategies: {
7
- clickNext: (nextButtonSelector: Selector, timeout?: number) => import("./types").PaginationStrategy;
8
- clickLoadMore: (buttonSelector: Selector, timeout?: number) => import("./types").PaginationStrategy;
9
- infiniteScroll: (timeout?: number) => import("./types").PaginationStrategy;
7
+ clickNext: (nextButtonSelector: Selector, timeout?: number) => PaginationStrategy;
8
+ clickLoadMore: (buttonSelector: Selector, timeout?: number) => PaginationStrategy;
9
+ infiniteScroll: (timeout?: number) => PaginationStrategy;
10
10
  };
11
11
  /**
12
12
  * @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
13
13
  */
14
14
  export declare const TableStrategies: {
15
- clickNext: (nextButtonSelector: Selector, timeout?: number) => import("./types").PaginationStrategy;
16
- clickLoadMore: (buttonSelector: Selector, timeout?: number) => import("./types").PaginationStrategy;
17
- infiniteScroll: (timeout?: number) => import("./types").PaginationStrategy;
15
+ clickNext: (nextButtonSelector: Selector, timeout?: number) => PaginationStrategy;
16
+ clickLoadMore: (buttonSelector: Selector, timeout?: number) => PaginationStrategy;
17
+ infiniteScroll: (timeout?: number) => PaginationStrategy;
18
18
  };
19
19
  /**
20
20
  * A collection of pre-built sorting strategies.
package/dist/useTable.js CHANGED
@@ -26,6 +26,8 @@ exports.TableStrategies = pagination_1.TableStrategies;
26
26
  */
27
27
  exports.SortingStrategies = sorting_1.SortingStrategies;
28
28
  const useTable = (rootLocator, configOptions = {}) => {
29
+ // Store whether pagination was explicitly provided in config
30
+ const hasPaginationInConfig = configOptions.pagination !== undefined;
29
31
  const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, onReset: () => __awaiter(void 0, void 0, void 0, function* () { console.warn("⚠️ .reset() called but no 'onReset' strategy defined in config."); }) }, configOptions);
30
32
  const resolve = (item, parent) => {
31
33
  if (typeof item === 'string')
@@ -37,6 +39,7 @@ const useTable = (rootLocator, configOptions = {}) => {
37
39
  // Internal State
38
40
  let _headerMap = null;
39
41
  let _hasPaginated = false;
42
+ let _isInitialized = false;
40
43
  const logDebug = (msg) => {
41
44
  if (config.debug)
42
45
  console.log(`🔎 [SmartTable Debug] ${msg}`);
@@ -65,10 +68,11 @@ const useTable = (rootLocator, configOptions = {}) => {
65
68
  const contextMsg = context ? ` (${context})` : '';
66
69
  return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
67
70
  };
68
- const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
71
+ const _getMap = (timeout) => __awaiter(void 0, void 0, void 0, function* () {
69
72
  if (_headerMap)
70
73
  return _headerMap;
71
74
  logDebug('Mapping headers...');
75
+ const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
72
76
  if (config.autoScroll) {
73
77
  try {
74
78
  yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
@@ -77,7 +81,7 @@ const useTable = (rootLocator, configOptions = {}) => {
77
81
  }
78
82
  const headerLoc = resolve(config.headerSelector, rootLocator);
79
83
  try {
80
- yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
84
+ yield headerLoc.first().waitFor({ state: 'visible', timeout: headerTimeout });
81
85
  }
82
86
  catch (e) { /* Ignore hydration */ }
83
87
  // 1. Fetch data efficiently
@@ -126,7 +130,7 @@ const useTable = (rootLocator, configOptions = {}) => {
126
130
  }
127
131
  return result;
128
132
  });
129
- smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
133
+ smart.smartFill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
130
134
  var _a;
131
135
  logDebug(`Filling row with data: ${JSON.stringify(data)}`);
132
136
  // Fill each column
@@ -327,41 +331,35 @@ const useTable = (rootLocator, configOptions = {}) => {
327
331
  return clone.outerHTML;
328
332
  });
329
333
  });
330
- const sortingNamespace = {
331
- apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
332
- if (!config.sorting) {
333
- throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
334
+ // Helper to ensure initialization for async methods
335
+ const _ensureInitialized = () => __awaiter(void 0, void 0, void 0, function* () {
336
+ if (!_isInitialized) {
337
+ yield _getMap();
338
+ _isInitialized = true;
339
+ }
340
+ });
341
+ const result = {
342
+ init: (options) => __awaiter(void 0, void 0, void 0, function* () {
343
+ if (_isInitialized && _headerMap) {
344
+ return result;
334
345
  }
335
- logDebug(`Applying sort for column "${columnName}" (${direction})`);
336
- const context = {
337
- root: rootLocator,
338
- config: config,
339
- page: rootLocator.page(),
340
- resolve: resolve
341
- };
342
- yield config.sorting.doSort({ columnName, direction, context });
346
+ yield _getMap(options === null || options === void 0 ? void 0 : options.timeout);
347
+ _isInitialized = true;
348
+ return result;
343
349
  }),
344
- getState: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
345
- if (!config.sorting) {
346
- throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
350
+ getHeaders: () => __awaiter(void 0, void 0, void 0, function* () {
351
+ if (!_isInitialized || !_headerMap) {
352
+ throw new Error('Table not initialized. Call await table.init() first.');
347
353
  }
348
- logDebug(`Getting sort state for column "${columnName}"`);
349
- const context = {
350
- root: rootLocator,
351
- config: config,
352
- page: rootLocator.page(),
353
- resolve: resolve
354
- };
355
- return config.sorting.getSortState({ columnName, context });
356
- })
357
- };
358
- return {
359
- getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
354
+ return Array.from(_headerMap.keys());
355
+ }),
360
356
  getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
361
- const map = yield _getMap();
362
- const idx = map.get(columnName);
357
+ if (!_isInitialized || !_headerMap) {
358
+ throw new Error('Table not initialized. Call await table.init() first.');
359
+ }
360
+ const idx = _headerMap.get(columnName);
363
361
  if (idx === undefined)
364
- throw _createColumnError(columnName, map, 'header cell');
362
+ throw _createColumnError(columnName, _headerMap, 'header cell');
365
363
  return resolve(config.headerSelector, rootLocator).nth(idx);
366
364
  }),
367
365
  reset: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -375,14 +373,16 @@ const useTable = (rootLocator, configOptions = {}) => {
375
373
  yield config.onReset(context);
376
374
  _hasPaginated = false;
377
375
  _headerMap = null;
376
+ _isInitialized = false;
378
377
  logDebug("Table reset complete.");
379
378
  }),
380
379
  getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
381
380
  var _a, _b;
382
- const map = yield _getMap();
383
- const colIdx = map.get(column);
381
+ // Auto-init if needed (async methods can auto-init)
382
+ yield _ensureInitialized();
383
+ const colIdx = _headerMap.get(column);
384
384
  if (colIdx === undefined)
385
- throw _createColumnError(column, map);
385
+ throw _createColumnError(column, _headerMap);
386
386
  const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
387
387
  const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
388
388
  let currentPage = 1;
@@ -410,25 +410,37 @@ const useTable = (rootLocator, configOptions = {}) => {
410
410
  }
411
411
  return results;
412
412
  }),
413
- getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
413
+ getByRow: (filters, options) => {
414
+ // Throw error if not initialized (sync methods require explicit init)
415
+ if (!_isInitialized || !_headerMap) {
416
+ throw new Error('Table not initialized. Call await table.init() first.');
417
+ }
418
+ // Build locator chain (sync) - current page only
419
+ const allRows = resolve(config.rowSelector, rootLocator);
420
+ const matchedRows = _applyFilters(allRows, filters, _headerMap, (options === null || options === void 0 ? void 0 : options.exact) || false);
421
+ // Return first match (or sentinel) - lazy, doesn't check existence
422
+ const rowLocator = matchedRows.first();
423
+ return _makeSmart(rowLocator, _headerMap);
424
+ },
425
+ searchForRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
426
+ // Auto-init if needed (async methods can auto-init)
427
+ yield _ensureInitialized();
428
+ // Full pagination logic (existing _findRowLocator logic)
414
429
  let row = yield _findRowLocator(filters, options);
415
430
  if (!row) {
416
431
  row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
417
432
  }
418
- const smartRow = _makeSmart(row, yield _getMap());
419
- if (options === null || options === void 0 ? void 0 : options.asJSON) {
420
- return smartRow.toJSON();
421
- }
422
- return smartRow;
433
+ return _makeSmart(row, _headerMap);
423
434
  }),
424
435
  getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
425
- const map = yield _getMap();
436
+ // Auto-init if needed (async methods can auto-init)
437
+ yield _ensureInitialized();
426
438
  let rowLocators = resolve(config.rowSelector, rootLocator);
427
439
  if (options === null || options === void 0 ? void 0 : options.filter) {
428
- rowLocators = _applyFilters(rowLocators, options.filter, map, options.exact || false);
440
+ rowLocators = _applyFilters(rowLocators, options.filter, _headerMap, options.exact || false);
429
441
  }
430
442
  const rows = yield rowLocators.all();
431
- const smartRows = rows.map(loc => _makeSmart(loc, map));
443
+ const smartRows = rows.map(loc => _makeSmart(loc, _headerMap));
432
444
  if (options === null || options === void 0 ? void 0 : options.asJSON) {
433
445
  return Promise.all(smartRows.map(r => r.toJSON()));
434
446
  }
@@ -446,7 +458,140 @@ const useTable = (rootLocator, configOptions = {}) => {
446
458
  const content = `\n==================================================\n🤖 COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY 🤖\n==================================================\nI need a custom Pagination Strategy for 'playwright-smart-table'.\nContainer HTML:\n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n`;
447
459
  yield _handlePrompt('Smart Table Strategy', content, options);
448
460
  }),
449
- sorting: sortingNamespace,
461
+ sorting: {
462
+ apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
463
+ // Auto-init if needed (async methods can auto-init)
464
+ yield _ensureInitialized();
465
+ if (!config.sorting) {
466
+ throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
467
+ }
468
+ logDebug(`Applying sort for column "${columnName}" (${direction})`);
469
+ const context = {
470
+ root: rootLocator,
471
+ config: config,
472
+ page: rootLocator.page(),
473
+ resolve: resolve
474
+ };
475
+ yield config.sorting.doSort({ columnName, direction, context });
476
+ }),
477
+ getState: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
478
+ // Auto-init if needed (async methods can auto-init)
479
+ yield _ensureInitialized();
480
+ if (!config.sorting) {
481
+ throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
482
+ }
483
+ logDebug(`Getting sort state for column "${columnName}"`);
484
+ const context = {
485
+ root: rootLocator,
486
+ config: config,
487
+ page: rootLocator.page(),
488
+ resolve: resolve
489
+ };
490
+ return config.sorting.getSortState({ columnName, context });
491
+ })
492
+ },
493
+ iterateThroughTable: (callback, options) => __awaiter(void 0, void 0, void 0, function* () {
494
+ var _a, _b, _c, _d;
495
+ // Auto-init if needed (async methods can auto-init)
496
+ yield _ensureInitialized();
497
+ // Determine pagination strategy
498
+ const paginationStrategy = (_a = options === null || options === void 0 ? void 0 : options.pagination) !== null && _a !== void 0 ? _a : config.pagination;
499
+ // Check if pagination was explicitly provided in options or config
500
+ const hasPaginationInOptions = (options === null || options === void 0 ? void 0 : options.pagination) !== undefined;
501
+ if (!hasPaginationInOptions && !hasPaginationInConfig) {
502
+ throw new Error('No pagination strategy provided. Either set pagination in options or in table config.');
503
+ }
504
+ // Reset to initial page before starting
505
+ yield result.reset();
506
+ yield result.init();
507
+ // Create restricted table instance (excludes problematic methods)
508
+ const restrictedTable = {
509
+ init: result.init,
510
+ getHeaders: result.getHeaders,
511
+ getHeaderCell: result.getHeaderCell,
512
+ getByRow: result.getByRow,
513
+ getAllRows: result.getAllRows,
514
+ getColumnValues: result.getColumnValues,
515
+ generateConfigPrompt: result.generateConfigPrompt,
516
+ generateStrategyPrompt: result.generateStrategyPrompt,
517
+ sorting: result.sorting,
518
+ };
519
+ // Default functions
520
+ const getIsFirst = (_b = options === null || options === void 0 ? void 0 : options.getIsFirst) !== null && _b !== void 0 ? _b : (({ index }) => index === 0);
521
+ const getIsLast = (_c = options === null || options === void 0 ? void 0 : options.getIsLast) !== null && _c !== void 0 ? _c : (() => false);
522
+ // Create allData array (persists across iterations)
523
+ const allData = [];
524
+ const effectiveMaxIterations = (_d = options === null || options === void 0 ? void 0 : options.maxIterations) !== null && _d !== void 0 ? _d : config.maxPages;
525
+ let index = 0;
526
+ let paginationResult = true; // Will be set after first pagination attempt
527
+ let seenKeys = null; // Track seen keys across iterations for deduplication
528
+ logDebug(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations})`);
529
+ while (index < effectiveMaxIterations) {
530
+ // Get current rows
531
+ const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
532
+ let rows = rowLocators.map(loc => _makeSmart(loc, _headerMap));
533
+ // Deduplicate if dedupeStrategy provided (across all iterations)
534
+ if ((options === null || options === void 0 ? void 0 : options.dedupeStrategy) && rows.length > 0) {
535
+ if (!seenKeys) {
536
+ seenKeys = new Set();
537
+ }
538
+ const deduplicated = [];
539
+ for (const row of rows) {
540
+ const key = yield options.dedupeStrategy(row);
541
+ if (!seenKeys.has(key)) {
542
+ seenKeys.add(key);
543
+ deduplicated.push(row);
544
+ }
545
+ }
546
+ rows = deduplicated;
547
+ logDebug(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
548
+ }
549
+ // Determine flags (isLast will be checked after pagination attempt)
550
+ const isFirst = getIsFirst({ index });
551
+ let isLast = getIsLast({ index, paginationResult });
552
+ // Check if this is the last iteration due to maxIterations (before attempting pagination)
553
+ const isLastDueToMax = index === effectiveMaxIterations - 1;
554
+ // Call onFirst hook if applicable
555
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.onFirst)) {
556
+ yield options.onFirst({ index, rows, allData });
557
+ }
558
+ // Call main callback
559
+ const returnValue = yield callback({
560
+ index,
561
+ isFirst,
562
+ isLast,
563
+ rows,
564
+ allData,
565
+ table: restrictedTable,
566
+ });
567
+ // Append return value to allData
568
+ allData.push(returnValue);
569
+ // Attempt pagination (before checking if we should continue)
570
+ const context = {
571
+ root: rootLocator,
572
+ config: config,
573
+ page: rootLocator.page(),
574
+ resolve: resolve
575
+ };
576
+ paginationResult = yield paginationStrategy(context);
577
+ // Now check isLast with updated paginationResult
578
+ isLast = getIsLast({ index, paginationResult }) || isLastDueToMax;
579
+ // Call onLast hook if applicable (after we know pagination failed or we're at max iterations)
580
+ if (isLast && (options === null || options === void 0 ? void 0 : options.onLast)) {
581
+ yield options.onLast({ index, rows, allData });
582
+ }
583
+ // Check if we should continue
584
+ if (isLast || !paginationResult) {
585
+ logDebug(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult}, isLastDueToMax: ${isLastDueToMax})`);
586
+ break;
587
+ }
588
+ index++;
589
+ logDebug(`Iteration ${index} completed, continuing...`);
590
+ }
591
+ logDebug(`iterateThroughTable completed after ${index + 1} iterations, collected ${allData.length} items`);
592
+ return allData;
593
+ }),
450
594
  };
595
+ return result;
451
596
  };
452
597
  exports.useTable = useTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.3.1",
3
+ "version": "3.1.0",
4
4
  "description": "A smart table utility for Playwright with built-in pagination strategies that are fully extensible.",
5
5
  "repository": {
6
6
  "type": "git",