@rickcedwhat/playwright-smart-table 2.1.2 ā 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 +422 -69
- package/dist/strategies/index.js +0 -5
- package/dist/useTable.js +0 -34
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,20 +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
|
+
|
|
11
|
+
> **Note:** Requires `@playwright/test` as a peer dependency.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
---
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## šÆ Getting Started
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
### Step 1: Basic Table Interaction
|
|
16
18
|
|
|
17
|
-
For standard tables (
|
|
19
|
+
For standard HTML tables (`<table>`, `<tr>`, `<td>`), the library works out of the box with sensible defaults:
|
|
18
20
|
|
|
19
21
|
<!-- embed: quick-start -->
|
|
20
22
|
```typescript
|
|
@@ -30,9 +32,43 @@ await expect(row.getCell('Position')).toHaveText('Accountant');
|
|
|
30
32
|
```
|
|
31
33
|
<!-- /embed: quick-start -->
|
|
32
34
|
|
|
33
|
-
|
|
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
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## š§ Configuration & Advanced Scenarios
|
|
34
68
|
|
|
35
|
-
|
|
69
|
+
### Working with Paginated Tables
|
|
70
|
+
|
|
71
|
+
For tables that span multiple pages, configure a pagination strategy:
|
|
36
72
|
|
|
37
73
|
<!-- embed: pagination -->
|
|
38
74
|
```typescript
|
|
@@ -55,83 +91,131 @@ await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
|
55
91
|
```
|
|
56
92
|
<!-- /embed: pagination -->
|
|
57
93
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
The core power of this library is the SmartRow.
|
|
61
|
-
|
|
62
|
-
Unlike a standard Playwright Locator, a SmartRow is aware of its context within the table's schema. It extends the standard Locator API, so you can chain standard Playwright methods (.click(), .isVisible()) directly off it.
|
|
63
|
-
|
|
64
|
-
<!-- embed: smart-row -->
|
|
65
|
-
```typescript
|
|
66
|
-
// 1. Get SmartRow via getByRow
|
|
67
|
-
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
68
|
-
|
|
69
|
-
// 2. Interact with cell (No more getByCell needed!)
|
|
70
|
-
// ā
Good: Resilient to column reordering
|
|
71
|
-
await row.getCell('Position').click();
|
|
94
|
+
### Debug Mode
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
const data = await row.toJSON();
|
|
75
|
-
console.log(data);
|
|
76
|
-
// { Name: "Airi Satou", Position: "Accountant", ... }
|
|
77
|
-
```
|
|
78
|
-
<!-- /embed: smart-row -->
|
|
79
|
-
|
|
80
|
-
š Advanced Usage
|
|
81
|
-
|
|
82
|
-
š Debug Mode
|
|
83
|
-
|
|
84
|
-
Having trouble finding rows? Enable debug mode to see exactly what the library sees (headers mapped, rows scanned, pagination triggers).
|
|
96
|
+
Enable debug logging to see exactly what the library is doing:
|
|
85
97
|
|
|
86
98
|
<!-- embed: advanced-debug -->
|
|
87
99
|
```typescript
|
|
88
100
|
const table = useTable(page.locator('#example'), {
|
|
89
101
|
headerSelector: 'thead th',
|
|
90
|
-
debug: true
|
|
102
|
+
debug: true // Enables verbose logging of internal operations
|
|
91
103
|
});
|
|
104
|
+
|
|
105
|
+
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
106
|
+
await expect(row).toBeVisible();
|
|
92
107
|
```
|
|
93
108
|
<!-- /embed: advanced-debug -->
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
This will log header mappings, row scans, and pagination triggers to help troubleshoot issues.
|
|
111
|
+
|
|
112
|
+
### Resetting Table State
|
|
96
113
|
|
|
97
|
-
If your tests navigate deep into a table
|
|
114
|
+
If your tests navigate deep into a paginated table, use `.reset()` to return to the first page:
|
|
98
115
|
|
|
99
116
|
<!-- embed: advanced-reset -->
|
|
100
117
|
```typescript
|
|
101
|
-
// Navigate deep into the table
|
|
102
|
-
// For the test to pass, we need a valid row. 'Angelica Ramos' is usually on page 1 or 2 depending on sorting.
|
|
118
|
+
// Navigate deep into the table by searching for a row on a later page
|
|
103
119
|
try {
|
|
104
120
|
await table.getByRow({ Name: 'Angelica Ramos' });
|
|
105
121
|
} catch (e) {}
|
|
106
122
|
|
|
107
123
|
// Reset internal state (and potentially UI) to Page 1
|
|
108
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();
|
|
109
129
|
```
|
|
110
130
|
<!-- /embed: advanced-reset -->
|
|
111
131
|
|
|
112
|
-
|
|
132
|
+
### Column Scanning
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
Efficiently extract all values from a specific column:
|
|
115
135
|
|
|
116
136
|
<!-- embed: advanced-column-scan -->
|
|
117
137
|
```typescript
|
|
118
138
|
// Quickly grab all text values from the "Office" column
|
|
119
139
|
const offices = await table.getColumnValues('Office');
|
|
120
140
|
expect(offices).toContain('Tokyo');
|
|
141
|
+
expect(offices.length).toBeGreaterThan(0);
|
|
121
142
|
```
|
|
122
143
|
<!-- /embed: advanced-column-scan -->
|
|
123
144
|
|
|
124
|
-
|
|
145
|
+
### Transforming Column Headers
|
|
146
|
+
|
|
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.
|
|
148
|
+
|
|
149
|
+
**Example 1: Renaming Empty Columns**
|
|
125
150
|
|
|
126
|
-
|
|
151
|
+
Tables with empty header cells (like Material UI DataGrids) get auto-assigned names like `__col_0`, `__col_1`. Transform them to meaningful names:
|
|
127
152
|
|
|
128
|
-
|
|
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 -->
|
|
129
182
|
|
|
130
|
-
|
|
183
|
+
**Example 2: Normalizing Column Names**
|
|
131
184
|
|
|
132
|
-
|
|
185
|
+
Clean up inconsistent column names (extra spaces, inconsistent casing):
|
|
133
186
|
|
|
134
|
-
|
|
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
|
|
135
219
|
|
|
136
220
|
<!-- embed: get-by-row -->
|
|
137
221
|
```typescript
|
|
@@ -144,18 +228,49 @@ await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
|
144
228
|
```
|
|
145
229
|
<!-- /embed: get-by-row -->
|
|
146
230
|
|
|
147
|
-
|
|
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 -->
|
|
148
262
|
|
|
149
|
-
|
|
263
|
+
#### <a name="getallrows"></a>`getAllRows(options?)`
|
|
150
264
|
|
|
151
|
-
|
|
265
|
+
**Purpose:** Inclusive retrieval - gets all rows matching optional filters.
|
|
152
266
|
|
|
153
|
-
Best for
|
|
267
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction.
|
|
154
268
|
|
|
155
269
|
<!-- embed: get-all-rows -->
|
|
156
270
|
```typescript
|
|
157
271
|
// 1. Get ALL rows on the current page
|
|
158
272
|
const allRows = await table.getAllRows();
|
|
273
|
+
expect(allRows.length).toBeGreaterThan(0);
|
|
159
274
|
|
|
160
275
|
// 2. Get subset of rows (Filtering)
|
|
161
276
|
const tokyoUsers = await table.getAllRows({
|
|
@@ -166,40 +281,278 @@ expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
|
166
281
|
// 3. Dump data to JSON
|
|
167
282
|
const data = await table.getAllRows({ asJSON: true });
|
|
168
283
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
284
|
+
expect(data.length).toBeGreaterThan(0);
|
|
285
|
+
expect(data[0]).toHaveProperty('Name');
|
|
169
286
|
```
|
|
170
287
|
<!-- /embed: get-all-rows -->
|
|
171
288
|
|
|
172
|
-
|
|
289
|
+
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
173
290
|
|
|
174
|
-
|
|
291
|
+
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
175
292
|
|
|
176
|
-
|
|
293
|
+
**Additional Examples:**
|
|
294
|
+
|
|
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", ... }
|
|
177
301
|
|
|
178
|
-
|
|
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
|
+
});
|
|
179
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
|
|
180
362
|
pagination: TableStrategies.clickNext((root) =>
|
|
181
|
-
root.page().getByRole('button', { name: 'Next' })
|
|
363
|
+
root.page().getByRole('button', { name: 'Next' })
|
|
182
364
|
)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### <a name="tablestrategiesinfinitescroll"></a>`TableStrategies.infiniteScroll()`
|
|
183
368
|
|
|
184
|
-
|
|
369
|
+
Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
185
370
|
|
|
371
|
+
```typescript
|
|
186
372
|
pagination: TableStrategies.infiniteScroll()
|
|
373
|
+
```
|
|
187
374
|
|
|
188
|
-
|
|
375
|
+
#### <a name="tablestrategiesclickloadmore"></a>`TableStrategies.clickLoadMore(selector)`
|
|
189
376
|
|
|
190
|
-
|
|
377
|
+
Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
191
378
|
|
|
192
|
-
|
|
379
|
+
```typescript
|
|
380
|
+
pagination: TableStrategies.clickLoadMore((root) =>
|
|
381
|
+
root.getByRole('button', { name: 'Load More' })
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Custom Strategies
|
|
386
|
+
|
|
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):
|
|
388
|
+
|
|
389
|
+
<!-- embed-type: PaginationStrategy -->
|
|
390
|
+
```typescript
|
|
391
|
+
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
392
|
+
```
|
|
393
|
+
<!-- /embed-type: PaginationStrategy -->
|
|
394
|
+
|
|
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 -->
|
|
193
405
|
|
|
194
|
-
|
|
406
|
+
---
|
|
195
407
|
|
|
196
|
-
|
|
408
|
+
## š ļø Developer Tools
|
|
197
409
|
|
|
198
|
-
|
|
410
|
+
### <a name="generateconfigprompt"></a>`generateConfigPrompt(options?)`
|
|
411
|
+
|
|
412
|
+
Generates a prompt you can paste into ChatGPT/Gemini to automatically generate the `TableConfig` for your specific HTML.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
199
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
|
|
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
|
|
200
553
|
|
|
201
|
-
|
|
554
|
+
---
|
|
202
555
|
|
|
203
|
-
|
|
556
|
+
## š License
|
|
204
557
|
|
|
205
|
-
|
|
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/useTable.js
CHANGED
|
@@ -272,40 +272,6 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
272
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`;
|
|
273
273
|
yield _handlePrompt('Smart Table Strategy', content, options);
|
|
274
274
|
}),
|
|
275
|
-
/* * š§ ROADMAP (v2.2) š§
|
|
276
|
-
* The following features are planned. Implementations are tentative.
|
|
277
|
-
* DO NOT DELETE THIS SECTION UNTIL IMPLEMENTED OR REMOVED.
|
|
278
|
-
* THIS IS BEING USED TO TRACK FUTURE DEVELOPMENT.
|
|
279
|
-
*/
|
|
280
|
-
// __roadmap__fill: async (data: Record<string, any>) => {
|
|
281
|
-
// /* // * Goal: Fill a row with data intelligently.
|
|
282
|
-
// * Priority: Medium
|
|
283
|
-
// * Challenge: Handling different input types (select, checkbox, custom divs) blindly.
|
|
284
|
-
// */
|
|
285
|
-
// // const row = ... get row context ...
|
|
286
|
-
// // for (const [col, val] of Object.entries(data)) {
|
|
287
|
-
// // const cell = row.getCell(col);
|
|
288
|
-
// // const input = cell.locator('input, select, [role="checkbox"]');
|
|
289
|
-
// // if (await input.count() > 1) console.warn("Ambiguous input");
|
|
290
|
-
// // // Heuristics go here...
|
|
291
|
-
// // }
|
|
292
|
-
// // Note: Maybe we could pass the locator in the options for more control.
|
|
293
|
-
// },
|
|
294
|
-
// __roadmap__auditPages: async (options: { maxPages: number, audit: (rows: SmartRow[], page: number) => Promise<void> }) => {
|
|
295
|
-
// /*
|
|
296
|
-
// * Goal: Walk through pages and run a verification function on every page.
|
|
297
|
-
// * Priority: Low (Specific use case)
|
|
298
|
-
// * Logic:
|
|
299
|
-
// * let page = 1;
|
|
300
|
-
// * while (page <= options.maxPages) {
|
|
301
|
-
// * const rows = await getAllRows();
|
|
302
|
-
// * await options.audit(rows, page);
|
|
303
|
-
// * if (!await pagination(ctx)) break;
|
|
304
|
-
// * page++;
|
|
305
|
-
// * }
|
|
306
|
-
// */
|
|
307
|
-
// // Note: Maybe make is possible to skip several pages at once if the pagination strategy supports it.
|
|
308
|
-
// }
|
|
309
275
|
};
|
|
310
276
|
};
|
|
311
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": [
|