@rickcedwhat/playwright-smart-table 2.3.0 → 3.0.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 getByRowAcrossPages for pagination
95
+ await expect(await table.getByRowAcrossPages({ 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.getByRowAcrossPages({ 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.getByRowAcrossPages({ "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,12 +313,15 @@ 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
 
@@ -534,13 +558,13 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
534
558
 
535
559
  <!-- embed-type: SmartRow -->
536
560
  ```typescript
537
- export type SmartRow = Omit<Locator, 'fill'> & {
561
+ export type SmartRow = Locator & {
538
562
  getCell(column: string): Locator;
539
563
  toJSON(): Promise<Record<string, string>>;
540
564
  /**
541
565
  * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
542
566
  */
543
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
567
+ smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
544
568
  };
545
569
  ```
546
570
  <!-- /embed-type: SmartRow -->
@@ -548,7 +572,7 @@ export type SmartRow = Omit<Locator, 'fill'> & {
548
572
  **Methods:**
549
573
  - `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
550
574
  - `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
575
+ - `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
576
 
553
577
  All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
554
578
 
@@ -578,7 +602,7 @@ export interface TableConfig {
578
602
  */
579
603
  debug?: boolean;
580
604
  /**
581
- * Strategy to reset the table to the first page.
605
+ * Strategy to reset the table to the initial page.
582
606
  * Called when table.reset() is invoked.
583
607
  */
584
608
  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: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean } & T\n ) => T['asJSON'] extends true ? Promise<Record<string, string>> : SmartRow;\n\n /**\n * Finds a row across multiple pages using pagination. Auto-initializes if needed.\n */\n getByRowAcrossPages: <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 /**\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, 'getByRowAcrossPages' | '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,10 +107,28 @@ 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
 
119
+ /**
120
+ * Finds a row on the current page only. Returns immediately (sync).
121
+ * Throws error if table is not initialized.
122
+ */
111
123
  getByRow: <T extends { asJSON?: boolean }>(
124
+ filters: Record<string, string | RegExp | number>,
125
+ options?: { exact?: boolean } & T
126
+ ) => T['asJSON'] extends true ? Promise<Record<string, string>> : SmartRow;
127
+
128
+ /**
129
+ * Finds a row across multiple pages using pagination. Auto-initializes if needed.
130
+ */
131
+ getByRowAcrossPages: <T extends { asJSON?: boolean }>(
112
132
  filters: Record<string, string | RegExp | number>,
113
133
  options?: { exact?: boolean, maxPages?: number } & T
114
134
  ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
@@ -147,5 +167,34 @@ export interface TableResult {
147
167
  */
148
168
  getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
149
169
  };
170
+
171
+ /**
172
+ * Iterates through paginated table data, calling the callback for each iteration.
173
+ * Callback return values are automatically appended to allData, which is returned.
174
+ */
175
+ iterateThroughTable: <T = any>(
176
+ callback: (context: {
177
+ index: number;
178
+ isFirst: boolean;
179
+ isLast: boolean;
180
+ rows: SmartRow[];
181
+ allData: T[];
182
+ table: RestrictedTableResult;
183
+ }) => T | Promise<T>,
184
+ options?: {
185
+ pagination?: PaginationStrategy;
186
+ dedupeStrategy?: DedupeStrategy;
187
+ maxIterations?: number;
188
+ getIsFirst?: (context: { index: number }) => boolean;
189
+ getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;
190
+ onFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
191
+ onLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
192
+ }
193
+ ) => Promise<T[]>;
150
194
  }
195
+
196
+ /**
197
+ * Restricted table result that excludes methods that shouldn't be called during iteration.
198
+ */
199
+ export type RestrictedTableResult = Omit<TableResult, 'getByRowAcrossPages' | 'iterateThroughTable' | 'reset'>;
151
200
  `;
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,10 +91,29 @@ 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>;
103
+ /**
104
+ * Finds a row on the current page only. Returns immediately (sync).
105
+ * Throws error if table is not initialized.
106
+ */
95
107
  getByRow: <T extends {
96
108
  asJSON?: boolean;
109
+ }>(filters: Record<string, string | RegExp | number>, options?: {
110
+ exact?: boolean;
111
+ } & T) => T['asJSON'] extends true ? Promise<Record<string, string>> : SmartRow;
112
+ /**
113
+ * Finds a row across multiple pages using pagination. Auto-initializes if needed.
114
+ */
115
+ getByRowAcrossPages: <T extends {
116
+ asJSON?: boolean;
97
117
  }>(filters: Record<string, string | RegExp | number>, options?: {
98
118
  exact?: boolean;
99
119
  maxPages?: number;
@@ -134,4 +154,41 @@ export interface TableResult {
134
154
  */
135
155
  getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
136
156
  };
157
+ /**
158
+ * Iterates through paginated table data, calling the callback for each iteration.
159
+ * Callback return values are automatically appended to allData, which is returned.
160
+ */
161
+ iterateThroughTable: <T = any>(callback: (context: {
162
+ index: number;
163
+ isFirst: boolean;
164
+ isLast: boolean;
165
+ rows: SmartRow[];
166
+ allData: T[];
167
+ table: RestrictedTableResult;
168
+ }) => T | Promise<T>, options?: {
169
+ pagination?: PaginationStrategy;
170
+ dedupeStrategy?: DedupeStrategy;
171
+ maxIterations?: number;
172
+ getIsFirst?: (context: {
173
+ index: number;
174
+ }) => boolean;
175
+ getIsLast?: (context: {
176
+ index: number;
177
+ paginationResult: boolean;
178
+ }) => boolean;
179
+ onFirst?: (context: {
180
+ index: number;
181
+ rows: SmartRow[];
182
+ allData: any[];
183
+ }) => void | Promise<void>;
184
+ onLast?: (context: {
185
+ index: number;
186
+ rows: SmartRow[];
187
+ allData: any[];
188
+ }) => void | Promise<void>;
189
+ }) => Promise<T[]>;
137
190
  }
191
+ /**
192
+ * Restricted table result that excludes methods that shouldn't be called during iteration.
193
+ */
194
+ export type RestrictedTableResult = Omit<TableResult, 'getByRowAcrossPages' | 'iterateThroughTable' | 'reset'>;
@@ -1,34 +1,20 @@
1
1
  import type { Locator } from '@playwright/test';
2
- import { TableConfig, TableContext, 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
- /**
8
- * Clicks a "Next" button.
9
- * @param selector - The CSS selector for the "Next" button.
10
- */
11
- NextButton: (selector: string) => ((context: TableContext) => Promise<boolean>);
12
- /**
13
- * Clicks numbered page links.
14
- * @param selector - The CSS selector for the page number links.
15
- */
16
- NumberedPages: (selector: string) => ((context: TableContext) => Promise<boolean>);
7
+ clickNext: (nextButtonSelector: Selector, timeout?: number) => PaginationStrategy;
8
+ clickLoadMore: (buttonSelector: Selector, timeout?: number) => PaginationStrategy;
9
+ infiniteScroll: (timeout?: number) => PaginationStrategy;
17
10
  };
18
11
  /**
19
12
  * @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
20
13
  */
21
14
  export declare const TableStrategies: {
22
- /**
23
- * Clicks a "Next" button.
24
- * @param selector - The CSS selector for the "Next" button.
25
- */
26
- NextButton: (selector: string) => ((context: TableContext) => Promise<boolean>);
27
- /**
28
- * Clicks numbered page links.
29
- * @param selector - The CSS selector for the page number links.
30
- */
31
- NumberedPages: (selector: string) => ((context: TableContext) => Promise<boolean>);
15
+ clickNext: (nextButtonSelector: Selector, timeout?: number) => PaginationStrategy;
16
+ clickLoadMore: (buttonSelector: Selector, timeout?: number) => PaginationStrategy;
17
+ infiniteScroll: (timeout?: number) => PaginationStrategy;
32
18
  };
33
19
  /**
34
20
  * A collection of pre-built sorting strategies.
package/dist/useTable.js CHANGED
@@ -12,50 +12,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useTable = exports.SortingStrategies = exports.TableStrategies = exports.PaginationStrategies = void 0;
13
13
  const typeContext_1 = require("./typeContext");
14
14
  const sorting_1 = require("./strategies/sorting");
15
+ const pagination_1 = require("./strategies/pagination");
15
16
  /**
16
17
  * A collection of pre-built pagination strategies.
17
18
  */
18
- exports.PaginationStrategies = {
19
- /**
20
- * Clicks a "Next" button.
21
- * @param selector - The CSS selector for the "Next" button.
22
- */
23
- NextButton: (selector) => {
24
- return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root }) {
25
- const nextButton = root.locator(selector);
26
- if ((yield nextButton.isVisible()) && (yield nextButton.isEnabled())) {
27
- yield nextButton.click();
28
- return true;
29
- }
30
- return false;
31
- });
32
- },
33
- /**
34
- * Clicks numbered page links.
35
- * @param selector - The CSS selector for the page number links.
36
- */
37
- NumberedPages: (selector) => {
38
- let currentPage = 1;
39
- return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root }) {
40
- currentPage++;
41
- const pageLink = root.locator(selector).filter({ hasText: String(currentPage) });
42
- if (yield pageLink.isVisible()) {
43
- yield pageLink.click();
44
- return true;
45
- }
46
- return false;
47
- });
48
- },
49
- };
19
+ exports.PaginationStrategies = pagination_1.PaginationStrategies;
50
20
  /**
51
21
  * @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
52
22
  */
53
- exports.TableStrategies = exports.PaginationStrategies;
23
+ exports.TableStrategies = pagination_1.TableStrategies;
54
24
  /**
55
25
  * A collection of pre-built sorting strategies.
56
26
  */
57
27
  exports.SortingStrategies = sorting_1.SortingStrategies;
58
28
  const useTable = (rootLocator, configOptions = {}) => {
29
+ // Store whether pagination was explicitly provided in config
30
+ const hasPaginationInConfig = configOptions.pagination !== undefined;
59
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);
60
32
  const resolve = (item, parent) => {
61
33
  if (typeof item === 'string')
@@ -67,6 +39,7 @@ const useTable = (rootLocator, configOptions = {}) => {
67
39
  // Internal State
68
40
  let _headerMap = null;
69
41
  let _hasPaginated = false;
42
+ let _isInitialized = false;
70
43
  const logDebug = (msg) => {
71
44
  if (config.debug)
72
45
  console.log(`🔎 [SmartTable Debug] ${msg}`);
@@ -95,10 +68,11 @@ const useTable = (rootLocator, configOptions = {}) => {
95
68
  const contextMsg = context ? ` (${context})` : '';
96
69
  return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
97
70
  };
98
- const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
71
+ const _getMap = (timeout) => __awaiter(void 0, void 0, void 0, function* () {
99
72
  if (_headerMap)
100
73
  return _headerMap;
101
74
  logDebug('Mapping headers...');
75
+ const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
102
76
  if (config.autoScroll) {
103
77
  try {
104
78
  yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
@@ -107,7 +81,7 @@ const useTable = (rootLocator, configOptions = {}) => {
107
81
  }
108
82
  const headerLoc = resolve(config.headerSelector, rootLocator);
109
83
  try {
110
- yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
84
+ yield headerLoc.first().waitFor({ state: 'visible', timeout: headerTimeout });
111
85
  }
112
86
  catch (e) { /* Ignore hydration */ }
113
87
  // 1. Fetch data efficiently
@@ -156,7 +130,7 @@ const useTable = (rootLocator, configOptions = {}) => {
156
130
  }
157
131
  return result;
158
132
  });
159
- 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* () {
160
134
  var _a;
161
135
  logDebug(`Filling row with data: ${JSON.stringify(data)}`);
162
136
  // Fill each column
@@ -357,41 +331,35 @@ const useTable = (rootLocator, configOptions = {}) => {
357
331
  return clone.outerHTML;
358
332
  });
359
333
  });
360
- const sortingNamespace = {
361
- apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
362
- if (!config.sorting) {
363
- 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;
364
345
  }
365
- logDebug(`Applying sort for column "${columnName}" (${direction})`);
366
- const context = {
367
- root: rootLocator,
368
- config: config,
369
- page: rootLocator.page(),
370
- resolve: resolve
371
- };
372
- 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;
373
349
  }),
374
- getState: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
375
- if (!config.sorting) {
376
- 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.');
377
353
  }
378
- logDebug(`Getting sort state for column "${columnName}"`);
379
- const context = {
380
- root: rootLocator,
381
- config: config,
382
- page: rootLocator.page(),
383
- resolve: resolve
384
- };
385
- return config.sorting.getSortState({ columnName, context });
386
- })
387
- };
388
- return {
389
- getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
354
+ return Array.from(_headerMap.keys());
355
+ }),
390
356
  getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
391
- const map = yield _getMap();
392
- 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);
393
361
  if (idx === undefined)
