@rickcedwhat/playwright-smart-table 6.17.1 → 6.19.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/useTable.js CHANGED
@@ -27,6 +27,7 @@ const filterEngine_1 = require("./filterEngine");
27
27
  const tableMapper_1 = require("./engine/tableMapper");
28
28
  const rowFinder_1 = require("./engine/rowFinder");
29
29
  const tableIteration_1 = require("./engine/tableIteration");
30
+ const rowResolution_1 = require("./engine/rowResolution");
30
31
  const debugUtils_1 = require("./utils/debugUtils");
31
32
  const smartRowArray_1 = require("./utils/smartRowArray");
32
33
  const elementTracker_1 = require("./utils/elementTracker");
@@ -69,9 +70,11 @@ const useTable = (rootLocator, configOptions = {}) => {
69
70
  const vp = config.strategies.viewport;
70
71
  let colRangeCache = null;
71
72
  let rowRangeCache = null;
73
+ let rowIndicesCache = null;
72
74
  _clearViewportCache = () => {
73
75
  colRangeCache = null;
74
76
  rowRangeCache = null;
77
+ rowIndicesCache = null;
75
78
  };
76
79
  config.strategies.viewport = Object.assign(Object.assign({}, vp), { getVisibleColumnRange: vp.getVisibleColumnRange
77
80
  ? async (ctx) => {
@@ -85,15 +88,23 @@ const useTable = (rootLocator, configOptions = {}) => {
85
88
  rowRangeCache = await vp.getVisibleRowRange(ctx);
86
89
  return rowRangeCache;
87
90
  }
91
+ : undefined, getVisibleRowIndices: vp.getVisibleRowIndices
92
+ ? async (ctx) => {
93
+ if (!rowIndicesCache)
94
+ rowIndicesCache = await vp.getVisibleRowIndices(ctx);
95
+ return rowIndicesCache;
96
+ }
88
97
  : undefined, scrollToColumn: vp.scrollToColumn
89
98
  ? async (ctx, colIndex) => {
90
99
  colRangeCache = null;
91
100
  rowRangeCache = null;
101
+ rowIndicesCache = null;
92
102
  await vp.scrollToColumn(ctx, colIndex);
93
103
  }
94
104
  : undefined, scrollToRow: vp.scrollToRow
95
105
  ? async (ctx, rowIndex) => {
96
106
  rowRangeCache = null;
107
+ rowIndicesCache = null;
97
108
  await vp.scrollToRow(ctx, rowIndex);
98
109
  }
99
110
  : undefined });
@@ -137,7 +148,7 @@ const useTable = (rootLocator, configOptions = {}) => {
137
148
  const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier) => {
138
149
  return (0, smartRow_1.default)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
139
150
  };
140
- const tableState = { currentPageIndex: 0 };
151
+ const tableState = { currentPageIndex: 0, empty: false };
141
152
  const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart, tableState, (useBulk) => _advancePage(useBulk));
142
153
  /** Builds a full TableContext/StrategyContext with getHeaderCell, getHeaders, scrollToColumn. Set after result is created. */
143
154
  let createStrategyContext = () => ({ root: rootLocator, config, page: rootLocator.page(), resolve });
@@ -215,7 +226,7 @@ const useTable = (rootLocator, configOptions = {}) => {
215
226
  set currentPageIndex(v) { tableState.currentPageIndex = v; },
216
227
  init: async (options) => {
217
228
  var _a;
218
- if (tableMapper.isInitialized())
229
+ if (tableMapper.isInitialized() || tableState.empty)
219
230
  return result;
220
231
  if (config.strategies.sorting)
221
232
  (0, validation_1.validateSortingStrategy)(config.strategies.sorting);
@@ -223,7 +234,21 @@ const useTable = (rootLocator, configOptions = {}) => {
223
234
  (0, validation_1.validateFillStrategy)(config.strategies.fill);
224
235
  (0, debugUtils_1.warnIfDebugInCI)(config);
225
236
  (0, debugUtils_1.logDebug)(config, 'info', 'Initializing table');
226
- const map = await tableMapper.getMap(options === null || options === void 0 ? void 0 : options.timeout);
237
+ let map;
238
+ try {
239
+ map = await tableMapper.getMap(options === null || options === void 0 ? void 0 : options.timeout);
240
+ }
241
+ catch (headerError) {
242
+ if (config.emptyState) {
243
+ const visible = await config.emptyState.isVisible().catch(() => false);
244
+ if (visible) {
245
+ tableState.empty = true;
246
+ (0, debugUtils_1.logDebug)(config, 'info', 'init: header resolution failed but emptyState locator is visible — table is empty');
247
+ return result;
248
+ }
249
+ }
250
+ throw headerError;
251
+ }
227
252
  (0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${map.size} columns`, Array.from(map.keys()));
228
253
  if ((_a = config.strategies.pagination) === null || _a === void 0 ? void 0 : _a.detectCurrentPage) {
229
254
  try {
@@ -269,6 +294,8 @@ const useTable = (rootLocator, configOptions = {}) => {
269
294
  return resolve(config.headerSelector, rootLocator).nth(idx);
270
295
  },
271
296
  countRows: async () => {
297
+ if (tableState.empty)
298
+ return 0;
272
299
  await _autoInit();
273
300
  const pag = config.strategies.pagination;
274
301
  const hasPagination = config.maxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
@@ -280,6 +307,7 @@ const useTable = (rootLocator, configOptions = {}) => {
280
307
  const tracker = new elementTracker_1.ElementTracker('countRows');
281
308
  let total = 0;
282
309
  let pagesScanned = 1;
310
+ let paginationError;
283
311
  try {
284
312
  while (true) {
285
313
  const newIndices = await tracker.getUnseenIndices(resolve(config.rowSelector, rootLocator));
@@ -292,11 +320,25 @@ const useTable = (rootLocator, configOptions = {}) => {
292
320
  pagesScanned++;
293
321
  }
294
322
  }
323
+ catch (e) {
324
+ paginationError = e;
325
+ throw e;
326
+ }
295
327
  finally {
296
328
  await tracker.cleanup(rootLocator.page());
329
+ // Always restore page 1, even if pagination threw mid-count (#348), so the table
330
+ // stays usable for callers that catch the error. If reset() itself fails, only let
331
+ // it surface when pagination succeeded — otherwise preserve the original error.
332
+ log(`countRows: ${total} total row(s) across ${pagesScanned} page(s) — resetting`);
333
+ try {
334
+ await result.reset();
335
+ }
336
+ catch (resetError) {
337
+ if (paginationError === undefined)
338
+ throw resetError;
339
+ (0, debugUtils_1.logDebug)(config, 'error', 'countRows: reset() failed after pagination error', resetError);
340
+ }
297
341
  }
298
- log(`countRows: ${total} total row(s) across ${pagesScanned} page(s) — resetting`);
299
- await result.reset();
300
342
  return total;
301
343
  },
302
344
  mapColumn: async (columnName, options = {}) => {
@@ -311,7 +353,10 @@ const useTable = (rootLocator, configOptions = {}) => {
311
353
  await cell.bringIntoView();
312
354
  const columnOverride = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[columnName];
313
355
  if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
314
- return await columnOverride.read(cell);
356
+ return await columnOverride.read(cell, {
357
+ row, columnName, columnIndex: map.get(columnName),
358
+ getCell: (name) => row.getCell(name),
359
+ });
315
360
  }
316
361
  const text = await cell.innerText();
317
362
  return (text || '').trim();
@@ -334,6 +379,7 @@ const useTable = (rootLocator, configOptions = {}) => {
334
379
  log("No goToFirst strategy configured. Table may not be on page 1.");
335
380
  }
336
381
  tableState.currentPageIndex = 0;
382
+ tableState.empty = false;
337
383
  tableMapper.clear();
338
384
  log("Table reset complete. Calling autoInit to restore state.");
339
385
  await _autoInit();
@@ -341,6 +387,7 @@ const useTable = (rootLocator, configOptions = {}) => {
341
387
  revalidate: async () => {
342
388
  log("Revalidating table structure...");
343
389
  await tableMapper.remapHeaders();
390
+ tableState.empty = false;
344
391
  log("Table revalidated.");
345
392
  },
346
393
  getRow: (filters, options = { exact: false }) => {
@@ -360,6 +407,55 @@ const useTable = (rootLocator, configOptions = {}) => {
360
407
  const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
361
408
  return _makeSmart(rowLocator, map, index);
362
409
  },
410
+ findRowByIndex: async (index, options) => {
411
+ var _a, _b;
412
+ log(`findRowByIndex: index=${index} options=${safeStringify(options)}`);
413
+ await _autoInit();
414
+ const map = tableMapper.getMapSync();
415
+ const resolveRI = config.strategies.resolveRowIndex;
416
+ const viewport = config.strategies.viewport;
417
+ // Identifying a row by its logical index requires a resolveRowIndex strategy. Without one
418
+ // the library can only offer render-window-relative getRowByIndex, or filter-based findRow.
419
+ if (!resolveRI) {
420
+ throw new Error(`findRowByIndex(${index}) requires a strategies.resolveRowIndex to identify rows by their logical index. ` +
421
+ `Configure one (e.g. read the grid's row-index attribute), or use getRowByIndex() (current render window) / findRow(filters).`);
422
+ }
423
+ // The currently-mounted row whose logical index === index, if any.
424
+ const findMounted = async () => {
425
+ const rows = await resolve(config.rowSelector, rootLocator).all();
426
+ for (const r of rows) {
427
+ if ((await resolveRI(r)) === index)
428
+ return r;
429
+ }
430
+ return null;
431
+ };
432
+ // 1. Already mounted (no navigation needed)?
433
+ let loc = await findMounted();
434
+ // 2. Fast path: jump straight to it via the viewport's random-access scroll.
435
+ if (!loc && (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow)) {
436
+ log(`findRowByIndex: scrolling to row ${index}`);
437
+ await viewport.scrollToRow(createStrategyContext(), index);
438
+ loc = await findMounted();
439
+ }
440
+ // 3. Fallback: advance pages (on infinite-scroll tables, a "page" is a scroll step) until
441
+ // the row is reached. Bounded by maxPages so an unreachable index can't loop forever.
442
+ if (!loc && config.strategies.pagination) {
443
+ const effectiveMaxPages = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
444
+ let pagesScanned = 1;
445
+ while (!loc && pagesScanned < effectiveMaxPages) {
446
+ if (!await _advancePage(false))
447
+ break;
448
+ pagesScanned++;
449
+ loc = await findMounted();
450
+ }
451
+ }
452
+ if (!loc) {
453
+ throw new Error(`findRowByIndex(${index}): could not reach row ${index}. ` +
454
+ `Ensure a viewport.scrollToRow or pagination can bring it into view (searched up to maxPages=${(_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages}).`);
455
+ }
456
+ await (0, debugUtils_1.debugDelay)(config, 'findRow');
457
+ return _makeSmart(loc, map, index, tableState.currentPageIndex);
458
+ },
363
459
  findRow: async (filters, options) => {
364
460
  log(`findRow: filters=${safeStringify(filters)} options=${safeStringify(options)}`);
365
461
  return rowFinder.findRow(filters, options);
@@ -369,10 +465,13 @@ const useTable = (rootLocator, configOptions = {}) => {
369
465
  return rowFinder.findRows(filters !== null && filters !== void 0 ? filters : {}, options);
370
466
  },
371
467
  isInitialized: () => {
372
- const initialized = tableMapper.isInitialized();
468
+ const initialized = tableMapper.isInitialized() || tableState.empty;
373
469
  log(`isInitialized: ${initialized}`);
374
470
  return initialized;
375
471
  },
472
+ isEmpty: () => {
473
+ return tableState.empty;
474
+ },
376
475
  sorting: {
377
476
  apply: async (columnName, direction) => {
378
477
  var _a;
@@ -419,6 +518,7 @@ const useTable = (rootLocator, configOptions = {}) => {
419
518
  // ─── Shared async row iterator ───────────────────────────────────────────
420
519
  [Symbol.asyncIterator]() {
421
520
  return __asyncGenerator(this, arguments, function* _a() {
521
+ var _b;
422
522
  yield __await(_autoInit());
423
523
  const map = tableMapper.getMapSync();
424
524
  const effectiveMaxPages = config.maxPages;
@@ -434,7 +534,13 @@ const useTable = (rootLocator, configOptions = {}) => {
434
534
  const pageRows = yield __await(rowLocators.all());
435
535
  const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
436
536
  for (const idx of newIndices) {
437
- yield yield __await({ row: _makeSmart(pageRows[idx], map, rowIndex, tableState.currentPageIndex, barrier), index: rowIndex, rowIndex, pageIndex: tableState.currentPageIndex });
537
+ // index = visit-order counter; rowIndex = logical/data index (B-hybrid, #362).
538
+ const logical = (_b = yield __await((0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], config, () => rowIndex))) !== null && _b !== void 0 ? _b : rowIndex;
539
+ const sr = _makeSmart(pageRows[idx], map, logical, tableState.currentPageIndex, barrier);
540
+ // Iterator rows share one per-page DOM snapshot (positional locators); disable
541
+ // toJSON's scroll-back recovery so reading one row can't invalidate the others (#366).
542
+ sr._inBatch = true;
543
+ yield yield __await({ row: sr, index: rowIndex, rowIndex: logical, pageIndex: tableState.currentPageIndex });
438
544
  rowIndex++;
439
545
  }
440
546
  if (pagesScanned >= effectiveMaxPages)
@@ -462,6 +568,7 @@ const useTable = (rootLocator, configOptions = {}) => {
462
568
  config,
463
569
  getPage: () => rootLocator.page(),
464
570
  getCurrentPageIndex: () => tableState.currentPageIndex,
571
+ getContext: () => createStrategyContext(),
465
572
  }, callback, options);
466
573
  },
467
574
  map: async (callback, options = {}) => {
@@ -476,6 +583,7 @@ const useTable = (rootLocator, configOptions = {}) => {
476
583
  config,
477
584
  getPage: () => rootLocator.page(),
478
585
  getCurrentPageIndex: () => tableState.currentPageIndex,
586
+ getContext: () => createStrategyContext(),
479
587
  }, callback, options);
480
588
  },
481
589
  toArray: async (callback, options = {}) => {
@@ -500,6 +608,7 @@ const useTable = (rootLocator, configOptions = {}) => {
500
608
  config,
501
609
  getPage: () => rootLocator.page(),
502
610
  getCurrentPageIndex: () => tableState.currentPageIndex,
611
+ getContext: () => createStrategyContext(),
503
612
  }, predicate, options);
504
613
  },
505
614
  generateConfig: async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "6.17.1",
3
+ "version": "6.19.0",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",
@@ -76,11 +76,21 @@
76
76
  },
77
77
  "typesVersions": {
78
78
  "*": {
79
- "types": ["./dist/types.d.ts"],
80
- "presets": ["./dist/presets/index.d.ts"],
81
- "presets/mui": ["./dist/presets/mui.d.ts"],
82
- "presets/rdg": ["./dist/presets/rdg.d.ts"],
83
- "presets/glide": ["./dist/presets/glide/index.d.ts"]
79
+ "types": [
80
+ "./dist/types.d.ts"
81
+ ],
82
+ "presets": [
83
+ "./dist/presets/index.d.ts"
84
+ ],
85
+ "presets/mui": [
86
+ "./dist/presets/mui.d.ts"
87
+ ],
88
+ "presets/rdg": [
89
+ "./dist/presets/rdg.d.ts"
90
+ ],
91
+ "presets/glide": [
92
+ "./dist/presets/glide/index.d.ts"
93
+ ]
84
94
  }
85
95
  },
86
96
  "keywords": [