@rickcedwhat/playwright-smart-table 6.24.0 → 6.25.0-next.0d74239

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.
Files changed (40) hide show
  1. package/dist/engine/cellNavigation.d.ts +24 -0
  2. package/dist/engine/cellNavigation.js +326 -0
  3. package/dist/engine/countRows.d.ts +28 -0
  4. package/dist/engine/countRows.js +149 -0
  5. package/dist/engine/rowByIndex.d.ts +20 -0
  6. package/dist/engine/rowByIndex.js +79 -0
  7. package/dist/engine/rowFinder.d.ts +4 -0
  8. package/dist/engine/rowFinder.js +15 -4
  9. package/dist/engine/rowSnapshot.d.ts +28 -0
  10. package/dist/engine/rowSnapshot.js +407 -0
  11. package/dist/engine/scanPages.d.ts +7 -2
  12. package/dist/engine/scanPages.js +11 -0
  13. package/dist/engine/sorting.d.ts +11 -0
  14. package/dist/engine/sorting.js +51 -0
  15. package/dist/engine/tableIteration.js +3 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/packageVersion.d.ts +1 -1
  18. package/dist/packageVersion.js +1 -1
  19. package/dist/presets/glide/index.js +0 -1
  20. package/dist/presets/rdg.js +0 -2
  21. package/dist/smartRow.js +51 -721
  22. package/dist/strategies/columns.d.ts +1 -1
  23. package/dist/strategies/contentReady.d.ts +19 -2
  24. package/dist/strategies/contentReady.js +81 -0
  25. package/dist/strategies/index.d.ts +7 -3
  26. package/dist/strategies/resolution.d.ts +1 -3
  27. package/dist/strategies/resolution.js +0 -28
  28. package/dist/types.d.ts +36 -7
  29. package/dist/useTable.js +51 -241
  30. package/dist/utils/locatorDescription.d.ts +15 -0
  31. package/dist/utils/locatorDescription.js +56 -0
  32. package/dist/utils/navigationBarrier.d.ts +0 -2
  33. package/dist/utils/navigationBarrier.js +0 -4
  34. package/dist/utils/safeStringify.d.ts +2 -0
  35. package/dist/utils/safeStringify.js +18 -0
  36. package/package.json +5 -6
  37. package/dist/typeContext.d.ts +0 -6
  38. package/dist/typeContext.js +0 -1133
  39. package/dist/utils.d.ts +0 -7
  40. package/dist/utils.js +0 -21
@@ -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,6 +1,23 @@
1
- import type { ContentReadyStrategy } from '../types';
2
- export type { ContentReadyStrategy };
1
+ import type { ContentReadyStrategy, RowReadyPredicate } from '../types';
3
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;
4
21
  /**
5
22
  * Polls the row's text content until two consecutive reads match.
6
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
  */
@@ -3,7 +3,6 @@ export * from './sorting';
3
3
  export * from './columns';
4
4
  export * from './headers';
5
5
  export * from './fill';
6
- export type { ColumnResolutionStrategy } from './resolution';
7
6
  export * from './dedupe';
8
7
  export * from './loading';
9
8
  export * from './stabilization';
@@ -99,13 +98,18 @@ export declare const Strategies: {
99
98
  dataAttribute: (options?: import("./viewport").DataAttributeViewportOptions) => import("..").ViewportStrategy;
100
99
  };
101
100
  ContentReady: {
101
+ until: <Arg = undefined>(predicate: import("..").RowReadyPredicate<Arg>, options?: {
102
+ arg?: Arg;
103
+ timeout?: number;
104
+ interval?: number;
105
+ }) => import("..").ContentReadyStrategy;
102
106
  textStable: (options?: {
103
107
  timeout?: number;
104
108
  interval?: number;
105
- }) => import("./contentReady").ContentReadyStrategy;
109
+ }) => import("..").ContentReadyStrategy;
106
110
  mutationSettled: (options?: {
107
111
  timeout?: number;
108
112
  quietPeriod?: number;
109
- }) => import("./contentReady").ContentReadyStrategy;
113
+ }) => import("..").ContentReadyStrategy;
110
114
  };
111
115
  };
@@ -1,4 +1,5 @@
1
1
  import { StrategyContext } from '../types';
