@rickcedwhat/playwright-smart-table 2.1.2 → 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 CHANGED
@@ -1,41 +1,79 @@
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
- This library abstracts away the complexity of testing dynamic web tables. It handles Pagination, Infinite Scroll, Virtualization, and Data Grids (MUI, AG-Grid) so your tests remain clean and readable.
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
- Requires @playwright/test as a peer dependency.
13
+ ---
12
14
 
13
- ⚔ Quick Start
15
+ ## šŸŽÆ Getting Started
14
16
 
15
- The Standard HTML Table
17
+ ### Step 1: Basic Table Interaction
16
18
 
17
- For standard tables (<table>, <tr>, <td>), no configuration is needed (defaults work for most standard HTML 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
23
+ // Example from: https://datatables.net/examples/data_sources/dom
21
24
  const table = useTable(page.locator('#example'), {
22
25
  headerSelector: 'thead th' // Override for this specific site
23
26
  });
24
27
 
25
- // šŸŖ„ Finds the row with Name="Airi Satou", then gets the Position cell.
26
- // If Airi is on Page 2, it handles pagination automatically.
28
+ // Find the row with Name="Airi Satou", then get the Position cell
27
29
  const row = await table.getByRow({ Name: 'Airi Satou' });
28
30
 
29
31
  await expect(row.getCell('Position')).toHaveText('Accountant');
30
32
  ```
31
33
  <!-- /embed: quick-start -->
32
34
 
33
- Complex Grids (Material UI / AG-Grid / Divs)
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
+ // Example from: https://datatables.net/examples/data_sources/dom
47
+
48
+ // Get SmartRow via getByRow
49
+ const row = await table.getByRow({ Name: 'Airi Satou' });
50
+
51
+ // Interact with cell using column name (resilient to column reordering)
52
+ await row.getCell('Position').click();
53
+
54
+ // Extract row data as JSON
55
+ const data = await row.toJSON();
56
+ console.log(data);
57
+ // { Name: "Airi Satou", Position: "Accountant", ... }
58
+ ```
59
+ <!-- /embed: smart-row -->
60
+
61
+ **Key Benefits:**
62
+ - āœ… Column names instead of indices (survives column reordering)
63
+ - āœ… Extends Playwright's `Locator` API (all `.click()`, `.isVisible()`, etc. work)
64
+ - āœ… `.toJSON()` for quick data extraction
65
+
66
+ ---
67
+
68
+ ## šŸ”§ Configuration & Advanced Scenarios
34
69
 
35
- For modern React grids, simply override the selectors and define a pagination strategy.
70
+ ### Working with Paginated Tables
71
+
72
+ For tables that span multiple pages, configure a pagination strategy:
36
73
 
37
74
  <!-- embed: pagination -->
38
75
  ```typescript
76
+ // Example from: https://datatables.net/examples/data_sources/dom
39
77
  const table = useTable(page.locator('#example'), {
40
78
  rowSelector: 'tbody tr',
41
79
  headerSelector: 'thead th',
@@ -55,86 +93,205 @@ await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
55
93
  ```
56
94
  <!-- /embed: pagination -->
57
95
 
58
- 🧠 SmartRow Pattern
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.
96
+ ### Debug Mode
63
97
 
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();
72
-
73
- // 3. Dump data from row
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).
98
+ Enable debug logging to see exactly what the library is doing:
85
99
 
86
100
  <!-- embed: advanced-debug -->
87
101
  ```typescript
102
+ // Example from: https://datatables.net/examples/data_sources/dom
88
103
  const table = useTable(page.locator('#example'), {
89
104
  headerSelector: 'thead th',
90
- debug: true
105
+ debug: true // Enables verbose logging of internal operations
91
106
  });
107
+
108
+ const row = await table.getByRow({ Name: 'Airi Satou' });
109
+ await expect(row).toBeVisible();
92
110
  ```
