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.
- package/CHANGELOG.md +40 -0
- package/README.md +129 -24
- package/SPEC-pom-integration.md +227 -0
- package/SPEC-swagger-api-tools.md +3101 -0
- package/index.js +503 -198
- package/package.json +2 -1
- package/pom/apom-tree-converter.js +5 -26
- package/recorder/page-object-generator.js +45 -1
- package/server/tool-definitions.js +54 -5
- package/server/tool-schemas.js +29 -0
- package/test-swagger-phase1.mjs +959 -0
- package/utils/api-generators/api-models-python.js +448 -0
- package/utils/api-generators/api-models-typescript.js +375 -0
- package/utils/code-generators/code-generator-base.js +111 -6
- package/utils/code-generators/playwright-python.js +74 -0
- package/utils/code-generators/playwright-typescript.js +69 -0
- package/utils/code-generators/pom-integrator.js +373 -0
- package/utils/code-generators/selenium-java.js +72 -0
- package/utils/code-generators/selenium-python.js +75 -0
- package/utils/hints-generator.js +114 -19
- package/utils/openapi/helpers.js +25 -0
- package/utils/openapi/parser.js +448 -0
- package/utils/openapi/ref-resolver.js +149 -0
- package/utils/openapi/type-mapper.js +174 -0
- package/nul +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|