2
+ /** @deprecated Not used by the library. Will be removed in v7. */
2
3
  export interface ColumnResolutionStrategy {
3
4
  /**
4
5
  * Resolves a column name (string or Regex) to a column index.
@@ -17,6 +18,3 @@ export interface ColumnResolutionStrategy {
17
18
  headerMap: Map<string, number>;
18
19
  }): string;
19
20
  }
20
- export declare const ResolutionStrategies: {
21
- default: ColumnResolutionStrategy;
22
- };
@@ -1,30 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ResolutionStrategies = void 0;
4
- exports.ResolutionStrategies = {
5
- default: {
6
- resolveIndex: ({ query, headerMap }) => {
7
- // 1. Exact / String Match
8
- if (typeof query === 'string') {
9
- if (headerMap.has(query))
10
- return headerMap.get(query);
11
- }
12
- // 2. Regex Match
13
- if (query instanceof RegExp) {
14
- for (const [colName, idx] of headerMap.entries()) {
15
- if (query.test(colName))
16
- return idx;
17
- }
18
- return undefined;
19
- }
20
- // 3. (Optional) Fuzzy String Match fallback could go here
21
- // But for strict default strategy, we might want to keep it simple first
22
- // The original code didn't do fuzzy *resolution* logic inside the get(), it just did strict get().
23
- // The fuzzy logic was only for *suggestions* on error.
24
- return undefined;
25
- },
26
- resolveName: ({ query }) => {
27
- return query.toString();
28
- }
29
- }
30
- };
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
  /**
@@ -760,6 +768,14 @@ export type RowIterationContext<T = any> = {
760
768
  index: number;
761
769
  /** 0-based page index — which page this row was collected from. */
762
770
  pageIndex: number;
771
+ /**
772
+ * Ends the iteration. No further pages are loaded, and the result of `map` / `filter`
773
+ * includes this row and nothing after it. If several rows call `stop()`, the earliest one wins.
774
+ *
775
+ * In `'sequential'` mode (the default) no callback after this row runs. In `'parallel'` and
776
+ * `'synchronized'` mode the rest of the current page is already in flight, so later rows'
777
+ * callbacks may still run (and their side effects happen) even though their results are dropped.
778
+ */
763
779
  stop: () => void;
764
780
  };
765
781
  /** Concurrency modes for row iteration. */
@@ -786,6 +802,11 @@ export type RowIterationOptions = {
786
802
  * @default false — uses goNext for one-page-at-a-time advancement
787
803
  */
788
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;
789
810
  };
790
811
  /**
791
812
  * Result of {@link useTable}. Implements `AsyncIterable` — `for await (const { row } of table)`
@@ -868,31 +889,35 @@ export interface TableResult<T = any> extends AsyncIterable<{
868
889
  * is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
869
890
  *
870
891
  * @param index 0-based logical/data-model row index
871
- * @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`.
872
894
  */
873
895
  findRowByIndex: (index: number, options?: {
874
896
  maxPages?: number;
897
+ signal?: AbortSignal;
875
898
  }) => Promise<SmartRow<T>>;
876
899
  /**
877
900
  * ASYNC: Searches for a single row across pages using pagination.
878
901
  * Auto-initializes the table if not already initialized.
879
902
  * @param filters - The filter criteria to match
880
- * @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`.
881
904
  */
882
905
  findRow: (filters: Record<string, FilterValue>, options?: {
883
906
  exact?: boolean;
884
907
  maxPages?: number;
908
+ signal?: AbortSignal;
885
909
  }) => Promise<SmartRow<T>>;
886
910
  /**
887
911
  * ASYNC: Searches for all matching rows across pages using pagination.
888
912
  * Auto-initializes the table if not already initialized.
889
913
  * @param filters - The filter criteria to match (omit or pass {} for all rows)
890
- * @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).
914
+ * @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`.
891
915
  */
892
916
  findRows: (filters?: Record<string, FilterValue>, options?: {
893
917
  exact?: boolean;
894
918
  maxPages?: number;
895
919
  useBulkPagination?: boolean;
920
+ signal?: AbortSignal;
896
921
  }) => Promise<SmartRowArray<T>>;
897
922
  /**
898
923
  * Resolves the named column and scrolls it into view.
@@ -903,11 +928,13 @@ export interface TableResult<T = any> extends AsyncIterable<{
903
928
  /**
904
929
  * Counts the number of rows, optionally filtered.
905
930
  * Without arguments, counts all rows. With filters, counts only matching rows.
906
- * Paginates when pagination is configured.
931
+ * Paginates when pagination is configured. `signal` cancels the page scan; the call
932
+ * rejects with `signal.reason` (the table is still reset to page 1).
907
933
  */
908
934
  countRows: (filters?: Record<string, FilterValue>, options?: {
909
935
  exact?: boolean;
910
936
  maxPages?: number;
937
+ signal?: AbortSignal;
911
938
  }) => Promise<number>;
912
939
  /**
913
940
  * Iterates over rows and extracts the value of a single column.
@@ -935,7 +962,8 @@ export interface TableResult<T = any> extends AsyncIterable<{
935
962
  /**
936
963
  * Iterates every row across all pages, calling the callback for side effects.
937
964
  * Execution is sequential by default (safe for interactions like clicking/filling).
938
- * Call `stop()` in the callback to end iteration early.
965
+ * Call `stop()` in the callback to end iteration early. In sequential mode no later row runs;
966
+ * in parallel mode the rest of the current page may still run (see `RowIterationContext.stop`).
939
967
  *
940
968
  * @param callback - Function receiving { row, rowIndex, stop }
941
969
  * @param options - maxPages, concurrency, dedupe, useBulkPagination
@@ -952,7 +980,8 @@ export interface TableResult<T = any> extends AsyncIterable<{
952
980
  * Defaults to `concurrency: 'sequential'` (safe for clicks/fills). Pass
953
981
  * `concurrency: 'parallel'` for read-only extraction, or `'synchronized'` when
954
982
  * navigation must stay lock-step on virtualized grids.
955
- * Call `stop()` to halt after the current page finishes.
983
+ * Call `stop()` to end early: the result includes the row that called it and nothing after it
984
+ * (in parallel mode, later rows on the same page may still run; see `RowIterationContext.stop`).
956
985
  *
957
986
  * @param callback - Function receiving { row, rowIndex, stop }
958
987
  * @param options - maxPages, concurrency, dedupe, useBulkPagination