394
- throw _createColumnError(columnName, map, 'header cell');
362
+ throw _createColumnError(columnName, _headerMap, 'header cell');
395
363
  return resolve(config.headerSelector, rootLocator).nth(idx);
396
364
  }),
397
365
  reset: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -405,14 +373,16 @@ const useTable = (rootLocator, configOptions = {}) => {
405
373
  yield config.onReset(context);
406
374
  _hasPaginated = false;
407
375
  _headerMap = null;
376
+ _isInitialized = false;
408
377
  logDebug("Table reset complete.");
409
378
  }),
410
379
  getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
411
380
  var _a, _b;
412
- const map = yield _getMap();
413
- const colIdx = map.get(column);
381
+ // Auto-init if needed (async methods can auto-init)
382
+ yield _ensureInitialized();
383
+ const colIdx = _headerMap.get(column);
414
384
  if (colIdx === undefined)
415
- throw _createColumnError(column, map);
385
+ throw _createColumnError(column, _headerMap);
416
386
  const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
417
387
  const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
418
388
  let currentPage = 1;
@@ -440,25 +410,45 @@ const useTable = (rootLocator, configOptions = {}) => {
440
410
  }
441
411
  return results;
442
412
  }),
443
- 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
+ const smartRow = _makeSmart(rowLocator, _headerMap);
424
+ if (options === null || options === void 0 ? void 0 : options.asJSON) {
425
+ return smartRow.toJSON();
426
+ }
427
+ return smartRow;
428
+ },
429
+ getByRowAcrossPages: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
430
+ // Auto-init if needed (async methods can auto-init)
431
+ yield _ensureInitialized();
432
+ // Full pagination logic (existing _findRowLocator logic)
444
433
  let row = yield _findRowLocator(filters, options);
