@rickcedwhat/playwright-smart-table 2.1.1 → 2.1.3

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