@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 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
  };
@@ -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";
@@ -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>[] : SmartRow[]>;
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: SmartRow[], allData: any[] }) => void | Promise<void>;
445
- afterLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;
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>[] : SmartRow[]>;
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: SmartRow[];
436
+ rows: SmartRowArray;
436
437
  allData: any[];
437
438
  }) => void | Promise<void>;
438
439
  afterLast?: (context: {
439
440
  index: number;
440
- rows: SmartRow[];
441
+ rows: SmartRowArray;
441
442
  allData: any[];
442
443
  }) => void | Promise<void>;
443
444
  /**
@@ -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); // Legacy(`🔎 [SmartTable Debug] ${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
- const _getMap = (timeout) => __awaiter(void 0, void 0, void 0, function* () {
84
- if (_headerMap)
85
- return _headerMap;
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 _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
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
- if (!_isInitialized) {
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 (_isInitialized && _headerMap)
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 _getMap(options === null || options === void 0 ? void 0 : options.timeout);
262
- _isInitialized = true;
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 _getMap();
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 _ensureInitialized();
286
- return Array.from(_headerMap.keys());
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 _ensureInitialized();
290
- const idx = _headerMap.get(columnName);
161
+ const map = yield tableMapper.getMap();
162
+ const idx = map.get(columnName);
291
163
  if (idx === undefined)
292
- throw _createColumnError(columnName, _headerMap, 'header cell');
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
- _headerMap = null;
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
- _headerMap = null; // Clear the map to force re-scanning
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 _ensureInitialized();
313
- const colIdx = _headerMap.get(column);
182
+ const map = yield tableMapper.getMap();
183
+ const colIdx = map.get(column);
314
184
  if (colIdx === undefined)
315
- throw _createColumnError(column, _headerMap);
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
- if (!_isInitialized || !_headerMap)
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, _headerMap, options.exact || false, rootLocator.page());
216
+ const matchedRows = filterEngine.applyFilters(allRows, filters, map, options.exact || false, rootLocator.page());
346
217
  const rowLocator = matchedRows.first();
347
- return _makeSmart(rowLocator, _headerMap, 0); // fallback index 0
218
+ return _makeSmart(rowLocator, map, 0); // fallback index 0
348
219
  },
349
220
  getRowByIndex: (index, options = {}) => {
350
- if (!_isInitialized || !_headerMap)
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, _headerMap, index);
225
+ return _makeSmart(rowLocator, map, index);
354
226
  },
355
227
  findRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
356
- (0, debugUtils_1.logDebug)(config, 'info', 'Searching for row', filters);
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
- var _a;
372
- yield _ensureInitialized();
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
- var _a, _b, _c, _d, _e;
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 _isInitialized;
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], _headerMap, 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
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
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
- matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
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
- const distance = levenshteinDistance(a.toLowerCase(), b.toLowerCase());
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.1.1",
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
- "dependencies": {}
48
+ "vitepress": "^1.6.4",
49
+ "vitest": "^4.0.18"
50
+ }
46
51
  }