@immense/vue-pom-generator 1.0.38 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,10 +2,10 @@ import path from "node:path";
2
2
  import process from "node:process";
3
3
  import { pathToFileURL, fileURLToPath } from "node:url";
4
4
  import fs from "node:fs";
5
+ import { parseExpression, parse } from "@babel/parser";
5
6
  import { JSDOM } from "jsdom";
6
7
  import { NodeTypes, stringifyExpression, ConstantTypes, createSimpleExpression } from "@vue/compiler-core";
7
8
  import { isArrayExpression, isStringLiteral, isTemplateLiteral, isAssignmentExpression, isIdentifier, isMemberExpression, isCallExpression, isExpressionStatement, isArrowFunctionExpression, isOptionalMemberExpression, isObjectExpression, isFile, isBlockStatement, isOptionalCallExpression, isLogicalExpression, isConditionalExpression, isSequenceExpression, isAssignmentPattern, isRestElement, isObjectPattern, isObjectProperty, isProgram, VISITOR_KEYS, isBooleanLiteral, isNumericLiteral, isNullLiteral } from "@babel/types";
8
- import { parseExpression, parse } from "@babel/parser";
9
9
  import { performance } from "node:perf_hooks";
10
10
  import * as compilerDom from "@vue/compiler-dom";
11
11
  import { parse as parse$2 } from "@vue/compiler-dom";
@@ -2054,9 +2054,6 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2054
2054
  })();
2055
2055
  const tryMergeWithExistingPrimary = (candidateActionName) => {
2056
2056
  const mergeKey = (args.pomMergeKey ?? "").trim();
2057
- if (isKeyed) {
2058
- return false;
2059
- }
2060
2057
  const existingEntry = primaryByActionName.get(candidateActionName);
2061
2058
  const existingPom = existingEntry?.pom;
2062
2059
  if (!existingEntry || !existingPom) {
@@ -2067,6 +2064,9 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2067
2064
  ...existingPom.alternateFormattedDataTestIds ?? []
2068
2065
  ];
2069
2066
  const sharesSelectorIdentity = existingSelectors.includes(formattedDataTestIdForPom);
2067
+ if (isKeyed && !sharesSelectorIdentity) {
2068
+ return false;
2069
+ }
2070
2070
  if (!mergeKey && !sharesSelectorIdentity) {
2071
2071
  return false;
2072
2072
  }
@@ -2113,6 +2113,11 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2113
2113
  conflicts = false;
2114
2114
  }
2115
2115
  }
2116
+ if (conflicts && nameCollisionBehavior === "error" && tryMergeWithExistingPrimary(actionName)) {
2117
+ methodName = candidate;
2118
+ mergedIntoExisting = true;
2119
+ break;
2120
+ }
2116
2121
  if (conflicts && nameCollisionBehavior === "error") {
2117
2122
  const roleSuffix = upperFirst(normalizedRole || "Element");
2118
2123
  const baseNameUpper = upperFirst(baseWithSuffix);
@@ -2159,11 +2164,6 @@ Fix: either (1) include ${args.bestKeyPlaceholder} in your :${attrLabel} templat
2159
2164
  reservedMembers.add(actionName);
2160
2165
  break;
2161
2166
  }
2162
- if (nameCollisionBehavior === "error" && tryMergeWithExistingPrimary(actionName)) {
2163
- methodName = candidate;
2164
- mergedIntoExisting = true;
2165
- break;
2166
- }
2167
2167
  if (!collisionDetails) {
2168
2168
  collisionDetails = { getterName: chosenGetterName, actionName };
2169
2169
  collisionHint = hint || (args.semanticNameHint ?? "").trim() || null;
@@ -3740,6 +3740,7 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3740
3740
  chunks.push(" {");
3741
3741
  if (pom.formattedDataTestId.includes("${") || allTestIds.length <= 1) {
3742
3742
  chunks.push(` await ${locatorName}${pom.formattedDataTestId.includes("${") ? `(${args})` : ""}.ClickAsync();`);
3743
+ chunks.push(` return new ${target}(Page);`);
3743
3744
  } else {
3744
3745
  chunks.push(" Exception? lastError = null;");
3745
3746
  chunks.push(` foreach (var testId in new[] { ${allTestIds.map(toCSharpTestIdExpression).join(", ")} })`);
@@ -3760,7 +3761,6 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3760
3761
  chunks.push(" }");
3761
3762
  chunks.push(' throw lastError ?? new System.Exception("[pom] Failed to navigate using any candidate test id.");');
3762
3763
  }
3763
- chunks.push(` return new ${target}(Page);`);
3764
3764
  chunks.push(" }");
3765
3765
  chunks.push("");
3766
3766
  continue;
@@ -4017,6 +4017,7 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4017
4017
  };
