@rickcedwhat/playwright-smart-table 2.0.9 → 2.1.1
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 +61 -77
- package/dist/strategies/index.js +25 -5
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +7 -1
- package/dist/types.d.ts +11 -1
- package/dist/useTable.js +14 -5
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -15,39 +15,46 @@ Requires @playwright/test as a peer dependency.
|
|
|
15
15
|
|
|
16
16
|
1. The Standard HTML Table
|
|
17
17
|
|
|
18
|
-
For standard tables (<table>, <tr>, <td>), no configuration is needed.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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');
|
|
18
|
+
For standard tables (<table>, <tr>, <td>), no configuration is needed (defaults work for most standard HTML tables).
|
|
19
|
+
|
|
20
|
+
<!-- embed: quick-start -->
|
|
21
|
+
```typescript
|
|
22
|
+
const table = useTable(page.locator('#example'), {
|
|
23
|
+
headerSelector: 'thead th' // Override for this specific site
|
|
31
24
|
});
|
|
32
25
|
|
|
26
|
+
// 🪄 Finds the row with Name="Airi Satou", then gets the Position cell.
|
|
27
|
+
// If Airi is on Page 2, it handles pagination automatically.
|
|
28
|
+
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
29
|
+
|
|
30
|
+
await expect(row.getCell('Position')).toHaveText('Accountant');
|
|
31
|
+
```
|
|
32
|
+
<!-- /embed: quick-start -->
|
|
33
33
|
|
|
34
34
|
2. Complex Grids (Material UI / AG-Grid / Divs)
|
|
35
35
|
|
|
36
36
|
For modern React grids, simply override the selectors and define a pagination strategy.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const table = useTable(page.locator('
|
|
41
|
-
rowSelector: '
|
|
42
|
-
headerSelector: '
|
|
43
|
-
cellSelector: '
|
|
38
|
+
<!-- embed: pagination -->
|
|
39
|
+
```typescript
|
|
40
|
+
const table = useTable(page.locator('#example'), {
|
|
41
|
+
rowSelector: 'tbody tr',
|
|
42
|
+
headerSelector: 'thead th',
|
|
43
|
+
cellSelector: 'td',
|
|
44
44
|
// Strategy: Tell it how to find the next page
|
|
45
|
-
pagination: TableStrategies.clickNext(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
pagination: TableStrategies.clickNext(() =>
|
|
46
|
+
page.getByRole('link', { name: 'Next' })
|
|
47
|
+
),
|
|
48
|
+
maxPages: 5 // Allow scanning up to 5 pages
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// ✅ Verify Colleen is NOT visible initially
|
|
52
|
+
await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
|
|
53
|
+
|
|
54
|
+
await expect(await table.getByRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
55
|
+
// NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
|
|
56
|
+
```
|
|
57
|
+
<!-- /embed: pagination -->
|
|
51
58
|
|
|
52
59
|
🧠 SmartRow Pattern
|
|
53
60
|
|
|
@@ -55,25 +62,21 @@ The core power of this library is the SmartRow.
|
|
|
55
62
|
|
|
56
63
|
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
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
<!-- embed: smart-row -->
|
|
66
|
+
```typescript
|
|
67
|
+
// 1. Get SmartRow via getByRow
|
|
68
|
+
const row = await table.getByRow({ Name: 'Airi Satou' });
|
|
61
69
|
|
|
70
|
+
// 2. Interact with cell (No more getByCell needed!)
|
|
62
71
|
// ✅ Good: Resilient to column reordering
|
|
63
|
-
await row.getCell('
|
|
64
|
-
|
|
65
|
-
// ❌ Bad: Brittle
|
|
66
|
-
await row.locator('td').nth(2).click();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
toJSON()
|
|
70
|
-
|
|
71
|
-
Extracts the entire row's data into a clean key-value object.
|
|
72
|
+
await row.getCell('Position').click();
|
|
72
73
|
|
|
74
|
+
// 3. Dump data from row
|
|
73
75
|
const data = await row.toJSON();
|
|
74
|
-
console.log(data);
|
|
75
|
-
// { Name: "
|
|
76
|
-
|
|
76
|
+
console.log(data);
|
|
77
|
+
// { Name: "Airi Satou", Position: "Accountant", ... }
|
|
78
|
+
```
|
|
79
|
+
<!-- /embed: smart-row -->
|
|
77
80
|
|
|
78
81
|
📖 API Reference
|
|
79
82
|
|
|
@@ -87,13 +90,16 @@ Returns Sentinel if 0 rows match (allows not.toBeVisible() assertions).
|
|
|
87
90
|
|
|
88
91
|
Auto-Paginates if the row isn't found on the current page.
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
<!-- embed: get-by-row -->
|
|
94
|
+
```typescript
|
|
95
|
+
// Find a row where Name is "Airi Satou" AND Office is "Tokyo"
|
|
96
|
+
const row = await table.getByRow({ Name: "Airi Satou", Office: "Tokyo" });
|
|
92
97
|
await expect(row).toBeVisible();
|
|
93
98
|
|
|
94
99
|
// Assert it does NOT exist
|
|
95
|
-
await expect(await table.getByRow({ Name: "Ghost" })).not.toBeVisible();
|
|
96
|
-
|
|
100
|
+
await expect(await table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
101
|
+
```
|
|
102
|
+
<!-- /embed: get-by-row -->
|
|
97
103
|
|
|
98
104
|
getAllRows(options?)
|
|
99
105
|
|
|
@@ -103,19 +109,22 @@ Returns: Array of SmartRow objects.
|
|
|
103
109
|
|
|
104
110
|
Best for: Checking existence ("at least one") or validating sort order.
|
|
105
111
|
|
|
112
|
+
<!-- embed: get-all-rows -->
|
|
113
|
+
```typescript
|
|
106
114
|
// 1. Get ALL rows on the current page
|
|
107
115
|
const allRows = await table.getAllRows();
|
|
108
116
|
|
|
109
117
|
// 2. Get subset of rows (Filtering)
|
|
110
|
-
const
|
|
111
|
-
filter: {
|
|
118
|
+
const tokyoUsers = await table.getAllRows({
|
|
119
|
+
filter: { Office: 'Tokyo' }
|
|
112
120
|
});
|
|
113
|
-
expect(
|
|
121
|
+
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
114
122
|
|
|
115
123
|
// 3. Dump data to JSON
|
|
116
124
|
const data = await table.getAllRows({ asJSON: true });
|
|
117
|
-
console.log(data); // [{ Name: "
|
|
118
|
-
|
|
125
|
+
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
126
|
+
```
|
|
127
|
+
<!-- /embed: get-all-rows -->
|
|
119
128
|
|
|
120
129
|
🧩 Pagination Strategies
|
|
121
130
|
|
|
@@ -123,44 +132,19 @@ This library uses the Strategy Pattern to handle navigation. You can use the bui
|
|
|
123
132
|
|
|
124
133
|
Built-in Strategies
|
|
125
134
|
|
|
126
|
-
clickNext(selector)
|
|
127
|
-
Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
|
|
135
|
+
clickNext(selector) Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
|
|
128
136
|
|
|
129
137
|
pagination: TableStrategies.clickNext((root) =>
|
|
130
138
|
root.page().getByRole('button', { name: 'Next' })
|
|
131
139
|
)
|
|
132
140
|
|
|
133
141
|
|
|
134
|
-
infiniteScroll()
|
|
135
|
-
Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
142
|
+
infiniteScroll() Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
136
143
|
|
|
137
144
|
pagination: TableStrategies.infiniteScroll()
|
|
138
145
|
|
|
139
146
|
|
|
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
|
-
|
|
147
|
+
clickLoadMore(selector) Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
164
148
|
|
|
165
149
|
🛠️ Developer Tools
|
|
166
150
|
|
|
@@ -170,8 +154,8 @@ generateConfigPrompt(options?)
|
|
|
170
154
|
|
|
171
155
|
Prints a prompt you can paste into ChatGPT/Gemini to generate the TableConfig for your specific HTML.
|
|
172
156
|
|
|
173
|
-
// Options: 'console' (default), '
|
|
174
|
-
await table.generateConfigPrompt({ output: '
|
|
157
|
+
// Options: 'console' (default), 'error' (Throw error to see prompt in trace/cloud)
|
|
158
|
+
await table.generateConfigPrompt({ output: 'console' });
|
|
175
159
|
|
|
176
160
|
|
|
177
161
|
generateStrategyPrompt(options?)
|
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 headerTransformer?: (text: string, index: number) => string
|
|
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";
|
package/dist/typeContext.js
CHANGED
|
@@ -39,7 +39,13 @@ export interface TableConfig {
|
|
|
39
39
|
cellSelector?: Selector;
|
|
40
40
|
pagination?: PaginationStrategy;
|
|
41
41
|
maxPages?: number;
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Hook to rename columns dynamically.
|
|
44
|
+
* * @param args.text - The default innerText of the header.
|
|
45
|
+
* @param args.index - The column index.
|
|
46
|
+
* @param args.locator - The specific header cell locator.
|
|
47
|
+
*/
|
|
48
|
+
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
43
49
|
autoScroll?: boolean;
|
|
44
50
|
}
|
|
45
51
|
|
package/dist/types.d.ts
CHANGED
|
@@ -26,7 +26,17 @@ export interface TableConfig {
|
|
|
26
26
|
cellSelector?: Selector;
|
|
27
27
|
pagination?: PaginationStrategy;
|
|
28
28
|
maxPages?: number;
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Hook to rename columns dynamically.
|
|
31
|
+
* * @param args.text - The default innerText of the header.
|
|
32
|
+
* @param args.index - The column index.
|
|
33
|
+
* @param args.locator - The specific header cell locator.
|
|
34
|
+
*/
|
|
35
|
+
headerTransformer?: (args: {
|
|
36
|
+
text: string;
|
|
37
|
+
index: number;
|
|
38
|
+
locator: Locator;
|
|
39
|
+
}) => string | Promise<string>;
|
|
30
40
|
autoScroll?: boolean;
|
|
31
41
|
}
|
|
32
42
|
export interface TableResult {
|
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) => 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 }, configOptions);
|
|
16
16
|
const resolve = (item, parent) => {
|
|
17
17
|
if (typeof item === 'string')
|
|
18
18
|
return parent.locator(item);
|
|
@@ -35,13 +35,22 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
35
35
|
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
36
36
|
}
|
|
37
37
|
catch (e) { /* Ignore hydration */ }
|
|
38
|
+
// 1. Fetch data efficiently
|
|
38
39
|
const texts = yield headerLoc.allInnerTexts();
|
|
39
|
-
|
|
40
|
+
const locators = yield headerLoc.all(); // Need specific locators for the transformer
|
|
41
|
+
// 2. Map Headers (Async)
|
|
42
|
+
const entries = yield Promise.all(texts.map((t, i) => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
43
|
let text = t.trim() || `__col_${i}`;
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
if (config.headerTransformer) {
|
|
45
|
+
text = yield config.headerTransformer({
|
|
46
|
+
text,
|
|
47
|
+
index: i,
|
|
48
|
+
locator: locators[i]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
43
51
|
return [text, i];
|
|
44
|
-
}));
|
|
52
|
+
})));
|
|
53
|
+
_headerMap = new Map(entries);
|
|
45
54
|
return _headerMap;
|
|
46
55
|
});
|
|
47
56
|
const _makeSmart = (rowLocator, map) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
}
|