@immense/vue-pom-generator 1.0.37 → 1.0.39

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";
@@ -3478,7 +3478,8 @@ async function generateFiles(componentHierarchyMap, vueFilesPathMap, basePageCla
3478
3478
  const fixtureRegistryFile = maybeGenerateFixtureRegistry(componentHierarchyMap, {
3479
3479
  generateFixtures,
3480
3480
  pomOutDir: outDir,
3481
- projectRoot
3481
+ projectRoot,
3482
+ customPomDir
3482
3483
  });
3483
3484
  if (fixtureRegistryFile) {
3484
3485
  writeGeneratedFile(fixtureRegistryFile);
@@ -3877,6 +3878,9 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
3877
3878
  const fixtureFileName = looksLikeFilePath ? path.basename(fixtureOutRel) : "fixtures.g.ts";
3878
3879
  const root = options.projectRoot ?? process.cwd();
3879
3880
  const fixtureOutDirAbs = path.isAbsolute(fixtureOutDirRel) ? fixtureOutDirRel : path.resolve(root, fixtureOutDirRel);
3881
+ const customPomDirRel = options.customPomDir ?? "tests/playwright/pom/custom";
3882
+ const customPomDirAbs = path.isAbsolute(customPomDirRel) ? customPomDirRel : path.resolve(root, customPomDirRel);
3883
+ const overridePomDirAbs = path.resolve(path.dirname(customPomDirAbs), "overrides");
3880
3884
  const pomDirAbs = path.isAbsolute(pomOutDir) ? pomOutDir : path.resolve(root, pomOutDir);
3881
3885
  const pomImport = toPosixRelativePath(fixtureOutDirAbs, pomDirAbs);
3882
3886
  const viewClassNames = Array.from(componentHierarchyMap.entries()).filter(([, deps]) => !!deps.isView).map(([name]) => name).sort((a, b) => a.localeCompare(b));
@@ -3899,6 +3903,22 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
3899
3903
  return false;
3900
3904
  return true;
3901
3905
  }).sort((a, b) => a.localeCompare(b));
3906
+ const fixtureClassNames = [...viewClassNames, ...componentClassNames];
3907
+ const overrideCtorEntries = fixtureClassNames.map((name) => {
3908
+ const overrideFilePath = path.join(overridePomDirAbs, `${name}.ts`);
3909
+ if (!fs.existsSync(overrideFilePath))
3910
+ return null;
3911
+ return {
3912
+ className: name,
3913
+ localIdentifier: `${name}Override`,
3914
+ importSpecifier: stripExtension(toPosixRelativePath(fixtureOutDirAbs, overrideFilePath))
3915
+ };
3916
+ }).filter((entry) => !!entry);
3917
+ const overrideCtorByClassName = new Map(overrideCtorEntries.map((entry) => [entry.className, entry.localIdentifier]));
3918
+ const overrideImports = overrideCtorEntries.length ? `${overrideCtorEntries.map((entry) => `import { ${entry.className} as ${entry.localIdentifier} } from "${entry.importSpecifier}";`).join("\n")}
3919
+
3920
+ ` : "";
3921
+ const fixtureCtorExpression = (name) => overrideCtorByClassName.get(name) ?? `Pom.${name}`;
3902
3922
  const header = `${eslintSuppressionHeader}/**
3903
3923
  * DO NOT MODIFY BY HAND
3904
3924
  *
@@ -3907,8 +3927,8 @@ function maybeGenerateFixtureRegistry(componentHierarchyMap, options) {
3907
3927
  */
3908
3928
 
3909
3929
  `;
