@rickcedwhat/playwright-smart-table 3.1.0 → 3.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/dist/useTable.js CHANGED
@@ -9,26 +9,23 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.useTable = exports.SortingStrategies = exports.TableStrategies = exports.PaginationStrategies = void 0;
12
+ exports.ColumnStrategies = exports.HeaderStrategies = exports.FillStrategies = exports.SortingStrategies = exports.TableStrategies = 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 fill_1 = require("./strategies/fill");
17
+ Object.defineProperty(exports, "FillStrategies", { enumerable: true, get: function () { return fill_1.FillStrategies; } });
18
+ const headers_1 = require("./strategies/headers");
19
+ Object.defineProperty(exports, "HeaderStrategies", { enumerable: true, get: function () { return headers_1.HeaderStrategies; } });
20
+ const columns_1 = require("./strategies/columns");
21
+ Object.defineProperty(exports, "ColumnStrategies", { enumerable: true, get: function () { return columns_1.ColumnStrategies; } });
16
22
  /**
17
- * A collection of pre-built pagination strategies.
23
+ * Main hook to interact with a table.
18
24
  */
19
- exports.PaginationStrategies = pagination_1.PaginationStrategies;
20
- /**
21
- * @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
22
- */
23
- exports.TableStrategies = pagination_1.TableStrategies;
24
- /**
25
- * A collection of pre-built sorting strategies.
26
- */
27
- exports.SortingStrategies = sorting_1.SortingStrategies;
28
25
  const useTable = (rootLocator, configOptions = {}) => {
29
26
  // Store whether pagination was explicitly provided in config
30
27
  const hasPaginationInConfig = configOptions.pagination !== undefined;
31
- const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, onReset: () => __awaiter(void 0, void 0, void 0, function* () { console.warn("⚠️ .reset() called but no 'onReset' strategy defined in config."); }) }, configOptions);
28
+ const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, fillStrategy: fill_1.FillStrategies.default, headerStrategy: headers_1.HeaderStrategies.visible, columnStrategy: columns_1.ColumnStrategies.default, onReset: () => __awaiter(void 0, void 0, void 0, function* () { console.warn("⚠️ .reset() called but no 'onReset' strategy defined in config."); }) }, configOptions);
32
29
  const resolve = (item, parent) => {
33
30
  if (typeof item === 'string')
34
31
  return parent.locator(item);
@@ -84,17 +81,33 @@ const useTable = (rootLocator, configOptions = {}) => {
84
81
  yield headerLoc.first().waitFor({ state: 'visible', timeout: headerTimeout });
85
82
  }
86
83
  catch (e) { /* Ignore hydration */ }
87
- // 1. Fetch data efficiently
88
- const texts = yield headerLoc.allInnerTexts();
89
- const locators = yield headerLoc.all();
84
+ // 1. Fetch headers using strategy
85
+ const strategy = config.headerStrategy || headers_1.HeaderStrategies.visible;
86
+ // We need to construct context - but wait, HeaderStrategies definition I made imports StrategyContext
87
+ // We need to import HeaderStrategies in useTable.ts first (it was imported as HeaderStrategies?).
88
+ // Wait, useTable.ts imports `FillStrategies`.
89
+ // I need to import `HeaderStrategies`.
90
+ const context = {
91
+ root: rootLocator,
92
+ config: config,
93
+ page: rootLocator.page(),
94
+ resolve: resolve
95
+ };
96
+ const rawHeaders = yield strategy(context);
90
97
  // 2. Map Headers (Async)
91
- const entries = yield Promise.all(texts.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
98
+ // Note: We lose locator access here for transformer unless strategy provides it.
99
+ // For now assuming transformer handles missing locator or we don't pass it if generic strategy.
100
+ const entries = yield Promise.all(rawHeaders.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
92
101
  let text = t.trim() || `__col_${i}`;
93
102
  if (config.headerTransformer) {
94
103
  text = yield config.headerTransformer({
95
104
  text,
96
105
  index: i,
97
- locator: locators[i]
106
+ locator: rootLocator.locator(config.headerSelector).nth(i) // Best effort lazy locator?
107
+ // Danger: scanning strategy implies nth(i) might not map to visual i if scrolled.
108
+ // But map index maps to logical index.
109
+ // If scanning returns 100 headers, nth(99) might not exist in DOM.
110
+ // Passing fallback locator or stub.
98
111
  });
99
112
  }
100
113
  return [text, i];
@@ -103,8 +116,12 @@ const useTable = (rootLocator, configOptions = {}) => {
103
116
  logDebug(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
104
117
  return _headerMap;
105
118
  });
106
- const _makeSmart = (rowLocator, map) => {
119
+ // Placeholder for the final table object, to be captured by closure
120
+ let finalTable = null;
121
+ const _makeSmart = (rowLocator, map, rowIndex) => {
107
122
  const smart = rowLocator;
123
+ smart.getRequestIndex = () => rowIndex;
124
+ smart.rowIndex = rowIndex;
108
125
  smart.getCell = (colName) => {
109
126
  const idx = map.get(colName);
110
127
  if (idx === undefined) {
@@ -112,12 +129,10 @@ const useTable = (rootLocator, configOptions = {}) => {
112
129
  const suggestion = _suggestColumnName(colName, availableColumns);
113
130
  throw new Error(`Column "${colName}" not found${suggestion}`);
114
131
  }
115
- if (typeof config.cellSelector === 'string') {
116
- return rowLocator.locator(config.cellSelector).nth(idx);
117
- }
118
- else {
119
- return resolve(config.cellSelector, rowLocator).nth(idx);
132
+ if (config.cellResolver) {
133
+ return config.cellResolver({ row: rowLocator, columnName: colName, columnIndex: idx, rowIndex });
120
134
  }
135
+ return resolve(config.cellSelector, rowLocator).nth(idx);
121
136
  };
122
137
  smart.toJSON = () => __awaiter(void 0, void 0, void 0, function* () {
123
138
  const result = {};
@@ -130,8 +145,8 @@ const useTable = (rootLocator, configOptions = {}) => {
130
145
  }
131
146
  return result;
132
147
  });
133
- smart.smartFill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
134
- var _a;
148
+ // @ts-ignore - Intentionally overriding Locator's fill method to accept object
149
+ smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
135
150
  logDebug(`Filling row with data: ${JSON.stringify(data)}`);
136
151
  // Fill each column
137
152
  for (const [colName, value] of Object.entries(data)) {
@@ -139,83 +154,34 @@ const useTable = (rootLocator, configOptions = {}) => {
139
154
  if (colIdx === undefined) {
140
155
  throw _createColumnError(colName, map, 'in fill data');
141
156
  }
142
- const cell = smart.getCell(colName);
143
- // Use custom input mapper for this column if provided, otherwise auto-detect
144
- let inputLocator;
145
- if ((_a = fillOptions === null || fillOptions === void 0 ? void 0 : fillOptions.inputMappers) === null || _a === void 0 ? void 0 : _a[colName]) {
146
- inputLocator = fillOptions.inputMappers[colName](cell);
147
- }
148
- else {
149
- // Auto-detect input type
150
- // Try different input types in order of commonality
151
- // Check for text input
152
- const textInput = cell.locator('input[type="text"], input:not([type]), textarea').first();
153
- const textInputCount = yield textInput.count().catch(() => 0);
154
- // Check for select
155
- const select = cell.locator('select').first();
156
- const selectCount = yield select.count().catch(() => 0);
157
- // Check for checkbox/radio
158
- const checkbox = cell.locator('input[type="checkbox"], input[type="radio"], [role="checkbox"]').first();
159
- const checkboxCount = yield checkbox.count().catch(() => 0);
160
- // Check for contenteditable or div-based inputs
161
- const contentEditable = cell.locator('[contenteditable="true"]').first();
162
- const contentEditableCount = yield contentEditable.count().catch(() => 0);
163
- // Determine which input to use (prioritize by commonality)
164
- if (textInputCount > 0 && selectCount === 0 && checkboxCount === 0) {
165
- inputLocator = textInput;
166
- }
167
- else if (selectCount > 0) {
168
- inputLocator = select;
169
- }
170
- else if (checkboxCount > 0) {
171
- inputLocator = checkbox;
172
- }
173
- else if (contentEditableCount > 0) {
174
- inputLocator = contentEditable;
175
- }
176
- else if (textInputCount > 0) {
177
- // Fallback to text input even if others exist
178
- inputLocator = textInput;
179
- }
180
- else {
181
- // No input found - try to click the cell itself (might trigger an editor)
182
- inputLocator = cell;
183
- }
184
- // Warn if multiple inputs found (ambiguous)
185
- const totalInputs = textInputCount + selectCount + checkboxCount + contentEditableCount;
186
- if (totalInputs > 1 && config.debug) {
187
- logDebug(`⚠️ Multiple inputs found in cell "${colName}" (${totalInputs} total). Using first match. Consider using inputMapper option for explicit control.`);
188
- }
189
- }
190
- // Fill based on value type and input type
191
- const inputTag = yield inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
192
- const inputType = yield inputLocator.getAttribute('type').catch(() => null);
193
- const isContentEditable = yield inputLocator.getAttribute('contenteditable').catch(() => null);
194
- logDebug(`Filling "${colName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
195
- if (inputType === 'checkbox' || inputType === 'radio') {
196
- // Boolean value for checkbox/radio
197
- const shouldBeChecked = Boolean(value);
198
- const isChecked = yield inputLocator.isChecked().catch(() => false);
199
- if (isChecked !== shouldBeChecked) {
200
- yield inputLocator.click();
201
- }
202
- }
203
- else if (inputTag === 'select') {
204
- // Select dropdown
205
- yield inputLocator.selectOption(String(value));
206
- }
207
- else if (isContentEditable === 'true') {
208
- // Contenteditable div
209
- yield inputLocator.click();
210
- yield inputLocator.fill(String(value));
211
- }
212
- else {
213
- // Text input, textarea, or generic
214
- yield inputLocator.fill(String(value));
215
- }
157
+ // Execute Column Strategy BEFORE filling
158
+ yield config.columnStrategy({
159
+ config: config,
160
+ root: rootLocator,
161
+ page: rootLocator.page(),
162
+ resolve,
163
+ column: colName,
164
+ index: colIdx,
165
+ rowLocator: rowLocator,
166
+ rowIndex: rowIndex
167
+ });
168
+ // Use configured strategy or default to internal DOM logic
169
+ const strategy = config.fillStrategy || fill_1.FillStrategies.default;
170
+ yield strategy({
171
+ row: smart,
172
+ columnName: colName,
173
+ value,
174
+ index: -1,
175
+ page: rootLocator.page(),
176
+ rootLocator,
177
+ table: finalTable,
178
+ fillOptions
179
+ });
216
180
  }
217
181
  logDebug('Fill operation completed');
218
182
  });
183
+ // Alias for explicit usage avoiding Locator.fill conflict
184
+ smart.smartFill = smart.fill;
219
185
  return smart;
220
186
  };
221
187
  const _applyFilters = (baseRows, filters, map, exact) => {
@@ -347,6 +313,21 @@ const useTable = (rootLocator, configOptions = {}) => {
347
313
  _isInitialized = true;
348
314
  return result;
349
315
  }),
316
+ scrollToColumn: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
317
+ const map = yield _getMap();
318
+ const idx = map.get(columnName);
319
+ if (idx === undefined) {
320
+ throw _createColumnError(columnName, map);
321
+ }
322
+ yield config.columnStrategy({
323
+ config: config,
324
+ root: rootLocator,
325
+ page: rootLocator.page(),
326
+ resolve,
327
+ column: columnName,
328
+ index: idx
329
+ });
330
+ }),
350
331
  getHeaders: () => __awaiter(void 0, void 0, void 0, function* () {
351
332
  if (!_isInitialized || !_headerMap) {
352
333
  throw new Error('Table not initialized. Call await table.init() first.');
@@ -410,17 +391,27 @@ const useTable = (rootLocator, configOptions = {}) => {
410
391
  }
411
392
  return results;
412
393
  }),
413
- getByRow: (filters, options) => {
414
- // Throw error if not initialized (sync methods require explicit init)
394
+ // @ts-ignore - implementing overload
395
+ getByRow: (filtersOrIndex, options = { exact: false }) => {
396
+ // Throw error if not initialized
415
397
  if (!_isInitialized || !_headerMap) {
416
398
  throw new Error('Table not initialized. Call await table.init() first.');
417
399
  }
418
- // Build locator chain (sync) - current page only
400
+ // Handle Index Overload (1-based index)
401
+ if (typeof filtersOrIndex === 'number') {
402
+ const rowIndex = filtersOrIndex - 1;
403
+ const rowLocator = resolve(config.rowSelector, rootLocator).nth(rowIndex);
404
+ return _makeSmart(rowLocator, _headerMap, rowIndex);
405
+ }
406
+ // Handle Filter Logic (Sync - lazy)
407
+ const filters = filtersOrIndex;
419
408
  const allRows = resolve(config.rowSelector, rootLocator);
420
- const matchedRows = _applyFilters(allRows, filters, _headerMap, (options === null || options === void 0 ? void 0 : options.exact) || false);
421
- // Return first match (or sentinel) - lazy, doesn't check existence
409
+ const matchedRows = _applyFilters(allRows, filters, _headerMap, options.exact || false);
422
410
  const rowLocator = matchedRows.first();
423
- return _makeSmart(rowLocator, _headerMap);
411
+ // We pass rowIndex=0 as a fallback.
412
+ // NOTE: Filter-based sync lookup doesn't know the absolute index easily without scanning.
413
+ // If accurate rowIndex is needed for filtered rows, use searchForRow or getAllCurrentRows.
414
+ return _makeSmart(rowLocator, _headerMap, 0);
424
415
  },
425
416
  searchForRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
426
417
  // Auto-init if needed (async methods can auto-init)
@@ -430,9 +421,9 @@ const useTable = (rootLocator, configOptions = {}) => {
430
421
  if (!row) {
431
422
  row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
432
423
  }
433
- return _makeSmart(row, _headerMap);
424
+ return _makeSmart(row, _headerMap, 0);
434
425
  }),
435
- getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
426
+ getAllCurrentRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
436
427
  // Auto-init if needed (async methods can auto-init)
437
428
  yield _ensureInitialized();
438
429
  let rowLocators = resolve(config.rowSelector, rootLocator);
@@ -440,16 +431,23 @@ const useTable = (rootLocator, configOptions = {}) => {
440
431
  rowLocators = _applyFilters(rowLocators, options.filter, _headerMap, options.exact || false);
441
432
  }
442
433
  const rows = yield rowLocators.all();
443
- const smartRows = rows.map(loc => _makeSmart(loc, _headerMap));
434
+ const smartRows = rows.map((loc, i) => _makeSmart(loc, _headerMap, i));
444
435
  if (options === null || options === void 0 ? void 0 : options.asJSON) {
445
436
  return Promise.all(smartRows.map(r => r.toJSON()));
446
437
  }
447
438
  return smartRows;
448
439
  }),
440
+ /**
441
+ * @deprecated Use getAllCurrentRows instead. This method will be removed in a future major version.
442
+ */
443
+ getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
444
+ console.warn("⚠️ [SmartTable] getAllRows is deprecated. Use getAllCurrentRows instead.");
445
+ return result.getAllCurrentRows(options);
446
+ }),
449
447
  generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
450
448
  const html = yield _getCleanHtml(rootLocator);
451
449
  const separator = "=".repeat(50);
452
- const content = `\n${separator}\n🤖 COPY INTO GEMINI/ChatGPT 🤖\n${separator}\nI am using 'playwright-smart-table'. Generate config for:\n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n${separator}\n`;
450
+ 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`;
453
451
  yield _handlePrompt('Smart Table Config', content, options);
454
452
  }),
455
453
  generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
@@ -510,11 +508,12 @@ const useTable = (rootLocator, configOptions = {}) => {
510
508
  getHeaders: result.getHeaders,
511
509
  getHeaderCell: result.getHeaderCell,
512
510
  getByRow: result.getByRow,
513
- getAllRows: result.getAllRows,
511
+ getAllCurrentRows: result.getAllCurrentRows,
514
512
  getColumnValues: result.getColumnValues,
515
513
  generateConfigPrompt: result.generateConfigPrompt,
516
514
  generateStrategyPrompt: result.generateStrategyPrompt,
517
515
  sorting: result.sorting,
516
+ scrollToColumn: result.scrollToColumn, // Add to restricted result as well
518
517
  };
519
518
  // Default functions
520
519
  const getIsFirst = (_b = options === null || options === void 0 ? void 0 : options.getIsFirst) !== null && _b !== void 0 ? _b : (({ index }) => index === 0);
@@ -529,7 +528,7 @@ const useTable = (rootLocator, configOptions = {}) => {
529
528
  while (index < effectiveMaxIterations) {
530
529
  // Get current rows
531
530
  const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
532
- let rows = rowLocators.map(loc => _makeSmart(loc, _headerMap));
531
+ let rows = rowLocators.map((loc, i) => _makeSmart(loc, _headerMap, i));
533
532
  // Deduplicate if dedupeStrategy provided (across all iterations)
534
533
  if ((options === null || options === void 0 ? void 0 : options.dedupeStrategy) && rows.length > 0) {
535
534
  if (!seenKeys) {
@@ -592,6 +591,10 @@ const useTable = (rootLocator, configOptions = {}) => {
592
591
  return allData;
593
592
  }),
594
593
  };
594
+ finalTable = result;
595
595
  return result;
596
596
  };
597
597
  exports.useTable = useTable;
598
+ exports.PaginationStrategies = pagination_1.PaginationStrategies;
599
+ exports.TableStrategies = pagination_1.TableStrategies;
600
+ exports.SortingStrategies = sorting_1.SortingStrategies;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "A smart table utility for Playwright with built-in pagination strategies that are fully extensible.",
5
5
  "repository": {
6
6
  "type": "git",