@rickcedwhat/playwright-smart-table 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +2 -0
  2. package/dist/engine/rowFinder.d.ts +26 -0
  3. package/dist/engine/rowFinder.js +161 -0
  4. package/dist/engine/tableMapper.d.ts +16 -0
  5. package/dist/engine/tableMapper.js +136 -0
  6. package/dist/{examples/glide-strategies → strategies/glide}/columns.d.ts +1 -1
  7. package/dist/{examples/glide-strategies → strategies/glide}/headers.d.ts +1 -1
  8. package/dist/{src/strategies → strategies}/glide.js +2 -2
  9. package/dist/strategies/index.d.ts +33 -2
  10. package/dist/strategies/index.js +6 -0
  11. package/dist/{src/strategies → strategies}/loading.d.ts +14 -0
  12. package/dist/{src/strategies → strategies}/loading.js +31 -0
  13. package/dist/strategies/pagination.d.ts +26 -4
  14. package/dist/strategies/pagination.js +52 -23
  15. package/dist/typeContext.d.ts +1 -1
  16. package/dist/typeContext.js +32 -7
  17. package/dist/types.d.ts +37 -5
  18. package/dist/useTable.d.ts +32 -3
  19. package/dist/useTable.js +81 -224
  20. package/dist/utils/stringUtils.js +12 -3
  21. package/package.json +10 -5
  22. package/dist/src/filterEngine.d.ts +0 -11
  23. package/dist/src/filterEngine.js +0 -39
  24. package/dist/src/index.d.ts +0 -2
  25. package/dist/src/index.js +0 -18
  26. package/dist/src/smartRow.d.ts +0 -7
  27. package/dist/src/smartRow.js +0 -160
  28. package/dist/src/strategies/columns.d.ts +0 -18
  29. package/dist/src/strategies/columns.js +0 -21
  30. package/dist/src/strategies/fill.d.ts +0 -7
  31. package/dist/src/strategies/fill.js +0 -88
  32. package/dist/src/strategies/headers.d.ts +0 -13
  33. package/dist/src/strategies/headers.js +0 -30
  34. package/dist/src/strategies/index.d.ts +0 -54
  35. package/dist/src/strategies/index.js +0 -43
  36. package/dist/src/strategies/pagination.d.ts +0 -33
  37. package/dist/src/strategies/pagination.js +0 -79
  38. package/dist/src/strategies/resolution.d.ts +0 -22
  39. package/dist/src/strategies/resolution.js +0 -30
  40. package/dist/src/strategies/sorting.d.ts +0 -12
  41. package/dist/src/strategies/sorting.js +0 -68
  42. package/dist/src/strategies/validation.d.ts +0 -22
  43. package/dist/src/strategies/validation.js +0 -54
  44. package/dist/src/strategies/virtualizedPagination.d.ts +0 -32
  45. package/dist/src/strategies/virtualizedPagination.js +0 -80
  46. package/dist/src/typeContext.d.ts +0 -6
  47. package/dist/src/typeContext.js +0 -465
  48. package/dist/src/types.d.ts +0 -458
  49. package/dist/src/types.js +0 -2
  50. package/dist/src/useTable.d.ts +0 -44
  51. package/dist/src/useTable.js +0 -641
  52. package/dist/src/utils/debugUtils.d.ts +0 -17
  53. package/dist/src/utils/debugUtils.js +0 -62
  54. package/dist/src/utils/smartRowArray.d.ts +0 -14
  55. package/dist/src/utils/smartRowArray.js +0 -22
  56. package/dist/src/utils/stringUtils.d.ts +0 -22
  57. package/dist/src/utils/stringUtils.js +0 -73
  58. package/dist/src/utils.d.ts +0 -7
  59. package/dist/src/utils.js +0 -29
  60. package/dist/utils/traceUtils.d.ts +0 -11
  61. package/dist/utils/traceUtils.js +0 -47
  62. /package/dist/{src/plugins.d.ts → plugins.d.ts} +0 -0
  63. /package/dist/{src/plugins.js → plugins.js} +0 -0
  64. /package/dist/{src/strategies → strategies}/dedupe.d.ts +0 -0
  65. /package/dist/{src/strategies → strategies}/dedupe.js +0 -0
  66. /package/dist/{examples/glide-strategies → strategies/glide}/columns.js +0 -0
  67. /package/dist/{examples/glide-strategies → strategies/glide}/headers.js +0 -0
  68. /package/dist/{src/strategies → strategies}/glide.d.ts +0 -0
  69. /package/dist/{src/strategies → strategies}/rdg.d.ts +0 -0
  70. /package/dist/{src/strategies → strategies}/rdg.js +0 -0
  71. /package/dist/{src/strategies → strategies}/stabilization.d.ts +0 -0
  72. /package/dist/{src/strategies → strategies}/stabilization.js +0 -0