4018
4018
  const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
4019
4019
  const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? /* @__PURE__ */ new Set();
4020
+ const customPomMethodSignaturesByClass = options.customPomMethodSignaturesByClass ?? /* @__PURE__ */ new Map();
4020
4021
  const attachmentsForThisClass = customPomAttachments.filter((a) => {
4021
4022
  if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
4022
4023
  return false;
@@ -4027,7 +4028,9 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4027
4028
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));
4028
4029
  }).map((a) => ({
4029
4030
  className: customPomClassIdentifierMap[a.className],
4030
- propertyName: a.propertyName
4031
+ propertyName: a.propertyName,
4032
+ flatten: a.flatten ?? false,
4033
+ methodSignatures: a.flatten ? customPomMethodSignaturesByClass.get(a.className) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map()
4031
4034
  }));
4032
4035
  let content = "";
4033
4036
  const sourceRel = toPosixRelativePath(outputDir, filePath);
@@ -4068,6 +4071,15 @@ export class ${className} extends BasePage {
4068
4071
  `;
4069
4072
  const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
4070
4073
  const componentRefsForInstances = isView ? usedComponentSet?.size ? usedComponentSet : childrenComponentSet : childrenComponentSet;
4074
+ const childInstancePropertyNames = Array.from(componentRefsForInstances).filter((child) => componentHierarchyMap.has(child) && componentHierarchyMap.get(child)?.dataTestIdSet.size).map((child) => child.split(".vue")[0]);
4075
+ const blockedViewPassthroughMethodNames = new Set(
4076
+ attachmentsForThisClass.filter((a) => a.flatten).flatMap((a) => Array.from(a.methodSignatures.keys()))
4077
+ );
4078
+ const reservedAttachmentPassthroughNames = /* @__PURE__ */ new Set([
4079
+ ...attachmentsForThisClass.map((a) => a.propertyName),
4080
+ ...widgetInstances.map((w) => w.propertyName),
4081
+ ...childInstancePropertyNames
4082
+ ]);
4071
4083
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
4072
4084
  content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
4073
4085
  content += getConstructor(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances, { testIdAttribute });
@@ -4076,8 +4088,9 @@ export class ${className} extends BasePage {
4076
4088
  content += getComponentInstances(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass);
4077
4089
  content += getConstructor(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute });
4078
4090
  }
4091
+ content += getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames);
4079
4092
  if (isView && componentRefsForInstances.size === 1) {
4080
- content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap);
4093
+ content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap, blockedViewPassthroughMethodNames);
4081
4094
  }
4082
4095
  if (isView && options.vueRouterFluentChaining) {
4083
4096
  const routeMeta = options.routeMetaByComponent?.[componentName] ?? null;
@@ -4088,7 +4101,7 @@ export class ${className} extends BasePage {
4088
4101
  content += "}\n";
4089
4102
  return content;
4090
4103
  }
4091
- function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap) {
4104
+ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap, blockedMethodNames = /* @__PURE__ */ new Set()) {
4092
4105
  const existingOnView = viewDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
4093
4106
  const methodToChildren = /* @__PURE__ */ new Map();
4094
4107
  for (const child of childrenComponentSet) {
@@ -4102,7 +4115,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
4102
4115
  for (const [name, sig] of methods.entries()) {
4103
4116
  if (!sig)
4104
4117
  continue;
4105
- if (existingOnView.has(name))
4118
+ if (existingOnView.has(name) || blockedMethodNames.has(name))
4106
4119
  continue;
4107
4120
  const list = methodToChildren.get(name) ?? [];
4108
4121
  list.push({ childProp, params: sig.params, argNames: sig.argNames });
@@ -4133,6 +4146,130 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
4133
4146
  ""
4134
4147
  ].join("\n");
4135
4148
  }
4149
+ function getAttachmentPassthroughMethods(ownerName, ownerDependencies, attachmentsForThisClass, reservedMemberNames) {
4150
+ if (!attachmentsForThisClass.some((a) => a.flatten && a.methodSignatures.size > 0)) {
4151
+ return "";
4152
+ }
4153
+ const existingOnClass = ownerDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
4154
+ const methodToAttachments = /* @__PURE__ */ new Map();
4155
+ for (const attachment of attachmentsForThisClass) {
4156
+ if (!attachment.flatten) {
4157
+ continue;
4158
+ }
4159
+ for (const [methodName, signature] of attachment.methodSignatures.entries()) {
4160
+ if (methodName === "constructor" || existingOnClass.has(methodName) || reservedMemberNames.has(methodName)) {
4161
+ continue;
4162
+ }
4163
+ const list = methodToAttachments.get(methodName) ?? [];
4164
+ list.push({
4165
+ propertyName: attachment.propertyName,
4166
+ params: signature.params,
4167
+ argNames: signature.argNames
4168
+ });
4169
+ methodToAttachments.set(methodName, list);
4170
+ }
4171
+ }
4172
+ const sorted = Array.from(methodToAttachments.entries()).sort((a, b) => a[0].localeCompare(b[0]));
4173
+ const lines = [];
4174
+ for (const [methodName, candidates] of sorted) {
4175
+ if (candidates.length !== 1) {
4176
+ continue;
4177
+ }
4178
+ const { propertyName, params, argNames } = candidates[0];
4179
+ const callArgs = argNames.join(", ");
4180
+ const invocation = callArgs ? `this.${propertyName}.${methodName}(${callArgs})` : `this.${propertyName}.${methodName}()`;
4181
+ lines.push(
4182
+ "",
4183
+ ` ${methodName}(${params}) {`,
4184
+ ` return ${invocation};`,
4185
+ " }"
4186
+ );
4187
+ }
4188
+ if (!lines.length) {
4189
+ return "";
4190
+ }
4191
+ return [
4192
+ "",
4193
+ ` // Passthrough methods composed from custom helper attachments of ${ownerName}.`,
4194
+ ...lines,
4195
+ ""
4196
+ ].join("\n");
4197
+ }
4198
+ function sliceNodeSource(source, node) {
4199
+ if (node.start == null || node.end == null) {
4200
+ return null;
4201
+ }
4202
+ const snippet = source.slice(node.start, node.end).trim();
4203
+ return snippet.length ? snippet : null;
4204
+ }
4205
+ function getCustomPomCallArgumentName(param) {
4206
+ if (param.type === "Identifier") {
4207
+ return param.name;
4208
+ }
4209
+ if (param.type === "AssignmentPattern") {
4210
+ return param.left.type === "Identifier" ? param.left.name : null;
4211
+ }
4212
+ if (param.type === "RestElement") {
4213
+ return param.argument.type === "Identifier" ? `...${param.argument.name}` : null;
4214
+ }
4215
+ return null;
4216
+ }
4217
+ function extractCustomPomMethodSignatures(source, exportName) {
4218
+ const signatures = /* @__PURE__ */ new Map();
4219
+ let ast;
4220
+ try {
4221
+ ast = parse(source, {
4222
+ sourceType: "module",
4223
+ plugins: ["typescript", "jsx"]
4224
+ });
4225
+ } catch {
4226
+ return signatures;
4227
+ }
4228
+ for (const statement of ast.program.body) {
4229
+ if (statement.type !== "ExportNamedDeclaration" || !statement.declaration || statement.declaration.type !== "ClassDeclaration") {
4230
+ continue;
4231
+ }
4232
+ const declaration = statement.declaration;
4233
+ if (declaration.id?.name !== exportName) {
4234
+ continue;
4235
+ }
4236
+ for (const member of declaration.body.body) {
4237
+ if (member.type !== "ClassMethod" || member.kind !== "method" || member.static || member.computed) {
4238
+ continue;
4239
+ }
4240
+ if (member.accessibility === "private" || member.accessibility === "protected") {
4241
+ continue;
4242
+ }
4243
+ if (member.key.type !== "Identifier") {
4244
+ continue;
4245
+ }
4246
+ const params = [];
4247
+ const argNames = [];
4248
+ let supported = true;
4249
+ member.params.forEach((param) => {
4250
+ if (!supported) {
4251
+ return;
4252
+ }
4253
+ const paramSource = sliceNodeSource(source, param);
4254
+ const argName = getCustomPomCallArgumentName(param);
4255
+ if (!paramSource || !argName) {
4256
+ supported = false;
4257
+ return;
4258
+ }
4259
+ params.push(paramSource);
4260
+ argNames.push(argName);
4261
+ });
4262
+ if (!supported) {
4263
+ continue;
4264
+ }
4265
+ signatures.set(member.key.name, {
4266
+ params: params.join(", "),
4267
+ argNames
4268
+ });
4269
+ }
4270
+ }
4271
+ return signatures;
4272
+ }
4136
4273
  function ensureDir(dir) {
4137
4274
  const normalized = dir.replace(/\\/g, "/");
4138
4275
  if (!fs.existsSync(normalized)) {
@@ -4176,6 +4313,7 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
4176
4313
  ]);
