@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.
Files changed (32) hide show
  1. package/README.md +45 -11
  2. package/RELEASE_NOTES.md +38 -27
  3. package/class-generation/index.ts +1421 -682
  4. package/dist/class-generation/index.d.ts +8 -0
  5. package/dist/class-generation/index.d.ts.map +1 -1
  6. package/dist/index.cjs +1501 -691
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +1504 -694
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/manifest-generator.d.ts.map +1 -1
  11. package/dist/method-generation.d.ts +2 -0
  12. package/dist/method-generation.d.ts.map +1 -1
  13. package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
  14. package/dist/plugin/support/build-plugin.d.ts +3 -1
  15. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  16. package/dist/plugin/support/dev-plugin.d.ts +3 -1
  17. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  18. package/dist/plugin/support-plugins.d.ts +3 -1
  19. package/dist/plugin/support-plugins.d.ts.map +1 -1
  20. package/dist/plugin/types.d.ts +32 -7
  21. package/dist/plugin/types.d.ts.map +1 -1
  22. package/dist/tests/fixtures/generated-tsc/BasePage.full.d.ts +19 -0
  23. package/dist/tests/fixtures/generated-tsc/BasePage.full.d.ts.map +1 -0
  24. package/dist/tests/fixtures/generated-tsc/BasePage.minimal.d.ts +8 -0
  25. package/dist/tests/fixtures/generated-tsc/BasePage.minimal.d.ts.map +1 -0
  26. package/dist/tests/fixtures/generated-tsc/Pointer.d.ts +6 -0
  27. package/dist/tests/fixtures/generated-tsc/Pointer.d.ts.map +1 -0
  28. package/dist/transform.d.ts +1 -0
  29. package/dist/transform.d.ts.map +1 -1
  30. package/dist/typescript-codegen.d.ts +34 -0
  31. package/dist/typescript-codegen.d.ts.map +1 -0
  32. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -3,11 +3,12 @@ import process from "node:process";
3
3
  import { pathToFileURL, fileURLToPath } from "node:url";
4
4
  import fs from "node:fs";
5
5
  import * as compilerDom from "@vue/compiler-dom";
6
- import { parse as parse$2 } from "@vue/compiler-dom";
6
+ import { parse as parse$2, NodeTypes as NodeTypes$1 } from "@vue/compiler-dom";
7
7
  import { parse as parse$1, compileScript } from "@vue/compiler-sfc";
8
8
  import { parseExpression, parse } from "@babel/parser";
9
+ import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression, ElementTypes } from "@vue/compiler-core";
10
+ import { Project, QuoteKind, NewLineKind, IndentationText, StructureKind, CodeBlockWriter, VariableDeclarationKind } from "ts-morph";
9
11
  import { JSDOM } from "jsdom";
10
- import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression } from "@vue/compiler-core";
11
12
  import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isArrowFunctionExpression, isOptionalMemberExpression, isObjectExpression, isFile, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
12
13
  import { performance } from "node:perf_hooks";
13
14
  import virtualImport from "vite-plugin-virtual";
@@ -62,9 +63,97 @@ function createLogger(options) {
62
63
  }
63
64
  };
64
65
  }