package/dist/useTable.js CHANGED
@@ -9,10 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.Strategies = exports.ResolutionStrategies = exports.CellNavigationStrategies = exports.HeaderStrategies = exports.FillStrategies = exports.SortingStrategies = exports.PaginationStrategies = exports.useTable = void 0;
12
+ exports.Strategies = exports.ResolutionStrategies = exports.CellNavigationStrategies = exports.HeaderStrategies = exports.FillStrategies = exports.DedupeStrategies = exports.SortingStrategies = exports.LoadingStrategies = exports.PaginationStrategies = exports.useTable = void 0;
13
13
  const typeContext_1 = require("./typeContext");
14
14
  const sorting_1 = require("./strategies/sorting");
15
15
  const pagination_1 = require("./strategies/pagination");
16
+ const dedupe_1 = require("./strategies/dedupe");
17
+ const loading_1 = require("./strategies/loading");
16
18
  const fill_1 = require("./strategies/fill");
17
19
  Object.defineProperty(exports, "FillStrategies", { enumerable: true, get: function () { return fill_1.FillStrategies; } });
18
20
  const headers_1 = require("./strategies/headers");
@@ -21,11 +23,12 @@ const columns_1 = require("./strategies/columns");
21
23
  Object.defineProperty(exports, "CellNavigationStrategies", { enumerable: true, get: function () { return columns_1.CellNavigationStrategies; } });
22
24
  const smartRow_1 = require("./smartRow");
23
25
  const filterEngine_1 = require("./filterEngine");
26
+ const tableMapper_1 = require("./engine/tableMapper");
27
+ const rowFinder_1 = require("./engine/rowFinder");
24
28
  const resolution_1 = require("./strategies/resolution");
25
29
  Object.defineProperty(exports, "ResolutionStrategies", { enumerable: true, get: function () { return resolution_1.ResolutionStrategies; } });
26
30
  const strategies_1 = require("./strategies");
27
31
  Object.defineProperty(exports, "Strategies", { enumerable: true, get: function () { return strategies_1.Strategies; } });
28
- const validation_1 = require("./strategies/validation");
29
32
  const debugUtils_1 = require("./utils/debugUtils");
30
33
  const smartRowArray_1 = require("./utils/smartRowArray");
