@rickcedwhat/playwright-smart-table 2.3.1 → 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 +58 -34
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +52 -3
- package/dist/types.d.ts +60 -3
- package/dist/useTable.d.ts +7 -7
- package/dist/useTable.js +195 -42
- package/package.json +1 -1
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 =
|
|
29
|
+
const row = table.getByRow({ Name: 'Airi Satou' });
|
|
30
30
|
|
|
31
|
-
|
|
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 =
|
|
50
|
+
const row = table.getByRow({ Name: 'Airi Satou' });
|
|
50
51
|
|
|
51
52
|
// Interact with cell using column name (resilient to column reordering)
|
|
52
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
129
|
+
await table.getByRowAcrossPages({ Name: 'Angelica Ramos' });
|
|
125
130
|
} catch (e) {}
|
|
126
131
|
|
|
127
|
-
// Reset internal state (and potentially UI) to
|
|
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
|
|
132
|
-
await expect(
|
|
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 `
|
|
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 =
|
|
163
|
+
const row = table.getByRow({ ID: '1' });
|
|
158
164
|
|
|
159
|
-
await row.
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 =
|
|
196
|
+
const row = table.getByRow({ ID: '1' });
|
|
187
197
|
|
|
188
198
|
// Use custom input mappers for specific columns
|
|
189
|
-
await row.
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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 =
|
|
264
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
|
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>;
|
package/dist/typeContext.d.ts
CHANGED
|
@@ -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 =
|
|
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";
|
package/dist/typeContext.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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'>;
|
package/dist/useTable.d.ts
CHANGED
|
@@ -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) =>
|
|
8
|
-
clickLoadMore: (buttonSelector: Selector, timeout?: number) =>
|
|
9
|
-
infiniteScroll: (timeout?: number) =>
|
|
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) =>
|
|
16
|
-
clickLoadMore: (buttonSelector: Selector, timeout?: number) =>
|
|
17
|
-
infiniteScroll: (timeout?: number) =>
|
|
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:
|
|
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.
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
345
|
-
if (!
|
|
346
|
-
throw new Error('
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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,
|
|
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
|
-
|
|
383
|
-
|
|
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,
|
|
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,45 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
410
410
|
}
|
|
411
411
|
return results;
|
|
412
412
|
}),
|
|
413
|
-
getByRow: (filters, options) =>
|
|
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)
|
|
414
433
|
let row = yield _findRowLocator(filters, options);
|
|
415
434
|
if (!row) {
|
|
416
435
|
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
417
436
|
}
|
|
418
|
-
const smartRow = _makeSmart(row,
|
|
437
|
+
const smartRow = _makeSmart(row, _headerMap);
|
|
419
438
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
420
439
|
return smartRow.toJSON();
|
|
421
440
|
}
|
|
422
441
|
return smartRow;
|
|
423
442
|
}),
|
|
424
443
|
getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
425
|
-
|
|
444
|
+
// Auto-init if needed (async methods can auto-init)
|
|
445
|
+
yield _ensureInitialized();
|
|
426
446
|
let rowLocators = resolve(config.rowSelector, rootLocator);
|
|
427
447
|
if (options === null || options === void 0 ? void 0 : options.filter) {
|
|
428
|
-
rowLocators = _applyFilters(rowLocators, options.filter,
|
|
448
|
+
rowLocators = _applyFilters(rowLocators, options.filter, _headerMap, options.exact || false);
|
|
429
449
|
}
|
|
430
450
|
const rows = yield rowLocators.all();
|
|
431
|
-
const smartRows = rows.map(loc => _makeSmart(loc,
|
|
451
|
+
const smartRows = rows.map(loc => _makeSmart(loc, _headerMap));
|
|
432
452
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
433
453
|
return Promise.all(smartRows.map(r => r.toJSON()));
|
|
434
454
|
}
|
|
@@ -446,7 +466,140 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
446
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`;
|
|
447
467
|
yield _handlePrompt('Smart Table Strategy', content, options);
|
|
448
468
|
}),
|
|
449
|
-
sorting:
|
|
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
|
+
}),
|
|
450
602
|
};
|
|
603
|
+
return result;
|
|
451
604
|
};
|
|
452
605
|
exports.useTable = useTable;
|
package/package.json
CHANGED