@rickcedwhat/playwright-smart-table 1.0.6 ā 2.0.0
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/presets.d.ts +18 -0
- package/dist/presets.js +30 -0
- package/dist/types.d.ts +45 -9
- package/dist/useTable.d.ts +2 -16
- package/dist/useTable.js +65 -139
- package/package.json +9 -4
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"
|
|
@@ -0,0 +1,18 @@
|
|
|
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) => import("./types").TableResult;
|
|
11
|
+
/**
|
|
12
|
+
* Preset for Navigation Menus.
|
|
13
|
+
* * Default Structure:
|
|
14
|
+
* - Row: li
|
|
15
|
+
* - Cell: null (The row IS the cell)
|
|
16
|
+
* - Columns: ['Item']
|
|
17
|
+
*/
|
|
18
|
+
export declare const useMenu: (menuLocator: Locator, options?: TableConfig) => import("./types").TableResult;
|
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,20 +1,56 @@
|
|
|
1
1
|
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
3
|
+
export type SmartRow = Locator & {
|
|
4
|
+
getCell(column: string): Locator;
|
|
5
|
+
toJSON(): Promise<Record<string, string>>;
|
|
6
|
+
};
|
|
7
|
+
export interface TableContext {
|
|
8
|
+
root: Locator;
|
|
9
|
+
config: Required<TableConfig>;
|
|
10
|
+
page: Page;
|
|
11
|
+
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
14
|
+
* A function that handles pagination logic.
|
|
15
|
+
* Returns true if more data was loaded (navigation occurred), false if end of data.
|
|
5
16
|
*/
|
|
6
|
-
export type
|
|
17
|
+
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
7
18
|
export interface TableConfig {
|
|
8
19
|
rowSelector?: Selector;
|
|
9
20
|
headerSelector?: Selector;
|
|
10
21
|
cellSelector?: Selector;
|
|
22
|
+
/**
|
|
23
|
+
* Strategy for handling pagination.
|
|
24
|
+
* Use presets from TableStrategies or write your own.
|
|
25
|
+
*/
|
|
11
26
|
pagination?: PaginationStrategy;
|
|
12
27
|
maxPages?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Optional hook to rename columns dynamically.
|
|
30
|
+
* Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
|
|
31
|
+
*/
|
|
32
|
+
headerTransformer?: (text: string, index: number) => string;
|
|
13
33
|
}
|
|
14
|
-
export interface
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
export interface TableResult {
|
|
35
|
+
getHeaders: () => Promise<string[]>;
|
|
36
|
+
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
37
|
+
/** * Find a specific row by its content.
|
|
38
|
+
* Default: Returns SmartRow (Locator).
|
|
39
|
+
* Option { asJSON: true }: Returns Record<string, string> (Data).
|
|
40
|
+
*/
|
|
41
|
+
getByRow: <T extends {
|
|
42
|
+
asJSON?: boolean;
|
|
43
|
+
}>(filters: Record<string, string | RegExp | number>, options?: {
|
|
44
|
+
exact?: boolean;
|
|
45
|
+
maxPages?: number;
|
|
46
|
+
} & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
47
|
+
/** * Get all rows on the current page.
|
|
48
|
+
* Default: Returns SmartRow[] (Locators).
|
|
49
|
+
* Option { asJSON: true }: Returns Record<string, string>[] (Data).
|
|
50
|
+
*/
|
|
51
|
+
getAllRows: <T extends {
|
|
52
|
+
asJSON?: boolean;
|
|
53
|
+
}>(options?: T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
54
|
+
generateConfigPrompt: () => Promise<void>;
|
|
55
|
+
generateStrategyPrompt: () => Promise<void>;
|
|
19
56
|
}
|
|
20
|
-
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
package/dist/useTable.d.ts
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
1
|
import { Locator } from '@playwright/test';
|
|
2
|
-
import { TableConfig } from './types';
|
|
3
|
-
export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) =>
|
|
4
|
-
getHeaders: () => Promise<string[]>;
|
|
5
|
-
getByRow: (filters: Record<string, string | RegExp | number>, options?: {
|
|
6
|
-
exact?: boolean;
|
|
7
|
-
maxPages?: number;
|
|
8
|
-
}) => Promise<Locator>;
|
|
9
|
-
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
10
|
-
getRows: () => Promise<Record<string, string>[]>;
|
|
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
|
-
*/
|
|
16
|
-
generateConfigPrompt: () => Promise<void>;
|
|
17
|
-
};
|
|
2
|
+
import { TableConfig, TableResult } from './types';
|
|
3
|
+
export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) => TableResult;
|
package/dist/useTable.js
CHANGED
|
@@ -11,8 +11,7 @@ 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, headerTransformer: undefined }, configOptions);
|
|
16
15
|
const resolve = (item, parent) => {
|
|
17
16
|
if (typeof item === 'string')
|
|
18
17
|
return parent.locator(item);
|
|
@@ -24,16 +23,46 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
24
23
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
24
|
if (_headerMap)
|
|
26
25
|
return _headerMap;
|
|
27
|
-
// Headers are still resolved relative to the table root (safer)
|
|
28
26
|
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
29
27
|
try {
|
|
30
28
|
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
31
29
|
}
|
|
32
30
|
catch (e) { /* Ignore hydration */ }
|
|
33
31
|
const texts = yield headerLoc.allInnerTexts();
|
|
34
|
-
_headerMap = new Map(texts.map((t, i) =>
|
|
32
|
+
_headerMap = new Map(texts.map((t, i) => {
|
|
33
|
+
let text = t.trim() || `__col_${i}`;
|
|
34
|
+
if (config.headerTransformer)
|
|
35
|
+
text = config.headerTransformer(text, i);
|
|
36
|
+
return [text, i];
|
|
37
|
+
}));
|
|
35
38
|
return _headerMap;
|
|
36
39
|
});
|
|
40
|
+
const _makeSmart = (rowLocator, map) => {
|
|
41
|
+
const smart = rowLocator;
|
|
42
|
+
smart.getCell = (colName) => {
|
|
43
|
+
const idx = map.get(colName);
|
|
44
|
+
if (idx === undefined)
|
|
45
|
+
throw new Error(`Column '${colName}' not found.`);
|
|
46
|
+
if (typeof config.cellSelector === 'string') {
|
|
47
|
+
return rowLocator.locator(config.cellSelector).nth(idx);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return resolve(config.cellSelector, rowLocator).nth(idx);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
smart.toJSON = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
const result = {};
|
|
55
|
+
const cells = typeof config.cellSelector === 'string'
|
|
56
|
+
? rowLocator.locator(config.cellSelector)
|
|
57
|
+
: resolve(config.cellSelector, rowLocator);
|
|
58
|
+
const texts = yield cells.allInnerTexts();
|
|
59
|
+
for (const [col, idx] of map.entries()) {
|
|
60
|
+
result[col] = (texts[idx] || '').trim();
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
});
|
|
64
|
+
return smart;
|
|
65
|
+
};
|
|
37
66
|
const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
|
|
38
67
|
var _a;
|
|
39
68
|
const map = yield _getMap();
|
|
@@ -41,29 +70,23 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
41
70
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
42
71
|
let currentPage = 1;
|
|
43
72
|
while (true) {
|
|
44
|
-
// 1. Row Locator uses ROOT (Matches your snippet)
|
|
45
73
|
let rowLocator = resolve(config.rowSelector, rootLocator);
|
|
46
74
|
for (const [colName, value] of Object.entries(filters)) {
|
|
47
75
|
const colIndex = map.get(colName);
|
|
48
76
|
if (colIndex === undefined)
|
|
49
|
-
throw new Error(`Column '${colName}' not found
|
|
77
|
+
throw new Error(`Column '${colName}' not found. Available: ${[...map.keys()].join(', ')}`);
|
|
50
78
|
const exact = options.exact || false;
|
|
51
79
|
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
52
|
-
// ā
MATCHING YOUR WORK LOGIC EXACTLY
|
|
53
|
-
// 2. Cell Template uses PAGE (Matches your snippet)
|
|
54
80
|
const cellTemplate = resolve(config.cellSelector, page);
|
|
55
|
-
// 3. Filter using .nth(colIndex)
|
|
56
81
|
rowLocator = rowLocator.filter({
|
|
57
82
|
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
58
83
|
});
|
|
59
84
|
}
|
|
60
85
|
const count = yield rowLocator.count();
|
|
61
|
-
if (count > 1)
|
|
86
|
+
if (count > 1)
|
|
62
87
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
63
|
-
}
|
|
64
88
|
if (count === 1)
|
|
65
89
|
return rowLocator.first();
|
|
66
|
-
// --- PAGINATION LOGIC ---
|
|
67
90
|
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
68
91
|
const context = {
|
|
69
92
|
root: rootLocator,
|
|
@@ -82,145 +105,48 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
82
105
|
});
|
|
83
106
|
return {
|
|
84
107
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
85
|
-
|
|
86
|
-
const row = yield _findRowLocator(filters, options);
|
|
87
|
-
if (!row)
|
|
88
|
-
return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_SENTINEL_" + Date.now() });
|
|
89
|
-
return row;
|
|
90
|
-
}),
|
|
91
|
-
getByCell: (rowFilters, targetColumn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
-
const row = yield _findRowLocator(rowFilters);
|
|
93
|
-
if (!row)
|
|
94
|
-
throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
|
|
95
|
-
const map = yield _getMap();
|
|
96
|
-
const colIndex = map.get(targetColumn);
|
|
97
|
-
if (colIndex === undefined)
|
|
98
|
-
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
|
-
if (typeof config.cellSelector === 'string') {
|
|
102
|
-
return row.locator(config.cellSelector).nth(colIndex);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
return resolve(config.cellSelector, row).nth(colIndex);
|
|
106
|
-
}
|
|
107
|
-
}),
|
|
108
|
-
getRows: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
109
109
|
const map = yield _getMap();
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const row = rowLocator.nth(i);
|
|
115
|
-
let cells;
|
|
116
|
-
if (typeof config.cellSelector === 'string') {
|
|
117
|
-
cells = row.locator(config.cellSelector);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
cells = resolve(config.cellSelector, row);
|
|
121
|
-
}
|
|
122
|
-
const cellTexts = yield cells.allInnerTexts();
|
|
123
|
-
const rowData = {};
|
|
124
|
-
for (const [colName, colIdx] of map.entries()) {
|
|
125
|
-
rowData[colName] = (cellTexts[colIdx] || "").trim();
|
|
126
|
-
}
|
|
127
|
-
results.push(rowData);
|
|
128
|
-
}
|
|
129
|
-
return results;
|
|
110
|
+
const idx = map.get(columnName);
|
|
111
|
+
if (idx === undefined)
|
|
112
|
+
throw new Error(`Column '${columnName}' not found.`);
|
|
113
|
+
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
130
114
|
}),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (typeof config.cellSelector === 'string') {
|
|
137
|
-
cells = row.locator(config.cellSelector);
|
|
115
|
+
getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
let row = yield _findRowLocator(filters, options);
|
|
117
|
+
// ā
FIX: Sentinel Logic for negative assertions (expect(row).not.toBeVisible())
|
|
118
|
+
if (!row) {
|
|
119
|
+
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
138
120
|
}
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
const smartRow = _makeSmart(row, yield _getMap());
|
|
122
|
+
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
|
+
return smartRow.toJSON();
|
|
141
126
|
}
|
|
142
|
-
|
|
127
|
+
return smartRow;
|
|
128
|
+
}),
|
|
129
|
+
getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
130
|
const map = yield _getMap();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
|
|
132
|
+
const smartRows = rowLocators.map(loc => _makeSmart(loc, map));
|
|
133
|
+
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
134
|
+
return Promise.all(smartRows.map(r => r.toJSON()));
|
|
147
135
|
}
|
|
148
|
-
return
|
|
136
|
+
return smartRows;
|
|
149
137
|
}),
|
|
150
|
-
/**
|
|
151
|
-
* š ļø DEV TOOL: Prints a prompt to the console.
|
|
152
|
-
* Copy the output and paste it into Gemini/ChatGPT to generate your config.
|
|
153
|
-
*/
|
|
154
138
|
generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
139
|
const html = yield rootLocator.evaluate((el) => el.outerHTML);
|
|
156
140
|
const separator = "=".repeat(50);
|
|
157
|
-
const prompt =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
`;
|
|
141
|
+
const prompt = `\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`;
|
|
142
|
+
console.log(prompt);
|
|
143
|
+
}),
|
|
144
|
+
generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
const container = rootLocator.locator('xpath=..');
|
|
146
|
+
const html = yield container.evaluate((el) => el.outerHTML);
|
|
147
|
+
const prompt = `\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`;
|
|
192
148
|
console.log(prompt);
|
|
193
149
|
})
|
|
194
150
|
};
|
|
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
151
|
};
|
|
226
152
|
exports.useTable = useTable;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A smart table utility for Playwright with built-in pagination strategies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,14 +10,19 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"prepublishOnly": "npm run build",
|
|
13
|
-
"publish": "npm publish --access public",
|
|
13
|
+
"publish-public": "npm publish --access public",
|
|
14
14
|
"test": "npx playwright test"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"playwright",
|
|
18
|
+
"testing",
|
|
19
|
+
"table",
|
|
20
|
+
"automation"
|
|
21
|
+
],
|
|
17
22
|
"author": "",
|
|
18
23
|
"license": "ISC",
|
|
19
24
|
"peerDependencies": {
|
|
20
|
-
"@playwright/test": "*"
|
|
25
|
+
"@playwright/test": "*"
|
|
21
26
|
},
|
|
22
27
|
"peerDependenciesMeta": {
|
|
23
28
|
"@playwright/test": {
|