@rickcedwhat/playwright-smart-table 6.1.0 → 6.2.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.
Files changed (72) hide show
  1. package/README.md +2 -0
  2. package/dist/engine/rowFinder.d.ts +26 -0
  3. package/dist/engine/rowFinder.js +161 -0
  4. package/dist/engine/tableMapper.d.ts +16 -0
  5. package/dist/engine/tableMapper.js +136 -0
  6. package/dist/{examples/glide-strategies → strategies/glide}/columns.d.ts +1 -1
  7. package/dist/{examples/glide-strategies → strategies/glide}/headers.d.ts +1 -1
  8. package/dist/{src/strategies → strategies}/glide.js +2 -2
  9. package/dist/strategies/index.d.ts +33 -2
  10. package/dist/strategies/index.js +6 -0
  11. package/dist/{src/strategies → strategies}/loading.d.ts +14 -0
  12. package/dist/{src/strategies → strategies}/loading.js +31 -0
  13. package/dist/strategies/pagination.d.ts +26 -4
  14. package/dist/strategies/pagination.js +52 -23
  15. package/dist/typeContext.d.ts +1 -1
  16. package/dist/typeContext.js +32 -7
  17. package/dist/types.d.ts +37 -5
  18. package/dist/useTable.d.ts +32 -3
  19. package/dist/useTable.js +81 -224
  20. package/dist/utils/stringUtils.js +12 -3
  21. package/package.json +10 -5
  22. package/dist/src/filterEngine.d.ts +0 -11
  23. package/dist/src/filterEngine.js +0 -39
  24. package/dist/src/index.d.ts +0 -2
  25. package/dist/src/index.js +0 -18
  26. package/dist/src/smartRow.d.ts +0 -7
  27. package/dist/src/smartRow.js +0 -160
  28. package/dist/src/strategies/columns.d.ts +0 -18
  29. package/dist/src/strategies/columns.js +0 -21
  30. package/dist/src/strategies/fill.d.ts +0 -7
  31. package/dist/src/strategies/fill.js +0 -88
  32. package/dist/src/strategies/headers.d.ts +0 -13
  33. package/dist/src/strategies/headers.js +0 -30
  34. package/dist/src/strategies/index.d.ts +0 -54
  35. package/dist/src/strategies/index.js +0 -43
  36. package/dist/src/strategies/pagination.d.ts +0 -33
  37. package/dist/src/strategies/pagination.js +0 -79
  38. package/dist/src/strategies/resolution.d.ts +0 -22
  39. package/dist/src/strategies/resolution.js +0 -30
  40. package/dist/src/strategies/sorting.d.ts +0 -12
  41. package/dist/src/strategies/sorting.js +0 -68
  42. package/dist/src/strategies/validation.d.ts +0 -22
  43. package/dist/src/strategies/validation.js +0 -54
  44. package/dist/src/strategies/virtualizedPagination.d.ts +0 -32
  45. package/dist/src/strategies/virtualizedPagination.js +0 -80
  46. package/dist/src/typeContext.d.ts +0 -6
  47. package/dist/src/typeContext.js +0 -465
  48. package/dist/src/types.d.ts +0 -458
  49. package/dist/src/types.js +0 -2
  50. package/dist/src/useTable.d.ts +0 -44
  51. package/dist/src/useTable.js +0 -641
  52. package/dist/src/utils/debugUtils.d.ts +0 -17
  53. package/dist/src/utils/debugUtils.js +0 -62
  54. package/dist/src/utils/smartRowArray.d.ts +0 -14
  55. package/dist/src/utils/smartRowArray.js +0 -22
  56. package/dist/src/utils/stringUtils.d.ts +0 -22
  57. package/dist/src/utils/stringUtils.js +0 -73
  58. package/dist/src/utils.d.ts +0 -7
  59. package/dist/src/utils.js +0 -29
  60. package/dist/utils/traceUtils.d.ts +0 -11
  61. package/dist/utils/traceUtils.js +0 -47
  62. /package/dist/{src/plugins.d.ts → plugins.d.ts} +0 -0
  63. /package/dist/{src/plugins.js → plugins.js} +0 -0
  64. /package/dist/{src/strategies → strategies}/dedupe.d.ts +0 -0
  65. /package/dist/{src/strategies → strategies}/dedupe.js +0 -0
  66. /package/dist/{examples/glide-strategies → strategies/glide}/columns.js +0 -0
  67. /package/dist/{examples/glide-strategies → strategies/glide}/headers.js +0 -0
  68. /package/dist/{src/strategies → strategies}/glide.d.ts +0 -0
  69. /package/dist/{src/strategies → strategies}/rdg.d.ts +0 -0
  70. /package/dist/{src/strategies → strategies}/rdg.js +0 -0
  71. /package/dist/{src/strategies → strategies}/stabilization.d.ts +0 -0
  72. /package/dist/{src/strategies → strategies}/stabilization.js +0 -0