93
111
  <!-- /embed: advanced-debug -->
94
112
 
95
- šŸ”„ Resetting State
113
+ This will log header mappings, row scans, and pagination triggers to help troubleshoot issues.
114
+
115
+ ### Resetting Table State
96
116
 
97
- If your tests navigate deep into a table (e.g., Page 5), subsequent searches might fail. Use .reset() to return to the start.
117
+ If your tests navigate deep into a paginated table, use `.reset()` to return to the first page:
98
118
 
99
119
  <!-- embed: advanced-reset -->
100
120
  ```typescript
101
- // Navigate deep into the table (simulated by finding a row on page 2)
102
- // For the test to pass, we need a valid row. 'Angelica Ramos' is usually on page 1 or 2 depending on sorting.
121
+ // Example from: https://datatables.net/examples/data_sources/dom
122
+ // Navigate deep into the table by searching for a row on a later page
103
123
  try {
104
124
  await table.getByRow({ Name: 'Angelica Ramos' });
105
125
  } catch (e) {}
106
126
 
107
127
  // Reset internal state (and potentially UI) to Page 1
108
128
  await table.reset();
129
+
130
+ // Now subsequent searches start from the beginning
131
+ const firstPageRow = await table.getByRow({ Name: 'Airi Satou' });
132
+ await expect(firstPageRow).toBeVisible();
109
133
  ```
110
134
  <!-- /embed: advanced-reset -->
111
135
 
112
- šŸ“Š Column Scanning
136
+ ### Column Scanning
113
137
 
114
- Need to verify a specific column is sorted or contains specific data? Use getColumnValues for a high-performance scan.
138
+ Efficiently extract all values from a specific column:
115
139
 
116
140
  <!-- embed: advanced-column-scan -->
117
141
  ```typescript
142
+ // Example from: https://datatables.net/examples/data_sources/dom
118
143
  // Quickly grab all text values from the "Office" column
119
144
  const offices = await table.getColumnValues('Office');
120
145
  expect(offices).toContain('Tokyo');
146
+ expect(offices.length).toBeGreaterThan(0);
121
147
  ```
122
148
  <!-- /embed: advanced-column-scan -->
123
149
 
124
- šŸ“– API Reference
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.
125
153
 
126
- getByRow(filters, options?)
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
+ });
127
165
 
128
- Strict Retrieval. Finds a single specific row.
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 -->
129
173
 
130
- Throws Error if >1 rows match (ambiguous query).
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"]`)
131
179
 
132
- Returns Sentinel if 0 rows match (allows not.toBeVisible() assertions).
180
+ **Custom input mappers:**
133
181
 
