@rickcedwhat/playwright-smart-table 2.1.0 → 2.1.2

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
@@ -8,46 +8,52 @@ This library abstracts away the complexity of testing dynamic web tables. It han
8
8
 
9
9
  npm install @rickcedwhat/playwright-smart-table
10
10
 
11
-
12
11
  Requires @playwright/test as a peer dependency.
13
12
 
14
13
  ⚔ Quick Start
15
14
 
16
- 1. The Standard HTML Table
17
-
18
- For standard tables (<table>, <tr>, <td>), no configuration is needed.
15
+ The Standard HTML Table
19
16
 
20
- import { test, expect } from '@playwright/test';
21
- import { useTable } from '@rickcedwhat/playwright-smart-table';
17
+ For standard tables (<table>, <tr>, <td>), no configuration is needed (defaults work for most standard HTML tables).
22
18
 
23
- test('Verify User Email', async ({ page }) => {
24
- const table = useTable(page.locator('#users-table'));
25
-
26
- // šŸŖ„ Finds the row with Name="Alice", then gets the Email cell.
27
- // If Alice is on Page 2, it handles pagination automatically.
28
- const row = await table.getByRow({ Name: 'Alice' });
29
-
30
- await expect(row.getCell('Email')).toHaveText('alice@example.com');
19
+ <!-- embed: quick-start -->
20
+ ```typescript
21
+ const table = useTable(page.locator('#example'), {
22
+ headerSelector: 'thead th' // Override for this specific site
31
23
  });
32
24
 
25
+ // šŸŖ„ Finds the row with Name="Airi Satou", then gets the Position cell.
26
+ // If Airi is on Page 2, it handles pagination automatically.
27
+ const row = await table.getByRow({ Name: 'Airi Satou' });
33
28
 
34
- 2. Complex Grids (Material UI / AG-Grid / Divs)
29
+ await expect(row.getCell('Position')).toHaveText('Accountant');
30
+ ```
31
+ <!-- /embed: quick-start -->
35
32
 
36
- For modern React grids, simply override the selectors and define a pagination strategy.
33
+ Complex Grids (Material UI / AG-Grid / Divs)
37
34
 
38
- import { useTable, TableStrategies } from '@rickcedwhat/playwright-smart-table';
35
+ For modern React grids, simply override the selectors and define a pagination strategy.
39
36
 
40
- const table = useTable(page.locator('.MuiDataGrid-root'), {
41
- rowSelector: '.MuiDataGrid-row',
42
- headerSelector: '.MuiDataGrid-columnHeader',
43
- cellSelector: '.MuiDataGrid-cell',
37
+ <!-- embed: pagination -->
38
+ ```typescript
39
+ const table = useTable(page.locator('#example'), {
40
+ rowSelector: 'tbody tr',
41
+ headerSelector: 'thead th',
42
+ cellSelector: 'td',
44
43
  // Strategy: Tell it how to find the next page
45
- pagination: TableStrategies.clickNext(
46
- // Use 'page' to find buttons outside the table container
47
- (root) => root.page().getByRole('button', { name: 'Go to next page' })
48
- )
44
+ pagination: TableStrategies.clickNext(() =>
45
+ page.getByRole('link', { name: 'Next' })
46
+ ),
47
+ maxPages: 5 // Allow scanning up to 5 pages
49
48
  });
50
49
 
50
+ // āœ… Verify Colleen is NOT visible initially
51
+ await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
52
+
53
+ await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
54
+ // NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
55
+ ```
56
+ <!-- /embed: pagination -->
51
57
 
52
58
  🧠 SmartRow Pattern
53
59
 
@@ -55,25 +61,65 @@ The core power of this library is the SmartRow.
55
61
 
56
62
  Unlike a standard Playwright Locator, a SmartRow is aware of its context within the table's schema. It extends the standard Locator API, so you can chain standard Playwright methods (.click(), .isVisible()) directly off it.
57
63
 
58
- getCell(columnName)
59
-
60
- Instead of writing brittle nth-child selectors, ask for the column by name.
64
+ <!-- embed: smart-row -->
65
+ ```typescript
66
+ // 1. Get SmartRow via getByRow
67
+ const row = await table.getByRow({ Name: 'Airi Satou' });
61
68
 
69
+ // 2. Interact with cell (No more getByCell needed!)
62
70
  // āœ… Good: Resilient to column reordering
63
- await row.getCell('Email').click();
71
+ await row.getCell('Position').click();
72
+
73
+ // 3. Dump data from row
74
+ const data = await row.toJSON();
75
+ console.log(data);
76
+ // { Name: "Airi Satou", Position: "Accountant", ... }
77
+ ```
78
+ <!-- /embed: smart-row -->
79
+
80
+ šŸš€ Advanced Usage
64
81
 
65
- // āŒ Bad: Brittle
66
- await row.locator('td').nth(2).click();
82
+ šŸ”Ž Debug Mode
67
83
 
84
+ Having trouble finding rows? Enable debug mode to see exactly what the library sees (headers mapped, rows scanned, pagination triggers).
68
85
 
69
- toJSON()
86
+ <!-- embed: advanced-debug -->
87
+ ```typescript
88
+ const table = useTable(page.locator('#example'), {
89
+ headerSelector: 'thead th',
90
+ debug: true
91
+ });
92
+ ```
93
+ <!-- /embed: advanced-debug -->
70
94
 
71
- Extracts the entire row's data into a clean key-value object.
95
+ šŸ”„ Resetting State
72
96
 
73
- const data = await row.toJSON();
74
- console.log(data);
75
- // { Name: "Alice", Role: "Admin", Status: "Active" }
97
+ If your tests navigate deep into a table (e.g., Page 5), subsequent searches might fail. Use .reset() to return to the start.
98
+
99
+ <!-- embed: advanced-reset -->
100
+ ```typescript
101
+ // Navigate deep into the table (simulated by finding a row on page 2)
102
+ // For the test to pass, we need a valid row. 'Angelica Ramos' is usually on page 1 or 2 depending on sorting.
103
+ try {
104
+ await table.getByRow({ Name: 'Angelica Ramos' });
105
+ } catch (e) {}
106
+
107
+ // Reset internal state (and potentially UI) to Page 1
108
+ await table.reset();
109
+ ```
110
+ <!-- /embed: advanced-reset -->
111
+
112
+ šŸ“Š Column Scanning
76
113
 
114
+ Need to verify a specific column is sorted or contains specific data? Use getColumnValues for a high-performance scan.
115
+
116
+ <!-- embed: advanced-column-scan -->
117
+ ```typescript
118
+ // Quickly grab all text values from the "Office" column
119
+ const offices = await table.getColumnValues('Office');
120
+ expect(offices).toContain('Tokyo');
121
+ ```
122
+ <!-- /embed: advanced-column-scan -->
77
123
 
78
124
  šŸ“– API Reference
79
125
 
@@ -87,13 +133,16 @@ Returns Sentinel if 0 rows match (allows not.toBeVisible() assertions).
87
133
 
88
134
  Auto-Paginates if the row isn't found on the current page.
89
135
 
90
- // Find a row where Name is "Alice" AND Role is "Admin"
91
- const row = await table.getByRow({ Name: "Alice", Role: "Admin" });
136
+ <!-- embed: get-by-row -->
137
+ ```typescript
138
+ // Find a row where Name is "Airi Satou" AND Office is "Tokyo"
139
+ const row = await table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
92
140
  await expect(row).toBeVisible();
93
141
 
94
142
  // Assert it does NOT exist
95
- await expect(await table.getByRow({ Name: "Ghost" })).not.toBeVisible();
96
-
143
+ await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
144
+ ```
145
+ <!-- /embed: get-by-row -->
97
146
 
98
147
  getAllRows(options?)
99
148
 
@@ -103,19 +152,22 @@ Returns: Array of SmartRow objects.
103
152
 
104
153
  Best for: Checking existence ("at least one") or validating sort order.
105
154
 
155
+ <!-- embed: get-all-rows -->
156
+ ```typescript
106
157
  // 1. Get ALL rows on the current page
107
158
  const allRows = await table.getAllRows();
108
159
 
109
160
  // 2. Get subset of rows (Filtering)
110
- const activeUsers = await table.getAllRows({
111
- filter: { Status: 'Active' }
161
+ const tokyoUsers = await table.getAllRows({
162
+ filter: { Office: 'Tokyo' }
112
163
  });
113
- expect(activeUsers.length).toBeGreaterThan(0); // "At least one active user"
164
+ expect(tokyoUsers.length).toBeGreaterThan(0);
114
165
 
115
166
  // 3. Dump data to JSON
116
167
  const data = await table.getAllRows({ asJSON: true });
117
- console.log(data); // [{ Name: "Alice", Status: "Active" }, ...]
118
-
168
+ console.log(data); // [{ Name: "Airi Satou", ... }, ...]
169
+ ```
170
+ <!-- /embed: get-all-rows -->
119
171
 
120
172
  🧩 Pagination Strategies
121
173
 
@@ -123,44 +175,17 @@ This library uses the Strategy Pattern to handle navigation. You can use the bui
123
175
 
124
176
  Built-in Strategies
125
177
 
126
- clickNext(selector)
127
- Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
178
+ clickNext(selector) Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
128
179
 
129
- pagination: TableStrategies.clickNext((root) =>
130
- root.page().getByRole('button', { name: 'Next' })
180
+ pagination: TableStrategies.clickNext((root) =>
181
+ root.page().getByRole('button', { name: 'Next' })
131
182
  )
132
183
 
133
-
134
- infiniteScroll()
135
- Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
184
+ infiniteScroll() Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
136
185
 
137
186
  pagination: TableStrategies.infiniteScroll()
138
187
 
139
-
140
- clickLoadMore(selector)
141
- Best for "Load More" buttons. Clicks and waits for row count to increase.
142
-
143
- Writing Custom Strategies
144
-
145
- A Strategy is just a function that receives the table context and returns a Promise<boolean> (true if navigation happened, false if we reached the end).
146
-
147
- import { PaginationStrategy } from '@rickcedwhat/playwright-smart-table';
148
-
149
- const myCustomStrategy: PaginationStrategy = async ({ root, page, config }) => {
150
- // 1. Check if we can navigate
151
- const nextBtn = page.getByTestId('custom-next-arrow');
152
- if (!await nextBtn.isVisible()) return false;
153
-
154
- // 2. Perform Navigation
155
- await nextBtn.click();
156
-
157
- // 3. Smart Wait (Crucial!)
158
- // Wait for a loading spinner to disappear, or data to change
159
- await expect(page.locator('.spinner')).not.toBeVisible();
160
-
161
- return true; // We successfully moved to the next page
162
- };
163
-
188
+ clickLoadMore(selector) Best for "Load More" buttons. Clicks and waits for row count to increase.
164
189
 
165
190
  šŸ› ļø Developer Tools
166
191
 
@@ -170,12 +195,11 @@ generateConfigPrompt(options?)
170
195
 
171
196
  Prints a prompt you can paste into ChatGPT/Gemini to generate the TableConfig for your specific HTML.
172
197
 
173
- // Options: 'console' (default), 'report' (Playwright HTML Report), 'file'
174
- await table.generateConfigPrompt({ output: 'report' });
175
-
198
+ // Options: 'console' (default), 'error' (Throw error to see prompt in trace/cloud)
199
+ await table.generateConfigPrompt({ output: 'console' });
176
200
 
177
201
  generateStrategyPrompt(options?)
178
202
 
179
203
  Prints a prompt to help you write a custom Pagination Strategy.
180
204
 
181
- await table.generateStrategyPrompt({ output: 'console' });
205
+ await table.generateStrategyPrompt({ output: 'console' });
@@ -34,20 +34,40 @@ exports.TableStrategies = {
34
34
  clickNext: (nextButtonSelector, timeout = 5000) => {
35
35
  return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root, config, resolve, page }) {
36
36
  const nextBtn = resolve(nextButtonSelector, root).first();
37
- // Check if button exists/enabled before clicking
38
- if (!(yield nextBtn.isVisible()) || !(yield nextBtn.isEnabled())) {
37
+ // Debug log (can be verbose, maybe useful for debugging only)
38
+ // console.log(`[Strategy: clickNext] Checking button...`);
39
+ // Check if button exists/enabled before clicking.
40
+ // We do NOT wait here because if the button isn't visible/enabled,
41
+ // we assume we reached the last page.
42
+ if (!(yield nextBtn.isVisible())) {
43
+ console.log(`[Strategy: clickNext] Button not visible. Stopping pagination.`);
44
+ return false;
45
+ }
46
+ if (!(yield nextBtn.isEnabled())) {
47
+ console.log(`[Strategy: clickNext] Button disabled. Stopping pagination.`);
39
48
  return false;
40
49
  }
41
50
  // 1. Snapshot current state
42
51
  const firstRow = resolve(config.rowSelector, root).first();
43
52
  const oldText = yield firstRow.innerText().catch(() => "");
44
53
  // 2. Click
45
- yield nextBtn.click();
46
- // 3. Smart Wait (Polling) - No 'expect' needed
47
- return yield waitForCondition(() => __awaiter(void 0, void 0, void 0, function* () {
54
+ console.log(`[Strategy: clickNext] Clicking next button...`);
55
+ try {
56
+ yield nextBtn.click({ timeout: 2000 });
57
+ }
58
+ catch (e) {
59
+ console.warn(`[Strategy: clickNext] Click failed (blocked or detached): ${e}`);
60
+ return false;
61
+ }
62
+ // 3. Smart Wait (Polling)
63
+ const success = yield waitForCondition(() => __awaiter(void 0, void 0, void 0, function* () {
48
64
  const newText = yield firstRow.innerText().catch(() => "");
49
65
  return newText !== oldText;
50
66
  }), timeout, page);
67
+ if (!success) {
68
+ console.warn(`[Strategy: clickNext] Warning: Table content did not change after clicking Next.`);
69
+ }
70
+ return success;
51
71
  });
52
72
  },
53
73
  /**
@@ -3,4 +3,4 @@
3
3
  * This file is generated by scripts/embed-types.js
4
4
  * It contains the raw text of types.ts to provide context for LLM prompts.
5
5
  */
6
- export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n};\n\nexport interface TableContext {\n root: Locator;\n config: Required<TableConfig>;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (Best for Cloud/QA Wolf to get clean text).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n getByRow: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\n\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n}\n";
6
+ export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n};\n\nexport interface TableContext {\n root: Locator;\n config: Required<TableConfig>;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (Best for Cloud/QA Wolf to get clean text).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n /**\n * Enable debug mode to log internal state to console.\n */\n debug?: boolean;\n /**\n * Strategy to reset the table to the first page.\n * Called when table.reset() is invoked.\n */\n onReset?: (context: TableContext) => Promise<void>;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n getByRow: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\n\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n}\n";
@@ -47,6 +47,15 @@ export interface TableConfig {
47
47
  */
48
48
  headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
49
49
  autoScroll?: boolean;
50
+ /**
51
+ * Enable debug mode to log internal state to console.
52
+ */
53
+ debug?: boolean;
54
+ /**
55
+ * Strategy to reset the table to the first page.
56
+ * Called when table.reset() is invoked.
57
+ */
58
+ onReset?: (context: TableContext) => Promise<void>;
50
59
  }
51
60
 
52
61
  export interface TableResult {
@@ -64,5 +73,15 @@ export interface TableResult {
64
73
 
65
74
  generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
66
75
  generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
76
+
77
+ /**
78
+ * Resets the table state (clears cache, flags) and invokes the onReset strategy.
79
+ */
80
+ reset: () => Promise<void>;
81
+
82
+ /**
83
+ * Scans a specific column across all pages and returns the values.
84
+ */
85
+ getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;
67
86
  }
68
87
  `;
package/dist/types.d.ts CHANGED
@@ -38,6 +38,15 @@ export interface TableConfig {
38
38
  locator: Locator;
39
39
  }) => string | Promise<string>;
40
40
  autoScroll?: boolean;
41
+ /**
42
+ * Enable debug mode to log internal state to console.
43
+ */
44
+ debug?: boolean;
45
+ /**
46
+ * Strategy to reset the table to the first page.
47
+ * Called when table.reset() is invoked.
48
+ */
49
+ onReset?: (context: TableContext) => Promise<void>;
41
50
  }
42
51
  export interface TableResult {
43
52
  getHeaders: () => Promise<string[]>;
@@ -56,4 +65,15 @@ export interface TableResult {
56
65
  } & T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
57
66
  generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
58
67
  generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
68
+ /**
69
+ * Resets the table state (clears cache, flags) and invokes the onReset strategy.
70
+ */
71
+ reset: () => Promise<void>;
72
+ /**
73
+ * Scans a specific column across all pages and returns the values.
74
+ */
75
+ getColumnValues: <V = string>(column: string, options?: {
76
+ mapper?: (cell: Locator) => Promise<V> | V;
77
+ maxPages?: number;
78
+ }) => Promise<V[]>;
59
79
  }
package/dist/useTable.js CHANGED
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useTable = void 0;
13
13
  const typeContext_1 = require("./typeContext");
14
14
  const useTable = (rootLocator, configOptions = {}) => {
15
- 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 }, configOptions);
15
+ 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);
16
16
  const resolve = (item, parent) => {
17
17
  if (typeof item === 'string')
18
18
  return parent.locator(item);
@@ -20,10 +20,17 @@ const useTable = (rootLocator, configOptions = {}) => {
20
20
  return item(parent);
21
21
  return item;
22
22
  };
23
+ // Internal State
23
24
  let _headerMap = null;
25
+ let _hasPaginated = false;
26
+ const logDebug = (msg) => {
27
+ if (config.debug)
28
+ console.log(`šŸ”Ž [SmartTable Debug] ${msg}`);
29
+ };
24
30
  const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
25
31
  if (_headerMap)
26
32
  return _headerMap;
33
+ logDebug('Mapping headers...');
27
34
  if (config.autoScroll) {
28
35
  try {
29
36
  yield rootLocator.scrollIntoViewIfNeeded({ timeout: 1000 });
@@ -37,7 +44,7 @@ const useTable = (rootLocator, configOptions = {}) => {
37
44
  catch (e) { /* Ignore hydration */ }
38
45
  // 1. Fetch data efficiently
39
46
  const texts = yield headerLoc.allInnerTexts();
40
- const locators = yield headerLoc.all(); // Need specific locators for the transformer
47
+ const locators = yield headerLoc.all();
41
48
  // 2. Map Headers (Async)
42
49
  const entries = yield Promise.all(texts.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
43
50
  let text = t.trim() || `__col_${i}`;
@@ -51,6 +58,7 @@ const useTable = (rootLocator, configOptions = {}) => {
51
58
  return [text, i];
52
59
  })));
53
60
  _headerMap = new Map(entries);
61
+ logDebug(`Mapped ${entries.length} columns: ${JSON.stringify(entries.map(e => e[0]))}`);
54
62
  return _headerMap;
55
63
  });
56
64
  const _makeSmart = (rowLocator, map) => {
@@ -99,15 +107,18 @@ const useTable = (rootLocator, configOptions = {}) => {
99
107
  const map = yield _getMap();
100
108
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
101
109
  let currentPage = 1;
110
+ logDebug(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
102
111
  while (true) {
103
112
  const allRows = resolve(config.rowSelector, rootLocator);
104
113
  const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
105
114
  const count = yield matchedRows.count();
115
+ logDebug(`Page ${currentPage}: Found ${count} matches.`);
106
116
  if (count > 1)
107
117
  throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
108
118
  if (count === 1)
109
119
  return matchedRows.first();
110
120
  if (currentPage < effectiveMaxPages) {
121
+ logDebug(`Page ${currentPage}: Not found. Attempting pagination...`);
111
122
  const context = {
112
123
  root: rootLocator,
113
124
  config: config,
@@ -116,9 +127,16 @@ const useTable = (rootLocator, configOptions = {}) => {
116
127
  };
117
128
  const didLoadMore = yield config.pagination(context);
118
129
  if (didLoadMore) {
130
+ _hasPaginated = true;
119
131
  currentPage++;
120
132
  continue;
121
133
  }
134
+ else {
135
+ logDebug(`Page ${currentPage}: Pagination failed (end of data).`);
136
+ }
137
+ }
138
+ if (_hasPaginated) {
139
+ 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.`);
122
140
  }
123
141
  return null;
124
142
  }
@@ -135,6 +153,34 @@ const useTable = (rootLocator, configOptions = {}) => {
135
153
  }
136
154
  console.log(finalPrompt);
137
155
  });
156
+ // Helper to extract clean HTML for prompts
157
+ const _getCleanHtml = (loc) => __awaiter(void 0, void 0, void 0, function* () {
158
+ return loc.evaluate((el) => {
159
+ const clone = el.cloneNode(true);
160
+ // 1. Remove Heavy/Useless Elements
161
+ const removeSelectors = 'script, style, svg, path, circle, rect, noscript, [hidden]';
162
+ clone.querySelectorAll(removeSelectors).forEach(n => n.remove());
163
+ // 2. Clean Attributes
164
+ const walker = document.createTreeWalker(clone, NodeFilter.SHOW_ELEMENT);
165
+ let currentNode = walker.currentNode;
166
+ while (currentNode) {
167
+ currentNode.removeAttribute('style'); // Inline styles are noise
168
+ currentNode.removeAttribute('data-reactid');
169
+ // 3. Condense Tailwind Classes (Heuristic)
170
+ // If class string is very long (>50 chars), keep the first few tokens and truncate.
171
+ // This preserves "MuiRow" but cuts "text-sm p-4 hover:bg-gray-50 ..."
172
+ const cls = currentNode.getAttribute('class');
173
+ if (cls && cls.length > 80) {
174
+ const tokens = cls.split(' ');
175
+ if (tokens.length > 5) {
176
+ currentNode.setAttribute('class', tokens.slice(0, 4).join(' ') + ' ...');
177
+ }
178
+ }
179
+ currentNode = walker.nextNode();
180
+ }
181
+ return clone.outerHTML;
182
+ });
183
+ });
138
184
  return {
139
185
  getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
140
186
  getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
@@ -144,6 +190,52 @@ const useTable = (rootLocator, configOptions = {}) => {
144
190
  throw new Error(`Column '${columnName}' not found.`);
145
191
  return resolve(config.headerSelector, rootLocator).nth(idx);
146
192
  }),
193
+ reset: () => __awaiter(void 0, void 0, void 0, function* () {
194
+ logDebug("Resetting table...");
195
+ const context = {
196
+ root: rootLocator,
197
+ config: config,
198
+ page: rootLocator.page(),
199
+ resolve: resolve
200
+ };
201
+ yield config.onReset(context);
202
+ _hasPaginated = false;
203
+ _headerMap = null;
204
+ logDebug("Table reset complete.");
205
+ }),
206
+ getColumnValues: (column, options) => __awaiter(void 0, void 0, void 0, function* () {
207
+ var _a, _b;
208
+ const map = yield _getMap();
209
+ const colIdx = map.get(column);
210
+ if (colIdx === undefined)
211
+ throw new Error(`Column '${column}' not found.`);
212
+ const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
213
+ const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
214
+ let currentPage = 1;
215
+ const results = [];
216
+ logDebug(`Getting column values for '${column}' (Pages: ${effectiveMaxPages})`);
217
+ while (true) {
218
+ const rows = yield resolve(config.rowSelector, rootLocator).all();
219
+ for (const row of rows) {
220
+ const cell = typeof config.cellSelector === 'string'
221
+ ? row.locator(config.cellSelector).nth(colIdx)
222
+ : resolve(config.cellSelector, row).nth(colIdx);
223
+ results.push(yield mapper(cell));
224
+ }
225
+ if (currentPage < effectiveMaxPages) {
226
+ const context = {
227
+ root: rootLocator, config, page: rootLocator.page(), resolve
228
+ };
229
+ if (yield config.pagination(context)) {
230
+ _hasPaginated = true;
231
+ currentPage++;
232
+ continue;
233
+ }
234
+ }
235
+ break;
236
+ }
237
+ return results;
238
+ }),
147
239
  getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
148
240
  let row = yield _findRowLocator(filters, options);
149
241
  if (!row) {
@@ -169,17 +261,51 @@ const useTable = (rootLocator, configOptions = {}) => {
169
261
  return smartRows;
170
262
  }),
171
263
  generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
172
- const html = yield rootLocator.evaluate((el) => el.outerHTML);
264
+ const html = yield _getCleanHtml(rootLocator);
173
265
  const separator = "=".repeat(50);
174
- 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, 5000)} ...\n\`\`\`\n${separator}\n`;
266
+ 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`;
175
267
  yield _handlePrompt('Smart Table Config', content, options);
176
268
  }),
177
269
  generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
178
270
  const container = rootLocator.locator('xpath=..');
179
- const html = yield container.evaluate((el) => el.outerHTML);
180
- const content = `\n==================================================\nšŸ¤– COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY šŸ¤–\n==================================================\nI need a custom Pagination Strategy for 'playwright-smart-table'.\nContainer HTML:\n\`\`\`html\n${html.substring(0, 5000)} ...\n\`\`\`\n`;
271
+ const html = yield _getCleanHtml(container);
272
+ const content = `\n==================================================\nšŸ¤– COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY šŸ¤–\n==================================================\nI need a custom Pagination Strategy for 'playwright-smart-table'.\nContainer HTML:\n\`\`\`html\n${html.substring(0, 10000)} ...\n\`\`\`\n`;
181
273
  yield _handlePrompt('Smart Table Strategy', content, options);
