@rickcedwhat/playwright-smart-table 6.19.0 → 6.20.1-next.19d3cd3

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
@@ -145,8 +145,14 @@ const useTable = (rootLocator, configOptions = {}) => {
145
145
  // Placeholder for the final table object
146
146
  let finalTable = null;
147
147
  // Helper factory
148
- const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier) => {
149
- return (0, smartRow_1.default)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
148
+ const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier, rowSelector) => {
149
+ const effectiveLocator = rowSelector
150
+ ? rootLocator.locator(rowSelector)
151
+ : rowLocator;
152
+ const sr = (0, smartRow_1.default)(effectiveLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
153
+ if (rowSelector)
154
+ sr._selfHealing = true;
155
+ return sr;
150
156
  };
151
157
  const tableState = { currentPageIndex: 0, empty: false };
152
158
  const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart, tableState, (useBulk) => _advancePage(useBulk));
@@ -281,9 +287,10 @@ const useTable = (rootLocator, configOptions = {}) => {
281
287
  await headerCell.scrollIntoViewIfNeeded();
282
288
  },
283
289
  getHeaders: async () => {
290
+ var _a;
284
291
  log("getHeaders: fetching available columns");
285
292
  const map = await tableMapper.getMap();
286
- return Array.from(map.keys());
293
+ return [...map.keys(), ...Object.keys((_a = config.syntheticColumns) !== null && _a !== void 0 ? _a : {})];
287
294
  },