445
434
  if (!row) {
446
435
  row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
447
436
  }
448
- const smartRow = _makeSmart(row, yield _getMap());
437
+ const smartRow = _makeSmart(row, _headerMap);
449
438
  if (options === null || options === void 0 ? void 0 : options.asJSON) {
450
439
  return smartRow.toJSON();
451
440
  }
452
441
  return smartRow;
453
442
  }),
454
443
  getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
455
- const map = yield _getMap();
444
+ // Auto-init if needed (async methods can auto-init)
445
+ yield _ensureInitialized();
456
446
  let rowLocators = resolve(config.rowSelector, rootLocator);
457
447
  if (options === null || options === void 0 ? void 0 : options.filter) {
458
- rowLocators = _applyFilters(rowLocators, options.filter, map, options.exact || false);
448
+ rowLocators = _applyFilters(rowLocators, options.filter, _headerMap, options.exact || false);
459
449
  }
460
450
  const rows = yield rowLocators.all();
461
- const smartRows = rows.map(loc => _makeSmart(loc, map));
451
+ const smartRows = rows.map(loc => _makeSmart(loc, _headerMap));
462
452
  if (options === null || options === void 0 ? void 0 : options.asJSON) {
463
453
  return Promise.all(smartRows.map(r => r.toJSON()));
464
454
  }