31
34
  /**
@@ -41,8 +44,11 @@ const useTable = (rootLocator, configOptions = {}) => {
41
44
  header: headers_1.HeaderStrategies.visible,
42
45
  cellNavigation: columns_1.CellNavigationStrategies.default,
43
46
  pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
47
+ loading: {
48
+ isHeaderLoading: loading_1.LoadingStrategies.Headers.stable(200)
49
+ }
44
50
  };
45
- const config = Object.assign(Object.assign({ rowSelector: "tbody tr", headerSelector: "thead th", cellSelector: "td", maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, onReset: () => __awaiter(void 0, void 0, void 0, function* () { }) }, configOptions), { strategies: Object.assign(Object.assign({}, defaultStrategies), configOptions.strategies) });
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) });
46
52
  const resolve = (item, parent) => {
47
53
  if (typeof item === 'string')
48
54
  return parent.locator(item);
@@ -51,16 +57,13 @@ const useTable = (rootLocator, configOptions = {}) => {
51
57
  return item;
52
58
  };
53
59
  // Internal State
54
- let _headerMap = null;
55
60
  let _hasPaginated = false;
56
- let _isInitialized = false;
57
61
  // Helpers
58
62
  const log = (msg) => {
59
- (0, debugUtils_1.logDebug)(config, 'verbose', msg); // Legacy(`🔎 [SmartTable Debug] ${msg}`);
63
+ (0, debugUtils_1.logDebug)(config, 'verbose', msg);
60
64
  };
61
65
  const _createColumnError = (colName, map, context) => {
62
66
  const availableColumns = Array.from(map.keys());
63
- // Use Suggestion Logic from ResolutionStrategy (if we had a fuzzy one, for now manual suggest)
64
67
  const lowerCol = colName.toLowerCase();
65
68
  const suggestions = availableColumns.filter(col => col.toLowerCase().includes(lowerCol) ||
66
69
  lowerCol.includes(col.toLowerCase()) ||
@@ -73,130 +76,21 @@ const useTable = (rootLocator, configOptions = {}) => {
73
76
  suggestion = `. Available columns: ${availableColumns.map(c => `"${c}"`).join(', ')}`;
74
77
  }
75
78
  else if (availableColumns.length > 0) {
76
- suggestion = `. Available columns (first 5): ${availableColumns.slice(0, 5).map(c => `"${c}"`).join(', ')}, ...`;
79
+ suggestion = `. Available columns (first 10 of ${availableColumns.length}): ${availableColumns.slice(0, 10).map(c => `"${c}"`).join(', ')}, ...`;
77
80
  }
78
81
  const contextMsg = context ? ` (${context})` : '';
79
82
  return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
80
83
  };
81
- const _getMap = (timeout) => __awaiter(void 0, void 0, void 0, function* () {
82
- if (_headerMap)
83
- return _headerMap;
84
- log('Mapping headers...');
85
- const headerTimeout = timeout !== null && timeout !== void 0 ? timeout : 3000;
86
- if (config.autoScroll) {
87
- try {
88
- yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
89
- }
90
- catch (e) { }
91
- }
92
- const headerLoc = resolve(config.headerSelector, rootLocator);
93
- try {
94
- yield headerLoc.first().waitFor({ state: 'visible', timeout: headerTimeout });
95
- }
96
- catch (e) { /* Ignore hydration */ }
97
- const strategy = config.strategies.header || headers_1.HeaderStrategies.visible;
98
- const context = {
99
- root: rootLocator,
100
- config: config,
101
- page: rootLocator.page(),
102
- resolve: resolve
103
- };
104
- const rawHeaders = yield strategy(context);
105
- const entries = yield Promise.all(rawHeaders.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
106
- let text = t.trim() || `__col_${i}`;
107
- if (config.headerTransformer) {
108
- text = yield config.headerTransformer({
109
- text,
110
- index: i,
111
- locator: rootLocator.locator(config.headerSelector).nth(i)
112
- });
113
- }
114
- return [text, i];
115
- })));
116
- // Validation: Check for empty table
117
- if (entries.length === 0) {
118
- throw new Error(`Initialization Error: No columns found using selector "${config.headerSelector}". Check your selector or ensure the table is visible.`);
119
- }
120
- // Validation: Check for duplicates
121
- const seen = new Set();
122
- const duplicates = new Set();
123
- for (const [name] of entries) {
124
- if (seen.has(name)) {
125
- duplicates.add(name);
126
- }
127
- seen.add(name);
128
- }
129
- if (duplicates.size > 0) {
130
- const dupList = Array.from(duplicates).map(d => `"${d}"`).join(', ');
131
- throw new Error(`Initialization Error: Duplicate column names found: ${dupList}. Use 'headerTransformer' to rename duplicate columns.`);
132
- }
133
- _headerMap = new Map(entries);
134
- log(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
135
- return _headerMap;
136
- });
84
+ // Engines
85
+ const filterEngine = new filterEngine_1.FilterEngine(config, resolve);
86
+ const tableMapper = new tableMapper_1.TableMapper(rootLocator, config, resolve);
137
87
  // Placeholder for the final table object
138
88
  let finalTable = null;
139
- const filterEngine = new filterEngine_1.FilterEngine(config, resolve);
140
89
  // Helper factory
141
90
  const _makeSmart = (rowLocator, map, rowIndex) => {
142
- // Use the wrapped SmartRow logic
143
91
  return (0, smartRow_1.createSmartRow)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable);
144
92
  };
