@cerios/playwright-table 1.1.3 → 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,447 +1,875 @@
1
- # 🎭 Playwright Tables | By Cerios
2
-
3
- ## Introduction
4
-
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.
6
-
7
- ---
8
-
9
- ## Installation
10
-
11
- First, install and familiarize yourself with the Playwright testing library:
12
- [Playwright Documentation](https://github.com/microsoft/playwright/blob/main/README.md)
13
-
14
- Then, install this package:
15
-
16
- ```bash
17
- npm i @cerios/playwright-table
18
- ```
19
-
20
- Or as Dev dependency
21
-
22
- ```bash
23
- npm i -D @cerios/playwright-table
24
- ```
25
-
26
- ---
27
-
28
- ## Usage
29
-
30
- ### Basic Setup
31
-
32
- To use the package, create a new `PlaywrightTable` instance by passing a locator for the table element:
33
-
34
- ```ts
35
- const table = new PlaywrightTable(page.locator("table"));
36
- ```
37
-
38
- By default, the following selectors are used:
39
-
40
- - Header rows: `thead > tr`
41
- - Header cells: `th`
42
- - Body rows: `tbody > tr`
43
- - Body cells: `td`
44
-
45
- ---
46
-
47
- ## Methods and Examples
48
-
49
- ### 1. `getHeaderRows`
50
-
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
52
-
53
- #### Example:
54
-
55
- HTML Table:
56
-
57
- <table>
58
- <thead>
59
- <tr>
60
- <th rowspan="2">Awesome</th>
61
- <th>First Name</th>
62
- <th>Last Name</th>
63
- </tr>
64
- <tr>
65
- <th colspan="2" style="text-align: center;">Full Name</th>
66
- </tr>
67
- </thead>
68
- </table>
69
-
70
- Code:
71
-
72
- ```ts
73
- const headers = await table.getHeaderRows();
74
- console.log(headers);
75
- ```
76
-
77
- Output:
78
-
79
- ```json
80
- [
81
- ["Awesome", "First Name", "Last Name"],
82
- ["Awesome", "Full Name", "Full Name__C1"]
83
- ]
84
- ```
85
-
86
- ---
87
-
88
- ### 2. `getMainHeaderRow`
89
-
90
- Retrieves the main header row (default is the last row).
91
-
92
- #### Example:
93
-
94
- HTML Table:
95
-
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>
107
-
108
- Code:
109
-
110
- ```ts
111
- const mainHeader = await table.getMainHeaderRow();
112
- console.log(mainHeader);
113
- // Output: ["First Name", "Last Name"]
114
- ```
115
-
116
- ---
117
-
118
- ### 3. `getBodyRows`
119
-
120
- Retrieves all body rows of the table.
121
-
122
- #### Example:
123
-
124
- HTML Table:
125
-
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>
138
-
139
- Code:
140
-
141
- ```ts
142
- const rows = await table.getBodyRows();
143
- console.log(rows);
144
- // Output: [["Ronald", "Veth"], ["Logan", "Deacon"]]
145
- ```
146
-
147
- ---
148
-
149
- ### 4. `getBodyCellLocator`
150
-
151
- Retrieves a specific cell locator in the body of the table.
152
-
153
- #### Example:
154
-
155
- HTML Table:
156
-
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>
169
-
170
- Code:
171
-
172
- ```ts
173
- const cellLocator = table.getBodyCellLocator(1, 1);
174
- await expect(cellLocator).toHaveText("Cerios");
175
- ```
176
-
177
- ---
178
-
179
- ### 5. `getBodyCellLocatorByRowConditions`
180
-
181
- Retrieves a cell locator in the body of the table based on row conditions and a target header.
182
-
183
- #### Example:
184
-
185
- HTML Table:
186
-
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>
205
-
206
- Code:
207
-
208
- ```ts
209
- const cellLocator = await table.getBodyCellLocatorByRowConditions({ "First Name": "Principal" }, "Last Name");
210
- await expect(cellLocator).toHaveText("Cerios");
211
- ```
212
-
213
- ---
214
-
215
- ### 6. `getAllBodyCellLocatorsByHeaderName`
216
-
217
- Retrieves all cell locators in the body of the table for a specific header name.
218
-
219
- #### Example:
220
-
221
- HTML Table:
222
-
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>
241
-
242
- Code:
243
-
244
- ```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"]
249
- }
250
- ```
251
-
252
- ---
253
-
254
- ### 7. `getAllBodyCellLocatorsByHeaderIndex`
255
-
256
- Retrieves all cell locators in the body of the table for a specific header index.
257
-
258
- #### Example:
259
-
260
- HTML Table:
261
-
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>
280
-
281
- Code:
282
-
283
- ```ts
284
- const locators = await table.getAllBodyCellLocatorsByHeaderIndex(1);
285
- for (const locator of locators) {
286
- console.log(await locator.textContent());
287
- // Output: ["Veth", "Cerios"]
288
- }
289
- ```
290
-
291
- ---
292
-
293
- ### 8. `getJson`
294
-
295
- Converts the table data into a JSON object.
296
-
297
- #### Example:
298
-
299
- HTML Table **SIMPLE**:
300
-
301
- <table>
302
- <thead>
303
- <tr>
304
- <th>id</th>
305
- <th>name</th>
306
- <th>yearOfBirth</th>
307
- </tr>
308
- </thead>
309
- <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>
320
- </tbody>
321
- </table>
322
-
323
- Code:
324
-
325
- ```ts
326
- const json = await table.getJson();
327
- console.log(json);
328
- ```
329
-
330
- Output:
331
-
332
- ```json
333
- [
334
- {
335
- "id": 1,
336
- "name": "Ronald",
337
- "yearOfBirth": 1987
338
- },
339
- {
340
- "id": 2,
341
- "name": "Cerios",
342
- "yearOfBirth": 2022
343
- }
344
- ]
345
- ```
346
-
347
- #### Example:
348
-
349
- HTML Table **COMPLEX**:
350
-
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>
375
-
376
- Code:
377
-
378
- ```ts
379
- const json = await table.getJson();
380
- console.log(json);
381
- ```
382
-
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
- ]
398
- ```
399
-
400
- ---
401
-
402
- ### 9. `waitForHeaderRows`
403
-
404
- Waits for the header rows to be loaded.
405
-
406
- #### Example:
407
-
408
- ```ts
409
- await table.waitForHeaderRows({ timeout: 5000 });
410
- ```
411
-
412
- ---
413
-
414
- ### 10. `waitForBodyRows`
415
-
416
- Waits for the body rows to be loaded.
417
-
418
- #### Example:
419
-
420
- ```ts
421
- await table.waitForBodyRows({ timeout: 5000 });
422
- ```
423
-
424
- ---
425
-
426
- ## Advanced Options
427
-
428
- You can customize the behavior of the table parsing by passing options to the `PlaywrightTable` constructor. For example:
429
-
430
- ```ts
431
- const table = new PlaywrightTable(page.locator("table"), {
432
- header: {
433
- rowSelector: ".custom-header-row",
434
- columnSelector: ".custom-header-cell",
435
- },
436
- row: {
437
- rowSelector: ".custom-body-row",
438
- columnSelector: ".custom-body-cell",
439
- },
440
- });
441
- ```
442
-
443
- ---
444
-
445
- ## License
446
-
447
- This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
1
+ # 🎭 Playwright Tables | By Cerios
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
+
8
+ ## Introduction
9
+
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
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ First, ensure you have Playwright installed:
28
+ [Playwright Documentation](https://github.com/microsoft/playwright/blob/main/README.md)
29
+
30
+ Then, install this package:
31
+
32
+ ```bash
33
+ npm i @cerios/playwright-table
34
+ ```
35
+
36
+ Or as a dev dependency:
37
+
38
+ ```bash
39
+ npm i -D @cerios/playwright-table
40
+ ```
41
+
42
+ > ⚠️ **Upgrading from v1.x?** See the [Migration Guide](./MIGRATIONv2.md) for breaking changes and upgrade instructions.
43
+
44
+ ---
45
+
46
+ ## Quick Start
47
+
48
+ ### Basic Usage
49
+
50
+ ```ts
51
+ import { PlaywrightTable } from "@cerios/playwright-table";
52
+
53
+ // Create a table instance
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 });
67
+ ```
68
+
69
+ ### Default Selectors
70
+
71
+ By default, the following CSS selectors are used:
72
+
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)).
79
+
80
+ ---
81
+
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.).
87
+
88
+ **Parameters:**
89
+
90
+ - `options?: HeaderRowOptions` - Configure header parsing behavior
91
+
92
+ **Returns:** `Promise<HeaderRow[]>` - Array of header rows
93
+
94
+ **Example:**
95
+
96
+ HTML with complex headers
97
+
98
+ <table>
99
+ <thead>
100
+ <tr>
101
+ <th rowspan="2">Name</th>
102
+ <th colspan="2">Details</th>
103
+ </tr>
104
+ <tr>
105
+ <th>Age</th>
106
+ <th>City</th>
107
+ </tr>
108
+ </thead>
109
+ </table>
110
+
111
+ ```ts
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
+ });
129
+ console.log(headers);
130
+ // Output: [
131
+ // ["Name", "Details", "Details__C1"],
132
+ // ["Name", "Age", "City"]
133
+ // ]
134
+ ```
135
+
136
+ **Options:**
137
+
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
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ### 2. `getMainHeaderRow(options?)`
153
+
154
+ Retrieves the main header row. By default, uses the **last header row**. Customizable via constructor.
155
+
156
+ **Parameters:**
157
+
158
+ - `options?: HeaderRowOptions` - Configure header parsing
159
+
160
+ **Returns:** `Promise<HeaderRow>` - The main header row
161
+
162
+ **Throws:**
163
+
164
+ - Error if no header rows are available
165
+ - Error if `setMainHeaderRow` index is out of bounds
166
+
167
+ **Example:**
168
+
169
+ ```ts
170
+ // Use first row as main header
171
+ const table = new PlaywrightTable(page.locator("table"), {
172
+ header: { setMainHeaderRow: 0 },
173
+ });
174
+
175
+ const mainHeader = await table.getMainHeaderRow();
176
+ console.log(mainHeader);
177
+ // Output: ["Name", "Age", "City"]
178
+ ```
179
+
180
+ ---
181
+
182
+ ### 3. `getBodyRows(options?)`
183
+
184
+ Retrieves all body rows with support for rowspan and colspan.
185
+
186
+ **Parameters:**
187
+
188
+ - `options?: { cellContentType?: CellContentType }` - Content extraction type
189
+
190
+ **Returns:** `Promise<BodyRow[]>` - Array of body rows
191
+
192
+ **Throws:** Error if no body rows found
193
+
194
+ **Example:**
195
+
196
+ ```ts
197
+ const rows = await table.getBodyRows({
198
+ cellContentType: CellContentType.TextContent, // Include hidden elements
199
+ });
200
+ console.log(rows);
201
+ // Output: [["John", "30", "NYC"], ["Jane", "25", "LA"]]
202
+ ```
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
+
209
+ ---
210
+
211
+ ### 4. `getBodyCellLocator(rowNumber, headerPosition)`
212
+
213
+ Gets a Playwright Locator for a specific cell by row and column indices (0-based).
214
+
215
+ **Parameters:**
216
+
217
+ - `rowNumber: number` - Row index (0-based)
218
+ - `headerPosition: number` - Column index (0-based)
219
+
220
+ **Returns:** `Locator` - Playwright locator for the cell
221
+
222
+ **Example:**
223
+
224
+ ```ts
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();
229
+ ```
230
+
231
+ ---
232
+
233
+ ### 5. `getBodyCellLocatorByRowConditions(conditions, targetHeader, options?)`
234
+
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).
236
+
237
+ **Parameters:**
238
+
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
242
+
243
+ **Returns:** `Promise<Locator>` - Cell locator
244
+
245
+ **Throws:**
246
+
247
+ - Error if no matching row found
248
+ - Error if specified headers don't exist
249
+
250
+ **Example:**
251
+
252
+ ```ts
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");
263
+ ```
264
+
265
+ ---
266
+
267
+ ### 6. `getAllBodyCellLocatorsByHeaderName(headerName, options?)`
268
+
269
+ Gets all cell locators in a column by header name.
270
+
271
+ **Parameters:**
272
+
273
+ - `headerName: string` - Column header name
274
+ - `options?: TableOptions` - Table loading options
275
+
276
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
277
+
278
+ **Throws:** Error if header not found
279
+
280
+ **Example:**
281
+
282
+ ```ts
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/);
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"]
292
+ ```
293
+
294
+ ---
295
+
296
+ ### 7. `getAllBodyCellLocatorsByHeaderIndex(headerIndex, options?)`
297
+
298
+ Gets all cell locators in a column by column index (0-based).
299
+
300
+ **Parameters:**
301
+
302
+ - `headerIndex: number` - Column index (0-based)
303
+ - `options?: TableOptions` - Table loading options
304
+
305
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
306
+
307
+ **Throws:** Error if index out of bounds
308
+
309
+ **Example:**
310
+
311
+ ```ts
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();
320
+ }
321
+ ```
322
+
323
+ ---
324
+
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:**
330
+
331
+ - `options?: TableOptions` - Configure parsing behavior
332
+
333
+ **Returns:** `Promise<any[]>` - Array of row objects
334
+
335
+ **Throws:** Error if no headers or body rows found
336
+
337
+ **Simple Example:**
338
+
339
+ <table>
340
+ <thead><tr><th>Name</th><th>Age</th></tr></thead>
341
+ <tbody>
342
+ <tr><td>John</td><td>30</td></tr>
343
+ <tr><td>Jane</td><td>25</td></tr>
344
+ </tbody>
345
+ </table>
346
+
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
+
357
+ const json = await table.getJson();
358
+ console.log(json);
359
+ // Output:
360
+ // [
361
+ // { "Name": "John", "Age": "30" },
362
+ // { "Name": "Jane", "Age": "25" }
363
+ // ]
364
+ ```
365
+
366
+ **Complex Example with Options:**
367
+
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,
375
+ },
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
+ // ]
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Waiting Methods
393
+
394
+ ### 9. `waitForEmpty(options?)`
395
+
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:**
409
+
410
+ ```ts
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
457
+ ```
458
+
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
495
+ ```
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
+
504
+ ---
505
+
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
511
+
512
+ **Returns:** `Promise<{ header: number; body: number }>`
513
+
514
+ **Example:**
515
+
516
+ ```ts
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
+ }
530
+ ```
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
+
539
+ ---
540
+
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
551
+
552
+ **Throws:** Error if the specified header is not found in the table
553
+
554
+ **Example:**
555
+
556
+ ```ts
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
+ }
576
+ ```
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
+
634
+ ---
635
+
636
+ ## Advanced Options
637
+
638
+ ### Custom Selectors
639
+
640
+ Customize selectors for non-standard table structures or div-based tables:
641
+
642
+ ```ts
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"), {
661
+ header: {
662
+ rowSelector: ".divTableHeading > .divTableRow",
663
+ columnSelector: ".divTableHead",
664
+ },
665
+ row: {
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"
684
+ },
685
+ });
686
+ ```
687
+
688
+ ---
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
+
861
+ ## License
862
+
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**