@rickcedwhat/playwright-smart-table 6.1.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Production-ready table testing for Playwright with smart column-aware locators.**
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/@rickcedwhat/playwright-smart-table.svg)](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
5
+ [![npm version](https://img.shields.io/github/package-json/v/rickcedwhat/playwright-smart-table?label=npm&color=blue&t=2)](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  ---
@@ -79,10 +79,12 @@ const allActive = await table.findRows({ Status: 'Active' });
79
79
  ## Key Features
80
80
 
81
81
  - 🎯 **Smart Locators** - Find rows by content, not position
82
+ - 🧠 **Fuzzy Matching** - Smart suggestions for typos (e.g., incorrectly typed "Firstname" suggests "First Name" in error messages)
83
+ - ⚡ **Smart Initialization** - Handles loading states and dynamic headers automatically
82
84
  - 📄 **Auto-Pagination** - Search across all pages automatically
83
85
  - 🔍 **Column-Aware Access** - Access cells by column name
84
86
  - 🛠️ **Debug Mode** - Visual debugging with slow motion and logging
85
- - 🔌 **Extensible Strategies** - Support any table implementation
87
+ - 🔌 **[Extensible Strategies](docs/concepts/strategies.md)** - Support any table implementation
86
88
  - 💪 **Type-Safe** - Full TypeScript support
87
89
  - 🚀 **Production-Ready** - Battle-tested in real-world applications
88
90
 
@@ -0,0 +1,27 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { FinalTableConfig, Selector, SmartRow, FilterValue } from '../types';
3
+ import { FilterEngine } from '../filterEngine';
4
+ import { TableMapper } from './tableMapper';
5
+ import { SmartRowArray } from '../utils/smartRowArray';
6
+ export declare class RowFinder<T = any> {
7
+ private rootLocator;
8
+ private config;
9
+ private filterEngine;
10
+ private tableMapper;
11
+ private makeSmartRow;
12
+ private resolve;
13
+ constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number) => SmartRow<T>);
14
+ private log;
15
+ findRow(filters: Record<string, FilterValue>, options?: {
16
+ exact?: boolean;
17
+ maxPages?: number;
18
+ }): Promise<SmartRow<T>>;
19
+ findRows(filtersOrOptions?: (Partial<T> | Record<string, FilterValue>) & ({
20
+ exact?: boolean;
21
+ maxPages?: number;
22
+ }), legacyOptions?: {
23
+ exact?: boolean;
24
+ maxPages?: number;
25
+ }): Promise<SmartRowArray<T>>;
26
+ private findRowLocator;
27
+ }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.RowFinder = void 0;
24
+ const debugUtils_1 = require("../utils/debugUtils");
25
+ const smartRowArray_1 = require("../utils/smartRowArray");
26
+ const validation_1 = require("../strategies/validation");
27
+ class RowFinder {
28
+ constructor(rootLocator, config, resolve, filterEngine, tableMapper, makeSmartRow) {
29
+ this.rootLocator = rootLocator;
30
+ this.config = config;
31
+ this.filterEngine = filterEngine;
32
+ this.tableMapper = tableMapper;
33
+ this.makeSmartRow = makeSmartRow;
34
+ this.resolve = resolve;
35
+ }
36
+ log(msg) {
37
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
38
+ }
39
+ findRow(filters_1) {
40
+ return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
41
+ (0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
42
+ yield this.tableMapper.getMap();
43
+ const rowLocator = yield this.findRowLocator(filters, options);
44
+ if (rowLocator) {
45
+ (0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
46
+ yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
47
+ return this.makeSmartRow(rowLocator, yield this.tableMapper.getMap(), 0);
48
+ }
49
+ (0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
50
+ yield (0, debugUtils_1.debugDelay)(this.config, 'findRow');
51
+ const sentinel = this.resolve(this.config.rowSelector, this.rootLocator)
52
+ .filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
53
+ return this.makeSmartRow(sentinel, yield this.tableMapper.getMap(), 0);
54
+ });
55
+ }
56
+ findRows(filtersOrOptions,
57
+ // Deprecated: verify legacy usage pattern support
58
+ legacyOptions) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ // Detect argument pattern:
61
+ // Pattern A: findRows({ Name: 'Alice' }, { maxPages: 5 })
62
+ // Pattern B: findRows({ maxPages: 5 }) <-- No filters, just options
63
+ // Pattern C: findRows({ Name: 'Alice' }) <-- Only filters
64
+ var _a, _b;
65
+ let filters = {};
66
+ let options = {};
67
+ if (legacyOptions) {
68
+ // Pattern A
69
+ filters = filtersOrOptions;
70
+ options = legacyOptions;
71
+ }
72
+ else {
73
+ // Pattern B or C
74
+ // We need to separate unknown keys (filters) from known options (exact, maxPages)
75
+ // However, filtersOrOptions can be null/undefined
76
+ if (filtersOrOptions) {
77
+ const _c = filtersOrOptions, { exact, maxPages } = _c, rest = __rest(_c, ["exact", "maxPages"]);
78
+ options = { exact, maxPages };
79
+ filters = rest;
80
+ }
81
+ }
82
+ const map = yield this.tableMapper.getMap();
83
+ const allRows = [];
84
+ const effectiveMaxPages = (_b = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
85
+ let pageCount = 0;
86
+ const collectMatches = () => __awaiter(this, void 0, void 0, function* () {
87
+ var _a, _b;
88
+ // ... logic ...
89
+ let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
90
+ // Only apply filters if we have them
91
+ if (Object.keys(filters).length > 0) {
92
+ rowLocators = this.filterEngine.applyFilters(rowLocators, filters, map, (_a = options.exact) !== null && _a !== void 0 ? _a : false, this.rootLocator.page());
93
+ }
94
+ const currentRows = yield rowLocators.all();
95
+ const isRowLoading = (_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isRowLoading;
96
+ for (let i = 0; i < currentRows.length; i++) {
97
+ const smartRow = this.makeSmartRow(currentRows[i], map, allRows.length + i);
98
+ if (isRowLoading && (yield isRowLoading(smartRow)))
99
+ continue;
100
+ allRows.push(smartRow);
101
+ }
102
+ });
103
+ // Scan first page
104
+ yield collectMatches();
105
+ // Pagination Loop - Corrected logic
106
+ // We always scan at least 1 page.
107
+ // If maxPages > 1, and we have a pagination strategy, we try to go next.
108
+ while (pageCount < effectiveMaxPages - 1 && this.config.strategies.pagination) {
109
+ const context = {
110
+ root: this.rootLocator,
111
+ config: this.config,
112
+ resolve: this.resolve,
113
+ page: this.rootLocator.page()
114
+ };
115
+ // Check if we should stop? (e.g. if we found enough rows? No, findRows finds ALL)
116
+ const paginationResult = yield this.config.strategies.pagination(context);
117
+ const didPaginate = yield (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
118
+ if (!didPaginate)
119
+ break;
120
+ pageCount++;
121
+ // Wait for reload logic if needed? Usually pagination handles it.
122
+ yield collectMatches();
123
+ }
124
+ return (0, smartRowArray_1.createSmartRowArray)(allRows);
125
+ });
126
+ }
127
+ findRowLocator(filters_1) {
128
+ return __awaiter(this, arguments, void 0, function* (filters, options = {}) {
129
+ var _a, _b;
130
+ const map = yield this.tableMapper.getMap();
131
+ const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
132
+ let currentPage = 1;
133
+ this.log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
134
+ while (true) {
135
+ // Check Loading
136
+ if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
137
+ const isLoading = yield this.config.strategies.loading.isTableLoading({
138
+ root: this.rootLocator,
139
+ config: this.config,
140
+ page: this.rootLocator.page(),
141
+ resolve: this.resolve
142
+ });
143
+ if (isLoading) {
144
+ this.log('Table is loading... waiting');
145
+ yield this.rootLocator.page().waitForTimeout(200);
146
+ continue;
147
+ }
148
+ }
149
+ const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
150
+ const matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page());
151
+ const count = yield matchedRows.count();
152
+ this.log(`Page ${currentPage}: Found ${count} matches.`);
153
+ if (count > 1) {
154
+ const sampleData = [];
155
+ try {
156
+ const firstFewRows = yield matchedRows.all();
157
+ const sampleCount = Math.min(firstFewRows.length, 3);
158
+ for (let i = 0; i < sampleCount; i++) {
159
+ const rowData = yield this.makeSmartRow(firstFewRows[i], map, 0).toJSON();
160
+ sampleData.push(JSON.stringify(rowData));
161
+ }
162
+ }
163
+ catch (e) { }
164
+ const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
165
+ throw new Error(`Ambiguous Row: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
166
+ `Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
167
+ }
168
+ if (count === 1)
169
+ return matchedRows.first();
170
+ if (currentPage < effectiveMaxPages) {
171
+ this.log(`Page ${currentPage}: Not found. Attempting pagination...`);
172
+ const context = {
173
+ root: this.rootLocator,
174
+ config: this.config,
175
+ resolve: this.resolve,
176
+ page: this.rootLocator.page()
177
+ };
178
+ const paginationResult = yield this.config.strategies.pagination(context);
179
+ const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
180
+ if (didLoadMore) {
181
+ currentPage++;
182
+ continue;
183
+ }
184
+ else {
185
+ this.log(`Page ${currentPage}: Pagination failed (end of data).`);
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ });
191
+ }
192
+ }
193
+ exports.RowFinder = RowFinder;
@@ -0,0 +1,16 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { FinalTableConfig, Selector } from '../types';
3
+ export declare class TableMapper {
4
+ private _headerMap;
5
+ private config;
6
+ private rootLocator;
7
+ private resolve;
8
+ constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator);
9
+ private log;
10
+ getMap(timeout?: number): Promise<Map<string, number>>;
11
+ remapHeaders(): Promise<void>;
12
+ getMapSync(): Map<string, number> | null;
13
+ isInitialized(): boolean;
14
+ clear(): void;
15
+ private processHeaders;
16
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TableMapper = void 0;
13
+ const headers_1 = require("../strategies/headers");
14
+ const debugUtils_1 = require("../utils/debugUtils");
15
+ class TableMapper {
16
+ constructor(rootLocator, config, resolve) {
17
+ this._headerMap = null;
18
+ this.rootLocator = rootLocator;
19
+ this.config = config;
20
+ this.resolve = resolve;
21
+ }
22
+ log(msg) {
23
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
24
+ }
25
+ getMap(timeout) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ var _a;
28
+ if (this._headerMap)
29
+ return this._headerMap;
30
+ this.log('Mapping headers...');
31
+ const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
32
+ const startTime = Date.now();
33
+ if (this.config.autoScroll) {
34
+ try {
35
+ yield this.rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
36
+ }
37
+ catch (e) { }
38
+ }
39
+ const headerLoc = this.resolve(this.config.headerSelector, this.rootLocator);
40
+ const strategy = this.config.strategies.header || headers_1.HeaderStrategies.visible;
41
+ const context = {
42
+ root: this.rootLocator,
43
+ config: this.config,
44
+ page: this.rootLocator.page(),
45
+ resolve: this.resolve
46
+ };
47
+ let lastError = null;
48
+ while (Date.now() - startTime < headerTimeout) {
49
+ // 1. Wait for visibility
50
+ try {
51
+ yield headerLoc.first().waitFor({ state: 'visible', timeout: 200 });
52
+ }
53
+ catch (e) {
54
+ // Continue to check existing/loading state even if not strictly "visible" yet
55
+ }
56
+ // 2. Check Smart Loading State
57
+ if ((_a = this.config.strategies.loading) === null || _a === void 0 ? void 0 : _a.isHeaderLoading) {
58
+ const isStable = !(yield this.config.strategies.loading.isHeaderLoading(context));
59
+ if (!isStable) {
60
+ this.log('Headers are loading/unstable... waiting');
61
+ yield new Promise(r => setTimeout(r, 100));
62
+ continue;
63
+ }
64
+ }
65
+ // 3. Attempt Scan
66
+ try {
67
+ const rawHeaders = yield strategy(context);
68
+ const entries = yield this.processHeaders(rawHeaders);
69
+ // Success
70
+ this._headerMap = new Map(entries);
71
+ this.log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
72
+ return this._headerMap;
73
+ }
74
+ catch (e) {
75
+ lastError = e;
76
+ this.log(`Header mapping failed (retrying): ${e.message}`);
77
+ yield new Promise(r => setTimeout(r, 100));
78
+ }
79
+ }
80
+ throw lastError || new Error(`Timed out waiting for headers after ${headerTimeout}ms`);
81
+ });
82
+ }
83
+ remapHeaders() {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ this._headerMap = null;
86
+ yield this.getMap();
87
+ });
88
+ }
89
+ getMapSync() {
90
+ return this._headerMap;
91
+ }
92
+ isInitialized() {
93
+ return this._headerMap !== null;
94
+ }
95
+ clear() {
96
+ this._headerMap = null;
97
+ }
98
+ processHeaders(rawHeaders) {
99
+ return __awaiter(this, void 0, void 0, function* () {
100
+ const seenHeaders = new Set();
101
+ const entries = [];
102
+ for (let i = 0; i < rawHeaders.length; i++) {
103
+ let text = rawHeaders[i].trim() || `__col_${i}`;
104
+ if (this.config.headerTransformer) {
105
+ text = yield this.config.headerTransformer({
106
+ text,
107
+ index: i,
108
+ locator: this.rootLocator.locator(this.config.headerSelector).nth(i),
109
+ seenHeaders
110
+ });
111
+ }
112
+ entries.push([text, i]);
113
+ seenHeaders.add(text);
114
+ }
115
+ // Validation: Check for empty table
116
+ if (entries.length === 0) {
117
+ throw new Error(`Initialization Error: No columns found using selector "${this.config.headerSelector}". Check your selector or ensure the table is visible.`);
118
+ }
119
+ // Validation: Check for duplicates
120
+ const seen = new Set();
121
+ const duplicates = new Set();
122
+ for (const [name] of entries) {
123
+ if (seen.has(name)) {
124
+ duplicates.add(name);
125
+ }
126
+ seen.add(name);
127
+ }
128
+ if (duplicates.size > 0) {
129
+ const dupList = Array.from(duplicates).map(d => `"${d}"`).join(', ');
130
+ throw new Error(`Initialization Error: Duplicate column names found: ${dupList}. Use 'headerTransformer' to rename duplicate columns.`);
131
+ }
132
+ return entries;
133
+ });
134
+ }
135
+ }
136
+ exports.TableMapper = TableMapper;
@@ -1,5 +1,5 @@
1
1
  import { Locator, Page } from "@playwright/test";
