@cerios/playwright-table 1.1.3 → 2.0.1

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,911 @@
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
+ normalizeWhitespace?: boolean, // Collapse internal whitespace/newlines (default: false)
144
+ stripIconGlyphs?: boolean, // Remove icon-font private-use glyphs from headers (default: false)
145
+ colspan?: {
146
+ enabled?: boolean, // Enable colspan parsing (default: false)
147
+ suffix?: boolean // Add suffix to colspan cells (default: false)
148
+ }
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ### 2. `getMainHeaderRow(options?)`
155
+
156
+ Retrieves the main header row. By default, uses the **last header row**. Customizable via constructor.
157
+
158
+ **Parameters:**
159
+
160
+ - `options?: HeaderRowOptions` - Configure header parsing
161
+
162
+ **Returns:** `Promise<HeaderRow>` - The main header row
163
+
164
+ **Throws:**
165
+
166
+ - Error if no header rows are available
167
+ - Error if `setMainHeaderRow` index is out of bounds
168
+
169
+ **Example:**
170
+
171
+ ```ts
172
+ // Use first row as main header
173
+ const table = new PlaywrightTable(page.locator("table"), {
174
+ header: { setMainHeaderRow: 0 },
175
+ });
176
+
177
+ const mainHeader = await table.getMainHeaderRow();
178
+ console.log(mainHeader);
179
+ // Output: ["Name", "Age", "City"]
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 3. `getBodyRows(options?)`
185
+
186
+ Retrieves all body rows with support for rowspan and colspan.
187
+
188
+ **Parameters:**
189
+
190
+ - `options?: TableOptions` - Full table options (`bodyRowOptions`, `headerRowOptions`) and legacy `cellContentType` shortcut
191
+
192
+ **Returns:** `Promise<BodyRow[]>` - Array of body rows
193
+
194
+ **Throws:** Error if no body rows found
195
+
196
+ **Example:**
197
+
198
+ ```ts
199
+ // Legacy shortcut
200
+ const rows = await table.getBodyRows({
201
+ cellContentType: CellContentType.TextContent, // Include hidden elements
202
+ });
203
+
204
+ // Full table options (useful for custom grids with noisy headers)
205
+ const rowsWithOptions = await table.getBodyRows({
206
+ bodyRowOptions: { cellContentType: CellContentType.InnerText },
207
+ headerRowOptions: {
208
+ normalizeWhitespace: true,
209
+ stripIconGlyphs: true,
210
+ },
211
+ });
212
+
213
+ console.log(rows);
214
+ // Output: [["John", "30", "NYC"], ["Jane", "25", "LA"]]
215
+ ```
216
+
217
+ **Cell Content Types:**
218
+
219
+ - `CellContentType.InnerText` - Rendered text (default, excludes hidden elements)
220
+ - `CellContentType.TextContent` - Raw text content (includes hidden elements)
221
+
222
+ Header sanitization options (`normalizeWhitespace`, `stripIconGlyphs`) can be used in all methods that accept `HeaderRowOptions` or `TableOptions`, such as `getHeaderRows`, `getMainHeaderRow`, `getJson`, `findRowIndex`, and `getBodyCellLocatorByRowConditions`.
223
+
224
+ ---
225
+
226
+ ### 4. `getBodyCellLocator(rowNumber, headerPosition)`
227
+
228
+ Gets a Playwright Locator for a specific cell by row and column indices (0-based).
229
+
230
+ **Parameters:**
231
+
232
+ - `rowNumber: number` - Row index (0-based)
233
+ - `headerPosition: number` - Column index (0-based)
234
+
235
+ **Returns:** `Locator` - Playwright locator for the cell
236
+
237
+ **Example:**
238
+
239
+ ```ts
240
+ // Get cell at row 1, column 2
241
+ const cellLocator = table.getBodyCellLocator(1, 2);
242
+ await expect(cellLocator).toHaveText("Expected Value");
243
+ await cellLocator.click();
244
+ ```
245
+
246
+ ---
247
+
248
+ ### 5. `getBodyCellLocatorByRowConditions(conditions, targetHeader, options?)`
249
+
250
+ 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).
251
+
252
+ **Parameters:**
253
+
254
+ - `conditions: Record<string, string> | [string, string]` - Header-value pairs to match (object) or single [header, value] tuple
255
+ - `targetHeader: string` - Column name to retrieve
256
+ - `options?: TableOptions` - Table loading options
257
+
258
+ **Returns:** `Promise<Locator>` - Cell locator
259
+
260
+ **Throws:**
261
+
262
+ - Error if no matching row found
263
+ - Error if specified headers don't exist
264
+
265
+ **Example:**
266
+
267
+ ```ts
268
+ // Find cell with multiple conditions (object syntax)
269
+ const emailCell = await table.getBodyCellLocatorByRowConditions({ "First name": "John", Status: "Active" }, "Email");
270
+ await expect(emailCell).toContainText("@example.com");
271
+
272
+ // Find cell with single condition (tuple syntax)
273
+ const actionCell = await table.getBodyCellLocatorByRowConditions(["Username", "john.doe"], "Actions");
274
+ await actionCell.locator("button.delete").click();
275
+
276
+ // Find cell with single condition (object syntax also works)
277
+ const statusCell = await table.getBodyCellLocatorByRowConditions({ Username: "john.doe" }, "Status");
278
+ ```
279
+
280
+ ---
281
+
282
+ ### 6. `getAllBodyCellLocatorsByHeaderName(headerName, options?)`
283
+
284
+ Gets all cell locators in a column by header name.
285
+
286
+ **Parameters:**
287
+
288
+ - `headerName: string` - Column header name
289
+ - `options?: TableOptions` - Table loading options
290
+
291
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
292
+
293
+ **Throws:** Error if header not found
294
+
295
+ **Example:**
296
+
297
+ ```ts
298
+ // Get all cells in "Status" column
299
+ const statusCells = await table.getAllBodyCellLocatorsByHeaderName("Status");
300
+ for (const cell of statusCells) {
301
+ await expect(cell).toHaveText(/Active|Inactive/);
302
+ }
303
+
304
+ // Extract all values from "Name" column
305
+ const names = await Promise.all(statusCells.map(cell => cell.textContent()));
306
+ console.log(names); // ["John", "Jane", "Bob"]
307
+ ```
308
+
309
+ ---
310
+
311
+ ### 7. `getAllBodyCellLocatorsByHeaderIndex(headerIndex, options?)`
312
+
313
+ Gets all cell locators in a column by column index (0-based).
314
+
315
+ **Parameters:**
316
+
317
+ - `headerIndex: number` - Column index (0-based)
318
+ - `options?: TableOptions` - Table loading options
319
+
320
+ **Returns:** `Promise<Locator[]>` - Array of cell locators
321
+
322
+ **Throws:** Error if index out of bounds
323
+
324
+ **Example:**
325
+
326
+ ```ts
327
+ // Get all cells in first column (index 0)
328
+ const firstColumnCells = await table.getAllBodyCellLocatorsByHeaderIndex(0);
329
+ const values = await Promise.all(firstColumnCells.map(c => c.textContent()));
330
+ console.log(values);
331
+
332
+ // Verify all cells are visible
333
+ for (const cell of firstColumnCells) {
334
+ await expect(cell).toBeVisible();
335
+ }
336
+ ```
337
+
338
+ ---
339
+
340
+ ### 8. `getJson(options?)`
341
+
342
+ Converts the entire table to a JSON array of objects. **Headers become object keys**, values are cell contents.
343
+
344
+ **Parameters:**
345
+
346
+ - `options?: TableOptions` - Configure parsing behavior
347
+
348
+ **Returns:** `Promise<any[]>` - Array of row objects
349
+
350
+ **Throws:** Error if no headers or body rows found
351
+
352
+ **Simple Example:**
353
+
354
+ <table>
355
+ <thead><tr><th>Name</th><th>Age</th></tr></thead>
356
+ <tbody>
357
+ <tr><td>John</td><td>30</td></tr>
358
+ <tr><td>Jane</td><td>25</td></tr>
359
+ </tbody>
360
+ </table>
361
+
362
+ ```ts
363
+ // HTML:
364
+ <table>
365
+ <thead><tr><th>Name</th><th>Age</th></tr></thead>
366
+ <tbody>
367
+ <tr><td>John</td><td>30</td></tr>
368
+ <tr><td>Jane</td><td>25</td></tr>
369
+ </tbody>
370
+ </table>
371
+
372
+ const json = await table.getJson();
373
+ console.log(json);
374
+ // Output:
375
+ // [
376
+ // { "Name": "John", "Age": "30" },
377
+ // { "Name": "Jane", "Age": "25" }
378
+ // ]
379
+ ```
380
+
381
+ **Complex Example with Options:**
382
+
383
+ ```ts
384
+ // HTML with colspan, rowspan, empty cells, and duplicates
385
+ const json = await table.getJson({
386
+ headerRowOptions: {
387
+ colspan: { enabled: true, suffix: true },
388
+ duplicateSuffix: true,
389
+ emptyCellReplacement: true,
390
+ },
391
+ });
392
+ // Output:
393
+ // [
394
+ // {
395
+ // "Awesome Rowspan": "Value1",
396
+ // "Name": "John",
397
+ // "Name__C1": "Doe",
398
+ // "{{Empty}}": "EmptyValue",
399
+ // "Duplicate": "First",
400
+ // "Duplicate__D1": "Second"
401
+ // }
402
+ // ]
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Waiting Methods
408
+
409
+ ### 9. `waitForEmpty(options?)`
410
+
411
+ Waits for the table body to be completely empty (no body rows).
412
+
413
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBe(0);`
414
+
415
+ **Parameters:**
416
+
417
+ - `options?: PollingOptions`
418
+
419
+ **Returns:** `Promise<void>`
420
+
421
+ **Throws:** Error if table is not empty within timeout
422
+
423
+ **Example:**
424
+
425
+ ```ts
426
+ // Click delete all button
427
+ await page.locator("button.delete-all").click();
428
+
429
+ // Wait for table to clear
430
+ await table.waitForEmpty({ timeout: 5000 });
431
+
432
+ // Verify empty state
433
+ const rows = await table.getBodyRows();
434
+ expect(rows).toHaveLength(0);
435
+ ```
436
+
437
+ ---
438
+
439
+ ### 10. `waitForNonEmpty(options?)`
440
+
441
+ 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.
442
+
443
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBeGreaterThan(0);`
444
+
445
+ **Parameters:**
446
+
447
+ - `options?: PollingOptions`
448
+
449
+ **Returns:** `Promise<void>`
450
+
451
+ **Throws:**
452
+
453
+ - Error if table remains empty (no rows) within timeout
454
+ - Error if table has rows but all cells are empty within timeout
455
+
456
+ **Example:**
457
+
458
+ ```ts
459
+ // Trigger data load
460
+ await page.locator("button.load-data").click();
461
+
462
+ // Wait for table to populate with actual data
463
+ await table.waitForNonEmpty({ timeout: 5000 });
464
+
465
+ // Now safe to process data - guaranteed to have content
466
+ const data = await table.getJson();
467
+ expect(data.length).toBeGreaterThan(0);
468
+
469
+ // Useful for dynamically loaded tables where structure appears before data
470
+ await searchInput.fill("search term");
471
+ await table.waitForNonEmpty(); // Waits for rows AND content, not just empty row elements
472
+ ```
473
+
474
+ ---
475
+
476
+ ### 11. `waitForExactRowCount(count, options?)`
477
+
478
+ Waits for the table to have exactly the specified number of body rows.
479
+
480
+ > 💡 **Tip:** Consider using `expect.poll` for more flexibility: `await expect.poll(async () => (await table.getBodyRows()).length).toBe(10);`
481
+
482
+ **Parameters:**
483
+
484
+ - `count: number` - The exact number of rows expected
485
+ - `options?: PollingOptions` - Polling options (timeout, interval)
486
+
487
+ **Returns:** `Promise<void>`
488
+
489
+ **Throws:**
490
+
491
+ - Error if row count doesn't match within timeout
492
+ - Error if count is negative or not an integer
493
+
494
+ **Example:**
495
+
496
+ ```ts
497
+ // Wait for table to have exactly 10 rows
498
+ await table.waitForExactRowCount(10, { timeout: 5000 });
499
+
500
+ // Verify table has exactly 0 rows (alternative to waitForEmpty)
501
+ await table.waitForExactRowCount(0);
502
+
503
+ // Wait after pagination change
504
+ await page.locator("button.page-2").click();
505
+ await table.waitForExactRowCount(25);
506
+
507
+ // Verify filtered results
508
+ await page.locator("input.search").fill("admin");
509
+ await table.waitForExactRowCount(3); // Expect exactly 3 admin users
510
+ ```
511
+
512
+ **Use Cases:**
513
+
514
+ - Verifying exact result counts after filtering
515
+ - Validating pagination behavior
516
+ - Testing data deletion/addition
517
+ - Ensuring precise table state for assertions
518
+
519
+ ---
520
+
521
+ ### 12. `getRowCount()`
522
+
523
+ Gets the count of header and body rows in the table. Lightweight method that doesn't fetch cell data, only counts rows.
524
+
525
+ **Parameters:** None
526
+
527
+ **Returns:** `Promise<{ header: number; body: number }>`
528
+
529
+ **Example:**
530
+
531
+ ```ts
532
+ // Get current row counts
533
+ const counts = await table.getRowCount();
534
+ console.log(`Headers: ${counts.header}, Body: ${counts.body}`);
535
+
536
+ // Assert specific row count
537
+ const { body } = await table.getRowCount();
538
+ expect(body).toBe(10);
539
+
540
+ // Check if table is empty efficiently
541
+ const rowCounts = await table.getRowCount();
542
+ if (rowCounts.body === 0) {
543
+ console.log("Table is empty");
544
+ }
545
+ ```
546
+
547
+ **Use Cases:**
548
+
549
+ - Quick validation of table size without fetching data
550
+ - Performance-sensitive checks in loops
551
+ - Conditional logic based on row counts
552
+ - Monitoring table size during pagination
553
+
554
+ ---
555
+
556
+ ### 13. `getDistinctColumnValues(headerName, options?)`
557
+
558
+ Gets all distinct (unique) values from a specific column. Returns a sorted array of unique string values, excluding empty values.
559
+
560
+ **Parameters:**
561
+
562
+ - `headerName: string` - The header name of the column to extract distinct values from
563
+ - `options?: TableOptions` - Optional table loading options
564
+
565
+ **Returns:** `Promise<string[]>` - Sorted array of distinct values
566
+
567
+ **Throws:** Error if the specified header is not found in the table
568
+
569
+ **Example:**
570
+
571
+ ```ts
572
+ // Get all unique status values
573
+ const statuses = await table.getDistinctColumnValues("Status");
574
+ // Result: ["Active", "Inactive", "Pending"]
575
+
576
+ // Get distinct countries for filtering
577
+ const countries = await table.getDistinctColumnValues("Country");
578
+ console.log(`Available countries: ${countries.join(", ")}`);
579
+
580
+ // Verify expected values exist
581
+ const roles = await table.getDistinctColumnValues("Role");
582
+ expect(roles).toContain("Admin");
583
+ expect(roles).toHaveLength(3);
584
+
585
+ // Build dynamic filters from table data
586
+ const categories = await table.getDistinctColumnValues("Category");
587
+ for (const category of categories) {
588
+ await page.locator(`button[data-filter="${category}"]`).click();
589
+ // Verify filtering works
590
+ }
591
+ ```
592
+
593
+ **Use Cases:**
594
+
595
+ - Extracting filter options from table data
596
+ - Validating data variety and uniqueness
597
+ - Building dynamic test data sets
598
+ - Verifying dropdown/select options match table content
599
+
600
+ ---
601
+
602
+ ### 14. `findRowIndex(conditions, options?)`
603
+
604
+ Finds the index of the first body row matching the specified conditions. Returns -1 if no matching row is found.
605
+
606
+ **Parameters:**
607
+
608
+ - `conditions: Record<string, string>` - Record of header names and expected cell values to match
609
+ - `options?: TableOptions` - Optional table loading options
610
+
611
+ **Returns:** `Promise<number>` - The 0-based index of the first matching row, or -1 if not found
612
+
613
+ **Throws:** Error if specified headers don't exist in the table
614
+
615
+ **Example:**
616
+
617
+ ```ts
618
+ // Find index of row where Status is "Active"
619
+ const index = await table.findRowIndex({ Status: "Active" });
620
+ if (index >= 0) {
621
+ console.log(`Found at row ${index}`);
622
+ const cell = table.getBodyCellLocator(index, 0);
623
+ await cell.click();
624
+ }
625
+
626
+ // Find row and interact with it
627
+ const rowIndex = await table.findRowIndex({ Username: "john.doe" });
628
+ if (rowIndex >= 0) {
629
+ const deleteButton = table.getBodyCellLocator(rowIndex, 3).locator("button.delete");
630
+ await deleteButton.click();
631
+ } else {
632
+ console.log("User not found");
633
+ }
634
+
635
+ // Check if specific data exists before taking action
636
+ const orderIndex = await table.findRowIndex({ "Order ID": "12345" });
637
+ if (orderIndex === -1) {
638
+ throw new Error("Order not found in table");
639
+ }
640
+ ```
641
+
642
+ **Use Cases:**
643
+
644
+ - Finding row position for interaction without fetching all data
645
+ - Conditional logic based on row existence
646
+ - Getting row indices for further locator operations
647
+ - Checking data presence without waiting
648
+
649
+ ---
650
+
651
+ ## Advanced Options
652
+
653
+ ### Custom Selectors
654
+
655
+ Customize selectors for non-standard table structures or div-based tables:
656
+
657
+ ```ts
658
+ const table = new PlaywrightTable(page.locator(".custom-table"), {
659
+ header: {
660
+ setMainHeaderRow: 0, // Use first header row as main (default: last row)
661
+ rowSelector: ".header-row", // Custom header row selector
662
+ columnSelector: ".header-cell", // Custom header cell selector
663
+ },
664
+ row: {
665
+ rowSelector: ".data-row", // Custom body row selector
666
+ columnSelector: ".data-cell", // Custom body cell selector
667
+ },
668
+ });
669
+ ```
670
+
671
+ ### Div-based Tables
672
+
673
+ ```ts
674
+ // Works with div-based table structures
675
+ const divTable = new PlaywrightTable(page.locator(".divTable"), {
676
+ header: {
677
+ rowSelector: ".divTableHeading > .divTableRow",
678
+ columnSelector: ".divTableHead",
679
+ },
680
+ row: {
681
+ rowSelector: ".divTableBody > .divTableRow",
682
+ columnSelector: ".divTableCell",
683
+ },
684
+ });
685
+ ```
686
+
687
+ ### ARIA Treegrid Tables
688
+
689
+ For `role="treegrid"` structures, make sure header and body row selectors are scoped to different containers.
690
+ If both use the same selector (for example `div[role='row']`), header rows can be included in body rows.
691
+
692
+ ```ts
693
+ const table = new PlaywrightTable(page.locator("[role='treegrid']"), {
694
+ header: {
695
+ setMainHeaderRow: 0,
696
+ rowSelector: "[data-kind='header-group'] > div[role='row']",
697
+ columnSelector: "div[role='columnheader']",
698
+ },
699
+ row: {
700
+ rowSelector: "[data-kind='body-group'] > div[role='row']",
701
+ columnSelector: "div[role='gridcell']",
702
+ },
703
+ });
704
+ ```
705
+
706
+ ### Header Row Options
707
+
708
+ Control how headers are processed:
709
+
710
+ ```ts
711
+ const headers = await table.getHeaderRows({
712
+ cellContentType: CellContentType.TextContent, // Include hidden text
713
+ emptyCellReplacement: true, // Replace "" with "{{Empty}}"
714
+ duplicateSuffix: true, // "Name", "Name__D1", "Name__D2"
715
+ normalizeWhitespace: true, // "Account\nName" -> "Account Name"
716
+ stripIconGlyphs: true, // Removes private-use icon glyph chars from header text
717
+ colspan: {
718
+ enabled: true, // Process colspan attributes
719
+ suffix: true, // "Col", "Col__C1", "Col__C2"
720
+ },
721
+ });
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Error Handling
727
+
728
+ The library provides **detailed error messages** with context to help you debug issues:
729
+
730
+ ```ts
731
+ try {
732
+ await table.getBodyCellLocatorByRowConditions({ "Invalid Header": "value" }, "Target");
733
+ } catch (error) {
734
+ console.error(error.message);
735
+ // Error output:
736
+ // Header "Invalid Header" not found.
737
+ // Available headers: [First name, Last name, Date of birth]
738
+ // Header row locator: Locator@table >> thead >> tr
739
+ }
740
+ ```
741
+
742
+ **Error Context Includes:**
743
+
744
+ - Locator descriptions with full selector chains
745
+ - Available headers when header not found
746
+ - Row/column indices when out of bounds
747
+ - Cell content types in use
748
+ - Validation failures with expected vs actual values
749
+
750
+ ---
751
+
752
+ ## Best Practices
753
+
754
+ ### 1. Wait for Dynamic Content
755
+
756
+ ```ts
757
+ // ❌ Bad - might fail if table still loading
758
+ const data = await table.getJson();
759
+
760
+ // ✅ Good - wait for content first
761
+ await table.waitForNonEmpty();
762
+ const data = await table.getJson();
763
+
764
+ // ✅ Good - wait for specific row count
765
+ await table.waitForExactRowCount(5);
766
+ const data = await table.getJson();
767
+
768
+ // ✅ Good - use expect.poll for flexible waiting
769
+ await expect.poll(async () => (await table.getBodyRows()).length).toBe(5);
770
+ const data = await table.getJson();
771
+ ```
772
+
773
+ ### 2. Use `expect.poll` for Flexible Waiting
774
+
775
+ While this library provides dedicated wait methods, Playwright's `expect.poll` offers more flexibility when combined with retrieval methods:
776
+
777
+ ```ts
778
+ import { expect } from "@playwright/test";
779
+
780
+ // Wait for exact row count
781
+ await expect.poll(async () => (await table.getBodyRows()).length).toBe(10);
782
+
783
+ // Wait for table to be empty
784
+ await expect.poll(async () => (await table.getJson()).length).toBe(0);
785
+
786
+ // Wait for at least N rows
787
+ await expect.poll(async () => (await table.getBodyRows()).length).toBeGreaterThanOrEqual(5);
788
+
789
+ // Wait for specific data to appear
790
+ await expect.poll(() => table.getJson()).toContainEqual({ Name: "John", Status: "Active" });
791
+
792
+ // Wait for column to have specific value
793
+ await expect.poll(() => table.getDistinctColumnValues("Status")).toContain("Completed");
794
+
795
+ // Custom timeout and intervals
796
+ await expect
797
+ .poll(async () => (await table.getBodyRows()).length, {
798
+ message: "Waiting for table to have 10 rows",
799
+ timeout: 10000,
800
+ intervals: [100, 250, 500, 1000],
801
+ })
802
+ .toBe(10);
803
+ ```
804
+
805
+ **Benefits of `expect.poll`:**
806
+
807
+ - ✅ Native Playwright API with familiar assertion syntax
808
+ - ✅ Rich assertion library (`toBe`, `toContain`, `toEqual`, `toMatch`, etc.)
809
+ - ✅ Better error messages with actual vs expected values
810
+ - ✅ Works with any retrieval method (`getBodyRows`, `getJson`, `getDistinctColumnValues`, etc.)
811
+
812
+ ### 3. Use Appropriate Content Type
813
+
814
+ ```ts
815
+ // For visible text only (respects CSS visibility)
816
+ const rows = await table.getBodyRows({
817
+ cellContentType: CellContentType.InnerText,
818
+ });
819
+
820
+ // For all text including hidden elements
821
+ const rows = await table.getBodyRows({
822
+ cellContentType: CellContentType.TextContent,
823
+ });
824
+ ```
825
+
826
+ ### 4. Handle Complex Tables
827
+
828
+ ```ts
829
+ // Enable all options for complex tables with colspan/rowspan
830
+ const json = await table.getJson({
831
+ headerRowOptions: {
832
+ colspan: { enabled: true, suffix: true },
833
+ duplicateSuffix: true,
834
+ emptyCellReplacement: true,
835
+ },
836
+ });
837
+ ```
838
+
839
+ ### 5. Leverage Cell Locators for Interactions
840
+
841
+ ```ts
842
+ // Get locator first, then interact
843
+ const cell = await table.getBodyCellLocatorByRowConditions({ Name: "John Doe" }, "Actions");
844
+ await cell.locator("button.edit").click();
845
+ await cell.locator("button.delete").click();
846
+ ```
847
+
848
+ ---
849
+
850
+ ## TypeScript Support
851
+
852
+ Full TypeScript support with exported types:
853
+
854
+ ```ts
855
+ import {
856
+ PlaywrightTable,
857
+ HeaderRow,
858
+ BodyRow,
859
+ Cell,
860
+ CellContentType,
861
+ HeaderRowOptions,
862
+ TableOptions,
863
+ PollingOptions,
864
+ RowKind,
865
+ } from "@cerios/playwright-table";
866
+ ```
867
+
868
+ ---
869
+
870
+ ## Performance
871
+
872
+ The library is optimized for performance:
873
+
874
+ - ✅ **Parallel data fetching** - All cells in a row are fetched simultaneously
875
+ - ✅ **Efficient DOM queries** - Minimizes Playwright API calls
876
+ - ✅ **Smart polling** - Configurable intervals and timeouts
877
+ - ✅ **Lazy evaluation** - Only fetches data when needed
878
+
879
+ ---
880
+
881
+ ## Browser Support
882
+
883
+ Works with all Playwright-supported browsers:
884
+
885
+ - ✅ Chromium
886
+ - ✅ Firefox
887
+ - ✅ WebKit
888
+
889
+ ---
890
+
891
+ ## Contributing
892
+
893
+ Contributions are welcome! Please check out the [GitHub repository](https://github.com/CeriosTesting/playwright-tables).
894
+
895
+ ---
896
+
897
+ ## License
898
+
899
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
900
+
901
+ ---
902
+
903
+ ## Support
904
+
905
+ - 📖 [Documentation](https://github.com/CeriosTesting/playwright-tables)
906
+ - 🐛 [Report Issues](https://github.com/CeriosTesting/playwright-tables/issues)
907
+ - 💬 [Discussions](https://github.com/CeriosTesting/playwright-tables/discussions)
908
+
909
+ ---
910
+
911
+ **Made with ❤️ by Cerios**