145
- const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
146
- var _a;
147
- const map = yield _getMap();
148
- const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
149
- let currentPage = 1;
150
- log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
151
- while (true) {
152
- const allRows = resolve(config.rowSelector, rootLocator);
153
- // Use FilterEngine
154
- const matchedRows = filterEngine.applyFilters(allRows, filters, map, options.exact || false, rootLocator.page());
155
- const count = yield matchedRows.count();
156
- log(`Page ${currentPage}: Found ${count} matches.`);
157
- if (count > 1) {
158
- // Sample data logic (simplified for refactor, kept inline or moved to util if needed)
159
- const sampleData = [];
160
- try {
161
- const firstFewRows = yield matchedRows.all();
162
- const sampleCount = Math.min(firstFewRows.length, 3);
163
- for (let i = 0; i < sampleCount; i++) {
164
- const rowData = yield _makeSmart(firstFewRows[i], map).toJSON();
165
- sampleData.push(JSON.stringify(rowData));
166
- }
167
- }
168
- catch (e) { }
169
- const sampleMsg = sampleData.length > 0 ? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}` : '';
170
- throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
171
- `Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
172
- }
173
- if (count === 1)
174
- return matchedRows.first();
175
- if (currentPage < effectiveMaxPages) {
176
- log(`Page ${currentPage}: Not found. Attempting pagination...`);
177
- const context = {
178
- root: rootLocator,
179
- config: config,
180
- page: rootLocator.page(),
181
- resolve: resolve
182
- };
183
- const paginationResult = yield config.strategies.pagination(context);
184
- const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
185
- if (didLoadMore) {
186
- _hasPaginated = true;
187
- currentPage++;
188
- continue;
189
- }
190
- else {
191
- log(`Page ${currentPage}: Pagination failed (end of data).`);
192
- }
193
- }
194
- if (_hasPaginated) {
195
- 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.`);
196
- }
197
- return null;
198
- }
199
- });
93
+ const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart);
200
94
  const _getCleanHtml = (loc) => __awaiter(void 0, void 0, void 0, function* () {
201
95
  return loc.evaluate((el) => {
202
96
  const clone = el.cloneNode(true);
@@ -232,28 +126,21 @@ const useTable = (rootLocator, configOptions = {}) => {
232
126
  console.log(finalPrompt);
233
127
  });
234
128
  const _ensureInitialized = () => __awaiter(void 0, void 0, void 0, function* () {
235
- if (!_isInitialized) {
236
- yield _getMap();
237
- _isInitialized = true;
238
- }
129
+ yield tableMapper.getMap();
239
130
  });
240
131
  const result = {
241
132
  init: (options) => __awaiter(void 0, void 0, void 0, function* () {
242
- if (_isInitialized && _headerMap)
133
+ if (tableMapper.isInitialized())
243
134
  return result;
244
135
  (0, debugUtils_1.warnIfDebugInCI)(config);
245
136
  (0, debugUtils_1.logDebug)(config, 'info', 'Initializing table');
246
- yield _getMap(options === null || options === void 0 ? void 0 : options.timeout);
247
- _isInitialized = true;
248
- if (_headerMap) {
249
- (0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${_headerMap.size} columns`, Array.from(_headerMap.keys()));
250
- // Trace event removed - redundant with debug logging
251
- }
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()));
252
139
  yield (0, debugUtils_1.debugDelay)(config, 'default');
253
140
  return result;
254
141
  }),
255
142
  scrollToColumn: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
256
- const map = yield _getMap();
143
+ const map = yield tableMapper.getMap();
257
144
  const idx = map.get(columnName);
258
145
  if (idx === undefined)
259
146
  throw _createColumnError(columnName, map);
@@ -267,16 +154,14 @@ const useTable = (rootLocator, configOptions = {}) => {
267
154
  });
268
155
  }),
269
156
  getHeaders: () => __awaiter(void 0, void 0, void 0, function* () {
270
- if (!_isInitialized || !_headerMap)
271
- throw new Error('Table not initialized. Call await table.init() first, or use async methods like table.findRow() or table.getRows() which auto-initialize.');
272
- return Array.from(_headerMap.keys());
157
+ const map = yield tableMapper.getMap();
158
+ return Array.from(map.keys());
273
159
  }),
274
160
  getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
275
- if (!_isInitialized || !_headerMap)
276
- throw new Error('Table not initialized. Call await table.init() first.');
277
- const idx = _headerMap.get(columnName);
161
+ const map = yield tableMapper.getMap();
162
+ const idx = map.get(columnName);
278
163
  if (idx === undefined)