4177
4314
  const usedImportIdentifiers = /* @__PURE__ */ new Set();
4178
4315
  const customPomClassIdentifierMap2 = {};
4316
+ const customPomMethodSignaturesByClass2 = /* @__PURE__ */ new Map();
4179
4317
  const ensureUniqueIdentifier = (base2) => {
4180
4318
  let candidate = base2;
4181
4319
  let i = 2;
@@ -4189,7 +4327,10 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
4189
4327
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
4190
4328
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
4191
4329
  if (!fs.existsSync(customDirAbs)) {
4192
- return;
4330
+ return {
4331
+ classIdentifierMap: customPomClassIdentifierMap2,
4332
+ methodSignaturesByClass: customPomMethodSignaturesByClass2
4333
+ };
4193
4334
  }
4194
4335
  const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
4195
4336
  for (const file of files) {
@@ -4211,8 +4352,12 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4211
4352
  } else {
4212
4353
  localIdentifier = ensureUniqueIdentifier(requested);
4213
4354
  }
4214
- customPomClassIdentifierMap2[exportName] = localIdentifier;
4215
4355
  const customFileAbs = path.join(customDirAbs, file);
4356
+ customPomClassIdentifierMap2[exportName] = localIdentifier;
4357
+ const customPomMethodSignatures = extractCustomPomMethodSignatures(fs.readFileSync(customFileAbs, "utf8"), exportName);
4358
+ if (customPomMethodSignatures.size > 0) {
4359
+ customPomMethodSignaturesByClass2.set(exportName, customPomMethodSignatures);
4360
+ }
4216
4361
  const fromOutputDir = outputDir;
4217
4362
  const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
4218
4363
  if (localIdentifier !== exportName) {
@@ -4221,10 +4366,15 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4221
4366
  imports.push(`import { ${exportName} } from "${importPath}";`);
4222
4367
  }
4223
4368
  }