@@ -476,7 +466,140 @@ const useTable = (rootLocator, configOptions = {}) => {
476
466
  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`;
477
467
  yield _handlePrompt('Smart Table Strategy', content, options);
478
468
  }),
479
- sorting: sortingNamespace,
469
+ sorting: {
470
+ apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
471
+ // Auto-init if needed (async methods can auto-init)
472
+ yield _ensureInitialized();
473
+ if (!config.sorting) {
474
+ throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
475
+ }
476
+ logDebug(`Applying sort for column "${columnName}" (${direction})`);
477
+ const context = {
478
+ root: rootLocator,
479
+ config: config,
480
+ page: rootLocator.page(),
481
+ resolve: resolve
482
+ };
483
+ yield config.sorting.doSort({ columnName, direction, context });
484
+ }),
485
+ getState: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
486
+ // Auto-init if needed (async methods can auto-init)
487
+ yield _ensureInitialized();
488
+ if (!config.sorting) {
489
+ throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
490
+ }
491
+ logDebug(`Getting sort state for column "${columnName}"`);
492
+ const context = {
493
+ root: rootLocator,
494
+ config: config,
495
+ page: rootLocator.page(),
496
+ resolve: resolve
497
+ };
498
+ return config.sorting.getSortState({ columnName, context });
499
+ })
500
+ },
501
+ iterateThroughTable: (callback, options) => __awaiter(void 0, void 0, void 0, function* () {
502
+ var _a, _b, _c, _d;
503
+ // Auto-init if needed (async methods can auto-init)
504
+ yield _ensureInitialized();
505
+ // Determine pagination strategy
506
+ const paginationStrategy = (_a = options === null || options === void 0 ? void 0 : options.pagination) !== null && _a !== void 0 ? _a : config.pagination;
507
+ // Check if pagination was explicitly provided in options or config
508
+ const hasPaginationInOptions = (options === null || options === void 0 ? void 0 : options.pagination) !== undefined;
509
+ if (!hasPaginationInOptions && !hasPaginationInConfig) {
510
+ throw new Error('No pagination strategy provided. Either set pagination in options or in table config.');
511
+ }
512
+ // Reset to initial page before starting
513
+ yield result.reset();
514
+ yield result.init();
515
+ // Create restricted table instance (excludes problematic methods)
516
+ const restrictedTable = {
517
+ init: result.init,
518
+ getHeaders: result.getHeaders,
519
+ getHeaderCell: result.getHeaderCell,
520
+ getByRow: result.getByRow,
521
+ getAllRows: result.getAllRows,
522
+ getColumnValues: result.getColumnValues,
523
+ generateConfigPrompt: result.generateConfigPrompt,
524
+ generateStrategyPrompt: result.generateStrategyPrompt,
525
+ sorting: result.sorting,
526
+ };
527
+ // Default functions
528
+ const getIsFirst = (_b = options === null || options === void 0 ? void 0 : options.getIsFirst) !== null && _b !== void 0 ? _b : (({ index }) => index === 0);
529
+ const getIsLast = (_c = options === null || options === void 0 ? void 0 : options.getIsLast) !== null && _c !== void 0 ? _c : (() => false);
530
+ // Create allData array (persists across iterations)
531
+ const allData = [];
532
+ const effectiveMaxIterations = (_d = options === null || options === void 0 ? void 0 : options.maxIterations) !== null && _d !== void 0 ? _d : config.maxPages;
533
+ let index = 0;
534
+ let paginationResult = true; // Will be set after first pagination attempt
535
+ let seenKeys = null; // Track seen keys across iterations for deduplication
536
+ logDebug(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations})`);
537
+ while (index < effectiveMaxIterations) {
538
+ // Get current rows
539
+ const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
540
+ let rows = rowLocators.map(loc => _makeSmart(loc, _headerMap));
541
+ // Deduplicate if dedupeStrategy provided (across all iterations)
542
+ if ((options === null || options === void 0 ? void 0 : options.dedupeStrategy) && rows.length > 0) {
543
+ if (!seenKeys) {
544
+ seenKeys = new Set();
545
+ }
546
+ const deduplicated = [];
547
+ for (const row of rows) {
548
+ const key = yield options.dedupeStrategy(row);
549
+ if (!seenKeys.has(key)) {
550
+ seenKeys.add(key);
551
+ deduplicated.push(row);
552
+ }
553
+ }
554
+ rows = deduplicated;
555
+ logDebug(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
556
+ }
557
+ // Determine flags (isLast will be checked after pagination attempt)
558
+ const isFirst = getIsFirst({ index });
559
+ let isLast = getIsLast({ index, paginationResult });
560
+ // Check if this is the last iteration due to maxIterations (before attempting pagination)
561
+ const isLastDueToMax = index === effectiveMaxIterations - 1;
562
+ // Call onFirst hook if applicable
563
+ if (isFirst && (options === null || options === void 0 ? void 0 : options.onFirst)) {
564
+ yield options.onFirst({ index, rows, allData });
565
+ }
566
+ // Call main callback
567
+ const returnValue = yield callback({
568
+ index,
569
+ isFirst,
570
+ isLast,
571
+ rows,
572
+ allData,
573
+ table: restrictedTable,
574
+ });
575
+ // Append return value to allData
576
+ allData.push(returnValue);
577
+ // Attempt pagination (before checking if we should continue)
578
+ const context = {
579
+ root: rootLocator,
580
+ config: config,
581
+ page: rootLocator.page(),
582
+ resolve: resolve
583
+ };
584
+ paginationResult = yield paginationStrategy(context);
585
+ // Now check isLast with updated paginationResult
586
+ isLast = getIsLast({ index, paginationResult }) || isLastDueToMax;
587
+ // Call onLast hook if applicable (after we know pagination failed or we're at max iterations)
588
+ if (isLast && (options === null || options === void 0 ? void 0 : options.onLast)) {
589
+ yield options.onLast({ index, rows, allData });
590
+ }
591
+ // Check if we should continue
592
+ if (isLast || !paginationResult) {
593
+ logDebug(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult}, isLastDueToMax: ${isLastDueToMax})`);
594
+ break;
595
+ }
596
+ index++;
597
+ logDebug(`Iteration ${index} completed, continuing...`);
598
+ }
599
+ logDebug(`iterateThroughTable completed after ${index + 1} iterations, collected ${allData.length} items`);
600
+ return allData;
601
+ }),
480
602
  };
603
+ return result;
481
604
  };
482
605
  exports.useTable = useTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.3.0",
3
+ "version": "3.0.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",