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.
@@ -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
+ }