279
- throw _createColumnError(columnName, _headerMap, 'header cell');
164
+ throw _createColumnError(columnName, map, 'header cell');
280
165
  return resolve(config.headerSelector, rootLocator).nth(idx);
281
166
  }),
282
167
  reset: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -284,22 +169,20 @@ const useTable = (rootLocator, configOptions = {}) => {
284
169
  const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
285
170
  yield config.onReset(context);
286
171
  _hasPaginated = false;
287
- _headerMap = null;
288
- _isInitialized = false;
172
+ tableMapper.clear();
289
173
  log("Table reset complete.");
290
174
  }),
291
175
  revalidate: () => __awaiter(void 0, void 0, void 0, function* () {
292
176
  log("Revalidating table structure...");
293
- _headerMap = null; // Clear the map to force re-scanning
294
- yield _getMap(); // Re-scan headers
177
+ yield tableMapper.remapHeaders();
295
178
  log("Table revalidated.");
296
179
  }),
297
180
  getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
298
181
  var _a, _b;
299
- yield _ensureInitialized();
300
- const colIdx = _headerMap.get(column);
182
+ const map = yield tableMapper.getMap();
183
+ const colIdx = map.get(column);
301
184
  if (colIdx === undefined)
302
- throw _createColumnError(column, _headerMap);
185
+ throw _createColumnError(column, map);
303
186
  const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
304
187
  const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
305
188
  let currentPage = 1;
@@ -326,82 +209,33 @@ const useTable = (rootLocator, configOptions = {}) => {
326
209
  return results;
327
210
  }),
328
211
  getRow: (filters, options = { exact: false }) => {
329
- if (!_isInitialized || !_headerMap)
212
+ const map = tableMapper.getMapSync();
213
+ if (!map)
330
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.');
331
215
  const allRows = resolve(config.rowSelector, rootLocator);
332
- 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());
333
217
  const rowLocator = matchedRows.first();
334
- return _makeSmart(rowLocator, _headerMap, 0); // fallback index 0
218
+ return _makeSmart(rowLocator, map, 0); // fallback index 0
335
219
  },
336
220
  getRowByIndex: (index, options = {}) => {
337
- if (!_isInitialized || !_headerMap)
221
+ const map = tableMapper.getMapSync();
222
+ if (!map)
338
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.');
339
- const rowIndex = index - 1; // Convert 1-based to 0-based
340
- const rowLocator = resolve(config.rowSelector, rootLocator).nth(rowIndex);
341
- return _makeSmart(rowLocator, _headerMap, rowIndex);
224
+ const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
225
+ return _makeSmart(rowLocator, map, index);
342
226
  },
343
227
  findRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
344
- (0, debugUtils_1.logDebug)(config, 'info', 'Searching for row', filters);
345
- yield _ensureInitialized();
346
- let row = yield _findRowLocator(filters, options);
347
- if (row) {
348
- (0, debugUtils_1.logDebug)(config, 'info', 'Row found');
349
- yield (0, debugUtils_1.debugDelay)(config, 'findRow');
350
- return _makeSmart(row, _headerMap, 0);
351
- }
352
- (0, debugUtils_1.logDebug)(config, 'error', 'Row not found', filters);
353
- yield (0, debugUtils_1.debugDelay)(config, 'findRow');
354
- // Return sentinel row
355
- row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
356
- return _makeSmart(row, _headerMap, 0);
228
+ return rowFinder.findRow(filters, options);
357
229
  }),
358
230
  getRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
359
- yield _ensureInitialized();
360
- let rowLocators = resolve(config.rowSelector, rootLocator);
361
- if (options === null || options === void 0 ? void 0 : options.filter) {
362
- rowLocators = filterEngine.applyFilters(rowLocators, options.filter, _headerMap, options.exact || false, rootLocator.page());
363
- }
364
- const rows = yield rowLocators.all();
365
- const smartRows = rows.map((loc, i) => _makeSmart(loc, _headerMap, i));
366
- 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 }));
367
233
  }),
368
234
  findRows: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
