@rickcedwhat/playwright-smart-table 2.0.0 ā 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/strategies/index.js +11 -9
- package/dist/typeContext.d.ts +6 -0
- package/dist/typeContext.js +100 -0
- package/dist/types.d.ts +30 -5
- package/dist/useTable.js +61 -28
- package/package.json +5 -4
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/strategies/index.js
CHANGED
|
@@ -84,16 +84,18 @@ exports.TableStrategies = {
|
|
|
84
84
|
const oldCount = yield rows.count();
|
|
85
85
|
if (oldCount === 0)
|
|
86
86
|
return false;
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
yield page.keyboard.press('End');
|
|
91
|
-
// 2. Smart Wait: Wait for row count to increase
|
|
87
|
+
// Aggressive Scroll Logic:
|
|
88
|
+
// We use expect.poll to RETRY the scroll action if the count hasn't increased.
|
|
89
|
+
// This fixes flakiness where the first scroll might be missed by the intersection observer.
|
|
92
90
|
try {
|
|
93
|
-
yield
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
yield test_1.expect.poll(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
// 1. Trigger: Scroll the last row into view
|
|
93
|
+
yield rows.last().scrollIntoViewIfNeeded();
|
|
94
|
+
// 2. Force: Press "End" to help with stubborn window-scrollers
|
|
95
|
+
yield page.keyboard.press('End');
|
|
96
|
+
// 3. Return count for assertion
|
|
97
|
+
return rows.count();
|
|
98
|
+
}), { timeout }).toBeGreaterThan(oldCount);
|
|
97
99
|
return true;
|
|
98
100
|
}
|
|
99
101
|
catch (e) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* š¤ AUTO-GENERATED FILE. DO NOT EDIT.
|
|
3
|
+
* This file is generated by scripts/embed-types.js
|
|
4
|
+
* It contains the raw text of types.ts to provide context for LLM prompts.
|
|
5
|
+
*/
|
|
6
|
+
export declare const TYPE_CONTEXT = "\n// src/types.ts\n\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\n/**\n * A function that handles pagination logic.\n * Returns true if more data was loaded (navigation occurred), false if end of data.\n */\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport interface PromptOptions {\n /** * Where to output the prompt.\n * - 'console': Logs to stdout (default).\n * - 'report': Attaches to the Playwright HTML report (best for CI/VMs).\n * - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').\n */\n output?: 'console' | 'report' | 'file';\n \n /**\n * Include TypeScript interfaces in the prompt?\n * Helps LLMs generate correct code structure.\n * Default: true\n */\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n /**\n * Strategy for handling pagination.\n * Use presets from TableStrategies or write your own.\n */\n pagination?: PaginationStrategy;\n maxPages?: number;\n /**\n * Optional hook to rename columns dynamically.\n * Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.\n */\n headerTransformer?: (text: string, index: number) => string;\n /**\n * Automatically scroll the table into view on first interaction.\n * Helps ensure the table is visible in traces/videos.\n * Default: true\n */\n autoScroll?: boolean;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /** * Find a specific row by its content.\n * Default: Returns SmartRow (Locator).\n * Option { asJSON: true }: Returns Record<string, string> (Data).\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 /** * Get all rows on the current page.\n * Can be filtered by column values.\n * * @param options.asJSON - Returns data objects instead of SmartRows\n * @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })\n * @param options.exact - Exact match for filters (default: false)\n */\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { \n filter?: Record<string, string | RegExp | number>; \n exact?: boolean \n } & 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";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TYPE_CONTEXT = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* š¤ AUTO-GENERATED FILE. DO NOT EDIT.
|
|
6
|
+
* This file is generated by scripts/embed-types.js
|
|
7
|
+
* It contains the raw text of types.ts to provide context for LLM prompts.
|
|
8
|
+
*/
|
|
9
|
+
exports.TYPE_CONTEXT = `
|
|
10
|
+
// src/types.ts
|
|
11
|
+
|
|
12
|
+
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
13
|
+
|
|
14
|
+
export type SmartRow = Locator & {
|
|
15
|
+
getCell(column: string): Locator;
|
|
16
|
+
toJSON(): Promise<Record<string, string>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface TableContext {
|
|
20
|
+
root: Locator;
|
|
21
|
+
config: Required<TableConfig>;
|
|
22
|
+
page: Page;
|
|
23
|
+
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A function that handles pagination logic.
|
|
28
|
+
* Returns true if more data was loaded (navigation occurred), false if end of data.
|
|
29
|
+
*/
|
|
30
|
+
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
31
|
+
|
|
32
|
+
export interface PromptOptions {
|
|
33
|
+
/** * Where to output the prompt.
|
|
34
|
+
* - 'console': Logs to stdout (default).
|
|
35
|
+
* - 'report': Attaches to the Playwright HTML report (best for CI/VMs).
|
|
36
|
+
* - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').
|
|
37
|
+
*/
|
|
38
|
+
output?: 'console' | 'report' | 'file';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Include TypeScript interfaces in the prompt?
|
|
42
|
+
* Helps LLMs generate correct code structure.
|
|
43
|
+
* Default: true
|
|
44
|
+
*/
|
|
45
|
+
includeTypes?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TableConfig {
|
|
49
|
+
rowSelector?: Selector;
|
|
50
|
+
headerSelector?: Selector;
|
|
51
|
+
cellSelector?: Selector;
|
|
52
|
+
/**
|
|
53
|
+
* Strategy for handling pagination.
|
|
54
|
+
* Use presets from TableStrategies or write your own.
|
|
55
|
+
*/
|
|
56
|
+
pagination?: PaginationStrategy;
|
|
57
|
+
maxPages?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Optional hook to rename columns dynamically.
|
|
60
|
+
* Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
|
|
61
|
+
*/
|
|
62
|
+
headerTransformer?: (text: string, index: number) => string;
|
|
63
|
+
/**
|
|
64
|
+
* Automatically scroll the table into view on first interaction.
|
|
65
|
+
* Helps ensure the table is visible in traces/videos.
|
|
66
|
+
* Default: true
|
|
67
|
+
*/
|
|
68
|
+
autoScroll?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TableResult {
|
|
72
|
+
getHeaders: () => Promise<string[]>;
|
|
73
|
+
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
74
|
+
|
|
75
|
+
/** * Find a specific row by its content.
|
|
76
|
+
* Default: Returns SmartRow (Locator).
|
|
77
|
+
* Option { asJSON: true }: Returns Record<string, string> (Data).
|
|
78
|
+
*/
|
|
79
|
+
getByRow: <T extends { asJSON?: boolean }>(
|
|
80
|
+
filters: Record<string, string | RegExp | number>,
|
|
81
|
+
options?: { exact?: boolean, maxPages?: number } & T
|
|
82
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
83
|
+
|
|
84
|
+
/** * Get all rows on the current page.
|
|
85
|
+
* Can be filtered by column values.
|
|
86
|
+
* * @param options.asJSON - Returns data objects instead of SmartRows
|
|
87
|
+
* @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })
|
|
88
|
+
* @param options.exact - Exact match for filters (default: false)
|
|
89
|
+
*/
|
|
90
|
+
getAllRows: <T extends { asJSON?: boolean }>(
|
|
91
|
+
options?: {
|
|
92
|
+
filter?: Record<string, string | RegExp | number>;
|
|
93
|
+
exact?: boolean
|
|
94
|
+
} & T
|
|
95
|
+
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
96
|
+
|
|
97
|
+
generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
|
|
98
|
+
generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
`;
|
package/dist/types.d.ts
CHANGED
|
@@ -15,6 +15,20 @@ export interface TableContext {
|
|
|
15
15
|
* Returns true if more data was loaded (navigation occurred), false if end of data.
|
|
16
16
|
*/
|
|
17
17
|
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
18
|
+
export interface PromptOptions {
|
|
19
|
+
/** * Where to output the prompt.
|
|
20
|
+
* - 'console': Logs to stdout (default).
|
|
21
|
+
* - 'report': Attaches to the Playwright HTML report (best for CI/VMs).
|
|
22
|
+
* - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').
|
|
23
|
+
*/
|
|
24
|
+
output?: 'console' | 'report' | 'file';
|
|
25
|
+
/**
|
|
26
|
+
* Include TypeScript interfaces in the prompt?
|
|
27
|
+
* Helps LLMs generate correct code structure.
|
|
28
|
+
* Default: true
|
|
29
|
+
*/
|
|
30
|
+
includeTypes?: boolean;
|
|
31
|
+
}
|
|
18
32
|
export interface TableConfig {
|
|
19
33
|
rowSelector?: Selector;
|
|
20
34
|
headerSelector?: Selector;
|
|
@@ -30,6 +44,12 @@ export interface TableConfig {
|
|
|
30
44
|
* Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
|
|
31
45
|
*/
|
|
32
46
|
headerTransformer?: (text: string, index: number) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Automatically scroll the table into view on first interaction.
|
|
49
|
+
* Helps ensure the table is visible in traces/videos.
|
|
50
|
+
* Default: true
|
|
51
|
+
*/
|
|
52
|
+
autoScroll?: boolean;
|
|
33
53
|
}
|
|
34
54
|
export interface TableResult {
|
|
35
55
|
getHeaders: () => Promise<string[]>;
|
|
@@ -45,12 +65,17 @@ export interface TableResult {
|
|
|
45
65
|
maxPages?: number;
|
|
46
66
|
} & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
47
67
|
/** * Get all rows on the current page.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
68
|
+
* Can be filtered by column values.
|
|
69
|
+
* * @param options.asJSON - Returns data objects instead of SmartRows
|
|
70
|
+
* @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })
|
|
71
|
+
* @param options.exact - Exact match for filters (default: false)
|
|
50
72
|
*/
|
|
51
73
|
getAllRows: <T extends {
|
|
52
74
|
asJSON?: boolean;
|
|
53
|
-
}>(options?:
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
}>(options?: {
|
|
76
|
+
filter?: Record<string, string | RegExp | number>;
|
|
77
|
+
exact?: boolean;
|
|
78
|
+
} & T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
79
|
+
generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
|
|
80
|
+
generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
|
|
56
81
|
}
|
package/dist/useTable.js
CHANGED
|
@@ -10,8 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.useTable = void 0;
|
|
13
|
+
const test_1 = require("@playwright/test");
|
|
14
|
+
const typeContext_1 = require("./typeContext");
|
|
13
15
|
const useTable = (rootLocator, configOptions = {}) => {
|
|
14
|
-
const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, headerTransformer: undefined }, configOptions);
|
|
16
|
+
const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, headerTransformer: undefined, autoScroll: true }, configOptions);
|
|
15
17
|
const resolve = (item, parent) => {
|
|
16
18
|
if (typeof item === 'string')
|
|
17
19
|
return parent.locator(item);
|
|
@@ -23,6 +25,9 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
23
25
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
26
|
if (_headerMap)
|
|
25
27
|
return _headerMap;
|
|
28
|
+
if (config.autoScroll) {
|
|
29
|
+
yield rootLocator.scrollIntoViewIfNeeded();
|
|
30
|
+
}
|
|
26
31
|
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
27
32
|
try {
|
|
28
33
|
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
@@ -63,35 +68,39 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
63
68
|
});
|
|
64
69
|
return smart;
|
|
65
70
|
};
|
|
71
|
+
const _applyFilters = (baseRows, filters, map, exact) => {
|
|
72
|
+
let filtered = baseRows;
|
|
73
|
+
const page = rootLocator.page();
|
|
74
|
+
for (const [colName, value] of Object.entries(filters)) {
|
|
75
|
+
const colIndex = map.get(colName);
|
|
76
|
+
if (colIndex === undefined)
|
|
77
|
+
throw new Error(`Column '${colName}' not found.`);
|
|
78
|
+
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
79
|
+
const cellTemplate = resolve(config.cellSelector, page);
|
|
80
|
+
filtered = filtered.filter({
|
|
81
|
+
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return filtered;
|
|
85
|
+
};
|
|
66
86
|
const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
|
|
67
87
|
var _a;
|
|
68
88
|
const map = yield _getMap();
|
|
69
|
-
const page = rootLocator.page();
|
|
70
89
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
71
90
|
let currentPage = 1;
|
|
72
91
|
while (true) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (colIndex === undefined)
|
|
77
|
-
throw new Error(`Column '${colName}' not found. Available: ${[...map.keys()].join(', ')}`);
|
|
78
|
-
const exact = options.exact || false;
|
|
79
|
-
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
80
|
-
const cellTemplate = resolve(config.cellSelector, page);
|
|
81
|
-
rowLocator = rowLocator.filter({
|
|
82
|
-
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const count = yield rowLocator.count();
|
|
92
|
+
const allRows = resolve(config.rowSelector, rootLocator);
|
|
93
|
+
const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
|
|
94
|
+
const count = yield matchedRows.count();
|
|
86
95
|
if (count > 1)
|
|
87
96
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
88
97
|
if (count === 1)
|
|
89
|
-
return
|
|
98
|
+
return matchedRows.first();
|
|
90
99
|
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
91
100
|
const context = {
|
|
92
101
|
root: rootLocator,
|
|
93
102
|
config: config,
|
|
94
|
-
page: page,
|
|
103
|
+
page: rootLocator.page(),
|
|
95
104
|
resolve: resolve
|
|
96
105
|
};
|
|
97
106
|
const didLoadMore = yield config.pagination(context);
|
|
@@ -103,6 +112,29 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
103
112
|
return null;
|
|
104
113
|
}
|
|
105
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
|
+
});
|
|
106
138
|
return {
|
|
107
139
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
108
140
|
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -114,38 +146,39 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
114
146
|
}),
|
|
115
147
|
getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
148
|
let row = yield _findRowLocator(filters, options);
|
|
117
|
-
// ā
FIX: Sentinel Logic for negative assertions (expect(row).not.toBeVisible())
|
|
118
149
|
if (!row) {
|
|
119
150
|
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
120
151
|
}
|
|
121
152
|
const smartRow = _makeSmart(row, yield _getMap());
|
|
122
153
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
123
|
-
// If row doesn't exist, toJSON() returns empty object or throws?
|
|
124
|
-
// For safety, let's let it run naturally (it will likely return empty strings)
|
|
125
154
|
return smartRow.toJSON();
|
|
126
155
|
}
|
|
127
156
|
return smartRow;
|
|
128
157
|
}),
|
|
129
158
|
getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
159
|
const map = yield _getMap();
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
let rowLocators = resolve(config.rowSelector, rootLocator);
|
|
161
|
+
if (options === null || options === void 0 ? void 0 : options.filter) {
|
|
162
|
+
rowLocators = _applyFilters(rowLocators, options.filter, map, options.exact || false);
|
|
163
|
+
}
|
|
164
|
+
const rows = yield rowLocators.all();
|
|
165
|
+
const smartRows = rows.map(loc => _makeSmart(loc, map));
|
|
133
166
|
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
134
167
|
return Promise.all(smartRows.map(r => r.toJSON()));
|
|
135
168
|
}
|
|
136
169
|
return smartRows;
|
|
137
170
|
}),
|
|
138
|
-
generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
172
|
const html = yield rootLocator.evaluate((el) => el.outerHTML);
|
|
140
173
|
const separator = "=".repeat(50);
|
|
141
|
-
const
|
|
142
|
-
|
|
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`;
|
|
175
|
+
yield _handlePrompt('Smart Table Config', content, options);
|
|
143
176
|
}),
|
|
144
|
-
generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
+
generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
178
|
const container = rootLocator.locator('xpath=..');
|
|
146
179
|
const html = yield container.evaluate((el) => el.outerHTML);
|
|
147
|
-
const
|
|
148
|
-
|
|
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`;
|
|
181
|
+
yield _handlePrompt('Smart Table Strategy', content, options);
|
|
149
182
|
})
|
|
150
183
|
};
|
|
151
184
|
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "A smart table utility for Playwright with built-in pagination strategies.",
|
|
3
|
+
"version": "2.0.2",
|
|
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",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
-
"
|
|
11
|
+
"generate-types": "node scripts/embed-types.mjs",
|
|
12
|
+
"build": "npm run generate-types && tsc",
|
|
12
13
|
"prepublishOnly": "npm run build",
|
|
13
|
-
"
|
|
14
|
+
"release": "npm run build && npm publish --access public",
|
|
14
15
|
"test": "npx playwright test"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|