@rickcedwhat/playwright-smart-table 6.1.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/engine/rowFinder.d.ts +27 -0
- package/dist/engine/rowFinder.js +193 -0
- package/dist/engine/tableMapper.d.ts +16 -0
- package/dist/engine/tableMapper.js +136 -0
- package/dist/filterEngine.d.ts +2 -2
- package/dist/filterEngine.js +15 -4
- package/dist/smartRow.d.ts +1 -1
- package/dist/smartRow.js +57 -36
- package/dist/strategies/index.d.ts +4 -0
- package/dist/strategies/loading.d.ts +14 -0
- package/dist/strategies/loading.js +31 -0
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +38 -14
- package/dist/types.d.ts +34 -14
- package/dist/useTable.d.ts +5 -1
- package/dist/useTable.js +45 -242
- package/dist/utils/stringUtils.js +12 -3
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Production-ready table testing for Playwright with smart column-aware locators.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -79,10 +79,12 @@ 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
|
|
85
|
-
- 🔌 **Extensible Strategies** - Support any table implementation
|
|
87
|
+
- 🔌 **[Extensible Strategies](docs/concepts/strategies.md)** - Support any table implementation
|
|
86
88
|
- 💪 **Type-Safe** - Full TypeScript support
|
|
87
89
|
- 🚀 **Production-Ready** - Battle-tested in real-world applications
|
|
88
90
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
|
+
import { FinalTableConfig, Selector, SmartRow, FilterValue } 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, FilterValue>, options?: {
|
|
16
|
+
exact?: boolean;
|
|
17
|
+
maxPages?: number;
|
|
18
|
+
}): Promise<SmartRow<T>>;
|
|
19
|
+
findRows(filtersOrOptions?: (Partial<T> | Record<string, FilterValue>) & ({
|
|
20
|
+
exact?: boolean;
|
|
21
|
+
maxPages?: number;
|
|
22
|
+
}), legacyOptions?: {
|
|
23
|
+
exact?: boolean;
|
|
24
|
+
maxPages?: number;
|
|
25
|
+
}): Promise<SmartRowArray<T>>;
|
|
26
|
+
private findRowLocator;
|
|
27
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.RowFinder = void 0;
|
|
24
|
+
const debugUtils_1 = require("../utils/debugUtils");
|
|
25
|
+
const smartRowArray_1 = require("../utils/smartRowArray");
|
|
26
|
+
const validation_1 = require("../strategies/validation");
|
|
27
|
+
class RowFinder {
|
|
28
|
+
constructor(rootLocator, config, resolve, filterEngine, tableMapper, makeSmartRow) {
|
|
29
|
+
this.rootLocator = rootLocator;
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.filterEngine = filterEngine;
|
|
32
|
+
this.tableMapper = tableMapper;
|
|
33
|
+
this.makeSmartRow = makeSmartRow;
|
|
34
|
+
this.resolve = resolve;
|
|
35
|
+
}
|
|
36
|
+
log(msg) {
|
|
37
|
+
(0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
|
|
38
|
+
}
|
|
39
|
+
findRow(filters_1) {
|
|
40
|
+
return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
|
|
41
|
+
(0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
|
|
42
|
+
yield this.tableMapper.getMap();
|
|
43
|
+
const rowLocator = yield this.findRowLocator(filters, options);
|
|
44
|
+
if (rowLocator) {
|
|
45
|
+
(0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
|
|
46
|
+
yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
47
|
+
return this.makeSmartRow(rowLocator, yield this.tableMapper.getMap(), 0);
|
|
48
|
+
}
|
|
49
|
+
(0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
|
|
50
|
+
yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
|
|
51
|
+
const sentinel = this.resolve(this.config.rowSelector, this.rootLocator)
|
|
52
|
+
.filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
53
|
+
return this.makeSmartRow(sentinel, yield this.tableMapper.getMap(), 0);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
findRows(filtersOrOptions,
|
|
57
|
+
// Deprecated: verify legacy usage pattern support
|
|
58
|
+
legacyOptions) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
// Detect argument pattern:
|
|
61
|
+
// Pattern A: findRows({ Name: 'Alice' }, { maxPages: 5 })
|
|
62
|
+
// Pattern B: findRows({ maxPages: 5 }) <-- No filters, just options
|
|
63
|
+
// Pattern C: findRows({ Name: 'Alice' }) <-- Only filters
|
|
64
|
+
var _a, _b;
|
|
65
|
+
let filters = {};
|
|
66
|
+
let options = {};
|
|
67
|
+
if (legacyOptions) {
|
|
68
|
+
// Pattern A
|
|
69
|
+
filters = filtersOrOptions;
|
|
70
|
+
options = legacyOptions;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Pattern B or C
|
|
74
|
+
// We need to separate unknown keys (filters) from known options (exact, maxPages)
|
|
75
|
+
// However, filtersOrOptions can be null/undefined
|
|
76
|
+
if (filtersOrOptions) {
|
|
77
|
+
const _c = filtersOrOptions, { exact, maxPages } = _c, rest = __rest(_c, ["exact", "maxPages"]);
|
|
78
|
+
options = { exact, maxPages };
|
|
79
|
+
filters = rest;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const map = yield this.tableMapper.getMap();
|
|
83
|
+
const allRows = [];
|
|
84
|
+
const effectiveMaxPages = (_b = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
|
|
85
|
+
let pageCount = 0;
|
|
86
|
+
const collectMatches = () => __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
var _a, _b;
|
|
88
|
+
// ... logic ...
|
|
89
|
+
let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
90
|
+
// Only apply filters if we have them
|
|
91
|
+
if (Object.keys(filters).length > 0) {
|
|
92
|
+
rowLocators = this.filterEngine.applyFilters(rowLocators, filters, map, (_a = options.exact) !== null && _a !== void 0 ? _a : false, this.rootLocator.page());
|
|
93
|
+
}
|
|
94
|
+
const currentRows = yield rowLocators.all();
|
|
95
|
+
const isRowLoading = (_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isRowLoading;
|
|
96
|
+
for (let i = 0; i < currentRows.length; i++) {
|
|
97
|
+
const smartRow = this.makeSmartRow(currentRows[i], map, allRows.length + i);
|
|
98
|
+
if (isRowLoading && (yield isRowLoading(smartRow)))
|
|
99
|
+
continue;
|
|
100
|
+
allRows.push(smartRow);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
// Scan first page
|
|
104
|
+
yield collectMatches();
|
|
105
|
+
// Pagination Loop - Corrected logic
|
|
106
|
+
// We always scan at least 1 page.
|
|
107
|
+
// If maxPages > 1, and we have a pagination strategy, we try to go next.
|
|
108
|
+
while (pageCount < effectiveMaxPages - 1 && this.config.strategies.pagination) {
|
|
109
|
+
const context = {
|
|
110
|
+
root: this.rootLocator,
|
|
111
|
+
config: this.config,
|
|
112
|
+
resolve: this.resolve,
|
|
113
|
+
page: this.rootLocator.page()
|
|
114
|
+
};
|
|
115
|
+
// Check if we should stop? (e.g. if we found enough rows? No, findRows finds ALL)
|
|
116
|
+
const paginationResult = yield this.config.strategies.pagination(context);
|
|
117
|
+
const didPaginate = yield (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
|
|
118
|
+
if (!didPaginate)
|
|
119
|
+
break;
|
|
120
|
+
pageCount++;
|
|
121
|
+
// Wait for reload logic if needed? Usually pagination handles it.
|
|
122
|
+
yield collectMatches();
|
|
123
|
+
}
|
|
124
|
+
return (0, smartRowArray_1.createSmartRowArray)(allRows);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
findRowLocator(filters_1) {
|
|
128
|
+
return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
|
|
129
|
+
var _a, _b;
|
|
130
|
+
const map = yield this.tableMapper.getMap();
|
|
131
|
+
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
|
|
132
|
+
let currentPage = 1;
|
|
133
|
+
this.log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
|
|
134
|
+
while (true) {
|
|
135
|
+
// Check Loading
|
|
136
|
+
if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
|
|
137
|
+
const isLoading = yield this.config.strategies.loading.isTableLoading({
|
|
138
|
+
root: this.rootLocator,
|
|
139
|
+
config: this.config,
|
|
140
|
+
page: this.rootLocator.page(),
|
|
141
|
+
resolve: this.resolve
|
|
142
|
+
});
|
|
143
|
+
if (isLoading) {
|
|
144
|
+
this.log('Table is loading... waiting');
|
|
145
|
+
yield this.rootLocator.page().waitForTimeout(200);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
|
|
150
|
+
const matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page());
|
|
151
|
+
const count = yield matchedRows.count();
|
|
152
|
+
this.log(`Page ${currentPage}: Found ${count} matches.`);
|
|
153
|
+
if (count > 1) {
|
|
154
|
+
const sampleData = [];
|
|
155
|
+
try {
|
|
156
|
+
const firstFewRows = yield matchedRows.all();
|
|
157
|
+
const sampleCount = Math.min(firstFewRows.length, 3);
|
|
158
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
159
|
+
const rowData = yield this.makeSmartRow(firstFewRows[i], map, 0).toJSON();
|
|
160
|
+
sampleData.push(JSON.stringify(rowData));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (e) { }
|
|
164
|
+
const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
|
|
165
|
+
throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
|
|
166
|
+
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
167
|
+
}
|
|
168
|
+
if (count === 1)
|
|
169
|
+
return matchedRows.first();
|
|
170
|
+
if (currentPage < effectiveMaxPages) {
|
|
171
|
+
this.log(`Page ${currentPage}: Not found. Attempting pagination...`);
|
|
172
|
+
const context = {
|
|
173
|
+
root: this.rootLocator,
|
|
174
|
+
config: this.config,
|
|
175
|
+
resolve: this.resolve,
|
|
176
|
+
page: this.rootLocator.page()
|
|
177
|
+
};
|
|
178
|
+
const paginationResult = yield this.config.strategies.pagination(context);
|
|
179
|
+
const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
|
|
180
|
+
if (didLoadMore) {
|
|
181
|
+
currentPage++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.log(`Page ${currentPage}: Pagination failed (end of data).`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
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;
|
package/dist/filterEngine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Locator, Page } from "@playwright/test";
|
|
2
|
-
import { FinalTableConfig } from "./types";
|
|
2
|
+
import { FinalTableConfig, FilterValue } from "./types";
|
|
3
3
|
export declare class FilterEngine {
|
|
4
4
|
private config;
|
|
5
5
|
private resolve;
|
|
@@ -7,5 +7,5 @@ export declare class FilterEngine {
|
|
|
7
7
|
/**
|
|
8
8
|
* Applies filters to a set of rows.
|
|
9
9
|
*/
|
|
10
|
-
applyFilters(baseRows: Locator, filters: Record<string,
|
|
10
|
+
applyFilters(baseRows: Locator, filters: Record<string, FilterValue>, map: Map<string, number>, exact: boolean, page: Page): Locator;
|
|
11
11
|
}
|
package/dist/filterEngine.js
CHANGED
|
@@ -20,7 +20,7 @@ class FilterEngine {
|
|
|
20
20
|
if (colIndex === undefined) {
|
|
21
21
|
throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
|
|
22
22
|
}
|
|
23
|
-
const filterVal =
|
|
23
|
+
const filterVal = value;
|
|
24
24
|
// Use strategy if provided (For future: configured filter strategies)
|
|
25
25
|
// But for now, we implement the default logic or use custom if we add it to config later
|
|
26
26
|
// Default Filter Logic
|
|
@@ -29,9 +29,20 @@ class FilterEngine {
|
|
|
29
29
|
// filter({ has: ... }) checks if the row *contains* the matching cell.
|
|
30
30
|
// But we need to be specific about WHICH cell.
|
|
31
31
|
// Locator filtering by `has: locator.nth(index)` works if `locator` search is relative to the row.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const targetCell = cellTemplate.nth(colIndex);
|
|
33
|
+
if (typeof filterVal === 'function') {
|
|
34
|
+
// Locator-based filter: (cell) => cell.locator(...)
|
|
35
|
+
filtered = filtered.filter({
|
|
36
|
+
has: filterVal(targetCell)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// Text-based filter
|
|
41
|
+
const textVal = typeof filterVal === 'number' ? String(filterVal) : filterVal;
|
|
42
|
+
filtered = filtered.filter({
|
|
43
|
+
has: targetCell.getByText(textVal, { exact }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
return filtered;
|
|
37
48
|
}
|
package/dist/smartRow.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types
|
|
|
4
4
|
* Factory to create a SmartRow by extending a Playwright Locator.
|
|
5
5
|
* We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
|
|
6
6
|
*/
|
|
7
|
-
export declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig
|
|
7
|
+
export declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null) => SmartRowType<T>;
|
package/dist/smartRow.js
CHANGED
|
@@ -39,13 +39,23 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
39
39
|
return resolve(config.cellSelector, rowLocator).nth(idx);
|
|
40
40
|
};
|
|
41
41
|
smart.toJSON = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
|
+
var _a;
|
|
42
43
|
const result = {};
|
|
43
44
|
const page = rootLocator.page();
|
|
44
45
|
for (const [col, idx] of map.entries()) {
|
|
45
46
|
if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(col)) {
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
48
|
-
//
|
|
49
|
+
// Check if we have a data mapper for this column
|
|
50
|
+
const mapper = (_a = config.dataMapper) === null || _a === void 0 ? void 0 : _a[col];
|
|
51
|
+
if (mapper) {
|
|
52
|
+
// Use custom mapper
|
|
53
|
+
// Ensure we have the cell first (same navigation logic)
|
|
54
|
+
// ... wait, the navigation logic below assumes we need to navigate.
|
|
55
|
+
// If we have a mapper, we still need the cell locator.
|
|
56
|
+
// Let's reuse the navigation logic to get targetCell
|
|
57
|
+
}
|
|
58
|
+
// --- Navigation Logic Start ---
|
|
49
59
|
const cell = config.strategies.getCellLocator
|
|
50
60
|
? config.strategies.getCellLocator({
|
|
51
61
|
row: rowLocator,
|
|
@@ -56,7 +66,6 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
56
66
|
})
|
|
57
67
|
: resolve(config.cellSelector, rowLocator).nth(idx);
|
|
58
68
|
let targetCell = cell;
|
|
59
|
-
// Check if cell exists
|
|
60
69
|
const count = yield cell.count();
|
|
61
70
|
if (count === 0) {
|
|
62
71
|
// Optimization: Check if we are ALREADY at the target cell
|
|
@@ -68,47 +77,59 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
|
|
|
68
77
|
resolve
|
|
69
78
|
});
|
|
70
79
|
if (active && active.rowIndex === rowIndex && active.columnIndex === idx) {
|
|
71
|
-
if (config.debug)
|
|
72
|
-
console.log(`[SmartRow] Already at target cell (r:${active.rowIndex}, c:${active.columnIndex}), skipping navigation.`);
|
|
73
80
|
targetCell = active.locator;
|
|
74
|
-
// Skip navigation
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
// Skip navigation
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Cell doesn't exist - navigate to it
|
|
85
|
+
yield config.strategies.cellNavigation({
|
|
86
|
+
config: config,
|
|
87
|
+
root: rootLocator,
|
|
88
|
+
page: page,
|
|
89
|
+
resolve: resolve,
|
|
90
|
+
column: col,
|
|
91
|
+
index: idx,
|
|
92
|
+
rowLocator: rowLocator,
|
|
93
|
+
rowIndex: rowIndex
|
|
94
|
+
});
|
|
95
|
+
// Update targetCell after navigation if needed (e.g. active cell changed)
|
|
96
|
+
if (config.strategies.getActiveCell) {
|
|
97
|
+
const activeCell = yield config.strategies.getActiveCell({
|
|
98
|
+
config,
|
|
99
|
+
root: rootLocator,
|
|
100
|
+
page,
|
|
101
|
+
resolve
|
|
102
|
+
});
|
|
103
|
+
if (activeCell)
|
|
104
|
+
targetCell = activeCell.locator;
|
|
105
|
+
}
|
|
78
106
|
}
|
|
79
107
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
yield config.strategies.cellNavigation({
|
|
85
|
-
config: config,
|
|
86
|
-
root: rootLocator,
|
|
87
|
-
page: page,
|
|
88
|
-
resolve: resolve,
|
|
89
|
-
column: col,
|
|
90
|
-
index: idx,
|
|
91
|
-
rowLocator: rowLocator,
|
|
92
|
-
rowIndex: rowIndex
|
|
93
|
-
});
|
|
94
|
-
// Optimization: check if we can get the active cell directly
|
|
95
|
-
if (config.strategies.getActiveCell) {
|
|
96
|
-
const activeCell = yield config.strategies.getActiveCell({
|
|
97
|
-
config,
|
|
108
|
+
else {
|
|
109
|
+
// Fallback navigation without active cell check
|
|
110
|
+
yield config.strategies.cellNavigation({
|
|
111
|
+
config: config,
|
|
98
112
|
root: rootLocator,
|
|
99
|
-
page,
|
|
100
|
-
resolve
|
|
113
|
+
page: page,
|
|
114
|
+
resolve: resolve,
|
|
115
|
+
column: col,
|
|
116
|
+
index: idx,
|
|
117
|
+
rowLocator: rowLocator,
|
|
118
|
+
rowIndex: rowIndex
|
|
101
119
|
});
|
|
102
|
-
if (activeCell) {
|
|
103
|
-
if (config.debug) {
|
|
104
|
-
console.log(`[SmartRow.toJSON] switching to active cell locator (r:${activeCell.rowIndex}, c:${activeCell.columnIndex})`);
|
|
105
|
-
}
|
|
106
|
-
targetCell = activeCell.locator;
|
|
107
|
-
}
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
// --- Navigation Logic End ---
|
|
123
|
+
if (mapper) {
|
|
124
|
+
// Apply mapper
|
|
125
|
+
const mappedValue = yield mapper(targetCell);
|
|
126
|
+
result[col] = mappedValue;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Default string extraction
|
|
130
|
+
const text = yield targetCell.innerText();
|
|
131
|
+
result[col] = (text || '').trim();
|
|
132
|
+
}
|
|
112
133
|
}
|
|
113
134
|
return result;
|
|
114
135
|
});
|
|
@@ -50,5 +50,9 @@ export declare const Strategies: {
|
|
|
50
50
|
hasEmptyCells: () => (row: import("..").SmartRow) => Promise<boolean>;
|
|
51
51
|
never: () => Promise<boolean>;
|
|
52
52
|
};
|
|
53
|
+
Headers: {
|
|
54
|
+
stable: (duration?: number) => (context: import("..").TableContext) => Promise<boolean>;
|
|
55
|
+
never: () => Promise<boolean>;
|
|
56
|
+
};
|
|
53
57
|
};
|
|
54
58
|
};
|
|
@@ -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
|
};
|