65
- const INDENT = " ";
66
- const INDENT2 = `${INDENT}${INDENT}`;
67
- const INDENT3 = `${INDENT2}${INDENT}`;
66
+ function createTypeScriptProject() {
67
+ return new Project({
68
+ useInMemoryFileSystem: true,
69
+ manipulationSettings: {
70
+ indentationText: IndentationText.FourSpaces,
71
+ newLineKind: NewLineKind.LineFeed,
72
+ quoteKind: QuoteKind.Double,
73
+ useTrailingCommas: false
74
+ }
75
+ });
76
+ }
77
+ function ensureTrailingNewline(text) {
78
+ return text.endsWith("\n") ? text : `${text}
79
+ `;
80
+ }
81
+ function createTypeScriptWriter() {
82
+ return new CodeBlockWriter({
83
+ newLine: "\n",
84
+ useTabs: false,
85
+ indentNumberOfSpaces: 4
86
+ });
87
+ }
88
+ function renderTypeScript(write) {
89
+ const writer = createTypeScriptWriter();
90
+ write(writer);
91
+ return ensureTrailingNewline(writer.toString());
92
+ }
93
+ function buildCommentBlock(lines) {
94
+ return renderTypeScript((writer) => {
95
+ writer.writeLine("/**");
96
+ for (const line of lines) {
97
+ writer.writeLine(` * ${line}`);
98
+ }
99
+ writer.writeLine(" */");
100
+ });
101
+ }
102
+ function buildFilePrefix(options = {}) {
103
+ let prefix = "";
104
+ if (options.referenceLib) {
105
+ prefix += `/// <reference lib="${options.referenceLib}" />
106
+ `;
107
+ }
108
+ if (options.eslintDisableSortImports) {
109
+ prefix += "/* eslint-disable perfectionist/sort-imports */\n";
110
+ }
111
+ if (options.commentLines?.length) {
112
+ prefix += buildCommentBlock(options.commentLines);
113
+ }
114
+ return prefix;
115
+ }
116
+ function renderSourceFile(filePath, build, options = {}) {
117
+ const project = createTypeScriptProject();
118
+ const sourceFile = project.createSourceFile(filePath, "", { overwrite: true });
119
+ build(sourceFile);
120
+ const content = ensureTrailingNewline(sourceFile.getFullText());
121
+ return options.prefixText ? ensureTrailingNewline(`${options.prefixText}${content}`) : content;
122
+ }
123
+ function addNamedImport(sourceFile, options) {
124
+ return sourceFile.addImportDeclaration({
125
+ moduleSpecifier: options.moduleSpecifier,
126
+ isTypeOnly: options.isTypeOnly,
127
+ namedImports: options.namedImports
128
+ });
129
+ }
130
+ function addExportAll(sourceFile, moduleSpecifier) {
131
+ return sourceFile.addExportDeclaration({ moduleSpecifier });
132
+ }
133
+ function createClassMethod(method) {
134
+ return {
135
+ kind: StructureKind.Method,
136
+ ...method
137
+ };
138
+ }
139
+ function createClassProperty(property) {
140
+ return {
141
+ kind: StructureKind.Property,
142
+ ...property
143
+ };
144
+ }
145
+ function createClassGetter(getter) {
146
+ return {
147
+ kind: StructureKind.GetAccessor,
148
+ ...getter
149
+ };
150
+ }
151
+ function createClassConstructor(constructorDeclaration) {
152
+ return {
153
+ kind: StructureKind.Constructor,
154
+ ...constructorDeclaration
155
+ };
156
+ }
68
157
  function upperFirst$1(value) {
69
158
  if (!value) {
70
159
  return value;
@@ -74,12 +163,34 @@ function upperFirst$1(value) {
74
163
  function hasParam(params, name) {
75
164
  return Object.prototype.hasOwnProperty.call(params, name);
76
165
  }
77
- function formatParams(params) {
78
- const entries = Object.entries(params);
79
- if (!entries.length) {
80
- return "";
166
+ function splitTypeAndInitializer(typeExpression) {
167
+ const trimmed = typeExpression.trim();
168
+ const initializerIndex = trimmed.lastIndexOf("=");
169
+ if (initializerIndex < 0) {
170
+ return { type: trimmed };
81
171
  }
82
- return entries.map(([n, t]) => `${n}: ${t}`).join(", ");
172
+ return {
173
+ type: trimmed.slice(0, initializerIndex).trim(),
174
+ initializer: trimmed.slice(initializerIndex + 1).trim()
175
+ };
176
+ }
177
+ function createParameter(name, typeExpression) {
178
+ const { type, initializer } = splitTypeAndInitializer(typeExpression);
179
+ return {
180
+ name,
181
+ type: type || void 0,
182
+ initializer
183
+ };
184
+ }
185
+ function createParameters(params) {
186
+ return Object.entries(params).map(([name, typeExpression]) => createParameter(name, typeExpression));
187
+ }
188
+ function createInlineParameter(name, options = {}) {
189
+ return {
190
+ name,
191
+ type: options.type,
192
+ initializer: options.initializer
193
+ };
83
194
  }
84
195
  function removeByKeySegment(value) {
85
196
  const idx = value.lastIndexOf("ByKey");
@@ -107,113 +218,135 @@ function uniqueAlternates(primary, alternates) {
107
218
  function testIdExpression(formattedDataTestId) {
108
219
  return formattedDataTestId.includes("${") ? `\`${formattedDataTestId}\`` : JSON.stringify(formattedDataTestId);
109
220
  }
221
+ function createAsyncMethod(name, parameters, statements) {
222
+ return createClassMethod({
223
+ name,
224
+ isAsync: true,
225
+ parameters,
226
+ statements
227
+ });
228
+ }
110
229
  function generateClickMethod(methodName, formattedDataTestId, alternateFormattedDataTestIds, params) {
111
- let content;
112
230
  const name = `click${methodName}`;
113
231
  const noWaitName = `${name}NoWait`;
114
- const paramBlock = formatParams(params);
115
- const paramBlockWithWait = paramBlock ? `${paramBlock}, wait: boolean = true` : "wait: boolean = true";
232
+ const baseParameters = createParameters(params);
116
233
  const argsForForward = Object.keys(params).join(", ");
117
234
  const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
118
235
  if (alternates.length > 0) {
119
236
  const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
120
- const waitSignature = hasParam(params, "key") ? paramBlockWithWait : "wait: boolean = true";
121
- const waitArg = "wait";
122
- content = `${INDENT}async ${name}(${waitSignature}) {
123
- ${INDENT2}const candidates = [${candidatesExpr}] as const;
124
- ${INDENT2}let lastError: unknown;
125
- ${INDENT2}for (const testId of candidates) {
126
- ${INDENT3}const locator = this.locatorByTestId(testId);
127
- ${INDENT3}try {
128
- ${INDENT3}${INDENT}if (await locator.count()) {
129
- ${INDENT3}${INDENT2}await this.clickLocator(locator, "", ${waitArg});
130
- ${INDENT3}${INDENT2}return;
131
- ${INDENT3}${INDENT}}
132
- ${INDENT3}} catch (e) {
133
- ${INDENT3}${INDENT}lastError = e;
134
- ${INDENT3}}
135
- ${INDENT2}}
136
- ${INDENT2}throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to click any candidate locator for ${name}.");
137
- ${INDENT}}
138
- `;
139
- const noWaitSig = hasParam(params, "key") ? paramBlock : "";
237
+ const clickMethod = createAsyncMethod(
238
+ name,
239
+ hasParam(params, "key") ? [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })] : [createInlineParameter("wait", { type: "boolean", initializer: "true" })],
240
+ (writer) => {
241
+ writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
242
+ writer.writeLine("let lastError: unknown;");
243
+ writer.write("for (const testId of candidates) ").block(() => {
244
+ writer.writeLine("const locator = this.locatorByTestId(testId);");
245
+ writer.write("try ").block(() => {
246
+ writer.write("if (await locator.count()) ").block(() => {
247
+ writer.writeLine('await this.clickLocator(locator, "", wait);');
248
+ writer.writeLine("return;");
249
+ });
250
+ });
251
+ writer.write("catch (e) ").block(() => {
252
+ writer.writeLine("lastError = e;");
253
+ });
254
+ });
255
+ writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to click any candidate locator for ${name}.");`);
256
+ }
257
+ );
140
258
  const noWaitArgs = argsForForward ? `${argsForForward}, false` : "false";
141
- content += `
142
- ${INDENT}async ${noWaitName}(${noWaitSig}) {
143
- ${INDENT2}await this.${name}(${noWaitArgs});
144
- ${INDENT}}
145
- `;
146
- return content;
259
+ const noWaitMethod = createAsyncMethod(
260
+ noWaitName,
261
+ hasParam(params, "key") ? baseParameters : [],
262
+ (writer) => {
263
+ writer.writeLine(`await this.${name}(${noWaitArgs});`);
264
+ }
265
+ );
266
+ return [clickMethod, noWaitMethod];
147
267
  }
148
268
  if (hasParam(params, "key")) {
149
- content = `${INDENT}async ${name}(${paramBlockWithWait}) {
150
- ${INDENT2}await this.clickByTestId(\`${formattedDataTestId}\`, "", wait);
151
- ${INDENT}}
152
- `;
153
- content += `
154
- ${INDENT}async ${noWaitName}(${paramBlock}) {
155
- ${INDENT2}await this.${name}(${argsForForward}, false);
156
- ${INDENT}}
157
- `;
158
- } else {
159
- content = `${INDENT}async ${name}(wait: boolean = true) {
160
- ${INDENT2}await this.clickByTestId("${formattedDataTestId}", "", wait);
161
- ${INDENT}}
162
- `;
163
- content += `
164
- ${INDENT}async ${noWaitName}() {
165
- ${INDENT2}await this.${name}(false);
166
- ${INDENT}}
167
- `;
269
+ return [
270
+ createAsyncMethod(name, [...baseParameters, createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
271
+ writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`, "", wait);`);
272
+ }),
273
+ createAsyncMethod(noWaitName, baseParameters, (writer) => {
274
+ writer.writeLine(`await this.${name}(${argsForForward}, false);`);
275
+ })
276
+ ];
168
277
  }
169
- return content;
278
+ return [
279
+ createAsyncMethod(name, [createInlineParameter("wait", { type: "boolean", initializer: "true" })], (writer) => {
280
+ writer.writeLine(`await this.clickByTestId("${formattedDataTestId}", "", wait);`);
281
+ }),
282
+ createAsyncMethod(noWaitName, [], (writer) => {
283
+ writer.writeLine(`await this.${name}(false);`);
284
+ })
285
+ ];
170
286
  }
171
287
  function generateRadioMethod(methodName, formattedDataTestId) {
172
288
  const name = `select${methodName}`;
173
289
  const hasKey = formattedDataTestId.includes("${key}");
174
- if (hasKey) {
175
- return `${INDENT}async ${name}(key: string, annotationText: string = "") {
176
- ${INDENT2}await this.clickByTestId(\`${formattedDataTestId}\`, annotationText);
177
- ${INDENT}}
178
- `;
179
- }
180
- return `${INDENT}async ${name}(annotationText: string = "") {
181
- ${INDENT2}await this.clickByTestId("${formattedDataTestId}", annotationText);
182
- ${INDENT}}
183
- `;
290
+ const parameters = hasKey ? [
291
+ createInlineParameter("key", { type: "string" }),
292
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
293
+ ] : [createInlineParameter("annotationText", { type: "string", initializer: '""' })];
294
+ const testIdExpr = hasKey ? `\`${formattedDataTestId}\`` : `"${formattedDataTestId}"`;
295
+ return [
296
+ createAsyncMethod(name, parameters, (writer) => {
297
+ writer.writeLine(`await this.clickByTestId(${testIdExpr}, annotationText);`);
298
+ })
299
+ ];
184
300
  }
185
301
  function generateSelectMethod(methodName, formattedDataTestId) {
186
302
  const name = `select${methodName}`;
187
303
  const needsKey = formattedDataTestId.includes("${key}");
188
304
  const selectorExpr = needsKey ? `this.selectorForTestId(\`${formattedDataTestId}\`)` : `this.selectorForTestId("${formattedDataTestId}")`;
189
- const content = `${INDENT}async ${name}(value: string, annotationText: string = "") {
190
- ${INDENT2}const selector = ${selectorExpr};
191
- ${INDENT2}await this.animateCursorToElement(selector, false, 500, annotationText);
192
- ${INDENT2}await this.page.selectOption(selector, value);
193
- ${INDENT}}
194
-
195
- `;
196
- return content;
305
+ return [
306
+ createAsyncMethod(
307
+ name,
308
+ [
309
+ createInlineParameter("value", { type: "string" }),
310
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
311
+ ],
312
+ (writer) => {
313
+ writer.writeLine(`const selector = ${selectorExpr};`);
314
+ writer.writeLine("await this.animateCursorToElement(selector, false, 500, annotationText);");
315
+ writer.writeLine("await this.page.selectOption(selector, value);");
316
+ }
317
+ )
318
+ ];
197
319
  }
198
320
  function generateVSelectMethod(methodName, formattedDataTestId) {
199
321
  const name = `select${methodName}`;
200
- const content = [
201
- `${INDENT}async ${name}(value: string, timeOut = 500, annotationText: string = "") {
202
- `,
203
- `${INDENT2}await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);
204
- `,
205
- `${INDENT}}
206
- `
207
- ].join("");
208
- return content;
322
+ return [
323
+ createAsyncMethod(
324
+ name,
325
+ [
326
+ createInlineParameter("value", { type: "string" }),
327
+ createInlineParameter("timeOut", { type: "number", initializer: "500" }),
328
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
329
+ ],
330
+ (writer) => {
331
+ writer.writeLine(`await this.selectVSelectByTestId("${formattedDataTestId}", value, timeOut, annotationText);`);
332
+ }
333
+ )
334
+ ];
209
335
  }
210
336
  function generateTypeMethod(methodName, formattedDataTestId) {
211
337
  const name = `type${methodName}`;
212
- const content = `${INDENT}async ${name}(text: string, annotationText: string = "") {
213
- ${INDENT2}await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);
214
- ${INDENT}}
215
- `;
216
- return content;
338
+ return [
339
+ createAsyncMethod(
340
+ name,
341
+ [
342
+ createInlineParameter("text", { type: "string" }),
343
+ createInlineParameter("annotationText", { type: "string", initializer: '""' })
344
+ ],
345
+ (writer) => {
346
+ writer.writeLine(`await this.fillInputByTestId("${formattedDataTestId}", text, annotationText);`);
347
+ }
348
+ )
349
+ ];
217
350
  }
218
351
  function isAllDigits(value) {
219
352
  if (!value)
@@ -235,90 +368,119 @@ function generateGetElementByDataTestId(methodName, nativeRole, formattedDataTes
235
368
  if (needsKey) {
236
369
  const keyType = params.key || "string";
237
370
  const keyedPropertyName = getterNameOverride ?? removeByKeySegment(propertyName);
238
- return `${INDENT}get ${keyedPropertyName}() {
239
- ${INDENT2}return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));
240
- ${INDENT}}
241
-
242
- `;
371
+ return [
372
+ createClassGetter({
373
+ name: keyedPropertyName,
374
+ statements: [
375
+ `return this.keyedLocators((key: ${keyType}) => this.locatorByTestId(\`${formattedDataTestId}\`));`
376
+ ]
377
+ })
378
+ ];
243
379
  }
244
380
  const finalPropertyName = getterNameOverride ?? propertyName;
245
381
  const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
246
382
  if (alternates.length > 0) {
247
383
  const all = [formattedDataTestId, ...alternates];
248
384
  const locatorExpr = all.map((id) => `this.locatorByTestId(${testIdExpression(id)})`).reduce((acc, next) => `${acc}.or(${next})`);
249
- return `${INDENT}get ${finalPropertyName}() {
250
- ${INDENT2}return ${locatorExpr};
251
- ${INDENT}}
252
-
253
- `;
385
+ return [
386
+ createClassGetter({
387
+ name: finalPropertyName,
388
+ statements: [`return ${locatorExpr};`]
389
+ })
390
+ ];
254
391
  }
255
- return `${INDENT}get ${finalPropertyName}() {
256
- ${INDENT2}return this.locatorByTestId("${formattedDataTestId}");
257
- ${INDENT}}
258
-
259
- `;
392
+ return [
393
+ createClassGetter({
394
+ name: finalPropertyName,
395
+ statements: [`return this.locatorByTestId("${formattedDataTestId}");`]
396
+ })
397
+ ];
260
398
  }
261
399
  function generateNavigationMethod(args) {
262
400
  const { targetPageObjectModelClass: target, baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params } = args;
263
401
  const methodName = baseMethodName ? `goTo${upperFirst$1(baseMethodName)}` : `goTo${target.endsWith("Page") ? target.slice(0, -"Page".length) : target}`;
264
- const signature = `public ${methodName}(${formatParams(params)}): Fluent<${target}>`;
402
+ const parameters = createParameters(params);
265
403
  const alternates = uniqueAlternates(formattedDataTestId, alternateFormattedDataTestIds);
266
404
  const candidatesExpr = [formattedDataTestId, ...alternates].map(testIdExpression).join(", ");
267
405
  if (alternates.length > 0) {
268
- return `${INDENT}${signature} {
269
- ${INDENT2}return this.fluent(async () => {
270
- ${INDENT3}const candidates = [${candidatesExpr}] as const;
271
- ${INDENT3}let lastError: unknown;
272
- ${INDENT3}for (const testId of candidates) {
273
- ${INDENT3}${INDENT}const locator = this.locatorByTestId(testId);
274
- ${INDENT3}${INDENT}try {
275
- ${INDENT3}${INDENT2}if (await locator.count()) {
276
- ${INDENT3}${INDENT3}await this.clickLocator(locator);
277
- ${INDENT3}${INDENT3}return new ${target}(this.page);
278
- ${INDENT3}${INDENT2}}
279
- ${INDENT3}${INDENT}} catch (e) {
280
- ${INDENT3}${INDENT2}lastError = e;
281
- ${INDENT3}${INDENT}}
282
- ${INDENT3}}
283
- ${INDENT3}throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to navigate using any candidate locator for ${methodName}.");
284
- ${INDENT2}});
285
- ${INDENT}}
286
- `;
406
+ return [
407
+ createClassMethod({
408
+ name: methodName,
409
+ parameters,
410
+ returnType: `Fluent<${target}>`,
411
+ statements: (writer) => {
412
+ writer.write("return this.fluent(async () => ").block(() => {
413
+ writer.writeLine(`const candidates = [${candidatesExpr}] as const;`);
414
+ writer.writeLine("let lastError: unknown;");
415
+ writer.write("for (const testId of candidates) ").block(() => {
416
+ writer.writeLine("const locator = this.locatorByTestId(testId);");
417
+ writer.write("try ").block(() => {
418
+ writer.write("if (await locator.count()) ").block(() => {
419
+ writer.writeLine("await this.clickLocator(locator);");
420
+ writer.writeLine(`return new ${target}(this.page);`);
421
+ });
422
+ });
423
+ writer.write("catch (e) ").block(() => {
424
+ writer.writeLine("lastError = e;");
425
+ });
426
+ });
427
+ writer.writeLine(`throw (lastError instanceof Error) ? lastError : new Error("[pom] Failed to navigate using any candidate locator for ${methodName}.");`);
428
+ });
429
+ writer.writeLine(");");
430
+ }
431
+ })
432
+ ];
287
433
  }
288
- const clickExpr = `\`${formattedDataTestId}\``;
289
- return `${INDENT}${signature} {
290
- ${INDENT2}return this.fluent(async () => {
291
- ${INDENT3}await this.clickByTestId(${clickExpr});
292
- ${INDENT3}return new ${target}(this.page);
293
- ${INDENT2}});
294
- ${INDENT}}
295
- `;
434
+ return [
435
+ createClassMethod({
436
+ name: methodName,
437
+ parameters,
438
+ returnType: `Fluent<${target}>`,
439
+ statements: (writer) => {
440
+ writer.write("return this.fluent(async () => ").block(() => {
441
+ writer.writeLine(`await this.clickByTestId(\`${formattedDataTestId}\`);`);
442
+ writer.writeLine(`return new ${target}(this.page);`);
443
+ });
444
+ writer.writeLine(");");
445
+ }
446
+ })
447
+ ];
296
448
  }
297
- function generateViewObjectModelMethodContent(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
449
+ function generateViewObjectModelMembers(targetPageObjectModelClass, methodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params) {
298
450
  const baseMethodName = nativeRole === "radio" ? methodName || "Radio" : methodName;
299
- const getElementMethod = generateGetElementByDataTestId(baseMethodName, nativeRole, formattedDataTestId, alternateFormattedDataTestIds, getterNameOverride, params);
451
+ const members = generateGetElementByDataTestId(
452
+ baseMethodName,
453
+ nativeRole,
454
+ formattedDataTestId,
455
+ alternateFormattedDataTestIds,
456
+ getterNameOverride,
457
+ params
458
+ );
300
459
  if (targetPageObjectModelClass) {
301
- return getElementMethod + generateNavigationMethod({
302
- targetPageObjectModelClass,
303
- baseMethodName,
304
- formattedDataTestId,
305
- alternateFormattedDataTestIds,
306
- params
307
- });
460
+ return [
461
+ ...members,
462
+ ...generateNavigationMethod({
463
+ targetPageObjectModelClass,
464
+ baseMethodName,
465
+ formattedDataTestId,
466
+ alternateFormattedDataTestIds,
467
+ params
468
+ })
469
+ ];
308
470
  }
309
471
  if (nativeRole === "select") {
310
- return getElementMethod + generateSelectMethod(baseMethodName, formattedDataTestId);
472
+ return [...members, ...generateSelectMethod(baseMethodName, formattedDataTestId)];
311
473
  }
312
474
  if (nativeRole === "vselect") {
313
- return getElementMethod + generateVSelectMethod(baseMethodName, formattedDataTestId);
475
+ return [...members, ...generateVSelectMethod(baseMethodName, formattedDataTestId)];
314
476
  }
315
477
  if (nativeRole === "input") {
316
- return getElementMethod + generateTypeMethod(baseMethodName, formattedDataTestId);
478
+ return [...members, ...generateTypeMethod(baseMethodName, formattedDataTestId)];
317
479
  }
318
480
  if (nativeRole === "radio") {
319
- return getElementMethod + generateRadioMethod(baseMethodName || "Radio", formattedDataTestId);
481
+ return [...members, ...generateRadioMethod(baseMethodName || "Radio", formattedDataTestId)];
320
482
  }
321
- return getElementMethod + generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params);
483
+ return [...members, ...generateClickMethod(baseMethodName, formattedDataTestId, alternateFormattedDataTestIds, params)];
322
484
  }
323
485
  function isSimpleExpressionNode(value) {
324
486
  return value !== null && "type" in value && value.type === NodeTypes.SIMPLE_EXPRESSION;
@@ -3227,10 +3389,123 @@ async function parseRouterFileFromCwd(routerEntryPath, options = {}) {
3227
3389
  }
3228
3390
  });
3229
3391
  }
3230
- 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 */";
3231
3392
  const GENERATED_GITATTRIBUTES_BLOCK_START = "# BEGIN vue-pom-generator generated files";
3232
3393
  const GENERATED_GITATTRIBUTES_BLOCK_END = "# END vue-pom-generator generated files";
3233
- const eslintSuppressionHeader = "/* eslint-disable perfectionist/sort-imports */\n";
3394
+ const VUE_POM_GENERATOR_ERROR_PREFIX = "[vue-pom-generator]";
3395
+ class VuePomGeneratorError extends Error {
3396
+ constructor(message) {
3397
+ const normalized = message.startsWith(VUE_POM_GENERATOR_ERROR_PREFIX) ? message : `${VUE_POM_GENERATOR_ERROR_PREFIX} ${message}`;
3398
+ super(normalized);
3399
+ this.name = "VuePomGeneratorError";
3400
+ }
3401
+ }
3402
+ function splitParameterList(parameters) {
3403
+ const parts = [];
3404
+ let current = "";
3405
+ let braceDepth = 0;
3406
+ let bracketDepth = 0;
3407
+ let parenDepth = 0;
3408
+ let angleDepth = 0;
3409
+ let inSingleQuote = false;
3410
+ let inDoubleQuote = false;
3411
+ let inTemplateString = false;
3412
+ for (let index = 0; index < parameters.length; index += 1) {
3413
+ const char = parameters[index];
3414
+ const previous = index > 0 ? parameters[index - 1] : "";
3415
+ if (char === "'" && !inDoubleQuote && !inTemplateString && previous !== "\\") {
3416
+ inSingleQuote = !inSingleQuote;
3417
+ current += char;
3418
+ continue;
3419
+ }
3420
+ if (char === '"' && !inSingleQuote && !inTemplateString && previous !== "\\") {
3421
+ inDoubleQuote = !inDoubleQuote;
3422
+ current += char;
3423
+ continue;
3424
+ }
3425
+ if (char === "`" && !inSingleQuote && !inDoubleQuote && previous !== "\\") {
3426
+ inTemplateString = !inTemplateString;
3427
+ current += char;
3428
+ continue;
3429
+ }
3430
+ if (inSingleQuote || inDoubleQuote || inTemplateString) {
3431
+ current += char;
3432
+ continue;
3433
+ }
3434
+ switch (char) {
3435
+ case "{":
3436
+ braceDepth += 1;
3437
+ break;
3438
+ case "}":
3439
+ braceDepth -= 1;
3440
+ break;
3441
+ case "[":
3442
+ bracketDepth += 1;
3443
+ break;
3444
+ case "]":
3445
+ bracketDepth -= 1;
3446
+ break;
3447
+ case "(":
3448
+ parenDepth += 1;
3449
+ break;
3450
+ case ")":
3451
+ parenDepth -= 1;
3452
+ break;
3453
+ case "<":
3454
+ angleDepth += 1;
3455
+ break;
3456
+ case ">":
3457
+ angleDepth -= 1;
3458
+ break;
3459
+ case ",":
3460
+ if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
3461
+ const trimmed2 = current.trim();
3462
+ if (trimmed2) {
3463
+ parts.push(trimmed2);
3464
+ }
3465
+ current = "";
3466
+ continue;
3467
+ }
3468
+ break;
3469
+ }
3470
+ current += char;
3471
+ }
3472
+ const trimmed = current.trim();
3473
+ if (trimmed) {
3474
+ parts.push(trimmed);
3475
+ }
3476
+ return parts;
3477
+ }
3478
+ function parseParameterSignature(parameter) {
3479
+ const colonIndex = parameter.indexOf(":");
3480
+ if (colonIndex < 0) {
3481
+ return { name: parameter.trim() };
3482
+ }
3483
+ const rawName = parameter.slice(0, colonIndex).trim();
3484
+ const hasQuestionToken = rawName.endsWith("?");
3485
+ const name = hasQuestionToken ? rawName.slice(0, -1).trim() : rawName;
3486
+ const remainder = parameter.slice(colonIndex + 1).trim();
3487
+ const initializerIndex = remainder.lastIndexOf("=");
3488
+ if (initializerIndex < 0) {
3489
+ return {
3490
+ name,
3491
+ hasQuestionToken,
3492
+ type: remainder || void 0
3493
+ };
3494
+ }
3495
+ return {
3496
+ name,
3497
+ hasQuestionToken,
3498
+ type: remainder.slice(0, initializerIndex).trim() || void 0,
3499
+ initializer: remainder.slice(initializerIndex + 1).trim() || void 0
3500
+ };
3501
+ }
3502
+ function parseParameterSignatures(parameters) {
3503
+ const trimmed = parameters.trim();
3504
+ if (!trimmed) {
3505
+ return [];
3506
+ }
3507
+ return splitParameterList(trimmed).map(parseParameterSignature);
3508
+ }
3234
3509
  function toPosixRelativePath(fromDir, toFile) {
3235
3510
  let rel = path.relative(fromDir, toFile).replace(/\\/g, "/");
3236
3511
  if (!rel.startsWith(".")) {
@@ -3238,12 +3513,6 @@ function toPosixRelativePath(fromDir, toFile) {
3238
3513
  }
3239
3514
  return rel;
3240
3515
  }
3241
- function changeExtension(filePath, expectedExt, nextExtWithDot) {
3242
- const parsed = path.parse(filePath);
3243
- if (parsed.ext !== expectedExt)
3244
- return filePath;
3245
- return path.format({ ...parsed, base: `${parsed.name}${nextExtWithDot}`, ext: nextExtWithDot });
3246
- }
3247
3516
  function stripExtension(filePath) {
3248
3517
  const posix = (filePath ?? "").replace(/\\/g, "/");
3249
3518
  const parsed = path.posix.parse(posix);
@@ -3256,6 +3525,70 @@ function resolveRouterEntry(projectRoot, routerEntry) {
3256
3525
  const root = projectRoot ?? process.cwd();
3257
3526
  return path.isAbsolute(routerEntry) ? routerEntry : path.resolve(root, routerEntry);
3258
3527
  }
3528
+ function createCustomPomImportCollisionError(exportName, requested) {
3529
+ return new VuePomGeneratorError(
3530
+ `Custom POM import name collision detected for "${exportName}".
3531
+ The identifier "${requested}" conflicts with a generated POM class.
3532
+ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] to a unique name, or set generation.playwright.customPoms.importNameCollisionBehavior = "alias" to auto-alias collisions.`
3533
+ );
3534
+ }
3535
+ function normalizeComponentTagToClassName(tag) {
3536
+ const className = toPascalCase(tag);
3537
+ return className || void 0;
3538
+ }
3539
+ function collectReferencedComponentClassNames(nodes, names) {
3540
+ for (const node of nodes) {
3541
+ switch (node.type) {
3542
+ case NodeTypes$1.ELEMENT: {
3543
+ const element = node;
3544
+ if (element.tagType === ElementTypes.COMPONENT) {
3545
+ const className = normalizeComponentTagToClassName(element.tag);
3546
+ if (className) {
3547
+ names.add(className);
3548
+ }
3549
+ }
3550
+ collectReferencedComponentClassNames(element.children, names);
3551
+ break;
3552
+ }
3553
+ case NodeTypes$1.IF: {
3554
+ const ifNode = node;
3555
+ for (const branch of ifNode.branches) {
3556
+ collectReferencedComponentClassNames(branch.children, names);
3557
+ }
3558
+ break;
3559
+ }
3560
+ case NodeTypes$1.FOR: {
3561
+ const forNode = node;
3562
+ collectReferencedComponentClassNames(forNode.children, names);
3563
+ break;
3564
+ }
3565
+ }
3566
+ }
3567
+ }
3568
+ function getComponentClassNamesFromVueSource(source) {
3569
+ try {
3570
+ const { descriptor } = parse$1(source);
3571
+ const template = descriptor.template?.content?.trim();
3572
+ if (!template) {
3573
+ return [];
3574
+ }
3575
+ const root = parse$2(template);
3576
+ const names = /* @__PURE__ */ new Set();
3577
+ collectReferencedComponentClassNames(root.children, names);
3578
+ return [...names];
3579
+ } catch {
3580
+ return [];
3581
+ }
3582
+ }
3583
+ function resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot) {
3584
+ const mapped = vueFilesPathMap.get(targetClassName);
3585
+ const candidates = [
3586
+ mapped,
3587
+ path.join(projectRoot, "src", "views", `${targetClassName}.vue`),
3588
+ path.join(projectRoot, "src", "components", `${targetClassName}.vue`)
3589
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
3590
+ return candidates.find((candidate) => fs.existsSync(candidate));
3591
+ }
3259
3592
  async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, options = {}) {
3260
3593
  const root = projectRoot ?? process.cwd();
3261
3594
  const viewsDir = options.viewsDir ?? "src/views";
@@ -3290,35 +3623,40 @@ async function getRouteMetaByComponent(projectRoot, routerEntry, routerType, opt
3290
3623
  );
3291
3624
  }
3292
3625
  function generateRouteProperty(routeMeta) {
3293
- if (!routeMeta) {
3294
- return " static readonly route: { template: string } | null = null;\n";
3295
- }
3296
3626
  return [
3297
- " static readonly route: { template: string } | null = {",
3298
- ` template: ${JSON.stringify(routeMeta.template)},`,
3299
- " } as const;",
3300
- ""
3301
- ].join("\n");
3627
+ createClassProperty({
3628
+ name: "route",
3629
+ isStatic: true,
3630
+ isReadonly: true,
3631
+ type: "{ template: string } | null",
3632
+ initializer: routeMeta ? `{ template: ${JSON.stringify(routeMeta.template)} } as const` : "null"
3633
+ })
3634
+ ];
3302
3635
  }
3303
3636
  function generateGoToSelfMethod(componentName) {
3304
3637
  return [
3305
- "",
3306
- " async goTo() {",
3307
- " await this.goToSelf();",
3308
- " }",
3309
- "",
3310
- " async goToSelf() {",
3311
- ` const route = ${componentName}.route;`,
3312
- " if (!route) {",
3313
- ` throw new Error("[pom] No router path found for component/page-object '${componentName}'.");`,
3314
- " }",
3315
- " const runtimeEnv = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;",
3316
- " const runtimeBaseUrl = runtimeEnv?.PLAYWRIGHT_RUNTIME_BASE_URL ?? runtimeEnv?.PLAYWRIGHT_TEST_BASE_URL ?? runtimeEnv?.VITE_PLAYWRIGHT_BASE_URL;",
3317
- " const targetUrl = runtimeBaseUrl ? new URL(route.template, runtimeBaseUrl).toString() : route.template;",
3318
- " await this.page.goto(targetUrl);",
3319
- " }",
3320
- ""
3321
- ].join("\n");
3638
+ createClassMethod({
3639
+ name: "goTo",
3640
+ isAsync: true,
3641
+ statements: [
3642
+ "await this.goToSelf();"
3643
+ ]
3644
+ }),
3645
+ createClassMethod({
3646
+ name: "goToSelf",
3647
+ isAsync: true,
3648
+ statements: [
3649
+ `const route = ${componentName}.route;`,
3650
+ "if (!route) {",
3651
+ ` throw new Error("[pom] No router path found for component/page-object '${componentName}'.");`,
3652
+ "}",
3653
+ "const runtimeEnv = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;",
3654
+ "const runtimeBaseUrl = runtimeEnv?.PLAYWRIGHT_RUNTIME_BASE_URL ?? runtimeEnv?.PLAYWRIGHT_TEST_BASE_URL ?? runtimeEnv?.VITE_PLAYWRIGHT_BASE_URL;",
3655
+ "const targetUrl = runtimeBaseUrl ? new URL(route.template, runtimeBaseUrl).toString() : route.template;",
3656
+ "await this.page.goto(targetUrl);"
3657
+ ]
3658
+ })
3659
+ ];
3322
3660
  }
3323
3661
  function formatMethodParams(params) {
3324
3662
  if (!params)
@@ -3333,21 +3671,13 @@ function formatMethodParams(params) {
3333
3671
  };
3334
3672
  return entries.slice().sort((a, b) => score(a[0]) - score(b[0]) || a[0].localeCompare(b[0])).map(([name, typeExpr]) => `${name}: ${typeExpr}`).join(", ");
3335
3673
  }
3336
- function generateExtraClickMethodContent(spec) {
3674
+ function generateExtraClickMethodMembers(spec) {
3337
3675
  if (spec.kind !== "click") {
3338
- return "";
3676
+ return [];
3339
3677
  }
3340
3678
  const params = spec.params ?? {};
3341
3679
  const signatureParams = formatMethodParams(params);
3342
- const signature = signatureParams ? `(${signatureParams})` : "()";
3343
- const lines = [];
3344
- lines.push(
3345
- "",
3346
- ` async ${spec.name}${signature} {`
3347
- );
3348
- if (spec.keyLiteral !== void 0) {
3349
- lines.push(` const key = ${JSON.stringify(spec.keyLiteral)};`);
3350
- }
3680
+ const parameters = parseParameterSignatures(signatureParams);
3351
3681
  const hasAnnotationText = Object.prototype.hasOwnProperty.call(params, "annotationText");
3352
3682
  const hasWait = Object.prototype.hasOwnProperty.call(params, "wait");
3353
3683
  const annotationArg = hasAnnotationText ? "annotationText" : '""';
@@ -3355,9 +3685,6 @@ function generateExtraClickMethodContent(spec) {
3355
3685
  if (spec.selector.kind === "testId") {
3356
3686
  const needsTemplate = spec.selector.formattedDataTestId.includes("${");
3357
3687
  const testIdExpr = needsTemplate ? `\`${spec.selector.formattedDataTestId}\`` : JSON.stringify(spec.selector.formattedDataTestId);
3358
- if (needsTemplate) {
3359
- lines.push(` const testId = ${testIdExpr};`);
3360
- }
3361
3688
  const clickArgs = [];
3362
3689
  clickArgs.push(needsTemplate ? "testId" : testIdExpr);
3363
3690
  if (hasAnnotationText || hasWait) {
@@ -3366,33 +3693,54 @@ function generateExtraClickMethodContent(spec) {
3366
3693
  if (hasWait) {
3367
3694
  clickArgs.push(waitArg);
3368
3695
  }
3369
- lines.push(` await this.clickByTestId(${clickArgs.join(", ")});`);
3370
- lines.push(" }");
3371
- return `${lines.join("\n")}
3372
- `;
3696
+ return [
3697
+ createClassMethod({
3698
+ name: spec.name,
3699
+ isAsync: true,
3700
+ parameters,
3701
+ statements: (writer) => {
3702
+ if (spec.keyLiteral !== void 0) {
3703
+ writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
3704
+ }
3705
+ if (needsTemplate) {
3706
+ writer.writeLine(`const testId = ${testIdExpr};`);
3707
+ }
3708
+ writer.writeLine(`await this.clickByTestId(${clickArgs.join(", ")});`);
3709
+ }
3710
+ })
3711
+ ];
3373
3712
  }
3374
3713
  const rootNeedsTemplate = spec.selector.rootFormattedDataTestId.includes("${");
3375
3714
  const labelNeedsTemplate = spec.selector.formattedLabel.includes("${");
3376
3715
  const rootExpr = rootNeedsTemplate ? `\`${spec.selector.rootFormattedDataTestId}\`` : JSON.stringify(spec.selector.rootFormattedDataTestId);
3377
3716
  const labelExpr = labelNeedsTemplate ? `\`${spec.selector.formattedLabel}\`` : JSON.stringify(spec.selector.formattedLabel);
3378
- if (rootNeedsTemplate) {
3379
- lines.push(` const rootTestId = ${rootExpr};`);
3380
- }
3381
- if (labelNeedsTemplate) {
3382
- lines.push(` const label = ${labelExpr};`);
3383
- }
3384
3717
  const rootArg = rootNeedsTemplate ? "rootTestId" : rootExpr;
3385
3718
  const labelArg = labelNeedsTemplate ? "label" : labelExpr;
3386
- lines.push(` await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
3387
- lines.push(" }");
3388
- return `${lines.join("\n")}
3389
- `;
3719
+ return [
3720
+ createClassMethod({
3721
+ name: spec.name,
3722
+ isAsync: true,
3723
+ parameters,
3724
+ statements: (writer) => {
3725
+ if (spec.keyLiteral !== void 0) {
3726
+ writer.writeLine(`const key = ${JSON.stringify(spec.keyLiteral)};`);
3727
+ }
3728
+ if (rootNeedsTemplate) {
3729
+ writer.writeLine(`const rootTestId = ${rootExpr};`);
3730
+ }
3731
+ if (labelNeedsTemplate) {
3732
+ writer.writeLine(`const label = ${labelExpr};`);
3733
+ }
3734
+ writer.writeLine(`await this.clickWithinTestIdByLabel(${rootArg}, ${labelArg}, ${annotationArg}, ${waitArg});`);
3735
+ }
3736
+ })
3737
+ ];
3390
3738
  }
3391
- function generateMethodContentFromPom(primary, targetPageObjectModelClass) {
3739
+ function generateMethodMembersFromPom(primary, targetPageObjectModelClass) {
3392
3740
  if (primary.emitPrimary === false) {
3393
- return "";
3741
+ return [];
3394
3742
  }
3395
- return generateViewObjectModelMethodContent(
3743
+ return generateViewObjectModelMembers(
3396
3744
  targetPageObjectModelClass,
3397
3745
  primary.methodName,
3398
3746
  primary.nativeRole,
@@ -3426,14 +3774,14 @@ function generateMethodsContentForDependencies(dependencies) {
3426
3774
  return true;
3427
3775
  });
3428
3776
  const extras = (dependencies.pomExtraMethods ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
3429
- let content = "";
3777
+ const members = [];
3430
3778
  for (const { pom, target } of primarySpecs) {
3431
- content += generateMethodContentFromPom(pom, target);
3779
+ members.push(...generateMethodMembersFromPom(pom, target));
3432
3780
  }
3433
3781
  for (const extra of extras) {
3434
- content += generateExtraClickMethodContent(extra);
3782
+ members.push(...generateExtraClickMethodMembers(extra));
3435
3783
  }
3436
- return content;
3784
+ return members;
3437
3785
  }
3438
3786
  async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, options = {}) {
3439
3787
  const {
@@ -3446,6 +3794,7 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3446
3794
  customPomImportNameCollisionBehavior = "error",
3447
3795
  testIdAttribute,
3448
3796
  emitLanguages: emitLanguagesOverride,
3797
+ typescriptOutputStructure = "aggregated",
3449
3798
  csharp,
3450
3799
  vueRouterFluentChaining,
3451
3800
  routerEntry,
@@ -3467,7 +3816,16 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3467
3816
  generatedFilePaths.push(resolvedFilePath);
3468
3817
  };
3469
3818
  if (emitLanguages.includes("ts")) {
3470
- const files = await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
3819
+ const files = typescriptOutputStructure === "split" ? await generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
3820
+ customPomAttachments,
3821
+ projectRoot,
3822
+ customPomDir,
3823
+ customPomImportAliases,
3824
+ customPomImportNameCollisionBehavior,
3825
+ testIdAttribute,
3826
+ routeMetaByComponent,
3827
+ vueRouterFluentChaining
3828
+ }) : await generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, {
3471
3829
  customPomAttachments,
3472
3830
  projectRoot,
3473
3831
  customPomDir,
@@ -3504,6 +3862,100 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3504
3862
  createFile(file.filePath, file.content);
3505
3863
  }
3506
3864
  }
3865
+ async function generateSplitTypeScriptFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, options = {}) {
3866
+ const projectRoot = options.projectRoot ?? process.cwd();
3867
+ const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
3868
+ const base = ensureDir(outDir);
3869
+ const generatedClassNames = new Set(entries.map(([name]) => name));
3870
+ const referencedTargets = /* @__PURE__ */ new Set();
3871
+ for (const [, deps] of entries) {
3872
+ for (const dataTestId of deps.dataTestIdSet ?? []) {
3873
+ if (dataTestId.targetPageObjectModelClass) {
3874
+ referencedTargets.add(dataTestId.targetPageObjectModelClass);
3875
+ }
3876
+ }
3877
+ }
3878
+ const stubTargets = Array.from(referencedTargets).filter((target) => !generatedClassNames.has(target)).sort((a, b) => a.localeCompare(b));
3879
+ const availableClassNames = /* @__PURE__ */ new Set([...generatedClassNames, ...stubTargets]);
3880
+ const depsByClassName = new Map(entries);
3881
+ const generatedTsFilePathByComponent = /* @__PURE__ */ new Map();
3882
+ for (const className of availableClassNames) {
3883
+ generatedTsFilePathByComponent.set(className, path.join(base, `${className}.g.ts`));
3884
+ }
3885
+ const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
3886
+ customPomDir: options.customPomDir,
3887
+ customPomImportAliases: options.customPomImportAliases,
3888
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
3889
+ });
3890
+ const runtimeBasePagePath = path.join(base, "_pom-runtime", "class-generation", "BasePage.ts");
3891
+ const files = [];
3892
+ for (const [name, deps] of entries) {
3893
+ const filePath = generatedTsFilePathByComponent.get(name);
3894
+ if (!filePath) {
3895
+ continue;
3896
+ }
3897
+ const content = generateViewObjectModelContent(name, deps, componentHierarchyMap, vueFilesPathMap, runtimeBasePagePath, {
3898
+ outputDir: path.dirname(filePath),
3899
+ customPomAttachments: options.customPomAttachments ?? [],
3900
+ projectRoot,
3901
+ customPomDir: options.customPomDir,
3902
+ customPomImportAliases: options.customPomImportAliases,
3903
+ customPomClassIdentifierMap: customPomImportResolution.classIdentifierMap,
3904
+ customPomAvailableClassIdentifiers: customPomImportResolution.availableClassIdentifiers,
3905
+ customPomImportSpecifiersByClass: customPomImportResolution.importSpecifiersByClass,
3906
+ customPomMethodSignaturesByClass: customPomImportResolution.methodSignaturesByClass,
3907
+ generatedTsFilePathByComponent,
3908
+ testIdAttribute: options.testIdAttribute,
3909
+ vueRouterFluentChaining: options.vueRouterFluentChaining,
3910
+ routeMetaByComponent: options.routeMetaByComponent
3911
+ });
3912
+ files.push({ filePath, content });
3913
+ }
3914
+ for (const targetClassName of stubTargets) {
3915
+ const filePath = generatedTsFilePathByComponent.get(targetClassName);
3916
+ if (!filePath) {
3917
+ continue;
3918
+ }
3919
+ const outputDir = path.dirname(filePath);
3920
+ const basePageImportSpecifier = stripExtension(toPosixRelativePath(outputDir, runtimeBasePagePath));
3921
+ const composed = getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot);
3922
+ const childImports = getChildImportSpecifiers(outputDir, composed?.childClassNames ?? [], generatedTsFilePathByComponent);
3923
+ const members = composed?.members ?? getDefaultStubMembers();
3924
+ const content = renderSplitStubPomContent({
3925
+ className: targetClassName,
3926
+ basePageImportSpecifier,
3927
+ childImports,
3928
+ members
3929
+ });
3930
+ files.push({ filePath, content });
3931
+ }
3932
+ const runtimeAssetSpecs = getRuntimeGeneratedAssetSpecs(base, basePageClassPath);
3933
+ const runtimeFiles = buildRuntimeGeneratedFilesFromSpecs(runtimeAssetSpecs);
3934
+ const indexContent = renderSourceFile("index.ts", (sourceFile) => {
3935
+ for (const spec of runtimeAssetSpecs) {
3936
+ addExportAll(sourceFile, stripExtension(toPosixRelativePath(base, spec.outputPath)));
3937
+ }
3938
+ for (const [, filePath] of Array.from(generatedTsFilePathByComponent.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
3939
+ addExportAll(sourceFile, `./${stripExtension(path.basename(filePath))}`);
3940
+ }
3941
+ }, {
3942
+ prefixText: buildFilePrefix({
3943
+ eslintDisableSortImports: true,
3944
+ commentLines: [
3945
+ "POM exports",
3946
+ "DO NOT MODIFY BY HAND",
3947
+ "",
3948
+ "This file is auto-generated by vue-pom-generator.",
3949
+ "Changes should be made in the generator/template, not in the generated output."
3950
+ ]
3951
+ })
3952
+ });
3953
+ return [
3954
+ ...files,
3955
+ { filePath: path.join(base, "index.ts"), content: indexContent },
3956
+ ...runtimeFiles
3957
+ ];
3958
+ }
3507
3959
  function escapeGitAttributesPattern(value) {
3508
3960
  let output = "";
3509
3961
  for (let i = 0; i < value.length; i++) {
@@ -3942,91 +4394,190 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
3942
4394
  };
3943
4395
  }).filter((entry) => !!entry);
3944
4396
  const overrideCtorByClassName = new Map(overrideCtorEntries.map((entry) => [entry.className, entry.localIdentifier]));
3945
- const overrideImports = overrideCtorEntries.length ? `${overrideCtorEntries.map((entry) => `import { ${entry.className} as ${entry.localIdentifier} } from "${entry.importSpecifier}";`).join("\n")}
3946
-
3947
- ` : "";
3948
4397
  const fixtureCtorExpression = (name) => overrideCtorByClassName.get(name) ?? `Pom.${name}`;
3949
- const header = `${eslintSuppressionHeader}/**
3950
- * DO NOT MODIFY BY HAND
3951
- *
3952
- * This file is auto-generated by vue-pom-generator.
3953
- * Changes should be made in the generator/template, not in the generated output.
3954
- */
3955
-
3956
- `;
3957
- const fixturesTypeEntries = viewClassNames.map((name) => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`).join("\n");
3958
- const componentFixturesTypeEntries = componentClassNames.map((name) => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`).join("\n");
3959
- const pomFactoryType = `export type PomConstructor<T> = new (page: PwPage) => T;
3960
-
3961
- export interface PomFactory {
3962
- create<T>(ctor: PomConstructor<T>): T;
3963
- }
3964
-
3965
- `;
3966
- const fixturesContent = `${header}/** Generated Playwright fixtures (typed page objects). */
3967
-
3968
- import { expect, test as base } from "@playwright/test";
3969
- import type { Page as PwPage } from "@playwright/test";
3970
- import * as Pom from "${pomImport}";
3971
- ${overrideImports}export interface PlaywrightOptions {
3972
- animation: Pom.PlaywrightAnimationOptions;
3973
- }
3974
-
3975
- ${pomFactoryType}type PomSetupFixture = { pomSetup: void };
3976
- type PomFactoryFixture = { pomFactory: PomFactory };
3977
-
3978
- const pageCtors = {
3979
- ${fixturesTypeEntries}
3980
- } as const;
3981
- const componentCtors = {
3982
- ${componentFixturesTypeEntries}
3983
- } as const;
3984
-
3985
- export type GeneratedPageFixtures = { [K in keyof typeof pageCtors]: InstanceType<(typeof pageCtors)[K]> };
3986
- export type GeneratedComponentFixtures = { [K in keyof typeof componentCtors]: InstanceType<(typeof componentCtors)[K]> };
3987
-
3988
- const makePomFixture = <T>(Ctor: PomConstructor<T>) => async ({ page }: { page: PwPage }, use: (t: T) => Promise<void>) => {
3989
- await use(new Ctor(page));
3990
- };
3991
-
3992
- const createPomFixtures = <TMap extends Record<string, PomConstructor<any>>>(ctors: TMap) => {
3993
- const out: Record<string, any> = {};
3994
- for (const [key, Ctor] of Object.entries(ctors)) {
3995
- out[key] = makePomFixture(Ctor as PomConstructor<any>);
3996
- }
3997
- return out as any;
3998
- };
3999
-
4000
- const test = base.extend<PlaywrightOptions & PomSetupFixture & PomFactoryFixture & GeneratedPageFixtures & GeneratedComponentFixtures>({
4001
- animation: [{
4002
- pointer: { durationMilliseconds: 250, transitionStyle: "ease-in-out", clickDelayMilliseconds: 0 },
4003
- keyboard: { typeDelayMilliseconds: 100 },
4004
- }, { option: true }],
4005
- pomSetup: [async ({ animation }, use) => {
4006
- Pom.setPlaywrightAnimationOptions(animation);
4007
- await use();
4008
- }, { auto: true }],
4009
- pomFactory: async ({ page }, use) => {
4010
- await use({
4011
- create: <T>(ctor: PomConstructor<T>) => new ctor(page),
4398
+ const pageCtorEntries = viewClassNames.map((name) => ({
4399
+ fixtureName: lowerFirst(name),
4400
+ ctorExpression: fixtureCtorExpression(name)
4401
+ }));
4402
+ const componentCtorEntries = componentClassNames.map((name) => ({
4403
+ fixtureName: lowerFirst(name),
4404
+ ctorExpression: fixtureCtorExpression(name)
4405
+ }));
4406
+ const fixturesContent = renderSourceFile(fixtureFileName, (sourceFile) => {
4407
+ sourceFile.addStatements("/** Generated Playwright fixtures (typed page objects). */");
4408
+ addNamedImport(sourceFile, {
4409
+ moduleSpecifier: "@playwright/test",
4410
+ namedImports: [
4411
+ "expect",
4412
+ { name: "test", alias: "base" }
4413
+ ]
4012
4414
  });
4013
- },
4014
- ...createPomFixtures(pageCtors),
4015
- ...createPomFixtures(componentCtors),
4016
- });
4017
-
4018
- export { test, expect };
4019
- `;
4020
- return {
4021
- filePath: path.resolve(fixtureOutDirAbs, fixtureFileName),
4415
+ addNamedImport(sourceFile, {
4416
+ moduleSpecifier: "@playwright/test",
4417
+ isTypeOnly: true,
4418
+ namedImports: [{ name: "Page", alias: "PwPage" }]
4419
+ });
4420
+ sourceFile.addImportDeclaration({
4421
+ namespaceImport: "Pom",
4422
+ moduleSpecifier: pomImport
4423
+ });
4424
+ for (const entry of overrideCtorEntries) {
4425
+ addNamedImport(sourceFile, {
4426
+ moduleSpecifier: entry.importSpecifier,
4427
+ namedImports: [{ name: entry.className, alias: entry.localIdentifier }]
4428
+ });
4429
+ }
4430
+ sourceFile.addInterface({
4431
+ isExported: true,
4432
+ name: "PlaywrightOptions",
4433
+ properties: [{
4434
+ name: "animation",
4435
+ type: "Pom.PlaywrightAnimationOptions"
4436
+ }]
4437
+ });
4438
+ sourceFile.addTypeAlias({
4439
+ isExported: true,
4440
+ name: "PomConstructor",
4441
+ typeParameters: [{ name: "T" }],
4442
+ type: "new (page: PwPage) => T"
4443
+ });
4444
+ sourceFile.addInterface({
4445
+ isExported: true,
4446
+ name: "PomFactory",
4447
+ methods: [{
4448
+ name: "create",
4449
+ typeParameters: [{ name: "T" }],
4450
+ parameters: [{ name: "ctor", type: "PomConstructor<T>" }],
4451
+ returnType: "T"
4452
+ }]
4453
+ });
4454
+ sourceFile.addTypeAlias({
4455
+ name: "PomSetupFixture",
4456
+ type: "{ pomSetup: void }"
4457
+ });
4458
+ sourceFile.addTypeAlias({
4459
+ name: "PomFactoryFixture",
4460
+ type: "{ pomFactory: PomFactory }"
4461
+ });
4462
+ sourceFile.addVariableStatement({
4463
+ declarationKind: VariableDeclarationKind.Const,
4464
+ declarations: [{
4465
+ name: "pageCtors",
4466
+ initializer: (writer) => {
4467
+ writer.write("{").newLine();
4468
+ writer.indent(() => {
4469
+ for (const entry of pageCtorEntries) {
4470
+ writer.writeLine(`${entry.fixtureName}: ${entry.ctorExpression},`);
4471
+ }
4472
+ });
4473
+ writer.write("} as const");
4474
+ }
4475
+ }]
4476
+ });
4477
+ sourceFile.addVariableStatement({
4478
+ declarationKind: VariableDeclarationKind.Const,
4479
+ declarations: [{
4480
+ name: "componentCtors",
4481
+ initializer: (writer) => {
4482
+ writer.write("{").newLine();
4483
+ writer.indent(() => {
4484
+ for (const entry of componentCtorEntries) {
4485
+ writer.writeLine(`${entry.fixtureName}: ${entry.ctorExpression},`);
4486
+ }
4487
+ });
4488
+ writer.write("} as const");
4489
+ }
4490
+ }]
4491
+ });
4492
+ sourceFile.addTypeAlias({
4493
+ isExported: true,
4494
+ name: "GeneratedPageFixtures",
4495
+ type: "{ [K in keyof typeof pageCtors]: InstanceType<(typeof pageCtors)[K]> }"
4496
+ });
4497
+ sourceFile.addTypeAlias({
4498
+ isExported: true,
4499
+ name: "GeneratedComponentFixtures",
4500
+ type: "{ [K in keyof typeof componentCtors]: InstanceType<(typeof componentCtors)[K]> }"
4501
+ });
4502
+ sourceFile.addFunction({
4503
+ name: "makePomFixture",
4504
+ typeParameters: [{ name: "T" }],
4505
+ parameters: [{ name: "Ctor", type: "PomConstructor<T>" }],
4506
+ statements: [
4507
+ "return async ({ page }: { page: PwPage }, use: (t: T) => Promise<void>) => {",
4508
+ " await use(new Ctor(page));",
4509
+ "};"
4510
+ ]
4511
+ });
4512
+ sourceFile.addFunction({
4513
+ name: "createPomFixtures",
4514
+ typeParameters: [{ name: "TMap", constraint: "Record<string, PomConstructor<any>>" }],
4515
+ parameters: [{ name: "ctors", type: "TMap" }],
4516
+ statements: [
4517
+ "const out: Record<string, any> = {};",
4518
+ "for (const [key, Ctor] of Object.entries(ctors)) {",
4519
+ " out[key] = makePomFixture(Ctor as PomConstructor<any>);",
4520
+ "}",
4521
+ "return out as any;"
4522
+ ]
4523
+ });
4524
+ sourceFile.addVariableStatement({
4525
+ declarationKind: VariableDeclarationKind.Const,
4526
+ declarations: [{
4527
+ name: "test",
4528
+ initializer: (writer) => {
4529
+ writer.write("base.extend<PlaywrightOptions & PomSetupFixture & PomFactoryFixture & GeneratedPageFixtures & GeneratedComponentFixtures>(");
4530
+ writer.block(() => {
4531
+ writer.writeLine("animation: [{");
4532
+ writer.indent(() => {
4533
+ writer.writeLine('pointer: { durationMilliseconds: 250, transitionStyle: "ease-in-out", clickDelayMilliseconds: 0 },');
4534
+ writer.writeLine("keyboard: { typeDelayMilliseconds: 100 },");
4535
+ });
4536
+ writer.writeLine("}, { option: true }],");
4537
+ writer.writeLine("pomSetup: [async ({ animation }, use) => {");
4538
+ writer.indent(() => {
4539
+ writer.writeLine("Pom.setPlaywrightAnimationOptions(animation);");
4540
+ writer.writeLine("await use();");
4541
+ });
4542
+ writer.writeLine("}, { auto: true }],");
4543
+ writer.writeLine("pomFactory: async ({ page }, use) => {");
4544
+ writer.indent(() => {
4545
+ writer.writeLine("await use({");
4546
+ writer.indent(() => {
4547
+ writer.writeLine("create: <T>(ctor: PomConstructor<T>) => new ctor(page),");
4548
+ });
4549
+ writer.writeLine("});");
4550
+ });
4551
+ writer.writeLine("},");
4552
+ writer.writeLine("...createPomFixtures(pageCtors),");
4553
+ writer.writeLine("...createPomFixtures(componentCtors),");
4554
+ });
4555
+ writer.write(")");
4556
+ }
4557
+ }]
4558
+ });
4559
+ sourceFile.addExportDeclaration({
4560
+ namedExports: ["test", "expect"]
4561
+ });
4562
+ }, {
4563
+ prefixText: buildFilePrefix({
4564
+ eslintDisableSortImports: true,
4565
+ commentLines: [
4566
+ "DO NOT MODIFY BY HAND",
4567
+ "",
4568
+ "This file is auto-generated by vue-pom-generator.",
4569
+ "Changes should be made in the generator/template, not in the generated output."
4570
+ ]
4571
+ })
4572
+ });
4573
+ return {
4574
+ filePath: path.resolve(fixtureOutDirAbs, fixtureFileName),
4022
4575
  content: fixturesContent
4023
4576
  };
4024
4577
  }
4025
- function generateViewObjectModelContent(componentName, dependencies, componentHierarchyMap, vueFilesPathMap, basePageClassPath, options = {}) {
4026
- const { isView, childrenComponentSet, usedComponentSet, filePath } = dependencies;
4578
+ function prepareViewObjectModelClass(componentName, dependencies, componentHierarchyMap, options = {}) {
4579
+ const { isView, childrenComponentSet, usedComponentSet } = dependencies;
4027
4580
  const {
4028
- outputDir = path.dirname(filePath),
4029
- aggregated = false,
4030
4581
  customPomAttachments = [],
4031
4582
  testIdAttribute
4032
4583
  } = options;
@@ -4060,45 +4611,9 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4060
4611
  flatten: a.flatten ?? false,
4061
4612
  methodSignatures: a.flatten ? customPomMethodSignaturesByClass.get(a.className) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map()
4062
4613
  }));
4063
- let content = "";
4064
- const sourceRel = toPosixRelativePath(outputDir, filePath);
4065
- const kind = isView ? "Page" : "Component";
4066
- const doc = `/** ${kind} POM: ${componentName} (source: ${sourceRel}) */
4067
- `;
4068
- if (!aggregated) {
4069
- content = `${eslintSuppressionHeader}${doc}`;
4070
- if (isView || attachmentsForThisClass.length > 0) {
4071
- content += 'import type { Page as PwPage } from "@playwright/test";\n';
4072
- }
4073
- const projectRoot = options.projectRoot ?? process.cwd();
4074
- const fromAbs = path.isAbsolute(outputDir) ? outputDir : path.resolve(projectRoot, outputDir);
4075
- const toAbs = basePageClassPath ? path.isAbsolute(basePageClassPath) ? basePageClassPath : path.resolve(projectRoot, basePageClassPath) : "";
4076
- const basePageImport = path.relative(fromAbs, toAbs).replace(/\\/g, "/");
4077
- const basePageImportNoExt = stripExtension(basePageImport).replace(/\\/g, "/");
4078
- const basePageImportSpecifier = basePageImportNoExt.startsWith(".") ? basePageImportNoExt : `./${basePageImportNoExt}`;
4079
- content += `import { BasePage, Fluent } from '${basePageImportSpecifier}';
4080
-
4081
- `;
4082
- if (isView && childrenComponentSet.size > 0) {
4083
- childrenComponentSet.forEach((child) => {
4084
- if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
4085
- const childPath = vueFilesPathMap.get(child);
4086
- let relativePath = path.relative(outputDir, childPath || "");
4087
- relativePath = changeExtension(relativePath, ".vue", ".g").replace(/\\/g, "/");
4088
- content += `import { ${child} } from '${relativePath}';
4089
- `;
4090
- }
4091
- });
4092
- }
4093
- } else {
4094
- content = doc;
4095
- }
4096
- const className = toPascalCaseLocal(componentName);
4097
- content += `
4098
- export class ${className} extends BasePage {
4099
- `;
4100
4614
  const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
4101
4615
  const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
4616
+ const className = toPascalCaseLocal(componentName);
4102
4617
  const childInstancePropertyNames = Array.from(componentRefsForInstances).filter((child) => componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size).map((child) => child.split(".vue")[0]);
4103
4618
  const blockedViewPassthroughMethodNames = new Set(
4104
4619
  attachmentsForThisClass.filter((a) => a.flatten).flatMap((a) => Array.from(a.methodSignatures.keys()))
@@ -4108,26 +4623,144 @@ export class ${className} extends BasePage {
4108
4623
  ...widgetInstances.map((w) => w.propertyName),
4109
4624
  ...childInstancePropertyNames
4110
4625
  ]);
4626
+ const members = [];
4111
4627
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
4112
- content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
4113
- content += getConstructor(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances, { testIdAttribute });
4628
+ members.push(...getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances));
4629
+ members.push(getConstructor(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances, { testIdAttribute }));
4114
4630
  }
4115
4631
  if (!isView && attachmentsForThisClass.length > 0) {
4116
- content += getComponentInstances(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass);
4117
- content += getConstructor(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute });
4632
+ members.push(...getComponentInstances(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass));
4633
+ members.push(getConstructor(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute }));
4118
4634
  }
4119
- content += getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames);
4635
+ members.push(
4636
+ ...getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames)
4637
+ );
4120
4638
  if (isView && componentRefsForInstances.size === 1) {
4121
- content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap, blockedViewPassthroughMethodNames);
4639
+ members.push(
4640
+ ...getViewPassthroughMethods(
4641
+ componentName,
4642
+ dependencies,
4643
+ componentRefsForInstances,
4644
+ componentHierarchyMap,
4645
+ blockedViewPassthroughMethodNames
4646
+ )
4647
+ );
4122
4648
  }
4123
4649
  if (isView && options.vueRouterFluentChaining) {
4124
4650
  const routeMeta = options.routeMetaByComponent?.[componentName] ?? null;
4125
- content += generateRouteProperty(routeMeta);
4126
- content += generateGoToSelfMethod(className);
4651
+ members.push(...generateRouteProperty(routeMeta));
4652
+ members.push(...generateGoToSelfMethod(className));
4127
4653
  }
4128
- content += generateMethodsContentForDependencies(dependencies);
4129
- content += "}\n";
4130
- return content;
4654
+ members.push(...generateMethodsContentForDependencies(dependencies));
4655
+ return {
4656
+ className,
4657
+ componentRefsForInstances,
4658
+ attachmentsForThisClass,
4659
+ widgetInstances,
4660
+ isView,
4661
+ members
4662
+ };
4663
+ }
4664
+ function generateViewObjectModelContent(componentName, dependencies, componentHierarchyMap, _vueFilesPathMap, basePageClassPath, options = {}) {
4665
+ const { filePath } = dependencies;
4666
+ const outputDir = options.outputDir ?? path.dirname(filePath);
4667
+ const prepared = prepareViewObjectModelClass(componentName, dependencies, componentHierarchyMap, options);
4668
+ const sourceRel = toPosixRelativePath(outputDir, filePath);
4669
+ const kind = prepared.isView ? "Page" : "Component";
4670
+ const doc = `/** ${kind} POM: ${componentName} (source: ${sourceRel}) */`;
4671
+ const projectRoot = options.projectRoot ?? process.cwd();
4672
+ const fromAbs = path.isAbsolute(outputDir) ? outputDir : path.resolve(projectRoot, outputDir);
4673
+ const toAbs = basePageClassPath ? path.isAbsolute(basePageClassPath) ? basePageClassPath : path.resolve(projectRoot, basePageClassPath) : "";
4674
+ const basePageImport = path.relative(fromAbs, toAbs).replace(/\\/g, "/");
4675
+ const basePageImportNoExt = stripExtension(basePageImport).replace(/\\/g, "/");
4676
+ const basePageImportSpecifier = basePageImportNoExt.startsWith(".") ? basePageImportNoExt : `./${basePageImportNoExt}`;
4677
+ const needsPlaywrightPageImport = prepared.isView || prepared.attachmentsForThisClass.length > 0;
4678
+ const customPomImportSpecifiersByClass = options.customPomImportSpecifiersByClass ?? {};
4679
+ const customImports = Array.from(
4680
+ /* @__PURE__ */ new Set([
4681
+ ...prepared.attachmentsForThisClass.map((attachment) => attachment.className),
4682
+ ...prepared.widgetInstances.map((widget) => widget.className)
4683
+ ])
4684
+ ).reduce((imports, localIdentifier) => {
4685
+ const specifier = Object.values(customPomImportSpecifiersByClass).find((spec) => spec.localIdentifier === localIdentifier);
4686
+ if (!specifier) {
4687
+ return imports;
4688
+ }
4689
+ imports.push({
4690
+ moduleSpecifier: stripExtension(toPosixRelativePath(fromAbs, specifier.absolutePath)),
4691
+ name: specifier.exportName,
4692
+ alias: specifier.localIdentifier !== specifier.exportName ? specifier.localIdentifier : void 0
4693
+ });
4694
+ return imports;
4695
+ }, []).sort((a, b) => (a.alias ?? a.name).localeCompare(b.alias ?? b.name));
4696
+ const generatedImports = [];
4697
+ const importedGeneratedClasses = /* @__PURE__ */ new Set();
4698
+ const generatedTsFilePathByComponent = options.generatedTsFilePathByComponent;
4699
+ const addGeneratedImport = (className) => {
4700
+ if (!generatedTsFilePathByComponent || importedGeneratedClasses.has(className) || className === componentName) {
4701
+ return;
4702
+ }
4703
+ const generatedFilePath = generatedTsFilePathByComponent.get(className);
4704
+ if (!generatedFilePath) {
4705
+ return;
4706
+ }
4707
+ generatedImports.push({
4708
+ className,
4709
+ moduleSpecifier: stripExtension(toPosixRelativePath(fromAbs, generatedFilePath))
4710
+ });
4711
+ importedGeneratedClasses.add(className);
4712
+ };
4713
+ for (const child of prepared.componentRefsForInstances) {
4714
+ const childName = child.endsWith(".vue") ? child.slice(0, -4) : child;
4715
+ const childDeps = componentHierarchyMap.get(child) ?? componentHierarchyMap.get(childName);
4716
+ if (childDeps?.dataTestIdSet.size) {
4717
+ addGeneratedImport(childName);
4718
+ }
4719
+ }
4720
+ const targetClassNames = Array.from(
4721
+ new Set(
4722
+ Array.from(dependencies.dataTestIdSet ?? []).map((entry) => entry.targetPageObjectModelClass).filter((target) => typeof target === "string" && target.length > 0)
4723
+ )
4724
+ ).sort((a, b) => a.localeCompare(b));
4725
+ for (const targetClassName of targetClassNames) {
4726
+ addGeneratedImport(targetClassName);
4727
+ }
4728
+ generatedImports.sort((a, b) => a.className.localeCompare(b.className));
4729
+ const prefixText = `${buildFilePrefix({ eslintDisableSortImports: true })}${doc}
4730
+ `;
4731
+ return renderSourceFile(`${prepared.className}.ts`, (sourceFile) => {
4732
+ if (needsPlaywrightPageImport) {
4733
+ addNamedImport(sourceFile, {
4734
+ moduleSpecifier: "@playwright/test",
4735
+ isTypeOnly: true,
4736
+ namedImports: [{ name: "Page", alias: "PwPage" }]
4737
+ });
4738
+ }
4739
+ addNamedImport(sourceFile, {
4740
+ moduleSpecifier: basePageImportSpecifier,
4741
+ namedImports: ["BasePage", "Fluent"]
4742
+ });
4743
+ for (const customImport of customImports) {
4744
+ addNamedImport(sourceFile, {
4745
+ moduleSpecifier: customImport.moduleSpecifier,
4746
+ namedImports: [{ name: customImport.name, alias: customImport.alias }]
4747
+ });
4748
+ }
4749
+ for (const generatedImport of generatedImports) {
4750
+ addNamedImport(sourceFile, {
4751
+ moduleSpecifier: generatedImport.moduleSpecifier,
4752
+ namedImports: [generatedImport.className]
4753
+ });
4754
+ }
4755
+ const classDeclaration = sourceFile.addClass({
4756
+ name: prepared.className,
4757
+ isExported: true,
4758
+ extends: "BasePage"
4759
+ });
4760
+ for (const member of prepared.members) {
4761
+ addClassMember(classDeclaration, member);
4762
+ }
4763
+ }, { prefixText });
4131
4764
  }
4132
4765
  function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap, blockedMethodNames = /* @__PURE__ */ new Set()) {
4133
4766
  const existingOnView = viewDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
@@ -4151,32 +4784,26 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
4151
4784
  }
4152
4785
  }
4153
4786
  const sorted = Array.from(methodToChildren.entries()).sort((a, b) => a[0].localeCompare(b[0]));
4154
- const lines = [];
4155
- for (const [methodName, candidates] of sorted) {
4156
- if (candidates.length !== 1)
4157
- continue;
4787
+ const passthroughs = sorted.filter(([, candidates]) => candidates.length === 1);
4788
+ if (!passthroughs.length) {
4789
+ return [];
4790
+ }
4791
+ return passthroughs.map(([methodName, candidates]) => {
4158
4792
  const { childProp, params, argNames } = candidates[0];
4159
4793
  const callArgs = argNames.join(", ");
4160
- lines.push(
4161
- "",
4162
- ` async ${methodName}(${params}) {`,
4163
- ` return await this.${childProp}.${methodName}(${callArgs});`,
4164
- " }"
4165
- );
4166
- }
4167
- if (!lines.length) {
4168
- return "";
4169
- }
4170
- return [
4171
- "",
4172
- ` // Passthrough methods composed from child component POMs of ${viewName}.`,
4173
- ...lines,
4174
- ""
4175
- ].join("\n");
4794
+ return createClassMethod({
4795
+ name: methodName,
4796
+ isAsync: true,
4797
+ parameters: parseParameterSignatures(params),
4798
+ statements: [
4799
+ `return await this.${childProp}.${methodName}(${callArgs});`
4800
+ ]
4801
+ });
4802
+ });
4176
4803
  }
4177
4804
  function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmentsForThisClass, reservedMemberNames) {
4178
4805
  if (!attachmentsForThisClass.some((a) => a.flatten && a.methodSignatures.size > 0)) {
4179
- return "";
4806
+ return [];
4180
4807
  }
4181
4808
  const existingOnClass = ownerDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
4182
4809
  const methodToAttachments = /* @__PURE__ */ new Map();
@@ -4198,30 +4825,22 @@ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmen
4198
4825
  }
4199
4826
  }
4200
4827
  const sorted = Array.from(methodToAttachments.entries()).sort((a, b) => a[0].localeCompare(b[0]));
4201
- const lines = [];
4202
- for (const [methodName, candidates] of sorted) {
4203
- if (candidates.length !== 1) {
4204
- continue;
4205
- }
4828
+ const passthroughs = sorted.filter(([, candidates]) => candidates.length === 1);
4829
+ if (!passthroughs.length) {
4830
+ return [];
4831
+ }
4832
+ return passthroughs.map(([methodName, candidates]) => {
4206
4833
  const { propertyName, params, argNames } = candidates[0];
4207
4834
  const callArgs = argNames.join(", ");
4208
4835
  const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
4209
- lines.push(
4210
- "",
4211
- ` ${methodName}(${params}) {`,
4212
- ` return ${invocation};`,
4213
- " }"
4214
- );
4215
- }
4216
- if (!lines.length) {
4217
- return "";
4218
- }
4219
- return [
4220
- "",
4221
- ` // Passthrough methods composed from custom helper attachments of ${ownerName}.`,
4222
- ...lines,
4223
- ""
4224
- ].join("\n");
4836
+ return createClassMethod({
4837
+ name: methodName,
4838
+ parameters: parseParameterSignatures(params),
4839
+ statements: [
4840
+ `return ${invocation};`
4841
+ ]
4842
+ });
4843
+ });
4225
4844
  }
4226
4845
  function sliceNodeSource(source, node) {
4227
4846
  if (node.start == null || node.end == null) {
@@ -4305,12 +4924,292 @@ function ensureDir(dir) {
4305
4924
  }
4306
4925
  return normalized;
4307
4926
  }
4927
+ function resolvePluginAsset(relative) {
4928
+ try {
4929
+ return fileURLToPath(new URL(relative, import.meta.url));
4930
+ } catch {
4931
+ return path.resolve(__dirname, relative);
4932
+ }
4933
+ }
4934
+ function readTextAsset(absPath, description) {
4935
+ try {
4936
+ return fs.readFileSync(absPath, "utf8");
4937
+ } catch {
4938
+ throw new VuePomGeneratorError(`Failed to read ${description} at ${absPath}`);
4939
+ }
4940
+ }
4941
+ function getDefaultStubMembers() {
4942
+ return [
4943
+ createClassConstructor({
4944
+ parameters: [{ name: "page", type: "PwPage" }],
4945
+ statements: [
4946
+ "super(page);"
4947
+ ]
4948
+ })
4949
+ ];
4950
+ }
4951
+ function renderSplitStubPomContent(options) {
4952
+ const prefixText = buildFilePrefix({
4953
+ eslintDisableSortImports: true,
4954
+ commentLines: [
4955
+ `Stub POM: ${options.className}`,
4956
+ "DO NOT MODIFY BY HAND",
4957
+ "",
4958
+ "This file is auto-generated by vue-pom-generator.",
4959
+ "Changes should be made in the generator/template, not in the generated output."
4960
+ ]
4961
+ });
4962
+ return renderSourceFile(`${options.className}.ts`, (sourceFile) => {
4963
+ addNamedImport(sourceFile, {
4964
+ moduleSpecifier: "@playwright/test",
4965
+ isTypeOnly: true,
4966
+ namedImports: [{ name: "Page", alias: "PwPage" }]
4967
+ });
4968
+ addNamedImport(sourceFile, {
4969
+ moduleSpecifier: options.basePageImportSpecifier,
4970
+ namedImports: ["BasePage"]
4971
+ });
4972
+ for (const childImport of options.childImports) {
4973
+ addNamedImport(sourceFile, {
4974
+ moduleSpecifier: childImport.importPath,
4975
+ namedImports: [childImport.className]
4976
+ });
4977
+ }
4978
+ sourceFile.addStatements(buildCommentBlock([
4979
+ "Stub POM generated because it is referenced as a navigation target but",
4980
+ "did not have any generated test ids in this build."
4981
+ ]).trimEnd());
4982
+ const classDeclaration = sourceFile.addClass({
4983
+ name: options.className,
4984
+ isExported: true,
4985
+ extends: "BasePage"
4986
+ });
4987
+ for (const member of options.members) {
4988
+ addClassMember(classDeclaration, member);
4989
+ }
4990
+ }, { prefixText });
4991
+ }
4992
+ function getChildImportSpecifiers(outputDir, childClassNames, generatedTsFilePathByComponent) {
4993
+ return childClassNames.map((childClassName) => {
4994
+ const childFilePath = generatedTsFilePathByComponent.get(childClassName);
4995
+ if (!childFilePath) {
4996
+ return null;
4997
+ }
4998
+ return {
4999
+ className: childClassName,
5000
+ importPath: stripExtension(toPosixRelativePath(outputDir, childFilePath))
5001
+ };
5002
+ }).filter((entry) => !!entry).sort((a, b) => a.className.localeCompare(b.className));
5003
+ }
5004
+ function isConstructorMember(member) {
5005
+ return member.kind === StructureKind.Constructor;
5006
+ }
5007
+ function isGetterMember(member) {
5008
+ return member.kind === StructureKind.GetAccessor;
5009
+ }
5010
+ function isMethodMember(member) {
5011
+ return member.kind === StructureKind.Method;
5012
+ }
5013
+ function isPropertyMember(member) {
5014
+ return member.kind === StructureKind.Property;
5015
+ }
5016
+ function addClassMember(classDeclaration, member) {
5017
+ if (isConstructorMember(member)) {
5018
+ classDeclaration.addConstructor(member);
5019
+ return;
5020
+ }
5021
+ if (isGetterMember(member)) {
5022
+ classDeclaration.addGetAccessor(member);
5023
+ return;
5024
+ }
5025
+ if (isMethodMember(member)) {
5026
+ classDeclaration.addMethod(member);
5027
+ return;
5028
+ }
5029
+ if (isPropertyMember(member)) {
5030
+ classDeclaration.addProperty(member);
5031
+ return;
5032
+ }
5033
+ throw new Error(`Unsupported class member structure: ${String(member)}`);
5034
+ }
5035
+ function getRuntimeGeneratedAssetSpecs(baseDir, basePageClassPath) {
5036
+ const runtimeDirAbs = path.join(baseDir, "_pom-runtime");
5037
+ const runtimeClassGenAbs = path.join(runtimeDirAbs, "class-generation");
5038
+ const runtimeClassGenSourceDir = resolvePluginAsset("../class-generation");
5039
+ const runtimeClassGenFiles = fs.readdirSync(runtimeClassGenSourceDir).filter((file) => file.endsWith(".ts")).filter((file) => file !== "BasePage.ts" && file !== "index.ts").sort((left, right) => left.localeCompare(right));
5040
+ return [
5041
+ {
5042
+ absolutePath: resolvePluginAsset("../click-instrumentation.ts"),
5043
+ description: "click-instrumentation.ts",
5044
+ outputPath: path.join(runtimeDirAbs, "click-instrumentation.ts")
5045
+ },
5046
+ ...runtimeClassGenFiles.map((file) => ({
5047
+ absolutePath: path.join(runtimeClassGenSourceDir, file),
5048
+ description: file,
5049
+ outputPath: path.join(runtimeClassGenAbs, file)
5050
+ })),
5051
+ {
5052
+ absolutePath: basePageClassPath,
5053
+ description: "BasePage.ts",
5054
+ outputPath: path.join(runtimeClassGenAbs, "BasePage.ts")
5055
+ }
5056
+ ];
5057
+ }
5058
+ function buildRuntimeGeneratedFiles(baseDir, basePageClassPath) {
5059
+ return buildRuntimeGeneratedFilesFromSpecs(getRuntimeGeneratedAssetSpecs(baseDir, basePageClassPath));
5060
+ }
5061
+ function buildRuntimeGeneratedFilesFromSpecs(assetSpecs) {
5062
+ return assetSpecs.map((spec) => ({
5063
+ filePath: spec.outputPath,
5064
+ content: readTextAsset(spec.absolutePath, spec.description)
5065
+ }));
5066
+ }
5067
+ function resolveCustomPomImportResolution(generatedClassNames, projectRoot, options = {}) {
5068
+ const importAliases = {
5069
+ Toggle: "ToggleWidget",
5070
+ Checkbox: "CheckboxWidget",
5071
+ ...options.customPomImportAliases
5072
+ };
5073
+ const importCollisionBehavior = options.customPomImportNameCollisionBehavior ?? "error";
5074
+ const reservedIdentifiers = /* @__PURE__ */ new Set([
5075
+ "PwLocator",
5076
+ "PwPage",
5077
+ "BasePage",
5078
+ "Fluent",
5079
+ ...generatedClassNames
5080
+ ]);
5081
+ const usedImportIdentifiers = /* @__PURE__ */ new Set();
5082
+ const classIdentifierMap = {};
5083
+ const methodSignaturesByClass = /* @__PURE__ */ new Map();
5084
+ const importSpecifiersByClass = {};
5085
+ const ensureUniqueIdentifier = (base) => {
5086
+ let candidate = base;
5087
+ let i = 2;
5088
+ while (reservedIdentifiers.has(candidate) || usedImportIdentifiers.has(candidate)) {
5089
+ candidate = `${base}${i}`;
5090
+ i++;
5091
+ }
5092
+ usedImportIdentifiers.add(candidate);
5093
+ return candidate;
5094
+ };
5095
+ const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
5096
+ const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
5097
+ if (!fs.existsSync(customDirAbs)) {
5098
+ return {
5099
+ classIdentifierMap,
5100
+ methodSignaturesByClass,
5101
+ availableClassIdentifiers: /* @__PURE__ */ new Set(),
5102
+ importSpecifiersByClass
5103
+ };
5104
+ }
5105
+ const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
5106
+ for (const file of files) {
5107
+ const exportName = file.replace(/\.ts$/i, "");
5108
+ const requested = importAliases[exportName] ?? exportName;
5109
+ const collidesWithGeneratedClass = generatedClassNames.has(requested);
5110
+ const explicitAliasProvided = Object.prototype.hasOwnProperty.call(importAliases, exportName);
5111
+ if (collidesWithGeneratedClass && importCollisionBehavior === "error") {
5112
+ throw createCustomPomImportCollisionError(exportName, requested);
5113
+ }
5114
+ let localIdentifier = requested;
5115
+ if (collidesWithGeneratedClass && importCollisionBehavior === "alias") {
5116
+ const aliasBase = explicitAliasProvided ? requested : `${exportName}Custom`;
5117
+ localIdentifier = ensureUniqueIdentifier(aliasBase);
5118
+ } else {
5119
+ localIdentifier = ensureUniqueIdentifier(requested);
5120
+ }
5121
+ const customFileAbs = path.join(customDirAbs, file);
5122
+ classIdentifierMap[exportName] = localIdentifier;
5123
+ importSpecifiersByClass[exportName] = {
5124
+ exportName,
5125
+ localIdentifier,
5126
+ absolutePath: customFileAbs
5127
+ };
5128
+ const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
5129
+ if (customPomMethodSignatures.size > 0) {
5130
+ methodSignaturesByClass.set(exportName, customPomMethodSignatures);
5131
+ }
5132
+ }
5133
+ return {
5134
+ classIdentifierMap,
5135
+ methodSignaturesByClass,
5136
+ availableClassIdentifiers: new Set(Object.values(classIdentifierMap)),
5137
+ importSpecifiersByClass
5138
+ };
5139
+ }
5140
+ function getComposedStubBody(targetClassName, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot) {
5141
+ const filePath = resolveVueSourcePath(targetClassName, vueFilesPathMap, projectRoot);
5142
+ if (!filePath)
5143
+ return void 0;
5144
+ let source = "";
5145
+ try {
5146
+ source = fs.readFileSync(filePath, "utf8");
5147
+ } catch {
5148
+ return void 0;
5149
+ }
5150
+ const tags = getComponentClassNamesFromVueSource(source);
5151
+ const childClassNames = Array.from(
5152
+ new Set(
5153
+ tags.filter((name) => availableClassNames.has(name)).filter((name) => name !== targetClassName)
5154
+ )
5155
+ ).sort((a, b) => a.localeCompare(b));
5156
+ if (!childClassNames.length)
5157
+ return void 0;
5158
+ const methodToChildren = /* @__PURE__ */ new Map();
5159
+ for (const child of childClassNames) {
5160
+ const childDeps = depsByClassName.get(child);
5161
+ const methods = childDeps?.generatedMethods;
5162
+ if (!methods)
5163
+ continue;
5164
+ for (const [name, sig] of methods.entries()) {
5165
+ if (!sig)
5166
+ continue;
5167
+ const list = methodToChildren.get(name) ?? [];
5168
+ list.push({ child, params: sig.params, argNames: sig.argNames });
5169
+ methodToChildren.set(name, list);
5170
+ }
5171
+ }
5172
+ const passthroughMembers = [];
5173
+ for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
5174
+ if (candidatesForMethod.length !== 1 || methodName === "constructor")
5175
+ continue;
5176
+ const { child, params, argNames } = candidatesForMethod[0];
5177
+ const callArgs = argNames.join(", ");
5178
+ passthroughMembers.push(createClassMethod({
5179
+ name: methodName,
5180
+ isAsync: true,
5181
+ parameters: parseParameterSignatures(params),
5182
+ statements: [
5183
+ `return await this.${child}.${methodName}(${callArgs});`
5184
+ ]
5185
+ }));
5186
+ }
5187
+ return {
5188
+ childClassNames,
5189
+ members: [
5190
+ ...childClassNames.map((childClassName) => createClassProperty({
5191
+ name: childClassName,
5192
+ type: childClassName
5193
+ })),
5194
+ createClassConstructor({
5195
+ parameters: [{ name: "page", type: "PwPage" }],
5196
+ statements: (writer) => {
5197
+ writer.writeLine("super(page);");
5198
+ for (const childClassName of childClassNames) {
5199
+ writer.writeLine(`this.${childClassName} = new ${childClassName}(page);`);
5200
+ }
5201
+ }
5202
+ }),
5203
+ ...passthroughMembers
5204
+ ]
5205
+ };
5206
+ }
4308
5207
  async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, basePageClassPath, outDir, options = {}) {
4309
5208
  const projectRoot = options.projectRoot ?? process.cwd();
4310
5209
  const entries = Array.from(componentHierarchyMap.entries()).sort((a, b) => a[0].localeCompare(b[0]));
4311
5210
  const views = entries.filter(([, d]) => d.isView);
4312
5211
  const components = entries.filter(([, d]) => !d.isView);
4313
- const makeAggregatedContent = (header2, outputDir, items) => {
5212
+ const makeAggregatedContent = (outputDir, items) => {
4314
5213
  const imports = [];
4315
5214
  const generatedClassNames = new Set(items.map(([name]) => name));
4316
5215
  if (!basePageClassPath) {
@@ -4325,84 +5224,22 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
4325
5224
  imports.push(`export * from "${runtimeClassGenRel}/playwright-types";`);
4326
5225
  imports.push(`export * from "${runtimeClassGenRel}/Pointer";`);
4327
5226
  imports.push(`export * from "${runtimeClassGenRel}/BasePage";`);
4328
- const addCustomPomImports = () => {
4329
- const importAliases = {
4330
- Toggle: "ToggleWidget",
4331
- Checkbox: "CheckboxWidget",
4332
- ...options.customPomImportAliases
4333
- };
4334
- const importCollisionBehavior = options.customPomImportNameCollisionBehavior ?? "error";
4335
- const reservedIdentifiers = /* @__PURE__ */ new Set([
4336
- "PwLocator",
4337
- "PwPage",
4338
- "BasePage",
4339
- "Fluent",
4340
- ...generatedClassNames
4341
- ]);
4342
- const usedImportIdentifiers = /* @__PURE__ */ new Set();
4343
- const customPomClassIdentifierMap2 = {};
4344
- const customPomMethodSignaturesByClass2 = /* @__PURE__ */ new Map();
4345
- const ensureUniqueIdentifier = (base2) => {
4346
- let candidate = base2;
4347
- let i = 2;
4348
- while (reservedIdentifiers.has(candidate) || usedImportIdentifiers.has(candidate)) {
4349
- candidate = `${base2}${i}`;
4350
- i++;
4351
- }
4352
- usedImportIdentifiers.add(candidate);
4353
- return candidate;
4354
- };
4355
- const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
4356
- const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
4357
- if (!fs.existsSync(customDirAbs)) {
4358
- return {
4359
- classIdentifierMap: customPomClassIdentifierMap2,
4360
- methodSignaturesByClass: customPomMethodSignaturesByClass2
4361
- };
4362
- }
4363
- const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
4364
- for (const file of files) {
4365
- const exportName = file.replace(/\.ts$/i, "");
4366
- const requested = importAliases[exportName] ?? exportName;
4367
- const collidesWithGeneratedClass = generatedClassNames.has(requested);
4368
- const explicitAliasProvided = Object.prototype.hasOwnProperty.call(importAliases, exportName);
4369
- if (collidesWithGeneratedClass && importCollisionBehavior === "error") {
4370
- throw new Error(
4371
- `[vue-pom-generator] Custom POM import name collision detected for "${exportName}".
4372
- The identifier "${requested}" conflicts with a generated POM class.
4373
- Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] to a unique name, or set generation.playwright.customPoms.importNameCollisionBehavior = "alias" to auto-alias collisions.`
4374
- );
4375
- }
4376
- let localIdentifier = requested;
4377
- if (collidesWithGeneratedClass && importCollisionBehavior === "alias") {
4378
- const aliasBase = explicitAliasProvided ? requested : `${exportName}Custom`;
4379
- localIdentifier = ensureUniqueIdentifier(aliasBase);
4380
- } else {
4381
- localIdentifier = ensureUniqueIdentifier(requested);
4382
- }
4383
- const customFileAbs = path.join(customDirAbs, file);
4384
- customPomClassIdentifierMap2[exportName] = localIdentifier;
4385
- const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
4386
- if (customPomMethodSignatures.size > 0) {
4387
- customPomMethodSignaturesByClass2.set(exportName, customPomMethodSignatures);
4388
- }
4389
- const fromOutputDir = outputDir;
4390
- const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
4391
- if (localIdentifier !== exportName) {
4392
- imports.push(`import { ${exportName} as ${localIdentifier} } from "${importPath}";`);
4393
- } else {
4394
- imports.push(`import { ${exportName} } from "${importPath}";`);
4395
- }
5227
+ const customPomImportResolution = resolveCustomPomImportResolution(generatedClassNames, projectRoot, {
5228
+ customPomDir: options.customPomDir,
5229
+ customPomImportAliases: options.customPomImportAliases,
5230
+ customPomImportNameCollisionBehavior: options.customPomImportNameCollisionBehavior
5231
+ });
5232
+ const customPomClassIdentifierMap = customPomImportResolution.classIdentifierMap;
5233
+ const customPomMethodSignaturesByClass = customPomImportResolution.methodSignaturesByClass;
5234
+ const customPomAvailableClassIdentifiers = customPomImportResolution.availableClassIdentifiers;
5235
+ for (const importSpecifier of Object.values(customPomImportResolution.importSpecifiersByClass).sort((left, right) => left.exportName.localeCompare(right.exportName))) {
5236
+ const importPath = stripExtension(toPosixRelativePath(outputDir, importSpecifier.absolutePath));
5237
+ if (importSpecifier.localIdentifier !== importSpecifier.exportName) {
5238
+ imports.push(`import { ${importSpecifier.exportName} as ${importSpecifier.localIdentifier} } from "${importPath}";`);
5239
+ continue;
4396
5240
  }
4397
- return {
4398
- classIdentifierMap: customPomClassIdentifierMap2,
4399
- methodSignaturesByClass: customPomMethodSignaturesByClass2
4400
- };
4401
- };
4402
- const customPomImportResolution = addCustomPomImports();
4403
- const customPomClassIdentifierMap = customPomImportResolution?.classIdentifierMap ?? {};
4404
- const customPomMethodSignaturesByClass = customPomImportResolution?.methodSignaturesByClass ?? /* @__PURE__ */ new Map();
4405
- const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap));
5241
+ imports.push(`import { ${importSpecifier.exportName} } from "${importPath}";`);
5242
+ }
4406
5243
  const referencedTargets = /* @__PURE__ */ new Set();
4407
5244
  for (const [, deps] of items) {
4408
5245
  for (const dt of deps.dataTestIdSet) {
@@ -4414,145 +5251,18 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4414
5251
  const stubTargets = Array.from(referencedTargets).filter((t) => !generatedClassNames.has(t)).sort((a, b) => a.localeCompare(b));
4415
5252
  const availableClassNames = /* @__PURE__ */ new Set([...generatedClassNames, ...stubTargets]);
4416
5253
  const depsByClassName = new Map(entries);
4417
- const scanPascalCaseTags = (template) => {
4418
- const names = [];
4419
- const len = template.length;
4420
- let i = 0;
4421
- while (i < len) {
4422
- const ch = template[i];
4423
- if (ch !== "<") {
4424
- i++;
4425
- continue;
4426
- }
4427
- i++;
4428
- if (i >= len)
4429
- break;
4430
- if (template[i] === "/" || template[i] === "!" || template[i] === "?") {
4431
- i++;
4432
- continue;
4433
- }
4434
- while (i < len && (template[i] === " " || template[i] === "\n" || template[i] === " " || template[i] === "\r")) i++;
4435
- if (i >= len)
4436
- break;
4437
- const first = template[i];
4438
- if (first < "A" || first > "Z") {
4439
- continue;
4440
- }
4441
- const start = i;
4442
- i++;
4443
- while (i < len) {
4444
- const c = template[i];
4445
- const isLetter = c >= "A" && c <= "Z" || c >= "a" && c <= "z";
4446
- const isDigit = c >= "0" && c <= "9";
4447
- const isUnderscore = c === "_";
4448
- if (isLetter || isDigit || isUnderscore) {
4449
- i++;
4450
- continue;
4451
- }
4452
- break;
4453
- }
4454
- const name = template.slice(start, i);
4455
- if (name)
4456
- names.push(name);
4457
- }
4458
- return Array.from(new Set(names));
4459
- };
4460
- const getComposedStubBody = (targetClassName) => {
4461
- const mapped = vueFilesPathMap.get(targetClassName);
4462
- const candidates = [
4463
- mapped,
4464
- path.join(projectRoot, "src", "views", `${targetClassName}.vue`),
4465
- path.join(projectRoot, "src", "components", `${targetClassName}.vue`)
4466
- ].filter((p) => typeof p === "string" && p.length > 0);
4467
- const filePath = candidates.find((p) => fs.existsSync(p));
4468
- if (!filePath)
4469
- return void 0;
4470
- let source = "";
4471
- try {
4472
- source = fs.readFileSync(filePath, "utf8");
4473
- } catch {
4474
- return void 0;
4475
- }
4476
- const templateOpen = source.indexOf("<template");
4477
- const templateClose = source.lastIndexOf("</template>");
4478
- if (templateOpen === -1 || templateClose === -1 || templateClose <= templateOpen)
4479
- return void 0;
4480
- const afterOpenTag = source.indexOf(">", templateOpen);
4481
- if (afterOpenTag === -1 || afterOpenTag >= templateClose)
4482
- return void 0;
4483
- const template = source.slice(afterOpenTag + 1, templateClose);
4484
- if (!template)
4485
- return void 0;
4486
- const tags = scanPascalCaseTags(template);
4487
- const childClassNames = Array.from(
4488
- new Set(
4489
- tags.filter((name) => availableClassNames.has(name)).filter((name) => name !== targetClassName)
4490
- )
4491
- ).sort((a, b) => a.localeCompare(b));
4492
- if (!childClassNames.length)
4493
- return void 0;
4494
- const methodToChildren = /* @__PURE__ */ new Map();
4495
- for (const child of childClassNames) {
4496
- const childDeps = depsByClassName.get(child);
4497
- const methods = childDeps?.generatedMethods;
4498
- if (!methods)
4499
- continue;
4500
- for (const [name, sig] of methods.entries()) {
4501
- if (!sig)
4502
- continue;
4503
- const list = methodToChildren.get(name) ?? [];
4504
- list.push({ child, params: sig.params, argNames: sig.argNames });
4505
- methodToChildren.set(name, list);
4506
- }
4507
- }
4508
- const passthroughLines = [];
4509
- for (const [methodName, candidatesForMethod] of methodToChildren.entries()) {
4510
- if (candidatesForMethod.length !== 1)
4511
- continue;
4512
- if (methodName === "constructor")
4513
- continue;
4514
- const { child, params, argNames } = candidatesForMethod[0];
4515
- const callArgs = argNames.join(", ");
4516
- passthroughLines.push(
4517
- "",
4518
- ` async ${methodName}(${params}) {`,
4519
- ` return await this.${child}.${methodName}(${callArgs});`,
4520
- " }"
4521
- );
4522
- }
4523
- return {
4524
- childClassNames,
4525
- lines: [
4526
- ...childClassNames.map((c) => ` ${c}: ${c};`),
4527
- "",
4528
- " constructor(page: PwPage) {",
4529
- " super(page);",
4530
- ...childClassNames.map((c) => ` this.${c} = new ${c}(page);`),
4531
- " }",
4532
- ...passthroughLines
4533
- ]
4534
- };
4535
- };
4536
5254
  const stubs = stubTargets.map(
4537
5255
  (t) => (() => {
4538
- const composed = getComposedStubBody(t);
4539
- const body = composed?.lines ?? [
4540
- " constructor(page: PwPage) {",
4541
- " super(page);",
4542
- " }"
4543
- ];
4544
- return [
4545
- "/**\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 */",
4546
- `export class ${t} extends BasePage {`,
4547
- ...body,
4548
- "}"
4549
- ].join("\n");
5256
+ const composed = getComposedStubBody(t, availableClassNames, depsByClassName, vueFilesPathMap, projectRoot);
5257
+ return {
5258
+ className: t,
5259
+ members: composed?.members ?? getDefaultStubMembers(),
5260
+ isStub: true
5261
+ };
4550
5262
  })()
4551
5263
  );
4552
- const classes = items.map(
4553
- ([name, deps]) => generateViewObjectModelContent(name, deps, componentHierarchyMap, vueFilesPathMap, basePageClassPath, {
4554
- outputDir,
4555
- aggregated: true,
5264
+ const classes = items.map(([name, deps]) => {
5265
+ const prepared = prepareViewObjectModelClass(name, deps, componentHierarchyMap, {
4556
5266
  customPomAttachments: options.customPomAttachments ?? [],
4557
5267
  customPomClassIdentifierMap,
4558
5268
  customPomAvailableClassIdentifiers,
@@ -4560,67 +5270,70 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4560
5270
  testIdAttribute: options.testIdAttribute,
4561
5271
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4562
5272
  routeMetaByComponent: options.routeMetaByComponent
4563
- })
4564
- );
4565
- const baseContent = [
4566
- header2,
4567
- ...imports,
4568
- ...classes,
4569
- ...stubs.length ? ["", ...stubs] : []
4570
- ].filter(Boolean).join("\n\n");
4571
- return baseContent;
5273
+ });
5274
+ const sourceRel = toPosixRelativePath(outputDir, deps.filePath);
5275
+ const kind = deps.isView ? "Page" : "Component";
5276
+ return {
5277
+ className: prepared.className,
5278
+ doc: `/** ${kind} POM: ${name} (source: ${sourceRel}) */`,
5279
+ members: prepared.members,
5280
+ isStub: false
5281
+ };
5282
+ });
5283
+ const prefixText = buildFilePrefix({
5284
+ referenceLib: "es2015",
5285
+ eslintDisableSortImports: true,
5286
+ commentLines: [
5287
+ "Aggregated generated POMs",
5288
+ "DO NOT MODIFY BY HAND",
5289
+ "",
5290
+ "This file is auto-generated by vue-pom-generator.",
5291
+ "Changes should be made in the generator/template, not in the generated output."
5292
+ ]
5293
+ });
5294
+ return renderSourceFile("page-object-models.g.ts", (sourceFile) => {
5295
+ for (const line of imports) {
5296
+ sourceFile.addStatements(line);
5297
+ }
5298
+ for (const entry of [...classes, ...stubs]) {
5299
+ if (entry.isStub) {
5300
+ sourceFile.addStatements(buildCommentBlock([
5301
+ "Stub POM generated because it is referenced as a navigation target but",
5302
+ "did not have any generated test ids in this build."
5303
+ ]).trimEnd());
5304
+ } else {
5305
+ sourceFile.addStatements(entry.doc);
5306
+ }
5307
+ const classDeclaration = sourceFile.addClass({
5308
+ name: entry.className,
5309
+ isExported: true,
5310
+ extends: "BasePage"
5311
+ });
5312
+ for (const member of entry.members) {
5313
+ addClassMember(classDeclaration, member);
5314
+ }
5315
+ }
5316
+ }, { prefixText });
4572
5317
  };
4573
5318
  const base = ensureDir(outDir);
4574
5319
  const outputFile = path.join(base, "page-object-models.g.ts");
4575
- const header = `/// <reference lib="es2015" />
4576
- ${eslintSuppressionHeader}/**
4577
- * Aggregated generated POMs
4578
- ${AUTO_GENERATED_COMMENT}`;
4579
- const content = makeAggregatedContent(header, path.dirname(outputFile), [...views, ...components]);
5320
+ const content = makeAggregatedContent(path.dirname(outputFile), [...views, ...components]);
4580
5321
  const indexFile = path.join(base, "index.ts");
4581
- const indexContent = `${eslintSuppressionHeader}/**
4582
- * POM exports
4583
- ${AUTO_GENERATED_COMMENT}
4584
-
4585
- export * from "./page-object-models.g";
4586
- `;
4587
- const runtimeDirAbs = path.join(base, "_pom-runtime");
4588
- const runtimeClassGenAbs = path.join(runtimeDirAbs, "class-generation");
4589
- const readText = (absPath, description) => {
4590
- try {
4591
- return fs.readFileSync(absPath, "utf8");
4592
- } catch {
4593
- throw new Error(`Failed to read ${description} at ${absPath}`);
4594
- }
4595
- };
4596
- const resolvePluginAsset = (relative) => {
4597
- try {
4598
- return fileURLToPath(new URL(relative, import.meta.url));
4599
- } catch {
4600
- return path.resolve(__dirname, relative);
4601
- }
4602
- };
4603
- const clickInstrumentationAbs = resolvePluginAsset("../click-instrumentation.ts");
4604
- const pointerAbs = resolvePluginAsset("../class-generation/Pointer.ts");
4605
- const playwrightTypesAbs = resolvePluginAsset("../class-generation/playwright-types.ts");
4606
- const runtimeFiles = [
4607
- {
4608
- filePath: path.join(runtimeDirAbs, "click-instrumentation.ts"),
4609
- content: readText(clickInstrumentationAbs, "click-instrumentation.ts")
4610
- },
4611
- {
4612
- filePath: path.join(runtimeClassGenAbs, "Pointer.ts"),
4613
- content: readText(pointerAbs, "Pointer.ts")
4614
- },
4615
- {
4616
- filePath: path.join(runtimeClassGenAbs, "playwright-types.ts"),
4617
- content: readText(playwrightTypesAbs, "playwright-types.ts")
4618
- },
4619
- {
4620
- filePath: path.join(runtimeClassGenAbs, "BasePage.ts"),
4621
- content: readText(basePageClassPath, "BasePage.ts")
4622
- }
4623
- ];
5322
+ const indexContent = renderSourceFile("index.ts", (sourceFile) => {
5323
+ addExportAll(sourceFile, "./page-object-models.g");
5324
+ }, {
5325
+ prefixText: buildFilePrefix({
5326
+ eslintDisableSortImports: true,
5327
+ commentLines: [
5328
+ "POM exports",
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
+ });
5336
+ const runtimeFiles = buildRuntimeGeneratedFiles(base, basePageClassPath);
4624
5337
  return [
4625
5338
  { filePath: outputFile, content },
4626
5339
  { filePath: indexFile, content: indexContent },
@@ -4710,48 +5423,50 @@ function getWidgetInstancesForView(componentName, dataTestIdSet, availableClassI
4710
5423
  return out;
4711
5424
  }
4712
5425
  function getComponentInstances(childrenComponent, componentHierarchyMap, attachmentsForThisView = [], widgetInstances = []) {
4713
- let content = "\n";
5426
+ const declarations = [];
4714
5427
  for (const a of attachmentsForThisView) {
4715
- content += ` ${a.propertyName}: ${a.className};
4716
- `;
5428
+ declarations.push(createClassProperty({
5429
+ name: a.propertyName,
5430
+ type: a.className
5431
+ }));
4717
5432
  }
4718
5433
  for (const w of widgetInstances) {
4719
- content += ` ${w.propertyName}: ${w.className};
4720
- `;
5434
+ declarations.push(createClassProperty({
5435
+ name: w.propertyName,
5436
+ type: w.className
5437
+ }));
4721
5438
  }
4722
5439
  childrenComponent.forEach((child) => {
4723
5440
  if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
4724
5441
  const childName = child.split(".vue")[0];
4725
- content += ` ${childName}: ${childName};
4726
- `;
5442
+ declarations.push(createClassProperty({
5443
+ name: childName,
5444
+ type: childName
5445
+ }));
4727
5446
  }
4728
5447
  });
4729
- return `${content}
4730
- `;
5448
+ return declarations;
4731
5449
  }
4732
5450
  function getConstructor(childrenComponent, componentHierarchyMap, attachmentsForThisView = [], widgetInstances = [], options) {
4733
- let content = " constructor(page: PwPage) {\n";
4734
5451
  const attr = (options?.testIdAttribute ?? "data-testid").trim() || "data-testid";
4735
- content += ` super(page, { testIdAttribute: ${JSON.stringify(attr)} });
4736
- `;
4737
- for (const a of attachmentsForThisView) {
4738
- content += ` this.${a.propertyName} = new ${a.className}(page, this);
4739
- `;
4740
- }
4741
- for (const w of widgetInstances) {
4742
- content += ` this.${w.propertyName} = new ${w.className}(page, ${JSON.stringify(w.testId)});
4743
- `;
4744
- }
4745
- childrenComponent.forEach((child) => {
4746
- if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
4747
- const childName = child.split(".vue")[0];
4748
- content += ` this.${childName} = new ${childName}(page);
4749
- `;
5452
+ return createClassConstructor({
5453
+ parameters: [{ name: "page", type: "PwPage" }],
5454
+ statements: (writer) => {
5455
+ writer.writeLine(`super(page, { testIdAttribute: ${JSON.stringify(attr)} });`);
5456
+ for (const a of attachmentsForThisView) {
5457
+ writer.writeLine(`this.${a.propertyName} = new ${a.className}(page, this);`);
5458
+ }
5459
+ for (const w of widgetInstances) {
5460
+ writer.writeLine(`this.${w.propertyName} = new ${w.className}(page, ${JSON.stringify(w.testId)});`);
5461
+ }
5462
+ childrenComponent.forEach((child) => {
5463
+ if (componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size) {
5464
+ const childName = child.split(".vue")[0];
5465
+ writer.writeLine(`this.${childName} = new ${childName}(page);`);
5466
+ }
5467
+ });
4750
5468
  }
4751
5469
  });
4752
- content += " }";
4753
- return `${content}
4754
- `;
4755
5470
  }
4756
5471
  const TESTID_CLICK_EVENT_NAME = "__testid_event__";
4757
5472
  const TESTID_CLICK_EVENT_STRICT_FLAG = "__testid_click_event_strict__";
@@ -5396,6 +6111,7 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5396
6111
  const existingIdBehavior = options.existingIdBehavior ?? "preserve";
5397
6112
  const testIdAttribute = (options.testIdAttribute || "data-testid").trim() || "data-testid";
5398
6113
  const nameCollisionBehavior = options.nameCollisionBehavior ?? "suffix";
6114
+ const missingSemanticNameBehavior = options.missingSemanticNameBehavior ?? "ignore";
5399
6115
  const warn = options.warn;
5400
6116
  const vueFilesPathMap = options.vueFilesPathMap;
5401
6117
  const wrapperSearchRoots = options.wrapperSearchRoots ?? [];
@@ -5661,6 +6377,22 @@ function createTestIdTransform(componentName, componentHierarchyMap, nativeWrapp
5661
6377
  });
5662
6378
  };
5663
6379
  const { nativeWrappersValue, optionDataTestIdPrefixValue, semanticNameHint } = getNativeWrapperTransformInfo(element, componentName, nativeWrappers);
6380
+ const handlerDirective = element.props.find((p) => {
6381
+ return p.type === NodeTypes.DIRECTIVE && p.name === "bind" && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === "handler" && !!p.exp;
6382
+ }) ?? null;
6383
+ const handlerInfo = handlerDirective ? nodeHandlerAttributeInfo(element) : null;
6384
+ if (missingSemanticNameBehavior === "error" && nativeWrappers[element.tag]?.role === "button" && handlerDirective && !handlerInfo) {
6385
+ const loc = element.loc?.start;
6386
+ const locationHint = loc ? `${loc.line}:${loc.column}` : "unknown";
6387
+ const handlerSource = (handlerDirective.exp?.loc?.source ?? "").trim() || "<unknown>";
6388
+ throw new Error(
6389
+ `[vue-pom-generator] Could not derive a semantic POM action name for button-like wrapper in ${componentName} (${context.filename ?? "unknown"}:${locationHint}).
6390
+ Element: <${element.tag}>
6391
+ Handler: ${handlerSource}
6392
+
6393
+ 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.`
6394
+ );
6395
+ }
5664
6396
  if (nativeWrappersValue) {
5665
6397
  if (optionDataTestIdPrefixValue) {
5666
6398
  const existing = existingIdBehavior === "preserve" ? tryGetExistingElementDataTestId(element, testIdAttribute) : null;
@@ -5770,7 +6502,6 @@ Fix: remove the explicit ${attrLabel}, or change existingIdBehavior to "overwrit
5770
6502
  });
5771
6503
  return;
5772
6504
  }
5773
- const handlerInfo = nodeHandlerAttributeInfo(element);
5774
6505
  if (handlerInfo) {
5775
6506
  const testId = getHandlerAttributeValueDataTestId(handlerInfo.semanticNameHint);
5776
6507
  applyResolvedDataTestIdForElement({
@@ -5870,6 +6601,7 @@ function createBuildProcessorPlugin(options) {
5870
6601
  normalizedBasePagePath,
5871
6602
  outDir,
5872
6603
  emitLanguages,
6604
+ typescriptOutputStructure,
5873
6605
  csharp,
5874
6606
  generateFixtures,
5875
6607
  customPomAttachments,
@@ -5879,6 +6611,7 @@ function createBuildProcessorPlugin(options) {
5879
6611
  customPomImportNameCollisionBehavior,
5880
6612
  testIdAttribute,
5881
6613
  nameCollisionBehavior,
6614
+ missingSemanticNameBehavior,
5882
6615
  existingIdBehavior,
5883
6616
  nativeWrappers,
5884
6617
  excludedComponents,
@@ -5989,6 +6722,7 @@ function createBuildProcessorPlugin(options) {
5989
6722
  existingIdBehavior: existingIdBehavior ?? "preserve",
5990
6723
  testIdAttribute,
5991
6724
  nameCollisionBehavior,
6725
+ missingSemanticNameBehavior,
5992
6726
  warn: (message) => loggerRef.current.warn(message),
5993
6727
  vueFilesPathMap,
5994
6728
  wrapperSearchRoots: getWrapperSearchRoots()
@@ -6072,6 +6806,7 @@ function createBuildProcessorPlugin(options) {
6072
6806
  await generateFiles(componentHierarchyMap, vueFilesPathMap, normalizedBasePagePath, {
6073
6807
  outDir,
6074
6808
  emitLanguages,
6809
+ typescriptOutputStructure,
6075
6810
  csharp,
6076
6811
  generateFixtures,
6077
6812
  customPomAttachments,
@@ -6106,6 +6841,7 @@ function createDevProcessorPlugin(options) {
6106
6841
  basePageClassPath,
6107
6842
  outDir,
6108
6843
  emitLanguages,
6844
+ typescriptOutputStructure,
6109
6845
  csharp,
6110
6846
  generateFixtures,
6111
6847
  customPomAttachments,
@@ -6113,6 +6849,7 @@ function createDevProcessorPlugin(options) {
6113
6849
  customPomImportAliases,
6114
6850
  customPomImportNameCollisionBehavior,
6115
6851
  nameCollisionBehavior = "suffix",
6852
+ missingSemanticNameBehavior,
6116
6853
  existingIdBehavior,
6117
6854
  testIdAttribute,
6118
6855
  routerAwarePoms,
@@ -6277,6 +7014,7 @@ function createDevProcessorPlugin(options) {
6277
7014
  {
6278
7015
  existingIdBehavior: existingIdBehavior ?? "preserve",
6279
7016
  nameCollisionBehavior,
7017
+ missingSemanticNameBehavior,
6280
7018
  testIdAttribute,
6281
7019
  warn: (message) => loggerRef.current.warn(message),
6282
7020
  vueFilesPathMap: targetVuePathMap,
@@ -6317,6 +7055,7 @@ function createDevProcessorPlugin(options) {
6317
7055
  generateFiles(snapshotHierarchy, snapshotVuePathMap, normalizedBasePagePath, {
6318
7056
  outDir,
6319
7057
  emitLanguages,
7058
+ typescriptOutputStructure,
6320
7059
  csharp,
6321
7060
  generateFixtures,
6322
7061
  customPomAttachments,
@@ -6490,14 +7229,37 @@ function createDevProcessorPlugin(options) {
6490
7229
  };
6491
7230
  }
6492
7231
  function generateTestIdsModule(componentTestIds) {
6493
- const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([c, set]) => ` ${JSON.stringify(c)}: ${JSON.stringify(Array.from(set).sort())}`).join(",\n");
6494
- return `// Virtual module: test id manifest
6495
- export const testIdManifest = {
6496
- ${manifestEntries}
6497
- } as const;
6498
- export type TestIdManifest = typeof testIdManifest;
6499
- export type ComponentName = keyof TestIdManifest;
6500
- `;
7232
+ const manifestEntries = Array.from(componentTestIds.entries()).sort((a, b) => a[0].localeCompare(b[0]));
7233
+ return renderSourceFile("virtual-testids.ts", (sourceFile) => {
7234
+ sourceFile.addStatements("// Virtual module: test id manifest");
7235
+ sourceFile.addVariableStatement({
7236
+ declarationKind: VariableDeclarationKind.Const,
7237
+ isExported: true,
7238
+ declarations: [{
7239
+ name: "testIdManifest",
7240
+ initializer: (writer) => {
7241
+ writer.write("{").newLine();
7242
+ writer.indent(() => {
7243
+ manifestEntries.forEach(([componentName, testIds], index) => {
7244
+ const suffix = index === manifestEntries.length - 1 ? "" : ",";
7245
+ writer.writeLine(`${JSON.stringify(componentName)}: ${JSON.stringify(Array.from(testIds).sort())}${suffix}`);
7246
+ });
7247
+ });
7248
+ writer.write("} as const");
7249
+ }
7250
+ }]
7251
+ });
7252
+ sourceFile.addTypeAlias({
7253
+ isExported: true,
7254
+ name: "TestIdManifest",
7255
+ type: "typeof testIdManifest"
7256
+ });
7257
+ sourceFile.addTypeAlias({
7258
+ isExported: true,
7259
+ name: "ComponentName",
7260
+ type: "keyof TestIdManifest"
7261
+ });
7262
+ });
6501
7263
  }
6502
7264
  function createTestIdsVirtualModulesPlugin(componentTestIds) {
6503
7265
  const maybeModule = virtualImport;
@@ -6517,9 +7279,11 @@ function createSupportPlugins(options) {
6517
7279
  scanDirs,
6518
7280
  getWrapperSearchRoots,
6519
7281
  nameCollisionBehavior = "suffix",
7282
+ missingSemanticNameBehavior,
6520
7283
  existingIdBehavior,
6521
7284
  outDir,
6522
7285
  emitLanguages,
7286
+ typescriptOutputStructure,
6523
7287
  csharp,
6524
7288
  routerAwarePoms,
6525
7289
  routerEntry,
@@ -6563,6 +7327,7 @@ function createSupportPlugins(options) {
6563
7327
  normalizedBasePagePath,
6564
7328
  outDir,
6565
7329
  emitLanguages,
7330
+ typescriptOutputStructure,
6566
7331
  csharp,
6567
7332
  generateFixtures,
6568
7333
  customPomAttachments,
@@ -6572,6 +7337,7 @@ function createSupportPlugins(options) {
6572
7337
  customPomImportNameCollisionBehavior,
6573
7338
  testIdAttribute,
6574
7339
  nameCollisionBehavior,
7340
+ missingSemanticNameBehavior,
6575
7341
  existingIdBehavior,
6576
7342
  nativeWrappers,
6577
7343
  excludedComponents,
@@ -6593,6 +7359,7 @@ function createSupportPlugins(options) {
6593
7359
  basePageClassPath,
6594
7360
  outDir,
6595
7361
  emitLanguages,
7362
+ typescriptOutputStructure,
6596
7363
  csharp,
6597
7364
  generateFixtures,
6598
7365
  customPomAttachments,
@@ -6600,6 +7367,7 @@ function createSupportPlugins(options) {
6600
7367
  customPomImportAliases,
6601
7368
  customPomImportNameCollisionBehavior,
6602
7369
  nameCollisionBehavior,
7370
+ missingSemanticNameBehavior,
6603
7371
  existingIdBehavior,
6604
7372
  testIdAttribute,
6605
7373
  routerAwarePoms,
@@ -6974,6 +7742,41 @@ function assertNonEmptyStringArray(value, name) {
6974
7742
  assertNonEmptyString(entry, `${name}[${index}]`);
6975
7743
  }
6976
7744
  }
7745
+ function assertOneOf(value, allowed, name) {
7746
+ if (!value)
7747
+ return;
7748
+ if (allowed.includes(value)) {
7749
+ return;
7750
+ }
7751
+ throw new TypeError(`${name} must be one of: ${allowed.join(", ")}.`);
7752
+ }
7753
+ function assertErrorBehavior(value, name) {
7754
+ if (!value) {
7755
+ return;
7756
+ }
7757
+ if (value === "ignore" || value === "error") {
7758
+ return;
7759
+ }
7760
+ if (typeof value !== "object" || Array.isArray(value)) {
7761
+ throw new TypeError(`${name} must be "ignore", "error", or an object.`);
7762
+ }
7763
+ const supportedKeys = /* @__PURE__ */ new Set(["missingSemanticNameBehavior"]);
7764
+ for (const key of Object.keys(value)) {
7765
+ if (!supportedKeys.has(key)) {
7766
+ throw new TypeError(`${name} contains unsupported key "${key}".`);
7767
+ }
7768
+ }
7769
+ assertOneOf(value.missingSemanticNameBehavior, ["ignore", "error"], `${name}.missingSemanticNameBehavior`);
7770
+ }
7771
+ function resolveMissingSemanticNameBehavior(value) {
7772
+ if (!value) {
7773
+ return "ignore";
7774
+ }
7775
+ if (value === "ignore" || value === "error") {
7776
+ return value;
7777
+ }
7778
+ return value.missingSemanticNameBehavior ?? "ignore";
7779
+ }
6977
7780
  function assertRouterModuleShims(value, name) {
6978
7781
  if (!value)
6979
7782
  return;
@@ -7106,6 +7909,9 @@ function createVuePomGeneratorPlugins(options = {}) {
7106
7909
  const vuePluginOwnership = isNuxt ? "external" : options.vuePluginOwnership ?? "internal";
7107
7910
  const usesExternalVuePlugin = vuePluginOwnership === "external";
7108
7911
  const csharp = generationOptions?.csharp;
7912
+ const errorBehavior = options.errorBehavior;
7913
+ const missingSemanticNameBehavior = resolveMissingSemanticNameBehavior(errorBehavior);
7914
+ const typescriptOutputStructure = generationOptions?.playwright?.outputStructure ?? "aggregated";
7109
7915
  const generateFixtures = generationOptions?.playwright?.fixtures;
7110
7916
  const customPoms = generationOptions?.playwright?.customPoms;
7111
7917
  const resolvedCustomPomAttachments = customPoms?.attachments ?? [];
@@ -7137,8 +7943,10 @@ function createVuePomGeneratorPlugins(options = {}) {
7137
7943
  assertNonEmptyString(testIdAttribute, "[vue-pom-generator] injection.attribute");
7138
7944
  assertNonEmptyString(viewsDir, "[vue-pom-generator] injection.viewsDir");
7139
7945
  assertNonEmptyStringArray(wrapperSearchRoots, "[vue-pom-generator] injection.wrapperSearchRoots");
7946
+ assertErrorBehavior(errorBehavior, "[vue-pom-generator] errorBehavior");
7140
7947
  if (generationEnabled) {
7141
7948
  assertNonEmptyString(outDir, "[vue-pom-generator] generation.outDir");
7949
+ assertOneOf(typescriptOutputStructure, ["aggregated", "split"], "[vue-pom-generator] generation.playwright.outputStructure");
7142
7950
  assertRouterModuleShims(routerModuleShims, "[vue-pom-generator] generation.router.moduleShims");
7143
7951
  if (generationOptions?.router && routerType === "vue-router") {
7144
7952
  assertNonEmptyString(routerEntry, "[vue-pom-generator] generation.router.entry");
@@ -7148,7 +7956,7 @@ function createVuePomGeneratorPlugins(options = {}) {
7148
7956
  applyTemplateCompilerOptionsToResolvedVuePlugin(config, templateCompilerOptions, isNuxt ? "nuxt" : vuePluginOwnership);
7149
7957
  }
7150
7958
  loggerRef.current.info(`projectRoot=${projectRootRef.current}`);
7151
- loggerRef.current.info(`Active plugins: ${config.plugins.map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
7959
+ loggerRef.current.info(`Active plugins: ${(config.plugins ?? []).map((p) => p.name).filter((n) => n.includes("vue-pom")).join(", ")}`);
7152
7960
  }
7153
7961
  };
7154
7962
  const getViewsDirAbs = () => resolveFromProjectRoot(projectRootRef.current, viewsDir);
@@ -7182,9 +7990,11 @@ function createVuePomGeneratorPlugins(options = {}) {
7182
7990
  scanDirs,
7183
7991
  getWrapperSearchRoots: getWrapperSearchRootsAbs,
7184
7992
  nameCollisionBehavior,
7993
+ missingSemanticNameBehavior,
7185
7994
  existingIdBehavior,
7186
7995
  outDir,
7187
7996
  emitLanguages,
7997
+ typescriptOutputStructure,
7188
7998
  csharp,
7189
7999
  routerAwarePoms,
7190
8000
  routerEntry,