@cerios/playwright-table 1.1.1 → 2.0.0

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