@rickcedwhat/playwright-smart-table 1.0.6 → 1.0.7
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 +114 -80
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/presets.d.ts +42 -0
- package/dist/presets.js +30 -0
- package/dist/types.d.ts +9 -3
- package/dist/useTable.d.ts +2 -4
- package/dist/useTable.js +115 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,118 +1,152 @@
|
|
|
1
1
|
Playwright Smart Table 🧠
|
|
2
|
+
|
|
2
3
|
A production-ready, type-safe table wrapper for Playwright.
|
|
3
|
-
It handles the hard stuff automatically:
|
|
4
|
-
Pagination (Next buttons, Load More, Infinite Scroll)
|
|
5
|
-
Complex Grids (MUI, AG-Grid, React Table)
|
|
6
|
-
Strict Mode (Throws errors if your filters match multiple rows)
|
|
7
|
-
🚀 Installation
|
|
8
|
-
npm install playwright-smart-table
|
|
9
4
|
|
|
5
|
+
This library abstracts away the complexity of testing dynamic web tables. It handles Pagination, Infinite Scroll, Virtualization, and Data Grids (MUI, AG-Grid) so your tests remain clean and readable.
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
🏁 Quick Start
|
|
13
|
-
Standard HTML Table (No config needed)
|
|
14
|
-
import { test, expect } from '@playwright/test';
|
|
15
|
-
import { useTable } from 'playwright-smart-table';
|
|
7
|
+
📦 Installation
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
await page.goto('/users');
|
|
9
|
+
npm install @rickcedwhat/playwright-smart-table
|
|
19
10
|
|
|
20
|
-
// 1. Initialize (Defaults to <table>, <tr>, <td>)
|
|
21
|
-
const table = useTable(page.locator('#users-table'));
|
|
22
|
-
|
|
23
|
-
// 2. Find row across pages automatically!
|
|
24
|
-
// This will search Page 1, then Page 2, etc.
|
|
25
|
-
const row = await table.getByRow({ Name: 'Alice', Role: 'Admin' });
|
|
26
|
-
|
|
27
|
-
await expect(row).toBeVisible();
|
|
28
|
-
});
|
|
29
11
|
|
|
12
|
+
Requires @playwright/test as a peer dependency.
|
|
30
13
|
|
|
31
|
-
|
|
32
|
-
This library doesn't guess how your table works. You tell it using a Strategy.
|
|
33
|
-
1. Standard "Next" Button
|
|
34
|
-
For tables like Datatables.net or simple paginated lists.
|
|
35
|
-
import { TableStrategies } from 'playwright-smart-table';
|
|
36
|
-
|
|
37
|
-
const table = useTable(locator, {
|
|
38
|
-
// Strategy: Find button -> Click -> Wait for first row to change
|
|
39
|
-
pagination: TableStrategies.clickNext('[aria-label="Next Page"]')
|
|
40
|
-
});
|
|
14
|
+
🚀 Quick Start
|
|
41
15
|
|
|
16
|
+
1. The Standard HTML Table
|
|
42
17
|
|
|
43
|
-
|
|
44
|
-
For grids that load more data as you scroll down (AG-Grid, Virtual Lists, HTMX).
|
|
45
|
-
const table = useTable(locator, {
|
|
46
|
-
// Strategy: Aggressive scroll to bottom -> Wait for row count to increase
|
|
47
|
-
pagination: TableStrategies.infiniteScroll()
|
|
48
|
-
});
|
|
18
|
+
For standard tables (<table>, <tr>, <td>), no configuration is needed.
|
|
49
19
|
|
|
20
|
+
import { test, expect } from '@playwright/test';
|
|
21
|
+
import { useTable } from '@rickcedwhat/playwright-smart-table';
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
pagination
|
|
23
|
+
test('Verify User Email', async ({ page }) => {
|
|
24
|
+
const table = useTable(page.locator('#users-table'));
|
|
25
|
+
|
|
26
|
+
// 🪄 Magic: Finds the row with Name="Alice", then gets the Email cell
|
|
27
|
+
// If Alice is on Page 2, it handles pagination automatically.
|
|
28
|
+
await expect(
|
|
29
|
+
await table.getByCell({ Name: 'Alice' }, 'Email')
|
|
30
|
+
).toHaveText('alice@example.com');
|
|
56
31
|
});
|
|
57
32
|
|
|
58
33
|
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
2. Complex Grids (Material UI / AG-Grid / Divs)
|
|
35
|
+
|
|
36
|
+
For modern React grids that use <div> structures, simply override the selectors.
|
|
37
|
+
|
|
38
|
+
import { useTable, TableStrategies } from '@rickcedwhat/playwright-smart-table';
|
|
39
|
+
|
|
61
40
|
const table = useTable(page.locator('.MuiDataGrid-root'), {
|
|
62
41
|
rowSelector: '.MuiDataGrid-row',
|
|
63
42
|
headerSelector: '.MuiDataGrid-columnHeader',
|
|
64
43
|
cellSelector: '.MuiDataGrid-cell',
|
|
65
|
-
|
|
44
|
+
// Strategy: Tell it how to find the next page
|
|
45
|
+
pagination: TableStrategies.clickNext(
|
|
46
|
+
(root) => root.getByRole('button', { name: 'Go to next page' })
|
|
47
|
+
)
|
|
66
48
|
});
|
|
67
49
|
|
|
68
50
|
|
|
51
|
+
🧩 Pagination Strategies
|
|
52
|
+
|
|
53
|
+
This library uses the Strategy Pattern to handle navigation. This ensures future stability: we can add new ways to paginate without breaking existing tests.
|
|
54
|
+
|
|
55
|
+
clickNext(selector)
|
|
56
|
+
|
|
57
|
+
Best for standard tables (Datatables, lists).
|
|
58
|
+
|
|
59
|
+
Behavior: Clicks the button -> Waits for the first row of data to change.
|
|
60
|
+
|
|
61
|
+
Selector: Can be a CSS string OR a Playwright locator function.
|
|
62
|
+
|
|
63
|
+
// CSS String
|
|
64
|
+
pagination: TableStrategies.clickNext('button.next-page')
|
|
65
|
+
|
|
66
|
+
// Locator Function (More Robust)
|
|
67
|
+
pagination: TableStrategies.clickNext((root) => root.getByRole('button', { name: 'Next' }))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
infiniteScroll()
|
|
71
|
+
|
|
72
|
+
Best for Virtualized Grids (AG-Grid) or lazy-loading lists (HTMX).
|
|
73
|
+
|
|
74
|
+
Behavior: Aggressively scrolls the container/window to the bottom -> Waits for row count to increase.
|
|
75
|
+
|
|
76
|
+
pagination: TableStrategies.infiniteScroll()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
clickLoadMore(selector)
|
|
80
|
+
|
|
81
|
+
Best for "Load More" buttons.
|
|
82
|
+
|
|
83
|
+
Behavior: Clicks button -> Waits for row count to increase.
|
|
84
|
+
|
|
85
|
+
pagination: TableStrategies.clickLoadMore('button.load-more')
|
|
86
|
+
|
|
87
|
+
|
|
69
88
|
📖 API Reference
|
|
89
|
+
|
|
70
90
|
getByRow(filters, options?)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
|
|
92
|
+
Returns the Locator for a specific row.
|
|
93
|
+
|
|
94
|
+
Strict Mode: Throws an error if filters match more than 1 row.
|
|
95
|
+
|
|
96
|
+
Auto-Pagination: Will search up to maxPages to find the row.
|
|
97
|
+
|
|
98
|
+
// Find a row where Name is "Alice" AND Role is "Admin"
|
|
99
|
+
const row = await table.getByRow({ Name: "Alice", Role: "Admin" });
|
|
100
|
+
await expect(row).toBeVisible();
|
|
77
101
|
|
|
78
102
|
|
|
79
103
|
getByCell(filters, targetColumn)
|
|
80
|
-
|
|
81
|
-
Returns
|
|
82
|
-
|
|
104
|
+
|
|
105
|
+
Returns the Locator for a specific cell inside a matched row.
|
|
106
|
+
|
|
107
|
+
Use this for interactions (clicking edit buttons, checking checkboxes).
|
|
108
|
+
|
|
109
|
+
// Find Alice's row, then find the "Actions" column, then click the button inside it
|
|
83
110
|
await table.getByCell({ Name: "Alice" }, "Actions").getByRole('button').click();
|
|
84
111
|
|
|
85
112
|
|
|
113
|
+
getRowAsJSON(filters)
|
|
114
|
+
|
|
115
|
+
Returns a POJO (Plain Old JavaScript Object) of the row data. Useful for debugging or strict data assertions.
|
|
116
|
+
|
|
117
|
+
const data = await table.getRowAsJSON({ ID: "101" });
|
|
118
|
+
console.log(data);
|
|
119
|
+
// Output: { ID: "101", Name: "Alice", Status: "Active" }
|
|
120
|
+
|
|
121
|
+
|
|
86
122
|
getRows()
|
|
87
|
-
Dumps all rows on the current page as an array of objects. Great for verifying sort order.
|
|
88
|
-
const rows = await table.getRows();
|
|
89
|
-
expect(rows[0].Name).toBe("Alice");
|
|
90
|
-
expect(rows[1].Name).toBe("Bob");
|
|
91
123
|
|
|
124
|
+
Returns an array of all rows on the current page.
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const data = await table.getRowAsJSON({ ID: "123" });
|
|
96
|
-
console.log(data); // { ID: "123", Name: "Alice", Status: "Active" }
|
|
126
|
+
const allRows = await table.getRows();
|
|
127
|
+
expect(allRows[0].Name).toBe("Alice"); // Verify sort order
|
|
97
128
|
|
|
98
129
|
|
|
99
|
-
🛠️
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
🛠️ Developer Tools
|
|
131
|
+
|
|
132
|
+
The library includes helper tools to generate configurations for you.
|
|
133
|
+
|
|
134
|
+
// Print the HTML structure prompt to console
|
|
135
|
+
// Copy-paste the output into ChatGPT/Gemini to get your config object
|
|
136
|
+
await table.generateConfigPrompt();
|
|
137
|
+
|
|
138
|
+
// Print a prompt to help write a custom Pagination Strategy
|
|
139
|
+
await table.generateStrategyPrompt();
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
🛡️ Stability & Versioning
|
|
143
|
+
|
|
144
|
+
This package follows Semantic Versioning.
|
|
145
|
+
|
|
146
|
+
1.x.x: No breaking changes to the useTable signature.
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
useTable(locator, { pagination: myCustomStrategy });
|
|
148
|
+
New strategies may be added, but existing ones will remain stable.
|
|
117
149
|
|
|
150
|
+
To ensure stability in your projects, install with:
|
|
118
151
|
|
|
152
|
+
"@rickcedwhat/playwright-smart-table": "^1.0.0"
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Locator } from '@playwright/test';
|
|
2
|
+
import { TableConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Preset for Key-Value Forms.
|
|
5
|
+
* * Default Structure:
|
|
6
|
+
* - Row: div.form-group
|
|
7
|
+
* - Cell: Direct children (> *)
|
|
8
|
+
* - Columns: ['Label', 'Input']
|
|
9
|
+
*/
|
|
10
|
+
export declare const useForm: (rootLocator: Locator, options?: TableConfig) => {
|
|
11
|
+
getHeaders: () => Promise<string[]>;
|
|
12
|
+
getByRow: (filters: Record<string, string | RegExp | number>, options?: {
|
|
13
|
+
exact?: boolean;
|
|
14
|
+
maxPages?: number;
|
|
15
|
+
}) => Promise<Locator>;
|
|
16
|
+
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
17
|
+
getRows: () => Promise<Record<string, string>[]>;
|
|
18
|
+
getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
|
|
19
|
+
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
20
|
+
generateConfigPrompt: () => Promise<void>;
|
|
21
|
+
generateStrategyPrompt: () => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Preset for Navigation Menus.
|
|
25
|
+
* * Default Structure:
|
|
26
|
+
* - Row: li
|
|
27
|
+
* - Cell: null (The row IS the cell)
|
|
28
|
+
* - Columns: ['Item']
|
|
29
|
+
*/
|
|
30
|
+
export declare const useMenu: (menuLocator: Locator, options?: TableConfig) => {
|
|
31
|
+
getHeaders: () => Promise<string[]>;
|
|
32
|
+
getByRow: (filters: Record<string, string | RegExp | number>, options?: {
|
|
33
|
+
exact?: boolean;
|
|
34
|
+
maxPages?: number;
|
|
35
|
+
}) => Promise<Locator>;
|
|
36
|
+
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
37
|
+
getRows: () => Promise<Record<string, string>[]>;
|
|
38
|
+
getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
|
|
39
|
+
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
40
|
+
generateConfigPrompt: () => Promise<void>;
|
|
41
|
+
generateStrategyPrompt: () => Promise<void>;
|
|
42
|
+
};
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useMenu = exports.useForm = void 0;
|
|
4
|
+
const useTable_1 = require("./useTable");
|
|
5
|
+
/**
|
|
6
|
+
* Preset for Key-Value Forms.
|
|
7
|
+
* * Default Structure:
|
|
8
|
+
* - Row: div.form-group
|
|
9
|
+
* - Cell: Direct children (> *)
|
|
10
|
+
* - Columns: ['Label', 'Input']
|
|
11
|
+
*/
|
|
12
|
+
const useForm = (rootLocator, options = {}) => {
|
|
13
|
+
return (0, useTable_1.useTable)(rootLocator, Object.assign({
|
|
14
|
+
// Defaults:
|
|
15
|
+
rowSelector: 'div.form-group', cellSelector: (row) => row.locator('> *'), headerSelector: null, columnNames: ['Label', 'Input'] }, options));
|
|
16
|
+
};
|
|
17
|
+
exports.useForm = useForm;
|
|
18
|
+
/**
|
|
19
|
+
* Preset for Navigation Menus.
|
|
20
|
+
* * Default Structure:
|
|
21
|
+
* - Row: li
|
|
22
|
+
* - Cell: null (The row IS the cell)
|
|
23
|
+
* - Columns: ['Item']
|
|
24
|
+
*/
|
|
25
|
+
const useMenu = (menuLocator, options = {}) => {
|
|
26
|
+
return (0, useTable_1.useTable)(menuLocator, Object.assign({
|
|
27
|
+
// Defaults:
|
|
28
|
+
rowSelector: 'li', cellSelector: null, headerSelector: null, columnNames: ['Item'] }, options));
|
|
29
|
+
};
|
|
30
|
+
exports.useMenu = useMenu;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { Locator, Page } from '@playwright/test';
|
|
2
2
|
/**
|
|
3
|
-
* A selector can be a CSS string or
|
|
4
|
-
* We allow 'parent' to be Locator OR Page to match your working logic.
|
|
3
|
+
* A selector can be a CSS string, a function, or null (to disable/skip).
|
|
5
4
|
*/
|
|
6
|
-
export type Selector = string | ((parent: Locator | Page) => Locator);
|
|
5
|
+
export type Selector = string | ((parent: Locator | Page) => Locator) | null;
|
|
7
6
|
export interface TableConfig {
|
|
8
7
|
rowSelector?: Selector;
|
|
9
8
|
headerSelector?: Selector;
|
|
10
9
|
cellSelector?: Selector;
|
|
11
10
|
pagination?: PaginationStrategy;
|
|
12
11
|
maxPages?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Statically override specific column names.
|
|
14
|
+
* Use 'undefined' to keep the detected name for that index.
|
|
15
|
+
* Use this to name columns for Menus or Forms that have no headers.
|
|
16
|
+
* Example: ['MenuItem'] or [undefined, "Actions"]
|
|
17
|
+
*/
|
|
18
|
+
columnNames?: (string | undefined)[];
|
|
13
19
|
}
|
|
14
20
|
export interface TableContext {
|
|
15
21
|
root: Locator;
|
package/dist/useTable.d.ts
CHANGED
|
@@ -9,9 +9,7 @@ export declare const useTable: (rootLocator: Locator, configOptions?: TableConfi
|
|
|
9
9
|
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
10
10
|
getRows: () => Promise<Record<string, string>[]>;
|
|
11
11
|
getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
|
|
12
|
-
|
|
13
|
-
* 🛠️ DEV TOOL: Prints a prompt to the console.
|
|
14
|
-
* Copy the output and paste it into Gemini/ChatGPT to generate your config.
|
|
15
|
-
*/
|
|
12
|
+
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
16
13
|
generateConfigPrompt: () => Promise<void>;
|
|
14
|
+
generateStrategyPrompt: () => Promise<void>;
|
|
17
15
|
};
|
package/dist/useTable.js
CHANGED
|
@@ -11,27 +11,38 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.useTable = void 0;
|
|
13
13
|
const useTable = (rootLocator, configOptions = {}) => {
|
|
14
|
-
const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1 }, configOptions);
|
|
15
|
-
// ✅ UPDATE: Accept Locator OR Page (to match your work logic)
|
|
14
|
+
const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, columnNames: [] }, configOptions);
|
|
16
15
|
const resolve = (item, parent) => {
|
|
17
16
|
if (typeof item === 'string')
|
|
18
17
|
return parent.locator(item);
|
|
19
18
|
if (typeof item === 'function')
|
|
20
19
|
return item(parent);
|
|
21
|
-
|
|
20
|
+
throw new Error("Cannot resolve a null selector. Ensure your config defines selectors correctly.");
|
|
22
21
|
};
|
|
23
22
|
let _headerMap = null;
|
|
24
23
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
24
|
if (_headerMap)
|
|
26
25
|
return _headerMap;
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// 1. Scrape DOM (Only if headerSelector is NOT null)
|
|
27
|
+
let texts = [];
|
|
28
|
+
if (config.headerSelector) {
|
|
29
|
+
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
30
|
+
try {
|
|
31
|
+
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
32
|
+
texts = yield headerLoc.allInnerTexts();
|
|
33
|
+
}
|
|
34
|
+
catch (e) { /* Ignore hydration/empty/timeout */ }
|
|
35
|
+
}
|
|
36
|
+
// 2. Merge Scraped Data with Config Overrides
|
|
37
|
+
_headerMap = new Map();
|
|
38
|
+
const overrides = config.columnNames || [];
|
|
39
|
+
const colCount = Math.max(texts.length, overrides.length);
|
|
40
|
+
for (let i = 0; i < colCount; i++) {
|
|
41
|
+
const scrapedText = (texts[i] || "").trim() || `__col_${i}`;
|
|
42
|
+
const overrideText = overrides[i];
|
|
43
|
+
const finalName = (overrideText !== undefined) ? overrideText : scrapedText;
|
|
44
|
+
_headerMap.set(finalName, i);
|
|
31
45
|
}
|
|
32
|
-
catch (e) { /* Ignore hydration */ }
|
|
33
|
-
const texts = yield headerLoc.allInnerTexts();
|
|
34
|
-
_headerMap = new Map(texts.map((t, i) => [t.trim() || `__col_${i}`, i]));
|
|
35
46
|
return _headerMap;
|
|
36
47
|
});
|
|
37
48
|
const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
|
|
@@ -41,38 +52,67 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
41
52
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
42
53
|
let currentPage = 1;
|
|
43
54
|
while (true) {
|
|
44
|
-
|
|
55
|
+
if (!config.rowSelector)
|
|
56
|
+
throw new Error("rowSelector cannot be null");
|
|
45
57
|
let rowLocator = resolve(config.rowSelector, rootLocator);
|
|
46
58
|
for (const [colName, value] of Object.entries(filters)) {
|
|
47
59
|
const colIndex = map.get(colName);
|
|
48
60
|
if (colIndex === undefined)
|
|
49
|
-
throw new Error(`Column '${colName}' not found
|
|
61
|
+
throw new Error(`Column '${colName}' not found. Available: ${Array.from(map.keys())}`);
|
|
50
62
|
const exact = options.exact || false;
|
|
51
63
|
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// Case 1: No Cell Selector (Menu) - Filter the Row Itself
|
|
65
|
+
if (!config.cellSelector) {
|
|
66
|
+
if (exact) {
|
|
67
|
+
rowLocator = rowLocator.filter({ hasText: new RegExp(`^${escapeRegExp(String(filterVal))}$`) });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
rowLocator = rowLocator.filter({ hasText: filterVal });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Case 2: String Cell Selector - Standard Table Logic (Restored)
|
|
74
|
+
else if (typeof config.cellSelector === 'string') {
|
|
75
|
+
// RESTORED: This logic worked for standard tables.
|
|
76
|
+
// We resolve against the PAGE to create a generic locator template.
|
|
77
|
+
// Playwright handles the relative filtering correctly for standard tables.
|
|
78
|
+
const cellTemplate = resolve(config.cellSelector, page);
|
|
79
|
+
rowLocator = rowLocator.filter({
|
|
80
|
+
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Case 3: Function Cell Selector - Forms (Iterative Fallback)
|
|
84
|
+
else {
|
|
85
|
+
const count = yield rowLocator.count();
|
|
86
|
+
let matchFound = false;
|
|
87
|
+
for (let i = 0; i < count; i++) {
|
|
88
|
+
const specificRow = rowLocator.nth(i);
|
|
89
|
+
// Resolve cell relative to this specific row
|
|
90
|
+
const specificCell = config.cellSelector(specificRow).nth(colIndex);
|
|
91
|
+
if ((yield specificCell.getByText(filterVal, { exact }).count()) > 0) {
|
|
92
|
+
if (matchFound) {
|
|
93
|
+
throw new Error(`Strict Mode Violation: Found multiple rows matching ${JSON.stringify(filters)}.`);
|
|
94
|
+
}
|
|
95
|
+
rowLocator = specificRow;
|
|
96
|
+
matchFound = true;
|
|
97
|
+
// Break inner loop to proceed to next filter or return
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!matchFound) {
|
|
102
|
+
// Return empty locator to fail gracefully
|
|
103
|
+
return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_" + Date.now() });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
59
106
|
}
|
|
60
107
|
const count = yield rowLocator.count();
|
|
61
|
-
if (count > 1)
|
|
108
|
+
if (count > 1)
|
|
62
109
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
63
|
-
}
|
|
64
110
|
if (count === 1)
|
|
65
111
|
return rowLocator.first();
|
|
66
|
-
// --- PAGINATION
|
|
112
|
+
// --- PAGINATION ---
|
|
67
113
|
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
68
|
-
const context = {
|
|
69
|
-
|
|
70
|
-
config: config,
|
|
71
|
-
page: page,
|
|
72
|
-
resolve: resolve
|
|
73
|
-
};
|
|
74
|
-
const didLoadMore = yield config.pagination(context);
|
|
75
|
-
if (didLoadMore) {
|
|
114
|
+
const context = { root: rootLocator, config: config, page: page, resolve: resolve };
|
|
115
|
+
if (yield config.pagination(context)) {
|
|
76
116
|
currentPage++;
|
|
77
117
|
continue;
|
|
78
118
|
}
|
|
@@ -92,12 +132,14 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
92
132
|
const row = yield _findRowLocator(rowFilters);
|
|
93
133
|
if (!row)
|
|
94
134
|
throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
|
|
135
|
+
// Guard: getByCell makes no sense for Menus (no cells)
|
|
136
|
+
if (!config.cellSelector) {
|
|
137
|
+
throw new Error("getByCell is not supported when 'cellSelector' is null (e.g. Menus). Use getByRow instead.");
|
|
138
|
+
}
|
|
95
139
|
const map = yield _getMap();
|
|
96
140
|
const colIndex = map.get(targetColumn);
|
|
97
141
|
if (colIndex === undefined)
|
|
98
142
|
throw new Error(`Column '${targetColumn}' not found.`);
|
|
99
|
-
// Return the specific cell
|
|
100
|
-
// We scope this to the found ROW to ensure we get the right cell
|
|
101
143
|
if (typeof config.cellSelector === 'string') {
|
|
102
144
|
return row.locator(config.cellSelector).nth(colIndex);
|
|
103
145
|
}
|
|
@@ -112,14 +154,18 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
112
154
|
const results = [];
|
|
113
155
|
for (let i = 0; i < rowCount; i++) {
|
|
114
156
|
const row = rowLocator.nth(i);
|
|
115
|
-
let
|
|
116
|
-
if (
|
|
117
|
-
|
|
157
|
+
let cellTexts = [];
|
|
158
|
+
if (!config.cellSelector) {
|
|
159
|
+
cellTexts = [yield row.innerText()];
|
|
160
|
+
}
|
|
161
|
+
else if (typeof config.cellSelector === 'string') {
|
|
162
|
+
// For string selectors, we query all matching cells in the row
|
|
163
|
+
cellTexts = yield row.locator(config.cellSelector).allInnerTexts();
|
|
118
164
|
}
|
|
119
165
|
else {
|
|
120
|
-
|
|
166
|
+
// For function selectors, we resolve against the row
|
|
167
|
+
cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
|
|
121
168
|
}
|
|
122
|
-
const cellTexts = yield cells.allInnerTexts();
|
|
123
169
|
const rowData = {};
|
|
124
170
|
for (const [colName, colIdx] of map.entries()) {
|
|
125
171
|
rowData[colName] = (cellTexts[colIdx] || "").trim();
|
|
@@ -132,14 +178,16 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
132
178
|
const row = yield _findRowLocator(filters);
|
|
133
179
|
if (!row)
|
|
134
180
|
throw new Error(`Row not found: ${JSON.stringify(filters)}`);
|
|
135
|
-
let
|
|
136
|
-
if (
|
|
137
|
-
|
|
181
|
+
let cellTexts = [];
|
|
182
|
+
if (!config.cellSelector) {
|
|
183
|
+
cellTexts = [yield row.innerText()];
|
|
184
|
+
}
|
|
185
|
+
else if (typeof config.cellSelector === 'string') {
|
|
186
|
+
cellTexts = yield row.locator(config.cellSelector).allInnerTexts();
|
|
138
187
|
}
|
|
139
188
|
else {
|
|
140
|
-
|
|
189
|
+
cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
|
|
141
190
|
}
|
|
142
|
-
const cellTexts = yield cells.allInnerTexts();
|
|
143
191
|
const map = yield _getMap();
|
|
144
192
|
const result = {};
|
|
145
193
|
for (const [colName, colIndex] of map.entries()) {
|
|
@@ -147,80 +195,34 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
147
195
|
}
|
|
148
196
|
return result;
|
|
149
197
|
}),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
198
|
+
setColumnName: (colIndex, newNameOrFn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
const map = yield _getMap();
|
|
200
|
+
let oldName = "";
|
|
201
|
+
for (const [name, idx] of map.entries()) {
|
|
202
|
+
if (idx === colIndex) {
|
|
203
|
+
oldName = name;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!oldName)
|
|
208
|
+
oldName = `__col_${colIndex}`;
|
|
209
|
+
const newName = typeof newNameOrFn === 'function' ? newNameOrFn(oldName) : newNameOrFn;
|
|
210
|
+
if (map.has(oldName))
|
|
211
|
+
map.delete(oldName);
|
|
212
|
+
map.set(newName, colIndex);
|
|
213
|
+
}),
|
|
154
214
|
generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
215
|
const html = yield rootLocator.evaluate((el) => el.outerHTML);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
I am using a Playwright helper factory called 'useTable'.
|
|
163
|
-
I need you to generate the configuration object based on the HTML structure below.
|
|
164
|
-
|
|
165
|
-
Here is the table HTML:
|
|
166
|
-
\`\`\`html
|
|
167
|
-
${html}
|
|
168
|
-
\`\`\`
|
|
169
|
-
|
|
170
|
-
Based on this HTML, generate the configuration object matching this signature:
|
|
171
|
-
const table = useTable(page.locator('...'), {
|
|
172
|
-
// Find the rows (exclude headers and empty spacer rows if possible)
|
|
173
|
-
rowSelector: "...", // OR (root) => root.locator(...)
|
|
174
|
-
|
|
175
|
-
// Find the column headers
|
|
176
|
-
headerSelector: "...", // OR (root) => root.locator(...)
|
|
177
|
-
|
|
178
|
-
// Find the cell (relative to a specific row)
|
|
179
|
-
cellSelector: "...", // OR (row) => row.locator(...)
|
|
180
|
-
|
|
181
|
-
// Find the "Next Page" button (if it exists in the HTML)
|
|
182
|
-
paginationNextSelector: (root) => root.locator(...)
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
**Requirements:**
|
|
186
|
-
1. Prefer \`getByRole\` or \`getByTestId\` over CSS classes where possible.
|
|
187
|
-
2. If the table uses \`div\` structures (like React Table), ensure the \`rowSelector\` does not accidentally select the header row.
|
|
188
|
-
3. If there are "padding" or "loading" rows, use \`.filter()\` to exclude them.
|
|
189
|
-
|
|
190
|
-
${separator}
|
|
191
|
-
`;
|
|
192
|
-
console.log(prompt);
|
|
216
|
+
console.log(`\n=== CONFIG PROMPT ===\nI have this HTML:\n\`\`\`html\n${html}\n\`\`\`\nGenerate a 'useTable' config for it.`);
|
|
217
|
+
}),
|
|
218
|
+
generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
const container = rootLocator.locator('xpath=..');
|
|
220
|
+
const html = yield container.evaluate((el) => el.outerHTML);
|
|
221
|
+
console.log(`\n=== STRATEGY PROMPT ===\nI have this Container HTML:\n\`\`\`html\n${html.substring(0, 2000)}\n\`\`\`\nWrite a pagination strategy.`);
|
|
193
222
|
})
|
|
194
223
|
};
|
|
195
|
-
/**
|
|
196
|
-
* 🛠️ DEV TOOL: Prints a prompt to help write a custom Pagination Strategy.
|
|
197
|
-
* It snapshots the HTML *surrounding* the table to find buttons/scroll containers.
|
|
198
|
-
*/
|
|
199
|
-
generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
-
// 1. Get the parent container (often holds the pagination controls)
|
|
201
|
-
const container = rootLocator.locator('xpath=..');
|
|
202
|
-
const html = yield container.evaluate((el) => el.outerHTML);
|
|
203
|
-
const prompt = `
|
|
204
|
-
==================================================
|
|
205
|
-
🤖 COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY 🤖
|
|
206
|
-
==================================================
|
|
207
|
-
|
|
208
|
-
I am using 'playwright-smart-table'. I need a custom Pagination Strategy.
|
|
209
|
-
The table is inside this container HTML:
|
|
210
|
-
|
|
211
|
-
\`\`\`html
|
|
212
|
-
${html.substring(0, 5000)} ... (truncated)
|
|
213
|
-
\`\`\`
|
|
214
|
-
|
|
215
|
-
Write a strategy that implements this interface:
|
|
216
|
-
type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
217
|
-
|
|
218
|
-
Requirements:
|
|
219
|
-
1. Identify the "Next" button OR the scroll container.
|
|
220
|
-
2. Return 'true' if data loaded, 'false' if end of data.
|
|
221
|
-
3. Use context.resolve() to find elements.
|
|
222
|
-
`;
|
|
223
|
-
console.log(prompt);
|
|
224
|
-
});
|
|
225
224
|
};
|
|
226
225
|
exports.useTable = useTable;
|
|
226
|
+
function escapeRegExp(string) {
|
|
227
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
228
|
+
}
|
package/package.json
CHANGED