@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/dist/index.mjs CHANGED
@@ -1,5 +1,24 @@
1
- // src/playwright-table.ts
2
- import { expect as expect2 } from "@playwright/test";
1
+ // src/polling.ts
2
+ async function pollTable(fn, options) {
3
+ var _a, _b;
4
+ if (!options) {
5
+ options = {};
6
+ }
7
+ const timeout = (_a = options.timeout) != null ? _a : 3e4;
8
+ const interval = (_b = options.interval) != null ? _b : 100;
9
+ const startTime = Date.now();
10
+ let lastError;
11
+ while (Date.now() - startTime < timeout) {
12
+ try {
13
+ await fn();
14
+ return;
15
+ } catch (err) {
16
+ lastError = err;
17
+ await new Promise((resolve) => setTimeout(resolve, interval));
18
+ }
19
+ }
20
+ throw lastError || new Error("Playwright Tables: Polling timeout reached");
21
+ }
3
22
 
4
23
  // src/cell-content-type.ts
5
24
  var CellContentType = /* @__PURE__ */ ((CellContentType2) => {
@@ -10,34 +29,127 @@ var CellContentType = /* @__PURE__ */ ((CellContentType2) => {
10
29
 
11
30
  // src/table-utils.ts
12
31
  var TableUtils = class {
32
+ constructor() {
33
+ }
34
+ /**
35
+ * Extracts text content from a table cell.
36
+ * @param cell - The Playwright locator for the cell.
37
+ * @param cellContentType - The type of content to extract (InnerText or TextContent).
38
+ * @returns The trimmed text content of the cell, or empty string if no content.
39
+ * @throws Error if the cell locator doesn't exist in the DOM.
40
+ */
13
41
  static async getCellContent(cell, cellContentType) {
14
- const content = cellContentType === "innerText" /* InnerText */ ? await cell.innerText() : await cell.textContent();
15
- return (content == null ? void 0 : content.trim()) || "";
42
+ try {
43
+ const count = await cell.count();
44
+ if (count === 0) {
45
+ throw new Error("Cell locator does not exist in the DOM");
46
+ }
47
+ const content = cellContentType === "innerText" /* InnerText */ ? await cell.innerText() : await cell.textContent();
48
+ return (content == null ? void 0 : content.trim()) || "";
49
+ } catch (error) {
50
+ const locatorDescription = this.getLocatorDescription(cell);
51
+ throw new Error(
52
+ `Failed to get cell content.
53
+ Locator: ${locatorDescription}
54
+ Content type: ${cellContentType}
55
+ Error: ${error instanceof Error ? error.message : String(error)}`
56
+ );
57
+ }
16
58
  }
59
+ /**
60
+ * Parses rowspan and colspan attributes from a table cell.
61
+ * Fetches both attributes in parallel for optimal performance.
62
+ * @param cell - The Playwright locator for the cell.
63
+ * @returns Object containing rowspan and colspan values (defaults to 1 if not present).
64
+ * @throws Error if attributes contain invalid values (non-numeric or less than 1).
65
+ */
17
66
  static async parseSpanAttributes(cell) {
18
- return {
19
- rowspan: parseInt(await cell.getAttribute("rowspan") || "1", 10),
20
- colspan: parseInt(await cell.getAttribute("colspan") || "1", 10)
21
- };
67
+ try {
68
+ const [rowspanAttr, colspanAttr] = await Promise.all([
69
+ cell.getAttribute("rowspan"),
70
+ cell.getAttribute("colspan")
71
+ ]);
72
+ const rowspan = parseInt(rowspanAttr || "1", 10);
73
+ const colspan = parseInt(colspanAttr || "1", 10);
74
+ if (isNaN(rowspan) || rowspan < 1) {
75
+ throw new Error(`Invalid rowspan attribute: "${rowspanAttr}". Expected positive integer.`);
76
+ }
77
+ if (isNaN(colspan) || colspan < 1) {
78
+ throw new Error(`Invalid colspan attribute: "${colspanAttr}". Expected positive integer.`);
79
+ }
80
+ return { rowspan, colspan };
81
+ } catch (error) {
82
+ const locatorDescription = this.getLocatorDescription(cell);
83
+ throw new Error(
84
+ `Failed to parse span attributes.
85
+ Locator: ${locatorDescription}
86
+ Error: ${error instanceof Error ? error.message : String(error)}`
87
+ );
88
+ }
89
+ }
90
+ /**
91
+ * Extracts a human-readable locator description from a Playwright Locator.
92
+ * This captures Playwright's internal selector chain for better error messages.
93
+ * @param locator - The Playwright locator to describe.
94
+ * @returns A string representation of the locator's selector chain.
95
+ */
96
+ static getLocatorDescription(locator) {
97
+ const locatorString = locator.toString();
98
+ return locatorString;
22
99
  }
23
100
  };
24
101
 
25
102
  // src/table-body.ts
26
103
  var TableBody = class {
104
+ constructor() {
105
+ }
106
+ /**
107
+ * Extracts body rows from a table with proper handling of rowspan and colspan.
108
+ *
109
+ * @param rowLocator - The Playwright locator for body rows.
110
+ * @param columnsSelector - CSS selector for locating columns within rows (e.g., "td").
111
+ * @param bodyRowsOptions - Optional configuration for cell content extraction.
112
+ * @param bodyRowsOptions.cellContentType - The type of content to extract (InnerText or TextContent).
113
+ * @returns Promise resolving to an array of processed body rows.
114
+ * @throws Error if columnsSelector is empty or if row processing fails.
115
+ *
116
+ * @example
117
+ * const rows = await TableBody.getRows(
118
+ * page.locator('tbody tr'),
119
+ * 'td',
120
+ * { cellContentType: CellContentType.TextContent }
121
+ * );
122
+ */
27
123
  static async getRows(rowLocator, columnsSelector, bodyRowsOptions) {
124
+ if (!columnsSelector || columnsSelector.trim() === "") {
125
+ throw new Error("columnsSelector cannot be empty");
126
+ }
127
+ const locatorDescription = rowLocator.toString();
28
128
  const rows = [];
29
129
  const rowsCount = await rowLocator.count();
130
+ if (rowsCount === 0) {
131
+ return rows;
132
+ }
30
133
  const spannedCells = {};
31
134
  for (let rowIndex = 0; rowIndex < rowsCount; rowIndex++) {
32
- const row = rowLocator.nth(rowIndex);
33
- const columns = await this.extractColumns(
34
- row,
35
- columnsSelector,
36
- rowIndex,
37
- spannedCells,
38
- bodyRowsOptions == null ? void 0 : bodyRowsOptions.cellContentType
39
- );
40
- rows.push(columns);
135
+ try {
136
+ const row = rowLocator.nth(rowIndex);
137
+ const columns = await this.extractColumns(
138
+ row,
139
+ columnsSelector,
140
+ rowIndex,
141
+ spannedCells,
142
+ bodyRowsOptions == null ? void 0 : bodyRowsOptions.cellContentType
143
+ );
144
+ rows.push(columns);
145
+ } catch (error) {
146
+ throw new Error(
147
+ `Failed to process body row at index ${rowIndex}.
148
+ Body row locator: ${locatorDescription}
149
+ Columns selector: "${columnsSelector}"
150
+ Error: ${error instanceof Error ? error.message : String(error)}`
151
+ );
152
+ }
41
153
  }
42
154
  return rows;
43
155
  }
@@ -45,35 +157,77 @@ var TableBody = class {
45
157
  const columns = [];
46
158
  const columnLocators = row.locator(columnsSelector);
47
159
  const columnCount = await columnLocators.count();
160
+ if (columnCount === 0) {
161
+ this.applySpannedCells(spannedCells, rowIndex, columns);
162
+ return columns;
163
+ }
164
+ const cellDataPromises = [];
48
165
  for (let colIndex = 0; colIndex < columnCount; colIndex++) {
49
- await this.processColumn(
50
- columnLocators.nth(colIndex),
51
- colIndex,
52
- columns,
53
- rowIndex,
54
- spannedCells,
55
- cellContentType
56
- );
166
+ cellDataPromises.push(this.fetchCellData(columnLocators.nth(colIndex), cellContentType));
167
+ }
168
+ const cellDataArray = await Promise.all(cellDataPromises);
169
+ const spannedPositions = /* @__PURE__ */ new Set();
170
+ if (spannedCells[rowIndex]) {
171
+ spannedCells[rowIndex].forEach((_, colIndex) => {
172
+ if (spannedCells[rowIndex][colIndex] !== void 0) {
173
+ spannedPositions.add(colIndex);
174
+ }
175
+ });
176
+ }
177
+ let actualColIndex = 0;
178
+ for (let domCellIndex = 0; domCellIndex < columnCount; domCellIndex++) {
179
+ while (spannedPositions.has(actualColIndex)) {
180
+ actualColIndex++;
181
+ }
182
+ const cellData = cellDataArray[domCellIndex];
183
+ this.processCellData(cellData, actualColIndex, columns, rowIndex, spannedCells);
184
+ actualColIndex += cellData.colspan;
57
185
  }
58
186
  this.applySpannedCells(spannedCells, rowIndex, columns);
59
187
  return columns;
60
188
  }
61
- static async processColumn(column, colIndex, columns, rowIndex, spannedCells, cellContentType = "innerText" /* InnerText */) {
62
- const content = await TableUtils.getCellContent(column, cellContentType);
63
- const { rowspan, colspan } = await TableUtils.parseSpanAttributes(column);
189
+ /**
190
+ * Fetches cell content and span attributes in parallel.
191
+ * This method performs DOM operations that can be parallelized across multiple cells.
192
+ */
193
+ static async fetchCellData(column, cellContentType = "innerText" /* InnerText */) {
194
+ const [content, spanAttributes] = await Promise.all([
195
+ TableUtils.getCellContent(column, cellContentType),
196
+ TableUtils.parseSpanAttributes(column)
197
+ ]);
198
+ return {
199
+ content,
200
+ rowspan: spanAttributes.rowspan,
201
+ colspan: spanAttributes.colspan
202
+ };
203
+ }
204
+ /**
205
+ * Processes pre-fetched cell data and applies it to the columns array.
206
+ * This method is purely computational and doesn't perform any DOM operations.
207
+ */
208
+ static processCellData(cellData, colIndex, columns, rowIndex, spannedCells) {
209
+ const { content, rowspan, colspan } = cellData;
210
+ if (colIndex < 0) {
211
+ throw new Error(`Invalid column index: ${colIndex}. Must be non-negative.`);
212
+ }
213
+ if (rowIndex < 0) {
214
+ throw new Error(`Invalid row index: ${rowIndex}. Must be non-negative.`);
215
+ }
64
216
  for (let span = 0; span < colspan; span++) {
65
217
  columns[colIndex + span] = content;
66
218
  }
67
219
  if (rowspan > 1) {
68
- this.storeSpannedCells(rowIndex, colIndex, rowspan, content, spannedCells);
220
+ this.storeSpannedCells(rowIndex, colIndex, rowspan, colspan, content, spannedCells);
69
221
  }
70
222
  }
71
- static storeSpannedCells(rowIndex, colIndex, rowspan, content, spannedCells) {
72
- for (let span = 1; span < rowspan; span++) {
73
- if (!spannedCells[rowIndex + span]) {
74
- spannedCells[rowIndex + span] = [];
223
+ static storeSpannedCells(rowIndex, colIndex, rowspan, colspan, content, spannedCells) {
224
+ for (let rowSpan = 1; rowSpan < rowspan; rowSpan++) {
225
+ if (!spannedCells[rowIndex + rowSpan]) {
226
+ spannedCells[rowIndex + rowSpan] = [];
227
+ }
228
+ for (let colSpan = 0; colSpan < colspan; colSpan++) {
229
+ spannedCells[rowIndex + rowSpan][colIndex + colSpan] = content;
75
230
  }
76
- spannedCells[rowIndex + span][colIndex] = content;
77
231
  }
78
232
  }
79
233
  static applySpannedCells(spannedCells, rowIndex, columns) {
@@ -89,52 +243,131 @@ var TableBody = class {
89
243
 
90
244
  // src/table-header.ts
91
245
  var TableHeader = class {
246
+ constructor() {
247
+ }
248
+ /**
249
+ * Extracts header rows from a table with optional processing for colspan, rowspan, and duplicates.
250
+ *
251
+ * @param headerRowLocator - The Playwright locator for header rows.
252
+ * @param columnsSelector - CSS selector for locating header columns (e.g., "th").
253
+ * @param headerRowOptions - Optional configuration for header processing.
254
+ * @returns Promise resolving to an array of processed header rows.
255
+ * @throws Error if columnsSelector is empty or if no header rows are found.
256
+ *
257
+ * @example
258
+ * const headers = await TableHeader.getRows(
259
+ * page.locator('thead tr'),
260
+ * 'th',
261
+ * { duplicateSuffix: true, colspan: { enabled: true } }
262
+ * );
263
+ */
92
264
  static async getRows(headerRowLocator, columnsSelector, headerRowOptions) {
265
+ if (!columnsSelector || columnsSelector.trim() === "") {
266
+ throw new Error("columnsSelector cannot be empty");
267
+ }
268
+ const locatorDescription = headerRowLocator.toString();
93
269
  const options = this.getDefaultOptions(headerRowOptions);
94
270
  const rows = await headerRowLocator.all();
95
271
  const headerRows = [];
272
+ if (rows.length === 0) {
273
+ return headerRows;
274
+ }
96
275
  const rowSpans = /* @__PURE__ */ new Map();
97
- for (const row of rows) {
98
- const headerRow = await this.processRow(row, columnsSelector, rowSpans, options);
99
- headerRows.push(headerRow);
276
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
277
+ try {
278
+ const headerRow = await this.processRow(rows[rowIndex], columnsSelector, rowSpans, options);
279
+ headerRows.push(headerRow);
280
+ } catch (error) {
281
+ throw new Error(
282
+ `Failed to process header row at index ${rowIndex}.
283
+ Header row locator: ${locatorDescription}
284
+ Columns selector: "${columnsSelector}"
285
+ Error: ${error instanceof Error ? error.message : String(error)}`
286
+ );
287
+ }
100
288
  }
101
289
  this.applyColspanOptions(headerRows, options);
102
290
  this.applyDuplicateSuffix(headerRows, options);
103
291
  return headerRows;
104
292
  }
105
293
  static getDefaultOptions(options) {
106
- var _a, _b, _c, _d, _e, _f, _g;
294
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
107
295
  return {
108
296
  cellContentType: (_a = options == null ? void 0 : options.cellContentType) != null ? _a : "innerText" /* InnerText */,
109
297
  emptyCellReplacement: (_b = options == null ? void 0 : options.emptyCellReplacement) != null ? _b : false,
110
298
  duplicateSuffix: (_c = options == null ? void 0 : options.duplicateSuffix) != null ? _c : false,
299
+ normalizeWhitespace: (_d = options == null ? void 0 : options.normalizeWhitespace) != null ? _d : false,
300
+ stripIconGlyphs: (_e = options == null ? void 0 : options.stripIconGlyphs) != null ? _e : false,
111
301
  colspan: {
112
- enabled: (_e = (_d = options == null ? void 0 : options.colspan) == null ? void 0 : _d.enabled) != null ? _e : false,
113
- suffix: (_g = (_f = options == null ? void 0 : options.colspan) == null ? void 0 : _f.suffix) != null ? _g : false
302
+ enabled: (_g = (_f = options == null ? void 0 : options.colspan) == null ? void 0 : _f.enabled) != null ? _g : false,
303
+ suffix: (_i = (_h = options == null ? void 0 : options.colspan) == null ? void 0 : _h.suffix) != null ? _i : false
114
304
  }
115
305
  };
116
306
  }
117
307
  static async processRow(row, columnsSelector, rowSpans, options) {
118
308
  const headerRow = [];
119
309
  const cells = await row.locator(columnsSelector).all();
310
+ if (cells.length === 0) {
311
+ return headerRow;
312
+ }
313
+ const cellDataArray = await Promise.all(
314
+ cells.map(
315
+ (cell) => this.fetchHeaderCellData(cell, options.cellContentType, options.normalizeWhitespace, options.stripIconGlyphs)
316
+ )
317
+ );
120
318
  let columnIndex = 0;
121
- for (const cell of cells) {
122
- columnIndex = await this.processCell(cell, headerRow, rowSpans, columnIndex, options);
319
+ for (let i = 0; i < cellDataArray.length; i++) {
320
+ columnIndex = this.processHeaderCellData(
321
+ cellDataArray[i],
322
+ headerRow,
323
+ rowSpans,
324
+ columnIndex,
325
+ options.emptyCellReplacement
326
+ );
123
327
  }
124
328
  this.addRemainingRowSpans(headerRow, rowSpans, columnIndex);
125
329
  return headerRow;
126
330
  }
127
- static async processCell(cell, headerRow, rowSpans, columnIndex, options) {
331
+ /**
332
+ * Fetches header cell content and span attributes in parallel.
333
+ * This method performs DOM operations that can be parallelized across multiple cells.
334
+ */
335
+ static async fetchHeaderCellData(cell, cellContentType, normalizeWhitespace, stripIconGlyphs) {
336
+ const [text, spanAttributes] = await Promise.all([
337
+ TableUtils.getCellContent(cell, cellContentType),
338
+ TableUtils.parseSpanAttributes(cell)
339
+ ]);
340
+ return {
341
+ text: this.sanitizeHeaderText(text, normalizeWhitespace, stripIconGlyphs),
342
+ rowspan: spanAttributes.rowspan,
343
+ colspan: spanAttributes.colspan
344
+ };
345
+ }
346
+ static sanitizeHeaderText(text, normalizeWhitespace, stripIconGlyphs) {
347
+ let result = text;
348
+ if (stripIconGlyphs) {
349
+ result = result.replace(this.ICON_GLYPH_PATTERN, "");
350
+ }
351
+ if (normalizeWhitespace) {
352
+ result = result.replace(/\s+/g, " ");
353
+ }
354
+ return result.trim();
355
+ }
356
+ /**
357
+ * Processes pre-fetched header cell data and applies it to the header row.
358
+ * This method is purely computational and doesn't perform any DOM operations.
359
+ */
360
+ static processHeaderCellData(cellData, headerRow, rowSpans, columnIndex, emptyCellReplacement) {
128
361
  while (rowSpans.has(columnIndex)) {
129
362
  headerRow.push(rowSpans.get(columnIndex));
130
363
  rowSpans.delete(columnIndex);
131
364
  columnIndex++;
132
365
  }
133
- let text = await TableUtils.getCellContent(cell, options.cellContentType);
134
- if (!text && options.emptyCellReplacement) {
135
- text = "{{Empty}}";
366
+ let text = cellData.text;
367
+ if (!text && emptyCellReplacement) {
368
+ text = this.EMPTY_CELL_PLACEHOLDER;
136
369
  }
137
- const { colspan, rowspan } = await TableUtils.parseSpanAttributes(cell);
370
+ const { colspan, rowspan } = cellData;
138
371
  headerRow.push(text);
139
372
  this.handleColspan(headerRow, text, colspan);
140
373
  this.handleRowspan(rowSpans, text, columnIndex, colspan, rowspan);
@@ -142,7 +375,7 @@ var TableHeader = class {
142
375
  }
143
376
  static handleColspan(headerRow, text, colspan) {
144
377
  for (let i = 1; i < colspan; i++) {
145
- headerRow.push(`${text}__C${i}`);
378
+ headerRow.push(`${text}${this.COLSPAN_SUFFIX_PREFIX}${i}`);
146
379
  }
147
380
  }
148
381
  static handleRowspan(rowSpans, text, columnIndex, colspan, rowspan) {
@@ -168,20 +401,15 @@ var TableHeader = class {
168
401
  }
169
402
  }
170
403
  static removeColspanCells(headerRows) {
171
- for (const headerRow of headerRows) {
172
- for (let i = 0; i < headerRow.length; i++) {
173
- if (/__C\d/.test(headerRow[i])) {
174
- headerRow.splice(i, 1);
175
- i--;
176
- }
177
- }
404
+ for (let i = 0; i < headerRows.length; i++) {
405
+ headerRows[i] = headerRows[i].filter((cell) => !this.COLSPAN_PATTERN.test(cell));
178
406
  }
179
407
  }
180
408
  static removeColspanSuffix(headerRows) {
181
409
  for (const headerRow of headerRows) {
182
410
  for (let i = 0; i < headerRow.length; i++) {
183
- if (/__C\d/.test(headerRow[i])) {
184
- headerRow[i] = headerRow[i].replace(/__C\d/, "");
411
+ if (this.COLSPAN_PATTERN.test(headerRow[i])) {
412
+ headerRow[i] = headerRow[i].replace(this.COLSPAN_PATTERN, "");
185
413
  }
186
414
  }
187
415
  }
@@ -192,10 +420,10 @@ var TableHeader = class {
192
420
  const headerCountMap = /* @__PURE__ */ new Map();
193
421
  for (let i = 0; i < headerRow.length; i++) {
194
422
  const header = headerRow[i];
195
- if (headerCountMap.has(header)) {
196
- const count = headerCountMap.get(header);
197
- headerRow[i] = `${header}__D${count}`;
198
- headerCountMap.set(header, count + 1);
423
+ const currentCount = headerCountMap.get(header);
424
+ if (currentCount !== void 0) {
425
+ headerRow[i] = `${header}${this.DUPLICATE_SUFFIX_PREFIX}${currentCount}`;
426
+ headerCountMap.set(header, currentCount + 1);
199
427
  } else {
200
428
  headerCountMap.set(header, 1);
201
429
  }
@@ -203,78 +431,11 @@ var TableHeader = class {
203
431
  }
204
432
  }
205
433
  };
206
-
207
- // src/table-wait.ts
208
- import { expect } from "@playwright/test";
209
- var TableWait = class {
210
- static async waitForRows(rowLocator, cellSelector, rowKind, options) {
211
- const validateCellsWithContent = async () => {
212
- const cells = await rowLocator.locator(cellSelector).all();
213
- const cellsWithContent = (await Promise.all(
214
- cells.map(async (cell) => {
215
- const content = await cell.textContent();
216
- return content !== null && content.trim() !== "" ? cell : null;
217
- })
218
- )).filter((cell) => cell !== null);
219
- expect(cellsWithContent.length, `No ${rowKind} cells with content found`).toBeGreaterThan(0);
220
- };
221
- const validateRowAmount = (rows2, expectedAmount) => {
222
- expect(rows2.length, `Expected ${expectedAmount} ${rowKind} rows, but found ${rows2.length}`).toBe(expectedAmount);
223
- };
224
- const validateCellCount = async (rows2, expectedCount) => {
225
- const rowsWithMatchingCells = (await Promise.all(
226
- rows2.map(async (row) => {
227
- const cellCount = await row.locator(cellSelector).count();
228
- return cellCount === expectedCount ? row : null;
229
- })
230
- )).filter((row) => row !== null);
231
- expect(
232
- rowsWithMatchingCells.length,
233
- `Expected amount of ${expectedCount} ${rowKind} cells for row not found`
234
- ).toBeGreaterThan(0);
235
- };
236
- const validateCellContentCount = async (rows2, expectedContentCount) => {
237
- const rowsWithMatchingContent = (await Promise.all(
238
- rows2.map(async (row) => {
239
- const cells = await row.locator(cellSelector).all();
240
- const cellsWithContent = (await Promise.all(
241
- cells.map(async (cell) => {
242
- const content = await cell.textContent();
243
- return content !== null && content.trim() !== "" ? cell : null;
244
- })
245
- )).filter((cell) => cell !== null);
246
- return cellsWithContent.length === expectedContentCount ? row : null;
247
- })
248
- )).filter((row) => row !== null);
249
- expect(
250
- rowsWithMatchingContent.length,
251
- `Expected amount of ${expectedContentCount} ${rowKind} cells with content for row not found`
252
- ).toBeGreaterThan(0);
253
- };
254
- const rows = await rowLocator.all();
255
- if (options == null ? void 0 : options.row) {
256
- if (options.row.amount) {
257
- validateRowAmount(rows, options.row.amount);
258
- } else {
259
- expect(rows.length, `No ${rowKind} rows found`).toBeGreaterThan(0);
260
- }
261
- if (options.row.cell) {
262
- if (options.row.cell.totalCount) {
263
- await validateCellCount(rows, options.row.cell.totalCount);
264
- }
265
- if (options.row.cell.contentCount) {
266
- await validateCellContentCount(rows, options.row.cell.contentCount);
267
- } else {
268
- await validateCellsWithContent();
269
- }
270
- } else {
271
- await validateCellsWithContent();
272
- }
273
- } else {
274
- await validateCellsWithContent();
275
- }
276
- }
277
- };
434
+ TableHeader.EMPTY_CELL_PLACEHOLDER = "{{Empty}}";
435
+ TableHeader.COLSPAN_SUFFIX_PREFIX = "__C";
436
+ TableHeader.DUPLICATE_SUFFIX_PREFIX = "__D";
437
+ TableHeader.COLSPAN_PATTERN = /__C\d+/;
438
+ TableHeader.ICON_GLYPH_PATTERN = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/gu;
278
439
 
279
440
  // src/playwright-table.ts
280
441
  var PlaywrightTable = class {
@@ -309,85 +470,170 @@ var PlaywrightTable = class {
309
470
  constructor(_tableLocator, _options) {
310
471
  this._tableLocator = _tableLocator;
311
472
  this._options = _options;
312
- this._headers = [];
313
- this._rows = [];
314
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
315
- this._headerRowLocator = this._tableLocator.locator((_c = (_b = (_a = this._options) == null ? void 0 : _a.header) == null ? void 0 : _b.rowSelector) != null ? _c : "thead>tr");
316
- this._headerColumnSelector = (_f = (_e = (_d = this._options) == null ? void 0 : _d.header) == null ? void 0 : _e.columnSelector) != null ? _f : "th";
317
- this._bodyRowLocator = this._tableLocator.locator((_i = (_h = (_g = this._options) == null ? void 0 : _g.row) == null ? void 0 : _h.rowSelector) != null ? _i : "tbody>tr");
318
- this._bodyRowColumnSelector = (_l = (_k = (_j = this._options) == null ? void 0 : _j.row) == null ? void 0 : _k.columnSelector) != null ? _l : "td";
473
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
474
+ const headerRowSelector = (_b = (_a = this._options) == null ? void 0 : _a.header) == null ? void 0 : _b.rowSelector;
475
+ const bodyRowSelector = (_d = (_c = this._options) == null ? void 0 : _c.row) == null ? void 0 : _d.rowSelector;
476
+ if (headerRowSelector && bodyRowSelector && headerRowSelector === bodyRowSelector) {
477
+ console.warn(
478
+ `PlaywrightTable: header.rowSelector and row.rowSelector are identical ("${headerRowSelector}"). This can cause header rows to be included in body rows. Consider scoping selectors to separate row groups.`
479
+ );
480
+ }
481
+ this._headerRowLocator = this._tableLocator.locator((_g = (_f = (_e = this._options) == null ? void 0 : _e.header) == null ? void 0 : _f.rowSelector) != null ? _g : "thead>tr");
482
+ this._headerColumnSelector = (_j = (_i = (_h = this._options) == null ? void 0 : _h.header) == null ? void 0 : _i.columnSelector) != null ? _j : "th";
483
+ this._bodyRowLocator = this._tableLocator.locator((_m = (_l = (_k = this._options) == null ? void 0 : _k.row) == null ? void 0 : _l.rowSelector) != null ? _m : "tbody>tr");
484
+ this._bodyRowColumnSelector = (_p = (_o = (_n = this._options) == null ? void 0 : _n.row) == null ? void 0 : _o.columnSelector) != null ? _p : "td";
319
485
  }
320
486
  /**
321
487
  * Retrieves all header rows of the table.
322
- * @param options - Optional parameters for timeout and header row options.
323
- * @param options.timeout - Timeout in milliseconds for loading table data.
324
- * @param options.headerRowOptions - See {@link HeaderRowOptions} for available options.
488
+ * @param options - Optional parameters for header row parsing configuration.
325
489
  * @returns A promise that resolves to an array of header rows.
490
+ * @throws Error if no header rows found after table loads.
491
+ *
492
+ * @example
493
+ * // Get headers with colspan enabled
494
+ * const headers = await table.getHeaderRows({
495
+ * colspan: { enabled: true, suffix: true },
496
+ * duplicateSuffix: true
497
+ * });
498
+ *
499
+ * @see {@link getMainHeaderRow} for getting only the main header row
500
+ * @see {@link HeaderRowOptions} for all available configuration options
326
501
  */
327
502
  async getHeaderRows(options) {
328
- await this.load(options);
329
- return this._headers;
503
+ const table = await this.getTable({
504
+ headerRowOptions: options
505
+ });
506
+ return table.headerRows;
330
507
  }
331
508
  /**
332
509
  * Retrieves the main header row of the table.
333
- * @param options - Optional parameters for timeout and header row options.
334
- * @param options.timeout - Timeout in milliseconds for loading table data.
335
- * @param options.headerRowOptions - See {@link HeaderRowOptions} for available options.
510
+ * By default, uses the last header row if multiple exist. Can be customized via constructor.
511
+ * @param options - Optional parameters for header row parsing configuration.
336
512
  * @returns A promise that resolves to the main header row.
513
+ * @throws Error if no header rows available.
514
+ * @throws Error if setMainHeaderRow index is out of bounds (negative or >= header row count).
515
+ *
516
+ * @example
517
+ * // Get main header with custom index set in constructor
518
+ * const table = new PlaywrightTable(page.locator('table'), {
519
+ * header: { setMainHeaderRow: 0 } // Use first row as main header
520
+ * });
521
+ * const mainHeader = await table.getMainHeaderRow();
522
+ *
523
+ * @see {@link getHeaderRows} for getting all header rows
337
524
  */
338
525
  async getMainHeaderRow(options) {
339
- await this.load(options);
340
- return this.mainHeaderRow();
526
+ const table = await this.getTable({
527
+ headerRowOptions: options
528
+ });
529
+ return this.mainHeaderRow(table.headerRows);
341
530
  }
342
531
  /**
343
532
  * Retrieves all body rows of the table.
344
- * @param options.timout - Timeout in milliseconds for loading table data.
345
- * @param options.bodyRowOptions.cellContentType - The content type for the body rows.
533
+ * @param options - Optional table or body-row configuration.
534
+ * @param options.cellContentType - Legacy shortcut for body row content extraction. See {@link CellContentType}.
535
+ * @param options.bodyRowOptions - Body row parsing options.
536
+ * @param options.headerRowOptions - Header parsing options used when resolving row/colspan layouts.
346
537
  * @returns A promise that resolves to an array of body rows.
538
+ * @throws Error if no body rows found after table loads.
539
+ *
540
+ * @example
541
+ * // Get body rows with TextContent (includes hidden elements)
542
+ * const rows = await table.getBodyRows({
543
+ * cellContentType: CellContentType.TextContent
544
+ * });
545
+ *
546
+ * @example
547
+ * // Get body rows with default InnerText
548
+ * const rows = await table.getBodyRows();
549
+ *
550
+ * @example
551
+ * // Use full table options (for example to sanitize noisy treegrid headers)
552
+ * const rows = await table.getBodyRows({
553
+ * bodyRowOptions: { cellContentType: CellContentType.InnerText },
554
+ * headerRowOptions: { normalizeWhitespace: true, stripIconGlyphs: true }
555
+ * });
347
556
  */
348
557
  async getBodyRows(options) {
349
- await this.load({
350
- timeout: options == null ? void 0 : options.timeout,
351
- bodyRowOptions: options == null ? void 0 : options.bodyRowOptions,
352
- headerRowOptions: {
353
- colspan: { enabled: true }
354
- }
558
+ const tableOptions = this.normalizeGetBodyRowsOptions(options);
559
+ const table = await this.getTable({
560
+ bodyRowOptions: tableOptions.bodyRowOptions,
561
+ headerRowOptions: this.resolveHeaderRowOptions(tableOptions.headerRowOptions, {
562
+ forceColspanEnabled: true
563
+ })
355
564
  });
356
- return this._rows;
565
+ return table.bodyRows;
357
566
  }
358
567
  /**
359
- * Retrieves a specific cell locator in the body of the table.
568
+ * Retrieves a specific cell locator in the body of the table by row and column index.
360
569
  * @param rowNumber - The row number (0-based index).
361
- * @param headerPosition - The header position (0-based index).
570
+ * @param headerPosition - The column/header position (0-based index).
362
571
  * @returns A Playwright Locator for the specified cell.
572
+ *
573
+ * @example
574
+ * // Get cell at row 1, column 2
575
+ * const cellLocator = table.getBodyCellLocator(1, 2);
576
+ * await expect(cellLocator).toHaveText('Expected Text');
577
+ *
578
+ * @example
579
+ * // Get first cell in first row
580
+ * const firstCell = table.getBodyCellLocator(0, 0);
581
+ * await firstCell.click();
582
+ *
583
+ * @see {@link getBodyCellLocatorByRowConditions} for finding cells by content matching
584
+ * @see {@link getAllBodyCellLocatorsByHeaderIndex} for getting all cells in a column
585
+ * @see {@link getAllBodyCellLocatorsByHeaderName} for getting all cells under a header
363
586
  */
364
587
  getBodyCellLocator(rowNumber, headerPosition) {
365
588
  return this._bodyRowLocator.nth(rowNumber).locator(this._bodyRowColumnSelector).nth(headerPosition);
366
589
  }
367
590
  /**
368
591
  * Retrieves a cell locator in the body of the table based on row conditions and a target header.
369
- * @param conditions - A record of header names and their expected values.
370
- * @param targetHeader - The target header name for the desired cell.
371
- * @param options - See {@link LoadOptions} for available options.
592
+ * Finds the first row where all conditions match, then returns the cell in the target column.
593
+ * @param conditions - An object where keys are header names and values are expected cell contents, or a single [header, value] tuple.
594
+ * @param targetHeader - The header name of the column containing the cell to retrieve.
595
+ * @param options - Optional table loading options. See {@link TableOptions}.
372
596
  * @returns A promise that resolves to a Playwright Locator for the matching cell.
373
- * @throws An error if no matching row is found or if a header is not found.
597
+ * @throws Error if no row matches all specified conditions.
598
+ * @throws Error if any condition header or target header is not found in the table.
599
+ *
600
+ * @example
601
+ * // Find cell with multiple conditions
602
+ * const emailLocator = await table.getBodyCellLocatorByRowConditions(
603
+ * { "First name": "John", "Status": "Active" },
604
+ * "Email"
605
+ * );
606
+ * await expect(emailLocator).toContainText('@example.com');
607
+ *
608
+ * @example
609
+ * // Find cell with single condition using tuple syntax
610
+ * const actionCell = await table.getBodyCellLocatorByRowConditions(
611
+ * ["Username", "john.doe"],
612
+ * "Actions"
613
+ * );
614
+ * await actionCell.locator('button').click();
615
+ *
616
+ * @example
617
+ * // Find cell with single condition using object syntax
618
+ * const statusCell = await table.getBodyCellLocatorByRowConditions(
619
+ * { "Username": "john.doe" },
620
+ * "Status"
621
+ * );
622
+ *
623
+ * @see {@link getBodyCellLocator} for getting cells by direct index
624
+ * @see {@link getAllBodyCellLocatorsByHeaderName} for getting all cells in a column
374
625
  */
375
626
  async getBodyCellLocatorByRowConditions(conditions, targetHeader, options) {
376
627
  var _a;
377
- await this.load(options);
378
- const headers = await this.getMainHeaderRow();
379
- const targetHeaderIndex = headers.indexOf(targetHeader);
380
- if (targetHeaderIndex === -1) {
381
- throw new Error(`Header "${targetHeader}" not found.`);
382
- }
383
- for (let rowIndex = 0; rowIndex < this._rows.length; rowIndex++) {
384
- const row = this._rows[rowIndex];
628
+ const table = await this.getTable(options);
629
+ const mainHeader = this.mainHeaderRow(table.headerRows);
630
+ const targetHeaderIndex = this.getHeaderIndex(targetHeader, mainHeader);
631
+ const conditionsObj = Array.isArray(conditions) ? { [conditions[0]]: conditions[1] } : conditions;
632
+ for (let rowIndex = 0; rowIndex < table.bodyRows.length; rowIndex++) {
633
+ const row = table.bodyRows[rowIndex];
385
634
  let matches = true;
386
- for (const [header, value] of Object.entries(conditions)) {
387
- const headerIndex = headers.indexOf(header);
388
- if (headerIndex === -1) {
389
- throw new Error(`Header "${header}" not found.`);
390
- }
635
+ for (const [header, value] of Object.entries(conditionsObj)) {
636
+ const headerIndex = this.getHeaderIndex(header, mainHeader);
391
637
  const cellValue = (_a = row[headerIndex]) != null ? _a : "";
392
638
  if (cellValue !== value) {
393
639
  matches = false;
@@ -398,24 +644,42 @@ var PlaywrightTable = class {
398
644
  return this.getBodyCellLocator(rowIndex, targetHeaderIndex);
399
645
  }
400
646
  }
401
- throw new Error(`No row found matching conditions: ${JSON.stringify(conditions)}`);
647
+ throw new Error(
648
+ `No row found matching conditions: ${JSON.stringify(conditionsObj)}
649
+ Target header: "${targetHeader}"
650
+ Searched ${table.bodyRows.length} rows
651
+ Body row locator: ${this._bodyRowLocator.toString()}`
652
+ );
402
653
  }
403
654
  /**
404
655
  * Retrieves all cell locators in the body of the table for a specific header name.
405
- * @param header - The header name.
406
- * @param options - See {@link LoadOptions} for available options.
407
- * @returns A promise that resolves to an array of Playwright Locators for the cells.
408
- * @throws An error if the header is not found.
656
+ * Returns locators for every cell in the column identified by the header.
657
+ * @param header - The header name identifying the column.
658
+ * @param options - Optional table loading options. See {@link TableOptions}.
659
+ * @returns A promise that resolves to an array of Playwright Locators for all cells in the column.
660
+ * @throws Error if the header is not found in the table.
661
+ *
662
+ * @example
663
+ * // Get all cells in the "Status" column
664
+ * const statusCells = await table.getAllBodyCellLocatorsByHeaderName("Status");
665
+ * for (const cell of statusCells) {
666
+ * await expect(cell).toHaveText(/Active|Inactive/);
667
+ * }
668
+ *
669
+ * @example
670
+ * // Get text from all cells in "Name" column
671
+ * const nameCells = await table.getAllBodyCellLocatorsByHeaderName("Name");
672
+ * const names = await Promise.all(nameCells.map(cell => cell.textContent()));
673
+ *
674
+ * @see {@link getAllBodyCellLocatorsByHeaderIndex} for getting cells by column index
675
+ * @see {@link getBodyCellLocatorByRowConditions} for finding specific cells by content
409
676
  */
410
677
  async getAllBodyCellLocatorsByHeaderName(header, options) {
411
- await this.load(options);
412
- const headers = await this.getMainHeaderRow();
413
- const headerIndex = headers.indexOf(header);
414
- if (headerIndex === -1) {
415
- throw new Error(`Header "${header}" not found.`);
416
- }
678
+ const table = await this.getTable(options);
679
+ const headers = this.mainHeaderRow(table.headerRows);
680
+ const headerIndex = this.getHeaderIndex(header, headers);
417
681
  const locators = [];
418
- for (let rowIndex = 0; rowIndex < this._rows.length; rowIndex++) {
682
+ for (let rowIndex = 0; rowIndex < table.bodyRows.length; rowIndex++) {
419
683
  const cellLocator = this.getBodyCellLocator(rowIndex, headerIndex);
420
684
  locators.push(cellLocator);
421
685
  }
@@ -423,40 +687,79 @@ var PlaywrightTable = class {
423
687
  }
424
688
  /**
425
689
  * Retrieves all cell locators in the body of the table for a specific header index.
426
- * @param headerIndex - The header index (0-based).
427
- * @param options - See {@link LoadOptions} for available options.
428
- * @returns A promise that resolves to an array of Playwright Locators for the cells.
690
+ * Returns locators for every cell in the column at the specified position.
691
+ * @param headerIndex - The header/column index (0-based).
692
+ * @param options - Optional table loading options. See {@link TableOptions}.
693
+ * @returns A promise that resolves to an array of Playwright Locators for all cells in the column.
694
+ * @throws Error if the header index is out of bounds (negative or >= column count).
695
+ *
696
+ * @example
697
+ * // Get all cells in the third column (index 2)
698
+ * const thirdColumnCells = await table.getAllBodyCellLocatorsByHeaderIndex(2);
699
+ * for (const cell of thirdColumnCells) {
700
+ * await expect(cell).toBeVisible();
701
+ * }
702
+ *
703
+ * @example
704
+ * // Get all cells in first column
705
+ * const firstColumnCells = await table.getAllBodyCellLocatorsByHeaderIndex(0);
706
+ * const values = await Promise.all(firstColumnCells.map(c => c.textContent()));
707
+ *
708
+ * @see {@link getAllBodyCellLocatorsByHeaderName} for getting cells by header name
709
+ * @see {@link getBodyCellLocator} for getting a specific cell by row and column index
429
710
  */
430
711
  async getAllBodyCellLocatorsByHeaderIndex(headerIndex, options) {
431
- await this.load(options);
712
+ const table = await this.getTable(options);
713
+ const columnCount = this.mainHeaderRow(table.headerRows).length;
714
+ if (headerIndex < 0 || headerIndex >= columnCount) {
715
+ throw new Error(
716
+ `Header index ${headerIndex} out of bounds. Table has ${columnCount} columns.
717
+ Header row locator: ${this._headerRowLocator.toString()}`
718
+ );
719
+ }
432
720
  const locators = [];
433
- for (let rowIndex = 0; rowIndex < this._rows.length; rowIndex++) {
721
+ for (let rowIndex = 0; rowIndex < table.bodyRows.length; rowIndex++) {
434
722
  const cellLocator = this.getBodyCellLocator(rowIndex, headerIndex);
435
723
  locators.push(cellLocator);
436
724
  }
437
725
  return locators;
438
726
  }
439
727
  /**
440
- * Converts the table data into a JSON object.
441
- * @param options - See {@link LoadOptions} for available options.
442
- * @returns A promise that resolves to a JSON representation of the table.
728
+ * Converts the table data into a JSON array where each object represents a row.
729
+ * Keys are header names, values are cell contents.
730
+ * @param options - Optional table loading options. See {@link TableOptions}.
731
+ * @returns A promise that resolves to an array of objects representing table rows.
732
+ * @throws Error if no header rows or body rows found.
733
+ *
734
+ * @example
735
+ * // Convert table to JSON with default options
736
+ * const data = await table.getJson();
737
+ * // Result: [{"Name": "John", "Age": "30"}, {"Name": "Jane", "Age": "25"}]
738
+ *
739
+ * @example
740
+ * // Convert with colspan enabled and duplicate suffix
741
+ * const data = await table.getJson({
742
+ * headerRowOptions: {
743
+ * colspan: { enabled: true, suffix: true },
744
+ * duplicateSuffix: true,
745
+ * emptyCellReplacement: true
746
+ * }
747
+ * });
443
748
  */
444
749
  async getJson(options) {
445
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
446
- await this.load({
447
- timeout: options == null ? void 0 : options.timeout,
750
+ const table = await this.getTable({
448
751
  bodyRowOptions: options == null ? void 0 : options.bodyRowOptions,
449
- headerRowOptions: {
450
- colspan: {
451
- enabled: (_c = (_b = (_a = options == null ? void 0 : options.headerRowOptions) == null ? void 0 : _a.colspan) == null ? void 0 : _b.enabled) != null ? _c : true,
452
- suffix: (_f = (_e = (_d = options == null ? void 0 : options.headerRowOptions) == null ? void 0 : _d.colspan) == null ? void 0 : _e.suffix) != null ? _f : true
453
- },
454
- duplicateSuffix: (_h = (_g = options == null ? void 0 : options.headerRowOptions) == null ? void 0 : _g.duplicateSuffix) != null ? _h : true,
455
- emptyCellReplacement: (_j = (_i = options == null ? void 0 : options.headerRowOptions) == null ? void 0 : _i.emptyCellReplacement) != null ? _j : true
456
- }
752
+ headerRowOptions: this.resolveHeaderRowOptions(options == null ? void 0 : options.headerRowOptions, {
753
+ defaultColspanEnabled: true,
754
+ defaultColspanSuffix: true,
755
+ defaultDuplicateSuffix: true,
756
+ defaultEmptyCellReplacement: true,
757
+ defaultNormalizeWhitespace: false,
758
+ defaultStripIconGlyphs: false
759
+ })
457
760
  });
458
- const headers = this.mainHeaderRow();
459
- return this._rows.map((row) => {
761
+ const headers = this.mainHeaderRow(table.headerRows);
762
+ return table.bodyRows.map((row) => {
460
763
  const rowObj = {};
461
764
  headers.forEach((header, index) => {
462
765
  rowObj[header] = row[index] || "";
@@ -465,44 +768,322 @@ var PlaywrightTable = class {
465
768
  });
466
769
  }
467
770
  /**
468
- * Waits for the header rows to be loaded.
469
- * @param options - See {@link WaitForTableRowsOptions} for available options.
470
- * @returns A promise that resolves when the header rows are loaded.
771
+ * Waits for the table body to be empty (no body rows).
772
+ * @param options - Polling options (timeout, interval, retries).
773
+ * @returns A promise that resolves when table body is empty.
774
+ * @throws Error if table is not empty within timeout period.
775
+ *
776
+ * @example
777
+ * // Wait for table to clear after delete operation
778
+ * await table.waitForEmpty({ timeout: 5000 });
779
+ *
780
+ * @see {@link waitForNonEmpty} for waiting until table has data
781
+ *
782
+ * @remarks
783
+ * **Alternative:** Consider using Playwright's `expect.poll` for more flexibility:
784
+ * ```ts
785
+ * await expect.poll(async () => (await table.getBodyRows()).length).toBe(0);
786
+ * ```
471
787
  */
472
- async waitForHeaderRows(options) {
473
- await expect2(
474
- async () => await TableWait.waitForRows(this._headerRowLocator, this._headerColumnSelector, "header" /* Header */, options)
475
- ).toPass({
476
- timeout: options == null ? void 0 : options.timeout
477
- });
788
+ async waitForEmpty(options) {
789
+ await pollTable(async () => {
790
+ const rows = await this._bodyRowLocator.all();
791
+ if (rows.length > 0) {
792
+ throw new Error(
793
+ `Expected table to be empty, but found ${rows.length} body rows.
794
+ Body row locator: ${this._bodyRowLocator.toString()}`
795
+ );
796
+ }
797
+ }, options);
798
+ }
799
+ /**
800
+ * Waits for the table body to be non-empty (at least one body row with content).
801
+ * A row is considered valid if it has at least one cell containing text.
802
+ * @param options - Polling options (timeout, interval, retries).
803
+ * @returns A promise that resolves when table has at least one body row with content.
804
+ * @throws Error if table remains empty or has no rows with content within timeout period.
805
+ *
806
+ * @example
807
+ * // Wait for table to populate after search
808
+ * await table.waitForNonEmpty({ timeout: 5000 });
809
+ *
810
+ * @see {@link waitForEmpty} for waiting until table is cleared
811
+ *
812
+ * @remarks
813
+ * **Alternative:** Consider using Playwright's `expect.poll` for more flexibility:
814
+ * ```ts
815
+ * await expect.poll(async () => (await table.getBodyRows()).length).toBeGreaterThan(0);
816
+ * ```
817
+ */
818
+ async waitForNonEmpty(options) {
819
+ await pollTable(async () => {
820
+ const rows = await this._bodyRowLocator.all();
821
+ if (rows.length === 0) {
822
+ throw new Error(
823
+ `Expected table to be non-empty, but found 0 body rows.
824
+ Body row locator: ${this._bodyRowLocator.toString()}`
825
+ );
826
+ }
827
+ let hasRowWithContent = false;
828
+ for (const row of rows) {
829
+ const cells = await row.locator(this._bodyRowColumnSelector).all();
830
+ for (const cell of cells) {
831
+ const content = await cell.innerText();
832
+ if (content.trim().length > 0) {
833
+ hasRowWithContent = true;
834
+ break;
835
+ }
836
+ }
837
+ if (hasRowWithContent) break;
838
+ }
839
+ if (!hasRowWithContent) {
840
+ throw new Error(
841
+ `Expected table to have at least one row with content, but all ${rows.length} rows are empty.
842
+ Body row locator: ${this._bodyRowLocator.toString()}`
843
+ );
844
+ }
845
+ }, options);
846
+ }
847
+ /**
848
+ * Waits for the table body to have exactly N rows.
849
+ * @param count - The exact number of body rows expected.
850
+ * @param options - Polling options (timeout, interval, retries).
851
+ * @returns A promise that resolves when the table has exactly the specified number of rows.
852
+ * @throws Error if row count doesn't match within timeout period.
853
+ *
854
+ * @example
855
+ * // Wait for exactly 10 rows after pagination
856
+ * await table.waitForExactRowCount(10, { timeout: 5000 });
857
+ *
858
+ * @example
859
+ * // Verify table has exactly 0 rows (alternative to waitForEmpty)
860
+ * await table.waitForExactRowCount(0);
861
+ *
862
+ * @see {@link getRowCount} for getting current row counts
863
+ *
864
+ * @remarks
865
+ * **Alternative:** Consider using Playwright's `expect.poll` for more flexibility:
866
+ * ```ts
867
+ * await expect.poll(async () => (await table.getBodyRows()).length).toBe(10);
868
+ * ```
869
+ */
870
+ async waitForExactRowCount(count, options) {
871
+ if (count < 0 || !Number.isInteger(count)) {
872
+ throw new Error(`Row count must be a non-negative integer, got: ${count}`);
873
+ }
874
+ await pollTable(async () => {
875
+ const rows = await this._bodyRowLocator.all();
876
+ if (rows.length !== count) {
877
+ throw new Error(
878
+ `Expected exactly ${count} rows, but found ${rows.length}.
879
+ Body row locator: ${this._bodyRowLocator.toString()}`
880
+ );
881
+ }
882
+ }, options);
478
883
  }
479
884
  /**
480
- * Waits for the body rows to be loaded.
481
- * @param options - See {@link WaitForTableRowsOptions} for available options.
482
- * @returns A promise that resolves when the body rows are loaded.
885
+ * Gets the count of header and body rows in the table.
886
+ * Lightweight method that doesn't fetch cell data, only counts rows.
887
+ * @returns A promise that resolves to an object with header and body row counts.
888
+ *
889
+ * @example
890
+ * // Get current row counts
891
+ * const counts = await table.getRowCount();
892
+ * console.log(`Headers: ${counts.header}, Body: ${counts.body}`);
893
+ *
894
+ * @example
895
+ * // Assert specific row count
896
+ * const { body } = await table.getRowCount();
897
+ * expect(body).toBe(10);
483
898
  */
484
- async waitForBodyRows(options) {
485
- await expect2(
486
- async () => await TableWait.waitForRows(this._bodyRowLocator, this._bodyRowColumnSelector, "body" /* Body */, options)
487
- ).toPass({
488
- timeout: options == null ? void 0 : options.timeout
899
+ async getRowCount() {
900
+ const [headerRows, bodyRows] = await Promise.all([this._headerRowLocator.all(), this._bodyRowLocator.all()]);
901
+ return {
902
+ header: headerRows.length,
903
+ body: bodyRows.length
904
+ };
905
+ }
906
+ /**
907
+ * Gets all distinct (unique) values from a specific column.
908
+ * Returns a sorted array of unique string values, excluding empty values.
909
+ * @param headerName - The header name of the column to extract distinct values from.
910
+ * @param options - Optional table loading options. See {@link TableOptions}.
911
+ * @returns A promise that resolves to a sorted array of distinct values.
912
+ * @throws Error if the specified header is not found in the table.
913
+ *
914
+ * @example
915
+ * // Get all unique status values
916
+ * const statuses = await table.getDistinctColumnValues("Status");
917
+ * // Result: ["Active", "Inactive", "Pending"]
918
+ *
919
+ * @example
920
+ * // Get distinct countries for filtering
921
+ * const countries = await table.getDistinctColumnValues("Country");
922
+ * console.log(`Available countries: ${countries.join(", ")}`);
923
+ *
924
+ * @example
925
+ * // Verify expected values exist
926
+ * const roles = await table.getDistinctColumnValues("Role");
927
+ * expect(roles).toContain("Admin");
928
+ * expect(roles).toHaveLength(3);
929
+ *
930
+ * @see {@link getAllBodyCellLocatorsByHeaderName} for getting all cell locators in a column
931
+ * @see {@link getJson} for getting full table data
932
+ */
933
+ async getDistinctColumnValues(headerName, options) {
934
+ var _a;
935
+ const table = await this.getTable(options);
936
+ const mainHeader = this.mainHeaderRow(table.headerRows);
937
+ const headerIndex = this.getHeaderIndex(headerName, mainHeader);
938
+ const values = /* @__PURE__ */ new Set();
939
+ for (const row of table.bodyRows) {
940
+ const value = String((_a = row[headerIndex]) != null ? _a : "").trim();
941
+ if (value) values.add(value);
942
+ }
943
+ return Array.from(values).sort();
944
+ }
945
+ /**
946
+ * Finds the index of the first body row matching the specified conditions.
947
+ * Returns -1 if no matching row is found.
948
+ * @param conditions - Record of header names and expected cell values to match.
949
+ * @param options - Optional table loading options. See {@link TableOptions}.
950
+ * @returns A promise that resolves to the 0-based index of the first matching row, or -1 if not found.
951
+ * @throws Error if specified headers don't exist in the table.
952
+ *
953
+ * @example
954
+ * // Find index of row where Status is "Active"
955
+ * const index = await table.findRowIndex({ Status: "Active" });
956
+ * if (index >= 0) {
957
+ * console.log(`Found at row ${index}`);
958
+ * }
959
+ *
960
+ * @example
961
+ * // Find row and interact with it
962
+ * const rowIndex = await table.findRowIndex({ Username: "john.doe" });
963
+ * if (rowIndex >= 0) {
964
+ * const cell = table.getBodyCellLocator(rowIndex, 0);
965
+ * await cell.click();
966
+ * }
967
+ *
968
+ * @see {@link getBodyCellLocatorByRowConditions} for getting cell locators from matching rows
969
+ * @see {@link waitForRowByConditions} for waiting until matching row appears
970
+ */
971
+ async findRowIndex(conditions, options) {
972
+ var _a;
973
+ const table = await this.getTable({
974
+ headerRowOptions: this.resolveHeaderRowOptions(options == null ? void 0 : options.headerRowOptions, {
975
+ forceColspanEnabled: true
976
+ }),
977
+ bodyRowOptions: options == null ? void 0 : options.bodyRowOptions
489
978
  });
979
+ const mainHeader = this.mainHeaderRow(table.headerRows);
980
+ const headerIndices = /* @__PURE__ */ new Map();
981
+ for (const header of Object.keys(conditions)) {
982
+ headerIndices.set(header, this.getHeaderIndex(header, mainHeader));
983
+ }
984
+ for (let rowIndex = 0; rowIndex < table.bodyRows.length; rowIndex++) {
985
+ const row = table.bodyRows[rowIndex];
986
+ let rowMatches = true;
987
+ for (const [header, expectedValue] of Object.entries(conditions)) {
988
+ const headerIndex = headerIndices.get(header);
989
+ const cellValue = String((_a = row[headerIndex]) != null ? _a : "");
990
+ if (cellValue !== expectedValue) {
991
+ rowMatches = false;
992
+ break;
993
+ }
994
+ }
995
+ if (rowMatches) {
996
+ return rowIndex;
997
+ }
998
+ }
999
+ return -1;
490
1000
  }
491
- async load(options) {
492
- await expect2(async () => {
493
- await TableWait.waitForRows(this._headerRowLocator, this._headerColumnSelector, "header" /* Header */);
494
- await TableWait.waitForRows(this._bodyRowLocator, this._bodyRowColumnSelector, "body" /* Body */);
495
- }).toPass({ timeout: options == null ? void 0 : options.timeout });
496
- this._headers = await TableHeader.getRows(
497
- this._headerRowLocator,
498
- this._headerColumnSelector,
499
- options == null ? void 0 : options.headerRowOptions
500
- );
501
- this._rows = await TableBody.getRows(this._bodyRowLocator, this._bodyRowColumnSelector, options == null ? void 0 : options.bodyRowOptions);
1001
+ async getTable(options) {
1002
+ const [headerRows, bodyRows] = await Promise.all([
1003
+ TableHeader.getRows(this._headerRowLocator, this._headerColumnSelector, options == null ? void 0 : options.headerRowOptions),
1004
+ TableBody.getRows(this._bodyRowLocator, this._bodyRowColumnSelector, options == null ? void 0 : options.bodyRowOptions)
1005
+ ]);
1006
+ if (headerRows.length === 0) {
1007
+ throw new Error(
1008
+ `No header rows found, is table loaded completely?
1009
+ Header row locator: ${this._headerRowLocator.toString()}
1010
+ Table locator: ${this._tableLocator.toString()}`
1011
+ );
1012
+ }
1013
+ if (bodyRows.length === 0) {
1014
+ throw new Error(
1015
+ `No body rows found, is table loaded completely?
1016
+ Body row locator: ${this._bodyRowLocator.toString()}
1017
+ Table locator: ${this._tableLocator.toString()}`
1018
+ );
1019
+ }
1020
+ return { headerRows, bodyRows };
1021
+ }
1022
+ normalizeGetBodyRowsOptions(options) {
1023
+ if (!options) {
1024
+ return {};
1025
+ }
1026
+ if (options.bodyRowOptions || options.headerRowOptions) {
1027
+ return options;
1028
+ }
1029
+ if (options.cellContentType !== void 0) {
1030
+ return {
1031
+ bodyRowOptions: {
1032
+ cellContentType: options.cellContentType
1033
+ }
1034
+ };
1035
+ }
1036
+ return options;
502
1037
  }
503
- mainHeaderRow() {
504
- var _a, _b;
505
- return ((_b = (_a = this._options) == null ? void 0 : _a.header) == null ? void 0 : _b.setMainHeaderRow) ? this._headers[this._options.header.setMainHeaderRow] : this._headers[this._headers.length - 1];
1038
+ resolveHeaderRowOptions(headerOptions, defaults) {
1039
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1040
+ const resolvedColspanEnabled = (defaults == null ? void 0 : defaults.forceColspanEnabled) ? true : (_b = (_a = headerOptions == null ? void 0 : headerOptions.colspan) == null ? void 0 : _a.enabled) != null ? _b : defaults == null ? void 0 : defaults.defaultColspanEnabled;
1041
+ return {
1042
+ cellContentType: headerOptions == null ? void 0 : headerOptions.cellContentType,
1043
+ emptyCellReplacement: (_c = headerOptions == null ? void 0 : headerOptions.emptyCellReplacement) != null ? _c : defaults == null ? void 0 : defaults.defaultEmptyCellReplacement,
1044
+ duplicateSuffix: (_d = headerOptions == null ? void 0 : headerOptions.duplicateSuffix) != null ? _d : defaults == null ? void 0 : defaults.defaultDuplicateSuffix,
1045
+ normalizeWhitespace: (_e = headerOptions == null ? void 0 : headerOptions.normalizeWhitespace) != null ? _e : defaults == null ? void 0 : defaults.defaultNormalizeWhitespace,
1046
+ stripIconGlyphs: (_f = headerOptions == null ? void 0 : headerOptions.stripIconGlyphs) != null ? _f : defaults == null ? void 0 : defaults.defaultStripIconGlyphs,
1047
+ colspan: {
1048
+ enabled: resolvedColspanEnabled,
1049
+ suffix: (_h = (_g = headerOptions == null ? void 0 : headerOptions.colspan) == null ? void 0 : _g.suffix) != null ? _h : defaults == null ? void 0 : defaults.defaultColspanSuffix
1050
+ }
1051
+ };
1052
+ }
1053
+ mainHeaderRow(headers) {
1054
+ var _a, _b, _c;
1055
+ if (headers.length === 0) {
1056
+ throw new Error(
1057
+ `No header rows available. Ensure table has been loaded.
1058
+ Header row locator: ${this._headerRowLocator.toString()}`
1059
+ );
1060
+ }
1061
+ const headerIndex = (_c = (_b = (_a = this._options) == null ? void 0 : _a.header) == null ? void 0 : _b.setMainHeaderRow) != null ? _c : headers.length - 1;
1062
+ if (headerIndex < 0 || headerIndex >= headers.length) {
1063
+ throw new Error(
1064
+ `Header row index ${headerIndex} out of bounds. Table has ${headers.length} header rows.
1065
+ Header row locator: ${this._headerRowLocator.toString()}`
1066
+ );
1067
+ }
1068
+ return headers[headerIndex];
1069
+ }
1070
+ /**
1071
+ * Gets the index of a header in the main header row, throwing an error if not found.
1072
+ * @param header - The header name to find.
1073
+ * @param mainHeader - The main header row to search in.
1074
+ * @returns The 0-based index of the header.
1075
+ * @throws Error if the header is not found.
1076
+ */
1077
+ getHeaderIndex(header, mainHeader) {
1078
+ const index = mainHeader.indexOf(header);
1079
+ if (index === -1) {
1080
+ throw new Error(
1081
+ `Header "${header}" not found.
1082
+ Available headers: [${mainHeader.join(", ")}]
1083
+ Header row locator: ${this._headerRowLocator.toString()}`
1084
+ );
1085
+ }
1086
+ return index;
506
1087
  }
507
1088
  };
508
1089
  export {