369
- var _a, _b, _c, _d;
370
- yield _ensureInitialized();
371
- const allRows = [];
372
- 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;
373
- let pageCount = 0;
374
- // Collect rows from current page
375
- let rowLocators = resolve(config.rowSelector, rootLocator);
376
- rowLocators = filterEngine.applyFilters(rowLocators, filters, _headerMap, (_c = options === null || options === void 0 ? void 0 : options.exact) !== null && _c !== void 0 ? _c : false, rootLocator.page());
377
- let rows = yield rowLocators.all();
378
- allRows.push(...rows.map((loc, i) => _makeSmart(loc, _headerMap, i)));
379
- // Paginate and collect more rows
380
- while (pageCount < effectiveMaxPages && config.strategies.pagination) {
381
- const paginationResult = yield config.strategies.pagination({
382
- root: rootLocator,
383
- config,
384
- resolve,
385
- page: rootLocator.page()
386
- });
387
- const didPaginate = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
388
- if (!didPaginate)
389
- break;
390
- pageCount++;
391
- _hasPaginated = true;
392
- // Collect rows from new page
393
- rowLocators = resolve(config.rowSelector, rootLocator);
394
- rowLocators = filterEngine.applyFilters(rowLocators, filters, _headerMap, (_d = options === null || options === void 0 ? void 0 : options.exact) !== null && _d !== void 0 ? _d : false, rootLocator.page());
395
- rows = yield rowLocators.all();
396
- allRows.push(...rows.map((loc, i) => _makeSmart(loc, _headerMap, i)));
397
- }
398
- if (options === null || options === void 0 ? void 0 : options.asJSON) {
399
- return Promise.all(allRows.map(r => r.toJSON()));
400
- }
401
- return allRows;
235
+ return rowFinder.findRows(filters, options);
402
236
  }),
403
237
  isInitialized: () => {
404
- return _isInitialized;
238
+ return tableMapper.isInitialized();
405
239
  },
406
240
  sorting: {
407
241
  apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
@@ -421,7 +255,7 @@ const useTable = (rootLocator, configOptions = {}) => {
421
255
  })
422
256
  },
