@rickcedwhat/playwright-smart-table 2.0.1 ā 2.0.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 +89 -60
- package/dist/useTable.js +31 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install @rickcedwhat/playwright-smart-table
|
|
|
11
11
|
|
|
12
12
|
Requires @playwright/test as a peer dependency.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
ā” Quick Start
|
|
15
15
|
|
|
16
16
|
1. The Standard HTML Table
|
|
17
17
|
|
|
@@ -23,17 +23,17 @@ import { useTable } from '@rickcedwhat/playwright-smart-table';
|
|
|
23
23
|
test('Verify User Email', async ({ page }) => {
|
|
24
24
|
const table = useTable(page.locator('#users-table'));
|
|
25
25
|
|
|
26
|
-
// šŖ
|
|
26
|
+
// šŖ Finds the row with Name="Alice", then gets the Email cell.
|
|
27
27
|
// If Alice is on Page 2, it handles pagination automatically.
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
).toHaveText('alice@example.com');
|
|
28
|
+
const row = await table.getByRow({ Name: 'Alice' });
|
|
29
|
+
|
|
30
|
+
await expect(row.getCell('Email')).toHaveText('alice@example.com');
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
2. Complex Grids (Material UI / AG-Grid / Divs)
|
|
35
35
|
|
|
36
|
-
For modern React grids
|
|
36
|
+
For modern React grids, simply override the selectors and define a pagination strategy.
|
|
37
37
|
|
|
38
38
|
import { useTable, TableStrategies } from '@rickcedwhat/playwright-smart-table';
|
|
39
39
|
|
|
@@ -43,110 +43,139 @@ const table = useTable(page.locator('.MuiDataGrid-root'), {
|
|
|
43
43
|
cellSelector: '.MuiDataGrid-cell',
|
|
44
44
|
// Strategy: Tell it how to find the next page
|
|
45
45
|
pagination: TableStrategies.clickNext(
|
|
46
|
-
|
|
46
|
+
// Use 'page' to find buttons outside the table container
|
|
47
|
+
(root) => root.page().getByRole('button', { name: 'Go to next page' })
|
|
47
48
|
)
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
š§ SmartRow Pattern
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
The core power of this library is the SmartRow.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
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.
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
getCell(columnName)
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Instead of writing brittle nth-child selectors, ask for the column by name.
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// ā
Good: Resilient to column reordering
|
|
63
|
+
await row.getCell('Email').click();
|
|
62
64
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
+
// ā Bad: Brittle
|
|
66
|
+
await row.locator('td').nth(2).click();
|
|
65
67
|
|
|
66
|
-
// Locator Function (More Robust)
|
|
67
|
-
pagination: TableStrategies.clickNext((root) => root.getByRole('button', { name: 'Next' }))
|
|
68
68
|
|
|
69
|
+
toJSON()
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
Extracts the entire row's data into a clean key-value object.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
const data = await row.toJSON();
|
|
74
|
+
console.log(data);
|
|
75
|
+
// { Name: "Alice", Role: "Admin", Status: "Active" }
|
|
73
76
|
|
|
74
|
-
Behavior: Aggressively scrolls the container/window to the bottom -> Waits for row count to increase.
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
š API Reference
|
|
77
79
|
|
|
80
|
+
getByRow(filters, options?)
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
Strict Retrieval. Finds a single specific row.
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
Throws Error if >1 rows match (ambiguous query).
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
Returns Sentinel if 0 rows match (allows not.toBeVisible() assertions).
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
Auto-Paginates if the row isn't found on the current page.
|
|
86
89
|
|
|
90
|
+
// Find a row where Name is "Alice" AND Role is "Admin"
|
|
91
|
+
const row = await table.getByRow({ Name: "Alice", Role: "Admin" });
|
|
92
|
+
await expect(row).toBeVisible();
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
// Assert it does NOT exist
|
|
95
|
+
await expect(await table.getByRow({ Name: "Ghost" })).not.toBeVisible();
|
|
89
96
|
|
|
90
|
-
getByRow(filters, options?)
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
getAllRows(options?)
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
Inclusive Retrieval. Gets a collection of rows.
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
Returns: Array of SmartRow objects.
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
Best for: Checking existence ("at least one") or validating sort order.
|
|
105
|
+
|
|
106
|
+
// 1. Get ALL rows on the current page
|
|
107
|
+
const allRows = await table.getAllRows();
|
|
108
|
+
|
|
109
|
+
// 2. Get subset of rows (Filtering)
|
|
110
|
+
const activeUsers = await table.getAllRows({
|
|
111
|
+
filter: { Status: 'Active' }
|
|
112
|
+
});
|
|
113
|
+
expect(activeUsers.length).toBeGreaterThan(0); // "At least one active user"
|
|
101
114
|
|
|
115
|
+
// 3. Dump data to JSON
|
|
116
|
+
const data = await table.getAllRows({ asJSON: true });
|
|
117
|
+
console.log(data); // [{ Name: "Alice", Status: "Active" }, ...]
|
|
102
118
|
|
|
103
|
-
getByCell(filters, targetColumn)
|
|
104
119
|
|
|
105
|
-
|
|
120
|
+
š§© Pagination Strategies
|
|
106
121
|
|
|
107
|
-
|
|
122
|
+
This library uses the Strategy Pattern to handle navigation. You can use the built-in strategies or write your own.
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
await table.getByCell({ Name: "Alice" }, "Actions").getByRole('button').click();
|
|
124
|
+
Built-in Strategies
|
|
111
125
|
|
|
126
|
+
clickNext(selector)
|
|
127
|
+
Best for standard tables (Datatables, lists). Clicks a button and waits for data to change.
|
|
112
128
|
|
|
113
|
-
|
|
129
|
+
pagination: TableStrategies.clickNext((root) =>
|
|
130
|
+
root.page().getByRole('button', { name: 'Next' })
|
|
131
|
+
)
|
|
114
132
|
|
|
115
|
-
Returns a POJO (Plain Old JavaScript Object) of the row data. Useful for debugging or strict data assertions.
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
infiniteScroll()
|
|
135
|
+
Best for Virtualized Grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
|
|
136
|
+
|
|
137
|
+
pagination: TableStrategies.infiniteScroll()
|
|
120
138
|
|
|
121
139
|
|
|
122
|
-
|
|
140
|
+
clickLoadMore(selector)
|
|
141
|
+
Best for "Load More" buttons. Clicks and waits for row count to increase.
|
|
123
142
|
|
|
124
|
-
|
|
143
|
+
Writing Custom Strategies
|
|
125
144
|
|
|
126
|
-
|
|
127
|
-
expect(allRows[0].Name).toBe("Alice"); // Verify sort order
|
|
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).
|
|
128
146
|
|
|
147
|
+
import { PaginationStrategy } from '@rickcedwhat/playwright-smart-table';
|
|
129
148
|
|
|
130
|
-
|
|
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;
|
|
131
153
|
|
|
132
|
-
|
|
154
|
+
// 2. Perform Navigation
|
|
155
|
+
await nextBtn.click();
|
|
133
156
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
await
|
|
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
|
+
|
|
164
|
+
|
|
165
|
+
š ļø Developer Tools
|
|
137
166
|
|
|
138
|
-
|
|
139
|
-
await table.generateStrategyPrompt();
|
|
167
|
+
Don't waste time writing selectors manually. Use the generator tools to create your config.
|
|
140
168
|
|
|
169
|
+
generateConfigPrompt(options?)
|
|
141
170
|
|
|
142
|
-
|
|
171
|
+
Prints a prompt you can paste into ChatGPT/Gemini to generate the TableConfig for your specific HTML.
|
|
143
172
|
|
|
144
|
-
|
|
173
|
+
// Options: 'console' (default), 'report' (Playwright HTML Report), 'file'
|
|
174
|
+
await table.generateConfigPrompt({ output: 'report' });
|
|
145
175
|
|
|
146
|
-
1.x.x: No breaking changes to the useTable signature.
|
|
147
176
|
|
|
148
|
-
|
|
177
|
+
generateStrategyPrompt(options?)
|
|
149
178
|
|
|
150
|
-
|
|
179
|
+
Prints a prompt to help you write a custom Pagination Strategy.
|
|
151
180
|
|
|
152
|
-
|
|
181
|
+
await table.generateStrategyPrompt({ output: 'console' });
|
package/dist/useTable.js
CHANGED
|
@@ -25,7 +25,6 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
25
25
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
26
|
if (_headerMap)
|
|
27
27
|
return _headerMap;
|
|
28
|
-
// ā
New Feature: Auto-Scroll on first interaction
|
|
29
28
|
if (config.autoScroll) {
|
|
30
29
|
yield rootLocator.scrollIntoViewIfNeeded();
|
|
31
30
|
}
|
|
@@ -69,7 +68,6 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
69
68
|
});
|
|
70
69
|
return smart;
|
|
71
70
|
};
|
|
72
|
-
// ā»ļø HELPER: Centralized logic to filter a row locator
|
|
73
71
|
const _applyFilters = (baseRows, filters, map, exact) => {
|
|
74
72
|
let filtered = baseRows;
|
|
75
73
|
const page = rootLocator.page();
|
|
@@ -79,59 +77,34 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
79
77
|
throw new Error(`Column '${colName}' not found.`);
|
|
80
78
|
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
81
79
|
const cellTemplate = resolve(config.cellSelector, page);
|
|
82
|
-
// Filter the TRs that contain the matching cell at the specific index
|
|
83
80
|
filtered = filtered.filter({
|
|
84
81
|
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
85
82
|
});
|
|
86
83
|
}
|
|
87
84
|
return filtered;
|
|
88
85
|
};
|
|
89
|
-
const _handlePrompt = (promptName_1, content_1, ...args_1) => __awaiter(void 0, [promptName_1, content_1, ...args_1], void 0, function* (promptName, content, options = {}) {
|
|
90
|
-
const { output = 'console', includeTypes = true } = options; // Default includeTypes to true
|
|
91
|
-
let finalPrompt = content;
|
|
92
|
-
if (includeTypes) {
|
|
93
|
-
// ā
Inject the dynamic TYPE_CONTEXT
|
|
94
|
-
finalPrompt += `\n\nš Useful TypeScript Definitions š\n\`\`\`typescript\n${typeContext_1.TYPE_CONTEXT}\n\`\`\`\n`;
|
|
95
|
-
}
|
|
96
|
-
if (output === 'console') {
|
|
97
|
-
console.log(finalPrompt);
|
|
98
|
-
}
|
|
99
|
-
else if (output === 'report') {
|
|
100
|
-
if (test_1.test.info()) {
|
|
101
|
-
yield test_1.test.info().attach(promptName, {
|
|
102
|
-
body: finalPrompt,
|
|
103
|
-
contentType: 'text/markdown'
|
|
104
|
-
});
|
|
105
|
-
console.log(`ā
Attached '${promptName}' to Playwright Report.`);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
console.warn('ā ļø Cannot attach to report: No active test info found.');
|
|
109
|
-
console.log(finalPrompt);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// ... (file output logic) ...
|
|
113
|
-
});
|
|
114
86
|
const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
|
|
115
87
|
var _a;
|
|
116
88
|
const map = yield _getMap();
|
|
117
89
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
118
90
|
let currentPage = 1;
|
|
119
91
|
while (true) {
|
|
120
|
-
// 1. Get all rows
|
|
121
92
|
const allRows = resolve(config.rowSelector, rootLocator);
|
|
122
|
-
// 2. Apply filters using helper
|
|
123
93
|
const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
|
|
124
|
-
// 3. Check Count
|
|
125
94
|
const count = yield matchedRows.count();
|
|
126
95
|
if (count > 1)
|
|
127
96
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
128
97
|
if (count === 1)
|
|
129
98
|
return matchedRows.first();
|
|
130
|
-
// 4. Pagination Logic (unchanged)
|
|
131
99
|
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
100
|
+
const context = {
|
|
101
|
+
root: rootLocator,
|
|
102
|
+
config: config,
|
|
103
|
+
page: rootLocator.page(),
|
|
104
|
+
resolve: resolve
|
|
105
|
+
};
|
|
106
|
+
const didLoadMore = yield config.pagination(context);
|
|
107
|
+
if (didLoadMore) {
|
|
135
108
|
currentPage++;
|
|
136
109
|
continue;
|
|
137
110
|
}
|
|
@@ -139,6 +112,29 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
139
112
|
return null;
|
|
140
113
|
}
|
|
141
114
|
});
|
|
115
|
+
const _handlePrompt = (promptName_1, content_1, ...args_1) => __awaiter(void 0, [promptName_1, content_1, ...args_1], void 0, function* (promptName, content, options = {}) {
|
|
116
|
+
const { output = 'console', includeTypes = true } = options;
|
|
117
|
+
let finalPrompt = content;
|
|
118
|
+
if (includeTypes) {
|
|
119
|
+
finalPrompt += `\n\nš Useful TypeScript Definitions š\n\`\`\`typescript\n${typeContext_1.TYPE_CONTEXT}\n\`\`\`\n`;
|
|
120
|
+
}
|
|
121
|
+
if (output === 'console') {
|
|
122
|
+
console.log(finalPrompt);
|
|
123
|
+
}
|
|
124
|
+
else if (output === 'report') {
|
|
125
|
+
if (test_1.test.info()) {
|
|
126
|
+
yield test_1.test.info().attach(promptName, {
|
|
127
|
+
body: finalPrompt,
|
|
128
|
+
contentType: 'text/markdown'
|
|
129
|
+
});
|
|
130
|
+
console.log(`ā
Attached '${promptName}' to Playwright Report.`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.warn('ā ļø Cannot attach to report: No active test info found. Logging to console instead.');
|
|
134
|
+
console.log(finalPrompt);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
142
138
|
return {
|
|
143
139
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
144
140
|
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -150,14 +146,11 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
150
146
|
}),
|
|
151
147
|
getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
148
|
let row = yield _findRowLocator(filters, options);
|
|
153
|
-
// ā
FIX: Sentinel Logic for negative assertions (expect(row).not.toBeVisible())
|
|
154
149
|
if (!row) {
|
|
155
150
|
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
156
151
|
}
|
|
157
152
|
const smartRow = _makeSmart(row, yield _getMap());
|
|
158
153
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
159
|
-
// If row doesn't exist, toJSON() returns empty object or throws?
|
|
160
|
-
// For safety, let's let it run naturally (it will likely return empty strings)
|
|
161
154
|
return smartRow.toJSON();
|
|
162
155
|
}
|
|
163
156
|
return smartRow;
|
|
@@ -165,11 +158,9 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
165
158
|
getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
166
159
|
const map = yield _getMap();
|
|
167
160
|
let rowLocators = resolve(config.rowSelector, rootLocator);
|
|
168
|
-
// ā
NEW: Apply filters if they exist
|
|
169
161
|
if (options === null || options === void 0 ? void 0 : options.filter) {
|
|
170
162
|
rowLocators = _applyFilters(rowLocators, options.filter, map, options.exact || false);
|
|
171
163
|
}
|
|
172
|
-
// Convert Locator to array of Locators
|
|
173
164
|
const rows = yield rowLocators.all();
|
|
174
165
|
const smartRows = rows.map(loc => _makeSmart(loc, map));
|
|
175
166
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "A smart table utility for Playwright with built-in pagination strategies that are fully extensible.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|