@immense/vue-pom-generator 1.0.46 → 1.0.48
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/README.md +45 -11
- package/RELEASE_NOTES.md +38 -27
- package/class-generation/index.ts +1421 -682
- package/dist/class-generation/index.d.ts +8 -0
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/index.cjs +1501 -691
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1504 -694
- package/dist/index.mjs.map +1 -1
- package/dist/manifest-generator.d.ts.map +1 -1
- package/dist/method-generation.d.ts +2 -0
- package/dist/method-generation.d.ts.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
- package/dist/plugin/support/build-plugin.d.ts +3 -1
- package/dist/plugin/support/build-plugin.d.ts.map +1 -1
- package/dist/plugin/support/dev-plugin.d.ts +3 -1
- package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
- package/dist/plugin/support-plugins.d.ts +3 -1
- package/dist/plugin/support-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +32 -7
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/tests/fixtures/generated-tsc/BasePage.full.d.ts +19 -0
- package/dist/tests/fixtures/generated-tsc/BasePage.full.d.ts.map +1 -0
- package/dist/tests/fixtures/generated-tsc/BasePage.minimal.d.ts +8 -0
- package/dist/tests/fixtures/generated-tsc/BasePage.minimal.d.ts.map +1 -0
- package/dist/tests/fixtures/generated-tsc/Pointer.d.ts +6 -0
- package/dist/tests/fixtures/generated-tsc/Pointer.d.ts.map +1 -0
- package/dist/transform.d.ts +1 -0
- package/dist/transform.d.ts.map +1 -1
- package/dist/typescript-codegen.d.ts +34 -0
- package/dist/typescript-codegen.d.ts.map +1 -0
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -29,8 +29,9 @@ const fs = require("node:fs");
|
|
|
29
29
|
const compilerDom = require("@vue/compiler-dom");
|
|
30
30
|
const compilerSfc = require("@vue/compiler-sfc");
|
|
31
31
|
const parser = require("@babel/parser");
|
|
32
|
-
const jsdom = require("jsdom");
|
|
33
32
|
const compilerCore = require("@vue/compiler-core");
|
|
33
|
+
const tsMorph = require("ts-morph");
|
|
34
|
+
const jsdom = require("jsdom");
|
|
34
35
|
const types = require("@babel/types");
|
|
35
36
|
const node_perf_hooks = require("node:perf_hooks");
|
|
36
37
|
const virtualImport = require("vite-plugin-virtual");
|
|
@@ -103,9 +104,97 @@ function createLogger(options) {
|
|
|
103
104
|
}
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
function createTypeScriptProject() {
|
|
108
|
+
return new tsMorph.Project({
|
|
109
|
+
useInMemoryFileSystem: true,
|
|
110
|
+
manipulationSettings: {
|
|
111
|
+
indentationText: tsMorph.IndentationText.FourSpaces,
|
|
112
|
+
newLineKind: tsMorph.NewLineKind.LineFeed,
|
|
113
|
+
quoteKind: tsMorph.QuoteKind.Double,
|
|
114
|
+
useTrailingCommas: false
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function ensureTrailingNewline(text) {
|
|
119
|
+
return text.endsWith("\n") ? text : `${text}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function createTypeScriptWriter() {
|
|
123
|
+
return new tsMorph.CodeBlockWriter({
|
|
124
|
+
newLine: "\n",
|
|
125
|
+
useTabs: false,
|
|
126
|
+
indentNumberOfSpaces: 4
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function renderTypeScript(write) {
|
|
130
|
+
const writer = createTypeScriptWriter();
|
|
131
|
+
write(writer);
|
|
132
|
+
return ensureTrailingNewline(writer.toString());
|
|
133
|
+
}
|
|
134
|
+
function buildCommentBlock(lines) {
|
|
135
|
+
return renderTypeScript((writer) => {
|
|
136
|
+
writer.writeLine("/**");
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
writer.writeLine(` * ${line}`);
|
|
139
|
+
}
|
|
140
|
+
writer.writeLine(" */");
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function buildFilePrefix(options = {}) {
|
|
144
|
+
let prefix = "";
|
|
145
|
+
if (options.referenceLib) {
|
|
146
|
+
prefix += `/// <reference lib="${options.referenceLib}" />
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
if (options.eslintDisableSortImports) {
|
|
150
|
+
prefix += "/* eslint-disable perfectionist/sort-imports */\n";
|
|
151
|
+
}
|
|
152
|
+
if (options.commentLines?.length) {
|
|
153
|
+
prefix += buildCommentBlock(options.commentLines);
|
|
154
|
+
}
|
|
155
|
+
return prefix;
|
|
156
|
+
}
|
|
157
|
+
function renderSourceFile(filePath, build, options = {}) {
|
|
158
|
+
const project = createTypeScriptProject();
|
|
159
|
+
const sourceFile = project.createSourceFile(filePath, "", { overwrite: true });
|
|
160
|
+
build(sourceFile);
|
|
161
|
+
const content = ensureTrailingNewline(sourceFile.getFullText());
|
|
162
|
+
return options.prefixText ? ensureTrailingNewline(`${options.prefixText}${content}`) : content;
|
|
163
|
+
}
|
|
164
|
+
function addNamedImport(sourceFile, options) {
|
|
165
|
+
return sourceFile.addImportDeclaration({
|
|
166
|
+
moduleSpecifier: options.moduleSpecifier,
|
|
167
|
+
isTypeOnly: options.isTypeOnly,
|
|
168
|
+
namedImports: options.namedImports
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function addExportAll(sourceFile, moduleSpecifier) {
|
|
172
|
+
return sourceFile.addExportDeclaration({ moduleSpecifier });
|
|
173
|
+
}
|
|
174
|
+
function createClassMethod(method) {
|
|
175
|
+
return {
|
|
176
|
+
kind: tsMorph.StructureKind.Method,
|
|
177
|
+
...method
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function createClassProperty(property) {
|
|
181
|
+
return {
|
|
182
|
+
kind: tsMorph.StructureKind.Property,
|
|
183
|
+
...property
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createClassGetter(getter) {
|
|
187
|
+
return {
|
|
188
|
+
kind: tsMorph.StructureKind.GetAccessor,
|
|
189
|
+
...getter
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function createClassConstructor(constructorDeclaration) {
|
|
193
|
+
return {
|
|
194
|
+
kind: tsMorph.StructureKind.Constructor,
|
|
195
|
+
...constructorDeclaration
|
|
196
|
+
};
|
|
197
|
+
}
|
|
109
198
|
function upperFirst$1(value) {
|
|
110
199
|
if (!value) {
|
|
111
200
|
return value;
|
|
@@ -115,12 +204,34 @@ function upperFirst$1(value) {
|
|
|
115
204
|
function hasParam(params, name) {
|
|
116
205
|
return Object.prototype.hasOwnProperty.call(params, name);
|
|
117
206
|
}
|
|
118
|
-
function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
207
|
+
function splitTypeAndInitializer(typeExpression) {
|
|
208
|
+
const trimmed = typeExpression.trim();
|
|
209
|
+
const initializerIndex = trimmed.lastIndexOf("=");
|
|
210
|
+
if (initializerIndex < 0) {
|
|
211
|
+
return { type: trimmed };
|
|
122
212
|
}
|
|
123
|
-
return
|
|
213
|
+
return {
|
|
214
|
+
type: trimmed.slice(0, initializerIndex).trim(),
|
|
215
|
+
initializer: trimmed.slice(initializerIndex + 1).trim()
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createParameter(name, typeExpression) {
|
|
219
|
+
const { type, initializer } = splitTypeAndInitializer(typeExpression);
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
type: type || void 0,
|
|
223
|
+
initializer
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function createParameters(params) {
|
|
227
|
+
return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
|
|
228
|
+
}
|
|
229
|
+
function createInlineParameter(name, options = {}) {
|
|
230
|
+
return {
|
|
231
|
+
name,
|
|
232
|
+
type: options.type,
|
|
233
|
+
initializer: options.initializer
|
|
234
|
+
};
|
|
124
235
|
}
|
|
125
236
|
function removeByKeySegment(value) {
|
|
126
237
|
const idx = value.lastIndexOf("ByKey");
|
|
@@ -148,113 +259,135 @@ function uniqueAlternates(primary, alternates) {
|
|
|
148
259
|
function testIdExpression(formattedDataTestId) {
|
|
149
260
|
return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
|
|
150
261
|
}
|
|
262
|
+
function createAsyncMethod(name, parameters, statements) {
|
|
263
|
+
return createClassMethod({
|
|
264
|
+
name,
|
|
265
|
+
isAsync: true,
|
|
266
|
+
parameters,
|
|
267
|
+
statements
|
|
268
|
+
});
|
|
269
|
+
}
|
|
151
270
|
function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
|
|
152
|
-
let content;
|
|
153
271
|
const name = `click${methodName}`;
|
|
154
272
|
const noWaitName = `${name}NoWait`;
|
|
155
|
-
const
|
|
156
|
-
const paramBlockWithWait = paramBlock ? `${paramBlock}, wait: boolean = true` : "wait: boolean = true";
|
|
273
|
+
const baseParameters = createParameters(params);
|
|
157
274
|
const argsForForward = Object.keys(params).join(", ");
|
|
158
275
|
const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
|
|
159
276
|
if (alternates.length > 0) {
|
|
160
277
|
const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
${
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
278
|
+
const clickMethod = createAsyncMethod(
|
|
279
|
+
name,
|
|
280
|
+
hasParam(params, "key") ? [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })] : [createInlineParameter("wait", { type: "boolean", initializer: "true" })],
|
|
281
|
+
(writer) => {
|
|
282
|
+
writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
|
|
283
|
+
writer.writeLine("let lastError: unknown;");
|
|
284
|
+
writer.write("for (const testId of candidates) ").block(() => {
|
|
285
|
+
writer.writeLine("const locator = this.locatorByTestId(testId);");
|
|
286
|
+
writer.write("try ").block(() => {
|
|
287
|
+
writer.write("if (await locator.count()) ").block(() => {
|
|
288
|
+
writer.writeLine('await this.clickLocator(locator, "", wait);');
|
|
289
|
+
writer.writeLine("return;");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
writer.write("catch (e) ").block(() => {
|
|
293
|
+
writer.writeLine("lastError = e;");
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to click any candidate locator for ${name}.");`);
|
|
297
|
+
}
|
|
298
|
+
);
|
|
181
299
|
const noWaitArgs = argsForForward ? `${argsForForward}, false` : "false";
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
300
|
+
const noWaitMethod = createAsyncMethod(
|
|
301
|
+
noWaitName,
|
|
302
|
+
hasParam(params, "key") ? baseParameters : [],
|
|
303
|
+
(writer) => {
|
|
304
|
+
writer.writeLine(`await this.${name}(${noWaitArgs});`);
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
return [clickMethod, noWaitMethod];
|
|
188
308
|
}
|
|
189
309
|
if (hasParam(params, "key")) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`;
|
|
199
|
-
} else {
|
|
200
|
-
content = `${INDENT}async ${name}(wait: boolean = true) {
|
|
201
|
-
${INDENT2}await this.clickByTestId("${formattedDataTestId}", "", wait);
|
|
202
|
-
${INDENT}}
|
|
203
|
-
`;
|
|
204
|
-
content += `
|
|
205
|
-
${INDENT}async ${noWaitName}() {
|
|
206
|
-
${INDENT2}await this.${name}(false);
|
|
207
|
-
${INDENT}}
|
|
208
|
-
`;
|
|
310
|
+
return [
|
|
311
|
+
createAsyncMethod(name, [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
|
|
312
|
+
writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, "", wait);`);
|
|
313
|
+
}),
|
|
314
|
+
createAsyncMethod(noWaitName, baseParameters, (writer) => {
|
|
315
|
+
writer.writeLine(`await this.${name}(${argsForForward}, false);`);
|
|
316
|
+
})
|
|
317
|
+
];
|
|
209
318
|
}
|
|
210
|
-
return
|
|
319
|
+
return [
|
|
320
|
+
createAsyncMethod(name, [createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
|
|
321
|
+
writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", "", wait);`);
|
|
322
|
+
}),
|
|
323
|
+
createAsyncMethod(noWaitName, [], (writer) => {
|
|
324
|
+
writer.writeLine(`await this.${name}(false);`);
|
|
325
|
+
})
|
|
326
|
+
];
|
|
211
327
|
}
|
|
212
328
|
function generateRadioMethod(methodName, formattedDataTestId) {
|
|
213
329
|
const name = `select${methodName}`;
|
|
214
330
|
const hasKey = formattedDataTestId.includes("${key}");
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
`;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
331
|
+
const parameters = hasKey ? [
|
|
332
|
+
createInlineParameter("key", { type: "string" }),
|
|
333
|
+
createInlineParameter("annotationText", { type: "string", initializer: '""' })
|
|
334
|
+
] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
|
|
335
|
+
const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
|
|
336
|
+
return [
|
|
337
|
+
createAsyncMethod(name, parameters, (writer) => {
|
|
338
|
+
writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
|
|
339
|
+
})
|
|
340
|
+
];
|
|
225
341
|
}
|
|
226
342
|
function generateSelectMethod(methodName, formattedDataTestId) {
|
|
227
343
|
const name = `select${methodName}`;
|
|
228
344
|
const needsKey = formattedDataTestId.includes("${key}");
|
|
229
345
|
const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
346
|
+
return [
|
|
347
|
+
createAsyncMethod(
|
|
348
|
+
name,
|
|
349
|
+
[
|
|
350
|
+
createInlineParameter("value", { type: "string" }),
|
|
351
|
+
createInlineParameter("annotationText", { type: "string", initializer: '""' })
|
|
352
|
+
],
|
|
353
|
+
(writer) => {
|
|
354
|
+
writer.writeLine(`const selector = ${selectorExpr};`);
|
|
355
|
+
writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
|
|
356
|
+
writer.writeLine("await this.page.selectOption(selector, value);");
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
];
|
|
238
360
|
}
|
|
239
361
|
function generateVSelectMethod(methodName, formattedDataTestId) {
|
|
240
362
|
const name = `select${methodName}`;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
363
|
+
return [
|
|
364
|
+
createAsyncMethod(
|
|
365
|
+
name,
|
|
366
|
+
[
|
|
367
|
+
createInlineParameter("value", { type: "string" }),
|
|
368
|
+
createInlineParameter("timeOut", { type: "number", initializer: "500" }),
|
|
369
|
+
createInlineParameter("annotationText", { type: "string", initializer: '""' })
|
|
370
|
+
],
|
|
371
|
+
(writer) => {
|
|
372
|
+
writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
];
|
|
250
376
|
}
|
|
251
377
|
function generateTypeMethod(methodName, formattedDataTestId) {
|
|
252
378
|
const name = `type${methodName}`;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
379
|
+
return [
|
|
380
|
+
createAsyncMethod(
|
|
381
|
+
name,
|
|
382
|
+
[
|
|
383
|
+
createInlineParameter("text", { type: "string" }),
|
|
384
|
+
createInlineParameter("annotationText", { type: "string", initializer: '""' })
|
|
385
|
+
],
|
|
386
|
+
(writer) => {
|
|
387
|
+
writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
];
|
|
258
391
|
}
|
|
259
392
|
function isAllDigits(value) {
|
|
260
393
|
if (!value)
|
|
@@ -276,90 +409,119 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
|
|
|
276
409
|
if (needsKey) {
|
|
277
410
|
const keyType = params.key || "string";
|
|
278
411
|
const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
412
|
+
return [
|
|
413
|
+
createClassGetter({
|
|
414
|
+
name: keyedPropertyName,
|
|
415
|
+
statements: [
|
|
416
|
+
`return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
|
|
417
|
+
]
|
|
418
|
+
})
|
|
419
|
+
];
|
|
284
420
|
}
|
|
285
421
|
const finalPropertyName = getterNameOverride ?? propertyName;
|
|
286
422
|
const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
|
|
287
423
|
if (alternates.length > 0) {
|
|
288
424
|
const all = [formattedDataTestId, ...alternates];
|
|
289
425
|
const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
426
|
+
return [
|
|
427
|
+
createClassGetter({
|
|
428
|
+
name: finalPropertyName,
|
|
429
|
+
statements: [`return ${locatorExpr};`]
|
|
430
|
+
})
|
|
431
|
+
];
|
|
295
432
|
}
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
433
|
+
return [
|
|
434
|
+
createClassGetter({
|
|
435
|
+
name: finalPropertyName,
|
|
436
|
+
statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
|
|
437
|
+
})
|
|
438
|
+
];
|
|
301
439
|
}
|
|
302
440
|
function generateNavigationMethod(args) {
|
|
303
441
|
const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
|
|
304
442
|
const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
|
|
305
|
-
const
|
|
443
|
+
const parameters = createParameters(params);
|
|
306
444
|
const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
|
|
307
445
|
const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
|
|
308
446
|
if (alternates.length > 0) {
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
${
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
${
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
447
|
+
return [
|
|
448
|
+
createClassMethod({
|
|
449
|
+
name: methodName,
|
|
450
|
+
parameters,
|
|
451
|
+
returnType: `Fluent<${target}>`,
|
|
452
|
+
statements: (writer) => {
|
|
453
|
+
writer.write("return this.fluent(async () => ").block(() => {
|
|
454
|
+
writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
|
|
455
|
+
writer.writeLine("let lastError: unknown;");
|
|
456
|
+
writer.write("for (const testId of candidates) ").block(() => {
|
|
457
|
+
writer.writeLine("const locator = this.locatorByTestId(testId);");
|
|
458
|
+
writer.write("try ").block(() => {
|
|
459
|
+
writer.write("if (await locator.count()) ").block(() => {
|
|
460
|
+
writer.writeLine("await this.clickLocator(locator);");
|
|
461
|
+
writer.writeLine(`return new ${target}(this.page);`);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
writer.write("catch (e) ").block(() => {
|
|
465
|
+
writer.writeLine("lastError = e;");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to navigate using any candidate locator for ${methodName}.");`);
|
|
469
|
+
});
|
|
470
|
+
writer.writeLine(");");
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
];
|
|
328
474
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
475
|
+
return [
|
|
476
|
+
createClassMethod({
|
|
477
|
+
name: methodName,
|
|
478
|
+
parameters,
|
|
479
|
+
returnType: `Fluent<${target}>`,
|
|
480
|
+
statements: (writer) => {
|
|
481
|
+
writer.write("return this.fluent(async () => ").block(() => {
|
|
482
|
+
writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
|
|
483
|
+
writer.writeLine(`return new ${target}(this.page);`);
|
|
484
|
+
});
|
|
485
|
+
writer.writeLine(");");
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
];
|
|
337
489
|
}
|
|
338
|
-
function
|
|
490
|
+
function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
|
|
339
491
|
const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
|
|
340
|
-
const
|
|
492
|
+
const members = generateGetElementByDataTestId(
|
|
493
|
+
baseMethodName,
|
|
494
|
+
nativeRole,
|
|
495
|
+
formattedDataTestId,
|
|
496
|
+
alternateFormattedDataTestIds,
|
|
497
|
+
getterNameOverride,
|
|
498
|
+
params
|
|
499
|
+
);
|
|
341
500
|
if (targetPageObjectModelClass) {
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
501
|
+
return [
|
|
502
|
+
...members,
|
|
503
|
+
...generateNavigationMethod({
|
|
504
|
+
targetPageObjectModelClass,
|
|
505
|
+
baseMethodName,
|
|
506
|
+
formattedDataTestId,
|
|
507
|
+
alternateFormattedDataTestIds,
|
|
508
|
+
params
|
|
509
|
+
})
|
|
510
|
+
];
|
|
349
511
|
}
|
|
350
512
|
if (nativeRole === "select") {
|
|
351
|
-
return
|
|
513
|
+
return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
|
|
352
514
|
}
|
|
353
515
|
if (nativeRole === "vselect") {
|
|
354
|
-
return
|
|
516
|
+
return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
|
|
355
517
|
}
|
|
356
518
|
if (nativeRole === "input") {
|
|
357
|
-
return
|
|
519
|
+
return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
|
|
358
520
|
}
|
|
359
521
|
if (nativeRole === "radio") {
|
|
360
|
-
return
|
|
522
|
+
return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
|
|
361
523
|
}
|
|
362
|
-
return
|
|
524
|
+
return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
|
|
363
525
|
}
|
|
364
526
|
function isSimpleExpressionNode(value) {
|
|
365
527
|
return value !== null && "type" in value && value.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION;
|
|
@@ -3268,10 +3430,123 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
|
|
|
3268
3430
|
}
|
|
3269
3431
|
});
|
|
3270
3432
|
}
|
|
3271
|
-
const AUTO_GENERATED_COMMENT = " * DO NOT MODIFY BY HAND\n *\n * This file is auto-generated by vue-pom-generator.\n * Changes should be made in the generator/template, not in the generated output.\n */";
|
|
3272
3433
|
const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
|
|
3273
3434
|
const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
|
|
3274
|
-
const
|
|
3435
|
+
const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
|
|
3436
|
+
class VuePomGeneratorError extends Error {
|
|
3437
|
+
constructor(message) {
|
|
3438
|
+
const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
|
|
3439
|
+
super(normalized);
|
|
3440
|
+
this.name = "VuePomGeneratorError";
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function splitParameterList(parameters) {
|
|
3444
|
+
const parts = [];
|
|
3445
|
+
let current = "";
|
|
3446
|
+
let braceDepth = 0;
|
|
3447
|
+
let bracketDepth = 0;
|
|
3448
|
+
let parenDepth = 0;
|
|
3449
|
+
let angleDepth = 0;
|
|
3450
|
+
let inSingleQuote = false;
|
|
3451
|
+
let inDoubleQuote = false;
|
|
3452
|
+
let inTemplateString = false;
|
|
3453
|
+
for (let index = 0; index < parameters.length; index += 1) {
|
|
3454
|
+
const char = parameters[index];
|
|
3455
|
+
const previous = index > 0 ? parameters[index - 1] : "";
|
|
3456
|
+
if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
|
|
3457
|
+
inSingleQuote = !inSingleQuote;
|
|
3458
|
+
current += char;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
|
|
3462
|
+
inDoubleQuote = !inDoubleQuote;
|
|
3463
|
+
current += char;
|
|
3464
|
+
continue;
|
|
3465
|
+
}
|
|
3466
|
+
if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
|
|
3467
|
+
inTemplateString = !inTemplateString;
|
|
3468
|
+
current += char;
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
3472
|
+
current += char;
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
switch (char) {
|
|
3476
|
+
case "{":
|
|
3477
|
+
braceDepth += 1;
|
|
3478
|
+
break;
|
|
3479
|
+
case "}":
|
|
3480
|
+
braceDepth -= 1;
|
|
3481
|
+
break;
|
|
3482
|
+
case "[":
|
|
3483
|
+
bracketDepth += 1;
|
|
3484
|
+
break;
|
|
3485
|
+
case "]":
|
|
3486
|
+
bracketDepth -= 1;
|
|
3487
|
+
break;
|
|
3488
|
+
case "(":
|
|
3489
|
+
parenDepth += 1;
|
|
3490
|
+
break;
|
|
3491
|
+
case ")":
|
|
3492
|
+
parenDepth -= 1;
|
|
3493
|
+
break;
|
|
3494
|
+
case "<":
|
|
3495
|
+
angleDepth += 1;
|
|
3496
|
+
break;
|
|
3497
|
+
case ">":
|
|
3498
|
+
angleDepth -= 1;
|
|
3499
|
+
break;
|
|
3500
|
+
case ",":
|
|
3501
|
+
if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
|
|
3502
|
+
const trimmed2 = current.trim();
|
|
3503
|
+
if (trimmed2) {
|
|
3504
|
+
parts.push(trimmed2);
|
|
3505
|
+
}
|
|
3506
|
+
current = "";
|
|
3507
|
+
continue;
|
|
3508
|
+
}
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
current += char;
|
|
3512
|
+
}
|
|
3513
|
+
const trimmed = current.trim();
|
|
3514
|
+
if (trimmed) {
|
|
3515
|
+
parts.push(trimmed);
|
|
3516
|
+
}
|
|
3517
|
+
return parts;
|
|
3518
|
+
}
|
|
3519
|
+
function parseParameterSignature(parameter) {
|
|
3520
|
+
const colonIndex = parameter.indexOf(":");
|
|
3521
|
+
if (colonIndex < 0) {
|
|
3522
|
+
return { name: parameter.trim() };
|
|
3523
|
+
}
|
|
3524
|
+
const rawName = parameter.slice(0, colonIndex).trim();
|
|
3525
|
+
const hasQuestionToken = rawName.endsWith("?");
|
|
3526
|
+
const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
|
|
3527
|
+
const remainder = parameter.slice(colonIndex + 1).trim();
|
|
3528
|
+
const initializerIndex = remainder.lastIndexOf("=");
|
|
3529
|
+
if (initializerIndex < 0) {
|
|
3530
|
+
return {
|
|
3531
|
+
name,
|
|
3532
|
+
hasQuestionToken,
|
|
3533
|
+
type: remainder || void 0
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
return {
|
|
3537
|
+
name,
|
|
3538
|
+
hasQuestionToken,
|
|
3539
|
+
type: remainder.slice(0, initializerIndex).trim() || void 0,
|
|
3540
|
+
initializer: remainder.slice(initializerIndex + 1).trim() || void 0
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function parseParameterSignatures(parameters) {
|
|
3544
|
+
const trimmed = parameters.trim();
|
|
3545
|
+
if (!trimmed) {
|
|
3546
|
+
return [];
|
|
3547
|
+
}
|
|
3548
|
+
return splitParameterList(trimmed).map(parseParameterSignature);
|
|
3549
|
+
}
|
|
3275
3550
|
function toPosixRelativePath(fromDir, toFile) {
|
|
3276
3551
|
let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
|
|
3277
3552
|
if (!rel.startsWith(".")) {
|
|
@@ -3279,12 +3554,6 @@ function toPosixRelativePath(fromDir, toFile) {
|
|
|
3279
3554
|
}
|
|
3280
3555
|
return rel;
|
|
3281
3556
|
}
|
|
3282
|
-
function changeExtension(filePath, expectedExt, nextExtWithDot) {
|
|
3283
|
-
const parsed = path.parse(filePath);
|
|
3284
|
-
if (parsed.ext !== expectedExt)
|
|
3285
|
-
return filePath;
|
|
3286
|
-
return path.format({ ...parsed, base: `${parsed.name}${nextExtWithDot}`, ext: nextExtWithDot });
|
|
3287
|
-
}
|
|
3288
3557
|
function stripExtension(filePath) {
|
|
3289
3558
|
const posix = (filePath ?? "").replace(/\\/g, "/");
|
|
3290
3559
|
const parsed = path.posix.parse(posix);
|
|
@@ -3297,6 +3566,70 @@ function resolveRouterEntry(projectRoot, routerEntry) {
|
|
|
3297
3566
|
const root = projectRoot ?? process.cwd();
|
|
3298
3567
|
return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
|
|
3299
3568
|
}
|
|
3569
|
+
function createCustomPomImportCollisionError(exportName, requested) {
|
|
3570
|
+
return new VuePomGeneratorError(
|
|
3571
|
+
`Custom POM import name collision detected for "${exportName}".
|
|
3572
|
+
The identifier "${requested}" conflicts with a generated POM class.
|
|
3573
|
+
Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] to a unique name, or set generation.playwright.customPoms.importNameCollisionBehavior = "alias" to auto-alias collisions.`
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
function normalizeComponentTagToClassName(tag) {
|
|
3577
|
+
const className = toPascalCase(tag);
|
|
3578
|
+
return className || void 0;
|
|
3579
|
+
}
|
|
3580
|
+
function collectReferencedComponentClassNames(nodes, names) {
|
|
3581
|
+
for (const node of nodes) {
|
|
3582
|
+
switch (node.type) {
|
|
3583
|
+
case compilerDom.NodeTypes.ELEMENT: {
|
|
3584
|
+
const element = node;
|
|
3585
|
+
if (element.tagType === compilerCore.ElementTypes.COMPONENT) {
|
|
3586
|
+
const className = normalizeComponentTagToClassName(element.tag);
|
|
3587
|
+
if (className) {
|
|
3588
|
+
names.add(className);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
collectReferencedComponentClassNames(element.children, names);
|
|
3592
|
+
break;
|
|
3593
|
+
}
|
|
3594
|
+
case compilerDom.NodeTypes.IF: {
|
|
3595
|
+
const ifNode = node;
|
|
3596
|
+
for (const branch of ifNode.branches) {
|
|
3597
|
+
collectReferencedComponentClassNames(branch.children, names);
|
|
3598
|
+
}
|
|
3599
|
+
break;
|
|
3600
|
+
}
|
|
3601
|
+
case compilerDom.NodeTypes.FOR: {
|
|
3602
|
+
const forNode = node;
|
|
3603
|
+
collectReferencedComponentClassNames(forNode.children, names);
|
|
3604
|
+
break;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
function getComponentClassNamesFromVueSource(source) {
|
|
3610
|
+
try {
|
|
3611
|
+
const { descriptor } = compilerSfc.parse(source);
|
|
3612
|
+
const template = descriptor.template?.content?.trim();
|
|
3613
|
+
if (!template) {
|
|
3614
|
+
return [];
|
|
3615
|
+
}
|
|
3616
|
+
const root = compilerDom.parse(template);
|
|
3617
|
+
const names = /* @__PURE__ */ new Set();
|
|
3618
|
+
collectReferencedComponentClassNames(root.children, names);
|
|
3619
|
+
return [...names];
|
|
3620
|
+
} catch {
|
|
3621
|
+
return [];
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot) {
|
|
3625
|
+
const mapped = vueFilesPathMap.get(targetClassName);
|
|
3626
|
+
const candidates = [
|
|
3627
|
+
mapped,
|
|
3628
|
+
path.join(projectRoot, "src", "views", `${targetClassName}.vue`),
|
|
3629
|
+
path.join(projectRoot, "src", "components", `${targetClassName}.vue`)
|
|
3630
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
3631
|
+
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
3632
|
+
}
|
|
3300
3633
|
async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, options = {}) {
|
|
3301
3634
|
const root = projectRoot ?? process.cwd();
|
|
3302
3635
|
const viewsDir = options.viewsDir ?? "src/views";
|
|
@@ -3331,35 +3664,40 @@ async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, opt
|
|
|
3331
3664
|
);
|
|
3332
3665
|
}
|
|
3333
3666
|
function generateRouteProperty(routeMeta) {
|
|
3334
|
-
if (!routeMeta) {
|
|
3335
|
-
return " static readonly route: { template: string } | null = null;\n";
|
|
3336
|
-
}
|
|
3337
3667
|
return [
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3668
|
+
createClassProperty({
|
|
3669
|
+
name: "route",
|
|
3670
|
+
isStatic: true,
|
|
3671
|
+
isReadonly: true,
|
|
3672
|
+
type: "{ template: string } | null",
|
|
3673
|
+
initializer: routeMeta ? `{ template: ${JSON.stringify(routeMeta.template)} } as const` : "null"
|
|
3674
|
+
})
|
|
3675
|
+
];
|
|
3343
3676
|
}
|
|
3344
3677
|
function generateGoToSelfMethod(componentName) {
|
|
3345
3678
|
return [
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
"
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3679
|
+
createClassMethod({
|
|
3680
|
+
name: "goTo",
|
|
3681
|
+
isAsync: true,
|
|
3682
|
+
statements: [
|
|
3683
|
+
"await this.goToSelf();"
|
|
3684
|
+
]
|
|
3685
|
+
}),
|
|
3686
|
+
createClassMethod({
|
|
3687
|
+
name: "goToSelf",
|
|
3688
|
+
isAsync: true,
|
|
3689
|
+
statements: [
|
|
3690
|
+
`const route = ${componentName}.route;`,
|
|
3691
|
+
"if (!route) {",
|
|
3692
|
+
` throw new Error("[pom] No router path found for component/page-object '${componentName}'.");`,
|
|
3693
|
+
"}",
|
|
3694
|
+
"const runtimeEnv = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;",
|
|
3695
|
+
"const runtimeBaseUrl = runtimeEnv?.PLAYWRIGHT_RUNTIME_BASE_URL ?? runtimeEnv?.PLAYWRIGHT_TEST_BASE_URL ?? runtimeEnv?.VITE_PLAYWRIGHT_BASE_URL;",
|
|
3696
|
+
"const targetUrl = runtimeBaseUrl ? new URL(route.template, runtimeBaseUrl).toString() : route.template;",
|
|
3697
|
+
"await this.page.goto(targetUrl);"
|
|
3698
|
+
]
|
|
3699
|
+
})
|
|
3700
|
+
];
|
|
3363
3701
|
}
|
|
3364
3702
|
function formatMethodParams(params) {
|
|
3365
3703
|
if (!params)
|
|
@@ -3374,21 +3712,13 @@ function formatMethodParams(params) {
|
|
|
3374
3712
|
};
|
|
3375
3713
|
return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
|
|
3376
3714
|
}
|
|
3377
|
-
function
|
|
3715
|
+
function generateExtraClickMethodMembers(spec) {
|
|
3378
3716
|
if (spec.kind !== "click") {
|
|
3379
|
-
return
|
|
3717
|
+
return [];
|
|
3380
3718
|
}
|
|
3381
3719
|
const params = spec.params ?? {};
|
|
3382
3720
|
const signatureParams = formatMethodParams(params);
|
|
3383
|
-
const
|
|
3384
|
-
const lines = [];
|
|
3385
|
-
lines.push(
|
|
3386
|
-
"",
|
|
3387
|
-
` async ${spec.name}${signature} {`
|
|
3388
|
-
);
|
|
3389
|
-
if (spec.keyLiteral !== void 0) {
|
|
3390
|
-
lines.push(` const key = ${JSON.stringify(spec.keyLiteral)};`);
|
|
3391
|
-
}
|
|
3721
|
+
const parameters = parseParameterSignatures(signatureParams);
|
|
3392
3722
|
const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
|
|
3393
3723
|
const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
|
|
3394
3724
|
const annotationArg = hasAnnotationText ? "annotationText" : '""';
|
|
@@ -3396,9 +3726,6 @@ function generateExtraClickMethodContent(spec) {
|
|
|
3396
3726
|
if (spec.selector.kind === "testId") {
|
|
3397
3727
|
const needsTemplate = spec.selector.formattedDataTestId.includes("${");
|
|
3398
3728
|
const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
|
|
3399
|
-
if (needsTemplate) {
|
|
3400
|
-
lines.push(` const testId = ${testIdExpr};`);
|
|
3401
|
-
}
|
|
3402
3729
|
const clickArgs = [];
|
|
3403
3730
|
clickArgs.push(needsTemplate ? "testId" : testIdExpr);
|
|
3404
3731
|
if (hasAnnotationText || hasWait) {
|
|
@@ -3407,33 +3734,54 @@ function generateExtraClickMethodContent(spec) {
|
|
|
3407
3734
|
if (hasWait) {
|
|
3408
3735
|
clickArgs.push(waitArg);
|
|
3409
3736
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3737
|
+
return [
|
|
3738
|
+
createClassMethod({
|
|
3739
|
+
name: spec.name,
|
|
3740
|
+
isAsync: true,
|
|
3741
|
+
parameters,
|
|
3742
|
+
statements: (writer) => {
|
|
3743
|
+
if (spec.keyLiteral !== void 0) {
|
|
3744
|
+
writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
|
|
3745
|
+
}
|
|
3746
|
+
if (needsTemplate) {
|
|
3747
|
+
writer.writeLine(`const testId = ${testIdExpr};`);
|
|
3748
|
+
}
|
|
3749
|
+
writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
|
|
3750
|
+
}
|
|
3751
|
+
})
|
|
3752
|
+
];
|
|
3414
3753
|
}
|
|
3415
3754
|
const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
|
|
3416
3755
|
const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
|
|
3417
3756
|
const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
|
|
3418
3757
|
const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
|
|
3419
|
-
if (rootNeedsTemplate) {
|
|
3420
|
-
lines.push(` const rootTestId = ${rootExpr};`);
|
|
3421
|
-
}
|
|
3422
|
-
if (labelNeedsTemplate) {
|
|
3423
|
-
lines.push(` const label = ${labelExpr};`);
|
|
3424
|
-
}
|
|
3425
3758
|
const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
|
|
3426
3759
|
const labelArg = labelNeedsTemplate ? "label" : labelExpr;
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3760
|
+
return [
|
|
3761
|
+
createClassMethod({
|
|
3762
|
+
name: spec.name,
|
|
3763
|
+
isAsync: true,
|
|
3764
|
+
parameters,
|
|
3765
|
+
statements: (writer) => {
|
|
3766
|
+
if (spec.keyLiteral !== void 0) {
|
|
3767
|
+
writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
|
|
3768
|
+
}
|
|
3769
|
+
if (rootNeedsTemplate) {
|
|
3770
|
+
writer.writeLine(`const rootTestId = ${rootExpr};`);
|
|
3771
|
+
}
|
|
3772
|
+
if (labelNeedsTemplate) {
|
|
3773
|
+
writer.writeLine(`const label = ${labelExpr};`);
|
|
3774
|
+
}
|
|
3775
|
+
writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
|
|
3776
|
+
}
|
|
3777
|
+
})
|
|
3778
|
+
];
|
|
3431
3779
|
}
|
|
3432
|
-
function
|
|
3780
|
+
function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
|
|
3433
3781
|
if (primary.emitPrimary === false) {
|
|
3434
|
-
return
|
|
3782
|
+
return [];
|
|
3435
3783
|
}
|
|
3436
|
-
return
|
|
3784
|
+
return generateViewObjectModelMembers(
|
|
3437
3785
|
targetPageObjectModelClass,
|
|
3438
3786
|
primary.methodName,
|
|
3439
3787
|
primary.nativeRole,
|
|
@@ -3467,14 +3815,14 @@ function generateMethodsContentForDependencies(dependencies) {
|
|
|
3467
3815
|
return true;
|
|
3468
3816
|
});
|
|
3469
3817
|
const extras = (dependencies.pomExtraMethods ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
3470
|
-
|
|
3818
|
+
const members = [];
|
|
3471
3819
|
for (const { pom, target } of primarySpecs) {
|
|
3472
|
-
|
|
3820
|
+
members.push(...generateMethodMembersFromPom(pom, target));
|
|
3473
3821
|
}
|
|
3474
3822
|
for (const extra of extras) {
|
|
3475
|
-
|
|
3823
|
+
members.push(...generateExtraClickMethodMembers(extra));
|
|
3476
3824
|
}
|
|
3477
|
-
return
|
|
3825
|
+
return members;
|
|
3478
3826
|
}
|
|
3479
3827
|
async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, options = {}) {
|
|
3480
3828
|
const {
|
|
@@ -3487,6 +3835,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
|
|
|
3487
3835
|
customPomImportNameCollisionBehavior = "error",
|
|
3488
3836
|
testIdAttribute,
|
|
3489
3837
|
emitLanguages: emitLanguagesOverride,
|
|
3838
|
+
typescriptOutputStructure = "aggregated",
|
|
3490
3839
|
csharp,
|
|
3491
3840
|
vueRouterFluentChaining,
|
|
3492
3841
|
routerEntry,
|
|
@@ -3508,7 +3857,16 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
|
|
|
3508
3857
|
generatedFilePaths.push(resolvedFilePath);
|
|
3509
3858
|
};
|
|
3510
3859
|
if (emitLanguages.includes("ts")) {
|
|
3511
|
-
const files = await
|
|
3860
|
+
const files = typescriptOutputStructure === "split" ? await generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
|
|
3861
|
+
customPomAttachments,
|
|
3862
|
+
projectRoot,
|
|
3863
|
+
customPomDir,
|
|
3864
|
+
customPomImportAliases,
|
|
3865
|
+
customPomImportNameCollisionBehavior,
|
|
3866
|
+
testIdAttribute,
|
|
3867
|
+
routeMetaByComponent,
|
|
3868
|
+
vueRouterFluentChaining
|
|
3869
|
+
}) : await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
|
|
3512
3870
|
customPomAttachments,
|
|
3513
3871
|
projectRoot,
|
|
3514
3872
|
customPomDir,
|
|
@@ -3545,6 +3903,100 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
|
|
|
3545
3903
|
createFile(file.filePath, file.content);
|
|
3546
3904
|
}
|
|
3547
3905
|
}
|
|
3906
|
+
async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, options = {}) {
|
|
3907
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
3908
|
+
const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3909
|
+
const base = ensureDir(outDir);
|
|
3910
|
+
const generatedClassNames = new Set(entries.map(([name]) => name));
|
|
3911
|
+
const referencedTargets = /* @__PURE__ */ new Set();
|
|
3912
|
+
for (const [, deps] of entries) {
|
|
3913
|
+
for (const dataTestId of deps.dataTestIdSet ?? []) {
|
|
3914
|
+
if (dataTestId.targetPageObjectModelClass) {
|
|
3915
|
+
referencedTargets.add(dataTestId.targetPageObjectModelClass);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
const stubTargets = Array.from(referencedTargets).filter((target) => !generatedClassNames.has(target)).sort((a, b) => a.localeCompare(b));
|
|
3920
|
+
const availableClassNames = /* @__PURE__ */ new Set([...generatedClassNames, ...stubTargets]);
|
|
3921
|
+
const depsByClassName = new Map(entries);
|
|
3922
|
+
const generatedTsFilePathByComponent = /* @__PURE__ */ new Map();
|
|
3923
|
+
for (const className of availableClassNames) {
|
|
3924
|
+
generatedTsFilePathByComponent.set(className, path.join(base, `${className}.g.ts`));
|
|
3925
|
+
}
|
|
3926
|
+
const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
|
|
3927
|
+
customPomDir: options.customPomDir,
|
|
3928
|
+
customPomImportAliases: options.customPomImportAliases,
|
|
3929
|
+
customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
|
|
3930
|
+
});
|
|
3931
|
+
const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "BasePage.ts");
|
|
3932
|
+
const files = [];
|
|
3933
|
+
for (const [name, deps] of entries) {
|
|
3934
|
+
const filePath = generatedTsFilePathByComponent.get(name);
|
|
3935
|
+
if (!filePath) {
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
const content = generateViewObjectModelContent(name, deps, componentHierarchyMap, vueFilesPathMap, runtimeBasePagePath, {
|
|
3939
|
+
outputDir: path.dirname(filePath),
|
|
3940
|
+
customPomAttachments: options.customPomAttachments ?? [],
|
|
3941
|
+
projectRoot,
|
|
3942
|
+
customPomDir: options.customPomDir,
|
|
3943
|
+
customPomImportAliases: options.customPomImportAliases,
|
|
3944
|
+
customPomClassIdentifierMap: customPomImportResolution.classIdentifierMap,
|
|
3945
|
+
customPomAvailableClassIdentifiers: customPomImportResolution.availableClassIdentifiers,
|
|
3946
|
+
customPomImportSpecifiersByClass: customPomImportResolution.importSpecifiersByClass,
|
|
3947
|
+
customPomMethodSignaturesByClass: customPomImportResolution.methodSignaturesByClass,
|
|
3948
|
+
generatedTsFilePathByComponent,
|
|
3949
|
+
testIdAttribute: options.testIdAttribute,
|
|
3950
|
+
vueRouterFluentChaining: options.vueRouterFluentChaining,
|
|
3951
|
+
routeMetaByComponent: options.routeMetaByComponent
|
|
3952
|
+
});
|
|
3953
|
+
files.push({ filePath, content });
|
|
3954
|
+
}
|
|
3955
|
+
for (const targetClassName of stubTargets) {
|
|
3956
|
+
const filePath = generatedTsFilePathByComponent.get(targetClassName);
|
|
3957
|
+
if (!filePath) {
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
const outputDir = path.dirname(filePath);
|
|
3961
|
+
const basePageImportSpecifier = stripExtension(toPosixRelativePath(outputDir, runtimeBasePagePath));
|
|
3962
|
+
const composed = getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot);
|
|
3963
|
+
const childImports = getChildImportSpecifiers(outputDir, composed?.childClassNames ?? [], generatedTsFilePathByComponent);
|
|
3964
|
+
const members = composed?.members ?? getDefaultStubMembers();
|
|
3965
|
+
const content = renderSplitStubPomContent({
|
|
3966
|
+
className: targetClassName,
|
|
3967
|
+
basePageImportSpecifier,
|
|
3968
|
+
childImports,
|
|
3969
|
+
members
|
|
3970
|
+
});
|
|
3971
|
+
files.push({ filePath, content });
|
|
3972
|
+
}
|
|
3973
|
+
const runtimeAssetSpecs = getRuntimeGeneratedAssetSpecs(base, basePageClassPath);
|
|
3974
|
+
const runtimeFiles = buildRuntimeGeneratedFilesFromSpecs(runtimeAssetSpecs);
|
|
3975
|
+
const indexContent = renderSourceFile("index.ts", (sourceFile) => {
|
|
3976
|
+
for (const spec of runtimeAssetSpecs) {
|
|
3977
|
+
addExportAll(sourceFile, stripExtension(toPosixRelativePath(base, spec.outputPath)));
|
|
3978
|
+
}
|
|
3979
|
+
for (const [, filePath] of Array.from(generatedTsFilePathByComponent.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
3980
|
+
addExportAll(sourceFile, `./${stripExtension(path.basename(filePath))}`);
|
|
3981
|
+
}
|
|
3982
|
+
}, {
|
|
3983
|
+
prefixText: buildFilePrefix({
|
|
3984
|
+
eslintDisableSortImports: true,
|
|
3985
|
+
commentLines: [
|
|
3986
|
+
"POM exports",
|
|
3987
|
+
"DO NOT MODIFY BY HAND",
|
|
3988
|
+
"",
|
|
3989
|
+
"This file is auto-generated by vue-pom-generator.",
|
|
3990
|
+
"Changes should be made in the generator/template, not in the generated output."
|
|
3991
|
+
]
|
|
3992
|
+
})
|
|
3993
|
+
});
|
|
3994
|
+
return [
|
|
3995
|
+
...files,
|
|
3996
|
+
{ filePath: path.join(base, "index.ts"), content: indexContent },
|
|
3997
|
+
...runtimeFiles
|
|
3998
|
+
];
|
|
3999
|
+
}
|
|
3548
4000
|
function escapeGitAttributesPattern(value) {
|
|
3549
4001
|
let output = "";
|
|
3550
4002
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -3983,91 +4435,190 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
|
|
|
3983
4435
|
};
|
|
3984
4436
|
}).filter((entry) => !!entry);
|
|
3985
4437
|
const overrideCtorByClassName = new Map(overrideCtorEntries.map((entry) => [entry.className, entry.localIdentifier]));
|
|
3986
|
-
const overrideImports = overrideCtorEntries.length ? `${overrideCtorEntries.map((entry) => `import { ${entry.className} as ${entry.localIdentifier} } from "${entry.importSpecifier}";`).join("\n")}
|
|
3987
|
-
|
|
3988
|
-
` : "";
|
|
3989
4438
|
const fixtureCtorExpression = (name) => overrideCtorByClassName.get(name) ?? `Pom.${name}`;
|
|
3990
|
-
const
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
const
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
}
|
|
4005
|
-
|
|
4006
|
-
`;
|
|
4007
|
-
const fixturesContent = `${header}/** Generated Playwright fixtures (typed page objects). */
|
|
4008
|
-
|
|
4009
|
-
import { expect, test as base } from "@playwright/test";
|
|
4010
|
-
import type { Page as PwPage } from "@playwright/test";
|
|
4011
|
-
import * as Pom from "${pomImport}";
|
|
4012
|
-
${overrideImports}export interface PlaywrightOptions {
|
|
4013
|
-
animation: Pom.PlaywrightAnimationOptions;
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
|
-
${pomFactoryType}type PomSetupFixture = { pomSetup: void };
|
|
4017
|
-
type PomFactoryFixture = { pomFactory: PomFactory };
|
|
4018
|
-
|
|
4019
|
-
const pageCtors = {
|
|
4020
|
-
${fixturesTypeEntries}
|
|
4021
|
-
} as const;
|
|
4022
|
-
const componentCtors = {
|
|
4023
|
-
${componentFixturesTypeEntries}
|
|
4024
|
-
} as const;
|
|
4025
|
-
|
|
4026
|
-
export type GeneratedPageFixtures = { [K in keyof typeof pageCtors]: InstanceType<(typeof pageCtors)[K]> };
|
|
4027
|
-
export type GeneratedComponentFixtures = { [K in keyof typeof componentCtors]: InstanceType<(typeof componentCtors)[K]> };
|
|
4028
|
-
|
|
4029
|
-
const makePomFixture = <T>(Ctor: PomConstructor<T>) => async ({ page }: { page: PwPage }, use: (t: T) => Promise<void>) => {
|
|
4030
|
-
await use(new Ctor(page));
|
|
4031
|
-
};
|
|
4032
|
-
|
|
4033
|
-
const createPomFixtures = <TMap extends Record<string, PomConstructor<any>>>(ctors: TMap) => {
|
|
4034
|
-
const out: Record<string, any> = {};
|
|
4035
|
-
for (const [key, Ctor] of Object.entries(ctors)) {
|
|
4036
|
-
out[key] = makePomFixture(Ctor as PomConstructor<any>);
|
|
4037
|
-
}
|
|
4038
|
-
return out as any;
|
|
4039
|
-
};
|
|
4040
|
-
|
|
4041
|
-
const test = base.extend<PlaywrightOptions & PomSetupFixture & PomFactoryFixture & GeneratedPageFixtures & GeneratedComponentFixtures>({
|
|
4042
|
-
animation: [{
|
|
4043
|
-
pointer: { durationMilliseconds: 250, transitionStyle: "ease-in-out", clickDelayMilliseconds: 0 },
|
|
4044
|
-
keyboard: { typeDelayMilliseconds: 100 },
|
|
4045
|
-
}, { option: true }],
|
|
4046
|
-
pomSetup: [async ({ animation }, use) => {
|
|
4047
|
-
Pom.setPlaywrightAnimationOptions(animation);
|
|
4048
|
-
await use();
|
|
4049
|
-
}, { auto: true }],
|
|
4050
|
-
pomFactory: async ({ page }, use) => {
|
|
4051
|
-
await use({
|
|
4052
|
-
create: <T>(ctor: PomConstructor<T>) => new ctor(page),
|
|
4439
|
+
const pageCtorEntries = viewClassNames.map((name) => ({
|
|
4440
|
+
fixtureName: lowerFirst(name),
|
|
4441
|
+
ctorExpression: fixtureCtorExpression(name)
|
|
4442
|
+
}));
|
|
4443
|
+
const componentCtorEntries = componentClassNames.map((name) => ({
|
|
4444
|
+
fixtureName: lowerFirst(name),
|
|
4445
|
+
ctorExpression: fixtureCtorExpression(name)
|
|
4446
|
+
}));
|
|
4447
|
+
const fixturesContent = renderSourceFile(fixtureFileName, (sourceFile) => {
|
|
4448
|
+
sourceFile.addStatements("/** Generated Playwright fixtures (typed page objects). */");
|
|
4449
|
+
addNamedImport(sourceFile, {
|
|
4450
|
+
moduleSpecifier: "@playwright/test",
|
|
4451
|
+
namedImports: [
|
|
4452
|
+
"expect",
|
|
4453
|
+
{ name: "test", alias: "base" }
|
|
4454
|
+
]
|
|
4053
4455
|
});
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4456
|
+
addNamedImport(sourceFile, {
|
|
4457
|
+
moduleSpecifier: "@playwright/test",
|
|
4458
|
+
isTypeOnly: true,
|
|
4459
|
+
namedImports: [{ name: "Page", alias: "PwPage" }]
|
|
4460
|
+
});
|
|
4461
|
+
sourceFile.addImportDeclaration({
|
|
4462
|
+
namespaceImport: "Pom",
|
|
4463
|
+
moduleSpecifier: pomImport
|
|
4464
|
+
});
|
|
4465
|
+
for (const entry of overrideCtorEntries) {
|
|
4466
|
+
addNamedImport(sourceFile, {
|
|
4467
|
+
moduleSpecifier: entry.importSpecifier,
|
|
4468
|
+
namedImports: [{ name: entry.className, alias: entry.localIdentifier }]
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
sourceFile.addInterface({
|
|
4472
|
+
isExported: true,
|
|
4473
|
+
name: "PlaywrightOptions",
|
|
4474
|
+
properties: [{
|
|
4475
|
+
name: "animation",
|
|
4476
|
+
type: "Pom.PlaywrightAnimationOptions"
|
|
4477
|
+
}]
|
|
4478
|
+
});
|
|
4479
|
+
sourceFile.addTypeAlias({
|
|
4480
|
+
isExported: true,
|
|
4481
|
+
name: "PomConstructor",
|
|
4482
|
+
typeParameters: [{ name: "T" }],
|
|
4483
|
+
type: "new (page: PwPage) => T"
|
|
4484
|
+
});
|
|
4485
|
+
sourceFile.addInterface({
|
|
4486
|
+
isExported: true,
|
|
4487
|
+
name: "PomFactory",
|
|
4488
|
+
methods: [{
|
|
4489
|
+
name: "create",
|
|
4490
|
+
typeParameters: [{ name: "T" }],
|
|
4491
|
+
parameters: [{ name: "ctor", type: "PomConstructor<T>" }],
|
|
4492
|
+
returnType: "T"
|
|
4493
|
+
}]
|
|
4494
|
+
});
|
|
4495
|
+
sourceFile.addTypeAlias({
|
|
4496
|
+
name: "PomSetupFixture",
|
|
4497
|
+
type: "{ pomSetup: void }"
|
|
4498
|
+
});
|
|
4499
|
+
sourceFile.addTypeAlias({
|
|
4500
|
+
name: "PomFactoryFixture",
|
|
4501
|
+
type: "{ pomFactory: PomFactory }"
|
|
4502
|
+
});
|
|
4503
|
+
sourceFile.addVariableStatement({
|
|
4504
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
4505
|
+
declarations: [{
|
|
4506
|
+
name: "pageCtors",
|
|
4507
|
+
initializer: (writer) => {
|
|
4508
|
+
writer.write("{").newLine();
|
|
4509
|
+
writer.indent(() => {
|
|
4510
|
+
for (const entry of pageCtorEntries) {
|
|
4511
|
+
writer.writeLine(`${entry.fixtureName}: ${entry.ctorExpression},`);
|
|
4512
|
+
}
|
|
4513
|
+
});
|
|
4514
|
+
writer.write("} as const");
|
|
4515
|
+
}
|
|
4516
|
+
}]
|
|
4517
|
+
});
|
|
4518
|
+
sourceFile.addVariableStatement({
|
|
4519
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
4520
|
+
declarations: [{
|
|
4521
|
+
name: "componentCtors",
|
|
4522
|
+
initializer: (writer) => {
|
|
4523
|
+
writer.write("{").newLine();
|
|
4524
|
+
writer.indent(() => {
|
|
4525
|
+
for (const entry of componentCtorEntries) {
|
|
4526
|
+
writer.writeLine(`${entry.fixtureName}: ${entry.ctorExpression},`);
|
|
4527
|
+
}
|
|
4528
|
+
});
|
|
4529
|
+
writer.write("} as const");
|
|
4530
|
+
}
|
|
4531
|
+
}]
|
|
4532
|
+
});
|
|
4533
|
+
sourceFile.addTypeAlias({
|
|
4534
|
+
isExported: true,
|
|
4535
|
+
name: "GeneratedPageFixtures",
|
|
4536
|
+
type: "{ [K in keyof typeof pageCtors]: InstanceType<(typeof pageCtors)[K]> }"
|
|
4537
|
+
});
|
|
4538
|
+
sourceFile.addTypeAlias({
|
|
4539
|
+
isExported: true,
|
|
4540
|
+
name: "GeneratedComponentFixtures",
|
|
4541
|
+
type: "{ [K in keyof typeof componentCtors]: InstanceType<(typeof componentCtors)[K]> }"
|
|
4542
|
+
});
|
|
4543
|
+
sourceFile.addFunction({
|
|
4544
|
+
name: "makePomFixture",
|
|
4545
|
+
typeParameters: [{ name: "T" }],
|
|
4546
|
+
parameters: [{ name: "Ctor", type: "PomConstructor<T>" }],
|
|
4547
|
+
statements: [
|
|
4548
|
+
"return async ({ page }: { page: PwPage }, use: (t: T) => Promise<void>) => {",
|
|
4549
|
+
" await use(new Ctor(page));",
|
|
4550
|
+
"};"
|
|
4551
|
+
]
|
|
4552
|
+
});
|
|
4553
|
+
sourceFile.addFunction({
|
|
4554
|
+
name: "createPomFixtures",
|
|
4555
|
+
typeParameters: [{ name: "TMap", constraint: "Record<string, PomConstructor<any>>" }],
|
|
4556
|
+
parameters: [{ name: "ctors", type: "TMap" }],
|
|
4557
|
+
statements: [
|
|
4558
|
+
"const out: Record<string, any> = {};",
|
|
4559
|
+
"for (const [key, Ctor] of Object.entries(ctors)) {",
|
|
4560
|
+
" out[key] = makePomFixture(Ctor as PomConstructor<any>);",
|
|
4561
|
+
"}",
|
|
4562
|
+
"return out as any;"
|
|
4563
|
+
]
|
|
4564
|
+
});
|
|
4565
|
+
sourceFile.addVariableStatement({
|
|
4566
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
4567
|
+
declarations: [{
|
|
4568
|
+
name: "test",
|
|
4569
|
+
initializer: (writer) => {
|
|
4570
|
+
writer.write("base.extend<PlaywrightOptions & PomSetupFixture & PomFactoryFixture & GeneratedPageFixtures & GeneratedComponentFixtures>(");
|
|
4571
|
+
writer.block(() => {
|
|
4572
|
+
writer.writeLine("animation: [{");
|
|
4573
|
+
writer.indent(() => {
|
|
4574
|
+
writer.writeLine('pointer: { durationMilliseconds: 250, transitionStyle: "ease-in-out", clickDelayMilliseconds: 0 },');
|
|
4575
|
+
writer.writeLine("keyboard: { typeDelayMilliseconds: 100 },");
|
|
4576
|
+
});
|
|
4577
|
+
writer.writeLine("}, { option: true }],");
|
|
4578
|
+
writer.writeLine("pomSetup: [async ({ animation }, use) => {");
|
|
4579
|
+
writer.indent(() => {
|
|
4580
|
+
writer.writeLine("Pom.setPlaywrightAnimationOptions(animation);");
|
|
4581
|
+
writer.writeLine("await use();");
|
|
4582
|
+
});
|
|
4583
|
+
writer.writeLine("}, { auto: true }],");
|
|
4584
|
+
writer.writeLine("pomFactory: async ({ page }, use) => {");
|
|
4585
|
+
writer.indent(() => {
|
|
4586
|
+
writer.writeLine("await use({");
|
|
4587
|
+
writer.indent(() => {
|
|
4588
|
+
writer.writeLine("create: <T>(ctor: PomConstructor<T>) => new ctor(page),");
|
|
4589
|
+
});
|
|
4590
|
+
writer.writeLine("});");
|
|
4591
|
+
});
|
|
4592
|
+
writer.writeLine("},");
|
|
4593
|
+
writer.writeLine("...createPomFixtures(pageCtors),");
|
|
4594
|
+
writer.writeLine("...createPomFixtures(componentCtors),");
|
|
4595
|
+
});
|
|
4596
|
+
writer.write(")");
|
|
4597
|
+
}
|
|
4598
|
+
}]
|
|
4599
|
+
});
|
|
4600
|
+
sourceFile.addExportDeclaration({
|
|
4601
|
+
namedExports: ["test", "expect"]
|
|
4602
|
+
});
|
|
4603
|
+
}, {
|
|
4604
|
+
prefixText: buildFilePrefix({
|
|
4605
|
+
eslintDisableSortImports: true,
|
|
4606
|
+
commentLines: [
|
|
4607
|
+
"DO NOT MODIFY BY HAND",
|
|
4608
|
+
"",
|
|
4609
|
+
"This file is auto-generated by vue-pom-generator.",
|
|
4610
|
+
"Changes should be made in the generator/template, not in the generated output."
|
|
4611
|
+
]
|
|
4612
|
+
})
|
|
4613
|
+
});
|
|
4061
4614
|
return {
|
|
4062
4615
|
filePath: path.resolve(fixtureOutDirAbs, fixtureFileName),
|
|
4063
4616
|
content: fixturesContent
|
|
4064
4617
|
};
|
|
4065
4618
|
}
|
|
4066
|
-
function
|
|
4067
|
-
const { isView, childrenComponentSet, usedComponentSet
|
|
4619
|
+
function prepareViewObjectModelClass(componentName, dependencies, componentHierarchyMap, options = {}) {
|
|
4620
|
+
const { isView, childrenComponentSet, usedComponentSet } = dependencies;
|
|
4068
4621
|
const {
|
|
4069
|
-
outputDir = path.dirname(filePath),
|
|
4070
|
-
aggregated = false,
|
|
4071
4622
|
customPomAttachments = [],
|
|
4072
4623
|
testIdAttribute
|
|
4073
4624
|
} = options;
|
|
@@ -4101,45 +4652,9 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
|
|
|
4101
4652
|
flatten: a.flatten ?? false,
|
|
4102
4653
|
methodSignatures: a.flatten ? customPomMethodSignaturesByClass.get(a.className) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map()
|
|
4103
4654
|
}));
|
|
4104
|
-
let content = "";
|
|
4105
|
-
const sourceRel = toPosixRelativePath(outputDir, filePath);
|
|
4106
|
-
const kind = isView ? "Page" : "Component";
|
|
4107
|
-
const doc = `/** ${kind} POM: ${componentName} (source: ${sourceRel}) */
|
|
4108
|
-
`;
|
|
4109
|
-
if (!aggregated) {
|
|
4110
|
-
content = `${eslintSuppressionHeader}${doc}`;
|
|
4111
|
-
if (isView || attachmentsForThisClass.length > 0) {
|
|
4112
|
-
content += 'import type { Page as PwPage } from "@playwright/test";\n';
|
|
4113
|
-
}
|
|
4114
|
-
const projectRoot = options.projectRoot ?? process.cwd();
|
|
4115
|
-
const fromAbs = path.isAbsolute(outputDir) ? outputDir : path.resolve(projectRoot, outputDir);
|
|
4116
|
-
const toAbs = basePageClassPath ? path.isAbsolute(basePageClassPath) ? basePageClassPath : path.resolve(projectRoot, basePageClassPath) : "";
|
|
4117
|
-
const basePageImport = path.relative(fromAbs, toAbs).replace(/\\/g, "/");
|
|
4118
|
-
const basePageImportNoExt = stripExtension(basePageImport).replace(/\\/g, "/");
|
|
4119
|
-
const basePageImportSpecifier = basePageImportNoExt.startsWith(".") ? basePageImportNoExt : `./${basePageImportNoExt}`;
|
|
4120
|
-
content += `import { BasePage, Fluent } from '${basePageImportSpecifier}';
|
|
4121
|
-
|
|
4122
|
-
`;
|
|
4123
|
-
if (isView && childrenComponentSet.size > 0) {
|
|
4124
|
-
childrenComponentSet.forEach((child) => {
|
|
4125
|
-
if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
|
|
4126
|
-
const childPath = vueFilesPathMap.get(child);
|
|
4127
|
-
let relativePath = path.relative(outputDir, childPath || "");
|
|
4128
|
-
relativePath = changeExtension(relativePath, ".vue", ".g").replace(/\\/g, "/");
|
|
4129
|
-
content += `import { ${child} } from '${relativePath}';
|
|
4130
|
-
`;
|
|
4131
|
-
}
|
|
4132
|
-
});
|
|
4133
|
-
}
|
|
4134
|
-
} else {
|
|
4135
|
-
content = doc;
|
|
4136
|
-
}
|
|
4137
|
-
const className = toPascalCaseLocal(componentName);
|
|
4138
|
-
content += `
|
|
4139
|
-
export class ${className} extends BasePage {
|
|
4140
|
-
`;
|
|
4141
4655
|
const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
|
|
4142
4656
|
const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
|
|
4657
|
+
const className = toPascalCaseLocal(componentName);
|
|
4143
4658
|
const childInstancePropertyNames = Array.from(componentRefsForInstances).filter((child) => componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size).map((child) => child.split(".vue")[0]);
|
|
4144
4659
|
const blockedViewPassthroughMethodNames = new Set(
|
|
4145
4660
|
attachmentsForThisClass.filter((a) => a.flatten).flatMap((a) => Array.from(a.methodSignatures.keys()))
|
|
@@ -4149,26 +4664,144 @@ export class ${className} extends BasePage {
|
|
|
4149
4664
|
...widgetInstances.map((w) => w.propertyName),
|
|
4150
4665
|
...childInstancePropertyNames
|
|
4151
4666
|
]);
|
|
4667
|
+
const members = [];
|
|
4152
4668
|
if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
|
|
4153
|
-
|
|
4154
|
-
|
|
4669
|
+
members.push(...getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances));
|
|
4670
|
+
members.push(getConstructor(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances, { testIdAttribute }));
|
|
4155
4671
|
}
|
|
4156
4672
|
if (!isView && attachmentsForThisClass.length > 0) {
|
|
4157
|
-
|
|
4158
|
-
|
|
4673
|
+
members.push(...getComponentInstances(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass));
|
|
4674
|
+
members.push(getConstructor(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute }));
|
|
4159
4675
|
}
|
|
4160
|
-
|
|
4676
|
+
members.push(
|
|
4677
|
+
...getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames)
|
|
4678
|
+
);
|
|
4161
4679
|
if (isView && componentRefsForInstances.size === 1) {
|
|
4162
|
-
|
|
4680
|
+
members.push(
|
|
4681
|
+
...getViewPassthroughMethods(
|
|
4682
|
+
componentName,
|
|
4683
|
+
dependencies,
|
|
4684
|
+
componentRefsForInstances,
|
|
4685
|
+
componentHierarchyMap,
|
|
4686
|
+
blockedViewPassthroughMethodNames
|
|
4687
|
+
)
|
|
4688
|
+
);
|
|
4163
4689
|
}
|
|
4164
4690
|
if (isView && options.vueRouterFluentChaining) {
|
|
4165
4691
|
const routeMeta = options.routeMetaByComponent?.[componentName] ?? null;
|
|
4166
|
-
|
|
4167
|
-
|
|
4692
|
+
members.push(...generateRouteProperty(routeMeta));
|
|
4693
|
+
members.push(...generateGoToSelfMethod(className));
|
|
4168
4694
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4695
|
+
members.push(...generateMethodsContentForDependencies(dependencies));
|
|
4696
|
+
return {
|
|
4697
|
+
className,
|
|
4698
|
+
componentRefsForInstances,
|
|
4699
|
+
attachmentsForThisClass,
|
|
4700
|
+
widgetInstances,
|
|
4701
|
+
isView,
|
|
4702
|
+
members
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
function generateViewObjectModelContent(componentName, dependencies, componentHierarchyMap, _vueFilesPathMap, basePageClassPath, options = {}) {
|
|
4706
|
+
const { filePath } = dependencies;
|
|
4707
|
+
const outputDir = options.outputDir ?? path.dirname(filePath);
|
|
4708
|
+
const prepared = prepareViewObjectModelClass(componentName, dependencies, componentHierarchyMap, options);
|
|
4709
|
+
const sourceRel = toPosixRelativePath(outputDir, filePath);
|
|
4710
|
+
const kind = prepared.isView ? "Page" : "Component";
|
|
4711
|
+
const doc = `/** ${kind} POM: ${componentName} (source: ${sourceRel}) */`;
|
|
4712
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
4713
|
+
const fromAbs = path.isAbsolute(outputDir) ? outputDir : path.resolve(projectRoot, outputDir);
|
|
4714
|
+
const toAbs = basePageClassPath ? path.isAbsolute(basePageClassPath) ? basePageClassPath : path.resolve(projectRoot, basePageClassPath) : "";
|
|
4715
|
+
const basePageImport = path.relative(fromAbs, toAbs).replace(/\\/g, "/");
|
|
4716
|
+
const basePageImportNoExt = stripExtension(basePageImport).replace(/\\/g, "/");
|
|
4717
|
+
const basePageImportSpecifier = basePageImportNoExt.startsWith(".") ? basePageImportNoExt : `./${basePageImportNoExt}`;
|
|
4718
|
+
const needsPlaywrightPageImport = prepared.isView || prepared.attachmentsForThisClass.length > 0;
|
|
4719
|
+
const customPomImportSpecifiersByClass = options.customPomImportSpecifiersByClass ?? {};
|
|
4720
|
+
const customImports = Array.from(
|
|
4721
|
+
/* @__PURE__ */ new Set([
|
|
4722
|
+
...prepared.attachmentsForThisClass.map((attachment) => attachment.className),
|
|
4723
|
+
...prepared.widgetInstances.map((widget) => widget.className)
|
|
4724
|
+
])
|
|
4725
|
+
).reduce((imports, localIdentifier) => {
|
|
4726
|
+
const specifier = Object.values(customPomImportSpecifiersByClass).find((spec) => spec.localIdentifier === localIdentifier);
|
|
4727
|
+
if (!specifier) {
|
|
4728
|
+
return imports;
|
|
4729
|
+
}
|
|
4730
|
+
imports.push({
|
|
4731
|
+
moduleSpecifier: stripExtension(toPosixRelativePath(fromAbs, specifier.absolutePath)),
|
|
4732
|
+
name: specifier.exportName,
|
|
4733
|
+
alias: specifier.localIdentifier !== specifier.exportName ? specifier.localIdentifier : void 0
|
|
4734
|
+
});
|
|
4735
|
+
return imports;
|
|
4736
|
+
}, []).sort((a, b) => (a.alias ?? a.name).localeCompare(b.alias ?? b.name));
|
|
4737
|
+
const generatedImports = [];
|
|
4738
|
+
const importedGeneratedClasses = /* @__PURE__ */ new Set();
|
|
4739
|
+
const generatedTsFilePathByComponent = options.generatedTsFilePathByComponent;
|
|
4740
|
+
const addGeneratedImport = (className) => {
|
|
4741
|
+
if (!generatedTsFilePathByComponent || importedGeneratedClasses.has(className) || className === componentName) {
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
const generatedFilePath = generatedTsFilePathByComponent.get(className);
|
|
4745
|
+
if (!generatedFilePath) {
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
generatedImports.push({
|
|
4749
|
+
className,
|
|
4750
|
+
moduleSpecifier: stripExtension(toPosixRelativePath(fromAbs, generatedFilePath))
|
|
4751
|
+
});
|
|
4752
|
+
importedGeneratedClasses.add(className);
|
|
4753
|
+
};
|
|
4754
|
+
for (const child of prepared.componentRefsForInstances) {
|
|
4755
|
+
const childName = child.endsWith(".vue") ? child.slice(0, -4) : child;
|
|
4756
|
+
const childDeps = componentHierarchyMap.get(child) ?? componentHierarchyMap.get(childName);
|
|
4757
|
+
if (childDeps?.dataTestIdSet.size) {
|
|
4758
|
+
addGeneratedImport(childName);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
const targetClassNames = Array.from(
|
|
4762
|
+
new Set(
|
|
4763
|
+
Array.from(dependencies.dataTestIdSet ?? []).map((entry) => entry.targetPageObjectModelClass).filter((target) => typeof target === "string" && target.length > 0)
|
|
4764
|
+
)
|
|
4765
|
+
).sort((a, b) => a.localeCompare(b));
|
|
4766
|
+
for (const targetClassName of targetClassNames) {
|
|
4767
|
+
addGeneratedImport(targetClassName);
|
|
4768
|
+
}
|
|
4769
|
+
generatedImports.sort((a, b) => a.className.localeCompare(b.className));
|
|
4770
|
+
const prefixText = `${buildFilePrefix({ eslintDisableSortImports: true })}${doc}
|
|
4771
|
+
`;
|
|
4772
|
+
return renderSourceFile(`${prepared.className}.ts`, (sourceFile) => {
|
|
4773
|
+
if (needsPlaywrightPageImport) {
|
|
4774
|
+
addNamedImport(sourceFile, {
|
|
4775
|
+
moduleSpecifier: "@playwright/test",
|
|
4776
|
+
isTypeOnly: true,
|
|
4777
|
+
namedImports: [{ name: "Page", alias: "PwPage" }]
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
addNamedImport(sourceFile, {
|
|
4781
|
+
moduleSpecifier: basePageImportSpecifier,
|
|
4782
|
+
namedImports: ["BasePage", "Fluent"]
|
|
4783
|
+
});
|
|
4784
|
+
for (const customImport of customImports) {
|
|
4785
|
+
addNamedImport(sourceFile, {
|
|
4786
|
+
moduleSpecifier: customImport.moduleSpecifier,
|
|
4787
|
+
namedImports: [{ name: customImport.name, alias: customImport.alias }]
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
for (const generatedImport of generatedImports) {
|
|
4791
|
+
addNamedImport(sourceFile, {
|
|
4792
|
+
moduleSpecifier: generatedImport.moduleSpecifier,
|
|
4793
|
+
namedImports: [generatedImport.className]
|
|
4794
|
+
});
|
|
4795
|
+
}
|
|
4796
|
+
const classDeclaration = sourceFile.addClass({
|
|
4797
|
+
name: prepared.className,
|
|
4798
|
+
isExported: true,
|
|
4799
|
+
extends: "BasePage"
|
|
4800
|
+
});
|
|
4801
|
+
for (const member of prepared.members) {
|
|
4802
|
+
addClassMember(classDeclaration, member);
|
|
4803
|
+
}
|
|
4804
|
+
}, { prefixText });
|
|
4172
4805
|
}
|
|
4173
4806
|
function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap, blockedMethodNames = /* @__PURE__ */ new Set()) {
|
|
4174
4807
|
const existingOnView = viewDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
|
|
@@ -4192,32 +4825,26 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
|
|
|
4192
4825
|
}
|
|
4193
4826
|
}
|
|
4194
4827
|
const sorted = Array.from(methodToChildren.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4195
|
-
const
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4828
|
+
const passthroughs = sorted.filter(([, candidates]) => candidates.length === 1);
|
|
4829
|
+
if (!passthroughs.length) {
|
|
4830
|
+
return [];
|
|
4831
|
+
}
|
|
4832
|
+
return passthroughs.map(([methodName, candidates]) => {
|
|
4199
4833
|
const { childProp, params, argNames } = candidates[0];
|
|
4200
4834
|
const callArgs = argNames.join(", ");
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
}
|
|
4211
|
-
return [
|
|
4212
|
-
"",
|
|
4213
|
-
` // Passthrough methods composed from child component POMs of ${viewName}.`,
|
|
4214
|
-
...lines,
|
|
4215
|
-
""
|
|
4216
|
-
].join("\n");
|
|
4835
|
+
return createClassMethod({
|
|
4836
|
+
name: methodName,
|
|
4837
|
+
isAsync: true,
|
|
4838
|
+
parameters: parseParameterSignatures(params),
|
|
4839
|
+
statements: [
|
|
4840
|
+
`return await this.${childProp}.${methodName}(${callArgs});`
|
|
4841
|
+
]
|
|
4842
|
+
});
|
|
4843
|
+
});
|
|
4217
4844
|
}
|
|
4218
4845
|
function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmentsForThisClass, reservedMemberNames) {
|
|
4219
4846
|
if (!attachmentsForThisClass.some((a) => a.flatten && a.methodSignatures.size > 0)) {
|
|
4220
|
-
return
|
|
4847
|
+
return [];
|
|
4221
4848
|
}
|
|
4222
4849
|
const existingOnClass = ownerDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
|
|
4223
4850
|
const methodToAttachments = /* @__PURE__ */ new Map();
|
|
@@ -4239,30 +4866,22 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
|
|
|
4239
4866
|
}
|
|
4240
4867
|
}
|
|
4241
4868
|
const sorted = Array.from(methodToAttachments.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4242
|
-
const
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4869
|
+
const passthroughs = sorted.filter(([, candidates]) => candidates.length === 1);
|
|
4870
|
+
if (!passthroughs.length) {
|
|
4871
|
+
return [];
|
|
4872
|
+
}
|
|
4873
|
+
return passthroughs.map(([methodName, candidates]) => {
|
|
4247
4874
|
const { propertyName, params, argNames } = candidates[0];
|
|
4248
4875
|
const callArgs = argNames.join(", ");
|
|
4249
4876
|
const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
return "";
|
|
4259
|
-
}
|
|
4260
|
-
return [
|
|
4261
|
-
"",
|
|
4262
|
-
` // Passthrough methods composed from custom helper attachments of ${ownerName}.`,
|
|
4263
|
-
...lines,
|
|
4264
|
-
""
|
|
4265
|
-
].join("\n");
|
|
4877
|
+
return createClassMethod({
|
|
4878
|
+
name: methodName,
|
|
4879
|
+
parameters: parseParameterSignatures(params),
|
|
4880
|
+
statements: [
|
|
4881
|
+
`return ${invocation};`
|
|
4882
|
+
]
|
|
4883
|
+
});
|
|
4884
|
+
});
|
|
4266
4885
|
}
|
|
4267
4886
|
function sliceNodeSource(source, node) {
|
|
4268
4887
|
if (node.start == null || node.end == null) {
|
|
@@ -4346,12 +4965,292 @@ function ensureDir(dir) {
|
|
|
4346
4965
|
}
|
|
4347
4966
|
return normalized;
|
|
4348
4967
|
}
|
|
4968
|
+
function resolvePluginAsset(relative) {
|
|
4969
|
+
try {
|
|
4970
|
+
return node_url.fileURLToPath(new URL(relative, typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4971
|
+
} catch {
|
|
4972
|
+
return path.resolve(__dirname, relative);
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
function readTextAsset(absPath, description) {
|
|
4976
|
+
try {
|
|
4977
|
+
return fs.readFileSync(absPath, "utf8");
|
|
4978
|
+
} catch {
|
|
4979
|
+
throw new VuePomGeneratorError(`Failed to read ${description} at ${absPath}`);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
function getDefaultStubMembers() {
|
|
4983
|
+
return [
|
|
4984
|
+
createClassConstructor({
|
|
4985
|
+
parameters: [{ name: "page", type: "PwPage" }],
|
|
4986
|
+
statements: [
|
|
4987
|
+
"super(page);"
|
|
4988
|
+
]
|
|
4989
|
+
})
|
|
4990
|
+
];
|
|
4991
|
+
}
|
|
4992
|
+
function renderSplitStubPomContent(options) {
|
|
4993
|
+
const prefixText = buildFilePrefix({
|
|
4994
|
+
eslintDisableSortImports: true,
|
|
4995
|
+
commentLines: [
|
|
4996
|
+
`Stub POM: ${options.className}`,
|
|
4997
|
+
"DO NOT MODIFY BY HAND",
|
|
4998
|
+
"",
|
|
4999
|
+
"This file is auto-generated by vue-pom-generator.",
|
|
5000
|
+
"Changes should be made in the generator/template, not in the generated output."
|
|
5001
|
+
]
|
|
5002
|
+
});
|
|
5003
|
+
return renderSourceFile(`${options.className}.ts`, (sourceFile) => {
|
|
5004
|
+
addNamedImport(sourceFile, {
|
|
5005
|
+
moduleSpecifier: "@playwright/test",
|
|
5006
|
+
isTypeOnly: true,
|
|
5007
|
+
namedImports: [{ name: "Page", alias: "PwPage" }]
|
|
5008
|
+
});
|
|
5009
|
+
addNamedImport(sourceFile, {
|
|
5010
|
+
moduleSpecifier: options.basePageImportSpecifier,
|
|
5011
|
+
namedImports: ["BasePage"]
|
|
5012
|
+
});
|
|
5013
|
+
for (const childImport of options.childImports) {
|
|
5014
|
+
addNamedImport(sourceFile, {
|
|
5015
|
+
moduleSpecifier: childImport.importPath,
|
|
5016
|
+
namedImports: [childImport.className]
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
sourceFile.addStatements(buildCommentBlock([
|
|
5020
|
+
"Stub POM generated because it is referenced as a navigation target but",
|
|
5021
|
+
"did not have any generated test ids in this build."
|
|
5022
|
+
]).trimEnd());
|
|
5023
|
+
const classDeclaration = sourceFile.addClass({
|
|
5024
|
+
name: options.className,
|
|
5025
|
+
isExported: true,
|
|
5026
|
+
extends: "BasePage"
|
|
5027
|
+
});
|
|
5028
|
+
for (const member of options.members) {
|
|
5029
|
+
addClassMember(classDeclaration, member);
|
|
5030
|
+
}
|
|
5031
|
+
}, { prefixText });
|
|
5032
|
+
}
|
|
5033
|
+
function getChildImportSpecifiers(outputDir, childClassNames, generatedTsFilePathByComponent) {
|
|
5034
|
+
return childClassNames.map((childClassName) => {
|
|
5035
|
+
const childFilePath = generatedTsFilePathByComponent.get(childClassName);
|
|
5036
|
+
if (!childFilePath) {
|
|
5037
|
+
return null;
|
|
5038
|
+
}
|
|
5039
|
+
return {
|
|
5040
|
+
className: childClassName,
|
|
5041
|
+
importPath: stripExtension(toPosixRelativePath(outputDir, childFilePath))
|
|
5042
|
+
};
|
|
5043
|
+
}).filter((entry) => !!entry).sort((a, b) => a.className.localeCompare(b.className));
|
|
5044
|
+
}
|
|
5045
|
+
function isConstructorMember(member) {
|
|
5046
|
+
return member.kind === tsMorph.StructureKind.Constructor;
|
|
5047
|
+
}
|
|
5048
|
+
function isGetterMember(member) {
|
|
5049
|
+
return member.kind === tsMorph.StructureKind.GetAccessor;
|
|
5050
|
+
}
|
|
5051
|
+
function isMethodMember(member) {
|
|
5052
|
+
return member.kind === tsMorph.StructureKind.Method;
|
|
5053
|
+
}
|
|
5054
|
+
function isPropertyMember(member) {
|
|
5055
|
+
return member.kind === tsMorph.StructureKind.Property;
|
|
5056
|
+
}
|
|
5057
|
+
function addClassMember(classDeclaration, member) {
|
|
5058
|
+
if (isConstructorMember(member)) {
|
|
5059
|
+
classDeclaration.addConstructor(member);
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
if (isGetterMember(member)) {
|
|
5063
|
+
classDeclaration.addGetAccessor(member);
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
if (isMethodMember(member)) {
|
|
5067
|
+
classDeclaration.addMethod(member);
|
|
5068
|
+
return;
|
|
5069
|
+
}
|
|
5070
|
+
if (isPropertyMember(member)) {
|
|
5071
|
+
classDeclaration.addProperty(member);
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
throw new Error(`Unsupported class member structure: ${String(member)}`);
|
|
5075
|
+
}
|
|
5076
|
+
function getRuntimeGeneratedAssetSpecs(baseDir, basePageClassPath) {
|
|
5077
|
+
const runtimeDirAbs = path.join(baseDir, "_pom-runtime");
|
|
5078
|
+
const runtimeClassGenAbs = path.join(runtimeDirAbs, "class-generation");
|
|
5079
|
+
const runtimeClassGenSourceDir = resolvePluginAsset("../class-generation");
|
|
5080
|
+
const runtimeClassGenFiles = fs.readdirSync(runtimeClassGenSourceDir).filter((file) => file.endsWith(".ts")).filter((file) => file !== "BasePage.ts" && file !== "index.ts").sort((left, right) => left.localeCompare(right));
|
|
5081
|
+
return [
|
|
5082
|
+
{
|
|
5083
|
+
absolutePath: resolvePluginAsset("../click-instrumentation.ts"),
|
|
5084
|
+
description: "click-instrumentation.ts",
|
|
5085
|
+
outputPath: path.join(runtimeDirAbs, "click-instrumentation.ts")
|
|
5086
|
+
},
|
|
5087
|
+
...runtimeClassGenFiles.map((file) => ({
|
|
5088
|
+
absolutePath: path.join(runtimeClassGenSourceDir, file),
|
|
5089
|
+
description: file,
|
|
5090
|
+
outputPath: path.join(runtimeClassGenAbs, file)
|
|
5091
|
+
})),
|
|
5092
|
+
{
|
|
5093
|
+
absolutePath: basePageClassPath,
|
|
5094
|
+
description: "BasePage.ts",
|
|
5095
|
+
outputPath: path.join(runtimeClassGenAbs, "BasePage.ts")
|
|
5096
|
+
}
|
|
5097
|
+
];
|
|
5098
|
+
}
|
|
5099
|
+
function buildRuntimeGeneratedFiles(baseDir, basePageClassPath) {
|
|
5100
|
+
return buildRuntimeGeneratedFilesFromSpecs(getRuntimeGeneratedAssetSpecs(baseDir, basePageClassPath));
|
|
5101
|
+
}
|
|
5102
|
+
function buildRuntimeGeneratedFilesFromSpecs(assetSpecs) {
|
|
5103
|
+
return assetSpecs.map((spec) => ({
|
|
5104
|
+
filePath: spec.outputPath,
|
|
5105
|
+
content: readTextAsset(spec.absolutePath, spec.description)
|
|
5106
|
+
}));
|
|
5107
|
+
}
|
|
5108
|
+
function resolveCustomPomImportResolution(generatedClassNames, projectRoot, options = {}) {
|
|
5109
|
+
const importAliases = {
|
|
5110
|
+
Toggle: "ToggleWidget",
|
|
5111
|
+
Checkbox: "CheckboxWidget",
|
|
5112
|
+
...options.customPomImportAliases
|
|
5113
|
+
};
|
|
5114
|
+
const importCollisionBehavior = options.customPomImportNameCollisionBehavior ?? "error";
|
|
5115
|
+
const reservedIdentifiers = /* @__PURE__ */ new Set([
|
|
5116
|
+
"PwLocator",
|
|
5117
|
+
"PwPage",
|
|
5118
|
+
"BasePage",
|
|
5119
|
+
"Fluent",
|
|
5120
|
+
...generatedClassNames
|
|
5121
|
+
]);
|
|
5122
|
+
const usedImportIdentifiers = /* @__PURE__ */ new Set();
|
|
5123
|
+
const classIdentifierMap = {};
|
|
5124
|
+
const methodSignaturesByClass = /* @__PURE__ */ new Map();
|
|
5125
|
+
const importSpecifiersByClass = {};
|
|
5126
|
+
const ensureUniqueIdentifier = (base) => {
|
|
5127
|
+
let candidate = base;
|
|
5128
|
+
let i = 2;
|
|
5129
|
+
while (reservedIdentifiers.has(candidate) || usedImportIdentifiers.has(candidate)) {
|
|
5130
|
+
candidate = `${base}${i}`;
|
|
5131
|
+
i++;
|
|
5132
|
+
}
|
|
5133
|
+
usedImportIdentifiers.add(candidate);
|
|
5134
|
+
return candidate;
|
|
5135
|
+
};
|
|
5136
|
+
const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
|
|
5137
|
+
const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
|
|
5138
|
+
if (!fs.existsSync(customDirAbs)) {
|
|
5139
|
+
return {
|
|
5140
|
+
classIdentifierMap,
|
|
5141
|
+
methodSignaturesByClass,
|
|
5142
|
+
availableClassIdentifiers: /* @__PURE__ */ new Set(),
|
|
5143
|
+
importSpecifiersByClass
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
|
|
5147
|
+
for (const file of files) {
|
|
5148
|
+
const exportName = file.replace(/\.ts$/i, "");
|
|
5149
|
+
const requested = importAliases[exportName] ?? exportName;
|
|
5150
|
+
const collidesWithGeneratedClass = generatedClassNames.has(requested);
|
|
5151
|
+
const explicitAliasProvided = Object.prototype.hasOwnProperty.call(importAliases, exportName);
|
|
5152
|
+
if (collidesWithGeneratedClass && importCollisionBehavior === "error") {
|
|
5153
|
+
throw createCustomPomImportCollisionError(exportName, requested);
|
|
5154
|
+
}
|
|
5155
|
+
let localIdentifier = requested;
|
|
5156
|
+
if (collidesWithGeneratedClass && importCollisionBehavior === "alias") {
|
|
5157
|
+
const aliasBase = explicitAliasProvided ? requested : `${exportName}Custom`;
|
|
5158
|
+
localIdentifier = ensureUniqueIdentifier(aliasBase);
|
|
5159
|
+
} else {
|
|
5160
|
+
localIdentifier = ensureUniqueIdentifier(requested);
|
|
5161
|
+
}
|
|
5162
|
+
const customFileAbs = path.join(customDirAbs, file);
|
|
5163
|
+
classIdentifierMap[exportName] = localIdentifier;
|
|
5164
|
+
importSpecifiersByClass[exportName] = {
|
|
5165
|
+
exportName,
|
|
5166
|
+
localIdentifier,
|
|
5167
|
+
absolutePath: customFileAbs
|
|
5168
|
+
};
|
|
5169
|
+
const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
|
|
5170
|
+
if (customPomMethodSignatures.size > 0) {
|
|
5171
|
+
methodSignaturesByClass.set(exportName, customPomMethodSignatures);
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
return {
|
|
5175
|
+
classIdentifierMap,
|
|
5176
|
+
methodSignaturesByClass,
|
|
5177
|
+
availableClassIdentifiers: new Set(Object.values(classIdentifierMap)),
|
|
5178
|
+
importSpecifiersByClass
|
|
5179
|
+
};
|
|
5180
|
+
}
|
|
5181
|
+
function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
|
|
5182
|
+
const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
|
|
5183
|
+
if (!filePath)
|
|
5184
|
+
return void 0;
|
|
5185
|
+
let source = "";
|
|
5186
|
+
try {
|
|
5187
|
+
source = fs.readFileSync(filePath, "utf8");
|
|
5188
|
+
} catch {
|
|
5189
|
+
return void 0;
|
|
5190
|
+
}
|
|
5191
|
+
const tags = getComponentClassNamesFromVueSource(source);
|
|
5192
|
+
const childClassNames = Array.from(
|
|
5193
|
+
new Set(
|
|
5194
|
+
tags.filter((name) => availableClassNames.has(name)).filter((name) => name !== targetClassName)
|
|
5195
|
+
)
|
|
5196
|
+
).sort((a, b) => a.localeCompare(b));
|
|
5197
|
+
if (!childClassNames.length)
|
|
5198
|
+
return void 0;
|
|
5199
|
+
const methodToChildren = /* @__PURE__ */ new Map();
|
|
5200
|
+
for (const child of childClassNames) {
|
|
5201
|
+
const childDeps = depsByClassName.get(child);
|
|
5202
|
+
const methods = childDeps?.generatedMethods;
|
|
5203
|
+
if (!methods)
|
|
5204
|
+
continue;
|
|
5205
|
+
for (const [name, sig] of methods.entries()) {
|
|
5206
|
+
if (!sig)
|
|
5207
|
+
continue;
|
|
5208
|
+
const list = methodToChildren.get(name) ?? [];
|
|
5209
|
+
list.push({ child, params: sig.params, argNames: sig.argNames });
|
|
5210
|
+
methodToChildren.set(name, list);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
const passthroughMembers = [];
|
|
5214
|
+
for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
|
|
5215
|
+
if (candidatesForMethod.length !== 1 || methodName === "constructor")
|
|
5216
|
+
continue;
|
|
5217
|
+
const { child, params, argNames } = candidatesForMethod[0];
|
|
5218
|
+
const callArgs = argNames.join(", ");
|
|
5219
|
+
passthroughMembers.push(createClassMethod({
|
|
5220
|
+
name: methodName,
|
|
5221
|
+
isAsync: true,
|
|
5222
|
+
parameters: parseParameterSignatures(params),
|
|
5223
|
+
statements: [
|
|
5224
|
+
`return await this.${child}.${methodName}(${callArgs});`
|
|
5225
|
+
]
|
|
5226
|
+
}));
|
|
5227
|
+
}
|
|
5228
|
+
return {
|
|
5229
|
+
childClassNames,
|
|
5230
|
+
members: [
|
|
5231
|
+
...childClassNames.map((childClassName) => createClassProperty({
|
|
5232
|
+
name: childClassName,
|
|
5233
|
+
type: childClassName
|
|
5234
|
+
})),
|
|
5235
|
+
createClassConstructor({
|
|
5236
|
+
parameters: [{ name: "page", type: "PwPage" }],
|
|
5237
|
+
statements: (writer) => {
|
|
5238
|
+
writer.writeLine("super(page);");
|
|
5239
|
+
for (const childClassName of childClassNames) {
|
|
5240
|
+
writer.writeLine(`this.${childClassName} = new ${childClassName}(page);`);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
}),
|
|
5244
|
+
...passthroughMembers
|
|
5245
|
+
]
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
4349
5248
|
async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, options = {}) {
|
|
4350
5249
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
4351
5250
|
const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4352
5251
|
const views = entries.filter(([, d]) => d.isView);
|
|
4353
5252
|
const components = entries.filter(([, d]) => !d.isView);
|
|
4354
|
-
const makeAggregatedContent = (
|
|
5253
|
+
const makeAggregatedContent = (outputDir, items) => {
|
|
4355
5254
|
const imports = [];
|
|
4356
5255
|
const generatedClassNames = new Set(items.map(([name]) => name));
|
|
4357
5256
|
if (!basePageClassPath) {
|
|
@@ -4366,84 +5265,22 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
|
|
|
4366
5265
|
imports.push(`export * from "${runtimeClassGenRel}/playwright-types";`);
|
|
4367
5266
|
imports.push(`export * from "${runtimeClassGenRel}/Pointer";`);
|
|
4368
5267
|
imports.push(`export * from "${runtimeClassGenRel}/BasePage";`);
|
|
4369
|
-
const
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
"
|
|
4381
|
-
|
|
4382
|
-
]);
|
|
4383
|
-
const usedImportIdentifiers = /* @__PURE__ */ new Set();
|
|
4384
|
-
const customPomClassIdentifierMap2 = {};
|
|
4385
|
-
const customPomMethodSignaturesByClass2 = /* @__PURE__ */ new Map();
|
|
4386
|
-
const ensureUniqueIdentifier = (base2) => {
|
|
4387
|
-
let candidate = base2;
|
|
4388
|
-
let i = 2;
|
|
4389
|
-
while (reservedIdentifiers.has(candidate) || usedImportIdentifiers.has(candidate)) {
|
|
4390
|
-
candidate = `${base2}${i}`;
|
|
4391
|
-
i++;
|
|
4392
|
-
}
|
|
4393
|
-
usedImportIdentifiers.add(candidate);
|
|
4394
|
-
return candidate;
|
|
4395
|
-
};
|
|
4396
|
-
const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
|
|
4397
|
-
const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
|
|
4398
|
-
if (!fs.existsSync(customDirAbs)) {
|
|
4399
|
-
return {
|
|
4400
|
-
classIdentifierMap: customPomClassIdentifierMap2,
|
|
4401
|
-
methodSignaturesByClass: customPomMethodSignaturesByClass2
|
|
4402
|
-
};
|
|
4403
|
-
}
|
|
4404
|
-
const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
|
|
4405
|
-
for (const file of files) {
|
|
4406
|
-
const exportName = file.replace(/\.ts$/i, "");
|
|
4407
|
-
const requested = importAliases[exportName] ?? exportName;
|
|
4408
|
-
const collidesWithGeneratedClass = generatedClassNames.has(requested);
|
|
4409
|
-
const explicitAliasProvided = Object.prototype.hasOwnProperty.call(importAliases, exportName);
|
|
4410
|
-
if (collidesWithGeneratedClass && importCollisionBehavior === "error") {
|
|
4411
|
-
throw new Error(
|
|
4412
|
-
`[vue-pom-generator] Custom POM import name collision detected for "${exportName}".
|
|
4413
|
-
The identifier "${requested}" conflicts with a generated POM class.
|
|
4414
|
-
Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] to a unique name, or set generation.playwright.customPoms.importNameCollisionBehavior = "alias" to auto-alias collisions.`
|
|
4415
|
-
);
|
|
4416
|
-
}
|
|
4417
|
-
let localIdentifier = requested;
|
|
4418
|
-
if (collidesWithGeneratedClass && importCollisionBehavior === "alias") {
|
|
4419
|
-
const aliasBase = explicitAliasProvided ? requested : `${exportName}Custom`;
|
|
4420
|
-
localIdentifier = ensureUniqueIdentifier(aliasBase);
|
|
4421
|
-
} else {
|
|
4422
|
-
localIdentifier = ensureUniqueIdentifier(requested);
|
|
4423
|
-
}
|
|
4424
|
-
const customFileAbs = path.join(customDirAbs, file);
|
|
4425
|
-
customPomClassIdentifierMap2[exportName] = localIdentifier;
|
|
4426
|
-
const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
|
|
4427
|
-
if (customPomMethodSignatures.size > 0) {
|
|
4428
|
-
customPomMethodSignaturesByClass2.set(exportName, customPomMethodSignatures);
|
|
4429
|
-
}
|
|
4430
|
-
const fromOutputDir = outputDir;
|
|
4431
|
-
const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
|
|
4432
|
-
if (localIdentifier !== exportName) {
|
|
4433
|
-
imports.push(`import { ${exportName} as ${localIdentifier} } from "${importPath}";`);
|
|
4434
|
-
} else {
|
|
4435
|
-
imports.push(`import { ${exportName} } from "${importPath}";`);
|
|
4436
|
-
}
|
|
5268
|
+
const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
|
|
5269
|
+
customPomDir: options.customPomDir,
|
|
5270
|
+
customPomImportAliases: options.customPomImportAliases,
|
|
5271
|
+
customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
|
|
5272
|
+
});
|
|
5273
|
+
const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
|
|
5274
|
+
const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
|
|
5275
|
+
const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
|
|
5276
|
+
for (const importSpecifier of Object.values(customPomImportResolution.importSpecifiersByClass).sort((left, right) => left.exportName.localeCompare(right.exportName))) {
|
|
5277
|
+
const importPath = stripExtension(toPosixRelativePath(outputDir, importSpecifier.absolutePath));
|
|
5278
|
+
if (importSpecifier.localIdentifier !== importSpecifier.exportName) {
|
|
5279
|
+
imports.push(`import { ${importSpecifier.exportName} as ${importSpecifier.localIdentifier} } from "${importPath}";`);
|
|
5280
|
+
continue;
|
|
4437
5281
|
}
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
methodSignaturesByClass: customPomMethodSignaturesByClass2
|
|
4441
|
-
};
|
|
4442
|
-
};
|
|
4443
|
-
const customPomImportResolution = addCustomPomImports();
|
|
4444
|
-
const customPomClassIdentifierMap = customPomImportResolution?.classIdentifierMap ?? {};
|
|
4445
|
-
const customPomMethodSignaturesByClass = customPomImportResolution?.methodSignaturesByClass ?? /* @__PURE__ */ new Map();
|
|
4446
|
-
const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap));
|
|
5282
|
+
imports.push(`import { ${importSpecifier.exportName} } from "${importPath}";`);
|
|
5283
|
+
}
|
|
4447
5284
|
const referencedTargets = /* @__PURE__ */ new Set();
|
|
4448
5285
|
for (const [, deps] of items) {
|
|
4449
5286
|
for (const dt of deps.dataTestIdSet) {
|
|
@@ -4455,145 +5292,18 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
|
|
|
4455
5292
|
const stubTargets = Array.from(referencedTargets).filter((t) => !generatedClassNames.has(t)).sort((a, b) => a.localeCompare(b));
|
|
4456
5293
|
const availableClassNames = /* @__PURE__ */ new Set([...generatedClassNames, ...stubTargets]);
|
|
4457
5294
|
const depsByClassName = new Map(entries);
|
|
4458
|
-
const scanPascalCaseTags = (template) => {
|
|
4459
|
-
const names = [];
|
|
4460
|
-
const len = template.length;
|
|
4461
|
-
let i = 0;
|
|
4462
|
-
while (i < len) {
|
|
4463
|
-
const ch = template[i];
|
|
4464
|
-
if (ch !== "<") {
|
|
4465
|
-
i++;
|
|
4466
|
-
continue;
|
|
4467
|
-
}
|
|
4468
|
-
i++;
|
|
4469
|
-
if (i >= len)
|
|
4470
|
-
break;
|
|
4471
|
-
if (template[i] === "/" || template[i] === "!" || template[i] === "?") {
|
|
4472
|
-
i++;
|
|
4473
|
-
continue;
|
|
4474
|
-
}
|
|
4475
|
-
while (i < len && (template[i] === " " || template[i] === "\n" || template[i] === " " || template[i] === "\r")) i++;
|
|
4476
|
-
if (i >= len)
|
|
4477
|
-
break;
|
|
4478
|
-
const first = template[i];
|
|
4479
|
-
if (first < "A" || first > "Z") {
|
|
4480
|
-
continue;
|
|
4481
|
-
}
|
|
4482
|
-
const start = i;
|
|
4483
|
-
i++;
|
|
4484
|
-
while (i < len) {
|
|
4485
|
-
const c = template[i];
|
|
4486
|
-
const isLetter = c >= "A" && c <= "Z" || c >= "a" && c <= "z";
|
|
4487
|
-
const isDigit = c >= "0" && c <= "9";
|
|
4488
|
-
const isUnderscore = c === "_";
|
|
4489
|
-
if (isLetter || isDigit || isUnderscore) {
|
|
4490
|
-
i++;
|
|
4491
|
-
continue;
|
|
4492
|
-
}
|
|
4493
|
-
break;
|
|
4494
|
-
}
|
|
4495
|
-
const name = template.slice(start, i);
|
|
4496
|
-
if (name)
|
|
4497
|
-
names.push(name);
|
|
4498
|
-
}
|
|
4499
|
-
return Array.from(new Set(names));
|
|
4500
|
-
};
|
|
4501
|
-
const getComposedStubBody = (targetClassName) => {
|
|
4502
|
-
const mapped = vueFilesPathMap.get(targetClassName);
|
|
4503
|
-
const candidates = [
|
|
4504
|
-
mapped,
|
|
4505
|
-
path.join(projectRoot, "src", "views", `${targetClassName}.vue`),
|
|
4506
|
-
path.join(projectRoot, "src", "components", `${targetClassName}.vue`)
|
|
4507
|
-
].filter((p) => typeof p === "string" && p.length > 0);
|
|
4508
|
-
const filePath = candidates.find((p) => fs.existsSync(p));
|
|
4509
|
-
if (!filePath)
|
|
4510
|
-
return void 0;
|
|
4511
|
-
let source = "";
|
|
4512
|
-
try {
|
|
4513
|
-
source = fs.readFileSync(filePath, "utf8");
|
|
4514
|
-
} catch {
|
|
4515
|
-
return void 0;
|
|
4516
|
-
}
|
|
4517
|
-
const templateOpen = source.indexOf("<template");
|
|
4518
|
-
const templateClose = source.lastIndexOf("</template>");
|
|
4519
|
-
if (templateOpen === -1 || templateClose === -1 || templateClose <= templateOpen)
|
|
4520
|
-
return void 0;
|
|
4521
|
-
const afterOpenTag = source.indexOf(">", templateOpen);
|
|
4522
|
-
if (afterOpenTag === -1 || afterOpenTag >= templateClose)
|
|
4523
|
-
return void 0;
|
|
4524
|
-
const template = source.slice(afterOpenTag + 1, templateClose);
|
|
4525
|
-
if (!template)
|
|
4526
|
-
return void 0;
|
|
4527
|
-
const tags = scanPascalCaseTags(template);
|
|
4528
|
-
const childClassNames = Array.from(
|
|
4529
|
-
new Set(
|
|
4530
|
-
tags.filter((name) => availableClassNames.has(name)).filter((name) => name !== targetClassName)
|
|
4531
|
-
)
|
|
4532
|
-
).sort((a, b) => a.localeCompare(b));
|
|
4533
|
-
if (!childClassNames.length)
|
|
4534
|
-
return void 0;
|
|
4535
|
-
const methodToChildren = /* @__PURE__ */ new Map();
|
|
4536
|
-
for (const child of childClassNames) {
|
|
4537
|
-
const childDeps = depsByClassName.get(child);
|
|
4538
|
-
const methods = childDeps?.generatedMethods;
|
|
4539
|
-
if (!methods)
|
|
4540
|
-
continue;
|
|
4541
|
-
for (const [name, sig] of methods.entries()) {
|
|
4542
|
-
if (!sig)
|
|
4543
|
-
continue;
|
|
4544
|
-
const list = methodToChildren.get(name) ?? [];
|
|
4545
|
-
list.push({ child, params: sig.params, argNames: sig.argNames });
|
|
4546
|
-
methodToChildren.set(name, list);
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
const passthroughLines = [];
|
|
4550
|
-
for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
|
|
4551
|
-
if (candidatesForMethod.length !== 1)
|
|
4552
|
-
continue;
|
|
4553
|
-
if (methodName === "constructor")
|
|
4554
|
-
continue;
|
|
4555
|
-
const { child, params, argNames } = candidatesForMethod[0];
|
|
4556
|
-
const callArgs = argNames.join(", ");
|
|
4557
|
-
passthroughLines.push(
|
|
4558
|
-
"",
|
|
4559
|
-
` async ${methodName}(${params}) {`,
|
|
4560
|
-
` return await this.${child}.${methodName}(${callArgs});`,
|
|
4561
|
-
" }"
|
|
4562
|
-
);
|
|
4563
|
-
}
|
|
4564
|
-
return {
|
|
4565
|
-
childClassNames,
|
|
4566
|
-
lines: [
|
|
4567
|
-
...childClassNames.map((c) => ` ${c}: ${c};`),
|
|
4568
|
-
"",
|
|
4569
|
-
" constructor(page: PwPage) {",
|
|
4570
|
-
" super(page);",
|
|
4571
|
-
...childClassNames.map((c) => ` this.${c} = new ${c}(page);`),
|
|
4572
|
-
" }",
|
|
4573
|
-
...passthroughLines
|
|
4574
|
-
]
|
|
4575
|
-
};
|
|
4576
|
-
};
|
|
4577
5295
|
const stubs = stubTargets.map(
|
|
4578
5296
|
(t) => (() => {
|
|
4579
|
-
const composed = getComposedStubBody(t);
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
return [
|
|
4586
|
-
"/**\n * Stub POM generated because it is referenced as a navigation target but\n * did not have any generated test ids in this build.\n */",
|
|
4587
|
-
`export class ${t} extends BasePage {`,
|
|
4588
|
-
...body,
|
|
4589
|
-
"}"
|
|
4590
|
-
].join("\n");
|
|
5297
|
+
const composed = getComposedStubBody(t, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot);
|
|
5298
|
+
return {
|
|
5299
|
+
className: t,
|
|
5300
|
+
members: composed?.members ?? getDefaultStubMembers(),
|
|
5301
|
+
isStub: true
|
|
5302
|
+
};
|
|
4591
5303
|
})()
|
|
4592
5304
|
);
|
|
4593
|
-
const classes = items.map(
|
|
4594
|
-
|
|
4595
|
-
outputDir,
|
|
4596
|
-
aggregated: true,
|
|
5305
|
+
const classes = items.map(([name, deps]) => {
|
|
5306
|
+
const prepared = prepareViewObjectModelClass(name, deps, componentHierarchyMap, {
|
|
4597
5307
|
customPomAttachments: options.customPomAttachments ?? [],
|
|
4598
5308
|
customPomClassIdentifierMap,
|
|
4599
5309
|
customPomAvailableClassIdentifiers,
|
|
@@ -4601,67 +5311,70 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
|
|
|
4601
5311
|
testIdAttribute: options.testIdAttribute,
|
|
4602
5312
|
vueRouterFluentChaining: options.vueRouterFluentChaining,
|
|
4603
5313
|
routeMetaByComponent: options.routeMetaByComponent
|
|
4604
|
-
})
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
5314
|
+
});
|
|
5315
|
+
const sourceRel = toPosixRelativePath(outputDir, deps.filePath);
|
|
5316
|
+
const kind = deps.isView ? "Page" : "Component";
|
|
5317
|
+
return {
|
|
5318
|
+
className: prepared.className,
|
|
5319
|
+
doc: `/** ${kind} POM: ${name} (source: ${sourceRel}) */`,
|
|
5320
|
+
members: prepared.members,
|
|
5321
|
+
isStub: false
|
|
5322
|
+
};
|
|
5323
|
+
});
|
|
5324
|
+
const prefixText = buildFilePrefix({
|
|
5325
|
+
referenceLib: "es2015",
|
|
5326
|
+
eslintDisableSortImports: true,
|
|
5327
|
+
commentLines: [
|
|
5328
|
+
"Aggregated generated POMs",
|
|
5329
|
+
"DO NOT MODIFY BY HAND",
|
|
5330
|
+
"",
|
|
5331
|
+
"This file is auto-generated by vue-pom-generator.",
|
|
5332
|
+
"Changes should be made in the generator/template, not in the generated output."
|
|
5333
|
+
]
|
|
5334
|
+
});
|
|
5335
|
+
return renderSourceFile("page-object-models.g.ts", (sourceFile) => {
|
|
5336
|
+
for (const line of imports) {
|
|
5337
|
+
sourceFile.addStatements(line);
|
|
5338
|
+
}
|
|
5339
|
+
for (const entry of [...classes, ...stubs]) {
|
|
5340
|
+
if (entry.isStub) {
|
|
5341
|
+
sourceFile.addStatements(buildCommentBlock([
|
|
5342
|
+
"Stub POM generated because it is referenced as a navigation target but",
|
|
5343
|
+
"did not have any generated test ids in this build."
|
|
5344
|
+
]).trimEnd());
|
|
5345
|
+
} else {
|
|
5346
|
+
sourceFile.addStatements(entry.doc);
|
|
5347
|
+
}
|
|
5348
|
+
const classDeclaration = sourceFile.addClass({
|
|
5349
|
+
name: entry.className,
|
|
5350
|
+
isExported: true,
|
|
5351
|
+
extends: "BasePage"
|
|
5352
|
+
});
|
|
5353
|
+
for (const member of entry.members) {
|
|
5354
|
+
addClassMember(classDeclaration, member);
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
}, { prefixText });
|
|
4613
5358
|
};
|
|
4614
5359
|
const base = ensureDir(outDir);
|
|
4615
5360
|
const outputFile = path.join(base, "page-object-models.g.ts");
|
|
4616
|
-
const
|
|
4617
|
-
${eslintSuppressionHeader}/**
|
|
4618
|
-
* Aggregated generated POMs
|
|
4619
|
-
${AUTO_GENERATED_COMMENT}`;
|
|
4620
|
-
const content = makeAggregatedContent(header, path.dirname(outputFile), [...views, ...components]);
|
|
5361
|
+
const content = makeAggregatedContent(path.dirname(outputFile), [...views, ...components]);
|
|
4621
5362
|
const indexFile = path.join(base, "index.ts");
|
|
4622
|
-
const indexContent =
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
const resolvePluginAsset = (relative) => {
|
|
4638
|
-
try {
|
|
4639
|
-
return node_url.fileURLToPath(new URL(relative, typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4640
|
-
} catch {
|
|
4641
|
-
return path.resolve(__dirname, relative);
|
|
4642
|
-
}
|
|
4643
|
-
};
|
|
4644
|
-
const clickInstrumentationAbs = resolvePluginAsset("../click-instrumentation.ts");
|
|
4645
|
-
const pointerAbs = resolvePluginAsset("../class-generation/Pointer.ts");
|
|
4646
|
-
const playwrightTypesAbs = resolvePluginAsset("../class-generation/playwright-types.ts");
|
|
4647
|
-
const runtimeFiles = [
|
|
4648
|
-
{
|
|
4649
|
-
filePath: path.join(runtimeDirAbs, "click-instrumentation.ts"),
|
|
4650
|
-
content: readText(clickInstrumentationAbs, "click-instrumentation.ts")
|
|
4651
|
-
},
|
|
4652
|
-
{
|
|
4653
|
-
filePath: path.join(runtimeClassGenAbs, "Pointer.ts"),
|
|
4654
|
-
content: readText(pointerAbs, "Pointer.ts")
|
|
4655
|
-
},
|
|
4656
|
-
{
|
|
4657
|
-
filePath: path.join(runtimeClassGenAbs, "playwright-types.ts"),
|
|
4658
|
-
content: readText(playwrightTypesAbs, "playwright-types.ts")
|
|
4659
|
-
},
|
|
4660
|
-
{
|
|
4661
|
-
filePath: path.join(runtimeClassGenAbs, "BasePage.ts"),
|
|
4662
|
-
content: readText(basePageClassPath, "BasePage.ts")
|
|
4663
|
-
}
|
|
4664
|
-
];
|
|
5363
|
+
const indexContent = renderSourceFile("index.ts", (sourceFile) => {
|
|
5364
|
+
addExportAll(sourceFile, "./page-object-models.g");
|
|
5365
|
+
}, {
|
|
5366
|
+
prefixText: buildFilePrefix({
|
|
5367
|
+
eslintDisableSortImports: true,
|
|
5368
|
+
commentLines: [
|
|
5369
|
+
"POM exports",
|
|
5370
|
+
"DO NOT MODIFY BY HAND",
|
|
5371
|
+
"",
|
|
5372
|
+
"This file is auto-generated by vue-pom-generator.",
|
|
5373
|
+
"Changes should be made in the generator/template, not in the generated output."
|
|
5374
|
+
]
|
|
5375
|
+
})
|
|
5376
|
+
});
|
|
5377
|
+
const runtimeFiles = buildRuntimeGeneratedFiles(base, basePageClassPath);
|
|
4665
5378
|
return [
|
|
4666
5379
|
{ filePath: outputFile, content },
|
|
4667
5380
|
{ filePath: indexFile, content: indexContent },
|
|
@@ -4751,48 +5464,50 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
|
|
|
4751
5464
|
return out;
|
|
4752
5465
|
}
|
|
4753
5466
|
function getComponentInstances(childrenComponent, componentHierarchyMap, attachmentsForThisView = [], widgetInstances = []) {
|
|
4754
|
-
|
|
5467
|
+
const declarations = [];
|
|
4755
5468
|
for (const a of attachmentsForThisView) {
|
|
4756
|
-
|
|
4757
|
-
|
|
5469
|
+
declarations.push(createClassProperty({
|
|
5470
|
+
name: a.propertyName,
|
|
5471
|
+
type: a.className
|
|
5472
|
+
}));
|
|
4758
5473
|
}
|
|
4759
5474
|
for (const w of widgetInstances) {
|
|
4760
|
-
|
|
4761
|
-
|
|
5475
|
+
declarations.push(createClassProperty({
|
|
5476
|
+
name: w.propertyName,
|
|
5477
|
+
type: w.className
|
|
5478
|
+
}));
|
|
4762
5479
|
}
|
|
4763
5480
|
childrenComponent.forEach((child) => {
|
|
4764
5481
|
if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
|
|
4765
5482
|
const childName = child.split(".vue")[0];
|
|
4766
|
-
|
|
4767
|
-
|
|
5483
|
+
declarations.push(createClassProperty({
|
|
5484
|
+
name: childName,
|
|
5485
|
+
type: childName
|
|
5486
|
+
}));
|
|
4768
5487
|
}
|
|
4769
5488
|
});
|
|
4770
|
-
return
|
|
4771
|
-
`;
|
|
5489
|
+
return declarations;
|
|
4772
5490
|
}
|
|
4773
5491
|
function getConstructor(childrenComponent, componentHierarchyMap, attachmentsForThisView = [], widgetInstances = [], options) {
|
|
4774
|
-
let content = " constructor(page: PwPage) {\n";
|
|
4775
5492
|
const attr = (options?.testIdAttribute ?? "data-testid").trim() || "data-testid";
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
5493
|
+
return createClassConstructor({
|
|
5494
|
+
parameters: [{ name: "page", type: "PwPage" }],
|
|
5495
|
+
statements: (writer) => {
|
|
5496
|
+
writer.writeLine(`super(page, { testIdAttribute: ${JSON.stringify(attr)} });`);
|
|
5497
|
+
for (const a of attachmentsForThisView) {
|
|
5498
|
+
writer.writeLine(`this.${a.propertyName} = new ${a.className}(page, this);`);
|
|
5499
|
+
}
|
|
5500
|
+
for (const w of widgetInstances) {
|
|
5501
|
+
writer.writeLine(`this.${w.propertyName} = new ${w.className}(page, ${JSON.stringify(w.testId)});`);
|
|
5502
|
+
}
|
|
5503
|
+
childrenComponent.forEach((child) => {
|
|
5504
|
+
if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
|
|
5505
|
+
const childName = child.split(".vue")[0];
|
|
5506
|
+
writer.writeLine(`this.${childName} = new ${childName}(page);`);
|
|
5507
|
+
}
|
|
5508
|
+
});
|
|
4791
5509
|
}
|
|
4792
5510
|
});
|
|
4793
|
-
content += " }";
|
|
4794
|
-
return `${content}
|
|
4795
|
-
`;
|
|
4796
5511
|
}
|
|
4797
5512
|
const TESTID_CLICK_EVENT_NAME = "__testid_event__";
|
|
4798
5513
|
const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
|
|
@@ -5437,6 +6152,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
5437
6152
|
const existingIdBehavior = options.existingIdBehavior ?? "preserve";
|
|
5438
6153
|
const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
|
|
5439
6154
|
const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
|
|
6155
|
+
const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "ignore";
|
|
5440
6156
|
const warn = options.warn;
|
|
5441
6157
|
const vueFilesPathMap = options.vueFilesPathMap;
|
|
5442
6158
|
const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
|
|
@@ -5702,6 +6418,22 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
|
|
|
5702
6418
|
});
|
|
5703
6419
|
};
|
|
5704
6420
|
const { nativeWrappersValue, optionDataTestIdPrefixValue, semanticNameHint } = getNativeWrapperTransformInfo(element, componentName, nativeWrappers);
|
|
6421
|
+
const handlerDirective = element.props.find((p) => {
|
|
6422
|
+
return p.type === compilerCore.NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === compilerCore.NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
|
|
6423
|
+
}) ?? null;
|
|
6424
|
+
const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
|
|
6425
|
+
if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
|
|
6426
|
+
const loc = element.loc?.start;
|
|
6427
|
+
const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
|
|
6428
|
+
const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
|
|
6429
|
+
throw new Error(
|
|
6430
|
+
`[vue-pom-generator] Could not derive a semantic POM action name for button-like wrapper in ${componentName} (${context.filename ?? "unknown"}:${locationHint}).
|
|
6431
|
+
Element: <${element.tag}>
|
|
6432
|
+
Handler: ${handlerSource}
|
|
6433
|
+
|
|
6434
|
+
Fix: move complex inline logic into a named function (for example, const onAction = () => ...; then bind :handler="onAction"), or simplify the handler to a direct identifier/call the generator can name. You can also set errorBehavior = "ignore" to keep generic fallback behavior.`
|
|
6435
|
+
);
|
|
6436
|
+
}
|
|
5705
6437
|
if (nativeWrappersValue) {
|
|
5706
6438
|
if (optionDataTestIdPrefixValue) {
|
|
5707
6439
|
const existing = existingIdBehavior === "preserve" ? tryGetExistingElementDataTestId(element, testIdAttribute) : null;
|
|
@@ -5811,7 +6543,6 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
|
|
|
5811
6543
|
});
|
|
5812
6544
|
return;
|
|
5813
6545
|
}
|
|
5814
|
-
const handlerInfo = nodeHandlerAttributeInfo(element);
|
|
5815
6546
|
if (handlerInfo) {
|
|
5816
6547
|
const testId = getHandlerAttributeValueDataTestId(handlerInfo.semanticNameHint);
|
|
5817
6548
|
applyResolvedDataTestIdForElement({
|
|
@@ -5911,6 +6642,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
5911
6642
|
normalizedBasePagePath,
|
|
5912
6643
|
outDir,
|
|
5913
6644
|
emitLanguages,
|
|
6645
|
+
typescriptOutputStructure,
|
|
5914
6646
|
csharp,
|
|
5915
6647
|
generateFixtures,
|
|
5916
6648
|
customPomAttachments,
|
|
@@ -5920,6 +6652,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
5920
6652
|
customPomImportNameCollisionBehavior,
|
|
5921
6653
|
testIdAttribute,
|
|
5922
6654
|
nameCollisionBehavior,
|
|
6655
|
+
missingSemanticNameBehavior,
|
|
5923
6656
|
existingIdBehavior,
|
|
5924
6657
|
nativeWrappers,
|
|
5925
6658
|
excludedComponents,
|
|
@@ -6030,6 +6763,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
6030
6763
|
existingIdBehavior: existingIdBehavior ?? "preserve",
|
|
6031
6764
|
testIdAttribute,
|
|
6032
6765
|
nameCollisionBehavior,
|
|
6766
|
+
missingSemanticNameBehavior,
|
|
6033
6767
|
warn: (message) => loggerRef.current.warn(message),
|
|
6034
6768
|
vueFilesPathMap,
|
|
6035
6769
|
wrapperSearchRoots: getWrapperSearchRoots()
|
|
@@ -6113,6 +6847,7 @@ function createBuildProcessorPlugin(options) {
|
|
|
6113
6847
|
await generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
|
|
6114
6848
|
outDir,
|
|
6115
6849
|
emitLanguages,
|
|
6850
|
+
typescriptOutputStructure,
|
|
6116
6851
|
csharp,
|
|
6117
6852
|
generateFixtures,
|
|
6118
6853
|
customPomAttachments,
|
|
@@ -6147,6 +6882,7 @@ function createDevProcessorPlugin(options) {
|
|
|
6147
6882
|
basePageClassPath,
|
|
6148
6883
|
outDir,
|
|
6149
6884
|
emitLanguages,
|
|
6885
|
+
typescriptOutputStructure,
|
|
6150
6886
|
csharp,
|
|
6151
6887
|
generateFixtures,
|
|
6152
6888
|
customPomAttachments,
|
|
@@ -6154,6 +6890,7 @@ function createDevProcessorPlugin(options) {
|
|
|
6154
6890
|
customPomImportAliases,
|
|
6155
6891
|
customPomImportNameCollisionBehavior,
|
|
6156
6892
|
nameCollisionBehavior = "suffix",
|
|
6893
|
+
missingSemanticNameBehavior,
|
|
6157
6894
|
existingIdBehavior,
|
|
6158
6895
|
testIdAttribute,
|
|
6159
6896
|
routerAwarePoms,
|
|
@@ -6318,6 +7055,7 @@ function createDevProcessorPlugin(options) {
|
|
|
6318
7055
|
{
|
|
6319
7056
|
existingIdBehavior: existingIdBehavior ?? "preserve",
|
|
6320
7057
|
nameCollisionBehavior,
|
|
7058
|
+
missingSemanticNameBehavior,
|
|
6321
7059
|
testIdAttribute,
|
|
6322
7060
|
warn: (message) => loggerRef.current.warn(message),
|
|
6323
7061
|
vueFilesPathMap: targetVuePathMap,
|
|
@@ -6358,6 +7096,7 @@ function createDevProcessorPlugin(options) {
|
|
|
6358
7096
|
generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
|
|
6359
7097
|
outDir,
|
|
6360
7098
|
emitLanguages,
|
|
7099
|
+
typescriptOutputStructure,
|
|
6361
7100
|
csharp,
|
|
6362
7101
|
generateFixtures,
|
|
6363
7102
|
customPomAttachments,
|
|
@@ -6531,14 +7270,37 @@ function createDevProcessorPlugin(options) {
|
|
|
6531
7270
|
};
|
|
6532
7271
|
}
|
|
6533
7272
|
function generateTestIdsModule(componentTestIds) {
|
|
6534
|
-
const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0]))
|
|
6535
|
-
return
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
7273
|
+
const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
7274
|
+
return renderSourceFile("virtual-testids.ts", (sourceFile) => {
|
|
7275
|
+
sourceFile.addStatements("// Virtual module: test id manifest");
|
|
7276
|
+
sourceFile.addVariableStatement({
|
|
7277
|
+
declarationKind: tsMorph.VariableDeclarationKind.Const,
|
|
7278
|
+
isExported: true,
|
|
7279
|
+
declarations: [{
|
|
7280
|
+
name: "testIdManifest",
|
|
7281
|
+
initializer: (writer) => {
|
|
7282
|
+
writer.write("{").newLine();
|
|
7283
|
+
writer.indent(() => {
|
|
7284
|
+
manifestEntries.forEach(([componentName, testIds], index) => {
|
|
7285
|
+
const suffix = index === manifestEntries.length - 1 ? "" : ",";
|
|
7286
|
+
writer.writeLine(`${JSON.stringify(componentName)}: ${JSON.stringify(Array.from(testIds).sort())}${suffix}`);
|
|
7287
|
+
});
|
|
7288
|
+
});
|
|
7289
|
+
writer.write("} as const");
|
|
7290
|
+
}
|
|
7291
|
+
}]
|
|
7292
|
+
});
|
|
7293
|
+
sourceFile.addTypeAlias({
|
|
7294
|
+
isExported: true,
|
|
7295
|
+
name: "TestIdManifest",
|
|
7296
|
+
type: "typeof testIdManifest"
|
|
7297
|
+
});
|
|
7298
|
+
sourceFile.addTypeAlias({
|
|
7299
|
+
isExported: true,
|
|
7300
|
+
name: "ComponentName",
|
|
7301
|
+
type: "keyof TestIdManifest"
|
|
7302
|
+
});
|
|
7303
|
+
});
|
|
6542
7304
|
}
|
|
6543
7305
|
function createTestIdsVirtualModulesPlugin(componentTestIds) {
|
|
6544
7306
|
const maybeModule = virtualImport;
|
|
@@ -6558,9 +7320,11 @@ function createSupportPlugins(options) {
|
|
|
6558
7320
|
scanDirs,
|
|
6559
7321
|
getWrapperSearchRoots,
|
|
6560
7322
|
nameCollisionBehavior = "suffix",
|
|
7323
|
+
missingSemanticNameBehavior,
|
|
6561
7324
|
existingIdBehavior,
|
|
6562
7325
|
outDir,
|
|
6563
7326
|
emitLanguages,
|
|
7327
|
+
typescriptOutputStructure,
|
|
6564
7328
|
csharp,
|
|
6565
7329
|
routerAwarePoms,
|
|
6566
7330
|
routerEntry,
|
|
@@ -6604,6 +7368,7 @@ function createSupportPlugins(options) {
|
|
|
6604
7368
|
normalizedBasePagePath,
|
|
6605
7369
|
outDir,
|
|
6606
7370
|
emitLanguages,
|
|
7371
|
+
typescriptOutputStructure,
|
|
6607
7372
|
csharp,
|
|
6608
7373
|
generateFixtures,
|
|
6609
7374
|
customPomAttachments,
|
|
@@ -6613,6 +7378,7 @@ function createSupportPlugins(options) {
|
|
|
6613
7378
|
customPomImportNameCollisionBehavior,
|
|
6614
7379
|
testIdAttribute,
|
|
6615
7380
|
nameCollisionBehavior,
|
|
7381
|
+
missingSemanticNameBehavior,
|
|
6616
7382
|
existingIdBehavior,
|
|
6617
7383
|
nativeWrappers,
|
|
6618
7384
|
excludedComponents,
|
|
@@ -6634,6 +7400,7 @@ function createSupportPlugins(options) {
|
|
|
6634
7400
|
basePageClassPath,
|
|
6635
7401
|
outDir,
|
|
6636
7402
|
emitLanguages,
|
|
7403
|
+
typescriptOutputStructure,
|
|
6637
7404
|
csharp,
|
|
6638
7405
|
generateFixtures,
|
|
6639
7406
|
customPomAttachments,
|
|
@@ -6641,6 +7408,7 @@ function createSupportPlugins(options) {
|
|
|
6641
7408
|
customPomImportAliases,
|
|
6642
7409
|
customPomImportNameCollisionBehavior,
|
|
6643
7410
|
nameCollisionBehavior,
|
|
7411
|
+
missingSemanticNameBehavior,
|
|
6644
7412
|
existingIdBehavior,
|
|
6645
7413
|
testIdAttribute,
|
|
6646
7414
|
routerAwarePoms,
|
|
@@ -7015,6 +7783,41 @@ function assertNonEmptyStringArray(value, name) {
|
|
|
7015
7783
|
assertNonEmptyString(entry, `${name}[${index}]`);
|
|
7016
7784
|
}
|
|
7017
7785
|
}
|
|
7786
|
+
function assertOneOf(value, allowed, name) {
|
|
7787
|
+
if (!value)
|
|
7788
|
+
return;
|
|
7789
|
+
if (allowed.includes(value)) {
|
|
7790
|
+
return;
|
|
7791
|
+
}
|
|
7792
|
+
throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
|
|
7793
|
+
}
|
|
7794
|
+
function assertErrorBehavior(value, name) {
|
|
7795
|
+
if (!value) {
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
if (value === "ignore" || value === "error") {
|
|
7799
|
+
return;
|
|
7800
|
+
}
|
|
7801
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
7802
|
+
throw new TypeError(`${name} must be "ignore", "error", or an object.`);
|
|
7803
|
+
}
|
|
7804
|
+
const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
|
|
7805
|
+
for (const key of Object.keys(value)) {
|
|
7806
|
+
if (!supportedKeys.has(key)) {
|
|
7807
|
+
throw new TypeError(`${name} contains unsupported key "${key}".`);
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
|
|
7811
|
+
}
|
|
7812
|
+
function resolveMissingSemanticNameBehavior(value) {
|
|
7813
|
+
if (!value) {
|
|
7814
|
+
return "ignore";
|
|
7815
|
+
}
|
|
7816
|
+
if (value === "ignore" || value === "error") {
|
|
7817
|
+
return value;
|
|
7818
|
+
}
|
|
7819
|
+
return value.missingSemanticNameBehavior ?? "ignore";
|
|
7820
|
+
}
|
|
7018
7821
|
function assertRouterModuleShims(value, name) {
|
|
7019
7822
|
if (!value)
|
|
7020
7823
|
return;
|
|
@@ -7147,6 +7950,9 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
7147
7950
|
const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
|
|
7148
7951
|
const usesExternalVuePlugin = vuePluginOwnership === "external";
|
|
7149
7952
|
const csharp = generationOptions?.csharp;
|
|
7953
|
+
const errorBehavior = options.errorBehavior;
|
|
7954
|
+
const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
|
|
7955
|
+
const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
|
|
7150
7956
|
const generateFixtures = generationOptions?.playwright?.fixtures;
|
|
7151
7957
|
const customPoms = generationOptions?.playwright?.customPoms;
|
|
7152
7958
|
const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
|
|
@@ -7178,8 +7984,10 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
7178
7984
|
assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
|
|
7179
7985
|
assertNonEmptyString(viewsDir, "[vue-pom-generator] injection.viewsDir");
|
|
7180
7986
|
assertNonEmptyStringArray(wrapperSearchRoots, "[vue-pom-generator] injection.wrapperSearchRoots");
|
|
7987
|
+
assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
|
|
7181
7988
|
if (generationEnabled) {
|
|
7182
7989
|
assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
|
|
7990
|
+
assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
|
|
7183
7991
|
assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
|
|
7184
7992
|
if (generationOptions?.router && routerType === "vue-router") {
|
|
7185
7993
|
assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
|
|
@@ -7189,7 +7997,7 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
7189
7997
|
applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompilerOptions, isNuxt ? "nuxt" : vuePluginOwnership);
|
|
7190
7998
|
}
|
|
7191
7999
|
loggerRef.current.info(`projectRoot=${projectRootRef.current}`);
|
|
7192
|
-
loggerRef.current.info(`Active plugins: ${config.plugins.map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
|
|
8000
|
+
loggerRef.current.info(`Active plugins: ${(config.plugins ?? []).map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
|
|
7193
8001
|
}
|
|
7194
8002
|
};
|
|
7195
8003
|
const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, viewsDir);
|
|
@@ -7223,9 +8031,11 @@ function createVuePomGeneratorPlugins(options = {}) {
|
|
|
7223
8031
|
scanDirs,
|
|
7224
8032
|
getWrapperSearchRoots: getWrapperSearchRootsAbs,
|
|
7225
8033
|
nameCollisionBehavior,
|
|
8034
|
+
missingSemanticNameBehavior,
|
|
7226
8035
|
existingIdBehavior,
|
|
7227
8036
|
outDir,
|
|
7228
8037
|
emitLanguages,
|
|
8038
|
+
typescriptOutputStructure,
|
|
7229
8039
|
csharp,
|
|
7230
8040
|
routerAwarePoms,
|
|
7231
8041
|
routerEntry,
|