@rickcedwhat/playwright-smart-table 1.0.5 → 1.0.7

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
@@ -1,118 +1,152 @@
1
1
  Playwright Smart Table 🧠
2
+
2
3
  A production-ready, type-safe table wrapper for Playwright.
3
- It handles the hard stuff automatically:
4
- Pagination (Next buttons, Load More, Infinite Scroll)
5
- Complex Grids (MUI, AG-Grid, React Table)
6
- Strict Mode (Throws errors if your filters match multiple rows)
7
- 🚀 Installation
8
- npm install playwright-smart-table
9
4
 
5
+ This library abstracts away the complexity of testing dynamic web tables. It handles Pagination, Infinite Scroll, Virtualization, and Data Grids (MUI, AG-Grid) so your tests remain clean and readable.
10
6
 
11
- (Note: Requires @playwright/test as a peer dependency)
12
- 🏁 Quick Start
13
- Standard HTML Table (No config needed)
14
- import { test, expect } from '@playwright/test';
15
- import { useTable } from 'playwright-smart-table';
7
+ 📦 Installation
16
8
 
17
- test('Verify User', async ({ page }) => {
18
- await page.goto('/users');
9
+ npm install @rickcedwhat/playwright-smart-table
19
10
 
20
- // 1. Initialize (Defaults to <table>, <tr>, <td>)
21
- const table = useTable(page.locator('#users-table'));
22
-
23
- // 2. Find row across pages automatically!
24
- // This will search Page 1, then Page 2, etc.
25
- const row = await table.getByRow({ Name: 'Alice', Role: 'Admin' });
26
-
27
- await expect(row).toBeVisible();
28
- });
29
11
 
12
+ Requires @playwright/test as a peer dependency.
30
13
 
31
- 🧩 Pagination Strategies
32
- This library doesn't guess how your table works. You tell it using a Strategy.
33
- 1. Standard "Next" Button
34
- For tables like Datatables.net or simple paginated lists.
35
- import { TableStrategies } from 'playwright-smart-table';
36
-
37
- const table = useTable(locator, {
38
- // Strategy: Find button -> Click -> Wait for first row to change
39
- pagination: TableStrategies.clickNext('[aria-label="Next Page"]')
40
- });
14
+ 🚀 Quick Start
41
15
 
16
+ 1. The Standard HTML Table
42
17
 
43
- 2. Infinite Scroll
44
- For grids that load more data as you scroll down (AG-Grid, Virtual Lists, HTMX).
45
- const table = useTable(locator, {
46
- // Strategy: Aggressive scroll to bottom -> Wait for row count to increase
47
- pagination: TableStrategies.infiniteScroll()
48
- });
18
+ For standard tables (<table>, <tr>, <td>), no configuration is needed.
49
19
 
20
+ import { test, expect } from '@playwright/test';
21
+ import { useTable } from '@rickcedwhat/playwright-smart-table';
50
22
 