423
257
  iterateThroughTable: (callback, options) => __awaiter(void 0, void 0, void 0, function* () {
424
- var _a, _b, _c, _d;
258
+ var _a, _b, _c, _d, _e, _f, _g;
425
259
  yield _ensureInitialized();
426
260
  const paginationStrategy = (_a = options === null || options === void 0 ? void 0 : options.pagination) !== null && _a !== void 0 ? _a : config.strategies.pagination;
427
261
  const hasPaginationInOptions = (options === null || options === void 0 ? void 0 : options.pagination) !== undefined;
@@ -429,6 +263,7 @@ const useTable = (rootLocator, configOptions = {}) => {
429
263
  throw new Error('No pagination strategy provided.');
430
264
  yield result.reset();
431
265
  yield result.init();
266
+ const map = tableMapper.getMapSync();
432
267
  const restrictedTable = {
433
268
  init: result.init,
434
269
  getHeaders: result.getHeaders,
@@ -451,6 +286,7 @@ const useTable = (rootLocator, configOptions = {}) => {
451
286
  const effectiveMaxIterations = (_d = options === null || options === void 0 ? void 0 : options.maxIterations) !== null && _d !== void 0 ? _d : config.maxPages;
452
287
  const batchSize = options === null || options === void 0 ? void 0 : options.batchSize;
453
288
  const isBatching = batchSize !== undefined && batchSize > 1;
289
+ const autoFlatten = (_e = options === null || options === void 0 ? void 0 : options.autoFlatten) !== null && _e !== void 0 ? _e : false;
454
290
  let index = 0;
455
291
  let paginationResult = true;
456
292
  let seenKeys = null;
@@ -459,19 +295,28 @@ const useTable = (rootLocator, configOptions = {}) => {
459
295
  log(`Starting iterateThroughTable (maxIterations: ${effectiveMaxIterations}, batchSize: ${batchSize !== null && batchSize !== void 0 ? batchSize : 'none'})`);
460
296
  while (index < effectiveMaxIterations) {
461
297
  const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
462
- let rows = rowLocators.map((loc, i) => _makeSmart(loc, _headerMap, i));
463
- if ((options === null || options === void 0 ? void 0 : options.dedupeStrategy) && rows.length > 0) {
298
+ const smartRowsArray = [];
299
+ const isRowLoading = (_f = config.strategies.loading) === null || _f === void 0 ? void 0 : _f.isRowLoading;
300
+ for (let i = 0; i < rowLocators.length; i++) {
301
+ const smartRow = _makeSmart(rowLocators[i], map, i);
302
+ if (isRowLoading && (yield isRowLoading(smartRow)))
303
+ continue;
304
+ smartRowsArray.push(smartRow);
305
+ }
306
+ let rows = (0, smartRowArray_1.createSmartRowArray)(smartRowsArray);
307
+ const dedupeStrategy = (_g = options === null || options === void 0 ? void 0 : options.dedupeStrategy) !== null && _g !== void 0 ? _g : config.strategies.dedupe;
308
+ if (dedupeStrategy && rows.length > 0) {
464
309
  if (!seenKeys)
465
310
  seenKeys = new Set();
466
311
  const deduplicated = [];
467
312
  for (const row of rows) {
468
- const key = yield options.dedupeStrategy(row);
313
+ const key = yield dedupeStrategy(row);
469
314
  if (!seenKeys.has(key)) {
470
315
  seenKeys.add(key);
471
316
  deduplicated.push(row);
472
317
  }
473
318
  }
474
- rows = deduplicated;
319
+ rows = (0, smartRowArray_1.createSmartRowArray)(deduplicated);
475
320
  log(`Deduplicated ${rowLocators.length} rows to ${rows.length} unique rows (total seen: ${seenKeys.size})`);
476
321
  }
477
322
  // Add rows to batch if batching is enabled
@@ -489,7 +334,7 @@ const useTable = (rootLocator, configOptions = {}) => {
489
334
  let isLast = getIsLast({ index: callbackIndex, paginationResult });
490
335
  const isLastDueToMax = index === effectiveMaxIterations - 1;
491
336
  if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
492
- yield options.beforeFirst({ index: callbackIndex, rows: callbackRows, allData });
337
+ yield options.beforeFirst({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(callbackRows), allData });
493
338
  }
494
339
  const batchInfo = isBatching ? {
495
340
  startIndex: batchStartIndex,
@@ -500,12 +345,17 @@ const useTable = (rootLocator, configOptions = {}) => {
500
345
  index: callbackIndex,
501
346
  isFirst,
502
347
  isLast,
503
- rows: callbackRows,
348
+ rows: (0, smartRowArray_1.createSmartRowArray)(callbackRows),
504
349
  allData,
505
350
  table: restrictedTable,
506
351
  batchInfo
507
352
  });
508
- allData.push(returnValue);
353
+ if (autoFlatten && Array.isArray(returnValue)) {
354
+ allData.push(...returnValue);
355
+ }
356
+ else {
357
+ allData.push(returnValue);
358
+ }
509
359
  // Determine if this is truly the last iteration
510
360
  let finalIsLast = isLastDueToMax;
511
361
  if (!isLastIteration) {
@@ -516,7 +366,7 @@ const useTable = (rootLocator, configOptions = {}) => {
516
366
  finalIsLast = getIsLast({ index: callbackIndex, paginationResult }) || !paginationResult;
517
367
  }
518
368
  if (finalIsLast && (options === null || options === void 0 ? void 0 : options.afterLast)) {
519
- yield options.afterLast({ index: callbackIndex, rows: callbackRows, allData });
369
+ yield options.afterLast({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(callbackRows), allData });
520
370
  }
521
371
  if (finalIsLast || !paginationResult) {
522
372
  log(`Reached last iteration (index: ${index}, paginationResult: ${paginationResult})`);
@@ -540,7 +390,7 @@ const useTable = (rootLocator, configOptions = {}) => {
540
390
  const isFirst = getIsFirst({ index: callbackIndex });
541
391
  const isLast = true;
542
392
  if (isFirst && (options === null || options === void 0 ? void 0 : options.beforeFirst)) {
543
- yield options.beforeFirst({ index: callbackIndex, rows: batchRows, allData });
393
+ yield options.beforeFirst({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(batchRows), allData });
544
394
  }
545
395
  const batchInfo = {
546
396
  startIndex: batchStartIndex,
@@ -551,14 +401,19 @@ const useTable = (rootLocator, configOptions = {}) => {
551
401
  index: callbackIndex,
552
402
  isFirst,
553
403
  isLast,
554
- rows: batchRows,
404
+ rows: (0, smartRowArray_1.createSmartRowArray)(batchRows),
555
405
  allData,
556
406
  table: restrictedTable,
557
407
  batchInfo
558
408
  });
559
- allData.push(returnValue);
409
+ if (autoFlatten && Array.isArray(returnValue)) {
410
+ allData.push(...returnValue);
411
+ }
412
+ else {
413
+ allData.push(returnValue);
414
+ }
560
415
  if (options === null || options === void 0 ? void 0 : options.afterLast) {
561
- yield options.afterLast({ index: callbackIndex, rows: batchRows, allData });
416
+ yield options.afterLast({ index: callbackIndex, rows: (0, smartRowArray_1.createSmartRowArray)(batchRows), allData });
562
417
  }
563
418
  log(`Pagination failed mid-batch (index: ${index})`);
564
419
  break;
@@ -573,7 +428,7 @@ const useTable = (rootLocator, configOptions = {}) => {
573
428
  generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
574
429
  const html = yield _getCleanHtml(rootLocator);
575
430
  const separator = "=".repeat(50);
576
- const content = `\n${separator}\n🤖 COPY INTO GEMINI/ChatGPT 🤖\n${separator}\nI am using 'playwright-smart-table'.\nTarget Table Locator: ${rootLocator.toString()}\nGenerate config for:\n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n${separator}\n`;
431
+ const content = `\n${separator} \n🤖 COPY INTO GEMINI / ChatGPT 🤖\n${separator} \nI am using 'playwright-smart-table'.\nTarget Table Locator: ${rootLocator.toString()} \nGenerate config for: \n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n${separator}\n`;
577
432
  yield _handlePrompt('Smart Table Config', content, options);
578
433
  }),
579
434
  };
@@ -581,5 +436,7 @@ const useTable = (rootLocator, configOptions = {}) => {
581
436
  return result;
582
437
  };
583
438
  exports.useTable = useTable;
584
- exports.PaginationStrategies = pagination_1.PaginationStrategies;
439
+ exports.PaginationStrategies = Object.assign({}, pagination_1.PaginationStrategies);
440
+ exports.LoadingStrategies = loading_1.LoadingStrategies;
585
441
  exports.SortingStrategies = sorting_1.SortingStrategies;
442
+ exports.DedupeStrategies = dedupe_1.DedupeStrategies;
@@ -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.0",
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
  }
@@ -1,11 +0,0 @@
1
- import { Locator, Page } from "@playwright/test";
2
- import { FinalTableConfig } from "./types";
3
- export declare class FilterEngine {
4
- private config;
5
- private resolve;
6
- constructor(config: FinalTableConfig, resolve: (selector: any, parent: Locator | Page) => Locator);
7
- /**
8
- * Applies filters to a set of rows.
9
- */
10
- applyFilters(baseRows: Locator, filters: Record<string, string | RegExp | number>, map: Map<string, number>, exact: boolean, page: Page): Locator;
11
- }
@@ -1,39 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FilterEngine = void 0;
4
- const stringUtils_1 = require("./utils/stringUtils");
5
- class FilterEngine {
6
- constructor(config, resolve) {
7
- this.config = config;
8
- this.resolve = resolve;
9
- }
10
- /**
11
- * Applies filters to a set of rows.
12
- */
13
- applyFilters(baseRows, filters, map, exact, page) {
14
- let filtered = baseRows;
15
- // Iterate through each filter criteria
16
- for (const [colName, value] of Object.entries(filters)) {
17
- // Find column index
18
- const colIndex = map.get(colName);
19
- // TODO: Use ColumnStrategy for better resolution error handling
20
- if (colIndex === undefined) {
21
- throw new Error((0, stringUtils_1.buildColumnNotFoundError)(colName, Array.from(map.keys())));
22
- }
23
- const filterVal = typeof value === 'number' ? String(value) : value;
24
- // Use strategy if provided (For future: configured filter strategies)
25
- // But for now, we implement the default logic or use custom if we add it to config later
26
- // Default Filter Logic
27
- const cellTemplate = this.resolve(this.config.cellSelector, page);
28
- // This logic assumes 1:1 row-to-cell mapping based on index.
29
- // filter({ has: ... }) checks if the row *contains* the matching cell.
30
- // But we need to be specific about WHICH cell.
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
- });
35
- }
36
- return filtered;
37
- }
38
- }
39
- exports.FilterEngine = FilterEngine;
@@ -1,2 +0,0 @@
1
- export * from './useTable';
2
- export * from './types';