2
- import { FinalTableConfig } from "./types";
2
+ import { FinalTableConfig, FilterValue } from "./types";
3
3
  export declare class FilterEngine {
4
4
  private config;
5
5
  private resolve;
@@ -7,5 +7,5 @@ export declare class FilterEngine {
7
7
  /**
8
8
  * Applies filters to a set of rows.
9
9
  */
10
- applyFilters(baseRows: Locator, filters: Record<string, string | RegExp | number>, map: Map<string, number>, exact: boolean, page: Page): Locator;
10
+ applyFilters(baseRows: Locator, filters: Record<string, FilterValue>, map: Map<string, number>, exact: boolean, page: Page): Locator;
11
11
  }
@@ -20,7 +20,7 @@ class FilterEngine {
20
20
  if (colIndex === undefined) {
21
21
  throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
22
22
  }
23
- const filterVal = typeof value === 'number' ? String(value) : value;
23
+ const filterVal = value;
24
24
  // Use strategy if provided (For future: configured filter strategies)
25
25
  // But for now, we implement the default logic or use custom if we add it to config later
26
26
  // Default Filter Logic
@@ -29,9 +29,20 @@ class FilterEngine {
29
29
  // filter({ has: ... }) checks if the row *contains* the matching cell.
30
30
  // But we need to be specific about WHICH cell.
31
31
  // Locator filtering by `has: locator.nth(index)` works if `locator` search is relative to the row.
32
- filtered = filtered.filter({
33
- has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
34
- });
32
+ const targetCell = cellTemplate.nth(colIndex);
33
+ if (typeof filterVal === 'function') {
34
+ // Locator-based filter: (cell) => cell.locator(...)
35
+ filtered = filtered.filter({
36
+ has: filterVal(targetCell)
37
+ });
38
+ }
39
+ else {
40
+ // Text-based filter
41
+ const textVal = typeof filterVal === 'number' ? String(filterVal) : filterVal;
42
+ filtered = filtered.filter({
43
+ has: targetCell.getByText(textVal, { exact }),
44
+ });
45
+ }
35
46
  }
36
47
  return filtered;
37
48
  }
@@ -4,4 +4,4 @@ import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types
4
4
  * Factory to create a SmartRow by extending a Playwright Locator.
5
5
  * We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
6
6
  */
7
- export declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null) => SmartRowType<T>;
7
+ export declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig<T>, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null) => SmartRowType<T>;
package/dist/smartRow.js CHANGED
@@ -39,13 +39,23 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
39
39
  return resolve(config.cellSelector, rowLocator).nth(idx);