134
- Auto-Paginates if the row isn't found on the current page.
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
+
207
+ ### Transforming Column Headers
208
+
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.
210
+
211
+ **Example 1: Renaming Empty Columns**
212
+
213
+ Tables with empty header cells (like Material UI DataGrids) get auto-assigned names like `__col_0`, `__col_1`. Transform them to meaningful names:
214
+
215
+ <!-- embed: header-transformer -->
216
+ ```typescript
217
+ // Example from: https://mui.com/material-ui/react-table/
218
+ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
219
+ rowSelector: '.MuiDataGrid-row',
220
+ headerSelector: '.MuiDataGrid-columnHeader',
221
+ cellSelector: '.MuiDataGrid-cell',
222
+ pagination: TableStrategies.clickNext(
223
+ (root) => root.getByRole("button", { name: "Go to next page" })
224
+ ),
225
+ maxPages: 5,
226
+ // Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
227
+ headerTransformer: ({ text }) => {
228
+ // We know there is only one empty column which we will rename to "Actions" for easier reference
229
+ if (text.includes('__col_') || text.trim() === '') {
230
+ return 'Actions';
231
+ }
232
+ return text;
233
+ }
234
+ });
235
+
236
+ const headers = await table.getHeaders();
237
+ // Now we can reference the "Actions" column even if it has no header text
238
+ expect(headers).toContain('Actions');
239
+
240
+ // Use the renamed column
241
+ const row = await table.getByRow({ "Last name": "Melisandre" });
242
+ await row.getCell('Actions').getByLabel("Select row").click();
243
+ ```
244
+ <!-- /embed: header-transformer -->
245
+
246
+ **Example 2: Normalizing Column Names**
247
+
248
+ Clean up inconsistent column names (extra spaces, inconsistent casing):
249
+
250
+ <!-- embed: header-transformer-normalize -->
251
+ ```typescript
252
+ // Example from: https://the-internet.herokuapp.com/tables
253
+ const table = useTable(page.locator('#table1'), {
254
+ // Normalize column names: remove extra spaces, handle inconsistent casing
255
+ headerTransformer: ({ text }) => {
256
+ return text.trim()
257
+ .replace(/\s+/g, ' ') // Normalize whitespace
258
+ .replace(/^\s*|\s*$/g, ''); // Remove leading/trailing spaces
259
+ }
260
+ });
261
+
262
+ // Now column names are consistent
263
+ const row = await table.getByRow({ "Last Name": "Doe" });
264
+ await expect(row.getCell("Email")).toHaveText("jdoe@hotmail.com");
265
+ ```
266
+ <!-- /embed: header-transformer-normalize -->
267
+
268
+ ---
269
+
270
+ ## šŸ“– API Reference
271
+
272
+ ### Table Methods
273
+
274
+ #### <a name="getbyrow"></a>`getByRow(filters, options?)`
275
+
276
+ **Purpose:** Strict retrieval - finds exactly one row matching the filters.
277
+
278
+ **Behavior:**
279
+ - āœ… Returns `SmartRow` if exactly one match
280
+ - āŒ Throws error if multiple matches (ambiguous query)
281
+ - šŸ‘» Returns sentinel locator if no match (allows `.not.toBeVisible()` assertions)
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
+ ```
135
291
 
136
292
  <!-- embed: get-by-row -->
137
293
  ```typescript
294
+ // Example from: https://datatables.net/examples/data_sources/dom
138
295
  // Find a row where Name is "Airi Satou" AND Office is "Tokyo"
139
296
  const row = await table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
140
297
  await expect(row).toBeVisible();
@@ -144,18 +301,37 @@ await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
144
301
  ```
145
302
  <!-- /embed: get-by-row -->
146
303
 
147
- getAllRows(options?)
304
+ Get row data as JSON:
305
+ <!-- embed: get-by-row-json -->
306
+ ```typescript
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", ... }
310
+
311
+ expect(data).toHaveProperty('Name', 'Airi Satou');
312
+ expect(data).toHaveProperty('Position');
313
+ ```
314
+ <!-- /embed: get-by-row-json -->
148
315
 
149
- Inclusive Retrieval. Gets a collection of rows.
316
+ #### <a name="getallrows"></a>`getAllRows(options?)`
150
317
 
151
- Returns: Array of SmartRow objects.
318
+ **Purpose:** Inclusive retrieval - gets all rows matching optional filters.
152
319
 
153
- Best for: Checking existence ("at least one") or validating sort order.
320
+ **Best for:** Checking existence, validating sort order, bulk data extraction.
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
+ ```
154
328
 
155
329
  <!-- embed: get-all-rows -->
156
330
  ```typescript
331
+ // Example from: https://datatables.net/examples/data_sources/dom
157
332
  // 1. Get ALL rows on the current page
158
333
  const allRows = await table.getAllRows();
334
+ expect(allRows.length).toBeGreaterThan(0);
159
335
 
160
336
  // 2. Get subset of rows (Filtering)
161
337
  const tokyoUsers = await table.getAllRows({
@@ -166,40 +342,306 @@ expect(tokyoUsers.length).toBeGreaterThan(0);
166
342
  // 3. Dump data to JSON
167
343
  const data = await table.getAllRows({ asJSON: true });
168
344
  console.log(data); // [{ Name: "Airi Satou", ... }, ...]
345
+ expect(data.length).toBeGreaterThan(0);
346
+ expect(data[0]).toHaveProperty('Name');
169
347
  ```
