chrometools-mcp 3.3.8 → 3.4.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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * utils/code-generators/pom-integrator.js
3
+ *
4
+ * Links POM elements with scenario actions via selector matching.
5
+ * Also parses existing POM files to extract element metadata.
6
+ */
7
+
8
+ /**
9
+ * Match action selector to POM element
10
+ * @param {string} actionSelector - Selector from recorded action
11
+ * @param {Array} pomElements - Array of POM element metadata
12
+ * @returns {Object|null} - Matched POM element or null
13
+ */
14
+ export function matchActionToPomElement(actionSelector, pomElements) {
15
+ if (!actionSelector || !pomElements || pomElements.length === 0) {
16
+ return null;
17
+ }
18
+
19
+ // 1. Exact match
20
+ const exact = pomElements.find(el => el.selector === actionSelector);
21
+ if (exact) return exact;
22
+
23
+ // 2. Normalized match - strip tag prefix (e.g., "input#username" -> "#username")
24
+ const normalizedAction = normalizeSelector(actionSelector);
25
+ const normalized = pomElements.find(el => normalizeSelector(el.selector) === normalizedAction);
26
+ if (normalized) return normalized;
27
+
28
+ // 3. Key-based match - compare by id, name, or data-testid values
29
+ const actionKeys = extractSelectorKeys(actionSelector);
30
+ if (actionKeys) {
31
+ for (const el of pomElements) {
32
+ const elKeys = extractSelectorKeys(el.selector);
33
+ if (elKeys && keysMatch(actionKeys, elKeys)) {
34
+ return el;
35
+ }
36
+ }
37
+ }
38
+
39
+ // 4. No match
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Normalize selector by stripping tag prefix
45
+ * "input#username" -> "#username"
46
+ * "input[name='email']" -> "[name='email']"
47
+ */
48
+ function normalizeSelector(selector) {
49
+ // Strip leading tag name before #, ., or [
50
+ return selector.replace(/^[a-z]+(?=[#.\[])/, '');
51
+ }
52
+
53
+ /**
54
+ * Extract key identifiers from selector
55
+ * Returns { type, value } or null
56
+ */
57
+ function extractSelectorKeys(selector) {
58
+ // ID selector: #username or input#username
59
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
60
+ if (idMatch) return { type: 'id', value: idMatch[1] };
61
+
62
+ // Name attribute: [name="email"] or input[name="email"]
63
+ const nameMatch = selector.match(/\[name=["']([^"']+)["']\]/);
64
+ if (nameMatch) return { type: 'name', value: nameMatch[1] };
65
+
66
+ // Data-testid: [data-testid="submit-btn"]
67
+ const testIdMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
68
+ if (testIdMatch) return { type: 'data-testid', value: testIdMatch[1] };
69
+
70
+ // Data-test: [data-test="submit-btn"]
71
+ const dataTestMatch = selector.match(/\[data-test=["']([^"']+)["']\]/);
72
+ if (dataTestMatch) return { type: 'data-test', value: dataTestMatch[1] };
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Compare two extracted keys
79
+ */
80
+ function keysMatch(a, b) {
81
+ // Same type and value
82
+ if (a.type === b.type && a.value === b.value) return true;
83
+
84
+ // Cross-type: id matches name if values are the same
85
+ if (a.value === b.value) return true;
86
+
87
+ return false;
88
+ }
89
+
90
+ /**
91
+ * Parse existing POM file to extract element metadata
92
+ * @param {string} fileContent - POM file content
93
+ * @param {string} framework - Framework identifier
94
+ * @returns {Object} - { className, elements: [{name, selector, methodName, methodType}] }
95
+ */
96
+ export function parsePomFile(fileContent, framework) {
97
+ switch (framework) {
98
+ case 'playwright-typescript':
99
+ return parsePlaywrightTypeScript(fileContent);
100
+ case 'playwright-python':
101
+ return parsePlaywrightPython(fileContent);
102
+ case 'selenium-python':
103
+ return parseSeleniumPython(fileContent);
104
+ case 'selenium-java':
105
+ return parseSeleniumJava(fileContent);
106
+ default:
107
+ throw new Error(`Unsupported framework for POM parsing: ${framework}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Parse Playwright TypeScript POM file
113
+ * Looks for: this.X = page.locator('...')
114
+ * async fillX( / async clickX(
115
+ */
116
+ function parsePlaywrightTypeScript(content) {
117
+ const classMatch = content.match(/export\s+class\s+(\w+)/);
118
+ const className = classMatch ? classMatch[1] : 'UnknownPage';
119
+
120
+ const elements = [];
121
+ // Match both: page.locator('selector') and page.locator("selector")
122
+ // Handle selectors that contain the other type of quotes inside
123
+ const locatorRegex = /this\.(\w+)\s*=\s*page\.locator\((?:'([^']*)'|"([^"]*)")\)/g;
124
+ let match;
125
+
126
+ while ((match = locatorRegex.exec(content)) !== null) {
127
+ const name = match[1];
128
+ const selector = match[2] || match[3]; // group 2 for single-quoted, group 3 for double-quoted
129
+ if (name === 'page') continue;
130
+
131
+ const methodInfo = findTypeScriptMethod(content, name);
132
+ elements.push({
133
+ name,
134
+ selector,
135
+ tag: guessTagFromSelector(selector),
136
+ type: guessTypeFromSelector(selector),
137
+ methodName: methodInfo.methodName,
138
+ methodType: methodInfo.methodType
139
+ });
140
+ }
141
+
142
+ return { className, elements };
143
+ }
144
+
145
+ /**
146
+ * Find TypeScript method for element
147
+ */
148
+ function findTypeScriptMethod(content, elementName) {
149
+ const capName = elementName.charAt(0).toUpperCase() + elementName.slice(1);
150
+
151
+ // Check for fill method
152
+ const fillRegex = new RegExp(`async\\s+fill${capName}\\s*\\(`);
153
+ if (fillRegex.test(content)) {
154
+ return { methodName: `fill${capName}`, methodType: 'fill' };
155
+ }
156
+
157
+ // Check for click method
158
+ const clickRegex = new RegExp(`async\\s+click${capName}\\s*\\(`);
159
+ if (clickRegex.test(content)) {
160
+ return { methodName: `click${capName}`, methodType: 'click' };
161
+ }
162
+
163
+ // Check for select method
164
+ const selectRegex = new RegExp(`async\\s+select${capName}\\s*\\(`);
165
+ if (selectRegex.test(content)) {
166
+ return { methodName: `select${capName}`, methodType: 'select' };
167
+ }
168
+
169
+ // Default based on element name heuristics
170
+ return { methodName: elementName, methodType: 'click' };
171
+ }
172
+
173
+ /**
174
+ * Parse Playwright Python POM file
175
+ * Looks for: self.X = page.locator('...')
176
+ * def fill_X( / def click_X(
177
+ */
178
+ function parsePlaywrightPython(content) {
179
+ const classMatch = content.match(/class\s+(\w+)/);
180
+ const className = classMatch ? classMatch[1] : 'UnknownPage';
181
+
182
+ const elements = [];
183
+ const locatorRegex = /self\.(\w+)\s*=\s*page\.locator\((?:'([^']*)'|"([^"]*)")\)/g;
184
+ let match;
185
+
186
+ while ((match = locatorRegex.exec(content)) !== null) {
187
+ const name = match[1];
188
+ const selector = match[2] || match[3];
189
+ if (name === 'page') continue;
190
+
191
+ const methodInfo = findPythonMethod(content, name);
192
+ elements.push({
193
+ name,
194
+ selector,
195
+ tag: guessTagFromSelector(selector),
196
+ type: guessTypeFromSelector(selector),
197
+ methodName: methodInfo.methodName,
198
+ methodType: methodInfo.methodType
199
+ });
200
+ }
201
+
202
+ return { className, elements };
203
+ }
204
+
205
+ /**
206
+ * Find Python method for element
207
+ */
208
+ function findPythonMethod(content, elementName) {
209
+ const fillRegex = new RegExp(`def\\s+fill_${elementName}\\s*\\(`);
210
+ if (fillRegex.test(content)) {
211
+ return { methodName: `fill_${elementName}`, methodType: 'fill' };
212
+ }
213
+
214
+ const clickRegex = new RegExp(`def\\s+click_${elementName}\\s*\\(`);
215
+ if (clickRegex.test(content)) {
216
+ return { methodName: `click_${elementName}`, methodType: 'click' };
217
+ }
218
+
219
+ const selectRegex = new RegExp(`def\\s+select_${elementName}\\s*\\(`);
220
+ if (selectRegex.test(content)) {
221
+ return { methodName: `select_${elementName}`, methodType: 'select' };
222
+ }
223
+
224
+ return { methodName: elementName, methodType: 'click' };
225
+ }
226
+
227
+ /**
228
+ * Parse Selenium Python POM file
229
+ * Looks for: X = (By.CSS_SELECTOR, '...')
230
+ * def fill_X( / def click_X(
231
+ */
232
+ function parseSeleniumPython(content) {
233
+ const classMatch = content.match(/class\s+(\w+)/);
234
+ const className = classMatch ? classMatch[1] : 'UnknownPage';
235
+
236
+ const elements = [];
237
+ // Match: ELEMENT_NAME = (By.ID, 'value') or (By.CSS_SELECTOR, 'value') etc.
238
+ const locatorRegex = /(\w+)\s*=\s*\(By\.(\w+),\s*['"]([^'"]+)['"]\)/g;
239
+ let match;
240
+
241
+ while ((match = locatorRegex.exec(content)) !== null) {
242
+ const name = match[1].toLowerCase();
243
+ const byType = match[2];
244
+ const value = match[3];
245
+ const selector = convertByToSelector(byType, value);
246
+
247
+ const methodInfo = findPythonMethod(content, name);
248
+ elements.push({
249
+ name,
250
+ selector,
251
+ tag: guessTagFromSelector(selector),
252
+ type: guessTypeFromSelector(selector),
253
+ methodName: methodInfo.methodName,
254
+ methodType: methodInfo.methodType
255
+ });
256
+ }
257
+
258
+ return { className, elements };
259
+ }
260
+
261
+ /**
262
+ * Parse Selenium Java POM file
263
+ * Looks for: By X = By.cssSelector("...")
264
+ * void fillX( / void clickX(
265
+ */
266
+ function parseSeleniumJava(content) {
267
+ const classMatch = content.match(/public\s+class\s+(\w+)/);
268
+ const className = classMatch ? classMatch[1] : 'UnknownPage';
269
+
270
+ const elements = [];
271
+ // Match: private static final By ELEMENT = By.id("value") or By.cssSelector("value")
272
+ const locatorRegex = /(?:private\s+)?(?:static\s+)?(?:final\s+)?By\s+(\w+)\s*=\s*By\.(\w+)\(["']([^"']+)["']\)/g;
273
+ let match;
274
+
275
+ while ((match = locatorRegex.exec(content)) !== null) {
276
+ const name = match[1].toLowerCase();
277
+ const byMethod = match[2];
278
+ const value = match[3];
279
+ const selector = convertJavaByToSelector(byMethod, value);
280
+
281
+ const methodInfo = findJavaMethod(content, match[1]);
282
+ elements.push({
283
+ name,
284
+ selector,
285
+ tag: guessTagFromSelector(selector),
286
+ type: guessTypeFromSelector(selector),
287
+ methodName: methodInfo.methodName,
288
+ methodType: methodInfo.methodType
289
+ });
290
+ }
291
+
292
+ return { className, elements };
293
+ }
294
+
295
+ /**
296
+ * Find Java method for element
297
+ */
298
+ function findJavaMethod(content, elementName) {
299
+ // Convert UPPER_CASE to camelCase for method lookup
300
+ // e.g., SUBMIT_BTN -> submitBtn, EMAIL -> email
301
+ let camelName;
302
+ if (elementName === elementName.toUpperCase()) {
303
+ // ALL_CAPS: split by _, lowercase, then camelCase
304
+ camelName = elementName.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
305
+ } else {
306
+ camelName = elementName.replace(/_([a-z])/gi, (_, c) => c.toUpperCase());
307
+ }
308
+ const capName = camelName.charAt(0).toUpperCase() + camelName.slice(1);
309
+
310
+ const fillRegex = new RegExp(`void\\s+fill${capName}\\s*\\(`);
311
+ if (fillRegex.test(content)) {
312
+ return { methodName: `fill${capName}`, methodType: 'fill' };
313
+ }
314
+
315
+ const clickRegex = new RegExp(`void\\s+click${capName}\\s*\\(`);
316
+ if (clickRegex.test(content)) {
317
+ return { methodName: `click${capName}`, methodType: 'click' };
318
+ }
319
+
320
+ const selectRegex = new RegExp(`void\\s+select${capName}\\s*\\(`);
321
+ if (selectRegex.test(content)) {
322
+ return { methodName: `select${capName}`, methodType: 'select' };
323
+ }
324
+
325
+ return { methodName: camelName, methodType: 'click' };
326
+ }
327
+
328
+ /**
329
+ * Convert Selenium Python By type to CSS selector
330
+ */
331
+ function convertByToSelector(byType, value) {
332
+ switch (byType) {
333
+ case 'ID': return `#${value}`;
334
+ case 'NAME': return `[name="${value}"]`;
335
+ case 'CLASS_NAME': return `.${value}`;
336
+ case 'CSS_SELECTOR': return value;
337
+ case 'TAG_NAME': return value;
338
+ default: return value;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Convert Selenium Java By method to CSS selector
344
+ */
345
+ function convertJavaByToSelector(byMethod, value) {
346
+ switch (byMethod) {
347
+ case 'id': return `#${value}`;
348
+ case 'name': return `[name="${value}"]`;
349
+ case 'className': return `.${value}`;
350
+ case 'cssSelector': return value;
351
+ case 'tagName': return value;
352
+ default: return value;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Guess HTML tag from selector
358
+ */
359
+ function guessTagFromSelector(selector) {
360
+ const tagMatch = selector.match(/^(input|textarea|select|button|a)\b/);
361
+ return tagMatch ? tagMatch[1] : 'element';
362
+ }
363
+
364
+ /**
365
+ * Guess element type from selector
366
+ */
367
+ function guessTypeFromSelector(selector) {
368
+ if (selector.includes('type="password"')) return 'password';
369
+ if (selector.includes('type="email"')) return 'email';
370
+ if (selector.includes('type="text"')) return 'text';
371
+ if (selector.includes('type="submit"')) return 'submit';
372
+ return 'element';
373
+ }
@@ -283,6 +283,78 @@ export class SeleniumJavaGenerator extends CodeGeneratorBase {
283
283
  }
284
284
  }
285
285
 
286
+ // ========================================
287
+ // POM INTEGRATION
288
+ // ========================================
289
+
290
+ /**
291
+ * Generate POM import
292
+ */
293
+ generatePomImports(className, importPath) {
294
+ return [`import ${importPath || 'pages'}.${className};`];
295
+ }
296
+
297
+ /**
298
+ * Generate POM instantiation
299
+ */
300
+ generatePomInstantiation(className) {
301
+ const varName = className.charAt(0).toLowerCase() + className.slice(1);
302
+ return [this.indent(`${className} ${varName} = new ${className}(driver);`), ''];
303
+ }
304
+
305
+ /**
306
+ * Generate POM goto
307
+ */
308
+ generatePomGoto(url) {
309
+ const varName = this.options.pomClassName.charAt(0).toLowerCase() + this.options.pomClassName.slice(1);
310
+ return [this.indent(`${varName}.goTo();`)];
311
+ }
312
+
313
+ /**
314
+ * Generate POM-based action
315
+ */
316
+ generatePomAction(action, pomElement) {
317
+ const varName = this.options.pomClassName.charAt(0).toLowerCase() + this.options.pomClassName.slice(1);
318
+ const lines = [];
319
+
320
+ const comment = this.generateActionComment(action);
321
+ if (comment.length > 0) lines.push(...comment);
322
+
323
+ switch (action.type) {
324
+ case 'type': {
325
+ const text = action.data?.text || '';
326
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(text)}");`));
327
+ break;
328
+ }
329
+ case 'click':
330
+ if (pomElement.methodType === 'click') {
331
+ lines.push(this.indent(`${varName}.${pomElement.methodName}();`));
332
+ } else {
333
+ lines.push(this.indent(`${varName}.${pomElement.name}.click();`));
334
+ }
335
+ break;
336
+ case 'select': {
337
+ const value = action.data?.value || '';
338
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(value)}");`));
339
+ break;
340
+ }
341
+ case 'hover': {
342
+ lines.push(this.indent(`WebElement element = driver.findElement(By.cssSelector("${this.escapeString(pomElement.selector)}"));`));
343
+ lines.push(this.indent(`Actions hoverActions = new Actions(driver);`));
344
+ lines.push(this.indent(`hoverActions.moveToElement(element).perform();`));
345
+ break;
346
+ }
347
+ case 'navigate':
348
+ lines.push(this.indent(`${varName}.goTo();`));
349
+ break;
350
+ default:
351
+ return null;
352
+ }
353
+
354
+ if (lines.length > 0) lines.push('');
355
+ return lines;
356
+ }
357
+
286
358
  /**
287
359
  * Generate URL assertion
288
360
  */
@@ -303,6 +303,81 @@ export class SeleniumPythonGenerator extends CodeGeneratorBase {
303
303
  }
304
304
  }
305
305
 
306
+ // ========================================
307
+ // POM INTEGRATION
308
+ // ========================================
309
+
310
+ /**
311
+ * Generate POM import
312
+ */
313
+ generatePomImports(className, importPath) {
314
+ if (importPath) {
315
+ return [`from ${importPath} import ${className}`];
316
+ }
317
+ const moduleName = className.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
318
+ return [`from ${moduleName} import ${className}`];
319
+ }
320
+
321
+ /**
322
+ * Generate POM instantiation
323
+ */
324
+ generatePomInstantiation(className) {
325
+ const varName = className.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
326
+ return [this.indent(`${varName} = ${className}(driver)`), ''];
327
+ }
328
+
329
+ /**
330
+ * Generate POM goto
331
+ */
332
+ generatePomGoto(url) {
333
+ const varName = this.options.pomClassName.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
334
+ return [this.indent(`${varName}.goto()`)];
335
+ }
336
+
337
+ /**
338
+ * Generate POM-based action
339
+ */
340
+ generatePomAction(action, pomElement) {
341
+ const varName = this.options.pomClassName.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
342
+ const lines = [];
343
+
344
+ const comment = this.generateActionComment(action);
345
+ if (comment.length > 0) lines.push(...comment);
346
+
347
+ switch (action.type) {
348
+ case 'type': {
349
+ const text = action.data?.text || '';
350
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(text)}")`));
351
+ break;
352
+ }
353
+ case 'click':
354
+ if (pomElement.methodType === 'click') {
355
+ lines.push(this.indent(`${varName}.${pomElement.methodName}()`));
356
+ } else {
357
+ lines.push(this.indent(`${varName}.${pomElement.name}.click()`));
358
+ }
359
+ break;
360
+ case 'select': {
361
+ const value = action.data?.value || '';
362
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(value)}")`));
363
+ break;
364
+ }
365
+ case 'hover': {
366
+ lines.push(this.indent(`element = ${varName}.driver.find_element(By.CSS_SELECTOR, "${this.escapeString(pomElement.selector)}")`));
367
+ lines.push(this.indent(`ActionChains(${varName}.driver).move_to_element(element).perform()`));
368
+ break;
369
+ }
370
+ case 'navigate':
371
+ lines.push(this.indent(`${varName}.goto()`));
372
+ break;
373
+ default:
374
+ return null;
375
+ }
376
+
377
+ if (lines.length > 0) lines.push('');
378
+ return lines;
379
+ }
380
+
306
381
  /**
307
382
  * Generate URL assertion
308
383
  */