@rickcedwhat/playwright-smart-table 2.1.3 → 2.2.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 +148 -59
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +14 -1
- package/dist/types.d.ts +13 -1
- package/dist/useTable.js +140 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ For standard HTML tables (`<table>`, `<tr>`, `<td>`), the library works out of t
|
|
|
20
20
|
|
|
21
21
|
<!-- embed: quick-start -->
|
|
22
22
|
```typescript
|
|
23
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
23
24
|
const table = useTable(page.locator('#example'), {
|
|
24
25
|
headerSelector: 'thead th' // Override for this specific site
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
-
// If Airi is on Page 2, it handles pagination automatically.
|
|
28
|
+
// Find the row with Name="Airi Satou", then get the Position cell
|
|
29
29
|
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
30
30
|
|
|
31
31
|
await expect(row.getCell('Position')).toHaveText('Accountant');
|
|
@@ -43,14 +43,15 @@ The `SmartRow` is the core power of this library. Unlike a standard Playwright `
|
|
|
43
43
|
|
|
44
44
|
<!-- embed: smart-row -->
|
|
45
45
|
```typescript
|
|
46
|
-
//
|
|
46
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
47
|
+
|
|
48
|
+
// Get SmartRow via getByRow
|
|
47
49
|
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
48
50
|
|
|
49
|
-
//
|
|
50
|
-
// ✅ Good: Resilient to column reordering
|
|
51
|
+
// Interact with cell using column name (resilient to column reordering)
|
|
51
52
|
await row.getCell('Position').click();
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
+
// Extract row data as JSON
|
|
54
55
|
const data = await row.toJSON();
|
|
55
56
|
console.log(data);
|
|
56
57
|
// { Name: "Airi Satou", Position: "Accountant", ... }
|
|
@@ -72,6 +73,7 @@ For tables that span multiple pages, configure a pagination strategy:
|
|
|
72
73
|
|
|
73
74
|
<!-- embed: pagination -->
|
|
74
75
|
```typescript
|
|
76
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
75
77
|
const table = useTable(page.locator('#example'), {
|
|
76
78
|
rowSelector: 'tbody tr',
|
|
77
79
|
headerSelector: 'thead th',
|
|
@@ -97,6 +99,7 @@ Enable debug logging to see exactly what the library is doing:
|
|
|
97
99
|
|
|
98
100
|
<!-- embed: advanced-debug -->
|
|
99
101
|
```typescript
|
|
102
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
100
103
|
const table = useTable(page.locator('#example'), {
|
|
101
104
|
headerSelector: 'thead th',
|
|
102
105
|
debug: true // Enables verbose logging of internal operations
|
|
@@ -115,6 +118,7 @@ If your tests navigate deep into a paginated table, use `.reset()` to return to
|
|
|
115
118
|
|
|
116
119
|
<!-- embed: advanced-reset -->
|
|
117
120
|
```typescript
|
|
121
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
118
122
|
// Navigate deep into the table by searching for a row on a later page
|
|
119
123
|
try {
|
|
120
124
|
await table.getByRow({ Name: 'Angelica Ramos' });
|
|
@@ -135,6 +139,7 @@ Efficiently extract all values from a specific column:
|
|
|
135
139
|
|
|
136
140
|
<!-- embed: advanced-column-scan -->
|
|
137
141
|
```typescript
|
|
142
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
138
143
|
// Quickly grab all text values from the "Office" column
|
|
139
144
|
const offices = await table.getColumnValues('Office');
|
|
140
145
|
expect(offices).toContain('Tokyo');
|
|
@@ -142,6 +147,63 @@ expect(offices.length).toBeGreaterThan(0);
|
|
|
142
147
|
```
|
|
143
148
|
<!-- /embed: advanced-column-scan -->
|
|
144
149
|
|
|
150
|
+
### Filling Row Data
|
|
151
|
+
|
|
152
|
+
Use `fill()` to intelligently populate form fields in a table row. The method automatically detects input types (text inputs, selects, checkboxes, contenteditable divs) and fills them appropriately.
|
|
153
|
+
|
|
154
|
+
<!-- embed: fill-basic -->
|
|
155
|
+
```typescript
|
|
156
|
+
// Find a row and fill it with new data
|
|
157
|
+
const row = await table.getByRow({ ID: '1' });
|
|
158
|
+
|
|
159
|
+
await row.fill({
|
|
160
|
+
Name: 'John Updated',
|
|
161
|
+
Status: 'Inactive',
|
|
162
|
+
Active: false,
|
|
163
|
+
Notes: 'Updated notes here'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Verify the values were filled correctly
|
|
167
|
+
await expect(row.getCell('Name').locator('input')).toHaveValue('John Updated');
|
|
168
|
+
await expect(row.getCell('Status').locator('select')).toHaveValue('Inactive');
|
|
169
|
+
await expect(row.getCell('Active').locator('input[type="checkbox"]')).not.toBeChecked();
|
|
170
|
+
await expect(row.getCell('Notes').locator('textarea')).toHaveValue('Updated notes here');
|
|
171
|
+
```
|
|
172
|
+
<!-- /embed: fill-basic -->
|
|
173
|
+
|
|
174
|
+
**Auto-detection supports:**
|
|
175
|
+
- Text inputs (`input[type="text"]`, `textarea`)
|
|
176
|
+
- Select dropdowns (`select`)
|
|
177
|
+
- Checkboxes/radios (`input[type="checkbox"]`, `input[type="radio"]`, `[role="checkbox"]`)
|
|
178
|
+
- Contenteditable divs (`[contenteditable="true"]`)
|
|
179
|
+
|
|
180
|
+
**Custom input mappers:**
|
|
181
|
+
|
|
182
|
+
For edge cases where auto-detection doesn't work (e.g., custom components, multiple inputs in a cell), use per-column mappers:
|
|
183
|
+
|
|
184
|
+
<!-- embed: fill-custom-mappers -->
|
|
185
|
+
```typescript
|
|
186
|
+
const row = await table.getByRow({ ID: '1' });
|
|
187
|
+
|
|
188
|
+
// Use custom input mappers for specific columns
|
|
189
|
+
await row.fill({
|
|
190
|
+
Name: 'John Updated',
|
|
191
|
+
Status: 'Inactive'
|
|
192
|
+
}, {
|
|
193
|
+
inputMappers: {
|
|
194
|
+
// Name column has multiple inputs - target the primary one
|
|
195
|
+
Name: (cell) => cell.locator('.primary-input'),
|
|
196
|
+
// Status uses standard select, but we could customize if needed
|
|
197
|
+
Status: (cell) => cell.locator('select')
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Verify the values
|
|
202
|
+
await expect(row.getCell('Name').locator('.primary-input')).toHaveValue('John Updated');
|
|
203
|
+
await expect(row.getCell('Status').locator('select')).toHaveValue('Inactive');
|
|
204
|
+
```
|
|
205
|
+
<!-- /embed: fill-custom-mappers -->
|
|
206
|
+
|
|
145
207
|
### Transforming Column Headers
|
|
146
208
|
|
|
147
209
|
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.
|
|
@@ -152,6 +214,7 @@ Tables with empty header cells (like Material UI DataGrids) get auto-assigned na
|
|
|
152
214
|
|
|
153
215
|
<!-- embed: header-transformer -->
|
|
154
216
|
```typescript
|
|
217
|
+
// Example from: https://mui.com/material-ui/react-table/
|
|
155
218
|
const table = useTable(page.locator('.MuiDataGrid-root').first(), {
|
|
156
219
|
rowSelector: '.MuiDataGrid-row',
|
|
157
220
|
headerSelector: '.MuiDataGrid-columnHeader',
|
|
@@ -186,6 +249,7 @@ Clean up inconsistent column names (extra spaces, inconsistent casing):
|
|
|
186
249
|
|
|
187
250
|
<!-- embed: header-transformer-normalize -->
|
|
188
251
|
```typescript
|
|
252
|
+
// Example from: https://the-internet.herokuapp.com/tables
|
|
189
253
|
const table = useTable(page.locator('#table1'), {
|
|
190
254
|
// Normalize column names: remove extra spaces, handle inconsistent casing
|
|
191
255
|
headerTransformer: ({ text }) => {
|
|
@@ -215,10 +279,19 @@ await expect(row.getCell("Email")).toHaveText("jdoe@hotmail.com");
|
|
|
215
279
|
- ✅ Returns `SmartRow` if exactly one match
|
|
216
280
|
- ❌ Throws error if multiple matches (ambiguous query)
|
|
217
281
|
- 👻 Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
|
|
218
|
-
- 🔄 Auto-paginates if row isn't on current page
|
|
282
|
+
- 🔄 Auto-paginates if row isn't on current page (when `maxPages > 1` and pagination strategy is configured)
|
|
283
|
+
|
|
284
|
+
**Type Signature:**
|
|
285
|
+
```typescript
|
|
286
|
+
getByRow: <T extends { asJSON?: boolean }>(
|
|
287
|
+
filters: Record<string, string | RegExp | number>,
|
|
288
|
+
options?: { exact?: boolean, maxPages?: number } & T
|
|
289
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
290
|
+
```
|
|
219
291
|
|
|
220
292
|
<!-- embed: get-by-row -->
|
|
221
293
|
```typescript
|
|
294
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
222
295
|
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
223
296
|
const row = await table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
224
297
|
await expect(row).toBeVisible();
|
|
@@ -228,37 +301,17 @@ await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
|
228
301
|
```
|
|
229
302
|
<!-- /embed: get-by-row -->
|
|
230
303
|
|
|
231
|
-
|
|
232
|
-
<!-- embed
|
|
304
|
+
Get row data as JSON:
|
|
305
|
+
<!-- embed: get-by-row-json -->
|
|
233
306
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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>;
|
|
307
|
+
// Get row data directly as JSON object
|
|
308
|
+
const data = await table.getByRow({ Name: 'Airi Satou' }, { asJSON: true });
|
|
309
|
+
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
254
310
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*/
|
|
258
|
-
getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;
|
|
259
|
-
}
|
|
311
|
+
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
312
|
+
expect(data).toHaveProperty('Position');
|
|
260
313
|
```
|
|
261
|
-
<!-- /embed
|
|
314
|
+
<!-- /embed: get-by-row-json -->
|
|
262
315
|
|
|
263
316
|
#### <a name="getallrows"></a>`getAllRows(options?)`
|
|
264
317
|
|
|
@@ -266,8 +319,16 @@ export interface TableResult {
|
|
|
266
319
|
|
|
267
320
|
**Best for:** Checking existence, validating sort order, bulk data extraction.
|
|
268
321
|
|
|
322
|
+
**Type Signature:**
|
|
323
|
+
```typescript
|
|
324
|
+
getAllRows: <T extends { asJSON?: boolean }>(
|
|
325
|
+
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
326
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
327
|
+
```
|
|
328
|
+
|
|
269
329
|
<!-- embed: get-all-rows -->
|
|
270
330
|
```typescript
|
|
331
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
271
332
|
// 1. Get ALL rows on the current page
|
|
272
333
|
const allRows = await table.getAllRows();
|
|
273
334
|
expect(allRows.length).toBeGreaterThan(0);
|
|
@@ -286,24 +347,6 @@ expect(data[0]).toHaveProperty('Name');
|
|
|
286
347
|
```
|
|
287
348
|
<!-- /embed: get-all-rows -->
|
|
288
349
|
|
|
289
|
-
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
290
|
-
|
|
291
|
-
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
292
|
-
|
|
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", ... }
|
|
301
|
-
|
|
302
|
-
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
303
|
-
expect(data).toHaveProperty('Position');
|
|
304
|
-
```
|
|
305
|
-
<!-- /embed: get-by-row-json -->
|
|
306
|
-
|
|
307
350
|
Filter rows with exact match:
|
|
308
351
|
<!-- embed: get-all-rows-exact -->
|
|
309
352
|
```typescript
|
|
@@ -317,7 +360,33 @@ expect(exactMatches.length).toBeGreaterThan(0);
|
|
|
317
360
|
```
|
|
318
361
|
<!-- /embed: get-all-rows-exact -->
|
|
319
362
|
|
|
320
|
-
|
|
363
|
+
#### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
|
|
364
|
+
|
|
365
|
+
Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
|
|
366
|
+
|
|
367
|
+
**Type Signature:**
|
|
368
|
+
```typescript
|
|
369
|
+
getColumnValues: <V = string>(
|
|
370
|
+
column: string,
|
|
371
|
+
options?: {
|
|
372
|
+
mapper?: (cell: Locator) => Promise<V> | V,
|
|
373
|
+
maxPages?: number
|
|
374
|
+
}
|
|
375
|
+
) => Promise<V[]>;
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Basic usage:
|
|
379
|
+
<!-- embed: advanced-column-scan -->
|
|
380
|
+
```typescript
|
|
381
|
+
// Example from: https://datatables.net/examples/data_sources/dom
|
|
382
|
+
// Quickly grab all text values from the "Office" column
|
|
383
|
+
const offices = await table.getColumnValues('Office');
|
|
384
|
+
expect(offices).toContain('Tokyo');
|
|
385
|
+
expect(offices.length).toBeGreaterThan(0);
|
|
386
|
+
```
|
|
387
|
+
<!-- /embed: advanced-column-scan -->
|
|
388
|
+
|
|
389
|
+
With custom mapper:
|
|
321
390
|
<!-- embed: advanced-column-scan-mapper -->
|
|
322
391
|
```typescript
|
|
323
392
|
// Extract numeric values from a column
|
|
@@ -334,18 +403,33 @@ expect(ages.length).toBeGreaterThan(0);
|
|
|
334
403
|
```
|
|
335
404
|
<!-- /embed: advanced-column-scan-mapper -->
|
|
336
405
|
|
|
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
406
|
#### <a name="getheaders"></a>`getHeaders()`
|
|
342
407
|
|
|
343
408
|
Returns an array of all column names in the table.
|
|
344
409
|
|
|
410
|
+
**Type Signature:**
|
|
411
|
+
```typescript
|
|
412
|
+
getHeaders: () => Promise<string[]>;
|
|
413
|
+
```
|
|
414
|
+
|
|
345
415
|
#### <a name="getheadercell"></a>`getHeaderCell(columnName)`
|
|
346
416
|
|
|
347
417
|
Returns a Playwright `Locator` for the specified header cell.
|
|
348
418
|
|
|
419
|
+
**Type Signature:**
|
|
420
|
+
```typescript
|
|
421
|
+
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### <a name="reset"></a>`reset()`
|
|
425
|
+
|
|
426
|
+
Resets table state (clears cache, pagination flags) and invokes the `onReset` strategy to return to the first page.
|
|
427
|
+
|
|
428
|
+
**Type Signature:**
|
|
429
|
+
```typescript
|
|
430
|
+
reset: () => Promise<void>;
|
|
431
|
+
```
|
|
432
|
+
|
|
349
433
|
---
|
|
350
434
|
|
|
351
435
|
## 🧩 Pagination Strategies
|
|
@@ -450,9 +534,13 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
450
534
|
|
|
451
535
|
<!-- embed-type: SmartRow -->
|
|
452
536
|
```typescript
|
|
453
|
-
export type SmartRow = Locator & {
|
|
537
|
+
export type SmartRow = Omit<Locator, 'fill'> & {
|
|
454
538
|
getCell(column: string): Locator;
|
|
455
539
|
toJSON(): Promise<Record<string, string>>;
|
|
540
|
+
/**
|
|
541
|
+
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
542
|
+
*/
|
|
543
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
456
544
|
};
|
|
457
545
|
```
|
|
458
546
|
<!-- /embed-type: SmartRow -->
|
|
@@ -460,6 +548,7 @@ export type SmartRow = Locator & {
|
|
|
460
548
|
**Methods:**
|
|
461
549
|
- `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
|
|
462
550
|
- `toJSON()`: Extracts all cell data as a key-value object
|
|
551
|
+
- `fill(data, options?)`: Intelligently fills form fields in the row. Automatically detects input types or use `inputMappers` for custom control
|
|
463
552
|
|
|
464
553
|
All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
|
|
465
554
|
|
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 * 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";
|
|
6
|
+
export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Omit<Locator, 'fill'> & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport 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 FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n * Columns not specified here will use auto-detection.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n getByRow: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\n\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n}\n";
|
package/dist/typeContext.js
CHANGED
|
@@ -9,9 +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 = Locator & {
|
|
12
|
+
export type SmartRow = Omit<Locator, 'fill'> & {
|
|
13
13
|
getCell(column: string): Locator;
|
|
14
14
|
toJSON(): Promise<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
17
|
+
*/
|
|
18
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export interface TableContext {
|
|
@@ -58,6 +62,15 @@ export interface TableConfig {
|
|
|
58
62
|
onReset?: (context: TableContext) => Promise<void>;
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
export interface FillOptions {
|
|
66
|
+
/**
|
|
67
|
+
* Custom input mappers for specific columns.
|
|
68
|
+
* Maps column names to functions that return the input locator for that cell.
|
|
69
|
+
* Columns not specified here will use auto-detection.
|
|
70
|
+
*/
|
|
71
|
+
inputMappers?: Record<string, (cell: Locator) => Locator>;
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
export interface TableResult {
|
|
62
75
|
getHeaders: () => Promise<string[]>;
|
|
63
76
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +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 = Locator & {
|
|
3
|
+
export type SmartRow = Omit<Locator, 'fill'> & {
|
|
4
4
|
getCell(column: string): Locator;
|
|
5
5
|
toJSON(): Promise<Record<string, string>>;
|
|
6
|
+
/**
|
|
7
|
+
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
8
|
+
*/
|
|
9
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
6
10
|
};
|
|
7
11
|
export interface TableContext {
|
|
8
12
|
root: Locator;
|
|
@@ -48,6 +52,14 @@ export interface TableConfig {
|
|
|
48
52
|
*/
|
|
49
53
|
onReset?: (context: TableContext) => Promise<void>;
|
|
50
54
|
}
|
|
55
|
+
export interface FillOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Custom input mappers for specific columns.
|
|
58
|
+
* Maps column names to functions that return the input locator for that cell.
|
|
59
|
+
* Columns not specified here will use auto-detection.
|
|
60
|
+
*/
|
|
61
|
+
inputMappers?: Record<string, (cell: Locator) => Locator>;
|
|
62
|
+
}
|
|
51
63
|
export interface TableResult {
|
|
52
64
|
getHeaders: () => Promise<string[]>;
|
|
53
65
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
package/dist/useTable.js
CHANGED
|
@@ -27,6 +27,30 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
27
27
|
if (config.debug)
|
|
28
28
|
console.log(`🔎 [SmartTable Debug] ${msg}`);
|
|
29
29
|
};
|
|
30
|
+
const _suggestColumnName = (colName, availableColumns) => {
|
|
31
|
+
// Simple fuzzy matching - find columns with similar names
|
|
32
|
+
const lowerCol = colName.toLowerCase();
|
|
33
|
+
const suggestions = availableColumns.filter(col => col.toLowerCase().includes(lowerCol) ||
|
|
34
|
+
lowerCol.includes(col.toLowerCase()) ||
|
|
35
|
+
col.toLowerCase().replace(/\s+/g, '') === lowerCol.replace(/\s+/g, ''));
|
|
36
|
+
if (suggestions.length > 0 && suggestions[0] !== colName) {
|
|
37
|
+
return `. Did you mean "${suggestions[0]}"?`;
|
|
38
|
+
}
|
|
39
|
+
// Show similar column names (first 3)
|
|
40
|
+
if (availableColumns.length > 0 && availableColumns.length <= 10) {
|
|
41
|
+
return `. Available columns: ${availableColumns.map(c => `"${c}"`).join(', ')}`;
|
|
42
|
+
}
|
|
43
|
+
else if (availableColumns.length > 0) {
|
|
44
|
+
return `. Available columns (first 5): ${availableColumns.slice(0, 5).map(c => `"${c}"`).join(', ')}, ...`;
|
|
45
|
+
}
|
|
46
|
+
return '.';
|
|
47
|
+
};
|
|
48
|
+
const _createColumnError = (colName, map, context) => {
|
|
49
|
+
const availableColumns = Array.from(map.keys());
|
|
50
|
+
const suggestion = _suggestColumnName(colName, availableColumns);
|
|
51
|
+
const contextMsg = context ? ` (${context})` : '';
|
|
52
|
+
return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
|
|
53
|
+
};
|
|
30
54
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
55
|
if (_headerMap)
|
|
32
56
|
return _headerMap;
|
|
@@ -65,8 +89,11 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
65
89
|
const smart = rowLocator;
|
|
66
90
|
smart.getCell = (colName) => {
|
|
67
91
|
const idx = map.get(colName);
|
|
68
|
-
if (idx === undefined)
|
|
69
|
-
|
|
92
|
+
if (idx === undefined) {
|
|
93
|
+
const availableColumns = Array.from(map.keys());
|
|
94
|
+
const suggestion = _suggestColumnName(colName, availableColumns);
|
|
95
|
+
throw new Error(`Column "${colName}" not found${suggestion}`);
|
|
96
|
+
}
|
|
70
97
|
if (typeof config.cellSelector === 'string') {
|
|
71
98
|
return rowLocator.locator(config.cellSelector).nth(idx);
|
|
72
99
|
}
|
|
@@ -85,6 +112,92 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
85
112
|
}
|
|
86
113
|
return result;
|
|
87
114
|
});
|
|
115
|
+
smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
var _a;
|
|
117
|
+
logDebug(`Filling row with data: ${JSON.stringify(data)}`);
|
|
118
|
+
// Fill each column
|
|
119
|
+
for (const [colName, value] of Object.entries(data)) {
|
|
120
|
+
const colIdx = map.get(colName);
|
|
121
|
+
if (colIdx === undefined) {
|
|
122
|
+
throw _createColumnError(colName, map, 'in fill data');
|
|
123
|
+
}
|
|
124
|
+
const cell = smart.getCell(colName);
|
|
125
|
+
// Use custom input mapper for this column if provided, otherwise auto-detect
|
|
126
|
+
let inputLocator;
|
|
127
|
+
if ((_a = fillOptions === null || fillOptions === void 0 ? void 0 : fillOptions.inputMappers) === null || _a === void 0 ? void 0 : _a[colName]) {
|
|
128
|
+
inputLocator = fillOptions.inputMappers[colName](cell);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Auto-detect input type
|
|
132
|
+
// Try different input types in order of commonality
|
|
133
|
+
// Check for text input
|
|
134
|
+
const textInput = cell.locator('input[type="text"], input:not([type]), textarea').first();
|
|
135
|
+
const textInputCount = yield textInput.count().catch(() => 0);
|
|
136
|
+
// Check for select
|
|
137
|
+
const select = cell.locator('select').first();
|
|
138
|
+
const selectCount = yield select.count().catch(() => 0);
|
|
139
|
+
// Check for checkbox/radio
|
|
140
|
+
const checkbox = cell.locator('input[type="checkbox"], input[type="radio"], [role="checkbox"]').first();
|
|
141
|
+
const checkboxCount = yield checkbox.count().catch(() => 0);
|
|
142
|
+
// Check for contenteditable or div-based inputs
|
|
143
|
+
const contentEditable = cell.locator('[contenteditable="true"]').first();
|
|
144
|
+
const contentEditableCount = yield contentEditable.count().catch(() => 0);
|
|
145
|
+
// Determine which input to use (prioritize by commonality)
|
|
146
|
+
if (textInputCount > 0 && selectCount === 0 && checkboxCount === 0) {
|
|
147
|
+
inputLocator = textInput;
|
|
148
|
+
}
|
|
149
|
+
else if (selectCount > 0) {
|
|
150
|
+
inputLocator = select;
|
|
151
|
+
}
|
|
152
|
+
else if (checkboxCount > 0) {
|
|
153
|
+
inputLocator = checkbox;
|
|
154
|
+
}
|
|
155
|
+
else if (contentEditableCount > 0) {
|
|
156
|
+
inputLocator = contentEditable;
|
|
157
|
+
}
|
|
158
|
+
else if (textInputCount > 0) {
|
|
159
|
+
// Fallback to text input even if others exist
|
|
160
|
+
inputLocator = textInput;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// No input found - try to click the cell itself (might trigger an editor)
|
|
164
|
+
inputLocator = cell;
|
|
165
|
+
}
|
|
166
|
+
// Warn if multiple inputs found (ambiguous)
|
|
167
|
+
const totalInputs = textInputCount + selectCount + checkboxCount + contentEditableCount;
|
|
168
|
+
if (totalInputs > 1 && config.debug) {
|
|
169
|
+
logDebug(`⚠️ Multiple inputs found in cell "${colName}" (${totalInputs} total). Using first match. Consider using inputMapper option for explicit control.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Fill based on value type and input type
|
|
173
|
+
const inputTag = yield inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
174
|
+
const inputType = yield inputLocator.getAttribute('type').catch(() => null);
|
|
175
|
+
const isContentEditable = yield inputLocator.getAttribute('contenteditable').catch(() => null);
|
|
176
|
+
logDebug(`Filling "${colName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
|
|
177
|
+
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
178
|
+
// Boolean value for checkbox/radio
|
|
179
|
+
const shouldBeChecked = Boolean(value);
|
|
180
|
+
const isChecked = yield inputLocator.isChecked().catch(() => false);
|
|
181
|
+
if (isChecked !== shouldBeChecked) {
|
|
182
|
+
yield inputLocator.click();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (inputTag === 'select') {
|
|
186
|
+
// Select dropdown
|
|
187
|
+
yield inputLocator.selectOption(String(value));
|
|
188
|
+
}
|
|
189
|
+
else if (isContentEditable === 'true') {
|
|
190
|
+
// Contenteditable div
|
|
191
|
+
yield inputLocator.click();
|
|
192
|
+
yield inputLocator.fill(String(value));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Text input, textarea, or generic
|
|
196
|
+
yield inputLocator.fill(String(value));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
logDebug('Fill operation completed');
|
|
200
|
+
});
|
|
88
201
|
return smart;
|
|
89
202
|
};
|
|
90
203
|
const _applyFilters = (baseRows, filters, map, exact) => {
|
|
@@ -92,8 +205,9 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
92
205
|
const page = rootLocator.page();
|
|
93
206
|
for (const [colName, value] of Object.entries(filters)) {
|
|
94
207
|
const colIndex = map.get(colName);
|
|
95
|
-
if (colIndex === undefined)
|
|
96
|
-
throw
|
|
208
|
+
if (colIndex === undefined) {
|
|
209
|
+
throw _createColumnError(colName, map, 'in filter');
|
|
210
|
+
}
|
|
97
211
|
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
98
212
|
const cellTemplate = resolve(config.cellSelector, page);
|
|
99
213
|
filtered = filtered.filter({
|
|
@@ -113,8 +227,26 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
113
227
|
const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
|
|
114
228
|
const count = yield matchedRows.count();
|
|
115
229
|
logDebug(`Page ${currentPage}: Found ${count} matches.`);
|
|
116
|
-
if (count > 1)
|
|
117
|
-
|
|
230
|
+
if (count > 1) {
|
|
231
|
+
// Try to get sample row data to help user identify the issue
|
|
232
|
+
const sampleData = [];
|
|
233
|
+
try {
|
|
234
|
+
const firstFewRows = yield matchedRows.all();
|
|
235
|
+
const sampleCount = Math.min(firstFewRows.length, 3);
|
|
236
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
237
|
+
const rowData = yield _makeSmart(firstFewRows[i], map).toJSON();
|
|
238
|
+
sampleData.push(JSON.stringify(rowData));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
// If we can't extract sample data, that's okay - continue without it
|
|
243
|
+
}
|
|
244
|
+
const sampleMsg = sampleData.length > 0
|
|
245
|
+
? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}`
|
|
246
|
+
: '';
|
|
247
|
+
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
|
|
248
|
+
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
249
|
+
}
|
|
118
250
|
if (count === 1)
|
|
119
251
|
return matchedRows.first();
|
|
120
252
|
if (currentPage < effectiveMaxPages) {
|
|
@@ -187,7 +319,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
187
319
|
const map = yield _getMap();
|
|
188
320
|
const idx = map.get(columnName);
|
|
189
321
|
if (idx === undefined)
|
|
190
|
-
throw
|
|
322
|
+
throw _createColumnError(columnName, map, 'header cell');
|
|
191
323
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
192
324
|
}),
|
|
193
325
|
reset: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -208,7 +340,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
208
340
|
const map = yield _getMap();
|
|
209
341
|
const colIdx = map.get(column);
|
|
210
342
|
if (colIdx === undefined)
|
|
211
|
-
throw
|
|
343
|
+
throw _createColumnError(column, map);
|
|
212
344
|
const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
|
|
213
345
|
const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
|
|
214
346
|
let currentPage = 1;
|
package/package.json
CHANGED