288
295
  getHeaderCell: async (columnName) => {
289
296
  log(`getHeaderCell: column="${columnName}"`);
@@ -293,27 +300,95 @@ const useTable = (rootLocator, configOptions = {}) => {
293
300
  throw _createColumnError(columnName, map, 'header cell');
294
301
  return resolve(config.headerSelector, rootLocator).nth(idx);
295
302
  },
296
- countRows: async () => {
303
+ countRows: async (filters, options) => {
304
+ var _a, _b, _c, _d, _e;
297
305
  if (tableState.empty)
298
306
  return 0;
299
307
  await _autoInit();
308
+ const filtersRecord = (filters !== null && filters !== void 0 ? filters : {});
309
+ const hasFilters = Object.keys(filtersRecord).length > 0;
310
+ const effectiveMaxPages = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
300
311
  const pag = config.strategies.pagination;
301
- const hasPagination = config.maxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
312
+ const hasPagination = effectiveMaxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
313
+ const domFilters = {};
314
+ const overrideFilters = {};
315
+ const syntheticFilters = {};
316
+ if (hasFilters) {
317
+ for (const [key, value] of Object.entries(filtersRecord)) {
318
+ if ((_b = config.syntheticColumns) === null || _b === void 0 ? void 0 : _b[key]) {
319
+ syntheticFilters[key] = value;
320
+ }
321
+ else if ((_d = (_c = config.columnOverrides) === null || _c === void 0 ? void 0 : _c[key]) === null || _d === void 0 ? void 0 : _d.read) {
322
+ overrideFilters[key] = value;
323
+ }
324
+ else {
325
+ domFilters[key] = value;
326
+ }
327
+ }
328
+ }
329
+ const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
330
+ const resolveRows = () => {
331
+ var _a;
332
+ let rows = resolve(config.rowSelector, rootLocator);
333
+ if (Object.keys(domFilters).length > 0) {
334
+ const map = tableMapper.getMapSync();
335
+ if (!map)
336
+ throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
337
+ rows = filterEngine.applyFilters(rows, domFilters, map, (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false, rootLocator.page(), rootLocator);
338
+ }
339
+ return rows;
340
+ };
341
+ const countPostFilterMatches = async (candidates, indices) => {
342
+ var _a;
343
+ if (!hasPostFilters)
344
+ return indices.length;
345
+ const map = tableMapper.getMapSync();
346
+ if (!map)
347
+ throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
348
+ const exact = (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false;
349
+ const hasOverrides = Object.keys(overrideFilters).length > 0;
350
+ const hasSynthetics = Object.keys(syntheticFilters).length > 0;
351
+ let count = 0;
352
+ for (const idx of indices) {
353
+ const row = candidates[idx];
354
+ if (hasOverrides && !await rowFinder.matchesOverrideFilters(row, overrideFilters, map, exact))
355
+ continue;
356
+ if (hasSynthetics && !await rowFinder.matchesSyntheticFilters(row, syntheticFilters, map, exact))
357
+ continue;
358
+ count++;
359
+ }
360
+ return count;
361
+ };
362
+ // Wait for table to finish loading before counting
363
+ const isTableLoading = (_e = config.strategies.loading) === null || _e === void 0 ? void 0 : _e.isTableLoading;
364
+ if (isTableLoading) {
365
+ const ctx = createStrategyContext();
366
+ while (await isTableLoading(ctx)) {
367
+ log('countRows: table is loading... waiting');
368
+ await rootLocator.page().waitForTimeout(200);
369
+ }
370
+ }
302
371
  if (!hasPagination) {
303
- log("countRows: counting rows in current viewport (no pagination)");
304
- return resolve(config.rowSelector, rootLocator).count();
372
+ log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
373
+ if (!hasPostFilters)
374
+ return resolveRows().count();
375
+ const all = await resolveRows().all();
376
+ return countPostFilterMatches(all, all.map((_, i) => i));
305
377
  }
306
- log(`countRows: paginating up to ${config.maxPages} page(s)`);
378
+ log(`countRows: paginating up to ${effectiveMaxPages} page(s)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
307
379
  const tracker = new elementTracker_1.ElementTracker('countRows');
308
380
  let total = 0;
309
381
  let pagesScanned = 1;
310
382
  let paginationError;
311
383
  try {
312
384
  while (true) {
313
- const newIndices = await tracker.getUnseenIndices(resolve(config.rowSelector, rootLocator));
314
- total += newIndices.length;
315
- log(`countRows: page ${pagesScanned} — ${newIndices.length} row(s) (running total: ${total})`);
316
- if (pagesScanned >= config.maxPages)
385
+ const rowLocators = resolveRows();
386
+ const newIndices = await tracker.getUnseenIndices(rowLocators);
387
+ const candidates = await rowLocators.all();
388
+ const matched = await countPostFilterMatches(candidates, newIndices);
389
+ total += matched;
390
+ log(`countRows: page ${pagesScanned} — ${matched} row(s) (running total: ${total})`);
391
+ if (pagesScanned >= effectiveMaxPages)
317
392
  break;
318
393
  if (!await _advancePage(false))
319
394
  break;
@@ -370,7 +445,6 @@ const useTable = (rootLocator, configOptions = {}) => {
370
445
  reset: async () => {
371
446
  var _a;
372
447
  log("Resetting table...");
373
- await config.onReset(createStrategyContext());
374
448
  if ((_a = config.strategies.pagination) === null || _a === void 0 ? void 0 : _a.goToFirst) {
375
449
  log("Auto-navigating to first page...");
376
450
  await config.strategies.pagination.goToFirst(createStrategyContext());
@@ -383,6 +457,7 @@ const useTable = (rootLocator, configOptions = {}) => {
383
457
  tableMapper.clear();
384
458
  log("Table reset complete. Calling autoInit to restore state.");
385
459
  await _autoInit();
460
+ await config.onReset(createStrategyContext());
386
461
  },
387
462
  revalidate: async () => {
388
463
  log("Revalidating table structure...");
@@ -392,6 +467,11 @@ const useTable = (rootLocator, configOptions = {}) => {
392
467
  },
393
468
  getRow: (filters, options = { exact: false }) => {
394
469
  log(`getRow: filters=${safeStringify(filters)} exact=${options.exact}`);
470
+ const syntheticKeys = Object.keys(filters).filter(k => { var _a; return (_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[k]; });
471
+ if (syntheticKeys.length > 0) {
472
+ throw new Error(`getRow() cannot filter by synthetic column(s): ${syntheticKeys.join(', ')}. ` +
473
+ `Use findRow() instead — synthetic columns require async evaluation.`);
474
+ }
395
475
  const map = tableMapper.getMapSync();
396
476
  if (!map)
397
477
  throw new Error('Initialization Error: You attempted to access a row before the table structure was mapped. Please call "await table.init()" once before using synchronous row access.');
@@ -420,12 +500,19 @@ const useTable = (rootLocator, configOptions = {}) => {
420
500
  throw new Error(`findRowByIndex(${index}) requires a strategies.resolveRowIndex to identify rows by their logical index. ` +
421
501
  `Configure one (e.g. read the grid's row-index attribute), or use getRowByIndex() (current render window) / findRow(filters).`);
422
502
  }
503
+ let foundSelector;
423
504
  // The currently-mounted row whose logical index === index, if any.
424
505
  const findMounted = async () => {
425
506
  const rows = await resolve(config.rowSelector, rootLocator).all();
426
507
  for (const r of rows) {
427
- if ((await resolveRI(r)) === index)
508
+ const result = await resolveRI(r);
509
+ if (result === undefined)
510
+ continue;
511
+ const normalized = (0, rowResolution_1.normalizeRowIndexResult)(result);
512
+ if (normalized.index === index) {
513
+ foundSelector = normalized.selector;
428
514
  return r;
515
+ }
429
516
  }
430
517
  return null;
431
518
  };
@@ -454,7 +541,7 @@ const useTable = (rootLocator, configOptions = {}) => {
454
541
  `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
542
  }
456
543
  await (0, debugUtils_1.debugDelay)(config, 'findRow');
457
- return _makeSmart(loc, map, index, tableState.currentPageIndex);
544
+ return _makeSmart(loc, map, index, tableState.currentPageIndex, undefined, foundSelector);
458
545
  },
459
546
  findRow: async (filters, options) => {
460
547
  log(`findRow: filters=${safeStringify(filters)} options=${safeStringify(options)}`);
@@ -474,7 +561,7 @@ const useTable = (rootLocator, configOptions = {}) => {
474
561
  },
475
562
  sorting: {
476
563
  apply: async (columnName, direction) => {
477
- var _a;
564
+ var _a, _b, _c, _d, _e;
478
565
  await _autoInit();
479
566
  if (!config.strategies.sorting)
480
567
  throw new Error('No sorting strategy has been configured.');
@@ -489,13 +576,16 @@ const useTable = (rootLocator, configOptions = {}) => {
489
576
  }
490
577
  await config.strategies.sorting.doSort({ columnName, direction, context });
491
578
  if ((_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading) {
492
- const deadline = Date.now() + 10000;
579
+ const timeout = (_b = config.strategies.loading.sortStabilizationTimeout) !== null && _b !== void 0 ? _b : 10000;
580
+ const poll = (_c = config.strategies.loading.sortStabilizationPollInterval) !== null && _c !== void 0 ? _c : 100;
581
+ const deadline = Date.now() + timeout;
493
582
  while (Date.now() < deadline && await config.strategies.loading.isTableLoading(context)) {
494
- await rootLocator.page().waitForTimeout(100);
583
+ await rootLocator.page().waitForTimeout(poll);
495
584
  }
496
585
  }
497
586
  else {
498
- await rootLocator.page().waitForTimeout(200);
587
+ const fallback = (_e = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.sortStabilizationFallbackDelay) !== null && _e !== void 0 ? _e : 200;
588
+ await rootLocator.page().waitForTimeout(fallback);
499
589
  }
500
590
  await (0, debugUtils_1.debugDelay)(config, 'default');
501
591
  const newState = await config.strategies.sorting.getSortState({ columnName, context });
@@ -535,8 +625,9 @@ const useTable = (rootLocator, configOptions = {}) => {
535
625
  const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
536
626
  for (const idx of newIndices) {
537
627
  // 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);
628
+ const resolved = yield __await((0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], config, () => rowIndex));
629
+ const logical = (_b = resolved === null || resolved === void 0 ? void 0 : resolved.index) !== null && _b !== void 0 ? _b : rowIndex;
630
+ const sr = _makeSmart(pageRows[idx], map, logical, tableState.currentPageIndex, barrier, resolved === null || resolved === void 0 ? void 0 : resolved.selector);
540
631
  // Iterator rows share one per-page DOM snapshot (positional locators); disable
541
632
  // toJSON's scroll-back recovery so reading one row can't invalidate the others (#366).
542
633
  sr._inBatch = true;
@@ -563,7 +654,7 @@ const useTable = (rootLocator, configOptions = {}) => {
563
654
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
564
655
  getMap: () => tableMapper.getMapSync(),
565
656
  advancePage: _advancePage,
566
- makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
657
+ makeSmartRow: (loc, map, idx, pageIdx, barrier, sel) => _makeSmart(loc, map, idx, pageIdx, barrier, sel),
567
658
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
568
659
  config,
569
660
  getPage: () => rootLocator.page(),
@@ -578,7 +669,7 @@ const useTable = (rootLocator, configOptions = {}) => {
578
669
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
579
670
  getMap: () => tableMapper.getMapSync(),
580
671
  advancePage: _advancePage,
581
- makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
672
+ makeSmartRow: (loc, map, idx, pageIdx, barrier, sel) => _makeSmart(loc, map, idx, pageIdx, barrier, sel),
582
673
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
583
674
  config,
584
675
  getPage: () => rootLocator.page(),
@@ -603,7 +694,7 @@ const useTable = (rootLocator, configOptions = {}) => {
603
694
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
604
695
  getMap: () => tableMapper.getMapSync(),
605
696
  advancePage: _advancePage,
606
- makeSmartRow: (loc, map, idx, pageIdx, barrier) => _makeSmart(loc, map, idx, pageIdx, barrier),
697
+ makeSmartRow: (loc, map, idx, pageIdx, barrier, sel) => _makeSmart(loc, map, idx, pageIdx, barrier, sel),
607
698
  createSmartRowArray: smartRowArray_1.createSmartRowArray,
608
699
  config,
609
700
  getPage: () => rootLocator.page(),
@@ -18,7 +18,8 @@ class ElementTracker {
18
18
  const seenMap = win[trackerId];
19
19
  const newIndices = [];
20
20
  elements.forEach((el, index) => {
21
- const signature = el.textContent || '';
21
+ const htmlEl = el;
22
+ const signature = htmlEl.offsetTop + '|' + (el.textContent || '');
22
23
  if (seenMap.get(el) !== signature) {
23
24
  newIndices.push(index);
24
25
  }
@@ -39,8 +40,10 @@ class ElementTracker {
39
40
  const seenMap = win[trackerId];
40
41
  for (const index of indicesToCommit) {
41
42
  const el = elements[index];
42
- if (el)
43
- seenMap.set(el, el.textContent || '');
43
+ if (el) {
44
+ const htmlEl = el;
45
+ seenMap.set(el, htmlEl.offsetTop + '|' + (el.textContent || ''));
46
+ }
44
47
  }
45
48
  }, [this.id, indices]);
46
49
  }
@@ -56,5 +56,9 @@ function mergeTableConfig(base, overrides) {
56
56
  if (base.columnOverrides || overrides.columnOverrides) {
57
57
  result.columnOverrides = Object.assign(Object.assign({}, base.columnOverrides), overrides.columnOverrides);
58
58
  }
59
+ // Deep-merge syntheticColumns (override keys replace, preset keys retained)
60
+ if (base.syntheticColumns || overrides.syntheticColumns) {
61
+ result.syntheticColumns = Object.assign(Object.assign({}, base.syntheticColumns), overrides.syntheticColumns);
62
+ }
59
63
  return result;
60
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "6.19.0",
3
+ "version": "6.20.1-next.19d3cd3",
4
4
  "description": "Smart, column-aware table interactions for Playwright",
5
5
  "author": "Cedrick Catalan",
6
6
  "license": "MIT",
@@ -110,6 +110,11 @@
110
110
  "peerDependencies": {
111
111
  "@playwright/test": "^1.40.0"
112
112
  },
113
+ "peerDependenciesMeta": {
114
+ "@playwright/test": {
115
+ "optional": true
116
+ }
117
+ },
113
118
  "devDependencies": {
114
119
  "@commitlint/cli": "^21.0.0",
115
120
  "@commitlint/config-conventional": "^21.0.0",