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