@rickcedwhat/playwright-smart-table 6.1.1 → 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.
- package/README.md +2 -0
- package/dist/engine/rowFinder.d.ts +26 -0
- package/dist/engine/rowFinder.js +161 -0
- package/dist/engine/tableMapper.d.ts +16 -0
- package/dist/engine/tableMapper.js +136 -0
- 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 +4 -3
- package/dist/types.d.ts +4 -3
- package/dist/useTable.d.ts +4 -0
- package/dist/useTable.js +43 -242
- package/dist/utils/stringUtils.js +12 -3
- package/package.json +10 -5
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;
|
|
@@ -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
|
};
|
package/dist/typeContext.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* This file is generated by scripts/embed-types.js
|
|
4
4
|
* It contains the raw text of types.ts to provide context for LLM prompts.
|
|
5
5
|
*/
|
|
6
|
-
export declare const TYPE_CONTEXT = "\n/**\n * Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.\n * @example\n * // String selector\n * rowSelector: 'tbody tr'\n * \n * // Function selector\n * rowSelector: (root) => root.locator('[role=\"row\"]')\n */\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\n/**\n * Function to get a cell locator given row, column info.\n * Replaces the old cellResolver.\n */\nexport type GetCellLocatorFn = (args: {\n row: Locator;\n columnName: string;\n columnIndex: number;\n rowIndex?: number;\n page: Page;\n}) => Locator;\n\n/**\n * Function to get the currently active/focused cell.\n * Returns null if no cell is active.\n */\nexport type GetActiveCellFn = (args: TableContext) => Promise<{\n rowIndex: number;\n columnIndex: number;\n columnName?: string;\n locator: Locator;\n} | null>;\n\n\n/**\n * SmartRow - A Playwright Locator with table-aware methods.\n * \n * Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.\n * \n * @example\n * const row = table.getRow({ Name: 'John Doe' });\n * await row.click(); // Standard Locator method\n * const email = row.getCell('Email'); // Table-aware method\n * const data = await row.toJSON(); // Extract all row data\n * await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields\n */\nexport type SmartRow<T = any> = Locator & {\n /** Optional row index (0-based) if known */\n rowIndex?: number;\n\n /**\n * Get a cell locator by column name.\n * @param column - Column name (case-sensitive)\n * @returns Locator for the cell\n * @example\n * const emailCell = row.getCell('Email');\n * await expect(emailCell).toHaveText('john@example.com');\n */\n getCell(column: string): Locator;\n\n /**\n * Extract all cell data as a key-value object.\n * @param options - Optional configuration\n * @param options.columns - Specific columns to extract (extracts all if not specified)\n * @returns Promise resolving to row data\n * @example\n * const data = await row.toJSON();\n * // { Name: 'John', Email: 'john@example.com', ... }\n * \n * const partial = await row.toJSON({ columns: ['Name', 'Email'] });\n * // { Name: 'John', Email: 'john@example.com' }\n */\n toJSON(options?: { columns?: string[] }): Promise<T>;\n\n /**\n * Scrolls/paginates to bring this row into view.\n * Only works if rowIndex is known (e.g., from getRowByIndex).\n * @throws Error if rowIndex is unknown\n */\n bringIntoView(): Promise<void>;\n\n /**\n * Intelligently fills form fields in the row.\n * Automatically detects input types (text, select, checkbox, contenteditable).\n * \n * @param data - Column-value pairs to fill\n * @param options - Optional configuration\n * @param options.inputMappers - Custom input selectors per column\n * @example\n * // Auto-detection\n * await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });\n * \n * // Custom input mappers\n * await row.smartFill(\n * { Name: 'John' },\n * { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }\n * );\n */\n smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext & { rowLocator?: Locator; rowIndex?: number };\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\n/**\n * Debug configuration for development and troubleshooting\n */\nexport type DebugConfig = {\n /**\n * Slow down operations for debugging\n * - number: Apply same delay to all operations (ms)\n * - object: Granular delays per operation type\n */\n slow?: number | {\n pagination?: number;\n getCell?: number;\n findRow?: number;\n default?: number;\n };\n /**\n * Log level for debug output\n * - 'verbose': All logs (verbose, info, error)\n * - 'info': Info and error logs only\n * - 'error': Error logs only\n * - 'none': No logs\n */\n logLevel?: 'verbose' | 'info' | 'error' | 'none';\n};\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport type FillStrategy = (options: {\n row: SmartRow;\n columnName: string;\n value: any;\n index: number;\n page: Page;\n rootLocator: Locator;\n table: TableResult; // The parent table instance\n fillOptions?: FillOptions;\n}) => Promise<void>;\n\nexport type { HeaderStrategy } from './strategies/headers';\nexport type { CellNavigationStrategy } from './strategies/columns';\n\n/**\n * Strategy to resolve column names (string or regex) to their index.\n */\nexport type { ColumnResolutionStrategy } from './strategies/resolution';\n\n/**\n * Strategy to filter rows based on criteria.\n */\nexport interface FilterStrategy {\n apply(options: {\n rows: Locator;\n filter: { column: string, value: string | RegExp | number };\n colIndex: number;\n tableContext: TableContext;\n }): Locator;\n}\n\n/**\n * Strategy to check if the table or rows are loading.\n */\nexport interface LoadingStrategy {\n isTableLoading?: (context: TableContext) => Promise<boolean>;\n isRowLoading?: (row: SmartRow) => Promise<boolean>;\n}\n\n/**\n * Organized container for all table interaction strategies.\n */\nexport interface TableStrategies {\n /** Strategy for discovering/scanning headers */\n header?: HeaderStrategy;\n /** Strategy for navigating to specific cells (row + column) */\n cellNavigation?: CellNavigationStrategy;\n /** Strategy for filling form inputs */\n fill?: FillStrategy;\n /** Strategy for paginating through data */\n pagination?: PaginationStrategy;\n /** Strategy for sorting columns */\n sorting?: SortingStrategy;\n /** Strategy for deduplicating rows during iteration/scrolling */\n dedupe?: DedupeStrategy;\n /** Function to get a cell locator */\n getCellLocator?: GetCellLocatorFn;\n /** Function to get the currently active/focused cell */\n getActiveCell?: GetActiveCellFn;\n /** Custom helper to check if a table is fully loaded/ready */\n isTableLoaded?: (args: TableContext) => Promise<boolean>;\n /** Custom helper to check if a row is fully loaded/ready */\n isRowLoaded?: (args: { row: Locator, index: number }) => Promise<boolean>;\n /** Custom helper to check if a cell is fully loaded/ready (e.g. for editing) */\n isCellLoaded?: (args: { cell: Locator, column: string, row: Locator }) => Promise<boolean>;\n /** Strategy for detecting loading states */\n loading?: LoadingStrategy;\n}\n\n/**\n * Configuration options for useTable.\n */\nexport interface TableConfig {\n /** Selector for the table headers */\n headerSelector?: string;\n /** Selector for the table rows */\n rowSelector?: string;\n /** Selector for the cells within a row */\n cellSelector?: string;\n /** Number of pages to scan for verification */\n maxPages?: number;\n /** Hook to rename columns dynamically */\n headerTransformer?: (args: { text: string, index: number, locator: Locator, seenHeaders: Set<string> }) => string | Promise<string>;\n /** Automatically scroll to table on init */\n autoScroll?: boolean;\n /** Debug options for development and troubleshooting */\n debug?: DebugConfig;\n /** Reset hook */\n onReset?: (context: TableContext) => Promise<void>;\n /** All interaction strategies */\n strategies?: TableStrategies;\n}\n\nexport interface FinalTableConfig extends TableConfig {\n headerSelector: string;\n rowSelector: string;\n cellSelector: string;\n maxPages: number;\n autoScroll: boolean;\n debug?: TableConfig['debug'];\n headerTransformer: (args: { text: string, index: number, locator: Locator, seenHeaders: Set<string> }) => string | Promise<string>;\n onReset: (context: TableContext) => Promise<void>;\n strategies: TableStrategies;\n}\n\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\n/**\n * Options for generateConfigPrompt\n */\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n /**\n * Include TypeScript type definitions in the prompt\n * @default true\n */\n includeTypes?: boolean;\n}\n\nexport interface TableResult<T = any> {\n /**\n * Initializes the table by resolving headers. Must be called before using sync methods.\n * @param options Optional timeout for header resolution (default: 3000ms)\n */\n init(options?: { timeout?: number }): Promise<TableResult>;\n\n /**\n * SYNC: Checks if the table has been initialized.\n * @returns true if init() has been called and completed, false otherwise\n */\n isInitialized(): boolean;\n\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row by filters on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean }\n ) => SmartRow;\n\n /**\n * Gets a row by 1-based index on the current page.\n * Throws error if table is not initialized.\n * @param index 1-based row index\n * @param options Optional settings including bringIntoView\n */\n getRowByIndex: (\n index: number,\n options?: { bringIntoView?: boolean }\n ) => SmartRow;\n\n /**\n * ASYNC: Searches for a single row across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match and max pages\n */\n findRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n /**\n * ASYNC: Searches for all matching rows across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match, max pages, and asJSON\n */\n findRows: <R extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * Navigates to a specific column using the configured CellNavigationStrategy.\n */\n scrollToColumn: (columnName: string) => Promise<void>;\n\n /**\n * Gets all rows on the current page only (does not paginate).\n * Auto-initializes the table if not already initialized.\n * Returns a SmartRowArray which extends Array with a toJSON() helper method.\n * @param options - Filter options\n */\n getRows: (options?: { filter?: Record<string, any>, exact?: boolean }) => Promise<SmartRowArray>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Revalidates the table's structure (headers, columns) without resetting pagination or state.\n * Useful when columns change visibility or order dynamically.\n */\n revalidate: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n\n /**\n * Iterates through paginated table data, calling the callback for each iteration.\n * Callback return values are automatically appended to allData, which is returned.\n */\n iterateThroughTable: <T = any>(\n callback: (context: {\n index: number;\n isFirst: boolean;\n isLast: boolean;\n rows: SmartRowArray;\n allData: T[];\n table: RestrictedTableResult;\n batchInfo?: {\n startIndex: number;\n endIndex: number;\n size: number;\n };\n\n }) => T | T[] | Promise<T | T[]>,\n options?: {\n pagination?: PaginationStrategy;\n dedupeStrategy?: DedupeStrategy;\n maxIterations?: number;\n batchSize?: number;\n getIsFirst?: (context: { index: number }) => boolean;\n getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;\n beforeFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n afterLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n /**\n * If true, flattens array results from callback into the main data array.\n * If false (default), pushes the return value as-is (preserves batching/arrays).\n */\n autoFlatten?: boolean;\n }\n ) => Promise<T[]>;\n\n /**\n * Generate an AI-friendly configuration prompt for debugging.\n * Outputs table HTML and TypeScript definitions to help AI assistants generate config.\n */\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n}\n\n/**\n * Restricted table result that excludes methods that shouldn't be called during iteration.\n */\nexport type RestrictedTableResult<T = any> = Omit<TableResult<T>, 'searchForRow' | 'iterateThroughTable' | 'reset'>;\n";
|
|
6
|
+
export declare const TYPE_CONTEXT = "\n/**\n * Flexible selector type - can be a CSS string, function returning a Locator, or Locator itself.\n * @example\n * // String selector\n * rowSelector: 'tbody tr'\n * \n * // Function selector\n * rowSelector: (root) => root.locator('[role=\"row\"]')\n */\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\n/**\n * Function to get a cell locator given row, column info.\n * Replaces the old cellResolver.\n */\nexport type GetCellLocatorFn = (args: {\n row: Locator;\n columnName: string;\n columnIndex: number;\n rowIndex?: number;\n page: Page;\n}) => Locator;\n\n/**\n * Function to get the currently active/focused cell.\n * Returns null if no cell is active.\n */\nexport type GetActiveCellFn = (args: TableContext) => Promise<{\n rowIndex: number;\n columnIndex: number;\n columnName?: string;\n locator: Locator;\n} | null>;\n\n\n/**\n * SmartRow - A Playwright Locator with table-aware methods.\n * \n * Extends all standard Locator methods (click, isVisible, etc.) with table-specific functionality.\n * \n * @example\n * const row = table.getRow({ Name: 'John Doe' });\n * await row.click(); // Standard Locator method\n * const email = row.getCell('Email'); // Table-aware method\n * const data = await row.toJSON(); // Extract all row data\n * await row.smartFill({ Name: 'Jane', Status: 'Active' }); // Fill form fields\n */\nexport type SmartRow<T = any> = Locator & {\n /** Optional row index (0-based) if known */\n rowIndex?: number;\n\n /**\n * Get a cell locator by column name.\n * @param column - Column name (case-sensitive)\n * @returns Locator for the cell\n * @example\n * const emailCell = row.getCell('Email');\n * await expect(emailCell).toHaveText('john@example.com');\n */\n getCell(column: string): Locator;\n\n /**\n * Extract all cell data as a key-value object.\n * @param options - Optional configuration\n * @param options.columns - Specific columns to extract (extracts all if not specified)\n * @returns Promise resolving to row data\n * @example\n * const data = await row.toJSON();\n * // { Name: 'John', Email: 'john@example.com', ... }\n * \n * const partial = await row.toJSON({ columns: ['Name', 'Email'] });\n * // { Name: 'John', Email: 'john@example.com' }\n */\n toJSON(options?: { columns?: string[] }): Promise<T>;\n\n /**\n * Scrolls/paginates to bring this row into view.\n * Only works if rowIndex is known (e.g., from getRowByIndex).\n * @throws Error if rowIndex is unknown\n */\n bringIntoView(): Promise<void>;\n\n /**\n * Intelligently fills form fields in the row.\n * Automatically detects input types (text, select, checkbox, contenteditable).\n * \n * @param data - Column-value pairs to fill\n * @param options - Optional configuration\n * @param options.inputMappers - Custom input selectors per column\n * @example\n * // Auto-detection\n * await row.smartFill({ Name: 'John', Status: 'Active', Subscribe: true });\n * \n * // Custom input mappers\n * await row.smartFill(\n * { Name: 'John' },\n * { inputMappers: { Name: (cell) => cell.locator('.custom-input') } }\n * );\n */\n smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext & { rowLocator?: Locator; rowIndex?: number };\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\n/**\n * Debug configuration for development and troubleshooting\n */\nexport type DebugConfig = {\n /**\n * Slow down operations for debugging\n * - number: Apply same delay to all operations (ms)\n * - object: Granular delays per operation type\n */\n slow?: number | {\n pagination?: number;\n getCell?: number;\n findRow?: number;\n default?: number;\n };\n /**\n * Log level for debug output\n * - 'verbose': All logs (verbose, info, error)\n * - 'info': Info and error logs only\n * - 'error': Error logs only\n * - 'none': No logs\n */\n logLevel?: 'verbose' | 'info' | 'error' | 'none';\n};\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport type FillStrategy = (options: {\n row: SmartRow;\n columnName: string;\n value: any;\n index: number;\n page: Page;\n rootLocator: Locator;\n table: TableResult; // The parent table instance\n fillOptions?: FillOptions;\n}) => Promise<void>;\n\nexport type { HeaderStrategy } from './strategies/headers';\nexport type { CellNavigationStrategy } from './strategies/columns';\n\n/**\n * Strategy to resolve column names (string or regex) to their index.\n */\nexport type { ColumnResolutionStrategy } from './strategies/resolution';\n\n/**\n * Strategy to filter rows based on criteria.\n */\nexport interface FilterStrategy {\n apply(options: {\n rows: Locator;\n filter: { column: string, value: string | RegExp | number };\n colIndex: number;\n tableContext: TableContext;\n }): Locator;\n}\n\n/**\n * Strategy to check if the table or rows are loading.\n */\nexport interface LoadingStrategy {\n isTableLoading?: (context: TableContext) => Promise<boolean>;\n isRowLoading?: (row: SmartRow) => Promise<boolean>;\n isHeaderLoading?: (context: TableContext) => Promise<boolean>;\n}\n\n/**\n * Organized container for all table interaction strategies.\n */\nexport interface TableStrategies {\n /** Strategy for discovering/scanning headers */\n header?: HeaderStrategy;\n /** Strategy for navigating to specific cells (row + column) */\n cellNavigation?: CellNavigationStrategy;\n /** Strategy for filling form inputs */\n fill?: FillStrategy;\n /** Strategy for paginating through data */\n pagination?: PaginationStrategy;\n /** Strategy for sorting columns */\n sorting?: SortingStrategy;\n /** Strategy for deduplicating rows during iteration/scrolling */\n dedupe?: DedupeStrategy;\n /** Function to get a cell locator */\n getCellLocator?: GetCellLocatorFn;\n /** Function to get the currently active/focused cell */\n getActiveCell?: GetActiveCellFn;\n /** Custom helper to check if a table is fully loaded/ready */\n isTableLoaded?: (args: TableContext) => Promise<boolean>;\n /** Custom helper to check if a row is fully loaded/ready */\n isRowLoaded?: (args: { row: Locator, index: number }) => Promise<boolean>;\n /** Custom helper to check if a cell is fully loaded/ready (e.g. for editing) */\n isCellLoaded?: (args: { cell: Locator, column: string, row: Locator }) => Promise<boolean>;\n /** Strategy for detecting loading states */\n loading?: LoadingStrategy;\n}\n\n/**\n * Configuration options for useTable.\n */\nexport interface TableConfig {\n /** Selector for the table headers */\n headerSelector?: string;\n /** Selector for the table rows */\n rowSelector?: string;\n /** Selector for the cells within a row */\n cellSelector?: string;\n /** Number of pages to scan for verification */\n maxPages?: number;\n /** Hook to rename columns dynamically */\n headerTransformer?: (args: { text: string, index: number, locator: Locator, seenHeaders: Set<string> }) => string | Promise<string>;\n /** Automatically scroll to table on init */\n autoScroll?: boolean;\n /** Debug options for development and troubleshooting */\n debug?: DebugConfig;\n /** Reset hook */\n onReset?: (context: TableContext) => Promise<void>;\n /** All interaction strategies */\n strategies?: TableStrategies;\n}\n\nexport interface FinalTableConfig extends TableConfig {\n headerSelector: string;\n rowSelector: string;\n cellSelector: string;\n maxPages: number;\n autoScroll: boolean;\n debug?: TableConfig['debug'];\n headerTransformer: (args: { text: string, index: number, locator: Locator, seenHeaders: Set<string> }) => string | Promise<string>;\n onReset: (context: TableContext) => Promise<void>;\n strategies: TableStrategies;\n}\n\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\n/**\n * Options for generateConfigPrompt\n */\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n /**\n * Include TypeScript type definitions in the prompt\n * @default true\n */\n includeTypes?: boolean;\n}\n\nexport interface TableResult<T = any> {\n /**\n * Initializes the table by resolving headers. Must be called before using sync methods.\n * @param options Optional timeout for header resolution (default: 3000ms)\n */\n init(options?: { timeout?: number }): Promise<TableResult>;\n\n /**\n * SYNC: Checks if the table has been initialized.\n * @returns true if init() has been called and completed, false otherwise\n */\n isInitialized(): boolean;\n\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row by filters on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean }\n ) => SmartRow;\n\n /**\n * Gets a row by 1-based index on the current page.\n * Throws error if table is not initialized.\n * @param index 1-based row index\n * @param options Optional settings including bringIntoView\n */\n getRowByIndex: (\n index: number,\n options?: { bringIntoView?: boolean }\n ) => SmartRow;\n\n /**\n * ASYNC: Searches for a single row across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match and max pages\n */\n findRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n /**\n * ASYNC: Searches for all matching rows across pages using pagination.\n * Auto-initializes the table if not already initialized.\n * @param filters - The filter criteria to match\n * @param options - Search options including exact match, max pages, and asJSON\n */\n findRows: <R extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number } & R\n ) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRowArray>;\n\n /**\n * Navigates to a specific column using the configured CellNavigationStrategy.\n */\n scrollToColumn: (columnName: string) => Promise<void>;\n\n /**\n * Gets all rows on the current page only (does not paginate).\n * Auto-initializes the table if not already initialized.\n * Returns a SmartRowArray which extends Array with a toJSON() helper method.\n * @param options - Filter options\n */\n getRows: (options?: { filter?: Record<string, any>, exact?: boolean }) => Promise<SmartRowArray>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Revalidates the table's structure (headers, columns) without resetting pagination or state.\n * Useful when columns change visibility or order dynamically.\n */\n revalidate: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n\n /**\n * Iterates through paginated table data, calling the callback for each iteration.\n * Callback return values are automatically appended to allData, which is returned.\n */\n iterateThroughTable: <T = any>(\n callback: (context: {\n index: number;\n isFirst: boolean;\n isLast: boolean;\n rows: SmartRowArray;\n allData: T[];\n table: RestrictedTableResult;\n batchInfo?: {\n startIndex: number;\n endIndex: number;\n size: number;\n };\n\n }) => T | T[] | Promise<T | T[]>,\n options?: {\n pagination?: PaginationStrategy;\n dedupeStrategy?: DedupeStrategy;\n maxIterations?: number;\n batchSize?: number;\n getIsFirst?: (context: { index: number }) => boolean;\n getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;\n beforeFirst?: (context: { index: number, rows: SmartRowArray, allData: any[] }) => void | Promise<void>;\n afterLast?: (context: { index: number, rows: SmartRowArray, allData: any[] }) => void | Promise<void>;\n /**\n * If true, flattens array results from callback into the main data array.\n * If false (default), pushes the return value as-is (preserves batching/arrays).\n */\n autoFlatten?: boolean;\n }\n ) => Promise<T[]>;\n\n /**\n * Generate an AI-friendly configuration prompt for debugging.\n * Outputs table HTML and TypeScript definitions to help AI assistants generate config.\n */\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n}\n\n/**\n * Restricted table result that excludes methods that shouldn't be called during iteration.\n */\nexport type RestrictedTableResult<T = any> = Omit<TableResult<T>, 'searchForRow' | 'iterateThroughTable' | 'reset'>;\n";
|
package/dist/typeContext.js
CHANGED
|
@@ -216,6 +216,7 @@ export interface FilterStrategy {
|
|
|
216
216
|
export interface LoadingStrategy {
|
|
217
217
|
isTableLoading?: (context: TableContext) => Promise<boolean>;
|
|
218
218
|
isRowLoading?: (row: SmartRow) => Promise<boolean>;
|
|
219
|
+
isHeaderLoading?: (context: TableContext) => Promise<boolean>;
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
/**
|
|
@@ -366,7 +367,7 @@ export interface TableResult<T = any> {
|
|
|
366
367
|
findRows: <R extends { asJSON?: boolean }>(
|
|
367
368
|
filters: Record<string, string | RegExp | number>,
|
|
368
369
|
options?: { exact?: boolean, maxPages?: number } & R
|
|
369
|
-
) => Promise<R['asJSON'] extends true ? Record<string, string>[] :
|
|
370
|
+
) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRowArray>;
|
|
370
371
|
|
|
371
372
|
/**
|
|
372
373
|
* Navigates to a specific column using the configured CellNavigationStrategy.
|
|
@@ -441,8 +442,8 @@ export interface TableResult<T = any> {
|
|
|
441
442
|
batchSize?: number;
|
|
442
443
|
getIsFirst?: (context: { index: number }) => boolean;
|
|
443
444
|
getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;
|
|
444
|
-
beforeFirst?: (context: { index: number, rows:
|
|
445
|
-
afterLast?: (context: { index: number, rows:
|
|
445
|
+
beforeFirst?: (context: { index: number, rows: SmartRowArray, allData: any[] }) => void | Promise<void>;
|
|
446
|
+
afterLast?: (context: { index: number, rows: SmartRowArray, allData: any[] }) => void | Promise<void>;
|
|
446
447
|
/**
|
|
447
448
|
* If true, flattens array results from callback into the main data array.
|
|
448
449
|
* If false (default), pushes the return value as-is (preserves batching/arrays).
|
package/dist/types.d.ts
CHANGED
|
@@ -198,6 +198,7 @@ export interface FilterStrategy {
|
|
|
198
198
|
export interface LoadingStrategy {
|
|
199
199
|
isTableLoading?: (context: TableContext) => Promise<boolean>;
|
|
200
200
|
isRowLoading?: (row: SmartRow) => Promise<boolean>;
|
|
201
|
+
isHeaderLoading?: (context: TableContext) => Promise<boolean>;
|
|
201
202
|
}
|
|
202
203
|
/**
|
|
203
204
|
* Organized container for all table interaction strategies.
|
|
@@ -354,7 +355,7 @@ export interface TableResult<T = any> {
|
|
|
354
355
|
}>(filters: Record<string, string | RegExp | number>, options?: {
|
|
355
356
|
exact?: boolean;
|
|
356
357
|
maxPages?: number;
|
|
357
|
-
} & R) => Promise<R['asJSON'] extends true ? Record<string, string>[] :
|
|
358
|
+
} & R) => Promise<R['asJSON'] extends true ? Record<string, string>[] : SmartRowArray>;
|
|
358
359
|
/**
|
|
359
360
|
* Navigates to a specific column using the configured CellNavigationStrategy.
|
|
360
361
|
*/
|
|
@@ -432,12 +433,12 @@ export interface TableResult<T = any> {
|
|
|
432
433
|
}) => boolean;
|
|
433
434
|
beforeFirst?: (context: {
|
|
434
435
|
index: number;
|
|
435
|
-
rows:
|
|
436
|
+
rows: SmartRowArray;
|
|
436
437
|
allData: any[];
|
|
437
438
|
}) => void | Promise<void>;
|
|
438
439
|
afterLast?: (context: {
|
|
439
440
|
index: number;
|
|
440
|
-
rows:
|
|
441
|
+
rows: SmartRowArray;
|
|
441
442
|
allData: any[];
|
|
442
443
|
}) => void | Promise<void>;
|
|
443
444
|
/**
|
package/dist/useTable.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export declare const LoadingStrategies: {
|
|
|
34
34
|
hasEmptyCells: () => (row: SmartRowType) => Promise<boolean>;
|
|
35
35
|
never: () => Promise<boolean>;
|
|
36
36
|
};
|
|
37
|
+
Headers: {
|
|
38
|
+
stable: (duration?: number) => (context: TableContext) => Promise<boolean>;
|
|
39
|
+
never: () => Promise<boolean>;
|
|
40
|
+
};
|
|
37
41
|
};
|
|
38
42
|
export declare const SortingStrategies: {
|
|
39
43
|
AriaSort: () => import("./types").SortingStrategy;
|
package/dist/useTable.js
CHANGED
|
@@ -23,11 +23,12 @@ const columns_1 = require("./strategies/columns");
|
|
|
23
23
|
Object.defineProperty(exports, "CellNavigationStrategies", { enumerable: true, get: function () { return columns_1.CellNavigationStrategies; } });
|
|
24
24
|
const smartRow_1 = require("./smartRow");
|
|
25
25
|
const filterEngine_1 = require("./filterEngine");
|
|
26
|
+
const tableMapper_1 = require("./engine/tableMapper");
|
|
27
|
+
const rowFinder_1 = require("./engine/rowFinder");
|
|
26
28
|
const resolution_1 = require("./strategies/resolution");
|
|
27
29
|
Object.defineProperty(exports, "ResolutionStrategies", { enumerable: true, get: function () { return resolution_1.ResolutionStrategies; } });
|
|
28
30
|
const strategies_1 = require("./strategies");
|
|
29
31
|
Object.defineProperty(exports, "Strategies", { enumerable: true, get: function () { return strategies_1.Strategies; } });
|
|
30
|
-
const validation_1 = require("./strategies/validation");
|
|
31
32
|
const debugUtils_1 = require("./utils/debugUtils");
|
|
32
33
|
const smartRowArray_1 = require("./utils/smartRowArray");
|
|
33
34
|
/**
|
|
@@ -43,6 +44,9 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
43
44
|
header: headers_1.HeaderStrategies.visible,
|
|
44
45
|
cellNavigation: columns_1.CellNavigationStrategies.default,
|
|
45
46
|
pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
|
|
47
|
+
loading: {
|
|
48
|
+
isHeaderLoading: loading_1.LoadingStrategies.Headers.stable(200)
|
|
49
|
+
}
|
|
46
50
|
};
|
|
47
51
|
const config = Object.assign(Object.assign({ rowSelector: "tbody tr", headerSelector: "thead th", cellSelector: "td", maxPages: 1, headerTransformer: ({ text }) => text, autoScroll: true, onReset: () => __awaiter(void 0, void 0, void 0, function* () { }) }, configOptions), { strategies: Object.assign(Object.assign({}, defaultStrategies), configOptions.strategies) });
|
|
48
52
|
const resolve = (item, parent) => {
|
|
@@ -53,16 +57,13 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
53
57
|
return item;
|
|
54
58
|
};
|
|
55
59
|
// Internal State
|
|
56
|
-
let _headerMap = null;
|
|
57
60
|
let _hasPaginated = false;
|
|
58
|
-
let _isInitialized = false;
|
|
59
61
|
// Helpers
|
|
60
62
|
const log = (msg) => {
|
|
61
|
-
(0, debugUtils_1.logDebug)(config, 'verbose', msg);
|
|
63
|
+
(0, debugUtils_1.logDebug)(config, 'verbose', msg);
|
|
62
64
|
};
|
|
63
65
|
const _createColumnError = (colName, map, context) => {
|
|
64
66
|
const availableColumns = Array.from(map.keys());
|
|
65
|
-
// Use Suggestion Logic from ResolutionStrategy (if we had a fuzzy one, for now manual suggest)
|
|
66
67
|
const lowerCol = colName.toLowerCase();
|
|
67
68
|
const suggestions = availableColumns.filter(col => col.toLowerCase().includes(lowerCol) ||
|
|
68
69
|
lowerCol.includes(col.toLowerCase()) ||
|
|
@@ -80,138 +81,16 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
80
81
|
const contextMsg = context ? ` (${context})` : '';
|
|
81
82
|
return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
|
|
82
83
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
log('Mapping headers...');
|
|
87
|
-
const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
|
|
88
|
-
if (config.autoScroll) {
|
|
89
|
-
try {
|
|
90
|
-
yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
|
|
91
|
-
}
|
|
92
|
-
catch (e) { }
|
|
93
|
-
}
|
|
94
|
-
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
95
|
-
try {
|
|
96
|
-
yield headerLoc.first().waitFor({ state: 'visible', timeout: headerTimeout });
|
|
97
|
-
}
|
|
98
|
-
catch (e) { /* Ignore hydration */ }
|
|
99
|
-
const strategy = config.strategies.header || headers_1.HeaderStrategies.visible;
|
|
100
|
-
const context = {
|
|
101
|
-
root: rootLocator,
|
|
102
|
-
config: config,
|
|
103
|
-
page: rootLocator.page(),
|
|
104
|
-
resolve: resolve
|
|
105
|
-
};
|
|
106
|
-
const rawHeaders = yield strategy(context);
|
|
107
|
-
const seenHeaders = new Set();
|
|
108
|
-
const entries = [];
|
|
109
|
-
for (let i = 0; i < rawHeaders.length; i++) {
|
|
110
|
-
let text = rawHeaders[i].trim() || `__col_${i}`;
|
|
111
|
-
if (config.headerTransformer) {
|
|
112
|
-
text = yield config.headerTransformer({
|
|
113
|
-
text,
|
|
114
|
-
index: i,
|
|
115
|
-
locator: rootLocator.locator(config.headerSelector).nth(i),
|
|
116
|
-
seenHeaders
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
entries.push([text, i]);
|
|
120
|
-
seenHeaders.add(text);
|
|
121
|
-
}
|
|
122
|
-
// Validation: Check for empty table
|
|
123
|
-
if (entries.length === 0) {
|
|
124
|
-
throw new Error(`Initialization Error: No columns found using selector "${config.headerSelector}". Check your selector or ensure the table is visible.`);
|
|
125
|
-
}
|
|
126
|
-
// Validation: Check for duplicates
|
|
127
|
-
const seen = new Set();
|
|
128
|
-
const duplicates = new Set();
|
|
129
|
-
for (const [name] of entries) {
|
|
130
|
-
if (seen.has(name)) {
|
|
131
|
-
duplicates.add(name);
|
|
132
|
-
}
|
|
133
|
-
seen.add(name);
|
|
134
|
-
}
|
|
135
|
-
if (duplicates.size > 0) {
|
|
136
|
-
const dupList = Array.from(duplicates).map(d => `"${d}"`).join(', ');
|
|
137
|
-
throw new Error(`Initialization Error: Duplicate column names found: ${dupList}. Use 'headerTransformer' to rename duplicate columns.`);
|
|
138
|
-
}
|
|
139
|
-
_headerMap = new Map(entries);
|
|
140
|
-
log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
|
|
141
|
-
return _headerMap;
|
|
142
|
-
});
|
|
84
|
+
// Engines
|
|
85
|
+
const filterEngine = new filterEngine_1.FilterEngine(config, resolve);
|
|
86
|
+
const tableMapper = new tableMapper_1.TableMapper(rootLocator, config, resolve);
|
|
143
87
|
// Placeholder for the final table object
|
|
144
88
|
let finalTable = null;
|
|
145
|
-
const filterEngine = new filterEngine_1.FilterEngine(config, resolve);
|
|
146
89
|
// Helper factory
|
|
147
90
|
const _makeSmart = (rowLocator, map, rowIndex) => {
|
|
148
|
-
// Use the wrapped SmartRow logic
|
|
149
91
|
return (0, smartRow_1.createSmartRow)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable);
|
|
150
92
|
};
|
|
151
|
-
const
|
|
152
|
-
var _a, _b;
|
|
153
|
-
const map = yield _getMap();
|
|
154
|
-
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
155
|
-
let currentPage = 1;
|
|
156
|
-
log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
|
|
157
|
-
while (true) {
|
|
158
|
-
// Check for table loading
|
|
159
|
-
if ((_b = config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
|
|
160
|
-
const isLoading = yield config.strategies.loading.isTableLoading({ root: rootLocator, config, page: rootLocator.page(), resolve });
|
|
161
|
-
if (isLoading) {
|
|
162
|
-
log('Table is loading... waiting');
|
|
163
|
-
yield rootLocator.page().waitForTimeout(200);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const allRows = resolve(config.rowSelector, rootLocator);
|
|
168
|
-
// Use FilterEngine
|
|
169
|
-
const matchedRows = filterEngine.applyFilters(allRows, filters, map, options.exact || false, rootLocator.page());
|
|
170
|
-
const count = yield matchedRows.count();
|
|
171
|
-
log(`Page ${currentPage}: Found ${count} matches.`);
|
|
172
|
-
if (count > 1) {
|
|
173
|
-
// Sample data logic (simplified for refactor, kept inline or moved to util if needed)
|
|
174
|
-
const sampleData = [];
|
|
175
|
-
try {
|
|
176
|
-
const firstFewRows = yield matchedRows.all();
|
|
177
|
-
const sampleCount = Math.min(firstFewRows.length, 3);
|
|
178
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
179
|
-
const rowData = yield _makeSmart(firstFewRows[i], map).toJSON();
|
|
180
|
-
sampleData.push(JSON.stringify(rowData));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (e) { }
|
|
184
|
-
const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
|
|
185
|
-
throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
|
|
186
|
-
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
187
|
-
}
|
|
188
|
-
if (count === 1)
|
|
189
|
-
return matchedRows.first();
|
|
190
|
-
if (currentPage < effectiveMaxPages) {
|
|
191
|
-
log(`Page ${currentPage}: Not found. Attempting pagination...`);
|
|
192
|
-
const context = {
|
|
193
|
-
root: rootLocator,
|
|
194
|
-
config: config,
|
|
195
|
-
page: rootLocator.page(),
|
|
196
|
-
resolve: resolve
|
|
197
|
-
};
|
|
198
|
-
const paginationResult = yield config.strategies.pagination(context);
|
|
199
|
-
const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
|
|
200
|
-
if (didLoadMore) {
|
|
201
|
-
_hasPaginated = true;
|
|
202
|
-
currentPage++;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
log(`Page ${currentPage}: Pagination failed (end of data).`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (_hasPaginated) {
|
|
210
|
-
console.warn(`⚠️ [SmartTable] Row not found. The table has been paginated (Current Page: ${currentPage}). You may need to call 'await table.reset()' if the target row is on a previous page.`);
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
});
|
|
93
|
+
const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart);
|
|
215
94
|
const _getCleanHtml = (loc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
95
|
return loc.evaluate((el) => {
|
|
217
96
|
const clone = el.cloneNode(true);
|
|
@@ -247,28 +126,21 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
247
126
|
console.log(finalPrompt);
|
|
248
127
|
});
|
|
249
128
|
const _ensureInitialized = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
|
-
|
|
251
|
-
yield _getMap();
|
|
252
|
-
_isInitialized = true;
|
|
253
|
-
}
|
|
129
|
+
yield tableMapper.getMap();
|
|
254
130
|
});
|
|
255
131
|
const result = {
|
|
256
132
|
init: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
257
|
-
if (
|
|
133
|
+
if (tableMapper.isInitialized())
|
|
258
134
|
return result;
|
|
259
135
|
(0, debugUtils_1.warnIfDebugInCI)(config);
|
|
260
136
|
(0, debugUtils_1.logDebug)(config, 'info', 'Initializing table');
|
|
261
|
-
yield
|
|
262
|
-
|
|
263
|
-
if (_headerMap) {
|
|
264
|
-
(0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${_headerMap.size} columns`, Array.from(_headerMap.keys()));
|
|
265
|
-
// Trace event removed - redundant with debug logging
|
|
266
|
-
}
|
|
137
|
+
const map = yield tableMapper.getMap(options === null || options === void 0 ? void 0 : options.timeout);
|
|
138
|
+
(0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${map.size} columns`, Array.from(map.keys()));
|
|
267
139
|
yield (0, debugUtils_1.debugDelay)(config, 'default');
|
|
268
140
|
return result;
|
|
269
141
|
}),
|
|
270
142
|
scrollToColumn: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
271
|
-
const map = yield
|
|
143
|
+
const map = yield tableMapper.getMap();
|
|
272
144
|
const idx = map.get(columnName);
|
|
273
145
|
if (idx === undefined)
|
|
274
146
|
throw _createColumnError(columnName, map);
|
|
@@ -282,14 +154,14 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
282
154
|
});
|
|
283
155
|
}),
|
|
284
156
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
285
|
-
yield
|
|
286
|
-
return Array.from(
|
|
157
|
+
const map = yield tableMapper.getMap();
|
|
158
|
+
return Array.from(map.keys());
|
|
287
159
|
}),
|
|
288
160
|
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
289
|
-
yield
|
|
290
|
-
const idx =
|
|
161
|
+
const map = yield tableMapper.getMap();
|
|
162
|
+
const idx = map.get(columnName);
|
|
291
163
|
if (idx === undefined)
|
|
292
|
-
throw _createColumnError(columnName,
|
|
164
|
+
throw _createColumnError(columnName, map, 'header cell');
|
|
293
165
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
294
166
|
}),
|
|
295
167
|
reset: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -297,22 +169,20 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
297
169
|
const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
|
|
298
170
|
yield config.onReset(context);
|
|
299
171
|
_hasPaginated = false;
|
|
300
|
-
|
|
301
|
-
_isInitialized = false;
|
|
172
|
+
tableMapper.clear();
|
|
302
173
|
log("Table reset complete.");
|
|
303
174
|
}),
|
|
304
175
|
revalidate: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
305
176
|
log("Revalidating table structure...");
|
|
306
|
-
|
|
307
|
-
yield _getMap(); // Re-scan headers
|
|
177
|
+
yield tableMapper.remapHeaders();
|
|
308
178
|
log("Table revalidated.");
|
|
309
179
|
}),
|
|
310
180
|
getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
311
181
|
var _a, _b;
|
|
312
|
-
yield
|
|
313
|
-
const colIdx =
|
|
182
|
+
const map = yield tableMapper.getMap();
|
|
183
|
+
const colIdx = map.get(column);
|
|
314
184
|
if (colIdx === undefined)
|
|
315
|
-
throw _createColumnError(column,
|
|
185
|
+
throw _createColumnError(column, map);
|
|
316
186
|
const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
|
|
317
187
|
const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
|
|
318
188
|
let currentPage = 1;
|
|
@@ -339,103 +209,33 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
339
209
|
return results;
|
|
340
210
|
}),
|
|
341
211
|
getRow: (filters, options = { exact: false }) => {
|
|
342
|
-
|
|
212
|
+
const map = tableMapper.getMapSync();
|
|
213
|
+
if (!map)
|
|
343
214
|
throw new Error('Table not initialized. Call await table.init() first, or use async methods like table.findRow() or table.getRows() which auto-initialize.');
|
|
344
215
|
const allRows = resolve(config.rowSelector, rootLocator);
|
|
345
|
-
const matchedRows = filterEngine.applyFilters(allRows, filters,
|
|
216
|
+
const matchedRows = filterEngine.applyFilters(allRows, filters, map, options.exact || false, rootLocator.page());
|
|
346
217
|
const rowLocator = matchedRows.first();
|
|
347
|
-
return _makeSmart(rowLocator,
|
|
218
|
+
return _makeSmart(rowLocator, map, 0); // fallback index 0
|
|
348
219
|
},
|
|
349
220
|
getRowByIndex: (index, options = {}) => {
|
|
350
|
-
|
|
221
|
+
const map = tableMapper.getMapSync();
|
|
222
|
+
if (!map)
|
|
351
223
|
throw new Error('Table not initialized. Call await table.init() first, or use async methods like table.findRow() or table.getRows() which auto-initialize.');
|
|
352
224
|
const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
|
|
353
|
-
return _makeSmart(rowLocator,
|
|
225
|
+
return _makeSmart(rowLocator, map, index);
|
|
354
226
|
},
|
|
355
227
|
findRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
356
|
-
|
|
357
|
-
yield _ensureInitialized();
|
|
358
|
-
let row = yield _findRowLocator(filters, options);
|
|
359
|
-
if (row) {
|
|
360
|
-
(0, debugUtils_1.logDebug)(config, 'info', 'Row found');
|
|
361
|
-
yield (0, debugUtils_1.debugDelay)(config, 'findRow');
|
|
362
|
-
return _makeSmart(row, _headerMap, 0);
|
|
363
|
-
}
|
|
364
|
-
(0, debugUtils_1.logDebug)(config, 'error', 'Row not found', filters);
|
|
365
|
-
yield (0, debugUtils_1.debugDelay)(config, 'findRow');
|
|
366
|
-
// Return sentinel row
|
|
367
|
-
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
368
|
-
return _makeSmart(row, _headerMap, 0);
|
|
228
|
+
return rowFinder.findRow(filters, options);
|
|
369
229
|
}),
|
|
370
230
|
getRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let rowLocators = resolve(config.rowSelector, rootLocator);
|
|
374
|
-
if (options === null || options === void 0 ? void 0 : options.filter) {
|
|
375
|
-
rowLocators = filterEngine.applyFilters(rowLocators, options.filter, _headerMap, options.exact || false, rootLocator.page());
|
|
376
|
-
}
|
|
377
|
-
const allRowLocs = yield rowLocators.all();
|
|
378
|
-
const smartRows = [];
|
|
379
|
-
const isRowLoading = (_a = config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isRowLoading;
|
|
380
|
-
for (let i = 0; i < allRowLocs.length; i++) {
|
|
381
|
-
const smartRow = _makeSmart(allRowLocs[i], _headerMap, i);
|
|
382
|
-
if (isRowLoading) {
|
|
383
|
-
const loading = yield isRowLoading(smartRow);
|
|
384
|
-
if (loading)
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
smartRows.push(smartRow);
|
|
388
|
-
}
|
|
389
|
-
return (0, smartRowArray_1.createSmartRowArray)(smartRows);
|
|
231
|
+
console.warn('DEPRECATED: table.getRows() is deprecated and will be removed in a future version. Use table.findRows() instead.');
|
|
232
|
+
return rowFinder.findRows((options === null || options === void 0 ? void 0 : options.filter) || {}, Object.assign(Object.assign({}, options), { maxPages: 1 }));
|
|
390
233
|
}),
|
|
391
234
|
findRows: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
392
|
-
|
|
393
|
-
yield _ensureInitialized();
|
|
394
|
-
const allRows = [];
|
|
395
|
-
const effectiveMaxPages = (_b = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
|
|
396
|
-
let pageCount = 0;
|
|
397
|
-
// Collect rows from current page
|
|
398
|
-
let rowLocators = resolve(config.rowSelector, rootLocator);
|
|
399
|
-
rowLocators = filterEngine.applyFilters(rowLocators, filters, _headerMap, (_c = options === null || options === void 0 ? void 0 : options.exact) !== null && _c !== void 0 ? _c : false, rootLocator.page());
|
|
400
|
-
let currentRows = yield rowLocators.all();
|
|
401
|
-
const isRowLoading = (_d = config.strategies.loading) === null || _d === void 0 ? void 0 : _d.isRowLoading;
|
|
402
|
-
for (let i = 0; i < currentRows.length; i++) {
|
|
403
|
-
const smartRow = _makeSmart(currentRows[i], _headerMap, i);
|
|
404
|
-
if (isRowLoading && (yield isRowLoading(smartRow)))
|
|
405
|
-
continue;
|
|
406
|
-
allRows.push(smartRow);
|
|
407
|
-
}
|
|
408
|
-
// Paginate and collect more rows
|
|
409
|
-
while (pageCount < effectiveMaxPages && config.strategies.pagination) {
|
|
410
|
-
const paginationResult = yield config.strategies.pagination({
|
|
411
|
-
root: rootLocator,
|
|
412
|
-
config,
|
|
413
|
-
resolve,
|
|
414
|
-
page: rootLocator.page()
|
|
415
|
-
});
|
|
416
|
-
const didPaginate = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
|
|
417
|
-
if (!didPaginate)
|
|
418
|
-
break;
|
|
419
|
-
pageCount++;
|
|
420
|
-
_hasPaginated = true;
|
|
421
|
-
// Collect rows from new page
|
|
422
|
-
rowLocators = resolve(config.rowSelector, rootLocator);
|
|
423
|
-
rowLocators = filterEngine.applyFilters(rowLocators, filters, _headerMap, (_e = options === null || options === void 0 ? void 0 : options.exact) !== null && _e !== void 0 ? _e : false, rootLocator.page());
|
|
424
|
-
const newRows = yield rowLocators.all();
|
|
425
|
-
for (let i = 0; i < newRows.length; i++) {
|
|
426
|
-
const smartRow = _makeSmart(newRows[i], _headerMap, i);
|
|
427
|
-
if (isRowLoading && (yield isRowLoading(smartRow)))
|
|
428
|
-
continue;
|
|
429
|
-
allRows.push(smartRow);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
433
|
-
return Promise.all(allRows.map(r => r.toJSON()));
|
|
434
|
-
}
|
|
435
|
-
return allRows;
|
|
235
|
+
return rowFinder.findRows(filters, options);
|
|
436
236
|
}),
|
|
437
237
|
isInitialized: () => {
|
|
438
|
-
return
|
|
238
|
+
return tableMapper.isInitialized();
|
|
439
239
|
},
|
|
440
240
|
sorting: {
|
|
441
241
|
apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -463,6 +263,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
463
263
|
throw new Error('No pagination strategy provided.');
|
|
464
264
|
yield result.reset();
|
|
465
265
|
yield result.init();
|
|
266
|
+
const map = tableMapper.getMapSync();
|
|
466
267
|
const restrictedTable = {
|
|
467
268
|
init: result.init,
|
|
468
269
|
getHeaders: result.getHeaders,
|
|
@@ -497,7 +298,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
497
298
|
const smartRowsArray = [];
|
|
498
299
|
const isRowLoading = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.isRowLoading;
|
|
499
300
|
for (let i = 0; i < rowLocators.length; i++) {
|
|
500
|
-
const smartRow = _makeSmart(rowLocators[i],
|
|
301
|
+
const smartRow = _makeSmart(rowLocators[i], map, i);
|
|
501
302
|
if (isRowLoading && (yield isRowLoading(smartRow)))
|
|
502
303
|
continue;
|
|
503
304
|
smartRowsArray.push(smartRow);
|
|
@@ -533,7 +334,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
533
334
|
let isLast = getIsLast({ index: callbackIndex, paginationResult });
|
|
534
335
|
const isLastDueToMax = index === effectiveMaxIterations - 1;
|
|
535
336
|
if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
|
|
536
|
-
yield options.beforeFirst({ index: callbackIndex, rows: callbackRows, allData });
|
|
337
|
+
yield options.beforeFirst({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(callbackRows), allData });
|
|
537
338
|
}
|
|
538
339
|
const batchInfo = isBatching ? {
|
|
539
340
|
startIndex: batchStartIndex,
|
|
@@ -565,7 +366,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
565
366
|
finalIsLast = getIsLast({ index: callbackIndex, paginationResult }) || !paginationResult;
|
|
566
367
|
}
|
|
567
368
|
if (finalIsLast && (options === null || options === void 0 ? void 0 : options.afterLast)) {
|
|
568
|
-
yield options.afterLast({ index: callbackIndex, rows: callbackRows, allData });
|
|
369
|
+
yield options.afterLast({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(callbackRows), allData });
|
|
569
370
|
}
|
|
570
371
|
if (finalIsLast || !paginationResult) {
|
|
571
372
|
log(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult})`);
|
|
@@ -589,7 +390,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
589
390
|
const isFirst = getIsFirst({ index: callbackIndex });
|
|
590
391
|
const isLast = true;
|
|
591
392
|
if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
|
|
592
|
-
yield options.beforeFirst({ index: callbackIndex, rows: batchRows, allData });
|
|
393
|
+
yield options.beforeFirst({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(batchRows), allData });
|
|
593
394
|
}
|
|
594
395
|
const batchInfo = {
|
|
595
396
|
startIndex: batchStartIndex,
|
|
@@ -612,7 +413,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
612
413
|
allData.push(returnValue);
|
|
613
414
|
}
|
|
614
415
|
if (options === null || options === void 0 ? void 0 : options.afterLast) {
|
|
615
|
-
yield options.afterLast({ index: callbackIndex, rows: batchRows, allData });
|
|
416
|
+
yield options.afterLast({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(batchRows), allData });
|
|
616
417
|
}
|
|
617
418
|
log(`Pagination failed mid-batch (index: ${index})`);
|
|
618
419
|
break;
|
|
@@ -18,11 +18,19 @@ function levenshteinDistance(a, b) {
|
|
|
18
18
|
}
|
|
19
19
|
for (let i = 1; i <= b.length; i++) {
|
|
20
20
|
for (let j = 1; j <= a.length; j++) {
|
|
21
|
-
|
|
21
|
+
const charB = b.charAt(i - 1);
|
|
22
|
+
const charA = a.charAt(j - 1);
|
|
23
|
+
if (charB === charA) {
|
|
24
|
+
// Exact match
|
|
22
25
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
23
26
|
}
|
|
24
27
|
else {
|
|
25
|
-
|
|
28
|
+
let cost = 1;
|
|
29
|
+
// If characters match ignoring case, cost is only 0.1 (almost identical)
|
|
30
|
+
if (charB.toLowerCase() === charA.toLowerCase()) {
|
|
31
|
+
cost = 0.1;
|
|
32
|
+
}
|
|
33
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + cost, // substitution
|
|
26
34
|
matrix[i][j - 1] + 1, // insertion
|
|
27
35
|
matrix[i - 1][j] + 1 // deletion
|
|
28
36
|
);
|
|
@@ -36,7 +44,8 @@ function levenshteinDistance(a, b) {
|
|
|
36
44
|
* 1 = identical, 0 = completely different
|
|
37
45
|
*/
|
|
38
46
|
function stringSimilarity(a, b) {
|
|
39
|
-
|
|
47
|
+
// We do NOT modify case here anymore, because levenshteinDistance now handles case weighting
|
|
48
|
+
const distance = levenshteinDistance(a, b);
|
|
40
49
|
const maxLen = Math.max(a.length, b.length);
|
|
41
50
|
return maxLen === 0 ? 1 : 1 - (distance / maxLen);
|
|
42
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Smart, column-aware table interactions for Playwright",
|
|
5
5
|
"author": "Cedrick Catalan",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"docs:build": "vitepress build docs",
|
|
23
23
|
"build": "npm run generate-types && npm run generate-docs && npm run generate-all-api-docs && npm run update-all-api-signatures && tsc",
|
|
24
24
|
"prepublishOnly": "npm run build",
|
|
25
|
-
"test": "npx playwright test",
|
|
25
|
+
"test": "npm run test:unit && npx playwright test",
|
|
26
|
+
"test:unit": "vitest run --reporter=verbose --reporter=html",
|
|
27
|
+
"test:unit:ui": "vitest --ui",
|
|
28
|
+
"test:e2e": "npx playwright test",
|
|
26
29
|
"test:compatibility": "npx playwright test compatibility",
|
|
27
30
|
"prepare": "husky install"
|
|
28
31
|
},
|
|
@@ -38,9 +41,11 @@
|
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@playwright/test": "^1.49.1",
|
|
40
43
|
"@types/node": "^22.10.5",
|
|
44
|
+
"@vitest/ui": "^4.0.18",
|
|
45
|
+
"happy-dom": "^20.6.1",
|
|
41
46
|
"husky": "^9.1.7",
|
|
42
47
|
"typescript": "^5.7.2",
|
|
43
|
-
"vitepress": "^1.6.4"
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
"vitepress": "^1.6.4",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
}
|
|
46
51
|
}
|