@immense/vue-pom-generator 1.0.43 → 1.0.45

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.
@@ -1,3 +1,84 @@
1
+ import path from "node:path";
2
+ const DIRECT_TEST_CALLS = /* @__PURE__ */ new Set(["test", "it"]);
3
+ const TEST_WRAPPER_CALLS = /* @__PURE__ */ new Set(["only", "skip", "fixme", "fail"]);
4
+ const TEST_HOOK_CALLS = /* @__PURE__ */ new Set(["beforeEach", "beforeAll", "afterEach", "afterAll"]);
5
+ const SPEC_FILE_SUFFIXES = /* @__PURE__ */ new Set([
6
+ ".spec.ts",
7
+ ".spec.tsx",
8
+ ".spec.js",
9
+ ".spec.jsx",
10
+ ".spec.cts",
11
+ ".spec.ctsx",
12
+ ".spec.cjs",
13
+ ".spec.cjsx",
14
+ ".spec.mts",
15
+ ".spec.mtsx",
16
+ ".spec.mjs",
17
+ ".spec.mjsx"
18
+ ]);
19
+ function isSpecFile(filename) {
20
+ const basename = path.basename(filename);
21
+ return Array.from(SPEC_FILE_SUFFIXES).some((suffix) => basename.endsWith(suffix));
22
+ }
23
+ function isFunctionExpression(node) {
24
+ return node != null && typeof node === "object" && "type" in node && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression");
25
+ }
26
+ function getCallbackArgIndex(callee) {
27
+ if (callee.type === "Identifier" && DIRECT_TEST_CALLS.has(callee.name))
28
+ return 1;
29
+ if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && DIRECT_TEST_CALLS.has(callee.object.name) && callee.property.type === "Identifier") {
30
+ if (TEST_WRAPPER_CALLS.has(callee.property.name))
31
+ return 1;
32
+ if (TEST_HOOK_CALLS.has(callee.property.name))
33
+ return 0;
34
+ }
35
+ return null;
36
+ }
37
+ function getPageFixtureProperty(param) {
38
+ if (!param || param.type !== "ObjectPattern")
39
+ return null;
40
+ for (const property of param.properties) {
41
+ if (property.type !== "Property" || property.computed)
42
+ continue;
43
+ if (property.key.type === "Identifier" && property.key.name === "page")
44
+ return property;
45
+ }
46
+ return null;
47
+ }
48
+ const noPageFixtureInSpecsRule = {
49
+ meta: {
50
+ type: "problem",
51
+ docs: {
52
+ description: "Disallow Playwright's default `page` fixture in spec callbacks. Prefer generated fixtures and POMs instead."
53
+ },
54
+ messages: {
55
+ noPageFixture: "Do not destructure the default `page` fixture in spec callbacks. Use generated fixtures and POMs instead."
56
+ },
57
+ schema: []
58
+ },
59
+ create(context) {
60
+ const filename = context.getFilename();
61
+ if (!isSpecFile(filename))
62
+ return {};
63
+ return {
64
+ CallExpression(node) {
65
+ const callbackArgIndex = getCallbackArgIndex(node.callee);
66
+ if (callbackArgIndex == null)
67
+ return;
68
+ const callback = node.arguments[callbackArgIndex];
69
+ if (!isFunctionExpression(callback))
70
+ return;
71
+ const pageFixtureProperty = getPageFixtureProperty(callback.params[0]);
72
+ if (!pageFixtureProperty)
73
+ return;
74
+ context.report({
75
+ node: pageFixtureProperty,
76
+ messageId: "noPageFixture"
77
+ });
78
+ }
79
+ };
80
+ }
81
+ };
1
82
  function isVueTemplateFile(filename) {
2
83
  return filename.endsWith(".vue");
3
84
  }
@@ -103,10 +184,14 @@ const LOCATOR_ACTIONS = /* @__PURE__ */ new Set([
103
184
  "selectText"
104
185
  ]);
105
186
  const CHAIN_METHODS = /* @__PURE__ */ new Set(["last", "first", "nth", "filter"]);
