@rickcedwhat/playwright-smart-table 1.0.1 → 1.0.4
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/package.json +7 -3
- package/playwright.config.ts +0 -34
- package/src/index.ts +0 -3
- package/src/strategies/index.ts +0 -106
- package/src/types.ts +0 -25
- package/src/useTable.ts +0 -237
- package/tests/mui.spec.ts +0 -53
- package/tests/readme_verification.spec.ts +0 -45
- package/tests/strategies.spec.ts +0 -69
- package/tests/the-internet.spec.ts +0 -37
- package/tsconfig.json +0 -14
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"scripts": {
|
|
8
11
|
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
9
13
|
"test": "npx playwright test"
|
|
10
14
|
},
|
|
11
15
|
"keywords": ["playwright", "testing", "table", "automation"],
|
|
12
16
|
"author": "",
|
|
13
17
|
"license": "ISC",
|
|
14
18
|
"peerDependencies": {
|
|
15
|
-
"@playwright/test": "
|
|
19
|
+
"@playwright/test": "*"
|
|
16
20
|
},
|
|
17
21
|
"devDependencies": {
|
|
18
|
-
"@playwright/test": "^1.
|
|
22
|
+
"@playwright/test": "^1.50.0",
|
|
19
23
|
"@types/node": "^20.0.0",
|
|
20
24
|
"ts-node": "^10.9.0",
|
|
21
25
|
"typescript": "^5.0.0"
|
package/playwright.config.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// playwright.config.ts
|
|
2
|
-
import { defineConfig } from '@playwright/test';
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
// Look for test files in the "tests" directory, relative to this configuration file.
|
|
6
|
-
testDir: 'tests',
|
|
7
|
-
|
|
8
|
-
// Run all tests in parallel.
|
|
9
|
-
fullyParallel: true,
|
|
10
|
-
|
|
11
|
-
// Fail the build on CI if you accidentally left test.only in the source code.
|
|
12
|
-
forbidOnly: !!process.env.CI,
|
|
13
|
-
|
|
14
|
-
// Retry on CI only.
|
|
15
|
-
retries: process.env.CI ? 2 : 0,
|
|
16
|
-
|
|
17
|
-
// Opt out of parallel tests on CI.
|
|
18
|
-
workers: process.env.CI ? 1 : undefined,
|
|
19
|
-
|
|
20
|
-
// Reporter to use
|
|
21
|
-
reporter: 'html',
|
|
22
|
-
|
|
23
|
-
use: {
|
|
24
|
-
// Collect trace when retrying the failed test.
|
|
25
|
-
// Setting to 'on' forces it for every run (useful for debugging now).
|
|
26
|
-
trace: 'on',
|
|
27
|
-
|
|
28
|
-
// Record video for failures
|
|
29
|
-
video: 'retain-on-failure',
|
|
30
|
-
|
|
31
|
-
// Take screenshot on failure
|
|
32
|
-
screenshot: 'only-on-failure',
|
|
33
|
-
},
|
|
34
|
-
});
|
package/src/index.ts
DELETED
package/src/strategies/index.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// src/strategies/index.ts
|
|
2
|
-
import { expect } from '@playwright/test';
|
|
3
|
-
import { PaginationStrategy, Selector, TableContext } from '../types';
|
|
4
|
-
|
|
5
|
-
export const TableStrategies = {
|
|
6
|
-
/**
|
|
7
|
-
* Strategy: Clicks a "Next" button and waits for the first row of data to change.
|
|
8
|
-
* Best for: Standard pagination (Page 1 > Page 2 > Page 3)
|
|
9
|
-
* * @param nextButtonSelector - Selector for the next button (e.g. 'button.next' or getByRole('button', {name: 'Next'}))
|
|
10
|
-
* @param timeout - How long to wait for the table to refresh (default: 5000ms)
|
|
11
|
-
*/
|
|
12
|
-
clickNext: (nextButtonSelector: Selector, timeout = 5000): PaginationStrategy => {
|
|
13
|
-
return async ({ root, config, resolve }: TableContext) => {
|
|
14
|
-
// 1. Find the button using the table's helper
|
|
15
|
-
const nextBtn = resolve(nextButtonSelector, root).first();
|
|
16
|
-
|
|
17
|
-
// If button isn't there or disabled, we are at the end
|
|
18
|
-
if (!await nextBtn.isVisible() || !await nextBtn.isEnabled()) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 2. Snapshot the current state (text of the first row)
|
|
23
|
-
// We use the table's OWN row selector to ensure we are looking at real data
|
|
24
|
-
const firstRow = resolve(config.rowSelector, root).first();
|
|
25
|
-
const oldText = await firstRow.innerText().catch(() => ""); // Handle empty tables gracefully
|
|
26
|
-
|
|
27
|
-
// 3. Click the button
|
|
28
|
-
await nextBtn.click();
|
|
29
|
-
|
|
30
|
-
// 4. Smart Wait: Wait for the first row to have DIFFERENT text
|
|
31
|
-
try {
|
|
32
|
-
await expect(firstRow).not.toHaveText(oldText, { timeout });
|
|
33
|
-
return true; // Success: Data changed
|
|
34
|
-
} catch (e) {
|
|
35
|
-
return false; // Failed: Timed out (probably end of data or broken button)
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Strategy: Clicks a "Load More" button and waits for the row count to increase.
|
|
42
|
-
* Best for: Lists where "Load More" appends data to the bottom.
|
|
43
|
-
* * @param buttonSelector - Selector for the load more button
|
|
44
|
-
* @param timeout - Wait timeout (default: 5000ms)
|
|
45
|
-
*/
|
|
46
|
-
clickLoadMore: (buttonSelector: Selector, timeout = 5000): PaginationStrategy => {
|
|
47
|
-
return async ({ root, config, resolve }: TableContext) => {
|
|
48
|
-
const loadMoreBtn = resolve(buttonSelector, root).first();
|
|
49
|
-
|
|
50
|
-
if (!await loadMoreBtn.isVisible() || !await loadMoreBtn.isEnabled()) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 1. Snapshot: Count current rows
|
|
55
|
-
const rows = resolve(config.rowSelector, root);
|
|
56
|
-
const oldCount = await rows.count();
|
|
57
|
-
|
|
58
|
-
// 2. Click
|
|
59
|
-
await loadMoreBtn.click();
|
|
60
|
-
|
|
61
|
-
// 3. Smart Wait: Wait for row count to be greater than before
|
|
62
|
-
try {
|
|
63
|
-
await expect(async () => {
|
|
64
|
-
const newCount = await rows.count();
|
|
65
|
-
expect(newCount).toBeGreaterThan(oldCount);
|
|
66
|
-
}).toPass({ timeout });
|
|
67
|
-
|
|
68
|
-
return true;
|
|
69
|
-
} catch (e) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Strategy: Scrolls to the bottom of the table and waits for more rows to appear.
|
|
77
|
-
* Best for: Infinite Scroll grids (Ag-Grid, Virtual Lists)
|
|
78
|
-
* * @param timeout - Wait timeout (default: 5000ms)
|
|
79
|
-
*/
|
|
80
|
-
infiniteScroll: (timeout = 5000): PaginationStrategy => {
|
|
81
|
-
return async ({ root, config, resolve, page }: TableContext) => {
|
|
82
|
-
const rows = resolve(config.rowSelector, root);
|
|
83
|
-
const oldCount = await rows.count();
|
|
84
|
-
|
|
85
|
-
if (oldCount === 0) return false;
|
|
86
|
-
|
|
87
|
-
// 1. Scroll the very last row into view to trigger the fetch
|
|
88
|
-
await rows.last().scrollIntoViewIfNeeded();
|
|
89
|
-
|
|
90
|
-
// Optional: Press "End" to force scroll on stubborn grids (like AG Grid)
|
|
91
|
-
await page.keyboard.press('End');
|
|
92
|
-
|
|
93
|
-
// 2. Smart Wait: Wait for row count to increase
|
|
94
|
-
try {
|
|
95
|
-
await expect(async () => {
|
|
96
|
-
const newCount = await rows.count();
|
|
97
|
-
expect(newCount).toBeGreaterThan(oldCount);
|
|
98
|
-
}).toPass({ timeout });
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
} catch (e) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
import { Locator, Page } from '@playwright/test';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A selector can be a CSS string or a function.
|
|
6
|
-
* We allow 'parent' to be Locator OR Page to match your working logic.
|
|
7
|
-
*/
|
|
8
|
-
export type Selector = string | ((parent: Locator | Page) => Locator);
|
|
9
|
-
|
|
10
|
-
export interface TableConfig {
|
|
11
|
-
rowSelector?: Selector;
|
|
12
|
-
headerSelector?: Selector;
|
|
13
|
-
cellSelector?: Selector;
|
|
14
|
-
pagination?: PaginationStrategy;
|
|
15
|
-
maxPages?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface TableContext {
|
|
19
|
-
root: Locator;
|
|
20
|
-
config: Required<TableConfig>;
|
|
21
|
-
page: Page;
|
|
22
|
-
resolve: (item: Selector, parent: Locator | Page) => Locator;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
package/src/useTable.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
// src/useTable.ts
|
|
2
|
-
import { Locator, Page, expect } from '@playwright/test';
|
|
3
|
-
import { TableConfig, TableContext, Selector } from './types';
|
|
4
|
-
|
|
5
|
-
export const useTable = (rootLocator: Locator, configOptions: TableConfig = {}) => {
|
|
6
|
-
const config: Required<TableConfig> = {
|
|
7
|
-
rowSelector: "tbody tr",
|
|
8
|
-
headerSelector: "th",
|
|
9
|
-
cellSelector: "td",
|
|
10
|
-
pagination: undefined as any,
|
|
11
|
-
maxPages: 1,
|
|
12
|
-
...configOptions,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// ✅ UPDATE: Accept Locator OR Page (to match your work logic)
|
|
16
|
-
const resolve = (item: Selector, parent: Locator | Page): Locator => {
|
|
17
|
-
if (typeof item === 'string') return parent.locator(item);
|
|
18
|
-
if (typeof item === 'function') return item(parent);
|
|
19
|
-
return item;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let _headerMap: Map<string, number> | null = null;
|
|
23
|
-
|
|
24
|
-
const _getMap = async () => {
|
|
25
|
-
if (_headerMap) return _headerMap;
|
|
26
|
-
// Headers are still resolved relative to the table root (safer)
|
|
27
|
-
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
28
|
-
try {
|
|
29
|
-
await headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
30
|
-
} catch (e) { /* Ignore hydration */ }
|
|
31
|
-
|
|
32
|
-
const texts = await headerLoc.allInnerTexts();
|
|
33
|
-
_headerMap = new Map(texts.map((t, i) => [t.trim() || `__col_${i}`, i]));
|
|
34
|
-
return _headerMap;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const _findRowLocator = async (filters: Record<string, string | RegExp | number>, options: { exact?: boolean, maxPages?: number } = {}) => {
|
|
38
|
-
const map = await _getMap();
|
|
39
|
-
const page = rootLocator.page();
|
|
40
|
-
const effectiveMaxPages = options.maxPages ?? config.maxPages;
|
|
41
|
-
let currentPage = 1;
|
|
42
|
-
|
|
43
|
-
while (true) {
|
|
44
|
-
// 1. Row Locator uses ROOT (Matches your snippet)
|
|
45
|
-
let rowLocator = resolve(config.rowSelector, rootLocator);
|
|
46
|
-
|
|
47
|
-
for (const [colName, value] of Object.entries(filters)) {
|
|
48
|
-
const colIndex = map.get(colName);
|
|
49
|
-
if (colIndex === undefined) throw new Error(`Column '${colName}' not found.`);
|
|
50
|
-
|
|
51
|
-
const exact = options.exact || false;
|
|
52
|
-
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
53
|
-
|
|
54
|
-
// ✅ MATCHING YOUR WORK LOGIC EXACTLY
|
|
55
|
-
// 2. Cell Template uses PAGE (Matches your snippet)
|
|
56
|
-
const cellTemplate = resolve(config.cellSelector, page);
|
|
57
|
-
|
|
58
|
-
// 3. Filter using .nth(colIndex)
|
|
59
|
-
rowLocator = rowLocator.filter({
|
|
60
|
-
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const count = await rowLocator.count();
|
|
65
|
-
if (count > 1) {
|
|
66
|
-
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (count === 1) return rowLocator.first();
|
|
70
|
-
|
|
71
|
-
// --- PAGINATION LOGIC ---
|
|
72
|
-
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
73
|
-
const context: TableContext = {
|
|
74
|
-
root: rootLocator,
|
|
75
|
-
config: config,
|
|
76
|
-
page: page,
|
|
77
|
-
resolve: resolve
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const didLoadMore = await config.pagination(context);
|
|
81
|
-
if (didLoadMore) {
|
|
82
|
-
currentPage++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
getHeaders: async () => Array.from((await _getMap()).keys()),
|
|
92
|
-
|
|
93
|
-
getByRow: async (filters: Record<string, string | RegExp | number>, options: { exact?: boolean, maxPages?: number } = {}) => {
|
|
94
|
-
const row = await _findRowLocator(filters, options);
|
|
95
|
-
if (!row) return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_SENTINEL_" + Date.now() });
|
|
96
|
-
return row;
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
getByCell: async (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => {
|
|
100
|
-
const row = await _findRowLocator(rowFilters);
|
|
101
|
-
if (!row) throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
|
|
102
|
-
|
|
103
|
-
const map = await _getMap();
|
|
104
|
-
const colIndex = map.get(targetColumn);
|
|
105
|
-
if (colIndex === undefined) throw new Error(`Column '${targetColumn}' not found.`);
|
|
106
|
-
|
|
107
|
-
// Return the specific cell
|
|
108
|
-
// We scope this to the found ROW to ensure we get the right cell
|
|
109
|
-
if (typeof config.cellSelector === 'string') {
|
|
110
|
-
return row.locator(config.cellSelector).nth(colIndex);
|
|
111
|
-
} else {
|
|
112
|
-
return resolve(config.cellSelector, row).nth(colIndex);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
getRows: async () => {
|
|
117
|
-
const map = await _getMap();
|
|
118
|
-
const rowLocator = resolve(config.rowSelector, rootLocator);
|
|
119
|
-
const rowCount = await rowLocator.count();
|
|
120
|
-
const results: Record<string, string>[] = [];
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < rowCount; i++) {
|
|
123
|
-
const row = rowLocator.nth(i);
|
|
124
|
-
let cells: Locator;
|
|
125
|
-
if (typeof config.cellSelector === 'string') {
|
|
126
|
-
cells = row.locator(config.cellSelector);
|
|
127
|
-
} else {
|
|
128
|
-
cells = resolve(config.cellSelector, row);
|
|
129
|
-
}
|
|
130
|
-
const cellTexts = await cells.allInnerTexts();
|
|
131
|
-
const rowData: Record<string, string> = {};
|
|
132
|
-
for (const [colName, colIdx] of map.entries()) {
|
|
133
|
-
rowData[colName] = (cellTexts[colIdx] || "").trim();
|
|
134
|
-
}
|
|
135
|
-
results.push(rowData);
|
|
136
|
-
}
|
|
137
|
-
return results;
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
getRowAsJSON: async (filters: Record<string, string | RegExp | number>) => {
|
|
141
|
-
const row = await _findRowLocator(filters);
|
|
142
|
-
if (!row) throw new Error(`Row not found: ${JSON.stringify(filters)}`);
|
|
143
|
-
|
|
144
|
-
let cells: Locator;
|
|
145
|
-
if (typeof config.cellSelector === 'string') {
|
|
146
|
-
cells = row.locator(config.cellSelector);
|
|
147
|
-
} else {
|
|
148
|
-
cells = resolve(config.cellSelector, row);
|
|
149
|
-
}
|
|
150
|
-
const cellTexts = await cells.allInnerTexts();
|
|
151
|
-
const map = await _getMap();
|
|
152
|
-
const result: Record<string, string> = {};
|
|
153
|
-
for (const [colName, colIndex] of map.entries()) {
|
|
154
|
-
result[colName] = (cellTexts[colIndex] || "").trim();
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* 🛠️ DEV TOOL: Prints a prompt to the console.
|
|
161
|
-
* Copy the output and paste it into Gemini/ChatGPT to generate your config.
|
|
162
|
-
*/
|
|
163
|
-
generateConfigPrompt: async () => {
|
|
164
|
-
const html = await rootLocator.evaluate((el) => el.outerHTML);
|
|
165
|
-
const separator = "=".repeat(50);
|
|
166
|
-
const prompt = `
|
|
167
|
-
${separator}
|
|
168
|
-
🤖 COPY THE TEXT BELOW INTO GEMINI/ChatGPT 🤖
|
|
169
|
-
${separator}
|
|
170
|
-
|
|
171
|
-
I am using a Playwright helper factory called 'useTable'.
|
|
172
|
-
I need you to generate the configuration object based on the HTML structure below.
|
|
173
|
-
|
|
174
|
-
Here is the table HTML:
|
|
175
|
-
\`\`\`html
|
|
176
|
-
${html}
|
|
177
|
-
\`\`\`
|
|
178
|
-
|
|
179
|
-
Based on this HTML, generate the configuration object matching this signature:
|
|
180
|
-
const table = useTable(page.locator('...'), {
|
|
181
|
-
// Find the rows (exclude headers and empty spacer rows if possible)
|
|
182
|
-
rowSelector: "...", // OR (root) => root.locator(...)
|
|
183
|
-
|
|
184
|
-
// Find the column headers
|
|
185
|
-
headerSelector: "...", // OR (root) => root.locator(...)
|
|
186
|
-
|
|
187
|
-
// Find the cell (relative to a specific row)
|
|
188
|
-
cellSelector: "...", // OR (row) => row.locator(...)
|
|
189
|
-
|
|
190
|
-
// Find the "Next Page" button (if it exists in the HTML)
|
|
191
|
-
paginationNextSelector: (root) => root.locator(...)
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
**Requirements:**
|
|
195
|
-
1. Prefer \`getByRole\` or \`getByTestId\` over CSS classes where possible.
|
|
196
|
-
2. If the table uses \`div\` structures (like React Table), ensure the \`rowSelector\` does not accidentally select the header row.
|
|
197
|
-
3. If there are "padding" or "loading" rows, use \`.filter()\` to exclude them.
|
|
198
|
-
|
|
199
|
-
${separator}
|
|
200
|
-
`;
|
|
201
|
-
console.log(prompt);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 🛠️ DEV TOOL: Prints a prompt to help write a custom Pagination Strategy.
|
|
208
|
-
* It snapshots the HTML *surrounding* the table to find buttons/scroll containers.
|
|
209
|
-
*/
|
|
210
|
-
generateStrategyPrompt: async () => {
|
|
211
|
-
// 1. Get the parent container (often holds the pagination controls)
|
|
212
|
-
const container = rootLocator.locator('xpath=..');
|
|
213
|
-
const html = await container.evaluate((el) => el.outerHTML);
|
|
214
|
-
|
|
215
|
-
const prompt = `
|
|
216
|
-
==================================================
|
|
217
|
-
🤖 COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY 🤖
|
|
218
|
-
==================================================
|
|
219
|
-
|
|
220
|
-
I am using 'playwright-smart-table'. I need a custom Pagination Strategy.
|
|
221
|
-
The table is inside this container HTML:
|
|
222
|
-
|
|
223
|
-
\`\`\`html
|
|
224
|
-
${html.substring(0, 5000)} ... (truncated)
|
|
225
|
-
\`\`\`
|
|
226
|
-
|
|
227
|
-
Write a strategy that implements this interface:
|
|
228
|
-
type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
229
|
-
|
|
230
|
-
Requirements:
|
|
231
|
-
1. Identify the "Next" button OR the scroll container.
|
|
232
|
-
2. Return 'true' if data loaded, 'false' if end of data.
|
|
233
|
-
3. Use context.resolve() to find elements.
|
|
234
|
-
`;
|
|
235
|
-
console.log(prompt);
|
|
236
|
-
}
|
|
237
|
-
};
|
package/tests/mui.spec.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { test, expect } from '@playwright/test';
|
|
2
|
-
import { useTable } from '../src/useTable';
|
|
3
|
-
import { TableStrategies } from '../src/strategies';
|
|
4
|
-
|
|
5
|
-
test('Material UI Data Grid Interaction', async ({ page }) => {
|
|
6
|
-
// 1. Navigate to the MUI DataGrid demo
|
|
7
|
-
await page.goto('https://mui.com/material-ui/react-table/');
|
|
8
|
-
|
|
9
|
-
// The demo grid is usually slightly down the page
|
|
10
|
-
const tableLocator = page.locator('.MuiDataGrid-root').first();
|
|
11
|
-
await tableLocator.scrollIntoViewIfNeeded();
|
|
12
|
-
|
|
13
|
-
// 2. Initialize Smart Table
|
|
14
|
-
const table = useTable(tableLocator, {
|
|
15
|
-
rowSelector: '.MuiDataGrid-row',
|
|
16
|
-
headerSelector: '.MuiDataGrid-columnHeader',
|
|
17
|
-
cellSelector: '.MuiDataGrid-cell', // MUI uses distinct divs for cells
|
|
18
|
-
pagination: TableStrategies.clickNext(
|
|
19
|
-
(root) => root.getByRole("button", { name: "Go to next page" })
|
|
20
|
-
),
|
|
21
|
-
maxPages: 5
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Debug: See what columns were detected
|
|
25
|
-
console.log('Headers Detected:', await table.getHeaders());
|
|
26
|
-
|
|
27
|
-
// 3. Find a row (Melisandre is usually in the standard demo dataset)
|
|
28
|
-
// Note: We use "Last name" because that matches the visible header text
|
|
29
|
-
const row = await table.getByRow({ "Last name": "Melisandre" });
|
|
30
|
-
await expect(row).toBeVisible();
|
|
31
|
-
console.log('✅ Found Melisandre');
|
|
32
|
-
|
|
33
|
-
// 4. Check specific cell data
|
|
34
|
-
// "Age" column for Melisandre should be "150"
|
|
35
|
-
const ageCell = await table.getByCell({ "Last name": "Melisandre" }, "Age");
|
|
36
|
-
await expect(ageCell).toHaveText("150");
|
|
37
|
-
console.log('✅ Verified Age is 150');
|
|
38
|
-
|
|
39
|
-
// 5. Dump Data
|
|
40
|
-
const userData = await table.getRowAsJSON({ "Last name": "Melisandre" });
|
|
41
|
-
console.log('User Data JSON:', userData);
|
|
42
|
-
|
|
43
|
-
// This works because we added getHeaders() back to the helper
|
|
44
|
-
const headers = await table.getHeaders();
|
|
45
|
-
console.log(headers);
|
|
46
|
-
|
|
47
|
-
// 4. Change: Interact with the Checkbox
|
|
48
|
-
// Logic: Find the cell in the first column (__col_0) for the row with Age: 150
|
|
49
|
-
// Then click the input/label inside that cell.
|
|
50
|
-
await (await table.getByCell({ Age: "150" }, "__col_0"))
|
|
51
|
-
.getByLabel("Select row")
|
|
52
|
-
.click();
|
|
53
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { test, expect } from '@playwright/test';
|
|
2
|
-
import { useTable } from '../src/useTable';
|
|
3
|
-
|
|
4
|
-
test.describe('README.md Examples Verification', () => {
|
|
5
|
-
|
|
6
|
-
test('getRows() returns array of objects (Matches README)', async ({ page }) => {
|
|
7
|
-
// 1. Arrange: Go to a standard table with a "Name" column
|
|
8
|
-
await page.goto('https://datatables.net/examples/data_sources/dom');
|
|
9
|
-
const table = useTable(page.locator('#example'), {
|
|
10
|
-
headerSelector: 'thead th'
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
// 2. Act: Get all rows as JSON objects
|
|
14
|
-
const rows = await table.getRows();
|
|
15
|
-
|
|
16
|
-
// 3. Assert: Verify the structure matches the README example
|
|
17
|
-
console.log('First Row:', rows[0]);
|
|
18
|
-
|
|
19
|
-
// Datatables.net default sort order:
|
|
20
|
-
// Row 0: Airi Satou
|
|
21
|
-
// Row 1: Angelica Ramos
|
|
22
|
-
expect(rows[0]['Name']).toBe('Airi Satou');
|
|
23
|
-
expect(rows[1]['Name']).toBe('Angelica Ramos');
|
|
24
|
-
|
|
25
|
-
// Verify other columns to ensure mapping is correct
|
|
26
|
-
expect(rows[0]['Position']).toBe('Accountant');
|
|
27
|
-
expect(rows[0]['Office']).toBe('Tokyo');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('getRowAsJSON() returns single object', async ({ page }) => {
|
|
31
|
-
await page.goto('https://datatables.net/examples/data_sources/dom');
|
|
32
|
-
const table = useTable(page.locator('#example'), {
|
|
33
|
-
headerSelector: 'thead th'
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Act
|
|
37
|
-
const data = await table.getRowAsJSON({ Name: 'Ashton Cox' });
|
|
38
|
-
|
|
39
|
-
// Assert
|
|
40
|
-
expect(data['Name']).toBe('Ashton Cox');
|
|
41
|
-
expect(data['Position']).toBe('Junior Technical Author');
|
|
42
|
-
expect(data['Salary']).toContain('$86,000');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
});
|
package/tests/strategies.spec.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// tests/strategies.spec.ts
|
|
2
|
-
import { test, expect } from '@playwright/test';
|
|
3
|
-
import { useTable } from '../src/useTable';
|
|
4
|
-
import { TableStrategies } from '../src/strategies';
|
|
5
|
-
|
|
6
|
-
test.describe('Real World Strategy Tests', () => {
|
|
7
|
-
|
|
8
|
-
test('Strategy: Click Next (Datatables.net)', async ({ page }) => {
|
|
9
|
-
await page.goto('https://datatables.net/examples/data_sources/dom');
|
|
10
|
-
|
|
11
|
-
const tableLoc = page.locator('#example');
|
|
12
|
-
|
|
13
|
-
const table = useTable(tableLoc, {
|
|
14
|
-
rowSelector: 'tbody tr',
|
|
15
|
-
headerSelector: 'thead th',
|
|
16
|
-
cellSelector: 'td',
|
|
17
|
-
// Strategy: Standard "Next" Button
|
|
18
|
-
pagination: TableStrategies.clickNext('#example_next'),
|
|
19
|
-
maxPages: 3
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Verify Page 1
|
|
23
|
-
await expect(await table.getByRow({ Name: "Airi Satou" })).toBeVisible();
|
|
24
|
-
|
|
25
|
-
// Verify Page 2 (Triggers Click Next)
|
|
26
|
-
// "Bradley Greer" is usually on Page 2
|
|
27
|
-
console.log("🔎 Searching for Bradley Greer (Page 2)...");
|
|
28
|
-
await expect(await table.getByRow({ Name: "Bradley Greer" })).toBeVisible();
|
|
29
|
-
console.log("✅ Found row on Page 2!");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('Strategy: Infinite Scroll (HTMX Example)', async ({ page }) => {
|
|
33
|
-
await page.goto('https://htmx.org/examples/infinite-scroll/');
|
|
34
|
-
|
|
35
|
-
const tableLoc = page.locator('table');
|
|
36
|
-
|
|
37
|
-
const table = useTable(tableLoc, {
|
|
38
|
-
rowSelector: 'tbody tr',
|
|
39
|
-
headerSelector: 'thead th',
|
|
40
|
-
cellSelector: 'td',
|
|
41
|
-
// Strategy: Infinite Scroll (Scroll to bottom -> Wait for row count to increase)
|
|
42
|
-
pagination: TableStrategies.infiniteScroll(),
|
|
43
|
-
maxPages: 5
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// 1. Get initial count
|
|
47
|
-
const initialRows = await table.getRows();
|
|
48
|
-
console.log(`Initial Row Count: ${initialRows.length}`);
|
|
49
|
-
|
|
50
|
-
// 2. Trigger Scroll by searching for a row that doesn't exist yet
|
|
51
|
-
// HTMX demo generates random IDs. We'll simulate a deep search by asking for
|
|
52
|
-
// the 40th row (since it loads ~20 at a time).
|
|
53
|
-
|
|
54
|
-
// HACK: Since IDs are random, we simply check that the library *attempts* to scroll.
|
|
55
|
-
// We expect it to eventually fail finding "NonExistent", but verify it tried 5 pages.
|
|
56
|
-
console.log("🔎 Triggering Scroll...");
|
|
57
|
-
const missing = await table.getByRow({ "ID": "NonExistentID" });
|
|
58
|
-
|
|
59
|
-
// It should have tried 5 times (scrolling each time)
|
|
60
|
-
await expect(missing).not.toBeVisible();
|
|
61
|
-
|
|
62
|
-
const finalRows = await table.getRows();
|
|
63
|
-
console.log(`Final Row Count: ${finalRows.length}`);
|
|
64
|
-
|
|
65
|
-
expect(finalRows.length).toBeGreaterThan(initialRows.length);
|
|
66
|
-
console.log("✅ Infinite Scroll successfully loaded more rows!");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { test, expect } from '@playwright/test';
|
|
2
|
-
import { useTable } from '../src/useTable';
|
|
3
|
-
|
|
4
|
-
test('The Internet Herokuapp - Standard Table', async ({ page }) => {
|
|
5
|
-
//--------------------------------
|
|
6
|
-
// Arrange:
|
|
7
|
-
//--------------------------------
|
|
8
|
-
await page.goto("https://the-internet.herokuapp.com/tables");
|
|
9
|
-
|
|
10
|
-
// 1. Setup the helper
|
|
11
|
-
// We select the second table (.nth(1)) just like your snippet
|
|
12
|
-
const tableLocator = page.getByRole("table").nth(1);
|
|
13
|
-
|
|
14
|
-
// No config needed! Defaults (tbody tr, th, td) work perfectly here.
|
|
15
|
-
const table = useTable(tableLocator);
|
|
16
|
-
|
|
17
|
-
//--------------------------------
|
|
18
|
-
// Act & Assert:
|
|
19
|
-
//--------------------------------
|
|
20
|
-
|
|
21
|
-
// OPTION A: Check a specific value (Cleaner)
|
|
22
|
-
// This verifies that the row with Last Name "Doe" has the Email "jdoe@hotmail.com"
|
|
23
|
-
await expect(
|
|
24
|
-
await table.getByCell({ "Last Name": "Doe" }, "Email")
|
|
25
|
-
).toHaveText("jdoe@hotmail.com");
|
|
26
|
-
|
|
27
|
-
// OPTION B: Interact with the whole row
|
|
28
|
-
// getByRow returns the standard Locator for that specific TR
|
|
29
|
-
const row = await table.getByRow({ "Last Name": "Doe" });
|
|
30
|
-
|
|
31
|
-
// Example: Verify visibility
|
|
32
|
-
await expect(row).toBeVisible();
|
|
33
|
-
|
|
34
|
-
// Example: Verify the 'edit' link is inside this specific row
|
|
35
|
-
// (We use toBeVisible instead of click just to keep the test safe/repeatable)
|
|
36
|
-
await expect(row.getByRole('link', { name: 'edit' })).toBeVisible();
|
|
37
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2016",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"declaration": true, /* Generates .d.ts files so users get Intellisense */
|
|
6
|
-
"outDir": "./dist", /* Where the compiled JS goes */
|
|
7
|
-
"strict": true, /* Enable strict type checking */
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true
|
|
11
|
-
},
|
|
12
|
-
"include": ["src/**/*"],
|
|
13
|
-
"exclude": ["node_modules", "**/*.spec.ts"]
|
|
14
|
-
}
|