@rickcedwhat/playwright-smart-table 2.1.1 ā 2.1.3
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 +443 -50
- package/dist/strategies/index.js +0 -5
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +19 -0
- package/dist/types.d.ts +20 -0
- package/dist/useTable.js +99 -7
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
Playwright Smart Table š§
|
|
1
|
+
# Playwright Smart Table š§
|
|
2
2
|
|
|
3
|
-
A production-ready, type-safe table wrapper for Playwright.
|
|
3
|
+
A production-ready, type-safe table wrapper for Playwright that abstracts away the complexity of testing dynamic web tables. Handles pagination, infinite scroll, virtualization, and data grids (MUI, AG-Grid) so your tests remain clean and readable.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
š¦ Installation
|
|
5
|
+
## š¦ Installation
|
|
8
6
|
|
|
7
|
+
```bash
|
|
9
8
|
npm install @rickcedwhat/playwright-smart-table
|
|
9
|
+
```
|
|
10
10
|
|
|
11
|
+
> **Note:** Requires `@playwright/test` as a peer dependency.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
---
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
## šÆ Getting Started
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
### Step 1: Basic Table Interaction
|
|
17
18
|
|
|
18
|
-
For standard tables (
|
|
19
|
+
For standard HTML tables (`<table>`, `<tr>`, `<td>`), the library works out of the box with sensible defaults:
|
|
19
20
|
|
|
20
21
|
<!-- embed: quick-start -->
|
|
21
22
|
```typescript
|
|
@@ -31,9 +32,43 @@ await expect(row.getCell('Position')).toHaveText('Accountant');
|
|
|
31
32
|
```
|
|
32
33
|
<!-- /embed: quick-start -->
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
**What's happening here?**
|
|
36
|
+
- `useTable()` creates a smart table wrapper around your table locator
|
|
37
|
+
- `getByRow()` finds a specific row by column values
|
|
38
|
+
- The returned `SmartRow` knows its column structure, so `.getCell('Position')` works directly
|
|
39
|
+
|
|
40
|
+
### Step 2: Understanding SmartRow
|
|
41
|
+
|
|
42
|
+
The `SmartRow` is the core power of this library. Unlike a standard Playwright `Locator`, it understands your table's column structure.
|
|
43
|
+
|
|
44
|
+
<!-- embed: smart-row -->
|
|
45
|
+
```typescript
|
|
46
|
+
// 1. Get SmartRow via getByRow
|
|
47
|
+
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
48
|
+
|
|
49
|
+
// 2. Interact with cell
|
|
50
|
+
// ā
Good: Resilient to column reordering
|
|
51
|
+
await row.getCell('Position').click();
|
|
52
|
+
|
|
53
|
+
// 3. Dump data from row
|
|
54
|
+
const data = await row.toJSON();
|
|
55
|
+
console.log(data);
|
|
56
|
+
// { Name: "Airi Satou", Position: "Accountant", ... }
|
|
57
|
+
```
|
|
58
|
+
<!-- /embed: smart-row -->
|
|
59
|
+
|
|
60
|
+
**Key Benefits:**
|
|
61
|
+
- ā
Column names instead of indices (survives column reordering)
|
|
62
|
+
- ā
Extends Playwright's `Locator` API (all `.click()`, `.isVisible()`, etc. work)
|
|
63
|
+
- ā
`.toJSON()` for quick data extraction
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## š§ Configuration & Advanced Scenarios
|
|
68
|
+
|
|
69
|
+
### Working with Paginated Tables
|
|
70
|
+
|
|
71
|
+
For tables that span multiple pages, configure a pagination strategy:
|
|
37
72
|
|
|
38
73
|
<!-- embed: pagination -->
|
|
39
74
|
```typescript
|
|
@@ -56,39 +91,131 @@ await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
|
56
91
|
```
|
|
57
92
|
<!-- /embed: pagination -->
|
|
58
93
|
|
|
59
|
-
|
|
94
|
+
### Debug Mode
|
|
60
95
|
|
|
61
|
-
|
|
96
|
+
Enable debug logging to see exactly what the library is doing:
|
|
62
97
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<!-- embed: smart-row -->
|
|
98
|
+
<!-- embed: advanced-debug -->
|
|
66
99
|
```typescript
|
|
67
|
-
|
|
100
|
+
const table = useTable(page.locator('#example'), {
|
|
101
|
+
headerSelector: 'thead th',
|
|
102
|
+
debug: true // Enables verbose logging of internal operations
|
|
103
|
+
});
|
|
104
|
+
|
|
68
105
|
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
106
|
+
await expect(row).toBeVisible();
|
|
107
|
+
```
|
|
108
|
+
<!-- /embed: advanced-debug -->
|
|
69
109
|
|
|
70
|
-
|
|
71
|
-
// ā
Good: Resilient to column reordering
|
|
72
|
-
await row.getCell('Position').click();
|
|
110
|
+
This will log header mappings, row scans, and pagination triggers to help troubleshoot issues.
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
### Resetting Table State
|
|
113
|
+
|
|
114
|
+
If your tests navigate deep into a paginated table, use `.reset()` to return to the first page:
|
|
115
|
+
|
|
116
|
+
<!-- embed: advanced-reset -->
|
|
117
|
+
```typescript
|
|
118
|
+
// Navigate deep into the table by searching for a row on a later page
|
|
119
|
+
try {
|
|
120
|
+
await table.getByRow({ Name: 'Angelica Ramos' });
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
|
|
123
|
+
// Reset internal state (and potentially UI) to Page 1
|
|
124
|
+
await table.reset();
|
|
125
|
+
|
|
126
|
+
// Now subsequent searches start from the beginning
|
|
127
|
+
const firstPageRow = await table.getByRow({ Name: 'Airi Satou' });
|
|
128
|
+
await expect(firstPageRow).toBeVisible();
|
|
78
129
|
```
|
|
79
|
-
<!-- /embed:
|
|
130
|
+
<!-- /embed: advanced-reset -->
|
|
131
|
+
|
|
132
|
+
### Column Scanning
|
|
133
|
+
|
|
134
|
+
Efficiently extract all values from a specific column:
|
|
135
|
+
|
|
136
|
+
<!-- embed: advanced-column-scan -->
|
|
137
|
+
```typescript
|
|
138
|
+
// Quickly grab all text values from the "Office" column
|
|
139
|
+
const offices = await table.getColumnValues('Office');
|
|
140
|
+
expect(offices).toContain('Tokyo');
|
|
141
|
+
expect(offices.length).toBeGreaterThan(0);
|
|
142
|
+
```
|
|
143
|
+
<!-- /embed: advanced-column-scan -->
|
|
144
|
+
|
|
145
|
+
### Transforming Column Headers
|
|
80
146
|
|
|
81
|
-
|
|
147
|
+
Use `headerTransformer` to normalize or rename column headers. This is especially useful for tables with empty headers, inconsistent formatting, or when you want to use cleaner names in your tests.
|
|
82
148
|
|
|
83
|
-
|
|
149
|
+
**Example 1: Renaming Empty Columns**
|
|
84
150
|
|
|
85
|
-
|
|
151
|
+
Tables with empty header cells (like Material UI DataGrids) get auto-assigned names like `__col_0`, `__col_1`. Transform them to meaningful names:
|
|
86
152
|
|
|
87
|
-
|
|
153
|
+
<!-- embed: header-transformer -->
|
|
154
|
+
```typescript
|
|
155
|
+
const table = useTable(page.locator('.MuiDataGrid-root').first(), {
|
|
156
|
+
rowSelector: '.MuiDataGrid-row',
|
|
157
|
+
headerSelector: '.MuiDataGrid-columnHeader',
|
|
158
|
+
cellSelector: '.MuiDataGrid-cell',
|
|
159
|
+
pagination: TableStrategies.clickNext(
|
|
160
|
+
(root) => root.getByRole("button", { name: "Go to next page" })
|
|
161
|
+
),
|
|
162
|
+
maxPages: 5,
|
|
163
|
+
// Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
|
|
164
|
+
headerTransformer: ({ text }) => {
|
|
165
|
+
// We know there is only one empty column which we will rename to "Actions" for easier reference
|
|
166
|
+
if (text.includes('__col_') || text.trim() === '') {
|
|
167
|
+
return 'Actions';
|
|
168
|
+
}
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const headers = await table.getHeaders();
|
|
174
|
+
// Now we can reference the "Actions" column even if it has no header text
|
|
175
|
+
expect(headers).toContain('Actions');
|
|
176
|
+
|
|
177
|
+
// Use the renamed column
|
|
178
|
+
const row = await table.getByRow({ "Last name": "Melisandre" });
|
|
179
|
+
await row.getCell('Actions').getByLabel("Select row").click();
|
|
180
|
+
```
|
|
181
|
+
<!-- /embed: header-transformer -->
|
|
88
182
|
|
|
89
|
-
|
|
183
|
+
**Example 2: Normalizing Column Names**
|
|
90
184
|
|
|
91
|
-
|
|
185
|
+
Clean up inconsistent column names (extra spaces, inconsistent casing):
|
|
186
|
+
|
|
187
|
+
<!-- embed: header-transformer-normalize -->
|
|
188
|
+
```typescript
|
|
189
|
+
const table = useTable(page.locator('#table1'), {
|
|
190
|
+
// Normalize column names: remove extra spaces, handle inconsistent casing
|
|
191
|
+
headerTransformer: ({ text }) => {
|
|
192
|
+
return text.trim()
|
|
193
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
194
|
+
.replace(/^\s*|\s*$/g, ''); // Remove leading/trailing spaces
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Now column names are consistent
|
|
199
|
+
const row = await table.getByRow({ "Last Name": "Doe" });
|
|
200
|
+
await expect(row.getCell("Email")).toHaveText("jdoe@hotmail.com");
|
|
201
|
+
```
|
|
202
|
+
<!-- /embed: header-transformer-normalize -->
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## š API Reference
|
|
207
|
+
|
|
208
|
+
### Table Methods
|
|
209
|
+
|
|
210
|
+
#### <a name="getbyrow"></a>`getByRow(filters, options?)`
|
|
211
|
+
|
|
212
|
+
**Purpose:** Strict retrieval - finds exactly one row matching the filters.
|
|
213
|
+
|
|
214
|
+
**Behavior:**
|
|
215
|
+
- ā
Returns `SmartRow` if exactly one match
|
|
216
|
+
- ā Throws error if multiple matches (ambiguous query)
|
|
217
|
+
- š» Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
218
|
+
- š Auto-paginates if row isn't on current page
|
|
92
219
|
|
|
93
220
|
<!-- embed: get-by-row -->
|
|
94
221
|
```typescript
|
|
@@ -101,18 +228,49 @@ await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
|
101
228
|
```
|
|
102
229
|
<!-- /embed: get-by-row -->
|
|
103
230
|
|
|
104
|
-
|
|
231
|
+
**Type Signature:**
|
|
232
|
+
<!-- embed-type: TableResult -->
|
|
233
|
+
```typescript
|
|
234
|
+
export interface TableResult {
|
|
235
|
+
getHeaders: () => Promise<string[]>;
|
|
236
|
+
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
237
|
+
|
|
238
|
+
getByRow: <T extends { asJSON?: boolean }>(
|
|
239
|
+
filters: Record<string, string | RegExp | number>,
|
|
240
|
+
options?: { exact?: boolean, maxPages?: number } & T
|
|
241
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
242
|
+
|
|
243
|
+
getAllRows: <T extends { asJSON?: boolean }>(
|
|
244
|
+
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
245
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
246
|
+
|
|
247
|
+
generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
|
|
248
|
+
generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resets the table state (clears cache, flags) and invokes the onReset strategy.
|
|
252
|
+
*/
|
|
253
|
+
reset: () => Promise<void>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Scans a specific column across all pages and returns the values.
|
|
257
|
+
*/
|
|
258
|
+
getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
<!-- /embed-type: TableResult -->
|
|
105
262
|
|
|
106
|
-
|
|
263
|
+
#### <a name="getallrows"></a>`getAllRows(options?)`
|
|
107
264
|
|
|
108
|
-
|
|
265
|
+
**Purpose:** Inclusive retrieval - gets all rows matching optional filters.
|
|
109
266
|
|
|
110
|
-
Best for
|
|
267
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction.
|
|
111
268
|
|
|
112
269
|
<!-- embed: get-all-rows -->
|
|
113
270
|
```typescript
|
|
114
271
|
// 1. Get ALL rows on the current page
|
|
115
272
|
const allRows = await table.getAllRows();
|
|
273
|
+
expect(allRows.length).toBeGreaterThan(0);
|
|
116
274
|
|
|
117
275
|
// 2. Get subset of rows (Filtering)
|
|
118
276
|
const tokyoUsers = await table.getAllRows({
|
|
@@ -123,43 +281,278 @@ expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
|
123
281
|
// 3. Dump data to JSON
|
|
124
282
|
const data = await table.getAllRows({ asJSON: true });
|
|
125
283
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
284
|
+
expect(data.length).toBeGreaterThan(0);
|
|
285
|
+
expect(data[0]).toHaveProperty('Name');
|
|
126
286
|
```
|
|
127
287
|
<!-- /embed: get-all-rows -->
|
|
128
288
|
|
|
129
|
-
|
|
289
|
+
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
130
290
|
|
|
131
|
-
|
|
291
|
+
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
132
292
|
|
|
133
|
-
|
|
293
|
+
**Additional Examples:**
|
|
134
294
|
|
|
135
|
-
|
|
295
|
+
Get row data as JSON:
|
|
296
|
+
<!-- embed: get-by-row-json -->
|
|
297
|
+
```typescript
|
|
298
|
+
// Get row data directly as JSON object
|
|
299
|
+
const data = await table.getByRow({ Name: 'Airi Satou' }, { asJSON: true });
|
|
300
|
+
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
136
301
|
|
|
137
|
-
|
|
302
|
+
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
303
|
+
expect(data).toHaveProperty('Position');
|
|
304
|
+
```
|
|
305
|
+
<!-- /embed: get-by-row-json -->
|
|
306
|
+
|
|
307
|
+
Filter rows with exact match:
|
|
308
|
+
<!-- embed: get-all-rows-exact -->
|
|
309
|
+
```typescript
|
|
310
|
+
// Get rows with exact match (default is fuzzy/contains match)
|
|
311
|
+
const exactMatches = await table.getAllRows({
|
|
312
|
+
filter: { Office: 'Tokyo' },
|
|
313
|
+
exact: true // Requires exact string match
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(exactMatches.length).toBeGreaterThan(0);
|
|
317
|
+
```
|
|
318
|
+
<!-- /embed: get-all-rows-exact -->
|
|
319
|
+
|
|
320
|
+
Column scanning with custom mapper:
|
|
321
|
+
<!-- embed: advanced-column-scan-mapper -->
|
|
322
|
+
```typescript
|
|
323
|
+
// Extract numeric values from a column
|
|
324
|
+
const ages = await table.getColumnValues('Age', {
|
|
325
|
+
mapper: async (cell) => {
|
|
326
|
+
const text = await cell.innerText();
|
|
327
|
+
return parseInt(text, 10);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Now ages is an array of numbers
|
|
332
|
+
expect(ages.every(age => typeof age === 'number')).toBe(true);
|
|
333
|
+
expect(ages.length).toBeGreaterThan(0);
|
|
334
|
+
```
|
|
335
|
+
<!-- /embed: advanced-column-scan-mapper -->
|
|
336
|
+
|
|
337
|
+
#### <a name="reset"></a>`reset()`
|
|
338
|
+
|
|
339
|
+
Resets table state (clears cache, pagination flags) and invokes the `onReset` strategy to return to the first page.
|
|
340
|
+
|
|
341
|
+
#### <a name="getheaders"></a>`getHeaders()`
|
|
342
|
+
|
|
343
|
+
Returns an array of all column names in the table.
|
|
344
|
+
|
|
345
|
+
#### <a name="getheadercell"></a>`getHeaderCell(columnName)`
|
|
346
|
+
|
|
347
|
+
Returns a Playwright `Locator` for the specified header cell.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## š§© Pagination Strategies
|
|
352
|
+
|
|
353
|
+
This library uses the **Strategy Pattern** for pagination. Use built-in strategies or write custom ones.
|
|
354
|
+
|
|
355
|
+
### Built-in Strategies
|
|
356
|
+
|
|
357
|
+
#### <a name="tablestrategiesclicknext"></a>`TableStrategies.clickNext(selector)`
|
|
358
|
+
|
|
359
|
+
Best for standard paginated tables (Datatables, lists). Clicks a button/link and waits for table content to change.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
pagination: TableStrategies.clickNext((root) =>
|
|
138
363
|
root.page().getByRole('button', { name: 'Next' })
|
|
139
364
|
)
|
|
365
|
+
```
|
|
140
366
|
|
|
367
|
+
#### <a name="tablestrategiesinfinitescroll"></a>`TableStrategies.infiniteScroll()`
|
|
141
368
|
|
|
142
|
-
|
|
369
|
+
Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
143
370
|
|
|
371
|
+
```typescript
|
|
144
372
|
pagination: TableStrategies.infiniteScroll()
|
|
373
|
+
```
|
|
145
374
|
|
|
375
|
+
#### <a name="tablestrategiesclickloadmore"></a>`TableStrategies.clickLoadMore(selector)`
|
|
146
376
|
|
|
147
|
-
|
|
377
|
+
Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
148
378
|
|
|
149
|
-
|
|
379
|
+
```typescript
|
|
380
|
+
pagination: TableStrategies.clickLoadMore((root) =>
|
|
381
|
+
root.getByRole('button', { name: 'Load More' })
|
|
382
|
+
)
|
|
383
|
+
```
|
|
150
384
|
|
|
151
|
-
|
|
385
|
+
### Custom Strategies
|
|
152
386
|
|
|
153
|
-
|
|
387
|
+
A pagination strategy is a function that receives a `TableContext` and returns `Promise<boolean>` (true if more data loaded, false if no more pages):
|
|
154
388
|
|
|
155
|
-
|
|
389
|
+
<!-- embed-type: PaginationStrategy -->
|
|
390
|
+
```typescript
|
|
391
|
+
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
392
|
+
```
|
|
393
|
+
<!-- /embed-type: PaginationStrategy -->
|
|
156
394
|
|
|
157
|
-
|
|
158
|
-
|
|
395
|
+
<!-- embed-type: TableContext -->
|
|
396
|
+
```typescript
|
|
397
|
+
export interface TableContext {
|
|
398
|
+
root: Locator;
|
|
399
|
+
config: Required<TableConfig>;
|
|
400
|
+
page: Page;
|
|
401
|
+
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
<!-- /embed-type: TableContext -->
|
|
405
|
+
|
|
406
|
+
---
|
|
159
407
|
|
|
408
|
+
## š ļø Developer Tools
|
|
160
409
|
|
|
161
|
-
|
|
410
|
+
### <a name="generateconfigprompt"></a>`generateConfigPrompt(options?)`
|
|
162
411
|
|
|
163
|
-
|
|
412
|
+
Generates a prompt you can paste into ChatGPT/Gemini to automatically generate the `TableConfig` for your specific HTML.
|
|
164
413
|
|
|
414
|
+
```typescript
|
|
415
|
+
await table.generateConfigPrompt({ output: 'console' });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### <a name="generatestrategyprompt"></a>`generateStrategyPrompt(options?)`
|
|
419
|
+
|
|
420
|
+
Generates a prompt to help you write a custom pagination strategy.
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
165
423
|
await table.generateStrategyPrompt({ output: 'console' });
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Options:**
|
|
427
|
+
<!-- embed-type: PromptOptions -->
|
|
428
|
+
```typescript
|
|
429
|
+
export interface PromptOptions {
|
|
430
|
+
/**
|
|
431
|
+
* Output Strategy:
|
|
432
|
+
* - 'error': Throws an error with the prompt (Best for Cloud/QA Wolf to get clean text).
|
|
433
|
+
* - 'console': Standard console logs (Default).
|
|
434
|
+
*/
|
|
435
|
+
output?: 'console' | 'error';
|
|
436
|
+
includeTypes?: boolean;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
<!-- /embed-type: PromptOptions -->
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## š Type Reference
|
|
444
|
+
|
|
445
|
+
### Core Types
|
|
446
|
+
|
|
447
|
+
#### <a name="smartrow"></a>`SmartRow`
|
|
448
|
+
|
|
449
|
+
A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
450
|
+
|
|
451
|
+
<!-- embed-type: SmartRow -->
|
|
452
|
+
```typescript
|
|
453
|
+
export type SmartRow = Locator & {
|
|
454
|
+
getCell(column: string): Locator;
|
|
455
|
+
toJSON(): Promise<Record<string, string>>;
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
<!-- /embed-type: SmartRow -->
|
|
459
|
+
|
|
460
|
+
**Methods:**
|
|
461
|
+
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
462
|
+
- `toJSON()`: Extracts all cell data as a key-value object
|
|
463
|
+
|
|
464
|
+
All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
|
|
465
|
+
|
|
466
|
+
#### <a name="tableconfig"></a>`TableConfig`
|
|
467
|
+
|
|
468
|
+
Configuration options for `useTable()`.
|
|
469
|
+
|
|
470
|
+
<!-- embed-type: TableConfig -->
|
|
471
|
+
```typescript
|
|
472
|
+
export interface TableConfig {
|
|
473
|
+
rowSelector?: Selector;
|
|
474
|
+
headerSelector?: Selector;
|
|
475
|
+
cellSelector?: Selector;
|
|
476
|
+
pagination?: PaginationStrategy;
|
|
477
|
+
maxPages?: number;
|
|
478
|
+
/**
|
|
479
|
+
* Hook to rename columns dynamically.
|
|
480
|
+
* * @param args.text - The default innerText of the header.
|
|
481
|
+
* @param args.index - The column index.
|
|
482
|
+
* @param args.locator - The specific header cell locator.
|
|
483
|
+
*/
|
|
484
|
+
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
485
|
+
autoScroll?: boolean;
|
|
486
|
+
/**
|
|
487
|
+
* Enable debug mode to log internal state to console.
|
|
488
|
+
*/
|
|
489
|
+
debug?: boolean;
|
|
490
|
+
/**
|
|
491
|
+
* Strategy to reset the table to the first page.
|
|
492
|
+
* Called when table.reset() is invoked.
|
|
493
|
+
*/
|
|
494
|
+
onReset?: (context: TableContext) => Promise<void>;
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
<!-- /embed-type: TableConfig -->
|
|
498
|
+
|
|
499
|
+
**Property Descriptions:**
|
|
500
|
+
|
|
501
|
+
- `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
|
|
502
|
+
- `headerSelector`: CSS selector or function for header cells (default: `"th"`)
|
|
503
|
+
- `cellSelector`: CSS selector or function for data cells (default: `"td"`)
|
|
504
|
+
- `pagination`: Strategy function for navigating pages (default: no pagination)
|
|
505
|
+
- `maxPages`: Maximum pages to scan when searching (default: `1`)
|
|
506
|
+
- `headerTransformer`: Function to transform/rename column headers dynamically
|
|
507
|
+
- `autoScroll`: Automatically scroll table into view (default: `true`)
|
|
508
|
+
- `debug`: Enable verbose logging (default: `false`)
|
|
509
|
+
- `onReset`: Strategy called when `table.reset()` is invoked
|
|
510
|
+
|
|
511
|
+
#### <a name="selector"></a>`Selector`
|
|
512
|
+
|
|
513
|
+
Flexible selector type supporting strings, functions, or existing locators.
|
|
514
|
+
|
|
515
|
+
<!-- embed-type: Selector -->
|
|
516
|
+
```typescript
|
|
517
|
+
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
518
|
+
```
|
|
519
|
+
<!-- /embed-type: Selector -->
|
|
520
|
+
|
|
521
|
+
**Examples:**
|
|
522
|
+
```typescript
|
|
523
|
+
// String selector
|
|
524
|
+
rowSelector: 'tbody tr'
|
|
525
|
+
|
|
526
|
+
// Function selector (useful for complex cases)
|
|
527
|
+
rowSelector: (root) => root.locator('[role="row"]')
|
|
528
|
+
|
|
529
|
+
// Can also accept a Locator directly
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### <a name="paginationstrategy"></a>`PaginationStrategy`
|
|
533
|
+
|
|
534
|
+
Function signature for custom pagination logic.
|
|
535
|
+
|
|
536
|
+
<!-- embed-type: PaginationStrategy -->
|
|
537
|
+
```typescript
|
|
538
|
+
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
539
|
+
```
|
|
540
|
+
<!-- /embed-type: PaginationStrategy -->
|
|
541
|
+
|
|
542
|
+
Returns `true` if more data was loaded, `false` if pagination should stop.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## š Tips & Best Practices
|
|
547
|
+
|
|
548
|
+
1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
|
|
549
|
+
2. **Use Debug Mode**: When troubleshooting, enable `debug: true` to see what the library is doing
|
|
550
|
+
3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
|
|
551
|
+
4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
|
|
552
|
+
5. **Pagination Strategies**: Create reusable strategies for tables with similar pagination patterns
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## š License
|
|
557
|
+
|
|
558
|
+
ISC
|
package/dist/strategies/index.js
CHANGED
|
@@ -102,11 +102,6 @@ exports.TableStrategies = {
|
|
|
102
102
|
return false;
|
|
103
103
|
// 1. Trigger Scroll
|
|
104
104
|
yield rows.last().scrollIntoViewIfNeeded();
|
|
105
|
-
// Optional: Keyboard press for robust grid handling
|
|
106
|
-
try {
|
|
107
|
-
yield page.keyboard.press('End');
|
|
108
|
-
}
|
|
109
|
-
catch (e) { }
|
|
110
105
|
// 2. Smart Wait (Polling)
|
|
111
106
|
return yield waitForCondition(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
107
|
const newCount = yield rows.count();
|
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 = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n};\n\nexport interface TableContext {\n root: Locator;\n config: Required<TableConfig>;\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 (Best for Cloud/QA Wolf to get clean text).\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 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\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";
|
|
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\nexport interface TableContext {\n root: Locator;\n config: Required<TableConfig>;\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 (Best for Cloud/QA Wolf to get clean text).\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 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\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";
|
package/dist/typeContext.js
CHANGED
|
@@ -47,6 +47,15 @@ export interface TableConfig {
|
|
|
47
47
|
*/
|
|
48
48
|
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
49
49
|
autoScroll?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Enable debug mode to log internal state to console.
|
|
52
|
+
*/
|
|
53
|
+
debug?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Strategy to reset the table to the first page.
|
|
56
|
+
* Called when table.reset() is invoked.
|
|
57
|
+
*/
|
|
58
|
+
onReset?: (context: TableContext) => Promise<void>;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export interface TableResult {
|
|
@@ -64,5 +73,15 @@ export interface TableResult {
|
|
|
64
73
|
|
|
65
74
|
generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
|
|
66
75
|
generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resets the table state (clears cache, flags) and invokes the onReset strategy.
|
|
79
|
+
*/
|
|
80
|
+
reset: () => Promise<void>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scans a specific column across all pages and returns the values.
|
|
84
|
+
*/
|
|
85
|
+
getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;
|
|
67
86
|
}
|
|
68
87
|
`;
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,15 @@ export interface TableConfig {
|
|
|
38
38
|
locator: Locator;
|
|
39
39
|
}) => string | Promise<string>;
|
|
40
40
|
autoScroll?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Enable debug mode to log internal state to console.
|
|
43
|
+
*/
|
|
44
|
+
debug?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Strategy to reset the table to the first page.
|
|
47
|
+
* Called when table.reset() is invoked.
|
|
48
|
+
*/
|
|
49
|
+
onReset?: (context: TableContext) => Promise<void>;
|
|
41
50
|
}
|
|
42
51
|
export interface TableResult {
|
|
43
52
|
getHeaders: () => Promise<string[]>;
|
|
@@ -56,4 +65,15 @@ export interface TableResult {
|
|
|
56
65
|
} & T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
57
66
|
generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
|
|
58
67
|
generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Resets the table state (clears cache, flags) and invokes the onReset strategy.
|
|
70
|
+
*/
|
|
71
|
+
reset: () => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Scans a specific column across all pages and returns the values.
|
|
74
|
+
*/
|
|
75
|
+
getColumnValues: <V = string>(column: string, options?: {
|
|
76
|
+
mapper?: (cell: Locator) => Promise<V> | V;
|
|
77
|
+
maxPages?: number;
|
|
78
|
+
}) => Promise<V[]>;
|
|
59
79
|
}
|
package/dist/useTable.js
CHANGED
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.useTable = void 0;
|
|
13
13
|
const typeContext_1 = require("./typeContext");
|
|
14
14
|
const useTable = (rootLocator, configOptions = {}) => {
|
|
15
|
-
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 }, configOptions);
|
|
15
|
+
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);
|
|
16
16
|
const resolve = (item, parent) => {
|
|
17
17
|
if (typeof item === 'string')
|
|
18
18
|
return parent.locator(item);
|
|
@@ -20,10 +20,17 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
20
20
|
return item(parent);
|
|
21
21
|
return item;
|
|
22
22
|
};
|
|
23
|
+
// Internal State
|
|
23
24
|
let _headerMap = null;
|
|
25
|
+
let _hasPaginated = false;
|
|
26
|
+
const logDebug = (msg) => {
|
|
27
|
+
if (config.debug)
|
|
28
|
+
console.log(`š [SmartTable Debug] ${msg}`);
|
|
29
|
+
};
|
|
24
30
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
31
|
if (_headerMap)
|
|
26
32
|
return _headerMap;
|
|
33
|
+
logDebug('Mapping headers...');
|
|
27
34
|
if (config.autoScroll) {
|
|
28
35
|
try {
|
|
29
36
|
yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
|
|
@@ -37,7 +44,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
37
44
|
catch (e) { /* Ignore hydration */ }
|
|
38
45
|
// 1. Fetch data efficiently
|
|
39
46
|
const texts = yield headerLoc.allInnerTexts();
|
|
40
|
-
const locators = yield headerLoc.all();
|
|
47
|
+
const locators = yield headerLoc.all();
|
|
41
48
|
// 2. Map Headers (Async)
|
|
42
49
|
const entries = yield Promise.all(texts.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
50
|
let text = t.trim() || `__col_${i}`;
|
|
@@ -51,6 +58,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
51
58
|
return [text, i];
|
|
52
59
|
})));
|
|
53
60
|
_headerMap = new Map(entries);
|
|
61
|
+
logDebug(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
|
|
54
62
|
return _headerMap;
|
|
55
63
|
});
|
|
56
64
|
const _makeSmart = (rowLocator, map) => {
|
|
@@ -99,15 +107,18 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
99
107
|
const map = yield _getMap();
|
|
100
108
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
101
109
|
let currentPage = 1;
|
|
110
|
+
logDebug(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
|
|
102
111
|
while (true) {
|
|
103
112
|
const allRows = resolve(config.rowSelector, rootLocator);
|
|
104
113
|
const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
|
|
105
114
|
const count = yield matchedRows.count();
|
|
115
|
+
logDebug(`Page ${currentPage}: Found ${count} matches.`);
|
|
106
116
|
if (count > 1)
|
|
107
117
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
108
118
|
if (count === 1)
|
|
109
119
|
return matchedRows.first();
|
|
110
120
|
if (currentPage < effectiveMaxPages) {
|
|
121
|
+
logDebug(`Page ${currentPage}: Not found. Attempting pagination...`);
|
|
111
122
|
const context = {
|
|
112
123
|
root: rootLocator,
|
|
113
124
|
config: config,
|
|
@@ -116,9 +127,16 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
116
127
|
};
|
|
117
128
|
const didLoadMore = yield config.pagination(context);
|
|
118
129
|
if (didLoadMore) {
|
|
130
|
+
_hasPaginated = true;
|
|
119
131
|
currentPage++;
|
|
120
132
|
continue;
|
|
121
133
|
}
|
|
134
|
+
else {
|
|
135
|
+
logDebug(`Page ${currentPage}: Pagination failed (end of data).`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (_hasPaginated) {
|
|
139
|
+
console.warn(`ā ļø [SmartTable] Row not found. The table has been paginated (Current Page: ${currentPage}). You may need to call 'await table.reset()' if the target row is on a previous page.`);
|
|
122
140
|
}
|
|
123
141
|
return null;
|
|
124
142
|
}
|
|
@@ -135,6 +153,34 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
135
153
|
}
|
|
136
154
|
console.log(finalPrompt);
|
|
137
155
|
});
|
|
156
|
+
// Helper to extract clean HTML for prompts
|
|
157
|
+
const _getCleanHtml = (loc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
return loc.evaluate((el) => {
|
|
159
|
+
const clone = el.cloneNode(true);
|
|
160
|
+
// 1. Remove Heavy/Useless Elements
|
|
161
|
+
const removeSelectors = 'script, style, svg, path, circle, rect, noscript, [hidden]';
|
|
162
|
+
clone.querySelectorAll(removeSelectors).forEach(n => n.remove());
|
|
163
|
+
// 2. Clean Attributes
|
|
164
|
+
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_ELEMENT);
|
|
165
|
+
let currentNode = walker.currentNode;
|
|
166
|
+
while (currentNode) {
|
|
167
|
+
currentNode.removeAttribute('style'); // Inline styles are noise
|
|
168
|
+
currentNode.removeAttribute('data-reactid');
|
|
169
|
+
// 3. Condense Tailwind Classes (Heuristic)
|
|
170
|
+
// If class string is very long (>50 chars), keep the first few tokens and truncate.
|
|
171
|
+
// This preserves "MuiRow" but cuts "text-sm p-4 hover:bg-gray-50 ..."
|
|
172
|
+
const cls = currentNode.getAttribute('class');
|
|
173
|
+
if (cls && cls.length > 80) {
|
|
174
|
+
const tokens = cls.split(' ');
|
|
175
|
+
if (tokens.length > 5) {
|
|
176
|
+
currentNode.setAttribute('class', tokens.slice(0, 4).join(' ') + ' ...');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
currentNode = walker.nextNode();
|
|
180
|
+
}
|
|
181
|
+
return clone.outerHTML;
|
|
182
|
+
});
|
|
183
|
+
});
|
|
138
184
|
return {
|
|
139
185
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
140
186
|
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -144,6 +190,52 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
144
190
|
throw new Error(`Column '${columnName}' not found.`);
|
|
145
191
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
146
192
|
}),
|
|
193
|
+
reset: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
194
|
+
logDebug("Resetting table...");
|
|
195
|
+
const context = {
|
|
196
|
+
root: rootLocator,
|
|
197
|
+
config: config,
|
|
198
|
+
page: rootLocator.page(),
|
|
199
|
+
resolve: resolve
|
|
200
|
+
};
|
|
201
|
+
yield config.onReset(context);
|
|
202
|
+
_hasPaginated = false;
|
|
203
|
+
_headerMap = null;
|
|
204
|
+
logDebug("Table reset complete.");
|
|
205
|
+
}),
|
|
206
|
+
getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
var _a, _b;
|
|
208
|
+
const map = yield _getMap();
|
|
209
|
+
const colIdx = map.get(column);
|
|
210
|
+
if (colIdx === undefined)
|
|
211
|
+
throw new Error(`Column '${column}' not found.`);
|
|
212
|
+
const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
|
|
213
|
+
const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
|
|
214
|
+
let currentPage = 1;
|
|
215
|
+
const results = [];
|
|
216
|
+
logDebug(`Getting column values for '${column}' (Pages: ${effectiveMaxPages})`);
|
|
217
|
+
while (true) {
|
|
218
|
+
const rows = yield resolve(config.rowSelector, rootLocator).all();
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
const cell = typeof config.cellSelector === 'string'
|
|
221
|
+
? row.locator(config.cellSelector).nth(colIdx)
|
|
222
|
+
: resolve(config.cellSelector, row).nth(colIdx);
|
|
223
|
+
results.push(yield mapper(cell));
|
|
224
|
+
}
|
|
225
|
+
if (currentPage < effectiveMaxPages) {
|
|
226
|
+
const context = {
|
|
227
|
+
root: rootLocator, config, page: rootLocator.page(), resolve
|
|
228
|
+
};
|
|
229
|
+
if (yield config.pagination(context)) {
|
|
230
|
+
_hasPaginated = true;
|
|
231
|
+
currentPage++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
return results;
|
|
238
|
+
}),
|
|
147
239
|
getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
148
240
|
let row = yield _findRowLocator(filters, options);
|
|
149
241
|
if (!row) {
|
|
@@ -169,17 +261,17 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
169
261
|
return smartRows;
|
|
170
262
|
}),
|
|
171
263
|
generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
172
|
-
const html = yield rootLocator
|
|
264
|
+
const html = yield _getCleanHtml(rootLocator);
|
|
173
265
|
const separator = "=".repeat(50);
|
|
174
|
-
const content = `\n${separator}\nš¤ COPY INTO GEMINI/ChatGPT š¤\n${separator}\nI am using 'playwright-smart-table'. Generate config for:\n\`\`\`html\n${html.substring(0,
|
|
266
|
+
const content = `\n${separator}\nš¤ COPY INTO GEMINI/ChatGPT š¤\n${separator}\nI am using 'playwright-smart-table'. Generate config for:\n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n${separator}\n`;
|
|
175
267
|
yield _handlePrompt('Smart Table Config', content, options);
|
|
176
268
|
}),
|
|
177
269
|
generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
270
|
const container = rootLocator.locator('xpath=..');
|
|
179
|
-
const html = yield container
|
|
180
|
-
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,
|
|
271
|
+
const html = yield _getCleanHtml(container);
|
|
272
|
+
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`;
|
|
181
273
|
yield _handlePrompt('Smart Table Strategy', content, options);
|
|
182
|
-
})
|
|
274
|
+
}),
|
|
183
275
|
};
|
|
184
276
|
};
|
|
185
277
|
exports.useTable = useTable;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"build": "npm run generate-types && npm run generate-docs && tsc",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"test": "npx playwright test",
|
|
20
|
+
"test:compatibility": "npx playwright test compatibility",
|
|
20
21
|
"prepare": "husky install"
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|