182
- })
274
+ }),
275
+ /* * 🚧 ROADMAP (v2.2) 🚧
276
+ * The following features are planned. Implementations are tentative.
277
+ * DO NOT DELETE THIS SECTION UNTIL IMPLEMENTED OR REMOVED.
278
+ * THIS IS BEING USED TO TRACK FUTURE DEVELOPMENT.
279
+ */
280
+ // __roadmap__fill: async (data: Record<string, any>) => {
281
+ // /* // * Goal: Fill a row with data intelligently.
282
+ // * Priority: Medium
283
+ // * Challenge: Handling different input types (select, checkbox, custom divs) blindly.
284
+ // */
285
+ // // const row = ... get row context ...
286
+ // // for (const [col, val] of Object.entries(data)) {
287
+ // // const cell = row.getCell(col);
288
+ // // const input = cell.locator('input, select, [role="checkbox"]');
289
+ // // if (await input.count() > 1) console.warn("Ambiguous input");
290
+ // // // Heuristics go here...
291
+ // // }
292
+ // // Note: Maybe we could pass the locator in the options for more control.
293
+ // },
294
+ // __roadmap__auditPages: async (options: { maxPages: number, audit: (rows: SmartRow[], page: number) => Promise<void> }) => {
295
+ // /*
296
+ // * Goal: Walk through pages and run a verification function on every page.
297
+ // * Priority: Low (Specific use case)
298
+ // * Logic:
299
+ // * let page = 1;
300
+ // * while (page <= options.maxPages) {
301
+ // * const rows = await getAllRows();
302
+ // * await options.audit(rows, page);
303
+ // * if (!await pagination(ctx)) break;
304
+ // * page++;
305
+ // * }
306
+ // */
307
+ // // Note: Maybe make is possible to skip several pages at once if the pagination strategy supports it.
308
+ // }
183
309
  };
184
310
  };
185
311
  exports.useTable = useTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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",
@@ -13,9 +13,11 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "generate-types": "node scripts/embed-types.mjs",
16
- "build": "npm run generate-types && tsc",
16
+ "generate-docs": "node scripts/generate-readme.mjs",
17
+ "build": "npm run generate-types && npm run generate-docs && tsc",
17
18
  "prepublishOnly": "npm run build",
18
- "test": "npx playwright test"
19
+ "test": "npx playwright test",
20
+ "prepare": "husky install"
19
21
  },
20
22
  "keywords": [
21
23
  "playwright",
@@ -37,6 +39,7 @@
37
39
  "@playwright/test": "^1.50.0",
38
40
  "@types/node": "^20.0.0",
39
41
  "ts-node": "^10.9.0",
40
- "typescript": "^5.0.0"
42
+ "typescript": "^5.0.0",
43
+ "husky": "^8.0.0"
41
44
  }
42
45
  }