3910
- const fixturesTypeEntries = viewClassNames.map((name) => ` ${lowerFirst(name)}: Pom.${name},`).join("\n");
3911
- const componentFixturesTypeEntries = componentClassNames.map((name) => ` ${lowerFirst(name)}: Pom.${name},`).join("\n");
3930
+ const fixturesTypeEntries = viewClassNames.map((name) => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`).join("\n");
3931
+ const componentFixturesTypeEntries = componentClassNames.map((name) => ` ${lowerFirst(name)}: ${fixtureCtorExpression(name)},`).join("\n");
3912
3932
  const pomFactoryType = `export type PomConstructor<T> = new (page: PwPage) => T;
3913
3933
 
3914
3934
  export interface PomFactory {
@@ -3921,8 +3941,7 @@ export interface PomFactory {
3921
3941
  import { expect, test as base } from "@playwright/test";
3922
3942
  import type { Page as PwPage } from "@playwright/test";
3923
3943
  import * as Pom from "${pomImport}";
3924
-
3925
- export interface PlaywrightOptions {
3944
+ ${overrideImports}export interface PlaywrightOptions {
3926
3945
  animation: Pom.PlaywrightAnimationOptions;
3927
3946
  }
3928
3947
 
@@ -3998,6 +4017,7 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
3998
4017
  };
3999
4018
  const customPomClassIdentifierMap = options.customPomClassIdentifierMap ?? {};
4000
4019
  const customPomAvailableClassIdentifiers = options.customPomAvailableClassIdentifiers ?? /* @__PURE__ */ new Set();
4020
+ const customPomMethodSignaturesByClass = options.customPomMethodSignaturesByClass ?? /* @__PURE__ */ new Map();
4001
4021
  const attachmentsForThisClass = customPomAttachments.filter((a) => {
4002
4022
  if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
4003
4023
  return false;
@@ -4008,7 +4028,9 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4008
4028
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));
4009
4029
  }).map((a) => ({
4010
4030
  className: customPomClassIdentifierMap[a.className],
4011
- 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()
4012
4034
  }));
4013
4035
  let content = "";
4014
4036
  const sourceRel = toPosixRelativePath(outputDir, filePath);
@@ -4049,6 +4071,15 @@ export class ${className} extends BasePage {
4049
4071
  `;
4050
4072
  const widgetInstances = isView ? getWidgetInstancesForView(componentName, dependencies.dataTestIdSet, customPomAvailableClassIdentifiers) : [];
4051
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
+ ]);
4052
4083
  if (isView && (componentRefsForInstances.size > 0 || attachmentsForThisClass.length > 0 || widgetInstances.length > 0)) {
4053
4084
  content += getComponentInstances(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances);
4054
4085
  content += getConstructor(componentRefsForInstances, componentHierarchyMap, attachmentsForThisClass, widgetInstances, { testIdAttribute });
@@ -4057,8 +4088,9 @@ export class ${className} extends BasePage {
4057
4088
  content += getComponentInstances(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass);
4058
4089
  content += getConstructor(/* @__PURE__ */ new Set(), componentHierarchyMap, attachmentsForThisClass, [], { testIdAttribute });
4059
4090
  }
4091
+ content += getAttachmentPassthroughMethods(componentName, dependencies, attachmentsForThisClass, reservedAttachmentPassthroughNames);
4060
4092
  if (isView && componentRefsForInstances.size === 1) {
4061
- content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap);
4093
+ content += getViewPassthroughMethods(componentName, dependencies, componentRefsForInstances, componentHierarchyMap, blockedViewPassthroughMethodNames);
4062
4094
  }
4063
4095
  if (isView && options.vueRouterFluentChaining) {
4064
4096
  const routeMeta = options.routeMetaByComponent?.[componentName] ?? null;
@@ -4069,7 +4101,7 @@ export class ${className} extends BasePage {
4069
4101
  content += "}\n";
4070
4102
  return content;
4071
4103
  }
4072
- function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap) {
4104
+ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponentSet, componentHierarchyMap, blockedMethodNames = /* @__PURE__ */ new Set()) {
4073
4105
  const existingOnView = viewDependencies.generatedMethods ?? /* @__PURE__ */ new Map();
4074
4106
  const methodToChildren = /* @__PURE__ */ new Map();
4075
4107
  for (const child of childrenComponentSet) {
@@ -4083,7 +4115,7 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
4083
4115
  for (const [name, sig] of methods.entries()) {
4084
4116
  if (!sig)
4085
4117
  continue;
4086
- if (existingOnView.has(name))
4118
+ if (existingOnView.has(name) || blockedMethodNames.has(name))
4087
4119
  continue;
4088
4120
  const list = methodToChildren.get(name) ?? [];
4089
4121
  list.push({ childProp, params: sig.params, argNames: sig.argNames });
@@ -4114,6 +4146,130 @@ function getViewPassthroughMethods(viewName, viewDependencies, childrenComponent
4114
4146
  ""
4115
4147
  ].join("\n");
4116
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
+ }
4117
4273
  function ensureDir(dir) {
4118
4274
  const normalized = dir.replace(/\\/g, "/");
4119
4275
  if (!fs.existsSync(normalized)) {
@@ -4157,6 +4313,7 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
4157
4313
  ]);
4158
4314
  const usedImportIdentifiers = /* @__PURE__ */ new Set();
4159
4315
  const customPomClassIdentifierMap2 = {};
4316
+ const customPomMethodSignaturesByClass2 = /* @__PURE__ */ new Map();
4160
4317
  const ensureUniqueIdentifier = (base2) => {
4161
4318
  let candidate = base2;
4162
4319
  let i = 2;
@@ -4170,7 +4327,10 @@ async function generateAggregatedFiles(componentHierarchyMap, vueFilesPathMap, b
4170
4327
  const customDirRelOrAbs = options.customPomDir ?? "tests/playwright/pom/custom";
4171
4328
  const customDirAbs = path.isAbsolute(customDirRelOrAbs) ? customDirRelOrAbs : path.resolve(projectRoot, customDirRelOrAbs);
4172
4329
  if (!fs.existsSync(customDirAbs)) {
4173
- return;
4330
+ return {
4331
+ classIdentifierMap: customPomClassIdentifierMap2,
4332
+ methodSignaturesByClass: customPomMethodSignaturesByClass2
4333
+ };
4174
4334
  }
4175
4335
  const files = fs.readdirSync(customDirAbs).filter((f) => f.endsWith(".ts")).sort((a, b) => a.localeCompare(b));
4176
4336
  for (const file of files) {
@@ -4192,8 +4352,12 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4192
4352
  } else {
4193
4353
  localIdentifier = ensureUniqueIdentifier(requested);
4194
4354
  }
4195
- customPomClassIdentifierMap2[exportName] = localIdentifier;
4196
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
+ }
4197
4361
  const fromOutputDir = outputDir;
4198
4362
  const importPath = stripExtension(toPosixRelativePath(fromOutputDir, customFileAbs));
4199
4363
  if (localIdentifier !== exportName) {
@@ -4202,10 +4366,15 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4202
4366
  imports.push(`import { ${exportName} } from "${importPath}";`);
4203
4367
  }
4204
4368
  }
4205
- return customPomClassIdentifierMap2;
4369
+ return {
4370
+ classIdentifierMap: customPomClassIdentifierMap2,
4371
+ methodSignaturesByClass: customPomMethodSignaturesByClass2
4372
+ };
4206
4373
  };
4207
- const customPomClassIdentifierMap = addCustomPomImports();
4208
- 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));
4209
4378
  const referencedTargets = /* @__PURE__ */ new Set();
4210
4379
  for (const [, deps] of items) {
4211
4380
  for (const dt of deps.dataTestIdSet) {
@@ -4359,6 +4528,7 @@ Fix by setting generation.playwright.customPoms.importAliases["${exportName}"] t
4359
4528
  customPomAttachments: options.customPomAttachments ?? [],
4360
4529
  customPomClassIdentifierMap,
4361
4530
  customPomAvailableClassIdentifiers,
4531
+ customPomMethodSignaturesByClass,
4362
4532
  testIdAttribute: options.testIdAttribute,
4363
4533
  vueRouterFluentChaining: options.vueRouterFluentChaining,
4364
4534
  routeMetaByComponent: options.routeMetaByComponent