51
- 3. Load More Button
52
- For lists with a "Load More Results" button at the bottom.
53
- const table = useTable(locator, {
54
- // Strategy: Click button -> Wait for row count to increase
55
- pagination: TableStrategies.clickLoadMore('button.load-more')
23
+ test('Verify User Email', async ({ page }) => {
24
+ const table = useTable(page.locator('#users-table'));
25
+
26
+ // 🪄 Magic: Finds the row with Name="Alice", then gets the Email cell
27
+ // If Alice is on Page 2, it handles pagination automatically.
28
+ await expect(
29
+ await table.getByCell({ Name: 'Alice' }, 'Email')
30
+ ).toHaveText('alice@example.com');
56
31
  });
57
32
 
58
33
 
59
- ⚙️ Advanced Config (MUI / Grid / Divs)
60
- For complex div-based tables (like Material UI DataGrid), you can override the selectors.
34
+ 2. Complex Grids (Material UI / AG-Grid / Divs)
35
+
36
+ For modern React grids that use <div> structures, simply override the selectors.
37
+
38
+ import { useTable, TableStrategies } from '@rickcedwhat/playwright-smart-table';
39
+
61
40
  const table = useTable(page.locator('.MuiDataGrid-root'), {
62
41
  rowSelector: '.MuiDataGrid-row',
63
42
  headerSelector: '.MuiDataGrid-columnHeader',
64
43
  cellSelector: '.MuiDataGrid-cell',
65
- pagination: TableStrategies.clickNext('[aria-label="Go to next page"]')
44
+ // Strategy: Tell it how to find the next page
45
+ pagination: TableStrategies.clickNext(
46
+ (root) => root.getByRole('button', { name: 'Go to next page' })
47
+ )
66
48
  });
67
49
 
68
50
 
51
+ 🧩 Pagination Strategies
52
+
53
+ This library uses the Strategy Pattern to handle navigation. This ensures future stability: we can add new ways to paginate without breaking existing tests.
54
+
55
+ clickNext(selector)
56
+
57
+ Best for standard tables (Datatables, lists).
58
+
59
+ Behavior: Clicks the button -> Waits for the first row of data to change.
60
+
61
+ Selector: Can be a CSS string OR a Playwright locator function.
62
+
63
+ // CSS String
64
+ pagination: TableStrategies.clickNext('button.next-page')
65
+
66
+ // Locator Function (More Robust)
67
+ pagination: TableStrategies.clickNext((root) => root.getByRole('button', { name: 'Next' }))
68
+
69
+
70
+ infiniteScroll()
71
+
72
+ Best for Virtualized Grids (AG-Grid) or lazy-loading lists (HTMX).
73
+
74
+ Behavior: Aggressively scrolls the container/window to the bottom -> Waits for row count to increase.
75
+
76
+ pagination: TableStrategies.infiniteScroll()
77
+
78
+
79
+ clickLoadMore(selector)
80
+
81
+ Best for "Load More" buttons.
82
+
83
+ Behavior: Clicks button -> Waits for row count to increase.
84
+
85
+ pagination: TableStrategies.clickLoadMore('button.load-more')
86
+
87
+
69
88
  📖 API Reference
89
+
70
90
  getByRow(filters, options?)
71
- Finds a specific row matching the filters.
72
- filters: { "Column Name": "Value", "Age": "25" }
73
- options: { exact: true }
74
- Returns: Playwright Locator for that row.
75
- Throws error if multiple rows match.
76
- await table.getByRow({ Email: "alice@example.com" });
91
+
92
+ Returns the Locator for a specific row.
93
+
94
+ Strict Mode: Throws an error if filters match more than 1 row.
95
+
96
+ Auto-Pagination: Will search up to maxPages to find the row.
97
+
98
+ // Find a row where Name is "Alice" AND Role is "Admin"
99
+ const row = await table.getByRow({ Name: "Alice", Role: "Admin" });
100
+ await expect(row).toBeVisible();
77
101
 
78
102
 
79
103
  getByCell(filters, targetColumn)
80
- Finds a specific cell inside a filtered row. Useful for clicking action buttons.
81
- Returns: Playwright Locator for the cell.
82
- // Find the 'Edit' button for Alice
104
+
105
+ Returns the Locator for a specific cell inside a matched row.
106
+
107
+ Use this for interactions (clicking edit buttons, checking checkboxes).
108
+
109
+ // Find Alice's row, then find the "Actions" column, then click the button inside it
83
110
  await table.getByCell({ Name: "Alice" }, "Actions").getByRole('button').click();
84
111
 
85
112
 
113
+ getRowAsJSON(filters)
114
+
115
+ Returns a POJO (Plain Old JavaScript Object) of the row data. Useful for debugging or strict data assertions.
116
+
117
+ const data = await table.getRowAsJSON({ ID: "101" });
118
+ console.log(data);
119
+ // Output: { ID: "101", Name: "Alice", Status: "Active" }
120
+
121
+
86
122
  getRows()
87
- Dumps all rows on the current page as an array of objects. Great for verifying sort order.
88
- const rows = await table.getRows();
89
- expect(rows[0].Name).toBe("Alice");
90
- expect(rows[1].Name).toBe("Bob");
91
123
 
124
+ Returns an array of all rows on the current page.
92
125
 
93
- getRowAsJSON(filters)
94
- Returns a single row's data as a JSON object.
95
- const data = await table.getRowAsJSON({ ID: "123" });
96
- console.log(data); // { ID: "123", Name: "Alice", Status: "Active" }
126
+ const allRows = await table.getRows();
127
+ expect(allRows[0].Name).toBe("Alice"); // Verify sort order
97
128
 
98
129
 
99
- 🛠️ Custom Strategies
100
- Need to handle a custom pagination logic? Write your own strategy!
101
- A strategy is just a function that returns Promise<boolean> (true if data loaded, false if done).
102
- const myCustomStrategy = async ({ root, page, resolve }) => {
103
- // 1. Find your specific button
104
- const btn = resolve('.my-weird-button', root);
105
-
106
- if (!await btn.isVisible()) return false; // Stop pagination
107
-
108
- // 2. Click and Wait
109
- await btn.click();
110
- await page.waitForResponse(resp => resp.url().includes('/api/users'));
111
-
112
- return true; // We loaded more!
113
- };
130
+ 🛠️ Developer Tools
131
+
132
+ The library includes helper tools to generate configurations for you.
133
+
134
+ // Print the HTML structure prompt to console
135
+ // Copy-paste the output into ChatGPT/Gemini to get your config object
136
+ await table.generateConfigPrompt();
137
+
138
+ // Print a prompt to help write a custom Pagination Strategy
139
+ await table.generateStrategyPrompt();
140
+
141
+
142
+ 🛡️ Stability & Versioning
143
+
144
+ This package follows Semantic Versioning.
145
+
146
+ 1.x.x: No breaking changes to the useTable signature.
114
147
 
115
- // Usage
116
- useTable(locator, { pagination: myCustomStrategy });
148
+ New strategies may be added, but existing ones will remain stable.
117
149
 
150
+ To ensure stability in your projects, install with:
118
151
 
152
+ "@rickcedwhat/playwright-smart-table": "^1.0.0"
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './useTable';
2
2
  export * from './types';
3
3
  export * from './strategies';
4
+ export * from './presets';
package/dist/index.js CHANGED
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./useTable"), exports);
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./strategies"), exports);
20
+ __exportStar(require("./presets"), exports);
@@ -0,0 +1,42 @@
1
+ import { Locator } from '@playwright/test';
2
+ import { TableConfig } from './types';
3
+ /**
4
+ * Preset for Key-Value Forms.
5
+ * * Default Structure:
6
+ * - Row: div.form-group
7
+ * - Cell: Direct children (> *)
8
+ * - Columns: ['Label', 'Input']
9
+ */
10
+ export declare const useForm: (rootLocator: Locator, options?: TableConfig) => {
11
+ getHeaders: () => Promise<string[]>;
12
+ getByRow: (filters: Record<string, string | RegExp | number>, options?: {
13
+ exact?: boolean;
14
+ maxPages?: number;
15
+ }) => Promise<Locator>;
16
+ getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
17
+ getRows: () => Promise<Record<string, string>[]>;
18
+ getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
19
+ setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
20
+ generateConfigPrompt: () => Promise<void>;
21
+ generateStrategyPrompt: () => Promise<void>;
22
+ };
23
+ /**
24
+ * Preset for Navigation Menus.
25
+ * * Default Structure:
26
+ * - Row: li
27
+ * - Cell: null (The row IS the cell)
28
+ * - Columns: ['Item']
29
+ */
30
+ export declare const useMenu: (menuLocator: Locator, options?: TableConfig) => {
31
+ getHeaders: () => Promise<string[]>;
32
+ getByRow: (filters: Record<string, string | RegExp | number>, options?: {
33
+ exact?: boolean;
34
+ maxPages?: number;
35
+ }) => Promise<Locator>;
36
+ getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
37
+ getRows: () => Promise<Record<string, string>[]>;
38
+ getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
39
+ setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
40
+ generateConfigPrompt: () => Promise<void>;
41
+ generateStrategyPrompt: () => Promise<void>;
42
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMenu = exports.useForm = void 0;
4
+ const useTable_1 = require("./useTable");
5
+ /**
6
+ * Preset for Key-Value Forms.
7
+ * * Default Structure:
8
+ * - Row: div.form-group
9
+ * - Cell: Direct children (> *)
10
+ * - Columns: ['Label', 'Input']
11
+ */
12
+ const useForm = (rootLocator, options = {}) => {
13
+ return (0, useTable_1.useTable)(rootLocator, Object.assign({
14
+ // Defaults:
15
+ rowSelector: 'div.form-group', cellSelector: (row) => row.locator('> *'), headerSelector: null, columnNames: ['Label', 'Input'] }, options));
16
+ };
17
+ exports.useForm = useForm;
18
+ /**
19
+ * Preset for Navigation Menus.
20
+ * * Default Structure:
21
+ * - Row: li
22
+ * - Cell: null (The row IS the cell)
23
+ * - Columns: ['Item']
24
+ */
25
+ const useMenu = (menuLocator, options = {}) => {
26
+ return (0, useTable_1.useTable)(menuLocator, Object.assign({
27
+ // Defaults:
28
+ rowSelector: 'li', cellSelector: null, headerSelector: null, columnNames: ['Item'] }, options));
29
+ };
30
+ exports.useMenu = useMenu;
package/dist/types.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import { Locator, Page } from '@playwright/test';
2
2
  /**
3
- * A selector can be a CSS string or a function.
4
- * We allow 'parent' to be Locator OR Page to match your working logic.
3
+ * A selector can be a CSS string, a function, or null (to disable/skip).
5
4
  */
6
- export type Selector = string | ((parent: Locator | Page) => Locator);
5
+ export type Selector = string | ((parent: Locator | Page) => Locator) | null;
7
6
  export interface TableConfig {
8
7
  rowSelector?: Selector;
9
8
  headerSelector?: Selector;
10
9
  cellSelector?: Selector;
11
10
  pagination?: PaginationStrategy;
12
11
  maxPages?: number;
12
+ /**
13
+ * Statically override specific column names.
14
+ * Use 'undefined' to keep the detected name for that index.
15
+ * Use this to name columns for Menus or Forms that have no headers.
16
+ * Example: ['MenuItem'] or [undefined, "Actions"]
17
+ */
18
+ columnNames?: (string | undefined)[];
13
19
  }
14
20
  export interface TableContext {
15
21
  root: Locator;
@@ -9,9 +9,7 @@ export declare const useTable: (rootLocator: Locator, configOptions?: TableConfi
9
9
  getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
10
10
  getRows: () => Promise<Record<string, string>[]>;
11
11
  getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
12
- /**
13
- * 🛠️ DEV TOOL: Prints a prompt to the console.
14
- * Copy the output and paste it into Gemini/ChatGPT to generate your config.
15
- */
12
+ setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
16
13
  generateConfigPrompt: () => Promise<void>;
14
+ generateStrategyPrompt: () => Promise<void>;
17
15
  };
package/dist/useTable.js CHANGED
@@ -11,27 +11,38 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useTable = void 0;
13
13
  const useTable = (rootLocator, configOptions = {}) => {
14
- const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1 }, configOptions);
15
- // ✅ UPDATE: Accept Locator OR Page (to match your work logic)
14
+ const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, columnNames: [] }, configOptions);
16
15
  const resolve = (item, parent) => {
17
16
  if (typeof item === 'string')
18
17
  return parent.locator(item);
19
18
  if (typeof item === 'function')
20
19
  return item(parent);
21
- return item;
20
+ throw new Error("Cannot resolve a null selector. Ensure your config defines selectors correctly.");
22
21
  };
23
22
  let _headerMap = null;
24
23
  const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
25
24
  if (_headerMap)
26
25
  return _headerMap;
27
- // Headers are still resolved relative to the table root (safer)
28
- const headerLoc = resolve(config.headerSelector, rootLocator);
29
- try {
30
- yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
26
+ // 1. Scrape DOM (Only if headerSelector is NOT null)
27
+ let texts = [];
28
+ if (config.headerSelector) {
29
+ const headerLoc = resolve(config.headerSelector, rootLocator);
30
+ try {
31
+ yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
32
+ texts = yield headerLoc.allInnerTexts();
33
+ }
34
+ catch (e) { /* Ignore hydration/empty/timeout */ }
35
+ }
36
+ // 2. Merge Scraped Data with Config Overrides
37
+ _headerMap = new Map();
38
+ const overrides = config.columnNames || [];
39
+ const colCount = Math.max(texts.length, overrides.length);
40
+ for (let i = 0; i < colCount; i++) {
41
+ const scrapedText = (texts[i] || "").trim() || `__col_${i}`;
42
+ const overrideText = overrides[i];
43
+ const finalName = (overrideText !== undefined) ? overrideText : scrapedText;
44
+ _headerMap.set(finalName, i);
31
45
  }
32
- catch (e) { /* Ignore hydration */ }
33
- const texts = yield headerLoc.allInnerTexts();
34
- _headerMap = new Map(texts.map((t, i) => [t.trim() || `__col_${i}`, i]));
35
46
  return _headerMap;
36
47
  });
