@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/LICENSE +7 -7
- package/README.md +911 -447
- package/dist/index.d.mts +330 -92
- package/dist/index.d.ts +330 -92
- package/dist/index.js +823 -242
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +823 -242
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -23
package/README.md
CHANGED
|
@@ -1,447 +1,911 @@
|
|
|
1
|
-
# 🎭 Playwright Tables | By Cerios
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<th>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1
|
+
# 🎭 Playwright Tables | By Cerios
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@cerios/playwright-table)
|
|
4
|
+
[](https://www.npmjs.com/package/@cerios/playwright-table)
|
|
5
|
+
[](https://github.com/CeriosTesting/playwright-tables/blob/main/LICENSE)
|
|
6
|
+
[](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**
|