40
40
  };
41
41
  smart.toJSON = (options) => __awaiter(void 0, void 0, void 0, function* () {
42
+ var _a;
42
43
  const result = {};
43
44
  const page = rootLocator.page();
44
45
  for (const [col, idx] of map.entries()) {
45
46
  if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(col)) {
46
47
  continue;
47
48
  }
48
- // Get the cell locator
49
+ // Check if we have a data mapper for this column
50
+ const mapper = (_a = config.dataMapper) === null || _a === void 0 ? void 0 : _a[col];
51
+ if (mapper) {
52
+ // Use custom mapper
53
+ // Ensure we have the cell first (same navigation logic)
54
+ // ... wait, the navigation logic below assumes we need to navigate.
55
+ // If we have a mapper, we still need the cell locator.
56
+ // Let's reuse the navigation logic to get targetCell
57
+ }
58
+ // --- Navigation Logic Start ---
49
59
  const cell = config.strategies.getCellLocator
50
60
  ? config.strategies.getCellLocator({
51
61
  row: rowLocator,
@@ -56,7 +66,6 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
56
66
  })
57
67
  : resolve(config.cellSelector, rowLocator).nth(idx);
58
68
  let targetCell = cell;
59
- // Check if cell exists
60
69
  const count = yield cell.count();
61
70
  if (count === 0) {
62
71
  // Optimization: Check if we are ALREADY at the target cell
@@ -68,47 +77,59 @@ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve,
68
77
  resolve
69
78
  });
70
79
  if (active && active.rowIndex === rowIndex && active.columnIndex === idx) {
71
- if (config.debug)
72
- console.log(`[SmartRow] Already at target cell (r:${active.rowIndex}, c:${active.columnIndex}), skipping navigation.`);
73
80
  targetCell = active.locator;
74
- // Skip navigation and go to reading text
75
- const text = yield targetCell.innerText();
76
- result[col] = (text || '').trim();
77
- continue;
81
+ // Skip navigation
82
+ }
83
+ else {
84
+ // Cell doesn't exist - navigate to it
85
+ yield config.strategies.cellNavigation({
86
+ config: config,
87
+ root: rootLocator,
88
+ page: page,
89
+ resolve: resolve,
90
+ column: col,
91
+ index: idx,
92
+ rowLocator: rowLocator,
93
+ rowIndex: rowIndex
94
+ });
95
+ // Update targetCell after navigation if needed (e.g. active cell changed)
96
+ if (config.strategies.getActiveCell) {
97
+ const activeCell = yield config.strategies.getActiveCell({
98
+ config,
99
+ root: rootLocator,
100
+ page,
101
+ resolve
102
+ });
103
+ if (activeCell)
104
+ targetCell = activeCell.locator;
105
+ }
78
106
  }
79
107
  }