170
348
  <!-- /embed: get-all-rows -->
171
349
 
172
- 🧩 Pagination Strategies
350
+ Filter rows with exact match:
351
+ <!-- embed: get-all-rows-exact -->
352
+ ```typescript
353
+ // Get rows with exact match (default is fuzzy/contains match)
354
+ const exactMatches = await table.getAllRows({
355
+ filter: { Office: 'Tokyo' },
356
+ exact: true // Requires exact string match
357
+ });
173
358
 
174
- This library uses the Strategy Pattern to handle navigation. You can use the built-in strategies or write your own.
359
+ expect(exactMatches.length).toBeGreaterThan(0);
360
+ ```
361
+ <!-- /embed: get-all-rows-exact -->
175
362
 
176
- Built-in Strategies
363
+ #### <a name="getcolumnvalues"></a>`getColumnValues(column, options?)`
177
364
 
178
- clickNext(selector) Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
365
+ Scans a specific column across all pages and returns values. Supports custom mappers for extracting non-text data.
179
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:
390
+ <!-- embed: advanced-column-scan-mapper -->
391
+ ```typescript
392
+ // Extract numeric values from a column
393
+ const ages = await table.getColumnValues('Age', {
394
+ mapper: async (cell) => {
395
+ const text = await cell.innerText();
396
+ return parseInt(text, 10);
397
+ }
398
+ });
399
+
400
+ // Now ages is an array of numbers
401
+ expect(ages.every(age => typeof age === 'number')).toBe(true);
402
+ expect(ages.length).toBeGreaterThan(0);
403
+ ```
404
+ <!-- /embed: advanced-column-scan-mapper -->
405
+
406
+ #### <a name="getheaders"></a>`getHeaders()`
407
+
408
+ Returns an array of all column names in the table.
409
+
410
+ **Type Signature:**
411
+ ```typescript
412
+ getHeaders: () => Promise<string[]>;
413
+ ```
414
+
415
+ #### <a name="getheadercell"></a>`getHeaderCell(columnName)`
416
+
417
+ Returns a Playwright `Locator` for the specified header cell.
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
+
433
+ ---
434
+
435
+ ## 🧩 Pagination Strategies
436
+
437
+ This library uses the **Strategy Pattern** for pagination. Use built-in strategies or write custom ones.
438
+
439
+ ### Built-in Strategies
440
+
441
+ #### <a name="tablestrategiesclicknext"></a>`TableStrategies.clickNext(selector)`
442
+
443
+ Best for standard paginated tables (Datatables, lists). Clicks a button/link and waits for table content to change.
444
+
445
+ ```typescript
180
446
  pagination: TableStrategies.clickNext((root) =>
181
- root.page().getByRole('button', { name: 'Next' })
447
+ root.page().getByRole('button', { name: 'Next' })
182
448
  )
449
+ ```
450
+
451
+ #### <a name="tablestrategiesinfinitescroll"></a>`TableStrategies.infiniteScroll()`
183
452
 
184
- infiniteScroll() Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
453
+ Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
185
454
 
455
+ ```typescript
186
456
  pagination: TableStrategies.infiniteScroll()
457
+ ```
458
+
459
+ #### <a name="tablestrategiesclickloadmore"></a>`TableStrategies.clickLoadMore(selector)`
187
460
 
