@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 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
- // 🪄 Finds the row with Name="Airi Satou", then gets the Position cell.
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
- // 1. Get SmartRow via getByRow
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
- // 2. Interact with cell
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
- // 3. Dump data from row
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
- **Type Signature:**
232
- <!-- embed-type: TableResult -->
304
+ Get row data as JSON:
305
+ <!-- embed: get-by-row-json -->
233
306
  ```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>;
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
- * 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
- }
311
+ expect(data).toHaveProperty('Name', 'Airi Satou');
312
+ expect(data).toHaveProperty('Position');
260
313
  ```
261
- <!-- /embed-type: TableResult -->
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
- Column scanning with custom mapper:
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
 
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.1.3",
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",