package/README.md CHANGED
@@ -79,6 +79,8 @@ const allActive = await table.findRows({ Status: 'Active' });
79
79
  ## Key Features
80
80
 
81
81
  - 🎯 **Smart Locators** - Find rows by content, not position
82
+ - 🧠 **Fuzzy Matching** - Smart suggestions for typos (e.g., incorrectly typed "Firstname" suggests "First Name" in error messages)
83
+ - ⚡ **Smart Initialization** - Handles loading states and dynamic headers automatically
82
84
  - 📄 **Auto-Pagination** - Search across all pages automatically
83
85
  - 🔍 **Column-Aware Access** - Access cells by column name
84
86
  - 🛠️ **Debug Mode** - Visual debugging with slow motion and logging
@@ -0,0 +1,26 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { FinalTableConfig, Selector, SmartRow } from '../types';
3
+ import { FilterEngine } from '../filterEngine';
4
+ import { TableMapper } from './tableMapper';
5
+ import { SmartRowArray } from '../utils/smartRowArray';
6
+ export declare class RowFinder<T = any> {
7
+ private rootLocator;
8
+ private config;
9
+ private filterEngine;
10
+ private tableMapper;
11
+ private makeSmartRow;
12
+ private resolve;
13
+ constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number) => SmartRow<T>);
14
+ private log;
15
+ findRow(filters: Record<string, string | RegExp | number>, options?: {
16
+ exact?: boolean;
17
+ maxPages?: number;
18
+ }): Promise<SmartRow<T>>;
19
+ findRows<R extends {
20
+ asJSON?: boolean;
21
+ }>(filters: Partial<T> | Record<string, string | RegExp | number>, options?: {
22
+ exact?: boolean;
23
+ maxPages?: number;
24
+ } & R): Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRowArray<T>>;
25
+ private findRowLocator;
26
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RowFinder = void 0;
13
+ const debugUtils_1 = require("../utils/debugUtils");
14
+ const smartRowArray_1 = require("../utils/smartRowArray");
15
+ const validation_1 = require("../strategies/validation");
16
+ class RowFinder {
17
+ constructor(rootLocator, config, resolve, filterEngine, tableMapper, makeSmartRow) {
18
+ this.rootLocator = rootLocator;
19
+ this.config = config;
20
+ this.filterEngine = filterEngine;
21
+ this.tableMapper = tableMapper;
22
+ this.makeSmartRow = makeSmartRow;
23
+ this.resolve = resolve;
24
+ }
25
+ log(msg) {
26
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
27
+ }
28
+ findRow(filters_1) {
29
+ return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
30
+ (0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
31
+ yield this.tableMapper.getMap();
32
+ const rowLocator = yield this.findRowLocator(filters, options);
33
+ if (rowLocator) {
34
+ (0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
35
+ yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
36
+ return this.makeSmartRow(rowLocator, yield this.tableMapper.getMap(), 0);
37
+ }
38
+ (0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
39
+ yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
40
+ const sentinel = this.resolve(this.config.rowSelector, this.rootLocator)
41
+ .filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
42
+ return this.makeSmartRow(sentinel, yield this.tableMapper.getMap(), 0);
43
+ });
44
+ }
45
+ findRows(filters, options) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ var _a, _b;
48
+ const map = yield this.tableMapper.getMap();
49
+ const allRows = [];
50
+ const effectiveMaxPages = (_b = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
51
+ let pageCount = 0;
52
+ const collectMatches = () => __awaiter(this, void 0, void 0, function* () {
53
+ var _a, _b;
54
+ let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
55
+ rowLocators = this.filterEngine.applyFilters(rowLocators, filters, map, (_a = options === null || options === void 0 ? void 0 : options.exact) !== null && _a !== void 0 ? _a : false, this.rootLocator.page());
56
+ const currentRows = yield rowLocators.all();
57
+ const isRowLoading = (_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isRowLoading;
58
+ for (let i = 0; i < currentRows.length; i++) {
59
+ const smartRow = this.makeSmartRow(currentRows[i], map, i);
60
+ if (isRowLoading && (yield isRowLoading(smartRow)))
61
+ continue;
62
+ allRows.push(smartRow);
63
+ }
64
+ });
65
+ // Scan first page
66
+ yield collectMatches();
67
+ // Pagination Loop
68
+ while (pageCount < effectiveMaxPages && this.config.strategies.pagination) {
69
+ // Check if pagination needed? findRows assumes we want ALL matches across maxPages.
70
+ // If explicit maxPages is set, we paginate. If global maxPages is 1 (default), we stop.
71
+ // Wait, loop condition `pageCount < effectiveMaxPages`. If maxPages=1, 0 < 1 is true.
72
+ // We paginate AFTER first scan.
73
+ // If maxPages=1, we should NOT paginate.
74
+ if (effectiveMaxPages <= 1)
75
+ break;
76
+ const context = {
77
+ root: this.rootLocator,
78
+ config: this.config,
79
+ resolve: this.resolve,
80
+ page: this.rootLocator.page()
81
+ };
82
+ const paginationResult = yield this.config.strategies.pagination(context);
83
+ const didPaginate = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
84
+ if (!didPaginate)
85
+ break;
86
+ pageCount++;
87
+ yield collectMatches();
88
+ }
89
+ if (options === null || options === void 0 ? void 0 : options.asJSON) {
90
+ return Promise.all(allRows.map(r => r.toJSON()));
91
+ }
92
+ return (0, smartRowArray_1.createSmartRowArray)(allRows);
93
+ });
94
+ }
95
+ findRowLocator(filters_1) {
96
+ return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
97
+ var _a, _b;
98
+ const map = yield this.tableMapper.getMap();
99
+ const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
100
+ let currentPage = 1;
101
+ this.log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
102
+ while (true) {
103
+ // Check Loading
104
+ if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
105
+ const isLoading = yield this.config.strategies.loading.isTableLoading({
106
+ root: this.rootLocator,
107
+ config: this.config,
108
+ page: this.rootLocator.page(),
109
+ resolve: this.resolve
110
+ });
111
+ if (isLoading) {
112
+ this.log('Table is loading... waiting');
113
+ yield this.rootLocator.page().waitForTimeout(200);
114
+ continue;
115
+ }
116
+ }
117
+ const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
118
+ const matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page());
119
+ const count = yield matchedRows.count();
120
+ this.log(`Page ${currentPage}: Found ${count} matches.`);
121
+ if (count > 1) {
122
+ const sampleData = [];
123
+ try {
124
+ const firstFewRows = yield matchedRows.all();
125
+ const sampleCount = Math.min(firstFewRows.length, 3);
126
+ for (let i = 0; i < sampleCount; i++) {
127
+ const rowData = yield this.makeSmartRow(firstFewRows[i], map, 0).toJSON();
128
+ sampleData.push(JSON.stringify(rowData));
129
+ }
130
+ }
131
+ catch (e) { }
132
+ const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
133
+ throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
134
+ `Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
135
+ }
136
+ if (count === 1)
137
+ return matchedRows.first();
138
+ if (currentPage < effectiveMaxPages) {
139
+ this.log(`Page ${currentPage}: Not found. Attempting pagination...`);
140
+ const context = {
141
+ root: this.rootLocator,
142
+ config: this.config,
143
+ resolve: this.resolve,
144
+ page: this.rootLocator.page()
145
+ };
146
+ const paginationResult = yield this.config.strategies.pagination(context);
147
+ const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
148
+ if (didLoadMore) {
149
+ currentPage++;
150
+ continue;
151
+ }
152
+ else {
153
+ this.log(`Page ${currentPage}: Pagination failed (end of data).`);
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ });
159
+ }
160
+ }
161
+ exports.RowFinder = RowFinder;
@@ -0,0 +1,16 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { FinalTableConfig, Selector } from '../types';
3
+ export declare class TableMapper {
4
+ private _headerMap;
5
+ private config;
6
+ private rootLocator;
7
+ private resolve;
8
+ constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator);
9
+ private log;
10
+ getMap(timeout?: number): Promise<Map<string, number>>;
11
+ remapHeaders(): Promise<void>;
12
+ getMapSync(): Map<string, number> | null;
13
+ isInitialized(): boolean;
14
+ clear(): void;
15
+ private processHeaders;
16
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TableMapper = void 0;
13
+ const headers_1 = require("../strategies/headers");
14
+ const debugUtils_1 = require("../utils/debugUtils");
15
+ class TableMapper {
16
+ constructor(rootLocator, config, resolve) {
17
+ this._headerMap = null;
18
+ this.rootLocator = rootLocator;
19
+ this.config = config;
20
+ this.resolve = resolve;
21
+ }
22
+ log(msg) {
23
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
24
+ }
25
+ getMap(timeout) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ var _a;
28
+ if (this._headerMap)
29
+ return this._headerMap;
30
+ this.log('Mapping headers...');
31
+ const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
32
+ const startTime = Date.now();
33
+ if (this.config.autoScroll) {
34
+ try {
35
+ yield this.rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
36
+ }
37
+ catch (e) { }
38
+ }
39
+ const headerLoc = this.resolve(this.config.headerSelector, this.rootLocator);
40
+ const strategy = this.config.strategies.header || headers_1.HeaderStrategies.visible;
41
+ const context = {
42
+ root: this.rootLocator,
43
+ config: this.config,
44
+ page: this.rootLocator.page(),
45
+ resolve: this.resolve
46
+ };
47
+ let lastError = null;
48
+ while (Date.now() - startTime < headerTimeout) {
49
+ // 1. Wait for visibility
50
+ try {
51
+ yield headerLoc.first().waitFor({ state: 'visible', timeout: 200 });
52
+ }
53
+ catch (e) {
54
+ // Continue to check existing/loading state even if not strictly "visible" yet
55
+ }
56
+ // 2. Check Smart Loading State
57
+ if ((_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isHeaderLoading) {
58
+ const isStable = !(yield this.config.strategies.loading.isHeaderLoading(context));
59
+ if (!isStable) {
60
+ this.log('Headers are loading/unstable... waiting');
61
+ yield new Promise(r => setTimeout(r, 100));
62
+ continue;
63
+ }
64
+ }
65
+ // 3. Attempt Scan
66
+ try {
67
+ const rawHeaders = yield strategy(context);
68
+ const entries = yield this.processHeaders(rawHeaders);
69
+ // Success
70
+ this._headerMap = new Map(entries);
71
+ this.log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
72
+ return this._headerMap;
73
+ }
74
+ catch (e) {
75
+ lastError = e;
76
+ this.log(`Header mapping failed (retrying): ${e.message}`);
77
+ yield new Promise(r => setTimeout(r, 100));
78
+ }
79
+ }
80
+ throw lastError || new Error(`Timed out waiting for headers after ${headerTimeout}ms`);
81
+ });
82
+ }
83
+ remapHeaders() {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ this._headerMap = null;
86
+ yield this.getMap();
87
+ });
88
+ }
89
+ getMapSync() {
90
+ return this._headerMap;
91
+ }
92
+ isInitialized() {
93
+ return this._headerMap !== null;
94
+ }
95
+ clear() {
96
+ this._headerMap = null;
97
+ }
98
+ processHeaders(rawHeaders) {
99
+ return __awaiter(this, void 0, void 0, function* () {
100
+ const seenHeaders = new Set();
101
+ const entries = [];
102
+ for (let i = 0; i < rawHeaders.length; i++) {
103
+ let text = rawHeaders[i].trim() || `__col_${i}`;
104
+ if (this.config.headerTransformer) {
105
+ text = yield this.config.headerTransformer({
106
+ text,
107
+ index: i,
108
+ locator: this.rootLocator.locator(this.config.headerSelector).nth(i),
109
+ seenHeaders
110
+ });
111
+ }
112
+ entries.push([text, i]);
113
+ seenHeaders.add(text);
114
+ }
115
+ // Validation: Check for empty table
116
+ if (entries.length === 0) {
117
+ throw new Error(`Initialization Error: No columns found using selector "${this.config.headerSelector}". Check your selector or ensure the table is visible.`);
118
+ }
119
+ // Validation: Check for duplicates
120
+ const seen = new Set();
121
+ const duplicates = new Set();
122
+ for (const [name] of entries) {
123
+ if (seen.has(name)) {
124
+ duplicates.add(name);
125
+ }
126
+ seen.add(name);
127
+ }
128
+ if (duplicates.size > 0) {
129
+ const dupList = Array.from(duplicates).map(d => `"${d}"`).join(', ');
130
+ throw new Error(`Initialization Error: Duplicate column names found: ${dupList}. Use 'headerTransformer' to rename duplicate columns.`);
131
+ }
132
+ return entries;
133
+ });
134
+ }
135
+ }
136
+ exports.TableMapper = TableMapper;
@@ -1,4 +1,4 @@
1
- import { StrategyContext } from '../../src/types';
1
+ import { StrategyContext } from '../../types';
2
2
  /**
3
3
  * Strategy that clicks into the table to establish focus and then uses the Right Arrow key
4
4
  * to navigate to the target CELL (navigates down to the row, then right to the column).
@@ -1,4 +1,4 @@
1
- import { StrategyContext } from '../../src/types';
1
+ import { StrategyContext } from '../../types';
2
2
  /**
3
3
  * Scans for headers by finding a scrollable container and setting scrollLeft.
4
4
  */
@@ -10,8 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.GlideStrategies = exports.glideGetActiveCell = exports.glideGetCellLocator = exports.glidePaginationStrategy = exports.glideFillStrategy = void 0;
13
- const columns_1 = require("../../examples/glide-strategies/columns");
14
- const headers_1 = require("../../examples/glide-strategies/headers");
13
+ const columns_1 = require("./glide/columns");
14
+ const headers_1 = require("./glide/headers");
15
15
  const pagination_1 = require("./pagination");
16
16
  const stabilization_1 = require("./stabilization");
17
17
  const glideFillStrategy = (_a) => __awaiter(void 0, [_a], void 0, function* ({ value, page }) {
@@ -4,10 +4,21 @@ export * from './columns';
4
4
  export * from './headers';
5
5
  export * from './fill';
6
6
  export * from './resolution';
7
+ export * from './dedupe';
8
+ export * from './loading';
7
9
  export declare const Strategies: {
8
10
  Pagination: {
9
- clickNext: (nextButtonSelector: import("..").Selector, timeout?: number) => import("..").PaginationStrategy;
10
- infiniteScroll: (timeout?: number) => import("..").PaginationStrategy;
11
+ clickNext: (nextButtonSelector: import("..").Selector, options?: {
12
+ stabilization?: import("./stabilization").StabilizationStrategy;
13
+ timeout?: number;
14
+ }) => import("..").PaginationStrategy;
15
+ infiniteScroll: (options?: {
16
+ action?: "scroll" | "js-scroll";
17
+ scrollTarget?: import("..").Selector;
18
+ scrollAmount?: number;
19
+ stabilization?: import("./stabilization").StabilizationStrategy;
20
+ timeout?: number;
21
+ }) => import("..").PaginationStrategy;
11
22
  };
12
23
  Sorting: {
13
24
  AriaSort: () => import("..").SortingStrategy;
@@ -24,4 +35,24 @@ export declare const Strategies: {
24
35
  Resolution: {
25
36
  default: import("./resolution").ColumnResolutionStrategy;
26
37
  };
38
+ Dedupe: {
39
+ byTopPosition: (tolerance?: number) => import("..").DedupeStrategy;
40
+ };
41
+ Loading: {
42
+ Table: {
43
+ hasSpinner: (selector?: string) => ({ root }: import("..").TableContext) => Promise<boolean>;
44
+ custom: (fn: (context: import("..").TableContext) => Promise<boolean>) => (context: import("..").TableContext) => Promise<boolean>;
45
+ never: () => Promise<boolean>;
46
+ };
47
+ Row: {
48
+ hasClass: (className?: string) => (row: import("..").SmartRow) => Promise<boolean>;
49
+ hasText: (text?: string | RegExp) => (row: import("..").SmartRow) => Promise<boolean>;
50
+ hasEmptyCells: () => (row: import("..").SmartRow) => Promise<boolean>;
51
+ never: () => Promise<boolean>;
52
+ };
53
+ Headers: {
54
+ stable: (duration?: number) => (context: import("..").TableContext) => Promise<boolean>;
55
+ never: () => Promise<boolean>;
56
+ };
57
+ };
27
58
  };
@@ -21,12 +21,16 @@ const columns_1 = require("./columns");
21
21
  const headers_1 = require("./headers");
22
22
  const fill_1 = require("./fill");
23
23
  const resolution_1 = require("./resolution");
24
+ const dedupe_1 = require("./dedupe");
25
+ const loading_1 = require("./loading");
24
26
  __exportStar(require("./pagination"), exports);
25
27
  __exportStar(require("./sorting"), exports);
26
28
  __exportStar(require("./columns"), exports);
27
29
  __exportStar(require("./headers"), exports);
28
30
  __exportStar(require("./fill"), exports);
29
31
  __exportStar(require("./resolution"), exports);
32
+ __exportStar(require("./dedupe"), exports);
33
+ __exportStar(require("./loading"), exports);
30
34
  exports.Strategies = {
31
35
  Pagination: pagination_1.PaginationStrategies,
32
36
  Sorting: sorting_1.SortingStrategies,
@@ -34,4 +38,6 @@ exports.Strategies = {
34
38
  Header: headers_1.HeaderStrategies,
35
39
  Fill: fill_1.FillStrategies,
36
40
  Resolution: resolution_1.ResolutionStrategies,
41
+ Dedupe: dedupe_1.DedupeStrategies,
42
+ Loading: loading_1.LoadingStrategies,
37
43
  };
@@ -45,4 +45,18 @@ export declare const LoadingStrategies: {
45
45
  */
46
46
  never: () => Promise<boolean>;
47
47
  };
48
+ /**
49
+ * Strategies for detecting if headers are loading/stable.
50
+ */
51
+ Headers: {
52
+ /**
53
+ * Checks if the headers are stable (count and text) for a specified duration.
54
+ * @param duration Duration in ms for headers to remain unchanged to be considered stable (default: 200).
55
+ */
56
+ stable: (duration?: number) => (context: TableContext) => Promise<boolean>;
57
+ /**
58
+ * Assume headers are never loading (immediate snapshot).
59
+ */
60
+ never: () => Promise<boolean>;
61
+ };
48
62
  };
@@ -78,5 +78,36 @@ exports.LoadingStrategies = {
78
78
  * Assume row is never loading (default).
79
79
  */
80
80
  never: () => __awaiter(void 0, void 0, void 0, function* () { return false; })
81
+ },
82
+ /**
83
+ * Strategies for detecting if headers are loading/stable.
84
+ */
85
+ Headers: {
86
+ /**
87
+ * Checks if the headers are stable (count and text) for a specified duration.
88
+ * @param duration Duration in ms for headers to remain unchanged to be considered stable (default: 200).
89
+ */
90
+ stable: (duration = 200) => (context) => __awaiter(void 0, void 0, void 0, function* () {
91
+ const { config, resolve, root } = context;
92
+ const getHeaderTexts = () => __awaiter(void 0, void 0, void 0, function* () {
93
+ const headers = yield resolve(config.headerSelector, root).all();
94
+ return Promise.all(headers.map(h => h.innerText()));
95
+ });
96
+ const initial = yield getHeaderTexts();
97
+ // Wait for duration
98
+ yield context.page.waitForTimeout(duration);
99
+ const current = yield getHeaderTexts();
100
+ if (initial.length !== current.length)
101
+ return true; // Count changed, still loading
102
+ for (let i = 0; i < initial.length; i++) {
103
+ if (initial[i] !== current[i])
104
+ return true; // Content changed, still loading
105
+ }
106
+ return false; // Stable
107
+ }),
108
+ /**
109
+ * Assume headers are never loading (immediate snapshot).
110
+ */
111
+ never: () => __awaiter(void 0, void 0, void 0, function* () { return false; })
81
112
  }
82
113
  };
@@ -1,11 +1,33 @@
1
1
  import type { PaginationStrategy, Selector } from '../types';
2
+ import { StabilizationStrategy } from './stabilization';
2
3
  export declare const PaginationStrategies: {
3
4
  /**
4
- * Strategy: Clicks a "Next" button and waits for the first row of data to change.
5
+ * Strategy: Clicks a "Next" button and waits for stabilization.
6
+ * @param nextButtonSelector Selector for the next page button.
7
+ * @param options.stabilization Strategy to determine when the page has updated.
8
+ * Defaults to `contentChanged({ scope: 'first' })`.
9
+ * @param options.timeout Timeout for the click action.
5
10
  */
6
- clickNext: (nextButtonSelector: Selector, timeout?: number) => PaginationStrategy;
11
+ clickNext: (nextButtonSelector: Selector, options?: {
12
+ stabilization?: StabilizationStrategy;
13
+ timeout?: number;
14
+ }) => PaginationStrategy;
7
15
  /**
8
- * Strategy: Scrolls to the bottom and waits for more rows to appear.
16
+ * Strategy: Infinite Scroll (generic).
17
+ * Supports both simple "Scroll to Bottom" and "Virtualized Scroll".
18
+ *
19
+ * @param options.action 'scroll' (mouse wheel) or 'js-scroll' (direct scrollTop).
20
+ * @param options.scrollTarget Selector for the scroll container (defaults to table root).
21
+ * @param options.scrollAmount Amount to scroll in pixels (default 500).
22
+ * @param options.stabilization Strategy to determine if new content loaded.
23
+ * Defaults to `rowCountIncreased` (simple append).
24
+ * Use `contentChanged` for virtualization.
9
25
  */
10
- infiniteScroll: (timeout?: number) => PaginationStrategy;
26
+ infiniteScroll: (options?: {
27
+ action?: "scroll" | "js-scroll";
28
+ scrollTarget?: Selector;
29
+ scrollAmount?: number;
30
+ stabilization?: StabilizationStrategy;
31
+ timeout?: number;
32
+ }) => PaginationStrategy;
11
33
  };
@@ -10,41 +10,70 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.PaginationStrategies = void 0;
13
- const utils_1 = require("../utils");
13
+ const stabilization_1 = require("./stabilization");
14
14
  exports.PaginationStrategies = {
15
15
  /**
16
- * Strategy: Clicks a "Next" button and waits for the first row of data to change.
16
+ * Strategy: Clicks a "Next" button and waits for stabilization.
17
+ * @param nextButtonSelector Selector for the next page button.
18
+ * @param options.stabilization Strategy to determine when the page has updated.
19
+ * Defaults to `contentChanged({ scope: 'first' })`.
20
+ * @param options.timeout Timeout for the click action.
17
21
  */
18
- clickNext: (nextButtonSelector, timeout = 5000) => {
19
- return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root, config, resolve, page }) {
22
+ clickNext: (nextButtonSelector, options = {}) => {
23
+ return (context) => __awaiter(void 0, void 0, void 0, function* () {
24
+ var _a;
25
+ const { root, resolve, page } = context;
20
26
  const nextBtn = resolve(nextButtonSelector, root).first();
21
27
  if (!(yield nextBtn.isVisible()) || !(yield nextBtn.isEnabled())) {
22
28
  return false;
23
29
  }
24
- const firstRow = resolve(config.rowSelector, root).first();
25
- const oldText = yield firstRow.innerText().catch(() => "");
26
- yield nextBtn.click({ timeout: 2000 }).catch(() => { });
27
- const success = yield (0, utils_1.waitForCondition)(() => __awaiter(void 0, void 0, void 0, function* () {
28
- const newText = yield firstRow.innerText().catch(() => "");
29
- return newText !== oldText;
30
- }), timeout, page);
30
+ // Default stabilization: Wait for first row content to change
31
+ const stabilization = (_a = options.stabilization) !== null && _a !== void 0 ? _a : stabilization_1.StabilizationStrategies.contentChanged({ scope: 'first', timeout: options.timeout });
32
+ // Stabilization: Wrap action
33
+ const success = yield stabilization(context, () => __awaiter(void 0, void 0, void 0, function* () {
34
+ yield nextBtn.click({ timeout: 2000 }).catch(() => { });
35
+ }));
31
36
  return success;
32
37
  });
33
38
  },
34
39
  /**
35
- * Strategy: Scrolls to the bottom and waits for more rows to appear.
40
+ * Strategy: Infinite Scroll (generic).
41
+ * Supports both simple "Scroll to Bottom" and "Virtualized Scroll".
42
+ *
43
+ * @param options.action 'scroll' (mouse wheel) or 'js-scroll' (direct scrollTop).
44
+ * @param options.scrollTarget Selector for the scroll container (defaults to table root).
45
+ * @param options.scrollAmount Amount to scroll in pixels (default 500).
46
+ * @param options.stabilization Strategy to determine if new content loaded.
47
+ * Defaults to `rowCountIncreased` (simple append).
48
+ * Use `contentChanged` for virtualization.
36
49
  */
37
- infiniteScroll: (timeout = 5000) => {
38
- return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root, config, resolve, page }) {
39
- const rows = resolve(config.rowSelector, root);
40
- const oldCount = yield rows.count();
41
- if (oldCount === 0)
42
- return false;
43
- yield rows.last().scrollIntoViewIfNeeded();
44
- return yield (0, utils_1.waitForCondition)(() => __awaiter(void 0, void 0, void 0, function* () {
45
- const newCount = yield rows.count();
46
- return newCount > oldCount;
47
- }), timeout, page);
50
+ infiniteScroll: (options = {}) => {
51
+ return (context) => __awaiter(void 0, void 0, void 0, function* () {
52
+ var _a, _b;
53
+ const { root, resolve, page } = context;
54
+ const scrollTarget = options.scrollTarget
55
+ ? resolve(options.scrollTarget, root)
56
+ : root;
57
+ // Default stabilization: Wait for row count to increase (Append mode)
58
+ const stabilization = (_a = options.stabilization) !== null && _a !== void 0 ? _a : stabilization_1.StabilizationStrategies.rowCountIncreased({ timeout: options.timeout });
59
+ const amount = (_b = options.scrollAmount) !== null && _b !== void 0 ? _b : 500;
60
+ const doScroll = () => __awaiter(void 0, void 0, void 0, function* () {
61
+ const box = yield scrollTarget.boundingBox();
62
+ // Action: Scroll
63
+ if (options.action === 'js-scroll' || !box) {
64
+ yield scrollTarget.evaluate((el, y) => {
65
+ el.scrollTop += y;
66
+ }, amount);
67
+ }
68
+ else {
69
+ // Mouse Wheel
70
+ yield page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
71
+ yield page.mouse.wheel(0, amount);
72
+ }
73
+ });
74
+ // Stabilization: Wait
75
+ const success = yield stabilization(context, doScroll);
76
+ return success;
48
77
  });
49
78
  }
50
79
  };