4224
- return customPomClassIdentifierMap2;
4369
+ return {
4370
+ classIdentifierMap: customPomClassIdentifierMap2,
4371
+ methodSignaturesByClass: customPomMethodSignaturesByClass2
4372
+ };
4225
4373
  };
4226
- const customPomClassIdentifierMap = addCustomPomImports();
4227
- const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap ?? {}));
4374
+ const customPomImportResolution = addCustomPomImports();
4375
+ const customPomClassIdentifierMap = customPomImportResolution?.classIdentifierMap ?? {};
4376
+ const customPomMethodSignaturesByClass = customPomImportResolution?.methodSignaturesByClass ?? /* @__PURE__ */ new Map();
4377
+ const customPomAvailableClassIdentifiers = new Set(Object.values(customPomClassIdentifierMap));
4228
4378
  const referencedTargets = /* @__PURE__ */ new Set();
4229
4379
  for (const [, deps] of items) {
4230
4380
  for (const dt of deps.dataTestIdSet) {
@@ -4378,6 +4528,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4378
4528
  customPomAttachments: options.customPomAttachments ?? [],
4379
4529
  customPomClassIdentifierMap,
4380
4530
  customPomAvailableClassIdentifiers,
4531
+ customPomMethodSignaturesByClass,
4381
4532
  testIdAttribute: options.testIdAttribute,
4382
4533
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4383
4534
  routeMetaByComponent: options.routeMetaByComponent