187
+ function startsWithUppercaseLetter(value) {
188
+ const first = value.charCodeAt(0);
189
+ return first >= 65 && first <= 90;
190
+ }
106
191
  function getPomGetterName(node) {
107
192
  if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
108
193
  const name = node.property.name;
109
- if (/^[A-Z]/.test(name)) return name;
194
+ if (startsWithUppercaseLetter(name)) return name;
110
195
  }
111
196
  if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && CHAIN_METHODS.has(node.callee.property.name)) {
112
197
  return getPomGetterName(node.callee.object);
@@ -145,11 +230,13 @@ const noRawLocatorActionRule = {
145
230
  };
146
231
  const plugin = {
147
232
  rules: {
233
+ "no-page-fixture-in-specs": noPageFixtureInSpecsRule,
148
234
  "no-raw-locator-action": noRawLocatorActionRule,
149
235
  "remove-existing-test-id-attributes": removeExistingTestIdAttributesRule
150
236
  }
151
237
  };
152
238
  export {
239
+ noPageFixtureInSpecsRule,
153
240
  noRawLocatorActionRule,
154
241
  plugin,
155
242
  removeExistingTestIdAttributesRule
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../../eslint/remove-existing-test-id-attributes.ts","../../eslint/index.ts"],"sourcesContent":["import type { Rule } from \"eslint\";\nimport type { AST as VueAST } from \"vue-eslint-parser\";\n\ntype VAttribute = VueAST.VAttribute;\ntype VDirective = VueAST.VDirective;\ntype VElement = VueAST.VElement;\ntype VueAttribute = VAttribute | VDirective;\ntype VueTemplateVisitor = {\n\tVElement: (node: VElement) => void;\n};\n\nfunction isVueTemplateFile(filename: string): boolean {\n\treturn filename.endsWith(\".vue\");\n}\n\nfunction isWhitespaceCharacter(character: string): boolean {\n\treturn character === \" \"\n\t\t|| character === \"\\t\"\n\t\t|| character === \"\\n\"\n\t\t|| character === \"\\r\"\n\t\t|| character === \"\\f\";\n}\n\nfunction removeAttributeWithWhitespace(\n\tattribute: VueAttribute,\n\tcontext: Rule.RuleContext,\n\tfixer: Rule.RuleFixer,\n): Rule.Fix {\n\tconst sourceText = context.sourceCode.getText();\n\tconst [start, end] = attribute.range;\n\n\tlet adjustedStart = start;\n\twhile (adjustedStart > 0 && isWhitespaceCharacter(sourceText[adjustedStart - 1])) {\n\t\tadjustedStart -= 1;\n\t}\n\n\treturn fixer.removeRange([adjustedStart, end]);\n}\n\nfunction isTargetAttribute(attribute: VueAttribute, attributeName: string): boolean {\n\tif (!attribute.directive) {\n\t\treturn attribute.key.type === \"VIdentifier\" && attribute.key.name === attributeName;\n\t}\n\n\tif (attribute.key.type !== \"VDirectiveKey\") {\n\t\treturn false;\n\t}\n\n\tconst directiveName = attribute.key.name;\n\tconst argument = attribute.key.argument;\n\n\treturn directiveName.type === \"VIdentifier\"\n\t\t&& directiveName.name === \"bind\"\n\t\t&& argument?.type === \"VIdentifier\"\n\t\t&& argument.name === attributeName;\n}\n\nfunction findExistingTestIdAttribute(node: VElement, attributeName: string): VueAttribute | undefined {\n\treturn node.startTag.attributes.find(attribute => isTargetAttribute(attribute, attributeName));\n}\n\nfunction defineVueTemplateVisitor(\n\tcontext: Rule.RuleContext,\n\ttemplateVisitor: VueTemplateVisitor,\n): Rule.RuleListener {\n\tconst parserServices = context.sourceCode.parserServices as {\n\t\tdefineTemplateBodyVisitor?: (\n\t\t\ttemplateBodyVisitor: VueTemplateVisitor,\n\t\t\tscriptVisitor: Rule.RuleListener,\n\t\t\toptions: { templateBodyTriggerSelector: \"Program\" },\n\t\t) => Rule.RuleListener;\n\t};\n\n\tif (!parserServices.defineTemplateBodyVisitor) {\n\t\treturn {};\n\t}\n\n\treturn parserServices.defineTemplateBodyVisitor(\n\t\ttemplateVisitor,\n\t\t{},\n\t\t{ templateBodyTriggerSelector: \"Program\" },\n\t);\n}\n\nexport const removeExistingTestIdAttributesRule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Remove existing test-id attributes from Vue templates so vue-pom-generator can generate them consistently.\",\n\t\t},\n\t\tfixable: \"code\",\n\t\tmessages: {\n\t\t\tremoveExistingTestIdAttribute:\n\t\t\t\t\"Remove explicit {{attribute}}. vue-pom-generator can generate it; run this rule with --fix to clean legacy attributes project-wide.\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tattribute: {\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\tdescription: \"Attribute name to remove. Defaults to data-testid.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t},\n\tcreate(context): Rule.RuleListener {\n\t\tif (!isVueTemplateFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\tconst options = (context.options[0] ?? {}) as { attribute?: string };\n\t\tconst attributeName = (options.attribute ?? \"data-testid\").trim() || \"data-testid\";\n\n\t\treturn defineVueTemplateVisitor(context, {\n\t\t\tVElement(node: VElement) {\n\t\t\t\tconst existingAttribute = findExistingTestIdAttribute(node, attributeName);\n\t\t\t\tif (!existingAttribute) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcontext.report({\n\t\t\t\t\tnode: existingAttribute,\n\t\t\t\t\tmessageId: \"removeExistingTestIdAttribute\",\n\t\t\t\t\tdata: { attribute: attributeName },\n\t\t\t\t\tfix(fixer) {\n\t\t\t\t\t\treturn removeAttributeWithWhitespace(existingAttribute, context, fixer);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t},\n\t\t});\n\t},\n};","import type { Rule } from \"eslint\";\nimport type { CallExpression, Expression, MemberExpression } from \"estree\";\n\nimport { removeExistingTestIdAttributesRule } from \"./remove-existing-test-id-attributes\";\n\n/**\n * Playwright locator action methods that should be called via generated POM\n * methods rather than directly on element getters.\n */\nconst LOCATOR_ACTIONS = new Set([\n\t\"click\",\n\t\"dblclick\",\n\t\"fill\",\n\t\"check\",\n\t\"uncheck\",\n\t\"type\",\n\t\"clear\",\n\t\"selectOption\",\n\t\"setInputFiles\",\n\t\"tap\",\n\t\"hover\",\n\t\"focus\",\n\t\"dispatchEvent\",\n\t\"press\",\n\t\"selectText\",\n]);\n\n/**\n * Locator chain methods that are transparent for the purposes of this rule —\n * `.last().click()` is still a raw action on a POM getter.\n */\nconst CHAIN_METHODS = new Set([\"last\", \"first\", \"nth\", \"filter\"]);\n\n/**\n * Returns the PascalCase getter name if `node` is (or chains from) a direct\n * PascalCase member-expression access. Returns null otherwise.\n *\n * Handles:\n * pom.SubmitButton → \"SubmitButton\"\n * pom.SubmitButton.last() → \"SubmitButton\"\n * pom.SubmitButton.nth(0) → \"SubmitButton\"\n */\nfunction getPomGetterName(node: Expression): string | null {\n\tif (node.type === \"MemberExpression\" && !node.computed && node.property.type === \"Identifier\") {\n\t\tconst name = node.property.name;\n\t\tif (/^[A-Z]/.test(name)) return name;\n\t}\n\n\tif (\n\t\tnode.type === \"CallExpression\"\n\t\t&& node.callee.type === \"MemberExpression\"\n\t\t&& !node.callee.computed\n\t\t&& node.callee.property.type === \"Identifier\"\n\t\t&& CHAIN_METHODS.has(node.callee.property.name)\n\t) {\n\t\treturn getPomGetterName((node.callee as MemberExpression).object as Expression);\n\t}\n\n\treturn null;\n}\n\nexport const noRawLocatorActionRule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Disallow calling raw Playwright action methods directly on POM element getters. Use the generated typed POM methods instead (e.g. `clickSubmitButton()`).\",\n\t\t},\n\t\tmessages: {\n\t\t\tnoRawAction:\n\t\t\t\t\"Use the generated POM method instead of `{{getter}}.{{method}}()`. \"\n\t\t\t\t+ \"Call `click{{getter}}()` / `type{{getter}}(text)` or similar.\",\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\treturn {\n\t\t\tCallExpression(node: CallExpression) {\n\t\t\t\tif (node.callee.type !== \"MemberExpression\") return;\n\t\t\t\tconst callee = node.callee as MemberExpression;\n\t\t\t\tif (callee.computed || callee.property.type !== \"Identifier\") return;\n\n\t\t\t\tconst methodName = callee.property.name;\n\t\t\t\tif (!LOCATOR_ACTIONS.has(methodName)) return;\n\n\t\t\t\tconst getterName = getPomGetterName(callee.object as Expression);\n\t\t\t\tif (!getterName) return;\n\n\t\t\t\tcontext.report({\n\t\t\t\t\tnode,\n\t\t\t\t\tmessageId: \"noRawAction\",\n\t\t\t\t\tdata: { getter: getterName, method: methodName },\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport const plugin = {\n\trules: {\n\t\t\"no-raw-locator-action\": noRawLocatorActionRule,\n\t\t\"remove-existing-test-id-attributes\": removeExistingTestIdAttributesRule,\n\t},\n} satisfies { rules: Record<string, Rule.RuleModule> };\n\nexport { removeExistingTestIdAttributesRule };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../../eslint/no-page-fixture-in-specs.ts","../../eslint/remove-existing-test-id-attributes.ts","../../eslint/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Rule } from \"eslint\";\nimport type { ArrowFunctionExpression, CallExpression, FunctionExpression } from \"estree\";\n\nconst DIRECT_TEST_CALLS = new Set([\"test\", \"it\"]);\nconst TEST_WRAPPER_CALLS = new Set([\"only\", \"skip\", \"fixme\", \"fail\"]);\nconst TEST_HOOK_CALLS = new Set([\"beforeEach\", \"beforeAll\", \"afterEach\", \"afterAll\"]);\nconst SPEC_FILE_SUFFIXES = new Set([\n\t\".spec.ts\",\n\t\".spec.tsx\",\n\t\".spec.js\",\n\t\".spec.jsx\",\n\t\".spec.cts\",\n\t\".spec.ctsx\",\n\t\".spec.cjs\",\n\t\".spec.cjsx\",\n\t\".spec.mts\",\n\t\".spec.mtsx\",\n\t\".spec.mjs\",\n\t\".spec.mjsx\",\n]);\n\nfunction isSpecFile(filename: string): boolean {\n\tconst basename = path.basename(filename);\n\treturn Array.from(SPEC_FILE_SUFFIXES).some(suffix => basename.endsWith(suffix));\n}\n\nfunction isFunctionExpression(\n\tnode: CallExpression[\"arguments\"][number] | null | undefined,\n): node is ArrowFunctionExpression | FunctionExpression {\n\treturn node != null\n\t\t&& typeof node === \"object\"\n\t\t&& \"type\" in node\n\t\t&& (node.type === \"ArrowFunctionExpression\" || node.type === \"FunctionExpression\");\n}\n\nfunction getCallbackArgIndex(callee: CallExpression[\"callee\"]): number | null {\n\tif (callee.type === \"Identifier\" && DIRECT_TEST_CALLS.has(callee.name))\n\t\treturn 1;\n\n\tif (\n\t\tcallee.type === \"MemberExpression\"\n\t\t&& !callee.computed\n\t\t&& callee.object.type === \"Identifier\"\n\t\t&& DIRECT_TEST_CALLS.has(callee.object.name)\n\t\t&& callee.property.type === \"Identifier\"\n\t) {\n\t\tif (TEST_WRAPPER_CALLS.has(callee.property.name))\n\t\t\treturn 1;\n\n\t\tif (TEST_HOOK_CALLS.has(callee.property.name))\n\t\t\treturn 0;\n\t}\n\n\treturn null;\n}\n\nfunction getPageFixtureProperty(param: ArrowFunctionExpression[\"params\"][0] | FunctionExpression[\"params\"][0]) {\n\tif (!param || param.type !== \"ObjectPattern\")\n\t\treturn null;\n\n\tfor (const property of param.properties) {\n\t\tif (property.type !== \"Property\" || property.computed)\n\t\t\tcontinue;\n\n\t\tif (property.key.type === \"Identifier\" && property.key.name === \"page\")\n\t\t\treturn property;\n\t}\n\n\treturn null;\n}\n\nexport const noPageFixtureInSpecsRule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Disallow Playwright's default `page` fixture in spec callbacks. Prefer generated fixtures and POMs instead.\",\n\t\t},\n\t\tmessages: {\n\t\t\tnoPageFixture:\n\t\t\t\t\"Do not destructure the default `page` fixture in spec callbacks. Use generated fixtures and POMs instead.\",\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\tconst filename = context.getFilename();\n\t\tif (!isSpecFile(filename))\n\t\t\treturn {};\n\n\t\treturn {\n\t\t\tCallExpression(node: CallExpression) {\n\t\t\t\tconst callbackArgIndex = getCallbackArgIndex(node.callee);\n\t\t\t\tif (callbackArgIndex == null)\n\t\t\t\t\treturn;\n\n\t\t\t\tconst callback = node.arguments[callbackArgIndex];\n\t\t\t\tif (!isFunctionExpression(callback))\n\t\t\t\t\treturn;\n\n\t\t\t\tconst pageFixtureProperty = getPageFixtureProperty(callback.params[0]);\n\t\t\t\tif (!pageFixtureProperty)\n\t\t\t\t\treturn;\n\n\t\t\t\tcontext.report({\n\t\t\t\t\tnode: pageFixtureProperty,\n\t\t\t\t\tmessageId: \"noPageFixture\",\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t},\n};\n","import type { Rule } from \"eslint\";\nimport type { AST as VueAST } from \"vue-eslint-parser\";\n\ntype VAttribute = VueAST.VAttribute;\ntype VDirective = VueAST.VDirective;\ntype VElement = VueAST.VElement;\ntype VueAttribute = VAttribute | VDirective;\ntype VueTemplateVisitor = {\n\tVElement: (node: VElement) => void;\n};\n\nfunction isVueTemplateFile(filename: string): boolean {\n\treturn filename.endsWith(\".vue\");\n}\n\nfunction isWhitespaceCharacter(character: string): boolean {\n\treturn character === \" \"\n\t\t|| character === \"\\t\"\n\t\t|| character === \"\\n\"\n\t\t|| character === \"\\r\"\n\t\t|| character === \"\\f\";\n}\n\nfunction removeAttributeWithWhitespace(\n\tattribute: VueAttribute,\n\tcontext: Rule.RuleContext,\n\tfixer: Rule.RuleFixer,\n): Rule.Fix {\n\tconst sourceText = context.sourceCode.getText();\n\tconst [start, end] = attribute.range;\n\n\tlet adjustedStart = start;\n\twhile (adjustedStart > 0 && isWhitespaceCharacter(sourceText[adjustedStart - 1])) {\n\t\tadjustedStart -= 1;\n\t}\n\n\treturn fixer.removeRange([adjustedStart, end]);\n}\n\nfunction isTargetAttribute(attribute: VueAttribute, attributeName: string): boolean {\n\tif (!attribute.directive) {\n\t\treturn attribute.key.type === \"VIdentifier\" && attribute.key.name === attributeName;\n\t}\n\n\tif (attribute.key.type !== \"VDirectiveKey\") {\n\t\treturn false;\n\t}\n\n\tconst directiveName = attribute.key.name;\n\tconst argument = attribute.key.argument;\n\n\treturn directiveName.type === \"VIdentifier\"\n\t\t&& directiveName.name === \"bind\"\n\t\t&& argument?.type === \"VIdentifier\"\n\t\t&& argument.name === attributeName;\n}\n\nfunction findExistingTestIdAttribute(node: VElement, attributeName: string): VueAttribute | undefined {\n\treturn node.startTag.attributes.find(attribute => isTargetAttribute(attribute, attributeName));\n}\n\nfunction defineVueTemplateVisitor(\n\tcontext: Rule.RuleContext,\n\ttemplateVisitor: VueTemplateVisitor,\n): Rule.RuleListener {\n\tconst parserServices = context.sourceCode.parserServices as {\n\t\tdefineTemplateBodyVisitor?: (\n\t\t\ttemplateBodyVisitor: VueTemplateVisitor,\n\t\t\tscriptVisitor: Rule.RuleListener,\n\t\t\toptions: { templateBodyTriggerSelector: \"Program\" },\n\t\t) => Rule.RuleListener;\n\t};\n\n\tif (!parserServices.defineTemplateBodyVisitor) {\n\t\treturn {};\n\t}\n\n\treturn parserServices.defineTemplateBodyVisitor(\n\t\ttemplateVisitor,\n\t\t{},\n\t\t{ templateBodyTriggerSelector: \"Program\" },\n\t);\n}\n\nexport const removeExistingTestIdAttributesRule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Remove existing test-id attributes from Vue templates so vue-pom-generator can generate them consistently.\",\n\t\t},\n\t\tfixable: \"code\",\n\t\tmessages: {\n\t\t\tremoveExistingTestIdAttribute:\n\t\t\t\t\"Remove explicit {{attribute}}. vue-pom-generator can generate it; run this rule with --fix to clean legacy attributes project-wide.\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tattribute: {\n\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\tdescription: \"Attribute name to remove. Defaults to data-testid.\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t},\n\tcreate(context): Rule.RuleListener {\n\t\tif (!isVueTemplateFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\tconst options = (context.options[0] ?? {}) as { attribute?: string };\n\t\tconst attributeName = (options.attribute ?? \"data-testid\").trim() || \"data-testid\";\n\n\t\treturn defineVueTemplateVisitor(context, {\n\t\t\tVElement(node: VElement) {\n\t\t\t\tconst existingAttribute = findExistingTestIdAttribute(node, attributeName);\n\t\t\t\tif (!existingAttribute) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcontext.report({\n\t\t\t\t\tnode: existingAttribute,\n\t\t\t\t\tmessageId: \"removeExistingTestIdAttribute\",\n\t\t\t\t\tdata: { attribute: attributeName },\n\t\t\t\t\tfix(fixer) {\n\t\t\t\t\t\treturn removeAttributeWithWhitespace(existingAttribute, context, fixer);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t},\n\t\t});\n\t},\n};","import type { Rule } from \"eslint\";\nimport type { CallExpression, Expression, MemberExpression } from \"estree\";\n\nimport { noPageFixtureInSpecsRule } from \"./no-page-fixture-in-specs\";\nimport { removeExistingTestIdAttributesRule } from \"./remove-existing-test-id-attributes\";\n\n/**\n * Playwright locator action methods that should be called via generated POM\n * methods rather than directly on element getters.\n */\nconst LOCATOR_ACTIONS = new Set([\n\t\"click\",\n\t\"dblclick\",\n\t\"fill\",\n\t\"check\",\n\t\"uncheck\",\n\t\"type\",\n\t\"clear\",\n\t\"selectOption\",\n\t\"setInputFiles\",\n\t\"tap\",\n\t\"hover\",\n\t\"focus\",\n\t\"dispatchEvent\",\n\t\"press\",\n\t\"selectText\",\n]);\n\n/**\n * Locator chain methods that are transparent for the purposes of this rule —\n * `.last().click()` is still a raw action on a POM getter.\n */\nconst CHAIN_METHODS = new Set([\"last\", \"first\", \"nth\", \"filter\"]);\n\nfunction startsWithUppercaseLetter(value: string): boolean {\n\tconst first = value.charCodeAt(0);\n\treturn first >= 65 && first <= 90;\n}\n\n/**\n * Returns the PascalCase getter name if `node` is (or chains from) a direct\n * PascalCase member-expression access. Returns null otherwise.\n *\n * Handles:\n * pom.SubmitButton → \"SubmitButton\"\n * pom.SubmitButton.last() → \"SubmitButton\"\n * pom.SubmitButton.nth(0) → \"SubmitButton\"\n */\nfunction getPomGetterName(node: Expression): string | null {\n\tif (node.type === \"MemberExpression\" && !node.computed && node.property.type === \"Identifier\") {\n\t\tconst name = node.property.name;\n\t\tif (startsWithUppercaseLetter(name)) return name;\n\t}\n\n\tif (\n\t\tnode.type === \"CallExpression\"\n\t\t&& node.callee.type === \"MemberExpression\"\n\t\t&& !node.callee.computed\n\t\t&& node.callee.property.type === \"Identifier\"\n\t\t&& CHAIN_METHODS.has(node.callee.property.name)\n\t) {\n\t\treturn getPomGetterName((node.callee as MemberExpression).object as Expression);\n\t}\n\n\treturn null;\n}\n\nexport const noRawLocatorActionRule: Rule.RuleModule = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Disallow calling raw Playwright action methods directly on POM element getters. Use the generated typed POM methods instead (e.g. `clickSubmitButton()`).\",\n\t\t},\n\t\tmessages: {\n\t\t\tnoRawAction:\n\t\t\t\t\"Use the generated POM method instead of `{{getter}}.{{method}}()`. \"\n\t\t\t\t+ \"Call `click{{getter}}()` / `type{{getter}}(text)` or similar.\",\n\t\t},\n\t\tschema: [],\n\t},\n\tcreate(context) {\n\t\treturn {\n\t\t\tCallExpression(node: CallExpression) {\n\t\t\t\tif (node.callee.type !== \"MemberExpression\") return;\n\t\t\t\tconst callee = node.callee as MemberExpression;\n\t\t\t\tif (callee.computed || callee.property.type !== \"Identifier\") return;\n\n\t\t\t\tconst methodName = callee.property.name;\n\t\t\t\tif (!LOCATOR_ACTIONS.has(methodName)) return;\n\n\t\t\t\tconst getterName = getPomGetterName(callee.object as Expression);\n\t\t\t\tif (!getterName) return;\n\n\t\t\t\tcontext.report({\n\t\t\t\t\tnode,\n\t\t\t\t\tmessageId: \"noRawAction\",\n\t\t\t\t\tdata: { getter: getterName, method: methodName },\n\t\t\t\t});\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport const plugin = {\n\trules: {\n\t\t\"no-page-fixture-in-specs\": noPageFixtureInSpecsRule,\n\t\t\"no-raw-locator-action\": noRawLocatorActionRule,\n\t\t\"remove-existing-test-id-attributes\": removeExistingTestIdAttributesRule,\n\t},\n} satisfies { rules: Record<string, Rule.RuleModule> };\n\nexport { noPageFixtureInSpecsRule };\nexport { removeExistingTestIdAttributesRule };\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const noPageFixtureInSpecsRule: Rule.RuleModule;
3
+ //# sourceMappingURL=no-page-fixture-in-specs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-page-fixture-in-specs.d.ts","sourceRoot":"","sources":["../../eslint/no-page-fixture-in-specs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAuEnC,eAAO,MAAM,wBAAwB,EAAE,IAAI,CAAC,UAuC3C,CAAC"}
package/dist/index.cjs CHANGED
@@ -3353,7 +3353,10 @@ function generateGoToSelfMethod(componentName) {
3353
3353
  " if (!route) {",
3354
3354
  ` throw new Error("[pom] No router path found for component/page-object '${componentName}'.");`,
3355
3355
  " }",
3356
- " await this.page.goto(route.template);",
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);",
3357
3360
  " }",
3358
3361
  ""
3359
3362
  ].join("\n");
@@ -3712,6 +3715,26 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3712
3715
  " protected IPage Page { get; }",
3713
3716
  ` protected ILocator LocatorByTestId(string testId) => Page.Locator($"[${testIdAttribute}=\\"{testId}\\"]");`,
3714
3717
  " protected ILocator LocatorWithinTestIdByLabel(string rootTestId, string label, bool exact = true) => LocatorByTestId(rootTestId).GetByLabel(label, new() { Exact = exact });",
3718
+ " protected async Task<ILocator> ResolveEditableLocatorAsync(ILocator locator)",
3719
+ " {",
3720
+ ' var isEditable = await locator.EvaluateAsync<bool>(@"el => {',
3721
+ " if (!el || !(el instanceof HTMLElement)) return false;",
3722
+ " const tagName = el.tagName.toLowerCase();",
3723
+ " return tagName === 'input' || tagName === 'textarea' || tagName === 'select' || el.isContentEditable;",
3724
+ ' }");',
3725
+ " if (isEditable)",
3726
+ " {",
3727
+ " return locator;",
3728
+ " }",
3729
+ "",
3730
+ ` var descendant = locator.Locator("input, textarea, select, [contenteditable=''], [contenteditable='true'], [contenteditable]:not([contenteditable='false'])").First;`,
3731
+ " if (await descendant.CountAsync() > 0)",
3732
+ " {",
3733
+ " return descendant;",
3734
+ " }",
3735
+ "",
3736
+ " return locator;",
3737
+ " }",
3715
3738
  " protected async Task ClickWithinTestIdByLabelAsync(string rootTestId, string label, bool exact = true)",
3716
3739
  " {",
3717
3740
  " await LocatorWithinTestIdByLabel(rootTestId, label, exact).ClickAsync();",
@@ -3813,7 +3836,8 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3813
3836
  const callSuffix = pom.formattedDataTestId.includes("${") ? `(${args})` : "";
3814
3837
  const emitActionCall = (locatorAccess) => {
3815
3838
  if (pom.nativeRole === "input") {
3816
- chunks.push(` await ${locatorAccess}.FillAsync(text);`);
3839
+ chunks.push(` var editableLocator = await ResolveEditableLocatorAsync(${locatorAccess});`);
3840
+ chunks.push(" await editableLocator.FillAsync(text);");
3817
3841
  } else if (pom.nativeRole === "select") {
3818
3842
  chunks.push(` await ${locatorAccess}.SelectOptionAsync(value);`);
3819
3843
  } else if (pom.nativeRole === "vselect") {
@@ -3841,7 +3865,8 @@ function generateAggregatedCSharpFiles(componentHierarchyMap, outDir, options =
3841
3865
  chunks.push(" if (await locator.CountAsync() > 0)");
3842
3866
  chunks.push(" {");
3843
3867
  if (pom.nativeRole === "input") {
3844
- chunks.push(" await locator.FillAsync(text);");
3868
+ chunks.push(" var editableLocator = await ResolveEditableLocatorAsync(locator);");
3869
+ chunks.push(" await editableLocator.FillAsync(text);");
3845
3870
  } else if (pom.nativeRole === "select") {
3846
3871
  chunks.push(" await locator.SelectOptionAsync(value);");
3847
3872
  } else {
@@ -4065,7 +4090,8 @@ function generateViewObjectModelContent(componentName, dependencies, componentHi
4065
4090
  if (!Object.prototype.hasOwnProperty.call(customPomClassIdentifierMap, a.className))
4066
4091
  return false;
4067
4092
  const scope = a.attachTo ?? "views";
4068
- const scopeOk = isView ? scope === "views" || scope === "both" : scope === "components" || scope === "both";
4093
+ const scopeMatchesBoth = scope === "both" || scope === "pagesAndComponents";
4094
+ const scopeOk = isView ? scope === "views" || scopeMatchesBoth : scope === "components" || scopeMatchesBoth;
4069
4095
  if (!scopeOk)
4070
4096
  return false;
4071
4097
  return a.attachWhenUsesComponents.some((c) => hasChildComponent(c));