@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 +105 -81
- package/dist/strategies/index.js +25 -5
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +19 -0
- package/dist/types.d.ts +20 -0
- package/dist/useTable.js +133 -7
- package/package.json +7 -4
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
|
-
|
|
17
|
-
|
|
18
|
-
For standard tables (<table>, <tr>, <td>), no configuration is needed.
|
|
15
|
+
The Standard HTML Table
|
|
19
16
|
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
+
await expect(row.getCell('Position')).toHaveText('Accountant');
|
|
30
|
+
```
|
|
31
|
+
<!-- /embed: quick-start -->
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Complex Grids (Material UI / AG-Grid / Divs)
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
For modern React grids, simply override the selectors and define a pagination strategy.
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
š Resetting State
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
111
|
-
filter: {
|
|
161
|
+
const tokyoUsers = await table.getAllRows({
|
|
162
|
+
filter: { Office: 'Tokyo' }
|
|
112
163
|
});
|
|
113
|
-
expect(
|
|
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: "
|
|
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
|
-
|
|
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), '
|
|
174
|
-
await table.generateConfigPrompt({ output: '
|
|
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' });
|
package/dist/strategies/index.js
CHANGED
|
@@ -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
|
-
//
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
/**
|
package/dist/typeContext.d.ts
CHANGED
|
@@ -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";
|
package/dist/typeContext.js
CHANGED
|
@@ -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();
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|