chrometools-mcp 1.9.1 → 2.3.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/CHANGELOG.md +305 -0
- package/README.md +279 -53
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +625 -1875
- package/package.json +1 -1
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +63 -9
- package/recorder/scenario-executor.js +47 -27
- package/recorder/scenario-storage.js +251 -29
- package/server/tool-definitions.js +655 -0
- package/server/tool-schemas.js +295 -0
- package/utils/code-generators/code-generator-base.js +61 -0
- package/utils/code-generators/file-appender.js +202 -0
- package/utils/code-generators/playwright-python.js +84 -0
- package/utils/code-generators/playwright-typescript.js +95 -0
- package/utils/code-generators/selenium-java.js +123 -0
- package/utils/code-generators/selenium-python.js +82 -0
- package/utils/css-utils.js +151 -0
- package/utils/image-processing.js +236 -0
- package/utils/platform-utils.js +62 -0
- package/utils/url-to-project.js +141 -0
- package/utils/project-detector.js +0 -87
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recorder/page-object-generator.js
|
|
3
|
+
*
|
|
4
|
+
* Generates Page Object Model (POM) classes from page analysis
|
|
5
|
+
* Supports multiple frameworks: Playwright (TS/Python), Selenium (Python/Java)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analyze page and generate Page Object class
|
|
10
|
+
* @param {Object} page - Puppeteer page instance
|
|
11
|
+
* @param {Object} options - Generation options
|
|
12
|
+
* @returns {Promise<Object>} - Page object data and code
|
|
13
|
+
*/
|
|
14
|
+
export async function generatePageObject(page, options = {}) {
|
|
15
|
+
const {
|
|
16
|
+
className = null,
|
|
17
|
+
framework = 'playwright-typescript',
|
|
18
|
+
includeComments = true,
|
|
19
|
+
groupElements = true
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
// Analyze page structure
|
|
23
|
+
const pageAnalysis = await analyzePage(page);
|
|
24
|
+
|
|
25
|
+
// Generate class name from page title/URL if not provided
|
|
26
|
+
const finalClassName = className || generateClassName(pageAnalysis.title, pageAnalysis.url);
|
|
27
|
+
|
|
28
|
+
// Group elements by logical sections
|
|
29
|
+
const elementGroups = groupElements
|
|
30
|
+
? groupElementsBySection(pageAnalysis.elements)
|
|
31
|
+
: { main: pageAnalysis.elements };
|
|
32
|
+
|
|
33
|
+
// Generate code based on framework
|
|
34
|
+
const code = await generateCode(finalClassName, elementGroups, pageAnalysis, framework, includeComments);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
className: finalClassName,
|
|
39
|
+
url: pageAnalysis.url,
|
|
40
|
+
title: pageAnalysis.title,
|
|
41
|
+
elementCount: pageAnalysis.elements.length,
|
|
42
|
+
groups: Object.keys(elementGroups),
|
|
43
|
+
framework,
|
|
44
|
+
code
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Analyze page structure and extract interactive elements
|
|
50
|
+
* @param {Object} page - Puppeteer page instance
|
|
51
|
+
* @returns {Promise<Object>} - Page analysis data
|
|
52
|
+
*/
|
|
53
|
+
async function analyzePage(page) {
|
|
54
|
+
const analysis = await page.evaluate(() => {
|
|
55
|
+
const elements = [];
|
|
56
|
+
|
|
57
|
+
// Helper: Generate smart selector for element
|
|
58
|
+
function generateSelector(el) {
|
|
59
|
+
// Priority: id > name > data-testid > unique class > tag path
|
|
60
|
+
|
|
61
|
+
if (el.id) {
|
|
62
|
+
return `#${el.id}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (el.name) {
|
|
66
|
+
return `[name="${el.name}"]`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test');
|
|
70
|
+
if (testId) {
|
|
71
|
+
return `[data-testid="${testId}"]`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try to find unique class
|
|
75
|
+
if (el.className && typeof el.className === 'string') {
|
|
76
|
+
const classes = el.className.split(' ').filter(c => c.trim());
|
|
77
|
+
for (const cls of classes) {
|
|
78
|
+
if (document.querySelectorAll(`.${cls}`).length === 1) {
|
|
79
|
+
return `.${cls}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build CSS path
|
|
85
|
+
const path = [];
|
|
86
|
+
let current = el;
|
|
87
|
+
while (current && current !== document.body) {
|
|
88
|
+
let selector = current.tagName.toLowerCase();
|
|
89
|
+
|
|
90
|
+
// Add nth-child if needed for uniqueness
|
|
91
|
+
const parent = current.parentElement;
|
|
92
|
+
if (parent) {
|
|
93
|
+
const siblings = Array.from(parent.children).filter(
|
|
94
|
+
child => child.tagName === current.tagName
|
|
95
|
+
);
|
|
96
|
+
if (siblings.length > 1) {
|
|
97
|
+
const index = siblings.indexOf(current) + 1;
|
|
98
|
+
selector += `:nth-child(${index})`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
path.unshift(selector);
|
|
103
|
+
current = parent;
|
|
104
|
+
|
|
105
|
+
// Don't go too deep
|
|
106
|
+
if (path.length >= 5) break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return path.join(' > ');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Helper: Generate element name from element properties
|
|
113
|
+
function generateElementName(el) {
|
|
114
|
+
// Try label
|
|
115
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('title');
|
|
116
|
+
if (label) {
|
|
117
|
+
return label.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
119
|
+
.replace(/^_+|_+$/g, '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try text content for buttons/links
|
|
123
|
+
if (el.tagName === 'BUTTON' || el.tagName === 'A') {
|
|
124
|
+
const text = el.textContent.trim();
|
|
125
|
+
if (text && text.length < 30) {
|
|
126
|
+
return text.toLowerCase()
|
|
127
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
128
|
+
.replace(/^_+|_+$/g, '');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try name or id
|
|
133
|
+
if (el.name) return el.name;
|
|
134
|
+
if (el.id) return el.id;
|
|
135
|
+
|
|
136
|
+
// Fallback to type + index
|
|
137
|
+
return el.tagName.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Helper: Get element's semantic section
|
|
141
|
+
function getElementSection(el) {
|
|
142
|
+
// Check parents for semantic sections
|
|
143
|
+
let current = el;
|
|
144
|
+
while (current && current !== document.body) {
|
|
145
|
+
const tag = current.tagName.toLowerCase();
|
|
146
|
+
const role = current.getAttribute('role');
|
|
147
|
+
|
|
148
|
+
// Semantic HTML5 tags
|
|
149
|
+
if (tag === 'header') return 'header';
|
|
150
|
+
if (tag === 'nav') return 'navigation';
|
|
151
|
+
if (tag === 'main') return 'main';
|
|
152
|
+
if (tag === 'aside') return 'sidebar';
|
|
153
|
+
if (tag === 'footer') return 'footer';
|
|
154
|
+
if (tag === 'form') return 'form';
|
|
155
|
+
|
|
156
|
+
// ARIA roles
|
|
157
|
+
if (role === 'navigation') return 'navigation';
|
|
158
|
+
if (role === 'search') return 'search';
|
|
159
|
+
if (role === 'form') return 'form';
|
|
160
|
+
if (role === 'banner') return 'header';
|
|
161
|
+
if (role === 'contentinfo') return 'footer';
|
|
162
|
+
if (role === 'complementary') return 'sidebar';
|
|
163
|
+
|
|
164
|
+
current = current.parentElement;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 'main';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Extract interactive elements
|
|
171
|
+
const selectors = [
|
|
172
|
+
'input:not([type="hidden"])',
|
|
173
|
+
'textarea',
|
|
174
|
+
'select',
|
|
175
|
+
'button',
|
|
176
|
+
'a[href]',
|
|
177
|
+
'[role="button"]',
|
|
178
|
+
'[role="link"]',
|
|
179
|
+
'[onclick]',
|
|
180
|
+
'[contenteditable="true"]'
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const foundElements = document.querySelectorAll(selectors.join(', '));
|
|
184
|
+
|
|
185
|
+
foundElements.forEach((el, index) => {
|
|
186
|
+
// Skip hidden elements
|
|
187
|
+
const rect = el.getBoundingClientRect();
|
|
188
|
+
const styles = window.getComputedStyle(el);
|
|
189
|
+
if (rect.width === 0 || rect.height === 0 ||
|
|
190
|
+
styles.display === 'none' || styles.visibility === 'hidden') {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const elementInfo = {
|
|
195
|
+
tag: el.tagName.toLowerCase(),
|
|
196
|
+
type: el.type || 'element',
|
|
197
|
+
selector: generateSelector(el),
|
|
198
|
+
name: generateElementName(el),
|
|
199
|
+
text: el.textContent?.trim().substring(0, 50) || '',
|
|
200
|
+
section: getElementSection(el),
|
|
201
|
+
attributes: {
|
|
202
|
+
id: el.id || null,
|
|
203
|
+
name: el.name || null,
|
|
204
|
+
className: el.className || null,
|
|
205
|
+
placeholder: el.getAttribute('placeholder') || null,
|
|
206
|
+
ariaLabel: el.getAttribute('aria-label') || null,
|
|
207
|
+
role: el.getAttribute('role') || null,
|
|
208
|
+
href: el.href || null
|
|
209
|
+
},
|
|
210
|
+
position: {
|
|
211
|
+
top: rect.top,
|
|
212
|
+
left: rect.left,
|
|
213
|
+
width: rect.width,
|
|
214
|
+
height: rect.height
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
elements.push(elementInfo);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
url: window.location.href,
|
|
223
|
+
title: document.title,
|
|
224
|
+
elements
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return analysis;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Group elements by logical sections
|
|
233
|
+
* @param {Array} elements - Array of element info objects
|
|
234
|
+
* @returns {Object} - Grouped elements
|
|
235
|
+
*/
|
|
236
|
+
function groupElementsBySection(elements) {
|
|
237
|
+
const groups = {};
|
|
238
|
+
|
|
239
|
+
elements.forEach(el => {
|
|
240
|
+
const section = el.section || 'main';
|
|
241
|
+
if (!groups[section]) {
|
|
242
|
+
groups[section] = [];
|
|
243
|
+
}
|
|
244
|
+
groups[section].push(el);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return groups;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Generate class name from page title or URL
|
|
252
|
+
* @param {string} title - Page title
|
|
253
|
+
* @param {string} url - Page URL
|
|
254
|
+
* @returns {string} - Generated class name
|
|
255
|
+
*/
|
|
256
|
+
function generateClassName(title, url) {
|
|
257
|
+
// Try title first
|
|
258
|
+
if (title && title.length > 0 && title.length < 100) {
|
|
259
|
+
const cleaned = title
|
|
260
|
+
.replace(/[^a-zA-Z0-9\s]/g, '')
|
|
261
|
+
.trim()
|
|
262
|
+
.split(/\s+/)
|
|
263
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
264
|
+
.join('');
|
|
265
|
+
|
|
266
|
+
if (cleaned) {
|
|
267
|
+
return cleaned + 'Page';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Fallback to URL path
|
|
272
|
+
try {
|
|
273
|
+
const urlObj = new URL(url);
|
|
274
|
+
const pathParts = urlObj.pathname
|
|
275
|
+
.split('/')
|
|
276
|
+
.filter(p => p.length > 0 && p.length < 20)
|
|
277
|
+
.map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase());
|
|
278
|
+
|
|
279
|
+
if (pathParts.length > 0) {
|
|
280
|
+
return pathParts.join('') + 'Page';
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// Invalid URL
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 'Page';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Generate code based on framework
|
|
291
|
+
* @param {string} className - Class name
|
|
292
|
+
* @param {Object} elementGroups - Grouped elements
|
|
293
|
+
* @param {Object} pageAnalysis - Full page analysis
|
|
294
|
+
* @param {string} framework - Target framework
|
|
295
|
+
* @param {boolean} includeComments - Include comments
|
|
296
|
+
* @returns {string} - Generated code
|
|
297
|
+
*/
|
|
298
|
+
function generateCode(className, elementGroups, pageAnalysis, framework, includeComments) {
|
|
299
|
+
switch (framework) {
|
|
300
|
+
case 'playwright-typescript':
|
|
301
|
+
return generatePlaywrightTypeScript(className, elementGroups, pageAnalysis, includeComments);
|
|
302
|
+
case 'playwright-python':
|
|
303
|
+
return generatePlaywrightPython(className, elementGroups, pageAnalysis, includeComments);
|
|
304
|
+
case 'selenium-python':
|
|
305
|
+
return generateSeleniumPython(className, elementGroups, pageAnalysis, includeComments);
|
|
306
|
+
case 'selenium-java':
|
|
307
|
+
return generateSeleniumJava(className, elementGroups, pageAnalysis, includeComments);
|
|
308
|
+
default:
|
|
309
|
+
throw new Error(`Unsupported framework: ${framework}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Generate Playwright TypeScript Page Object
|
|
315
|
+
*/
|
|
316
|
+
function generatePlaywrightTypeScript(className, elementGroups, pageAnalysis, includeComments) {
|
|
317
|
+
const lines = [];
|
|
318
|
+
|
|
319
|
+
if (includeComments) {
|
|
320
|
+
lines.push(`/**`);
|
|
321
|
+
lines.push(` * ${className}`);
|
|
322
|
+
lines.push(` * Generated from: ${pageAnalysis.url}`);
|
|
323
|
+
lines.push(` * Page title: ${pageAnalysis.title}`);
|
|
324
|
+
lines.push(` */`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
lines.push(`import { Page, Locator } from '@playwright/test';`);
|
|
328
|
+
lines.push(``);
|
|
329
|
+
lines.push(`export class ${className} {`);
|
|
330
|
+
lines.push(` readonly page: Page;`);
|
|
331
|
+
lines.push(``);
|
|
332
|
+
|
|
333
|
+
// Generate locators for all elements
|
|
334
|
+
const allElements = Object.values(elementGroups).flat();
|
|
335
|
+
const uniqueElements = deduplicateElements(allElements);
|
|
336
|
+
|
|
337
|
+
uniqueElements.forEach(el => {
|
|
338
|
+
const locatorName = sanitizeIdentifier(el.name);
|
|
339
|
+
if (includeComments && el.text) {
|
|
340
|
+
lines.push(` /** ${el.text.substring(0, 60)} */`);
|
|
341
|
+
}
|
|
342
|
+
lines.push(` readonly ${locatorName}: Locator;`);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
lines.push(``);
|
|
346
|
+
lines.push(` constructor(page: Page) {`);
|
|
347
|
+
lines.push(` this.page = page;`);
|
|
348
|
+
|
|
349
|
+
uniqueElements.forEach(el => {
|
|
350
|
+
const locatorName = sanitizeIdentifier(el.name);
|
|
351
|
+
const selector = escapeSelectorForCode(el.selector);
|
|
352
|
+
lines.push(` this.${locatorName} = page.locator('${selector}');`);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
lines.push(` }`);
|
|
356
|
+
lines.push(``);
|
|
357
|
+
|
|
358
|
+
// Generate helper methods
|
|
359
|
+
lines.push(` async goto() {`);
|
|
360
|
+
lines.push(` await this.page.goto('${pageAnalysis.url}');`);
|
|
361
|
+
lines.push(` }`);
|
|
362
|
+
lines.push(``);
|
|
363
|
+
|
|
364
|
+
// Generate action methods for common patterns
|
|
365
|
+
generateActionMethods(lines, uniqueElements, 'typescript');
|
|
366
|
+
|
|
367
|
+
lines.push(`}`);
|
|
368
|
+
|
|
369
|
+
return lines.join('\n');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Generate Playwright Python Page Object
|
|
374
|
+
*/
|
|
375
|
+
function generatePlaywrightPython(className, elementGroups, pageAnalysis, includeComments) {
|
|
376
|
+
const lines = [];
|
|
377
|
+
|
|
378
|
+
if (includeComments) {
|
|
379
|
+
lines.push(`"""`);
|
|
380
|
+
lines.push(`${className}`);
|
|
381
|
+
lines.push(`Generated from: ${pageAnalysis.url}`);
|
|
382
|
+
lines.push(`Page title: ${pageAnalysis.title}`);
|
|
383
|
+
lines.push(`"""`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
lines.push(`from playwright.sync_api import Page, Locator`);
|
|
387
|
+
lines.push(``);
|
|
388
|
+
lines.push(`class ${className}:`);
|
|
389
|
+
lines.push(` def __init__(self, page: Page):`);
|
|
390
|
+
lines.push(` self.page = page`);
|
|
391
|
+
|
|
392
|
+
// Generate locators
|
|
393
|
+
const allElements = Object.values(elementGroups).flat();
|
|
394
|
+
const uniqueElements = deduplicateElements(allElements);
|
|
395
|
+
|
|
396
|
+
uniqueElements.forEach(el => {
|
|
397
|
+
const locatorName = sanitizeIdentifier(el.name, 'python');
|
|
398
|
+
const selector = escapeSelectorForCode(el.selector);
|
|
399
|
+
if (includeComments && el.text) {
|
|
400
|
+
lines.push(` # ${el.text.substring(0, 60)}`);
|
|
401
|
+
}
|
|
402
|
+
lines.push(` self.${locatorName} = page.locator('${selector}')`);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
lines.push(``);
|
|
406
|
+
lines.push(` def goto(self):`);
|
|
407
|
+
lines.push(` self.page.goto('${pageAnalysis.url}')`);
|
|
408
|
+
lines.push(``);
|
|
409
|
+
|
|
410
|
+
// Generate action methods
|
|
411
|
+
generateActionMethods(lines, uniqueElements, 'python');
|
|
412
|
+
|
|
413
|
+
return lines.join('\n');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Generate Selenium Python Page Object
|
|
418
|
+
*/
|
|
419
|
+
function generateSeleniumPython(className, elementGroups, pageAnalysis, includeComments) {
|
|
420
|
+
const lines = [];
|
|
421
|
+
|
|
422
|
+
if (includeComments) {
|
|
423
|
+
lines.push(`"""`);
|
|
424
|
+
lines.push(`${className}`);
|
|
425
|
+
lines.push(`Generated from: ${pageAnalysis.url}`);
|
|
426
|
+
lines.push(`Page title: ${pageAnalysis.title}`);
|
|
427
|
+
lines.push(`"""`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
lines.push(`from selenium.webdriver.common.by import By`);
|
|
431
|
+
lines.push(`from selenium.webdriver.remote.webdriver import WebDriver`);
|
|
432
|
+
lines.push(`from selenium.webdriver.support.ui import WebDriverWait`);
|
|
433
|
+
lines.push(`from selenium.webdriver.support import expected_conditions as EC`);
|
|
434
|
+
lines.push(``);
|
|
435
|
+
lines.push(`class ${className}:`);
|
|
436
|
+
lines.push(` def __init__(self, driver: WebDriver):`);
|
|
437
|
+
lines.push(` self.driver = driver`);
|
|
438
|
+
lines.push(` self.wait = WebDriverWait(driver, 10)`);
|
|
439
|
+
lines.push(``);
|
|
440
|
+
|
|
441
|
+
// Generate locators as tuples
|
|
442
|
+
const allElements = Object.values(elementGroups).flat();
|
|
443
|
+
const uniqueElements = deduplicateElements(allElements);
|
|
444
|
+
|
|
445
|
+
uniqueElements.forEach(el => {
|
|
446
|
+
const locatorName = sanitizeIdentifier(el.name, 'python').toUpperCase();
|
|
447
|
+
const { by, value } = convertToSeleniumLocator(el.selector);
|
|
448
|
+
if (includeComments && el.text) {
|
|
449
|
+
lines.push(` # ${el.text.substring(0, 60)}`);
|
|
450
|
+
}
|
|
451
|
+
lines.push(` ${locatorName} = (By.${by}, '${escapeSelectorForCode(value)}')`);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
lines.push(``);
|
|
455
|
+
lines.push(` def goto(self):`);
|
|
456
|
+
lines.push(` self.driver.get('${pageAnalysis.url}')`);
|
|
457
|
+
lines.push(``);
|
|
458
|
+
|
|
459
|
+
// Generate action methods for Selenium
|
|
460
|
+
generateSeleniumActionMethods(lines, uniqueElements);
|
|
461
|
+
|
|
462
|
+
return lines.join('\n');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Generate Selenium Java Page Object
|
|
467
|
+
*/
|
|
468
|
+
function generateSeleniumJava(className, elementGroups, pageAnalysis, includeComments) {
|
|
469
|
+
const lines = [];
|
|
470
|
+
|
|
471
|
+
if (includeComments) {
|
|
472
|
+
lines.push(`/**`);
|
|
473
|
+
lines.push(` * ${className}`);
|
|
474
|
+
lines.push(` * Generated from: ${pageAnalysis.url}`);
|
|
475
|
+
lines.push(` * Page title: ${pageAnalysis.title}`);
|
|
476
|
+
lines.push(` */`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
lines.push(`import org.openqa.selenium.By;`);
|
|
480
|
+
lines.push(`import org.openqa.selenium.WebDriver;`);
|
|
481
|
+
lines.push(`import org.openqa.selenium.WebElement;`);
|
|
482
|
+
lines.push(`import org.openqa.selenium.support.ui.WebDriverWait;`);
|
|
483
|
+
lines.push(`import org.openqa.selenium.support.ui.ExpectedConditions;`);
|
|
484
|
+
lines.push(`import java.time.Duration;`);
|
|
485
|
+
lines.push(``);
|
|
486
|
+
lines.push(`public class ${className} {`);
|
|
487
|
+
lines.push(` private WebDriver driver;`);
|
|
488
|
+
lines.push(` private WebDriverWait wait;`);
|
|
489
|
+
lines.push(``);
|
|
490
|
+
|
|
491
|
+
// Generate locators as static By fields
|
|
492
|
+
const allElements = Object.values(elementGroups).flat();
|
|
493
|
+
const uniqueElements = deduplicateElements(allElements);
|
|
494
|
+
|
|
495
|
+
uniqueElements.forEach(el => {
|
|
496
|
+
const locatorName = sanitizeIdentifier(el.name, 'java').toUpperCase();
|
|
497
|
+
const { by, value } = convertToSeleniumLocator(el.selector);
|
|
498
|
+
if (includeComments && el.text) {
|
|
499
|
+
lines.push(` /** ${el.text.substring(0, 60)} */`);
|
|
500
|
+
}
|
|
501
|
+
lines.push(` private static final By ${locatorName} = By.${by.toLowerCase()}("${escapeSelectorForCode(value)}");`);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
lines.push(``);
|
|
505
|
+
lines.push(` public ${className}(WebDriver driver) {`);
|
|
506
|
+
lines.push(` this.driver = driver;`);
|
|
507
|
+
lines.push(` this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));`);
|
|
508
|
+
lines.push(` }`);
|
|
509
|
+
lines.push(``);
|
|
510
|
+
lines.push(` public void goTo() {`);
|
|
511
|
+
lines.push(` driver.get("${pageAnalysis.url}");`);
|
|
512
|
+
lines.push(` }`);
|
|
513
|
+
lines.push(``);
|
|
514
|
+
|
|
515
|
+
// Generate action methods for Selenium Java
|
|
516
|
+
generateSeleniumJavaActionMethods(lines, uniqueElements);
|
|
517
|
+
|
|
518
|
+
lines.push(`}`);
|
|
519
|
+
|
|
520
|
+
return lines.join('\n');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Helper: Deduplicate elements by name
|
|
525
|
+
*/
|
|
526
|
+
function deduplicateElements(elements) {
|
|
527
|
+
const seen = new Map();
|
|
528
|
+
const unique = [];
|
|
529
|
+
|
|
530
|
+
elements.forEach(el => {
|
|
531
|
+
const key = sanitizeIdentifier(el.name);
|
|
532
|
+
if (!seen.has(key)) {
|
|
533
|
+
seen.set(key, true);
|
|
534
|
+
unique.push(el);
|
|
535
|
+
} else {
|
|
536
|
+
// Add index to make unique
|
|
537
|
+
let index = 2;
|
|
538
|
+
let newKey = `${key}_${index}`;
|
|
539
|
+
while (seen.has(newKey)) {
|
|
540
|
+
index++;
|
|
541
|
+
newKey = `${key}_${index}`;
|
|
542
|
+
}
|
|
543
|
+
seen.set(newKey, true);
|
|
544
|
+
unique.push({ ...el, name: newKey });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
return unique;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Helper: Sanitize identifier for code
|
|
553
|
+
*/
|
|
554
|
+
function sanitizeIdentifier(name, language = 'typescript') {
|
|
555
|
+
let cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
|
556
|
+
|
|
557
|
+
// Ensure doesn't start with number
|
|
558
|
+
if (/^\d/.test(cleaned)) {
|
|
559
|
+
cleaned = '_' + cleaned;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Python/Java conventions
|
|
563
|
+
if (language === 'python') {
|
|
564
|
+
return cleaned.toLowerCase();
|
|
565
|
+
} else if (language === 'java') {
|
|
566
|
+
// camelCase for Java
|
|
567
|
+
return cleaned.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// TypeScript: camelCase
|
|
571
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Helper: Escape selector for code strings
|
|
576
|
+
*/
|
|
577
|
+
function escapeSelectorForCode(selector) {
|
|
578
|
+
return selector.replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Helper: Convert CSS selector to Selenium locator
|
|
583
|
+
*/
|
|
584
|
+
function convertToSeleniumLocator(selector) {
|
|
585
|
+
// ID selector
|
|
586
|
+
if (selector.startsWith('#')) {
|
|
587
|
+
return { by: 'ID', value: selector.substring(1) };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Name attribute
|
|
591
|
+
if (selector.match(/^\[name="([^"]+)"\]$/)) {
|
|
592
|
+
const match = selector.match(/^\[name="([^"]+)"\]$/);
|
|
593
|
+
return { by: 'NAME', value: match[1] };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Default to CSS_SELECTOR
|
|
597
|
+
return { by: 'CSS_SELECTOR', value: selector };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Generate action methods (Playwright)
|
|
602
|
+
*/
|
|
603
|
+
function generateActionMethods(lines, elements, language) {
|
|
604
|
+
const inputs = elements.filter(el =>
|
|
605
|
+
el.tag === 'input' || el.tag === 'textarea'
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const buttons = elements.filter(el =>
|
|
609
|
+
el.tag === 'button' || (el.tag === 'a' && el.attributes.href)
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
if (language === 'typescript') {
|
|
613
|
+
// Fill methods for inputs
|
|
614
|
+
inputs.forEach(el => {
|
|
615
|
+
const methodName = `fill${capitalize(sanitizeIdentifier(el.name))}`;
|
|
616
|
+
lines.push(` async ${methodName}(text: string) {`);
|
|
617
|
+
lines.push(` await this.${sanitizeIdentifier(el.name)}.fill(text);`);
|
|
618
|
+
lines.push(` }`);
|
|
619
|
+
lines.push(``);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Click methods for buttons
|
|
623
|
+
buttons.forEach(el => {
|
|
624
|
+
const methodName = `click${capitalize(sanitizeIdentifier(el.name))}`;
|
|
625
|
+
lines.push(` async ${methodName}() {`);
|
|
626
|
+
lines.push(` await this.${sanitizeIdentifier(el.name)}.click();`);
|
|
627
|
+
lines.push(` }`);
|
|
628
|
+
lines.push(``);
|
|
629
|
+
});
|
|
630
|
+
} else if (language === 'python') {
|
|
631
|
+
// Fill methods for inputs
|
|
632
|
+
inputs.forEach(el => {
|
|
633
|
+
const methodName = `fill_${sanitizeIdentifier(el.name, 'python')}`;
|
|
634
|
+
lines.push(` def ${methodName}(self, text: str):`);
|
|
635
|
+
lines.push(` self.${sanitizeIdentifier(el.name, 'python')}.fill(text)`);
|
|
636
|
+
lines.push(``);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Click methods for buttons
|
|
640
|
+
buttons.forEach(el => {
|
|
641
|
+
const methodName = `click_${sanitizeIdentifier(el.name, 'python')}`;
|
|
642
|
+
lines.push(` def ${methodName}(self):`);
|
|
643
|
+
lines.push(` self.${sanitizeIdentifier(el.name, 'python')}.click()`);
|
|
644
|
+
lines.push(``);
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Generate Selenium Python action methods
|
|
651
|
+
*/
|
|
652
|
+
function generateSeleniumActionMethods(lines, elements) {
|
|
653
|
+
const inputs = elements.filter(el =>
|
|
654
|
+
el.tag === 'input' || el.tag === 'textarea'
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const buttons = elements.filter(el =>
|
|
658
|
+
el.tag === 'button' || (el.tag === 'a' && el.attributes.href)
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
inputs.forEach(el => {
|
|
662
|
+
const locatorName = sanitizeIdentifier(el.name, 'python').toUpperCase();
|
|
663
|
+
const methodName = `fill_${sanitizeIdentifier(el.name, 'python')}`;
|
|
664
|
+
lines.push(` def ${methodName}(self, text: str):`);
|
|
665
|
+
lines.push(` element = self.wait.until(EC.element_to_be_clickable(self.${locatorName}))`);
|
|
666
|
+
lines.push(` element.clear()`);
|
|
667
|
+
lines.push(` element.send_keys(text)`);
|
|
668
|
+
lines.push(``);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
buttons.forEach(el => {
|
|
672
|
+
const locatorName = sanitizeIdentifier(el.name, 'python').toUpperCase();
|
|
673
|
+
const methodName = `click_${sanitizeIdentifier(el.name, 'python')}`;
|
|
674
|
+
lines.push(` def ${methodName}(self):`);
|
|
675
|
+
lines.push(` element = self.wait.until(EC.element_to_be_clickable(self.${locatorName}))`);
|
|
676
|
+
lines.push(` element.click()`);
|
|
677
|
+
lines.push(``);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Generate Selenium Java action methods
|
|
683
|
+
*/
|
|
684
|
+
function generateSeleniumJavaActionMethods(lines, elements) {
|
|
685
|
+
const inputs = elements.filter(el =>
|
|
686
|
+
el.tag === 'input' || el.tag === 'textarea'
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
const buttons = elements.filter(el =>
|
|
690
|
+
el.tag === 'button' || (el.tag === 'a' && el.attributes.href)
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
inputs.forEach(el => {
|
|
694
|
+
const locatorName = sanitizeIdentifier(el.name, 'java').toUpperCase();
|
|
695
|
+
const methodName = `fill${capitalize(sanitizeIdentifier(el.name, 'java'))}`;
|
|
696
|
+
lines.push(` public void ${methodName}(String text) {`);
|
|
697
|
+
lines.push(` WebElement element = wait.until(ExpectedConditions.elementToBeClickable(${locatorName}));`);
|
|
698
|
+
lines.push(` element.clear();`);
|
|
699
|
+
lines.push(` element.sendKeys(text);`);
|
|
700
|
+
lines.push(` }`);
|
|
701
|
+
lines.push(``);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
buttons.forEach(el => {
|
|
705
|
+
const locatorName = sanitizeIdentifier(el.name, 'java').toUpperCase();
|
|
706
|
+
const methodName = `click${capitalize(sanitizeIdentifier(el.name, 'java'))}`;
|
|
707
|
+
lines.push(` public void ${methodName}() {`);
|
|
708
|
+
lines.push(` WebElement element = wait.until(ExpectedConditions.elementToBeClickable(${locatorName}));`);
|
|
709
|
+
lines.push(` element.click();`);
|
|
710
|
+
lines.push(` }`);
|
|
711
|
+
lines.push(``);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Helper: Capitalize first letter
|
|
717
|
+
*/
|
|
718
|
+
function capitalize(str) {
|
|
719
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
720
|
+
}
|