188
- clickLoadMore(selector) Best for "Load More" buttons. Clicks and waits for row count to increase.
461
+ Best for "Load More" buttons. Clicks and waits for row count to increase.
462
+
463
+ ```typescript
464
+ pagination: TableStrategies.clickLoadMore((root) =>
465
+ root.getByRole('button', { name: 'Load More' })
466
+ )
467
+ ```
468
+
469
+ ### Custom Strategies
470
+
471
+ A pagination strategy is a function that receives a `TableContext` and returns `Promise<boolean>` (true if more data loaded, false if no more pages):
472
+
473
+ <!-- embed-type: PaginationStrategy -->
474
+ ```typescript
475
+ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
476
+ ```
477
+ <!-- /embed-type: PaginationStrategy -->
478
+
479
+ <!-- embed-type: TableContext -->
480
+ ```typescript
481
+ export interface TableContext {
482
+ root: Locator;
483
+ config: Required<TableConfig>;
484
+ page: Page;
485
+ resolve: (selector: Selector, parent: Locator | Page) => Locator;
486
+ }
487
+ ```
488
+ <!-- /embed-type: TableContext -->
189
489
 
190
- šŸ› ļø Developer Tools
490
+ ---
191
491
 
192
- Don't waste time writing selectors manually. Use the generator tools to create your config.
492
+ ## šŸ› ļø Developer Tools
193
493
 
194
- generateConfigPrompt(options?)
494
+ ### <a name="generateconfigprompt"></a>`generateConfigPrompt(options?)`
195
495
 
196
- Prints a prompt you can paste into ChatGPT/Gemini to generate the TableConfig for your specific HTML.
496
+ Generates a prompt you can paste into ChatGPT/Gemini to automatically generate the `TableConfig` for your specific HTML.
197
497
 