80
- // Cell doesn't exist - navigate to it
81
- if (config.debug) {
82
- console.log(`[SmartRow.toJSON] Cell not found for column "${col}" (index ${idx}), navigating...`);
83
- }
84
- yield config.strategies.cellNavigation({
85
- config: config,
86
- root: rootLocator,
87
- page: page,
88
- resolve: resolve,
89
- column: col,
90
- index: idx,
91
- rowLocator: rowLocator,
92
- rowIndex: rowIndex
93
- });
94
- // Optimization: check if we can get the active cell directly
95
- if (config.strategies.getActiveCell) {
96
- const activeCell = yield config.strategies.getActiveCell({
97
- config,
108
+ else {
109
+ // Fallback navigation without active cell check
110
+ yield config.strategies.cellNavigation({
111
+ config: config,
98
112
  root: rootLocator,
99
- page,
100
- resolve
113
+ page: page,
114
+ resolve: resolve,
115
+ column: col,
116
+ index: idx,
117
+ rowLocator: rowLocator,
118
+ rowIndex: rowIndex
101
119
  });
102
- if (activeCell) {
103
- if (config.debug) {
104
- console.log(`[SmartRow.toJSON] switching to active cell locator (r:${activeCell.rowIndex}, c:${activeCell.columnIndex})`);
105
- }
106
- targetCell = activeCell.locator;
107
- }
108
120
  }
109
121
  }
110
- const text = yield targetCell.innerText();
111
- result[col] = (text || '').trim();
122
+ // --- Navigation Logic End ---
123
+ if (mapper) {
124
+ // Apply mapper
125
+ const mappedValue = yield mapper(targetCell);
126
+ result[col] = mappedValue;
127
+ }
128
+ else {
129
+ // Default string extraction
130
+ const text = yield targetCell.innerText();
131
+ result[col] = (text || '').trim();
132
+ }
112
133
  }
113
134
  return result;
114
135
  });