37
48
  const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
@@ -41,38 +52,67 @@ const useTable = (rootLocator, configOptions = {}) => {
41
52
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
42
53
  let currentPage = 1;
43
54
  while (true) {
44
- // 1. Row Locator uses ROOT (Matches your snippet)
55
+ if (!config.rowSelector)
56
+ throw new Error("rowSelector cannot be null");
45
57
  let rowLocator = resolve(config.rowSelector, rootLocator);
46
58
  for (const [colName, value] of Object.entries(filters)) {
47
59
  const colIndex = map.get(colName);
48
60
  if (colIndex === undefined)
49
- throw new Error(`Column '${colName}' not found.`);
61
+ throw new Error(`Column '${colName}' not found. Available: ${Array.from(map.keys())}`);
50
62
  const exact = options.exact || false;
51
63
  const filterVal = typeof value === 'number' ? String(value) : value;
52
- // MATCHING YOUR WORK LOGIC EXACTLY
53
- // 2. Cell Template uses PAGE (Matches your snippet)
54
- const cellTemplate = resolve(config.cellSelector, page);
55
- // 3. Filter using .nth(colIndex)
56
- rowLocator = rowLocator.filter({
57
- has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
58
- });
64
+ // Case 1: No Cell Selector (Menu) - Filter the Row Itself
65
+ if (!config.cellSelector) {
66
+ if (exact) {
67
+ rowLocator = rowLocator.filter({ hasText: new RegExp(`^${escapeRegExp(String(filterVal))}$`) });
68
+ }
69
+ else {
70
+ rowLocator = rowLocator.filter({ hasText: filterVal });
71
+ }
72
+ }
73
+ // Case 2: String Cell Selector - Standard Table Logic (Restored)
74
+ else if (typeof config.cellSelector === 'string') {
75
+ // RESTORED: This logic worked for standard tables.
76
+ // We resolve against the PAGE to create a generic locator template.
77
+ // Playwright handles the relative filtering correctly for standard tables.
78
+ const cellTemplate = resolve(config.cellSelector, page);
79
+ rowLocator = rowLocator.filter({
80
+ has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
81
+ });
82
+ }
83
+ // Case 3: Function Cell Selector - Forms (Iterative Fallback)
84
+ else {
85
+ const count = yield rowLocator.count();
86
+ let matchFound = false;
87
+ for (let i = 0; i < count; i++) {
88
+ const specificRow = rowLocator.nth(i);
89
+ // Resolve cell relative to this specific row
90
+ const specificCell = config.cellSelector(specificRow).nth(colIndex);
91
+ if ((yield specificCell.getByText(filterVal, { exact }).count()) > 0) {
92
+ if (matchFound) {
93
+ throw new Error(`Strict Mode Violation: Found multiple rows matching ${JSON.stringify(filters)}.`);
94
+ }
95
+ rowLocator = specificRow;
96
+ matchFound = true;
97
+ // Break inner loop to proceed to next filter or return
98
+ break;
99
+ }
100
+ }
101
+ if (!matchFound) {
102
+ // Return empty locator to fail gracefully
103
+ return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_" + Date.now() });
104
+ }
105
+ }
59
106
  }
60
107
  const count = yield rowLocator.count();
61
- if (count > 1) {
108
+ if (count > 1)
62
109
  throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
63
- }
64
110
  if (count === 1)
65
111
  return rowLocator.first();
66
- // --- PAGINATION LOGIC ---
112
+ // --- PAGINATION ---
67
113
  if (config.pagination && currentPage < effectiveMaxPages) {
68
- const context = {
69
- root: rootLocator,
70
- config: config,
71
- page: page,
72
- resolve: resolve
73
- };
74
- const didLoadMore = yield config.pagination(context);
75
- if (didLoadMore) {
114
+ const context = { root: rootLocator, config: config, page: page, resolve: resolve };
115
+ if (yield config.pagination(context)) {
76
116
  currentPage++;
77
117
  continue;
78
118
  }
@@ -92,12 +132,14 @@ const useTable = (rootLocator, configOptions = {}) => {
92
132
  const row = yield _findRowLocator(rowFilters);
93
133
  if (!row)
94
134
  throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
135
+ // Guard: getByCell makes no sense for Menus (no cells)
136
+ if (!config.cellSelector) {
137
+ throw new Error("getByCell is not supported when 'cellSelector' is null (e.g. Menus). Use getByRow instead.");
138
+ }
95
139
  const map = yield _getMap();
96
140
  const colIndex = map.get(targetColumn);
97
141
  if (colIndex === undefined)
98
142
  throw new Error(`Column '${targetColumn}' not found.`);
99
- // Return the specific cell
100
- // We scope this to the found ROW to ensure we get the right cell
101
143
  if (typeof config.cellSelector === 'string') {
102
144
  return row.locator(config.cellSelector).nth(colIndex);
103
145
  }
@@ -112,14 +154,18 @@ const useTable = (rootLocator, configOptions = {}) => {
112
154
  const results = [];
113
155
  for (let i = 0; i < rowCount; i++) {
114
156
  const row = rowLocator.nth(i);
115
- let cells;
116
- if (typeof config.cellSelector === 'string') {
117
- cells = row.locator(config.cellSelector);
157
+ let cellTexts = [];
158
+ if (!config.cellSelector) {
159
+ cellTexts = [yield row.innerText()];
160
+ }
161
+ else if (typeof config.cellSelector === 'string') {
162
+ // For string selectors, we query all matching cells in the row
163
+ cellTexts = yield row.locator(config.cellSelector).allInnerTexts();
118
164
  }
119
165
  else {
120
- cells = resolve(config.cellSelector, row);
166
+ // For function selectors, we resolve against the row
167
+ cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
121
168
  }
122
- const cellTexts = yield cells.allInnerTexts();
123
169
  const rowData = {};
124
170
  for (const [colName, colIdx] of map.entries()) {
125
171
  rowData[colName] = (cellTexts[colIdx] || "").trim();
@@ -132,14 +178,16 @@ const useTable = (rootLocator, configOptions = {}) => {
132
178
  const row = yield _findRowLocator(filters);
133
179
  if (!row)
134
180
  throw new Error(`Row not found: ${JSON.stringify(filters)}`);
135
- let cells;
136
- if (typeof config.cellSelector === 'string') {
137
- cells = row.locator(config.cellSelector);
181
+ let cellTexts = [];
182
+ if (!config.cellSelector) {
183
+ cellTexts = [yield row.innerText()];
184
+ }
185
+ else if (typeof config.cellSelector === 'string') {
186
+ cellTexts = yield row.locator(config.cellSelector).allInnerTexts();
138
187
  }
139
188
  else {
140
- cells = resolve(config.cellSelector, row);
189
+ cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
141
190
  }
142
- const cellTexts = yield cells.allInnerTexts();
143
191
  const map = yield _getMap();
144
192
  const result = {};
145
193
  for (const [colName, colIndex] of map.entries()) {
@@ -147,80 +195,34 @@ const useTable = (rootLocator, configOptions = {}) => {
147
195
  }
148
196
  return result;
149
197
  }),
150
- /**
151
- * 🛠️ DEV TOOL: Prints a prompt to the console.
152
- * Copy the output and paste it into Gemini/ChatGPT to generate your config.
153
- */
198
+ setColumnName: (colIndex, newNameOrFn) => __awaiter(void 0, void 0, void 0, function* () {
199
+ const map = yield _getMap();
200
+ let oldName = "";
201
+ for (const [name, idx] of map.entries()) {
202
+ if (idx === colIndex) {
203
+ oldName = name;
204
+ break;
205
+ }
206
+ }
207
+ if (!oldName)
208
+ oldName = `__col_${colIndex}`;
209
+ const newName = typeof newNameOrFn === 'function' ? newNameOrFn(oldName) : newNameOrFn;
210
+ if (map.has(oldName))
211
+ map.delete(oldName);
212
+ map.set(newName, colIndex);
213
+ }),
154
214
  generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
155
215
  const html = yield rootLocator.evaluate((el) => el.outerHTML);
156
- const separator = "=".repeat(50);
157
- const prompt = `
158
- ${separator}
159
- 🤖 COPY THE TEXT BELOW INTO GEMINI/ChatGPT 🤖
160
- ${separator}
161
-
162
- I am using a Playwright helper factory called 'useTable'.
163
- I need you to generate the configuration object based on the HTML structure below.
164
-
165
- Here is the table HTML:
166
- \`\`\`html
167
- ${html}
168
- \`\`\`
169
-
170
- Based on this HTML, generate the configuration object matching this signature:
171
- const table = useTable(page.locator('...'), {
172
- // Find the rows (exclude headers and empty spacer rows if possible)
173
- rowSelector: "...", // OR (root) => root.locator(...)
174
-
175
- // Find the column headers
176
- headerSelector: "...", // OR (root) => root.locator(...)
177
-
178
- // Find the cell (relative to a specific row)
179
- cellSelector: "...", // OR (row) => row.locator(...)
180
-
181
- // Find the "Next Page" button (if it exists in the HTML)
182
- paginationNextSelector: (root) => root.locator(...)
183
- });
184
-
185
- **Requirements:**
186
- 1. Prefer \`getByRole\` or \`getByTestId\` over CSS classes where possible.
187
- 2. If the table uses \`div\` structures (like React Table), ensure the \`rowSelector\` does not accidentally select the header row.
188
- 3. If there are "padding" or "loading" rows, use \`.filter()\` to exclude them.
189
-
190
- ${separator}
191
- `;
192
- console.log(prompt);
216
+ console.log(`\n=== CONFIG PROMPT ===\nI have this HTML:\n\`\`\`html\n${html}\n\`\`\`\nGenerate a 'useTable' config for it.`);
217
+ }),
218
+ generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
219
+ const container = rootLocator.locator('xpath=..');
220
+ const html = yield container.evaluate((el) => el.outerHTML);
221
+ console.log(`\n=== STRATEGY PROMPT ===\nI have this Container HTML:\n\`\`\`html\n${html.substring(0, 2000)}\n\`\`\`\nWrite a pagination strategy.`);
193
222
  })
194
223
  };
195
- /**
196
- * 🛠️ DEV TOOL: Prints a prompt to help write a custom Pagination Strategy.
197
- * It snapshots the HTML *surrounding* the table to find buttons/scroll containers.
198
- */
199
- generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
200
- // 1. Get the parent container (often holds the pagination controls)
201
- const container = rootLocator.locator('xpath=..');
202
- const html = yield container.evaluate((el) => el.outerHTML);
203
- const prompt = `
204
- ==================================================
205
- 🤖 COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY 🤖
206
- ==================================================
207
-
208
- I am using 'playwright-smart-table'. I need a custom Pagination Strategy.
209
- The table is inside this container HTML:
210
-
211
- \`\`\`html
212
- ${html.substring(0, 5000)} ... (truncated)
213
- \`\`\`
214
-
215
- Write a strategy that implements this interface:
216
- type PaginationStrategy = (context: TableContext) => Promise<boolean>;
217
-
218
- Requirements:
219
- 1. Identify the "Next" button OR the scroll container.
220
- 2. Return 'true' if data loaded, 'false' if end of data.
221
- 3. Use context.resolve() to find elements.
222
- `;
223
- console.log(prompt);
224
- });
225
224
  };
226
225
  exports.useTable = useTable;
226
+ function escapeRegExp(string) {
227
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
228
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "A smart table utility for Playwright with built-in pagination strategies.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",