chrometools-mcp 3.3.8 → 3.3.9

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,375 @@
1
+ /**
2
+ * utils/api-generators/api-models-typescript.js
3
+ *
4
+ * Generates TypeScript interfaces/types from OpenAPI schemas.
5
+ */
6
+
7
+ import { TypeMapper } from '../openapi/type-mapper.js';
8
+ import { mergeAllOf } from '../openapi/ref-resolver.js';
9
+ import { arraysEqual, schemaSignature } from '../openapi/helpers.js';
10
+
11
+ export class ApiModelsTypeScriptGenerator {
12
+ constructor(schemas, options = {}) {
13
+ this.schemas = schemas;
14
+ this.options = {
15
+ style: 'interface', // 'interface' | 'type'
16
+ includeEnums: true,
17
+ includeValidation: false,
18
+ ...options
19
+ };
20
+ this.generatedEnums = new Set();
21
+ }
22
+
23
+ /**
24
+ * Generate full file content
25
+ * @param {Object} metadata - { title, source, version }
26
+ * @returns {string}
27
+ */
28
+ generate(metadata = {}) {
29
+ const lines = [];
30
+
31
+ // Header
32
+ lines.push(...this.generateHeader(metadata));
33
+ lines.push('');
34
+
35
+ // Enums first
36
+ if (this.options.includeEnums) {
37
+ const enumLines = this.generateAllEnums();
38
+ if (enumLines.length > 0) {
39
+ lines.push(...enumLines);
40
+ }
41
+ }
42
+
43
+ // Models in dependency order
44
+ const sorted = this.topologicalSort();
45
+ for (const name of sorted) {
46
+ const schema = this.schemas[name];
47
+ if (!schema) continue;
48
+
49
+ // Skip if it's a pure enum (already generated)
50
+ if (schema.enum && schema.type === 'string' && this.generatedEnums.has(name)) continue;
51
+
52
+ const modelLines = this.generateModel(name, schema);
53
+ if (modelLines.length > 0) {
54
+ lines.push(...modelLines);
55
+ lines.push('');
56
+ }
57
+ }
58
+
59
+ return lines.join('\n');
60
+ }
61
+
62
+ generateHeader(metadata) {
63
+ const lines = [];
64
+ lines.push(`// Generated from ${metadata.title || 'OpenAPI'} (${metadata.source || 'unknown source'})`);
65
+ lines.push(`// OpenAPI ${metadata.version || 'unknown'} | Generated at ${new Date().toISOString()}`);
66
+ return lines;
67
+ }
68
+
69
+ /**
70
+ * Find all enums in schemas and generate them
71
+ */
72
+ generateAllEnums() {
73
+ const lines = [];
74
+ for (const [name, schema] of Object.entries(this.schemas)) {
75
+ // Top-level enum schema
76
+ if (schema.enum && schema.type === 'string') {
77
+ lines.push(...this.generateEnum(name, schema.enum, schema.description));
78
+ lines.push('');
79
+ this.generatedEnums.add(name);
80
+ continue;
81
+ }
82
+ // Enums inside properties (skip if values match a top-level enum)
83
+ if (schema.properties) {
84
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
85
+ if (propSchema.enum && propSchema.type === 'string') {
86
+ // Check if values match an already-generated top-level enum
87
+ const matchingEnum = this._findMatchingEnum(propSchema.enum);
88
+ if (matchingEnum) continue;
89
+
90
+ const enumName = name + propName.charAt(0).toUpperCase() + propName.slice(1);
91
+ if (!this.generatedEnums.has(enumName)) {
92
+ lines.push(...this.generateEnum(enumName, propSchema.enum, propSchema.description));
93
+ lines.push('');
94
+ this.generatedEnums.add(enumName);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ return lines;
101
+ }
102
+
103
+ generateEnum(name, values, description) {
104
+ const lines = [];
105
+ if (description) {
106
+ lines.push(`/** ${description} */`);
107
+ }
108
+ lines.push(`export enum ${name} {`);
109
+ for (const value of values) {
110
+ let key = value.charAt(0).toUpperCase() + value.slice(1).replace(/[^a-zA-Z0-9]/g, '_');
111
+ if (/^\d/.test(key)) key = `_${key}`;
112
+ lines.push(` ${key} = '${value}',`);
113
+ }
114
+ lines.push('}');
115
+ return lines;
116
+ }
117
+
118
+ /**
119
+ * Generate a single model (interface or type)
120
+ */
121
+ generateModel(name, schema) {
122
+ // Handle allOf
123
+ if (schema.allOf) {
124
+ return this.generateAllOfModel(name, schema);
125
+ }
126
+
127
+ // Handle oneOf / anyOf → union type
128
+ if (schema.oneOf || schema.anyOf) {
129
+ return this.generateUnionType(name, schema.oneOf || schema.anyOf, schema.description);
130
+ }
131
+
132
+ // Standard object with properties
133
+ if (schema.type === 'object' || schema.properties) {
134
+ return this.generateObjectModel(name, schema);
135
+ }
136
+
137
+ // Simple type alias (e.g., type Foo = string)
138
+ if (schema.type && schema.type !== 'object') {
139
+ const tsType = TypeMapper.toTypeScript(schema);
140
+ return [`export type ${name} = ${tsType};`];
141
+ }
142
+
143
+ // Fallback: empty interface
144
+ if (this.options.style === 'interface') {
145
+ return [`export interface ${name} {}`];
146
+ }
147
+ return [`export type ${name} = Record<string, unknown>;`];
148
+ }
149
+
150
+ generateObjectModel(name, schema) {
151
+ const lines = [];
152
+ const required = new Set(schema.required || []);
153
+
154
+ if (schema.description) {
155
+ lines.push(`/** ${schema.description} */`);
156
+ }
157
+
158
+ if (this.options.style === 'interface') {
159
+ lines.push(`export interface ${name} {`);
160
+ } else {
161
+ lines.push(`export type ${name} = {`);
162
+ }
163
+
164
+ for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
165
+ const isRequired = required.has(propName);
166
+ const type = this.resolvePropertyType(name, propName, propSchema);
167
+ const opt = isRequired ? '' : '?';
168
+
169
+ if (propSchema.description && this.options.includeValidation) {
170
+ lines.push(` /** ${propSchema.description} */`);
171
+ }
172
+
173
+ lines.push(` ${propName}${opt}: ${type};`);
174
+ }
175
+
176
+ // additionalProperties
177
+ if (schema.additionalProperties) {
178
+ const valueType = typeof schema.additionalProperties === 'object'
179
+ ? TypeMapper.toTypeScript(schema.additionalProperties)
180
+ : 'any';
181
+ lines.push(` [key: string]: ${valueType};`);
182
+ }
183
+
184
+ if (this.options.style === 'interface') {
185
+ lines.push('}');
186
+ } else {
187
+ lines.push('};');
188
+ }
189
+
190
+ return lines;
191
+ }
192
+
193
+ generateAllOfModel(name, schema) {
194
+ const lines = [];
195
+ if (schema.description) {
196
+ lines.push(`/** ${schema.description} */`);
197
+ }
198
+
199
+ // Find named refs and inline schemas
200
+ const namedRefs = [];
201
+ const inlineSchemas = [];
202
+
203
+ for (const sub of schema.allOf) {
204
+ const refName = this._findSchemaName(sub);
205
+ if (refName) {
206
+ namedRefs.push(refName);
207
+ } else if (sub.properties) {
208
+ inlineSchemas.push(sub);
209
+ }
210
+ }
211
+
212
+ if (this.options.style === 'interface' && namedRefs.length > 0 && inlineSchemas.length > 0) {
213
+ // interface X extends A, B { extra props }
214
+ const merged = mergeAllOf(inlineSchemas);
215
+ lines.push(`export interface ${name} extends ${namedRefs.join(', ')} {`);
216
+ const required = new Set(merged.required || []);
217
+ for (const [propName, propSchema] of Object.entries(merged.properties || {})) {
218
+ const isRequired = required.has(propName);
219
+ const type = this.resolvePropertyType(name, propName, propSchema);
220
+ lines.push(` ${propName}${isRequired ? '' : '?'}: ${type};`);
221
+ }
222
+ lines.push('}');
223
+ } else if (namedRefs.length > 0 && inlineSchemas.length === 0) {
224
+ // Pure extension
225
+ if (this.options.style === 'interface') {
226
+ lines.push(`export interface ${name} extends ${namedRefs.join(', ')} {}`);
227
+ } else {
228
+ lines.push(`export type ${name} = ${namedRefs.join(' & ')};`);
229
+ }
230
+ } else {
231
+ // Merge everything into one
232
+ const merged = mergeAllOf(schema.allOf);
233
+ return this.generateObjectModel(name, merged);
234
+ }
235
+
236
+ return lines;
237
+ }
238
+
239
+ generateUnionType(name, schemas, description) {
240
+ const lines = [];
241
+ if (description) {
242
+ lines.push(`/** ${description} */`);
243
+ }
244
+ const types = schemas.map(s => {
245
+ const refName = this._findSchemaName(s);
246
+ if (refName) return refName;
247
+ return TypeMapper.toTypeScript(s);
248
+ });
249
+ lines.push(`export type ${name} = ${types.join(' | ')};`);
250
+ return lines;
251
+ }
252
+
253
+ /**
254
+ * Resolve type for a property, using enum name if available
255
+ */
256
+ resolvePropertyType(parentName, propName, propSchema) {
257
+ // Enum property → reference generated enum
258
+ if (propSchema.enum && propSchema.type === 'string' && this.options.includeEnums) {
259
+ const enumName = parentName + propName.charAt(0).toUpperCase() + propName.slice(1);
260
+ // Check if this enum matches a top-level enum
261
+ for (const [schemaName, schema] of Object.entries(this.schemas)) {
262
+ if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, propSchema.enum)) {
263
+ return propSchema.nullable ? `${schemaName} | null` : schemaName;
264
+ }
265
+ }
266
+ return propSchema.nullable ? `${enumName} | null` : enumName;
267
+ }
268
+
269
+ // Array with items that might be a known schema
270
+ if (propSchema.type === 'array' && propSchema.items) {
271
+ const itemName = this._findSchemaName(propSchema.items);
272
+ if (itemName) {
273
+ return propSchema.nullable ? `${itemName}[] | null` : `${itemName}[]`;
274
+ }
275
+ }
276
+
277
+ // Object reference
278
+ const refName = this._findSchemaName(propSchema);
279
+ if (refName) {
280
+ return propSchema.nullable ? `${refName} | null` : refName;
281
+ }
282
+
283
+ return TypeMapper.toTypeScript(propSchema);
284
+ }
285
+
286
+ /**
287
+ * Find a top-level enum schema whose values match
288
+ */
289
+ _findMatchingEnum(enumValues) {
290
+ for (const [name, schema] of Object.entries(this.schemas)) {
291
+ if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, enumValues)) {
292
+ return name;
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Try to find schema name for a resolved schema
300
+ */
301
+ _findSchemaName(schema) {
302
+ if (!schema || typeof schema !== 'object') return null;
303
+ if (schema.$circularRef) return schema.$circularRef;
304
+ if (schema._refName && this.schemas[schema._refName]) return schema._refName;
305
+
306
+ for (const [name, s] of Object.entries(this.schemas)) {
307
+ if (s === schema) return name;
308
+ if (s.properties && schema.properties) {
309
+ const sSig = schemaSignature(s.properties);
310
+ const schemaSig = schemaSignature(schema.properties);
311
+ if (sSig === schemaSig && sSig.length > 0) return name;
312
+ }
313
+ }
314
+ return null;
315
+ }
316
+
317
+ /**
318
+ * Topological sort schemas by dependencies
319
+ */
320
+ topologicalSort() {
321
+ const names = Object.keys(this.schemas);
322
+ const visited = new Set();
323
+ const result = [];
324
+
325
+ const visit = (name) => {
326
+ if (visited.has(name)) return;
327
+ visited.add(name);
328
+
329
+ const deps = this.getDependencies(name);
330
+ for (const dep of deps) {
331
+ if (this.schemas[dep]) visit(dep);
332
+ }
333
+ result.push(name);
334
+ };
335
+
336
+ for (const name of names) {
337
+ visit(name);
338
+ }
339
+
340
+ return result;
341
+ }
342
+
343
+ /**
344
+ * Get dependency names for a schema
345
+ */
346
+ getDependencies(name) {
347
+ const schema = this.schemas[name];
348
+ if (!schema) return [];
349
+ const deps = new Set();
350
+ const seen = new WeakSet();
351
+
352
+ const collectDeps = (obj) => {
353
+ if (!obj || typeof obj !== 'object') return;
354
+ if (obj.$circularRef) { deps.add(obj.$circularRef); return; }
355
+ if (seen.has(obj)) return;
356
+ seen.add(obj);
357
+ if (Array.isArray(obj)) { obj.forEach(collectDeps); return; }
358
+
359
+ const refName = this._findSchemaName(obj);
360
+ if (refName && refName !== name) deps.add(refName);
361
+
362
+ for (const val of Object.values(obj)) {
363
+ if (val && typeof val === 'object') collectDeps(val);
364
+ }
365
+ };
366
+
367
+ if (schema.properties) collectDeps(schema.properties);
368
+ if (schema.allOf) collectDeps(schema.allOf);
369
+ if (schema.oneOf) collectDeps(schema.oneOf);
370
+ if (schema.anyOf) collectDeps(schema.anyOf);
371
+ if (schema.items) collectDeps(schema.items);
372
+
373
+ return [...deps];
374
+ }
375
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { cleanSelector, getBestSelector, analyzeSelectorStability } from '../selector-cleaner.js';
9
+ import { matchActionToPomElement } from './pom-integrator.js';
9
10
 
10
11
  /**
11
12
  * Base code generator class
@@ -39,20 +40,51 @@ export class CodeGeneratorBase {
39
40
 
40
41
  // Generate imports
41
42
  lines.push(...this.generateImports());
43
+
44
+ // Generate POM import if POM mode
45
+ if (this.options.pomClassName) {
46
+ lines.push(...this.generatePomImports(this.options.pomClassName, this.options.pomImportPath));
47
+ }
48
+
42
49
  lines.push('');
43
50
 
44
51
  // Generate test function/method
45
52
  lines.push(...this.generateTestHeader(testName, description));
46
- lines.push('');
53
+
54
+ // Generate POM instantiation if POM mode
55
+ if (this.options.pomClassName) {
56
+ lines.push(...this.generatePomInstantiation(this.options.pomClassName));
57
+ } else {
58
+ lines.push('');
59
+ }
47
60
 
48
61
  // Generate navigation to entry URL
49
62
  if (entryUrl) {
50
- lines.push(...this.generateNavigate(entryUrl));
63
+ if (this.options.pomClassName) {
64
+ lines.push(...this.generatePomGoto(entryUrl));
65
+ } else {
66
+ lines.push(...this.generateNavigate(entryUrl));
67
+ }
51
68
  }
52
69
 
53
70
  // Generate actions
54
71
  for (const action of scenario.chain) {
55
- const actionCode = this.generateAction(action);
72
+ let actionCode = null;
73
+
74
+ // Try POM matching first
75
+ if (this.options.pomElements) {
76
+ const selector = this.prepareSelector(action);
77
+ const match = matchActionToPomElement(selector, this.options.pomElements);
78
+ if (match) {
79
+ actionCode = this.generatePomAction(action, match);
80
+ }
81
+ }
82
+
83
+ // Fallback to raw action
84
+ if (!actionCode) {
85
+ actionCode = this.generateAction(action);
86
+ }
87
+
56
88
  if (actionCode && actionCode.length > 0) {
57
89
  lines.push(...actionCode);
58
90
  }
@@ -252,18 +284,49 @@ export class CodeGeneratorBase {
252
284
 
253
285
  const lines = [];
254
286
 
287
+ // Generate POM import if POM mode (needed even in test-only for append)
288
+ if (this.options.pomClassName) {
289
+ lines.push(...this.generatePomImports(this.options.pomClassName, this.options.pomImportPath));
290
+ lines.push('');
291
+ }
292
+
255
293
  // Generate test function/method header
256
294
  lines.push(...this.generateTestHeader(testName, description));
257
- lines.push('');
295
+
296
+ // Generate POM instantiation if POM mode
297
+ if (this.options.pomClassName) {
298
+ lines.push(...this.generatePomInstantiation(this.options.pomClassName));
299
+ } else {
300
+ lines.push('');
301
+ }
258
302
 
259
303
  // Generate navigation to entry URL
260
304
  if (entryUrl) {
261
- lines.push(...this.generateNavigate(entryUrl));
305
+ if (this.options.pomClassName) {
306
+ lines.push(...this.generatePomGoto(entryUrl));
307
+ } else {
308
+ lines.push(...this.generateNavigate(entryUrl));
309
+ }
262
310
  }
263
311
 
264
312
  // Generate actions
265
313
  for (const action of scenario.chain) {
266
- const actionCode = this.generateAction(action);
314
+ let actionCode = null;
315
+
316
+ // Try POM matching first
317
+ if (this.options.pomElements) {
318
+ const selector = this.prepareSelector(action);
319
+ const match = matchActionToPomElement(selector, this.options.pomElements);
320
+ if (match) {
321
+ actionCode = this.generatePomAction(action, match);
322
+ }
323
+ }
324
+
325
+ // Fallback to raw action
326
+ if (!actionCode) {
327
+ actionCode = this.generateAction(action);
328
+ }
329
+
267
330
  if (actionCode && actionCode.length > 0) {
268
331
  lines.push(...actionCode);
269
332
  }
@@ -284,6 +347,48 @@ export class CodeGeneratorBase {
284
347
  return lines.join('\n');
285
348
  }
286
349
 
350
+ // ========================================
351
+ // POM INTEGRATION METHODS (override in subclasses)
352
+ // ========================================
353
+
354
+ /**
355
+ * Generate POM import statement
356
+ * @param {string} className - POM class name
357
+ * @param {string} importPath - Import path (optional)
358
+ * @returns {string[]} - Import lines
359
+ */
360
+ generatePomImports(className, importPath) {
361
+ return [];
362
+ }
363
+
364
+ /**
365
+ * Generate POM class instantiation
366
+ * @param {string} className - POM class name
367
+ * @returns {string[]} - Instantiation lines
368
+ */
369
+ generatePomInstantiation(className) {
370
+ return [];
371
+ }
372
+
373
+ /**
374
+ * Generate POM-based action (uses POM method instead of raw selector)
375
+ * @param {Object} action - Recorded action
376
+ * @param {Object} pomElement - Matched POM element metadata
377
+ * @returns {string[]|null} - Action lines or null for fallback
378
+ */
379
+ generatePomAction(action, pomElement) {
380
+ return null;
381
+ }
382
+
383
+ /**
384
+ * Generate POM goto (navigation via POM instance)
385
+ * @param {string} url - URL to navigate to
386
+ * @returns {string[]} - Navigation lines
387
+ */
388
+ generatePomGoto(url) {
389
+ return this.generateNavigate(url);
390
+ }
391
+
287
392
  /**
288
393
  * Append test to existing file content
289
394
  * Must be implemented by subclass for language-specific file parsing
@@ -228,6 +228,80 @@ export class PlaywrightPythonGenerator extends CodeGeneratorBase {
228
228
  }
229
229
  }
230
230
 
231
+ // ========================================
232
+ // POM INTEGRATION
233
+ // ========================================
234
+
235
+ /**
236
+ * Generate POM import
237
+ */
238
+ generatePomImports(className, importPath) {
239
+ if (importPath) {
240
+ return [`from ${importPath} import ${className}`];
241
+ }
242
+ // Convert ClassName to class_name for Python module
243
+ const moduleName = className.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
244
+ return [`from ${moduleName} import ${className}`];
245
+ }
246
+
247
+ /**
248
+ * Generate POM instantiation
249
+ */
250
+ generatePomInstantiation(className) {
251
+ const varName = className.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
252
+ return [this.indent(`${varName} = ${className}(page)`), ''];
253
+ }
254
+
255
+ /**
256
+ * Generate POM goto
257
+ */
258
+ generatePomGoto(url) {
259
+ const varName = this.options.pomClassName.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
260
+ return [this.indent(`${varName}.goto()`)];
261
+ }
262
+
263
+ /**
264
+ * Generate POM-based action
265
+ */
266
+ generatePomAction(action, pomElement) {
267
+ const varName = this.options.pomClassName.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
268
+ const lines = [];
269
+
270
+ const comment = this.generateActionComment(action);
271
+ if (comment.length > 0) lines.push(...comment);
272
+
273
+ switch (action.type) {
274
+ case 'type': {
275
+ const text = action.data?.text || '';
276
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(text)}")`));
277
+ break;
278
+ }
279
+ case 'click':
280
+ if (pomElement.methodType === 'click') {
281
+ lines.push(this.indent(`${varName}.${pomElement.methodName}()`));
282
+ } else {
283
+ lines.push(this.indent(`${varName}.${pomElement.name}.click()`));
284
+ }
285
+ break;
286
+ case 'select': {
287
+ const value = action.data?.value || '';
288
+ lines.push(this.indent(`${varName}.${pomElement.methodName}("${this.escapeString(value)}")`));
289
+ break;
290
+ }
291
+ case 'hover':
292
+ lines.push(this.indent(`${varName}.${pomElement.name}.hover()`));
293
+ break;
294
+ case 'navigate':
295
+ lines.push(this.indent(`${varName}.goto()`));
296
+ break;
297
+ default:
298
+ return null;
299
+ }
300
+
301
+ if (lines.length > 0) lines.push('');
302
+ return lines;
303
+ }
304
+
231
305
  /**
232
306
  * Generate URL assertion
233
307
  */
@@ -209,6 +209,75 @@ export class PlaywrightTypeScriptGenerator extends CodeGeneratorBase {
209
209
  }
210
210
  }
211
211
 
212
+ // ========================================
213
+ // POM INTEGRATION
214
+ // ========================================
215
+
216
+ /**
217
+ * Generate POM import
218
+ */
219
+ generatePomImports(className, importPath) {
220
+ return [`import { ${className} } from '${importPath || './' + className}';`];
221
+ }
222
+
223
+ /**
224
+ * Generate POM instantiation
225
+ */
226
+ generatePomInstantiation(className) {
227
+ const varName = className.charAt(0).toLowerCase() + className.slice(1);
228
+ return [this.indent(`const ${varName} = new ${className}(page);`), ''];
229
+ }
230
+
231
+ /**
232
+ * Generate POM goto
233
+ */
234
+ generatePomGoto(url) {
235
+ const varName = this.options.pomClassName.charAt(0).toLowerCase() + this.options.pomClassName.slice(1);
236
+ return [this.indent(`await ${varName}.goto();`)];
237
+ }
238
+
239
+ /**
240
+ * Generate POM-based action
241
+ */
242
+ generatePomAction(action, pomElement) {
243
+ const varName = this.options.pomClassName.charAt(0).toLowerCase() + this.options.pomClassName.slice(1);
244
+ const lines = [];
245
+
246
+ const comment = this.generateActionComment(action);
247
+ if (comment.length > 0) lines.push(...comment);
248
+
249
+ switch (action.type) {
250
+ case 'type': {
251
+ const text = action.data?.text || '';
252
+ lines.push(this.indent(`await ${varName}.${pomElement.methodName}('${this.escapeString(text)}');`));
253
+ break;
254
+ }
255
+ case 'click':
256
+ if (pomElement.methodType === 'click') {
257
+ lines.push(this.indent(`await ${varName}.${pomElement.methodName}();`));
258
+ } else {
259
+ lines.push(this.indent(`await ${varName}.${pomElement.name}.click();`));
260
+ }
261
+ break;
262
+ case 'select': {
263
+ const value = action.data?.value || '';
264
+ lines.push(this.indent(`await ${varName}.${pomElement.methodName}('${this.escapeString(value)}');`));
265
+ break;
266
+ }
267
+ case 'hover':
268
+ lines.push(this.indent(`await ${varName}.${pomElement.name}.hover();`));
269
+ break;
270
+ case 'navigate':
271
+ lines.push(this.indent(`await ${varName}.goto();`));
272
+ break;
273
+ default:
274
+ return null; // Fallback to raw action
275
+ }
276
+
277
+ if (lines.length > 0) lines.push('');
278
+ return lines;
279
+ }
280
+
212
281
  /**
213
282
  * Generate URL assertion
214
283
  */