@rickcedwhat/playwright-smart-table 6.25.0-next.ec8d20a → 6.25.0-next.f72ee4c

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.
@@ -2,7 +2,7 @@ import type { Locator } from '@playwright/test';
2
2
  import { StrategyContext } from '../types';
3
3
  /**
4
4
  * Primitive navigation functions that define HOW to move within a table.
5
- * The orchestration logic (WHEN to move) lives in _navigateToCell.
5
+ * The orchestration logic (WHEN to move) lives in navigateToCell (src/engine/cellNavigation.ts).
6
6
  */
7
7
  export interface NavigationPrimitives {
8
8
  goUp?: (context: StrategyContext) => Promise<void>;
@@ -1,5 +1,23 @@
1
- import type { ContentReadyStrategy } from '../types';
1
+ import type { ContentReadyStrategy, RowReadyPredicate } from '../types';
2
2
  export declare const ContentReadyStrategies: {
3
+ /**
4
+ * Waits in the browser until `predicate(rowElement, arg)` returns a truthy value, e.g. until
5
+ * a skeleton placeholder is gone or every cell has rendered. Resolves when `timeout` expires
6
+ * even if the predicate never passed, like the other content-ready strategies.
7
+ *
8
+ * Uses `locator.waitForFunction` (Playwright 1.62+), which re-resolves the row on each retry
9
+ * and polls without a round-trip per check. Older versions poll via `locator.evaluate`.
10
+ * `timeout` and `interval` must be positive finite milliseconds. `interval` only controls
11
+ * the older-version fallback; native polling is controlled by Playwright.
12
+ *
13
+ * @example
14
+ * contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton'))
15
+ */
16
+ until: <Arg = undefined>(predicate: RowReadyPredicate<Arg>, options?: {
17
+ arg?: Arg;
18
+ timeout?: number;
19
+ interval?: number;
20
+ }) => ContentReadyStrategy;
3
21
  /**
4
22
  * Polls the row's text content until two consecutive reads match.
5
23
  */
@@ -1,7 +1,88 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContentReadyStrategies = void 0;
4
+ function isTimeout(error) {
5
+ return (error === null || error === void 0 ? void 0 : error.name) === 'TimeoutError';
6
+ }
7
+ /** Playwright < 1.62 has no `locator.waitForFunction`: poll with one `evaluate` per check. */
8
+ async function pollPredicate(row, page, predicate, arg, timeout, interval) {
9
+ const deadline = Date.now() + timeout;
10
+ let source = predicate.toString();
11
+ // Build a serializable callback; the user predicate executes only inside the browser.
12
+ const wrap = (expression) => new Function(`return async (row, arg) => Boolean(await (${expression})(row, arg))`)();
13
+ let evaluateBoolean;
14
+ try {
15
+ evaluateBoolean = wrap(source);
16
+ }
17
+ catch (_a) {
18
+ // Like Playwright's serializer, normalize method shorthand into a function expression.
19
+ source = source.startsWith('async ') ? source.replace(/^async\s+/, 'async function ') : `function ${source}`;
20
+ evaluateBoolean = wrap(source);
21
+ }
22
+ let expired = false;
23
+ let deadlineTimer;
24
+ const timeoutReached = new Promise(resolve => {
25
+ deadlineTimer = setTimeout(() => { expired = true; resolve(); }, timeout);
26
+ });
27
+ const poll = async () => {
28
+ while (!expired && Date.now() < deadline) {
29
+ // Convert truthiness before serialization, including async DOM-element results.
30
+ if (await row.evaluate(evaluateBoolean, arg, { timeout: Math.max(1, deadline - Date.now()) }))
31
+ return;
32
+ if (expired || Date.now() >= deadline)
33
+ return;
34
+ await page.waitForTimeout(Math.min(interval, Math.max(0, deadline - Date.now())));
35
+ }
36
+ };
37
+ try {
38
+ // evaluate's timeout only bounds locator resolution, not an async predicate's promise.
39
+ await Promise.race([poll(), timeoutReached]);
40
+ }
41
+ finally {
42
+ clearTimeout(deadlineTimer);
43
+ }
44
+ }
4
45
  exports.ContentReadyStrategies = {
46
+ /**
47
+ * Waits in the browser until `predicate(rowElement, arg)` returns a truthy value, e.g. until
48
+ * a skeleton placeholder is gone or every cell has rendered. Resolves when `timeout` expires
49
+ * even if the predicate never passed, like the other content-ready strategies.
50
+ *
51
+ * Uses `locator.waitForFunction` (Playwright 1.62+), which re-resolves the row on each retry
52
+ * and polls without a round-trip per check. Older versions poll via `locator.evaluate`.
53
+ * `timeout` and `interval` must be positive finite milliseconds. `interval` only controls
54
+ * the older-version fallback; native polling is controlled by Playwright.
55
+ *
56
+ * @example
57
+ * contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton'))
58
+ */
59
+ until: (predicate, options = {}) => {
60
+ var _a, _b;
61
+ const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : 500;
62
+ const interval = (_b = options.interval) !== null && _b !== void 0 ? _b : 50;
63
+ if (!Number.isFinite(timeout) || timeout <= 0) {
64
+ throw new Error('ContentReady.until: timeout must be a positive finite number');
65
+ }
66
+ if (!Number.isFinite(interval) || interval <= 0) {
67
+ throw new Error('ContentReady.until: interval must be a positive finite number');
68
+ }
69
+ const fn = predicate;
70
+ const arg = options.arg;
71
+ return async (row, page) => {
72
+ try {
73
+ if (typeof row.waitForFunction === 'function') {
74
+ await row.waitForFunction(fn, arg, { timeout });
75
+ }
76
+ else {
77
+ await pollPredicate(row, page, fn, arg, timeout, interval);
78
+ }
79
+ }
80
+ catch (error) {
81
+ if (!isTimeout(error))
82
+ throw error;
83
+ }
84
+ };
85
+ },
5
86
  /**
6
87
  * Polls the row's text content until two consecutive reads match.
7
88
  */
@@ -98,6 +98,11 @@ export declare const Strategies: {
98
98
  dataAttribute: (options?: import("./viewport").DataAttributeViewportOptions) => import("..").ViewportStrategy;
99
99
  };
100
100
  ContentReady: {
101
+ until: <Arg = undefined>(predicate: import("..").RowReadyPredicate<Arg>, options?: {
102
+ arg?: Arg;
103
+ timeout?: number;
104
+ interval?: number;
105
+ }) => import("..").ContentReadyStrategy;
101
106
  textStable: (options?: {
102
107
  timeout?: number;
103
108
  interval?: number;
package/dist/types.d.ts CHANGED
@@ -428,6 +428,8 @@ export interface PaginationPrimitives {
428
428
  export type PaginationStrategy = PaginationPrimitives;
429
429
  export type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;
430
430
  export type ContentReadyStrategy = (row: Locator, page: Page) => Promise<void>;
431
+ /** Predicate for `Strategies.ContentReady.until`. Runs in the browser with the row element, so it must not close over test-side variables; pass values via `arg`. */
432
+ export type RowReadyPredicate<Arg = undefined> = (row: HTMLElement | SVGElement, arg: Arg) => unknown;
431
433
  export type FillStrategy = (options: {
432
434
  row: SmartRow;
433
435
  columnName: string;
@@ -638,12 +640,18 @@ export interface TableStrategies {
638
640
  * Without this, the clone captures stale content from the previous occupant of the
639
641
  * DOM slot — the element is at the correct position but React hasn't re-rendered yet.
640
642
  *
641
- * Use `Strategies.ContentReady.textStable()` for the built-in text-polling strategy.
643
+ * Built-ins: `Strategies.ContentReady.textStable()` (text stops changing),
644
+ * `mutationSettled()` (no DOM mutations for a quiet period), and `until(predicate)`
645
+ * (an in-browser condition holds, e.g. no skeleton placeholder).
642
646
  *
643
647
  * @example
644
648
  * strategies: {
645
649
  * contentReady: Strategies.ContentReady.textStable({ timeout: 500 }),
646
650
  * }
651
+ * @example
652
+ * strategies: {
653
+ * contentReady: Strategies.ContentReady.until(row => !row.querySelector('.skeleton')),
654
+ * }
647
655
  */
648
656
  contentReady?: ContentReadyStrategy;
649
657
  /**
@@ -794,6 +802,11 @@ export type RowIterationOptions = {
794
802
  * @default false — uses goNext for one-page-at-a-time advancement
795
803
  */
796
804
  useBulkPagination?: boolean;
805
+ /**
806
+ * Cancels the iteration between pages. When aborted, the call rejects with
807
+ * `signal.reason` (rows already processed are not returned).
808
+ */
809
+ signal?: AbortSignal;
797
810
  };
798
811
  /**
799
812
  * Result of {@link useTable}. Implements `AsyncIterable` — `for await (const { row } of table)`
@@ -876,31 +889,37 @@ export interface TableResult<T = any> extends AsyncIterable<{
876
889
  * is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
877
890
  *
878
891
  * @param index 0-based logical/data-model row index
879
- * @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages)
892
+ * @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages).
893
+ * `signal` cancels the page scan; the call rejects with `signal.reason`.
880
894
  */
881
895
  findRowByIndex: (index: number, options?: {
882
896
  maxPages?: number;
897
+ signal?: AbortSignal;
883
898
  }) => Promise<SmartRow<T>>;
884
899
  /**
885
900
  * ASYNC: Searches for a single row across pages using pagination.
886
901
  * Auto-initializes the table if not already initialized.
887
902
  * @param filters - The filter criteria to match
888
- * @param options - Search options including exact match and max pages
903
+ * @param options - Search options including exact match and max pages. `signal` cancels the page scan; the call rejects with `signal.reason`.
889
904
  */
890
905
  findRow: (filters: Record<string, FilterValue>, options?: {
891
906
  exact?: boolean;
892
907
  maxPages?: number;
908
+ signal?: AbortSignal;
893
909
  }) => Promise<SmartRow<T>>;
894
910
  /**
895
911
  * ASYNC: Searches for all matching rows across pages using pagination.
896
912
  * Auto-initializes the table if not already initialized.
913
+ * Returned rows can be read independently, including with `concurrency: 'synchronized'`.
914
+ * Use synchronized `map()` for coordinated column scrolling across virtualized rows.
897
915
  * @param filters - The filter criteria to match (omit or pass {} for all rows)
898
- * @param options - Search options (exact, maxPages). `useBulkPagination` defaults to `false`: pages advance one at a time via `goNext` so no intermediate page is skipped. Set it to `true` to opt into `goNextBulk` (faster, but skips the rows on jumped-over pages).
916
+ * @param options - Search options (exact, maxPages). `useBulkPagination` defaults to `false`: pages advance one at a time via `goNext` so no intermediate page is skipped. Set it to `true` to opt into `goNextBulk` (faster, but skips the rows on jumped-over pages). `signal` cancels the page scan; the call rejects with `signal.reason`.
899
917
  */
900
918
  findRows: (filters?: Record<string, FilterValue>, options?: {
901
919
  exact?: boolean;
902
920
  maxPages?: number;
903
921
  useBulkPagination?: boolean;
922
+ signal?: AbortSignal;
904
923
  }) => Promise<SmartRowArray<T>>;
905
924
  /**
906
925
  * Resolves the named column and scrolls it into view.
@@ -911,11 +930,13 @@ export interface TableResult<T = any> extends AsyncIterable<{
911
930
  /**
912
931
  * Counts the number of rows, optionally filtered.
913
932
  * Without arguments, counts all rows. With filters, counts only matching rows.
914
- * Paginates when pagination is configured.
933
+ * Paginates when pagination is configured. `signal` cancels the page scan; the call
934
+ * rejects with `signal.reason` (the table is still reset to page 1).
915
935
  */
916
936
  countRows: (filters?: Record<string, FilterValue>, options?: {
917
937
  exact?: boolean;
918
938
  maxPages?: number;
939
+ signal?: AbortSignal;
919
940
  }) => Promise<number>;
920
941
  /**
921
942
  * Iterates over rows and extracts the value of a single column.
package/dist/useTable.js CHANGED
@@ -27,29 +27,15 @@ 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");
31
- const scanPages_1 = require("./engine/scanPages");
30
+ const countRows_1 = require("./engine/countRows");
31
+ const sorting_1 = require("./engine/sorting");
32
+ const rowByIndex_1 = require("./engine/rowByIndex");
33
+ const safeStringify_1 = require("./utils/safeStringify");
32
34
  const debugUtils_1 = require("./utils/debugUtils");
33
35
  const smartRowArray_1 = require("./utils/smartRowArray");
34
- const elementTracker_1 = require("./utils/elementTracker");
35
- const loadingWait_1 = require("./utils/loadingWait");
36
36
  const pageIndex_1 = require("./utils/pageIndex");
37
37
  const smartRowInternals_1 = require("./utils/smartRowInternals");
38
38
  const locatorDescription_1 = require("./utils/locatorDescription");
39
- // Helper to safely serialize objects containing functions for logging
40
- const safeStringify = (obj) => {
41
- try {
42
- return JSON.stringify(obj, (_key, value) => {
43
- if (typeof value === 'function') {
44
- return '[Function]';
45
- }
46
- return value;
47
- });
48
- }
49
- catch (e) {
50
- return '[unserializable]';
51
- }
52
- };
53
39
  /**
54
40
  * Main hook to interact with a table.
55
41
  */
@@ -238,6 +224,11 @@ const useTable = (rootLocator, configOptions = {}) => {
238
224
  }
239
225
  return didAdvance;
240
226
  };
227
+ const sortingEnv = () => ({
228
+ config,
229
+ getPage: () => rootLocator.page(),
230
+ getContext: () => (Object.assign(Object.assign({}, createStrategyContext()), { getHeaderCell: result.getHeaderCell })),
231
+ });
241
232
  const result = {
242
233
  get currentPageIndex() { return tableState.currentPageIndex; },
243
234
  set currentPageIndex(v) {
@@ -326,133 +317,24 @@ const useTable = (rootLocator, configOptions = {}) => {
326
317
  return resolve(config.headerSelector, rootLocator).nth(idx);
327
318
  },
328
319
  countRows: async (filters, options) => {
329
- var _a, _b, _c, _d;
330
- var _e;
331
320
  if (tableState.empty)
332
321
  return 0;
333
322
  await _autoInit();
334
- const filtersRecord = (filters !== null && filters !== void 0 ? filters : {});
335
- const hasFilters = Object.keys(filtersRecord).length > 0;
336
- const effectiveMaxPages = (_e = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _e !== void 0 ? _e : config.maxPages;
337
- const pag = config.strategies.pagination;
338
- const hasPagination = effectiveMaxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
339
- const domFilters = {};
340
- const overrideFilters = {};
341
- const syntheticFilters = {};
342
- if (hasFilters) {
343
- for (const [key, value] of Object.entries(filtersRecord)) {
344
- if ((_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[key]) {
345
- syntheticFilters[key] = value;
346
- }
347
- else if ((_c = (_b = config.columnOverrides) === null || _b === void 0 ? void 0 : _b[key]) === null || _c === void 0 ? void 0 : _c.read) {
348
- overrideFilters[key] = value;
349
- }
350
- else {
351
- domFilters[key] = value;
352
- }
353
- }
354
- }
355
- const hasPostFilters = Object.keys(overrideFilters).length > 0 || Object.keys(syntheticFilters).length > 0;
356
- const resolveRows = () => {
357
- var _a;
358
- let rows = resolve(config.rowSelector, rootLocator);
359
- if (Object.keys(domFilters).length > 0) {
360
- const map = tableMapper.getMapSync();
361
- if (!map)
362
- throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
363
- 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);
364
- }
365
- return rows;
366
- };
367
- const countPostFilterMatches = async (candidates, indices) => {
368
- var _a;
369
- if (!hasPostFilters)
370
- return indices.length;
371
- const map = tableMapper.getMapSync();
372
- if (!map)
373
- throw new Error('Initialization Error: Table map not available. Call "await table.init()" first.');
374
- const exact = (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false;
375
- const hasOverrides = Object.keys(overrideFilters).length > 0;
376
- const hasSynthetics = Object.keys(syntheticFilters).length > 0;
377
- let count = 0;
378
- for (const idx of indices) {
379
- const row = candidates[idx];
380
- if (hasOverrides && !await rowFinder.matchesOverrideFilters(row, overrideFilters, map, exact))
381
- continue;
382
- if (hasSynthetics && !await rowFinder.matchesSyntheticFilters(row, syntheticFilters, map, exact))
383
- continue;
384
- count++;
385
- }
386
- return count;
387
- };
388
- // Wait for table to finish loading before counting
389
- const isTableLoading = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.isTableLoading;
390
- if (isTableLoading) {
391
- const ctx = createStrategyContext();
392
- await (0, loadingWait_1.waitWhileTableLoading)(config, ctx, rootLocator.page(), 'countRows');
393
- }
394
- if (!hasPagination) {
395
- log(`countRows: counting rows in current viewport (no pagination)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
396
- if (!hasPostFilters)
397
- return resolveRows().count();
398
- const all = await resolveRows().all();
399
- return countPostFilterMatches(all, all.map((_, i) => i));
400
- }
401
- log(`countRows: paginating up to ${effectiveMaxPages} page(s)${hasFilters ? ` filters=${safeStringify(filtersRecord)}` : ''}`);
402
- const tracker = new elementTracker_1.ElementTracker('countRows');
403
- let total = 0;
404
- let pagesScanned = 1;
405
- let paginationError;
406
- try {
407
- const scanned = await (0, scanPages_1.scanPages)({
408
- advancePage: _advancePage,
409
- getCurrentPageIndex: () => tableState.currentPageIndex,
410
- config,
411
- waitForReady: async () => {
412
- if (!isTableLoading)
413
- return;
414
- const ctx = createStrategyContext();
415
- await (0, loadingWait_1.waitWhileTableLoading)(config, ctx, rootLocator.page(), 'countRows');
416
- },
417
- }, async ({ pagesScanned: pageNum }) => {
418
- const rowLocators = resolveRows();
419
- const newIndices = await tracker.getUnseenIndices(rowLocators);
420
- const candidates = await rowLocators.all();
421
- const matched = await countPostFilterMatches(candidates, newIndices);
422
- total += matched;
423
- log(`countRows: page ${pageNum} — ${matched} row(s) (running total: ${total})`);
424
- return 'continue';
425
- }, {
426
- maxPages: effectiveMaxPages,
427
- useBulk: false,
428
- label: 'countRows',
429
- finalScanOnEof: true,
430
- });
431
- pagesScanned = scanned.pagesScanned;
432
- }
433
- catch (e) {
434
- paginationError = e;
435
- throw e;
436
- }
437
- finally {
438
- await tracker.cleanup(rootLocator.page());
439
- // Always restore page 1, even if pagination threw mid-count (#348), so the table
440
- // stays usable for callers that catch the error. If reset() itself fails, only let
441
- // it surface when pagination succeeded — otherwise preserve the original error.
442
- log(`countRows: ${total} total row(s) across ${pagesScanned} page(s) — resetting`);
443
- try {
444
- await result.reset();
445
- }
446
- catch (resetError) {
447
- if (paginationError === undefined)
448
- throw resetError;
449
- (0, debugUtils_1.logDebug)(config, 'error', 'countRows: reset() failed after pagination error', resetError);
450
- }
451
- }
452
- return total;
323
+ return (0, countRows_1.countRows)({
324
+ config,
325
+ rootLocator,
326
+ getRowLocators: () => resolve(config.rowSelector, rootLocator),
327
+ tableMapper,
328
+ filterEngine,
329
+ rowFinder,
330
+ advancePage: _advancePage,
331
+ getCurrentPageIndex: () => tableState.currentPageIndex,
332
+ getContext: () => createStrategyContext(),
333
+ reset: () => result.reset(),
334
+ }, filters, options);
453
335
  },
454
336
  mapColumn: async (columnName, options = {}) => {
455
- log(`mapColumn: column="${columnName}" options=${safeStringify(options)}`);
337
+ log(`mapColumn: column="${columnName}" options=${(0, safeStringify_1.safeStringify)(options)}`);
456
338
  await _autoInit();
457
339
  const map = await tableMapper.getMap();
458
340
  if (!map.has(columnName))
@@ -477,7 +359,7 @@ const useTable = (rootLocator, configOptions = {}) => {
477
359
  * Iterates over rows and extracts the value of a single column as strings.
478
360
  */
479
361
  getColumnValues: async (columnName, options = {}) => {
480
- log(`getColumnValues: column="${columnName}" options=${safeStringify(options)}`);
362
+ log(`getColumnValues: column="${columnName}" options=${(0, safeStringify_1.safeStringify)(options)}`);
481
363
  const values = await result.mapColumn(columnName, options);
482
364
  return values.map(v => String(v));
483
365
  },
@@ -505,7 +387,7 @@ const useTable = (rootLocator, configOptions = {}) => {
505
387
  log("Table revalidated.");
506
388
  },
507
389
  getRow: (filters, options = { exact: false }) => {
508
- log(`getRow: filters=${safeStringify(filters)} exact=${options.exact}`);
390
+ log(`getRow: filters=${(0, safeStringify_1.safeStringify)(filters)} exact=${options.exact}`);
509
391
  const syntheticKeys = Object.keys(filters).filter(k => { var _a; return (_a = config.syntheticColumns) === null || _a === void 0 ? void 0 : _a[k]; });
510
392
  if (syntheticKeys.length > 0) {
511
393
  throw new Error(`getRow() cannot filter by synthetic column(s): ${syntheticKeys.join(', ')}. ` +
@@ -533,67 +415,23 @@ const useTable = (rootLocator, configOptions = {}) => {
533
415
  return _makeSmart(rowLocator, map, index, undefined, undefined, undefined, true);
534
416
  },
535
417
  findRowByIndex: async (index, options) => {
536
- var _a, _b;
537
- log(`findRowByIndex: index=${index} options=${safeStringify(options)}`);
538
418
  await _autoInit();
539
- const map = tableMapper.getMapSync();
540
- const resolveRI = config.strategies.resolveRowIndex;
541
- const viewport = config.strategies.viewport;
542
- // Identifying a row by its logical index requires a resolveRowIndex strategy. Without one
543
- // the library can only offer render-window-relative getRowByIndex, or filter-based findRow.
544
- if (!resolveRI) {
545
- throw new Error(`findRowByIndex(${index}) requires a strategies.resolveRowIndex to identify rows by their logical index. ` +
546
- `Configure one (e.g. read the grid's row-index attribute), or use getRowByIndex() (current render window) / findRow(filters).`);
547
- }
548
- let foundSelector;
549
- // The currently-mounted row whose logical index === index, if any.
550
- const findMounted = async () => {
551
- const rows = await resolve(config.rowSelector, rootLocator).all();
552
- for (const r of rows) {
553
- const result = await resolveRI(r);
554
- if (result === undefined)
555
- continue;
556
- const normalized = (0, rowResolution_1.normalizeRowIndexResult)(result);
557
- if (normalized.index === index) {
558
- foundSelector = normalized.selector;
559
- return r;
560
- }
561
- }
562
- return null;
563
- };
564
- // 1. Already mounted (no navigation needed)?
565
- let loc = await findMounted();
566
- // 2. Fast path: jump straight to it via the viewport's random-access scroll.
567
- if (!loc && (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow)) {
568
- log(`findRowByIndex: scrolling to row ${index}`);
569
- await viewport.scrollToRow(createStrategyContext(), index);
570
- loc = await findMounted();
571
- }
572
- // 3. Fallback: advance pages (on infinite-scroll tables, a "page" is a scroll step) until
573
- // the row is reached. Bounded by maxPages so an unreachable index can't loop forever.
574
- if (!loc && config.strategies.pagination) {
575
- const effectiveMaxPages = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
576
- let pagesScanned = 1;
577
- while (!loc && pagesScanned < effectiveMaxPages) {
578
- if (!await _advancePage(false))
579
- break;
580
- pagesScanned++;
581
- loc = await findMounted();
582
- }
583
- }
584
- if (!loc) {
585
- throw new Error(`findRowByIndex(${index}): could not reach row ${index}. ` +
586
- `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}).`);
587
- }
588
- await (0, debugUtils_1.debugDelay)(config, 'findRow');
589
- return _makeSmart(loc, map, index, tableState.currentPageIndex, undefined, foundSelector);
419
+ return (0, rowByIndex_1.findRowByIndex)({
420
+ config,
421
+ getRowLocators: () => resolve(config.rowSelector, rootLocator),
422
+ getMap: () => tableMapper.getMapSync(),
423
+ advancePage: _advancePage,
424
+ getCurrentPageIndex: () => tableState.currentPageIndex,
425
+ getContext: () => createStrategyContext(),
426
+ makeSmartRow: (loc, map, idx, pageIdx, sel) => _makeSmart(loc, map, idx, pageIdx, undefined, sel),
427
+ }, index, options);
590
428
  },
591
429
  findRow: async (filters, options) => {
592
- log(`findRow: filters=${safeStringify(filters)} options=${safeStringify(options)}`);
430
+ log(`findRow: filters=${(0, safeStringify_1.safeStringify)(filters)} options=${(0, safeStringify_1.safeStringify)(options)}`);
593
431
  return rowFinder.findRow(filters, options);
594
432
  },
595
433
  findRows: async (filters, options) => {
596
- log(`findRows: filters=${safeStringify(filters !== null && filters !== void 0 ? filters : {})} options=${safeStringify(options)}`);
434
+ log(`findRows: filters=${(0, safeStringify_1.safeStringify)(filters !== null && filters !== void 0 ? filters : {})} options=${(0, safeStringify_1.safeStringify)(options)}`);
597
435
  return rowFinder.findRows(filters !== null && filters !== void 0 ? filters : {}, options);
598
436
  },
599
437
  isInitialized: () => {
@@ -606,49 +444,13 @@ const useTable = (rootLocator, configOptions = {}) => {
606
444
  },
607
445
  sorting: {
608
446
  apply: async (columnName, direction) => {
609
- var _a, _b;
610
- var _c, _d, _e, _f;
611
447
  await _autoInit();
612
- if (!config.strategies.sorting)
613
- throw new Error('No sorting strategy has been configured.');
614
- log(`Applying sort for column "${columnName}" (${direction})`);
615
- const context = Object.assign(Object.assign({}, createStrategyContext()), { getHeaderCell: result.getHeaderCell });
616
- const maxRetries = 3;
617
- for (let i = 0; i < maxRetries; i++) {
618
- const currentState = await config.strategies.sorting.getSortState({ columnName, context });
619
- if (currentState === direction) {
620
- log(`Sort for "${columnName}" is already "${direction}".`);
621
- return;
622
- }
623
- await config.strategies.sorting.doSort({ columnName, direction, context });
624
- if ((_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isTableLoading) {
625
- const timeout = (_d = (_c = config.strategies.loading.sortStabilizationTimeout) !== null && _c !== void 0 ? _c : config.strategies.loading.loadingTimeout) !== null && _d !== void 0 ? _d : 10000;
626
- const poll = (_e = config.strategies.loading.sortStabilizationPollInterval) !== null && _e !== void 0 ? _e : 100;
627
- await (0, loadingWait_1.waitWhileTableLoading)(config, context, rootLocator.page(), 'sorting.apply', {
628
- timeout,
629
- pollInterval: poll,
630
- });
631
- }
632
- else {
633
- const fallback = (_f = (_b = config.strategies.loading) === null || _b === void 0 ? void 0 : _b.sortStabilizationFallbackDelay) !== null && _f !== void 0 ? _f : 200;
634
- await rootLocator.page().waitForTimeout(fallback);
635
- }
636
- await (0, debugUtils_1.debugDelay)(config, 'default');
637
- const newState = await config.strategies.sorting.getSortState({ columnName, context });
638
- if (newState === direction) {
639
- log(`Successfully sorted "${columnName}" to "${direction}".`);
640
- return;
641
- }
642
- }
643
- throw new Error(`Failed to sort column "${columnName}" to "${direction}" after ${maxRetries} attempts.`);
448
+ return (0, sorting_1.applySort)(sortingEnv(), columnName, direction);
644
449
  },
645
450
  getState: async (columnName) => {
646
451
  log(`sorting.getState: column="${columnName}"`);
647
452
  await _autoInit();
648
- if (!config.strategies.sorting)
649
- throw new Error('No sorting strategy has been configured.');
650
- const context = Object.assign(Object.assign({}, createStrategyContext()), { getHeaderCell: result.getHeaderCell });
651
- return config.strategies.sorting.getSortState({ columnName, context });
453
+ return (0, sorting_1.getSortState)(sortingEnv(), columnName);
652
454
  }
653
455
  },
654
456
  // ─── Shared async row iterator ───────────────────────────────────────────
@@ -716,7 +518,7 @@ const useTable = (rootLocator, configOptions = {}) => {
716
518
  },
717
519
  // ─── Row iteration (delegated to engine/tableIteration) ──────────────────
718
520
  forEach: async (callback, options = {}) => {
719
- log(`forEach: options=${safeStringify(options)}`);
521
+ log(`forEach: options=${(0, safeStringify_1.safeStringify)(options)}`);
720
522
  await _autoInit();
721
523
  await (0, tableIteration_1.runForEach)({
722
524
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
@@ -731,7 +533,7 @@ const useTable = (rootLocator, configOptions = {}) => {
731
533
  }, callback, options);
732
534
  },
733
535
  map: async (callback, options = {}) => {
734
- log(`map: options=${safeStringify(options)}`);
536
+ log(`map: options=${(0, safeStringify_1.safeStringify)(options)}`);
735
537
  await _autoInit();
736
538
  return (0, tableIteration_1.runMap)({
737
539
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
@@ -746,7 +548,7 @@ const useTable = (rootLocator, configOptions = {}) => {
746
548
  }, callback, options);
747
549
  },
748
550
  toArray: async (callback, options = {}) => {
749
- log(`toArray: options=${safeStringify(options)}`);
551
+ log(`toArray: options=${(0, safeStringify_1.safeStringify)(options)}`);
750
552
  const cb = callback !== null && callback !== void 0 ? callback : (({ row }) => row.toJSON());
751
553
  try {
752
554
  return await result.map(cb, options);
@@ -756,7 +558,7 @@ const useTable = (rootLocator, configOptions = {}) => {
756
558
  }
757
559
  },
758
560
  filter: async (predicate, options = {}) => {
759
- log(`filter: options=${safeStringify(options)}`);
561
+ log(`filter: options=${(0, safeStringify_1.safeStringify)(options)}`);
760
562
  await _autoInit();
761
563
  return (0, tableIteration_1.runFilter)({
762
564
  getRowLocators: () => resolve(config.rowSelector, rootLocator),
@@ -51,7 +51,6 @@ class NavigationBarrier {
51
51
  * The fn receives a fresh `targetReached` check; if the previous row's scroll
52
52
  * already brought this row into view, fn can no-op immediately.
53
53
  */
54
- // fallow-ignore-next-line unused-class-member
55
54
  async runExclusiveRecovery(fn) {
56
55
  if (!this.recoveryMutex)
57
56
  this.recoveryMutex = new mutex_1.Mutex();
@@ -0,0 +1,2 @@
1
+ /** JSON.stringify for debug logs: functions become "[Function]" and cycles don't throw. */
2
+ export declare const safeStringify: (obj: any) => string;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeStringify = void 0;
4
+ /** JSON.stringify for debug logs: functions become "[Function]" and cycles don't throw. */
5
+ const safeStringify = (obj) => {
6
+ try {
7
+ return JSON.stringify(obj, (_key, value) => {
8
+ if (typeof value === 'function') {
9
+ return '[Function]';
10
+ }
11
+ return value;
12
+ });
13
+ }
14
+ catch (e) {
15
+ return '[unserializable]';
16
+ }
17
+ };
18
+ exports.safeStringify = safeStringify;
@@ -4,7 +4,7 @@ import { SmartRow } from '../types';
4
4
  */
5
5
  export interface SmartRowArray<T = any> extends Array<SmartRow<T>> {
6
6
  /**
7
- * Converts all rows to JSON objects
7
+ * Converts all rows to JSON objects, reading sequentially to avoid competing viewport moves.
8
8
  */
9
9
  toJSON(): Promise<T[]>;
10
10
  }