@cerios/playwright-table 1.1.1 → 2.0.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,14 +1,30 @@
1
1
  # 🎭 Playwright Tables | By Cerios
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@cerios/playwright-table.svg?style=flat-square)](https://www.npmjs.com/package/@cerios/playwright-table)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@cerios/playwright-table.svg?style=flat-square)](https://www.npmjs.com/package/@cerios/playwright-table)
5
+ [![license](https://img.shields.io/npm/l/@cerios/playwright-table.svg?style=flat-square)](https://github.com/CeriosTesting/playwright-tables/blob/main/LICENSE)
6
+ [![Playwright](https://img.shields.io/badge/playwright-%3E%3D1.40.0-45ba4b.svg?style=flat-square&logo=playwright)](https://playwright.dev)
7
+
3
8
  ## Introduction
4
9
 
5
- Testing HTML tables can be challenging due to their structure. This package simplifies the process by parsing HTML tables into JSON objects based on headers and rows, making it easier to assert table data in your tests.
10
+ Testing HTML tables can be challenging due to their complex structures with colspan, rowspan, dynamic content, and nested elements. This package simplifies the process by providing a comprehensive API to parse, validate, and interact with HTML tables in your Playwright tests.
11
+
12
+ **Key Features:**
13
+
14
+ - ✅ Full support for **colspan** and **rowspan** attributes
15
+ - ✅ Handles **multiple header rows** with flexible main header selection
16
+ - ✅ **Dynamic content loading** with polling and stability checks
17
+ - ✅ **Parallel data fetching** for optimal performance
18
+ - ✅ Robust **error handling** with detailed context
19
+ - ✅ Support for both **InnerText** and **TextContent** extraction
20
+ - ✅ Works with standard `<table>` elements and custom div-based tables
21
+ - ✅ Comprehensive **waiting utilities** for dynamic tables
6
22
 
7
23
  ---
8
24
 
9
25
  ## Installation
10
26
 
11
- First, install and familiarize yourself with the Playwright testing library:
27
+ First, ensure you have Playwright installed:
12
28
  [Playwright Documentation](https://github.com/microsoft/playwright/blob/main/README.md)
13
29
 
14
30
  Then, install this package:
@@ -17,431 +33,843 @@ Then, install this package:
17
33
  npm i @cerios/playwright-table
18
34
  ```
19
35
 
20
- Or as Dev dependency
36
+ Or as a dev dependency:
21
37
 
22
38
  ```bash
23
39
  npm i -D @cerios/playwright-table
24
40
  ```
25
41
 
26
- ---
42
+ > ⚠️ **Upgrading from v1.x?** See the [Migration Guide](./MIGRATIONv2.md) for breaking changes and upgrade instructions.
27
43
 
28
- ## Usage
44
+ ---
29
45
 
30
- ### Basic Setup
46
+ ## Quick Start
31
47
 
32
- To use the package, create a new `PlaywrightTable` instance by passing a locator for the table element:
48
+ ### Basic Usage
33
49
 
34
50
  ```ts
51
+ import { PlaywrightTable } from "@cerios/playwright-table";
52
+
53
+ // Create a table instance
35
54
  const table = new PlaywrightTable(page.locator("table"));
55
+
56
+ // Get table data as JSON
57
+ const data = await table.getJson();
58
+ console.log(data);
59
+ // Output: [{"Name": "John", "Age": "30"}, {"Name": "Jane", "Age": "25"}]
60
+
61
+ // Get specific cell locator
62
+ const cell = table.getBodyCellLocator(0, 1); // Row 0, Column 1
63
+ await expect(cell).toHaveText("30");
64
+
65
+ // Wait for dynamic content
66
+ await table.waitForExactRowCount(5, { timeout: 10000 });
36
67
  ```
37
68
 
38
- By default, the following selectors are used:
69
+ ### Default Selectors
70
+
71
+ By default, the following CSS selectors are used:
39
72
 
40
- - Header rows: `thead > tr`
41
- - Header cells: `th`
42
- - Body rows: `tbody > tr`
43
- - Body cells: `td`
73
+ - **Header rows:** `thead > tr`
74
+ - **Header cells:** `th`
75
+ - **Body rows:** `tbody > tr`
76
+ - **Body cells:** `td`
77
+
78
+ You can customize these selectors in the constructor (see [Advanced Options](#advanced-options)).
44
79
 
45
80
  ---
46
81
 
47
- ## Methods and Examples
82
+ ## Core Methods
83
+
84
+ ### 1. `getHeaderRows(options?)`
85
+
86
+ Retrieves all header rows with support for colspan and rowspan. Colspan cells get a suffix (`__C1`, `__C2`, etc.).
48
87
 
49
- ### 1. `getHeaderRows`
88
+ **Parameters:**
50
89
 
51
- Retrieves all header rows of the table. Works with colspan and rowspan. Every extra colspan of the same cell gets a counting postfix \_\_C
90
+ - `options?: HeaderRowOptions` - Configure header parsing behavior
52
91
 
53
- #### Example:
92
+ **Returns:** `Promise<HeaderRow[]>` - Array of header rows
54
93
 
55
- HTML Table:
94
+ **Example:**
95
+
96
+ HTML with complex headers
56
97
 
57
98
  <table>
58
99
  <thead>
59
100
  <tr>
60
- <th rowspan="2">Awesome</th>
61
- <th>First Name</th>
62
- <th>Last Name</th>
101
+ <th rowspan="2">Name</th>
102
+ <th colspan="2">Details</th>
63
103
  </tr>
64
104
  <tr>
65
- <th colspan="2" style="text-align: center;">Full Name</th>
105
+ <th>Age</th>
106
+ <th>City</th>
66
107
  </tr>
67
108
  </thead>
68
109
  </table>
69
110
 
70
- Code:
71
-
72
111
  ```ts
73
- const headers = await table.getHeaderRows();
112
+ // HTML with complex headers
113
+ <table>
114
+ <thead>
115
+ <tr>
116
+ <th rowspan="2">Name</th>
117
+ <th colspan="2">Details</th>
118
+ </tr>
119
+ <tr>
120
+ <th>Age</th>
121
+ <th>City</th>
122
+ </tr>
123
+ </thead>
124
+ </table>
125
+
126
+ const headers = await table.getHeaderRows({
127
+ colspan: { enabled: true, suffix: true }
128
+ });
74
129
  console.log(headers);
130
+ // Output: [
131
+ // ["Name", "Details", "Details__C1"],
132
+ // ["Name", "Age", "City"]
133
+ // ]
75
134
  ```
76
135
 
77
- Output:
136
+ **Options:**
78
137
 
79
- ```json
80
- [
81
- ["Awesome", "First Name", "Last Name"],
82
- ["Awesome", "Full Name", "Full Name__C1"]
83
- ]
138
+ ```ts
139
+ {
140
+ cellContentType?: CellContentType.InnerText | CellContentType.TextContent, // Default: InnerText
141
+ emptyCellReplacement?: boolean, // Replace empty cells with "{{Empty}}" (default: false)
142
+ duplicateSuffix?: boolean, // Add suffix to duplicate headers (default: false)
143
+ colspan?: {
144
+ enabled?: boolean, // Enable colspan parsing (default: false)
145
+ suffix?: boolean // Add suffix to colspan cells (default: false)
146
+ }
147
+ }
84
148
  ```
85
149
 
86
150
  ---
87
151
 
88
- ### 2. `getMainHeaderRow`
152
+ ### 2. `getMainHeaderRow(options?)`
89
153
 
90
- Retrieves the main header row (default is the last row).
154
+ Retrieves the main header row. By default, uses the **last header row**. Customizable via constructor.
91
155
 
92
- #### Example:
156
+ **Parameters:**
93
157
 
94
- HTML Table:
158
+ - `options?: HeaderRowOptions` - Configure header parsing
95
159
 
96
- <table>
97
- <thead>
98
- <tr>
99
- <th colspan="2">Full Name</th>
100
- </tr>
101
- <tr>
102
- <th>First Name</th>
103
- <th>Last Name</th>
104
- </tr>
105
- </thead>
106
- </table>
160
+ **Returns:** `Promise<HeaderRow>` - The main header row
107
161
 
108
- Code:
162
+ **Throws:**
163
+
164
+ - Error if no header rows are available
165
+ - Error if `setMainHeaderRow` index is out of bounds
166
+
167
+ **Example:**
109
168
 
110
169
  ```ts
170
+ // Use first row as main header
171
+ const table = new PlaywrightTable(page.locator("table"), {
172
+ header: { setMainHeaderRow: 0 },
173
+ });
174
+
111
175
  const mainHeader = await table.getMainHeaderRow();
112
176
  console.log(mainHeader);
113
- // Output: ["First Name", "Last Name"]
177
+ // Output: ["Name", "Age", "City"]
114
178
  ```
115
179
 
116
180
  ---
117
181
 
118
- ### 3. `getBodyRows`
182
+ ### 3. `getBodyRows(options?)`
119
183
 
120
- Retrieves all body rows of the table.
184
+ Retrieves all body rows with support for rowspan and colspan.
121
185
 
122
- #### Example:
186
+ **Parameters:**
123
187
 
124
- HTML Table:
188
+ - `options?: { cellContentType?: CellContentType }` - Content extraction type
125
189
 
126
- <table>
127
- <tbody>
128
- <tr>
129
- <td>Ronald</td>
130
- <td>Veth</td>
131
- </tr>
132
- <tr>
133
- <td>Logan</td>
134
- <td>Deacon</td>
135
- </tr>
136
- </tbody>
137
- </table>
190
+ **Returns:** `Promise<BodyRow[]>` - Array of body rows
191
+
192
+ **Throws:** Error if no body rows found
138
193
 
139
- Code:
194
+ **Example:**
140
195
 
141
196
  ```ts
142
- const rows = await table.getBodyRows();
197
+ const rows = await table.getBodyRows({
198
+ cellContentType: CellContentType.TextContent, // Include hidden elements
199
+ });
143
200
  console.log(rows);
144
- // Output: [["Ronald", "Veth"], ["Logan", "Deacon"]]
201
+ // Output: [["John", "30", "NYC"], ["Jane", "25", "LA"]]
145
202
  ```
146
203
 
204
+ **Cell Content Types:**
205
+
206
+ - `CellContentType.InnerText` - Rendered text (default, excludes hidden elements)
207
+ - `CellContentType.TextContent` - Raw text content (includes hidden elements)
208
+
147
209
  ---
148
210
 
149
- ### 4. `getBodyCellLocator`
211
+ ### 4. `getBodyCellLocator(rowNumber, headerPosition)`
150
212
 
151
- Retrieves a specific cell locator in the body of the table.
213
+ Gets a Playwright Locator for a specific cell by row and column indices (0-based).
152
214
 
153
- #### Example:
215
+ **Parameters:**
154
216
 
155
- HTML Table:
217
+ - `rowNumber: number` - Row index (0-based)
218
+ - `headerPosition: number` - Column index (0-based)
156
219
 
157
- <table>
158
- <tbody>
159
- <tr>
160
- <td>Ronald</td>
161
- <td>Veth</td>
162
- </tr>
163
- <tr>
164
- <td>Principal</td>
165
- <td>Cerios</td>
166
- </tr>
167
- </tbody>
168
- </table>
220
+ **Returns:** `Locator` - Playwright locator for the cell
169
221
 
170
- Code:
222
+ **Example:**
171
223
 
172
224
  ```ts
173
- const cellLocator = table.getBodyCellLocator(1, 1);
174
- await expect(cellLocator).toHaveText("Cerios");
225
+ // Get cell at row 1, column 2
226
+ const cellLocator = table.getBodyCellLocator(1, 2);
227
+ await expect(cellLocator).toHaveText("Expected Value");
228
+ await cellLocator.click();
175
229
  ```
176
230
 
177
231
  ---
178
232
 
179
- ### 5. `getBodyCellLocatorByRowConditions`
233
+ ### 5. `getBodyCellLocatorByRowConditions(conditions, targetHeader, options?)`
180
234
 
181
- Retrieves a cell locator in the body of the table based on row conditions and a target header.
235
+ Finds a cell by matching row conditions and returns the locator for a specific column. Supports both object syntax (multiple conditions) and tuple syntax (single condition).
182
236
 
183
- #### Example:
237
+ **Parameters:**
184
238
 
185
- HTML Table:
239
+ - `conditions: Record<string, string> | [string, string]` - Header-value pairs to match (object) or single [header, value] tuple
240
+ - `targetHeader: string` - Column name to retrieve
241
+ - `options?: TableOptions` - Table loading options
186
242
 
187
- <table>
188
- <thead>
189
- <tr>
190
- <th>First Name</th>
191
- <th>Last Name</th>
192
- </tr>
193
- </thead>
194
- <tbody>
195
- <tr>
196
- <td>Ronald</td>
197
- <td>Veth</td>
198
- </tr>
199
- <tr>
200
- <td>Principal</td>
201
- <td>Cerios</td>
202
- </tr>
203
- </tbody>
204
- </table>
243
+ **Returns:** `Promise<Locator>` - Cell locator
205
244
 
206
- Code:
245
+ **Throws:**
246
+
247
+ - Error if no matching row found
248
+ - Error if specified headers don't exist
249
+
250
+ **Example:**
207
251
 
208
252
  ```ts
209
- const cellLocator = await table.getBodyCellLocatorByRowConditions({ "First Name": "Principal" }, "Last Name");
210
- await expect(cellLocator).toHaveText("Cerios");
253
+ // Find cell with multiple conditions (object syntax)
254
+ const emailCell = await table.getBodyCellLocatorByRowConditions({ "First name": "John", Status: "Active" }, "Email");
255
+ await expect(emailCell).toContainText("@example.com");
256
+
257
+ // Find cell with single condition (tuple syntax)
258
+ const actionCell = await table.getBodyCellLocatorByRowConditions(["Username", "john.doe"], "Actions");
259
+ await actionCell.locator("button.delete").click();
260
+
261
+ // Find cell with single condition (object syntax also works)
262
+ const statusCell = await table.getBodyCellLocatorByRowConditions({ Username: "john.doe" }, "Status");
211
263
  ```
212
264
 
213
265
  ---
214
266
 
215
- ### 6. `getAllBodyCellLocatorsByHeaderName`
267
+ ### 6. `getAllBodyCellLocatorsByHeaderName(headerName, options?)`
216
268
 
217
- Retrieves all cell locators in the body of the table for a specific header name.
269
+ Gets all cell locators in a column by header name.
218
270
 
219
- #### Example:
271
+ **Parameters:**
220
272
 
221
- HTML Table:
273
+ - `headerName: string` - Column header name
274
+ - `options?: TableOptions` - Table loading options
222
275
 
223
- <table>
224
- <thead>
225
- <tr>
226
- <th>First Name</th>
227
- <th>Last Name</th>
228
- </tr>
229
- </thead>
230
- <tbody>
231
- <tr>
232
- <td>Ronald</td>
233
- <td>Veth</td>
234
- </tr>
235
- <tr>
236
- <td>Principal</td>
237
- <td>Cerios</td>
238
- </tr>
239
- </tbody>
240
- </table>
276
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
277
+
278
+ **Throws:** Error if header not found
241
279
 
242
- Code:
280
+ **Example:**
243
281
 
244
282
  ```ts
245
- const locators = await table.getAllBodyCellLocatorsByHeaderName("Last Name");
246
- for (const locator of locators) {
247
- console.log(await locator.textContent());
248
- // Output: ["Veth", "Cerios"]
283
+ // Get all cells in "Status" column
284
+ const statusCells = await table.getAllBodyCellLocatorsByHeaderName("Status");
285
+ for (const cell of statusCells) {
286
+ await expect(cell).toHaveText(/Active|Inactive/);
249
287
  }
288
+
289
+ // Extract all values from "Name" column
290
+ const names = await Promise.all(statusCells.map(cell => cell.textContent()));
291
+ console.log(names); // ["John", "Jane", "Bob"]
250
292
  ```
251
293
 
252
294
  ---
253
295
 
254
- ### 7. `getAllBodyCellLocatorsByHeaderIndex`
296
+ ### 7. `getAllBodyCellLocatorsByHeaderIndex(headerIndex, options?)`
255
297
 
256
- Retrieves all cell locators in the body of the table for a specific header index.
298
+ Gets all cell locators in a column by column index (0-based).
257
299
 
258
- #### Example:
300
+ **Parameters:**
259
301
 
260
- HTML Table:
302
+ - `headerIndex: number` - Column index (0-based)
303
+ - `options?: TableOptions` - Table loading options
261
304
 
262
- <table>
263
- <thead>
264
- <tr>
265
- <th>First Name</th>
266
- <th>Last Name</th>
267
- </tr>
268
- </thead>
269
- <tbody>
270
- <tr>
271
- <td>Ronald</td>
272
- <td>Veth</td>
273
- </tr>
274
- <tr>
275
- <td>Principal</td>
276
- <td>Cerios</td>
277
- </tr>
278
- </tbody>
279
- </table>
305
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
306
+
307
+ **Throws:** Error if index out of bounds
280
308
 
281
- Code:
309
+ **Example:**
282
310
 
283
311
  ```ts
284
- const locators = await table.getAllBodyCellLocatorsByHeaderIndex(1);
285
- for (const locator of locators) {
286
- console.log(await locator.textContent());
287
- // Output: ["Veth", "Cerios"]
312
+ // Get all cells in first column (index 0)
313
+ const firstColumnCells = await table.getAllBodyCellLocatorsByHeaderIndex(0);
314
+ const values = await Promise.all(firstColumnCells.map(c => c.textContent()));
315
+ console.log(values);
316
+
317
+ // Verify all cells are visible
318
+ for (const cell of firstColumnCells) {
319
+ await expect(cell).toBeVisible();
288
320
  }
289
321
  ```
290
322
 
291
323
  ---
292
324
 
293
- ### 8. `getJson`
325
+ ### 8. `getJson(options?)`
326
+
327
+ Converts the entire table to a JSON array of objects. **Headers become object keys**, values are cell contents.
328
+
329
+ **Parameters:**
294
330
 
295
- Converts the table data into a JSON object.
331
+ - `options?: TableOptions` - Configure parsing behavior
296
332
 
297
- #### Example:
333
+ **Returns:** `Promise<any[]>` - Array of row objects
298
334
 
299
- HTML Table **SIMPLE**:
335
+ **Throws:** Error if no headers or body rows found
336
+
337
+ **Simple Example:**
300
338
 
301
339
  <table>
302
- <thead>
303
- <tr>
304
- <th>id</th>
305
- <th>name</th>
306
- <th>yearOfBirth</th>
307
- </tr>
308
- </thead>
340
+ <thead><tr><th>Name</th><th>Age</th></tr></thead>
309
341
  <tbody>
310
- <tr>
311
- <td>1</td>
312
- <td>Ronald</td>
313
- <td>1987</td>
314
- </tr>
315
- <tr>
316
- <td>2</td>
317
- <td>Cerios</td>
318
- <td>2022</td>
319
- </tr>
342
+ <tr><td>John</td><td>30</td></tr>
343
+ <tr><td>Jane</td><td>25</td></tr>
320
344
  </tbody>
321
345
  </table>
322
346
 
323
- Code:
324
-
325
347
  ```ts
348
+ // HTML:
349
+ <table>
350
+ <thead><tr><th>Name</th><th>Age</th></tr></thead>
351
+ <tbody>
352
+ <tr><td>John</td><td>30</td></tr>
353
+ <tr><td>Jane</td><td>25</td></tr>
354
+ </tbody>
355
+ </table>
356
+
326
357
  const json = await table.getJson();
327
358
  console.log(json);
359
+ // Output:
360
+ // [
361
+ // { "Name": "John", "Age": "30" },
362
+ // { "Name": "Jane", "Age": "25" }
363
+ // ]
328
364
  ```
329
365
 
330
- Output:
366
+ **Complex Example with Options:**
331
367
 
332
- ```json
333
- [
334
- {
335
- "id": 1,
336
- "name": "Ronald",
337
- "yearOfBirth": 1987
368
+ ```ts
369
+ // HTML with colspan, rowspan, empty cells, and duplicates
370
+ const json = await table.getJson({
371
+ headerRowOptions: {
372
+ colspan: { enabled: true, suffix: true },
373
+ duplicateSuffix: true,
374
+ emptyCellReplacement: true,
338
375
  },
339
- {
340
- "id": 2,
341
- "name": "Cerios",
342
- "yearOfBirth": 2022
343
- }
344
- ]
376
+ });
377
+ // Output:
378
+ // [
379
+ // {
380
+ // "Awesome Rowspan": "Value1",
381
+ // "Name": "John",
382
+ // "Name__C1": "Doe",
383
+ // "{{Empty}}": "EmptyValue",
384
+ // "Duplicate": "First",
385
+ // "Duplicate__D1": "Second"
386
+ // }
387
+ // ]
345
388
  ```
346
389
 
347
- #### Example:
390
+ ---
348
391
 
349
- HTML Table **COMPLEX**:
392
+ ## Waiting Methods
350
393
 
351
- <table>
352
- <thead>
353
- <tr>
354
- <th rowspan="2">Awesome Rowspan</th>
355
- <th>Very Duplicate</th>
356
- <th colspan="3">Mega colspan</th>
357
- <th></th>
358
- <th></th>
359
- <th>Very Duplicate</th>
360
- </tr>
361
- </thead>
362
- <tbody>
363
- <tr>
364
- <td></td>
365
- <td>Ronald</td>
366
- <td>Veth</td>
367
- <td>Test</td>
368
- <td>Automator</td>
369
- <td>At</td>
370
- <td>Cerios</td>
371
- <td>!</td>
372
- </tr>
373
- </tbody>
374
- </table>
394
+ ### 9. `waitForEmpty(options?)`
375
395
 
376
- Code:
396
+ Waits for the table body to be completely empty (no body rows).
397
+
398
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBe(0);`
399
+
400
+ **Parameters:**
401
+
402
+ - `options?: PollingOptions`
403
+
404
+ **Returns:** `Promise<void>`
405
+
406
+ **Throws:** Error if table is not empty within timeout
407
+
408
+ **Example:**
377
409
 
378
410
  ```ts
379
- const json = await table.getJson();
380
- console.log(json);
411
+ // Click delete all button
412
+ await page.locator("button.delete-all").click();
413
+
414
+ // Wait for table to clear
415
+ await table.waitForEmpty({ timeout: 5000 });
416
+
417
+ // Verify empty state
418
+ const rows = await table.getBodyRows();
419
+ expect(rows).toHaveLength(0);
420
+ ```
421
+
422
+ ---
423
+
424
+ ### 10. `waitForNonEmpty(options?)`
425
+
426
+ Waits for the table body to have at least one body row with actual content. A row is considered valid if it has at least one cell containing text.
427
+
428
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBeGreaterThan(0);`
429
+
430
+ **Parameters:**
431
+
432
+ - `options?: PollingOptions`
433
+
434
+ **Returns:** `Promise<void>`
435
+
436
+ **Throws:**
437
+
438
+ - Error if table remains empty (no rows) within timeout
439
+ - Error if table has rows but all cells are empty within timeout
440
+
441
+ **Example:**
442
+
443
+ ```ts
444
+ // Trigger data load
445
+ await page.locator("button.load-data").click();
446
+
447
+ // Wait for table to populate with actual data
448
+ await table.waitForNonEmpty({ timeout: 5000 });
449
+
450
+ // Now safe to process data - guaranteed to have content
451
+ const data = await table.getJson();
452
+ expect(data.length).toBeGreaterThan(0);
453
+
454
+ // Useful for dynamically loaded tables where structure appears before data
455
+ await searchInput.fill("search term");
456
+ await table.waitForNonEmpty(); // Waits for rows AND content, not just empty row elements
381
457
  ```
382
458
 
383
- Output:
384
-
385
- ```json
386
- [
387
- {
388
- "Awesome Rowspan": "",
389
- "Very Duplicate": "Ronald",
390
- "Mega colspan": "Veth",
391
- "Mega colspan__C1": "Test",
392
- "Mega colspan__C2": "Automator",
393
- "{{Empty}}": "At",
394
- "{{Empty}}__D1": "Cerios",
395
- "Very Duplicate__D1": "!"
396
- }
397
- ]
459
+ ---
460
+
461
+ ### 11. `waitForExactRowCount(count, options?)`
462
+
463
+ Waits for the table to have exactly the specified number of body rows.
464
+
465
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBe(10);`
466
+
467
+ **Parameters:**
468
+
469
+ - `count: number` - The exact number of rows expected
470
+ - `options?: PollingOptions` - Polling options (timeout, interval)
471
+
472
+ **Returns:** `Promise<void>`
473
+
474
+ **Throws:**
475
+
476
+ - Error if row count doesn't match within timeout
477
+ - Error if count is negative or not an integer
478
+
479
+ **Example:**
480
+
481
+ ```ts
482
+ // Wait for table to have exactly 10 rows
483
+ await table.waitForExactRowCount(10, { timeout: 5000 });
484
+
485
+ // Verify table has exactly 0 rows (alternative to waitForEmpty)
486
+ await table.waitForExactRowCount(0);
487
+
488
+ // Wait after pagination change
489
+ await page.locator("button.page-2").click();
490
+ await table.waitForExactRowCount(25);
491
+
492
+ // Verify filtered results
493
+ await page.locator("input.search").fill("admin");
494
+ await table.waitForExactRowCount(3); // Expect exactly 3 admin users
398
495
  ```
399
496
 
497
+ **Use Cases:**
498
+
499
+ - Verifying exact result counts after filtering
500
+ - Validating pagination behavior
501
+ - Testing data deletion/addition
502
+ - Ensuring precise table state for assertions
503
+
400
504
  ---
401
505
 
402
- ### 9. `waitForHeaderRows`
506
+ ### 12. `getRowCount()`
507
+
508
+ Gets the count of header and body rows in the table. Lightweight method that doesn't fetch cell data, only counts rows.
509
+
510
+ **Parameters:** None
403
511
 
404
- Waits for the header rows to be loaded.
512
+ **Returns:** `Promise<{ header: number; body: number }>`
405
513
 
406
- #### Example:
514
+ **Example:**
407
515
 
408
516
  ```ts
409
- await table.waitForHeaderRows({ timeout: 5000 });
517
+ // Get current row counts
518
+ const counts = await table.getRowCount();
519
+ console.log(`Headers: ${counts.header}, Body: ${counts.body}`);
520
+
521
+ // Assert specific row count
522
+ const { body } = await table.getRowCount();
523
+ expect(body).toBe(10);
524
+
525
+ // Check if table is empty efficiently
526
+ const rowCounts = await table.getRowCount();
527
+ if (rowCounts.body === 0) {
528
+ console.log("Table is empty");
529
+ }
410
530
  ```
411
531
 
532
+ **Use Cases:**
533
+
534
+ - Quick validation of table size without fetching data
535
+ - Performance-sensitive checks in loops
536
+ - Conditional logic based on row counts
537
+ - Monitoring table size during pagination
538
+
412
539
  ---
413
540
 
414
- ### 10. `waitForBodyRows`
541
+ ### 13. `getDistinctColumnValues(headerName, options?)`
542
+
543
+ Gets all distinct (unique) values from a specific column. Returns a sorted array of unique string values, excluding empty values.
544
+
545
+ **Parameters:**
546
+
547
+ - `headerName: string` - The header name of the column to extract distinct values from
548
+ - `options?: TableOptions` - Optional table loading options
549
+
550
+ **Returns:** `Promise<string[]>` - Sorted array of distinct values
415
551
 
416
- Waits for the body rows to be loaded.
552
+ **Throws:** Error if the specified header is not found in the table
417
553
 
418
- #### Example:
554
+ **Example:**
419
555
 
420
556
  ```ts
421
- await table.waitForBodyRows({ timeout: 5000 });
557
+ // Get all unique status values
558
+ const statuses = await table.getDistinctColumnValues("Status");
559
+ // Result: ["Active", "Inactive", "Pending"]
560
+
561
+ // Get distinct countries for filtering
562
+ const countries = await table.getDistinctColumnValues("Country");
563
+ console.log(`Available countries: ${countries.join(", ")}`);
564
+
565
+ // Verify expected values exist
566
+ const roles = await table.getDistinctColumnValues("Role");
567
+ expect(roles).toContain("Admin");
568
+ expect(roles).toHaveLength(3);
569
+
570
+ // Build dynamic filters from table data
571
+ const categories = await table.getDistinctColumnValues("Category");
572
+ for (const category of categories) {
573
+ await page.locator(`button[data-filter="${category}"]`).click();
574
+ // Verify filtering works
575
+ }
422
576
  ```
423
577
 
578
+ **Use Cases:**
579
+
580
+ - Extracting filter options from table data
581
+ - Validating data variety and uniqueness
582
+ - Building dynamic test data sets
583
+ - Verifying dropdown/select options match table content
584
+
585
+ ---
586
+
587
+ ### 14. `findRowIndex(conditions, options?)`
588
+
589
+ Finds the index of the first body row matching the specified conditions. Returns -1 if no matching row is found.
590
+
591
+ **Parameters:**
592
+
593
+ - `conditions: Record<string, string>` - Record of header names and expected cell values to match
594
+ - `options?: TableOptions` - Optional table loading options
595
+
596
+ **Returns:** `Promise<number>` - The 0-based index of the first matching row, or -1 if not found
597
+
598
+ **Throws:** Error if specified headers don't exist in the table
599
+
600
+ **Example:**
601
+
602
+ ```ts
603
+ // Find index of row where Status is "Active"
604
+ const index = await table.findRowIndex({ Status: "Active" });
605
+ if (index >= 0) {
606
+ console.log(`Found at row ${index}`);
607
+ const cell = table.getBodyCellLocator(index, 0);
608
+ await cell.click();
609
+ }
610
+
611
+ // Find row and interact with it
612
+ const rowIndex = await table.findRowIndex({ Username: "john.doe" });
613
+ if (rowIndex >= 0) {
614
+ const deleteButton = table.getBodyCellLocator(rowIndex, 3).locator("button.delete");
615
+ await deleteButton.click();
616
+ } else {
617
+ console.log("User not found");
618
+ }
619
+
620
+ // Check if specific data exists before taking action
621
+ const orderIndex = await table.findRowIndex({ "Order ID": "12345" });
622
+ if (orderIndex === -1) {
623
+ throw new Error("Order not found in table");
624
+ }
625
+ ```
626
+
627
+ **Use Cases:**
628
+
629
+ - Finding row position for interaction without fetching all data
630
+ - Conditional logic based on row existence
631
+ - Getting row indices for further locator operations
632
+ - Checking data presence without waiting
633
+
424
634
  ---
425
635
 
426
636
  ## Advanced Options
427
637
 
428
- You can customize the behavior of the table parsing by passing options to the `PlaywrightTable` constructor. For example:
638
+ ### Custom Selectors
639
+
640
+ Customize selectors for non-standard table structures or div-based tables:
429
641
 
430
642
  ```ts
431
- const table = new PlaywrightTable(page.locator("table"), {
643
+ const table = new PlaywrightTable(page.locator(".custom-table"), {
644
+ header: {
645
+ setMainHeaderRow: 0, // Use first header row as main (default: last row)
646
+ rowSelector: ".header-row", // Custom header row selector
647
+ columnSelector: ".header-cell", // Custom header cell selector
648
+ },
649
+ row: {
650
+ rowSelector: ".data-row", // Custom body row selector
651
+ columnSelector: ".data-cell", // Custom body cell selector
652
+ },
653
+ });
654
+ ```
655
+
656
+ ### Div-based Tables
657
+
658
+ ```ts
659
+ // Works with div-based table structures
660
+ const divTable = new PlaywrightTable(page.locator(".divTable"), {
432
661
  header: {
433
- rowSelector: ".custom-header-row",
434
- columnSelector: ".custom-header-cell",
662
+ rowSelector: ".divTableHeading > .divTableRow",
663
+ columnSelector: ".divTableHead",
435
664
  },
436
665
  row: {
437
- rowSelector: ".custom-body-row",
438
- columnSelector: ".custom-body-cell",
666
+ rowSelector: ".divTableBody > .divTableRow",
667
+ columnSelector: ".divTableCell",
668
+ },
669
+ });
670
+ ```
671
+
672
+ ### Header Row Options
673
+
674
+ Control how headers are processed:
675
+
676
+ ```ts
677
+ const headers = await table.getHeaderRows({
678
+ cellContentType: CellContentType.TextContent, // Include hidden text
679
+ emptyCellReplacement: true, // Replace "" with "{{Empty}}"
680
+ duplicateSuffix: true, // "Name", "Name__D1", "Name__D2"
681
+ colspan: {
682
+ enabled: true, // Process colspan attributes
683
+ suffix: true, // "Col", "Col__C1", "Col__C2"
439
684
  },
440
685
  });
441
686
  ```
442
687
 
443
688
  ---
444
689
 
690
+ ## Error Handling
691
+
692
+ The library provides **detailed error messages** with context to help you debug issues:
693
+
694
+ ```ts
695
+ try {
696
+ await table.getBodyCellLocatorByRowConditions({ "Invalid Header": "value" }, "Target");
697
+ } catch (error) {
698
+ console.error(error.message);
699
+ // Error output:
700
+ // Header "Invalid Header" not found.
701
+ // Available headers: [First name, Last name, Date of birth]
702
+ // Header row locator: Locator@table >> thead >> tr
703
+ }
704
+ ```
705
+
706
+ **Error Context Includes:**
707
+
708
+ - Locator descriptions with full selector chains
709
+ - Available headers when header not found
710
+ - Row/column indices when out of bounds
711
+ - Cell content types in use
712
+ - Validation failures with expected vs actual values
713
+
714
+ ---
715
+
716
+ ## Best Practices
717
+
718
+ ### 1. Wait for Dynamic Content
719
+
720
+ ```ts
721
+ // ❌ Bad - might fail if table still loading
722
+ const data = await table.getJson();
723
+
724
+ // ✅ Good - wait for content first
725
+ await table.waitForNonEmpty();
726
+ const data = await table.getJson();
727
+
728
+ // ✅ Good - wait for specific row count
729
+ await table.waitForExactRowCount(5);
730
+ const data = await table.getJson();
731
+
732
+ // ✅ Good - use expect.poll for flexible waiting
733
+ await expect.poll(async () => (await table.getBodyRows()).length).toBe(5);
734
+ const data = await table.getJson();
735
+ ```
736
+
737
+ ### 2. Use `expect.poll` for Flexible Waiting
738
+
739
+ While this library provides dedicated wait methods, Playwright's `expect.poll` offers more flexibility when combined with retrieval methods:
740
+
741
+ ```ts
742
+ import { expect } from "@playwright/test";
743
+
744
+ // Wait for exact row count
745
+ await expect.poll(async () => (await table.getBodyRows()).length).toBe(10);
746
+
747
+ // Wait for table to be empty
748
+ await expect.poll(async () => (await table.getJson()).length).toBe(0);
749
+
750
+ // Wait for at least N rows
751
+ await expect.poll(async () => (await table.getBodyRows()).length).toBeGreaterThanOrEqual(5);
752
+
753
+ // Wait for specific data to appear
754
+ await expect.poll(() => table.getJson()).toContainEqual({ Name: "John", Status: "Active" });
755
+
756
+ // Wait for column to have specific value
757
+ await expect.poll(() => table.getDistinctColumnValues("Status")).toContain("Completed");
758
+
759
+ // Custom timeout and intervals
760
+ await expect
761
+ .poll(async () => (await table.getBodyRows()).length, {
762
+ message: "Waiting for table to have 10 rows",
763
+ timeout: 10000,
764
+ intervals: [100, 250, 500, 1000],
765
+ })
766
+ .toBe(10);
767
+ ```
768
+
769
+ **Benefits of `expect.poll`:**
770
+
771
+ - ✅ Native Playwright API with familiar assertion syntax
772
+ - ✅ Rich assertion library (`toBe`, `toContain`, `toEqual`, `toMatch`, etc.)
773
+ - ✅ Better error messages with actual vs expected values
774
+ - ✅ Works with any retrieval method (`getBodyRows`, `getJson`, `getDistinctColumnValues`, etc.)
775
+
776
+ ### 3. Use Appropriate Content Type
777
+
778
+ ```ts
779
+ // For visible text only (respects CSS visibility)
780
+ const rows = await table.getBodyRows({
781
+ cellContentType: CellContentType.InnerText,
782
+ });
783
+
784
+ // For all text including hidden elements
785
+ const rows = await table.getBodyRows({
786
+ cellContentType: CellContentType.TextContent,
787
+ });
788
+ ```
789
+
790
+ ### 4. Handle Complex Tables
791
+
792
+ ```ts
793
+ // Enable all options for complex tables with colspan/rowspan
794
+ const json = await table.getJson({
795
+ headerRowOptions: {
796
+ colspan: { enabled: true, suffix: true },
797
+ duplicateSuffix: true,
798
+ emptyCellReplacement: true,
799
+ },
800
+ });
801
+ ```
802
+
803
+ ### 5. Leverage Cell Locators for Interactions
804
+
805
+ ```ts
806
+ // Get locator first, then interact
807
+ const cell = await table.getBodyCellLocatorByRowConditions({ Name: "John Doe" }, "Actions");
808
+ await cell.locator("button.edit").click();
809
+ await cell.locator("button.delete").click();
810
+ ```
811
+
812
+ ---
813
+
814
+ ## TypeScript Support
815
+
816
+ Full TypeScript support with exported types:
817
+
818
+ ```ts
819
+ import {
820
+ PlaywrightTable,
821
+ HeaderRow,
822
+ BodyRow,
823
+ Cell,
824
+ CellContentType,
825
+ HeaderRowOptions,
826
+ TableOptions,
827
+ PollingOptions,
828
+ RowKind,
829
+ } from "@cerios/playwright-table";
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Performance
835
+
836
+ The library is optimized for performance:
837
+
838
+ - ✅ **Parallel data fetching** - All cells in a row are fetched simultaneously
839
+ - ✅ **Efficient DOM queries** - Minimizes Playwright API calls
840
+ - ✅ **Smart polling** - Configurable intervals and timeouts
841
+ - ✅ **Lazy evaluation** - Only fetches data when needed
842
+
843
+ ---
844
+
845
+ ## Browser Support
846
+
847
+ Works with all Playwright-supported browsers:
848
+
849
+ - ✅ Chromium
850
+ - ✅ Firefox
851
+ - ✅ WebKit
852
+
853
+ ---
854
+
855
+ ## Contributing
856
+
857
+ Contributions are welcome! Please check out the [GitHub repository](https://github.com/CeriosTesting/playwright-tables).
858
+
859
+ ---
860
+
445
861
  ## License
446
862
 
447
863
  This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
864
+
865
+ ---
866
+
867
+ ## Support
868
+
869
+ - 📖 [Documentation](https://github.com/CeriosTesting/playwright-tables)
870
+ - 🐛 [Report Issues](https://github.com/CeriosTesting/playwright-tables/issues)
871
+ - 💬 [Discussions](https://github.com/CeriosTesting/playwright-tables/discussions)
872
+
873
+ ---
874
+
875
+ **Made with ❤️ by Cerios**