@@ -50,5 +50,9 @@ export declare const Strategies: {
50
50
  hasEmptyCells: () => (row: import("..").SmartRow) => Promise<boolean>;
51
51
  never: () => Promise<boolean>;
52
52
  };
53
+ Headers: {
54
+ stable: (duration?: number) => (context: import("..").TableContext) => Promise<boolean>;
55
+ never: () => Promise<boolean>;
56
+ };
53
57
  };
54
58
  };
@@ -45,4 +45,18 @@ export declare const LoadingStrategies: {
45
45
  */
46
46
  never: () => Promise<boolean>;
47
47
  };
48
+ /**
49
+ * Strategies for detecting if headers are loading/stable.
50
+ */
51
+ Headers: {
52
+ /**
53
+ * Checks if the headers are stable (count and text) for a specified duration.
54
+ * @param duration Duration in ms for headers to remain unchanged to be considered stable (default: 200).
55
+ */
56
+ stable: (duration?: number) => (context: TableContext) => Promise<boolean>;
57
+ /**
58
+ * Assume headers are never loading (immediate snapshot).
59
+ */
60
+ never: () => Promise<boolean>;
61
+ };
48
62
  };
@@ -78,5 +78,36 @@ exports.LoadingStrategies = {
78
78
  * Assume row is never loading (default).
79
79
  */
80
80
  never: () => __awaiter(void 0, void 0, void 0, function* () { return false; })
81
+ },
82
+ /**
83
+ * Strategies for detecting if headers are loading/stable.
84
+ */
85
+ Headers: {
86
+ /**
87
+ * Checks if the headers are stable (count and text) for a specified duration.
88
+ * @param duration Duration in ms for headers to remain unchanged to be considered stable (default: 200).
89
+ */
90
+ stable: (duration = 200) => (context) => __awaiter(void 0, void 0, void 0, function* () {
91
+ const { config, resolve, root } = context;
92
+ const getHeaderTexts = () => __awaiter(void 0, void 0, void 0, function* () {
93
+ const headers = yield resolve(config.headerSelector, root).all();
94
+ return Promise.all(headers.map(h => h.innerText()));
95
+ });
96
+ const initial = yield getHeaderTexts();
97
+ // Wait for duration
98
+ yield context.page.waitForTimeout(duration);
99
+ const current = yield getHeaderTexts();
100
+ if (initial.length !== current.length)
101
+ return true; // Count changed, still loading
102
+ for (let i = 0; i < initial.length; i++) {
103
+ if (initial[i] !== current[i])
104
+ return true; // Content changed, still loading
105
+ }
106
+ return false; // Stable
107
+ }),
108
+ /**
109
+ * Assume headers are never loading (immediate snapshot).
110
+ */
111
+ never: () => __awaiter(void 0, void 0, void 0, function* () { return false; })
81
112
  }
82
113
  };