198
- // Options: 'console' (default), 'error' (Throw error to see prompt in trace/cloud)
498
+ ```typescript
199
499
  await table.generateConfigPrompt({ output: 'console' });
500
+ ```
501
+
502
+ ### <a name="generatestrategyprompt"></a>`generateStrategyPrompt(options?)`
503
+
504
+ Generates a prompt to help you write a custom pagination strategy.
505
+
506
+ ```typescript
507
+ await table.generateStrategyPrompt({ output: 'console' });
508
+ ```
509
+
510
+ **Options:**
511
+ <!-- embed-type: PromptOptions -->
512
+ ```typescript
513
+ export interface PromptOptions {
514
+ /**
515
+ * Output Strategy:
516
+ * - 'error': Throws an error with the prompt (Best for Cloud/QA Wolf to get clean text).
517
+ * - 'console': Standard console logs (Default).
518
+ */
519
+ output?: 'console' | 'error';
520
+ includeTypes?: boolean;
521
+ }
522
+ ```
523
+ <!-- /embed-type: PromptOptions -->
524
+
525
+ ---
526
+
527
+ ## šŸ“š Type Reference
528
+
529
+ ### Core Types
530
+
531
+ #### <a name="smartrow"></a>`SmartRow`
532
+
533
+ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
534
+
535
+ <!-- embed-type: SmartRow -->
536
+ ```typescript
537
+ export type SmartRow = Omit<Locator, 'fill'> & {
538
+ getCell(column: string): Locator;
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>;
544
+ };
545
+ ```
546
+ <!-- /embed-type: SmartRow -->
547
+
548
+ **Methods:**
549
+ - `getCell(column: string)`: Returns a `Locator` for the specified cell in this row
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
552
+
553
+ All standard Playwright `Locator` methods (`.click()`, `.isVisible()`, `.textContent()`, etc.) are also available.
554
+
555
+ #### <a name="tableconfig"></a>`TableConfig`
556
+
557
+ Configuration options for `useTable()`.
558
+
559
+ <!-- embed-type: TableConfig -->
560
+ ```typescript
561
+ export interface TableConfig {
562
+ rowSelector?: Selector;
563
+ headerSelector?: Selector;
564
+ cellSelector?: Selector;
565
+ pagination?: PaginationStrategy;
566
+ maxPages?: number;
567
+ /**
568
+ * Hook to rename columns dynamically.
569
+ * * @param args.text - The default innerText of the header.
570
+ * @param args.index - The column index.
571
+ * @param args.locator - The specific header cell locator.
572
+ */
573
+ headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
574
+ autoScroll?: boolean;
575
+ /**
576
+ * Enable debug mode to log internal state to console.
577
+ */
578
+ debug?: boolean;
579
+ /**
580
+ * Strategy to reset the table to the first page.
581
+ * Called when table.reset() is invoked.
582
+ */
583
+ onReset?: (context: TableContext) => Promise<void>;
584
+ }
585
+ ```
586
+ <!-- /embed-type: TableConfig -->
587
+
588
+ **Property Descriptions:**
589
+
590
+ - `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
591
+ - `headerSelector`: CSS selector or function for header cells (default: `"th"`)
592
+ - `cellSelector`: CSS selector or function for data cells (default: `"td"`)
593
+ - `pagination`: Strategy function for navigating pages (default: no pagination)
594
+ - `maxPages`: Maximum pages to scan when searching (default: `1`)
595
+ - `headerTransformer`: Function to transform/rename column headers dynamically
596
+ - `autoScroll`: Automatically scroll table into view (default: `true`)
597
+ - `debug`: Enable verbose logging (default: `false`)
598
+ - `onReset`: Strategy called when `table.reset()` is invoked
599
+
600
+ #### <a name="selector"></a>`Selector`
601
+
602
+ Flexible selector type supporting strings, functions, or existing locators.
603
+
604
+ <!-- embed-type: Selector -->
605
+ ```typescript
606
+ export type Selector = string | ((root: Locator | Page) => Locator);
607
+ ```
608
+ <!-- /embed-type: Selector -->
609
+
610
+ **Examples:**
611
+ ```typescript
612
+ // String selector
613
+ rowSelector: 'tbody tr'
614
+
615
+ // Function selector (useful for complex cases)
616
+ rowSelector: (root) => root.locator('[role="row"]')
617
+
618
+ // Can also accept a Locator directly
619
+ ```
620
+
621
+ #### <a name="paginationstrategy"></a>`PaginationStrategy`
622
+
623
+ Function signature for custom pagination logic.
624
+
625
+ <!-- embed-type: PaginationStrategy -->
626
+ ```typescript
627
+ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
628
+ ```
629
+ <!-- /embed-type: PaginationStrategy -->
630
+
631
+ Returns `true` if more data was loaded, `false` if pagination should stop.
632
+
633
+ ---
634
+
635
+ ## šŸš€ Tips & Best Practices
636
+
637
+ 1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
638
+ 2. **Use Debug Mode**: When troubleshooting, enable `debug: true` to see what the library is doing
639
+ 3. **Leverage SmartRow**: Use `.getCell()` instead of manual column indices - your tests will be more maintainable
640
+ 4. **Type Safety**: All methods are fully typed - use TypeScript for the best experience
641
+ 5. **Pagination Strategies**: Create reusable strategies for tables with similar pagination patterns
200
642
 
201
- generateStrategyPrompt(options?)
643
+ ---
202
644
 
203
- Prints a prompt to help you write a custom Pagination Strategy.
645
+ ## šŸ“ License
204
646
 
205
- await table.generateStrategyPrompt({ output: 'console' });
647
+ ISC
@@ -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();
@@ -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";
@@ -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
- throw new Error(`Column '${colName}' not found.`);
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 new Error(`Column '${colName}' not found.`);
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
- throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
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 new Error(`Column '${columnName}' not found.`);
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 new Error(`Column '${column}' not found.`);
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;
@@ -272,40 +404,6 @@ const useTable = (rootLocator, configOptions = {}) => {
272
404
  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
405
  yield _handlePrompt('Smart Table Strategy', content, options);
274
406
  }),
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
407
  };
310
408
  };
311
409
  exports.useTable = useTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "A smart table utility for Playwright with built-in pagination strategies that are fully extensible.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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": [