@immense/vue-pom-generator 1.0.44 → 1.0.46

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/README.md CHANGED
@@ -15,7 +15,7 @@ If you already use Playwright with `getByTestId`, the point is simple: this pack
15
15
  - **Can generate Playwright fixtures** so tests can request `userListPage` instead of constructing `new UserListPage(page)` manually.
16
16
  - **Can emit a single C# POM file** for Playwright .NET consumers.
17
17
  - **Exposes `virtual:testids`** so your app can import the current collected test-id manifest at runtime.
18
- - **Ships ESLint rules** to remove legacy manually-authored test ids and to discourage raw locator actions on generated getters.
18
+ - **Ships ESLint rules** to remove legacy manually-authored test ids, ban raw `page` fixture usage in spec callbacks, and discourage raw locator actions on generated getters.
19
19
 
20
20
  ## What this does not do
21
21
 
@@ -241,7 +241,7 @@ const pomConfig = defineVuePomGeneratorConfig({
241
241
  className: "Grid",
242
242
  propertyName: "grid",
243
243
  attachWhenUsesComponents: ["DataGrid"],
244
- attachTo: "both",
244
+ attachTo: "pagesAndComponents",
245
245
  flatten: true,
246
246
  },
247
247
  ],
@@ -351,7 +351,7 @@ When `generation.router` is enabled, each view POM gets:
351
351
 
352
352
  Important caveats:
353
353
 
354
- - `goToSelf()` literally does `page.goto(route.template)`
354
+ - `goToSelf()` calls `page.goto(...)`, resolving the route template against `PLAYWRIGHT_RUNTIME_BASE_URL`, `PLAYWRIGHT_TEST_BASE_URL`, or `VITE_PLAYWRIGHT_BASE_URL` when those runtime env vars are present
355
355
  - a dynamic route template like `/users/:id` stays `/users/:id`
356
356
  - if a component is matched by multiple routes, the generator currently picks one route template (the shortest one)
357
357
 
@@ -509,7 +509,7 @@ attachments: [
509
509
  className: "Grid",
510
510
  propertyName: "grid",
511
511
  attachWhenUsesComponents: ["DataGrid"],
512
- attachTo: "both",
512
+ attachTo: "pagesAndComponents",
513
513
  flatten: true,
514
514
  },
515
515
  ]
@@ -522,6 +522,7 @@ Actual semantics:
522
522
  - matching is based on the component usage collected from the Vue template, not runtime inspection
523
523
  - the generated constructor instantiates the helper as `new Helper(page, this)`
524
524
  - `attachTo` defaults to `"views"`
525
+ - `"pagesAndComponents"` is the clearer alias for `"both"`; both spellings are accepted for backward compatibility
525
526
 
526
527
  Why it exists:
527
528
 
@@ -717,6 +718,48 @@ rules: {
717
718
 
718
719
  This rule exists too. It flags direct raw Playwright actions on generated PascalCase getters (for example calling `.click()` directly on a generated getter) so teams use the generated action methods instead.
719
720
 
721
+ ### `no-page-fixture-in-specs`
722
+
723
+ This rule flags Playwright's default `page` fixture when it is destructured directly in `*.spec.*` test and hook callbacks.
724
+
725
+ What it does:
726
+
727
+ - flags `test("...", async ({ page }) => { ... })`
728
+ - flags hooks like `test.beforeEach(async ({ page }) => { ... })`
729
+ - ignores non-spec files such as custom fixtures/helpers
730
+ - ignores POM usage like `dashboardPage.page` because the rule is specifically about the raw fixture entry point
731
+
732
+ Why it exists:
733
+
734
+ - fixture-based POM tests are easier to refactor than raw `page`-driven tests
735
+ - it catches regressions where tests quietly slide back to `page.goto(...)` / `page.getBy...(...)`
736
+ - it makes the generator's Playwright-fixture story enforceable during refactors
737
+
738
+ Recommended usage:
739
+
740
+ 1. enable generated fixtures in the generator
741
+ 2. migrate specs from `({ page })` to generated fixtures like `({ dashboardPage })`
742
+ 3. turn this rule on for `tests/playwright/**/*.spec.ts`
743
+
744
+ Example flat config:
745
+
746
+ ```ts
747
+ import { plugin as vuePomGeneratorEslint } from "@immense/vue-pom-generator/eslint";
748
+
749
+ export default [
750
+ {
751
+ files: ["tests/playwright/**/*.spec.ts"],
752
+ plugins: {
753
+ "@immense/vue-pom-generator": vuePomGeneratorEslint,
754
+ },
755
+ rules: {
756
+ "@immense/vue-pom-generator/no-page-fixture-in-specs": "error",
757
+ "@immense/vue-pom-generator/no-raw-locator-action": "error",
758
+ },
759
+ },
760
+ ];
761
+ ```
762
+
720
763
  ## Configuration reference
721
764
 
722
765
  The sections below follow the actual `VuePomGeneratorPluginOptions` shape from `plugin/types.ts`.
@@ -1049,7 +1092,8 @@ This object holds Playwright-specific additions on top of the generated TypeScri
1049
1092
  - **Current options:**
1050
1093
  - `"views"`
1051
1094
  - `"components"`
1052
- - `"both"`
1095
+ - `"pagesAndComponents"`
1096
+ - `"both"` (backward-compatible alias)
1053
1097
 
1054
1098
  #### `attachments[].flatten`
1055
1099
 
package/RELEASE_NOTES.md CHANGED
@@ -1,33 +1,42 @@
1
1
  ● ## Highlights
2
2
 
3
- - Fixed handling of wrapped editable targets in class generation
4
- - Enhanced Pointer module to correctly resolve targets through wrapper elements
5
- - Added comprehensive test suite for pointer functionality (152 new test lines)
6
- - Improved test coverage for class generation edge cases
3
+ - Fixed dev-mode POM generation to match build mode behavior
4
+ - Added build–serve parity regression tests
5
+ - Improved error handling: fail fast on dev snapshot generation errors
6
+ - Fixed keyed POM deduplication and C# navigation returns
7
+ - Added automated PR release notes preview comments
7
8
 
8
9
  ## Changes
9
10
 
10
- **Bug Fixes**
11
- - Handle wrapped editable targets correctly in Pointer resolution logic
12
- - Updated class generation index to support wrapped target elements
11
+ **Testing & Quality**
12
+ - Added regression tests for build–serve parity (#7)
13
13
 
14
- **Testing**
15
- - Added new `tests/pointer.test.ts` with comprehensive pointer test coverage
16
- - Expanded `tests/class-generation-coverage.test.ts` with 43 additional test lines
14
+ **Bug Fixes**
15
+ - Fixed dev-mode POM generation parity with build mode (#5)
16
+ - Fail fast on dev snapshot generation errors (#6)
17
+ - Fixed keyed POM dedupe and C# navigation returns (#4)
17
18
 
18
- **Type Updates**
19
- - Minor update to `plugin/types.ts`
19
+ **CI/CD**
20
+ - Use sentinel package version in release workflow
21
+ - Added automated PR release-notes preview comments (#1)
20
22
 
21
23
  ## Breaking Changes
22
24
 
23
- None
25
+ None.
24
26
 
25
27
  ## Pull Requests Included
26
28
 
27
- None this release contains direct commits only.
29
+ - #7 test: add build–serve parity regression tests
30
+ (https://github.com/immense/vue-pom-generator/pull/7)
31
+ - #6 fix: fail fast on dev snapshot generation errors
32
+ (https://github.com/immense/vue-pom-generator/pull/6)
33
+ - #5 fix: dev-mode POM generation parity with build mode
34
+ (https://github.com/immense/vue-pom-generator/pull/5)
35
+ - #4 Fix keyed POM dedupe and C# navigation returns
36
+ (https://github.com/immense/vue-pom-generator/pull/4)
37
+ - #1 Add PR release-notes preview comments (https://github.com/immense/vue-pom-generator/pull/1)
28
38
 
29
39
  ## Testing
30
40
 
31
- Significant test coverage added: new dedicated pointer test suite (152 lines) and expanded class
32
- generation coverage tests (43 lines).
41
+ Added regression tests to ensure build and serve modes produce consistent output.
33
42
 
@@ -163,7 +163,10 @@ function generateGoToSelfMethod(componentName: string): string {
163
163
  " if (!route) {",
164
164
  ` throw new Error("[pom] No router path found for component/page-object '${componentName}'.");`,
165
165
  " }",
166
- " await this.page.goto(route.template);",
166
+ " const runtimeEnv = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;",
167
+ " const runtimeBaseUrl = runtimeEnv?.PLAYWRIGHT_RUNTIME_BASE_URL ?? runtimeEnv?.PLAYWRIGHT_TEST_BASE_URL ?? runtimeEnv?.VITE_PLAYWRIGHT_BASE_URL;",
168
+ " const targetUrl = runtimeBaseUrl ? new URL(route.template, runtimeBaseUrl).toString() : route.template;",
169
+ " await this.page.goto(targetUrl);",
167
170
  " }",
168
171
  "",
169
172
  ].join("\n");
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA8PD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAE7C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AA+BD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBAmFnC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../class-generation/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,sBAAsB,EAAoE,MAAM,UAAU,CAAC;AAQpH,OAAO,EAAE,oCAAoC,EAAE,CAAC;AA8ChD,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AASD,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiQD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE1D;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,oCAAoC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEzD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAE7C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,uDAAuD;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF,6EAA6E;IAC7E,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mDAAmD;IACnD,UAAU,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;IAEnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CAClD;AA+BD,wBAAsB,aAAa,CACjC,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAC1D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE,oBAAyB,iBAmFnC"}
@@ -1,5 +1,86 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const path = require("node:path");
4
+ const DIRECT_TEST_CALLS = /* @__PURE__ */ new Set(["test", "it"]);
5
+ const TEST_WRAPPER_CALLS = /* @__PURE__ */ new Set(["only", "skip", "fixme", "fail"]);
6
+ const TEST_HOOK_CALLS = /* @__PURE__ */ new Set(["beforeEach", "beforeAll", "afterEach", "afterAll"]);
7
+ const SPEC_FILE_SUFFIXES = /* @__PURE__ */ new Set([
8
+ ".spec.ts",
9
+ ".spec.tsx",
10
+ ".spec.js",
11
+ ".spec.jsx",
12
+ ".spec.cts",
13
+ ".spec.ctsx",
14
+ ".spec.cjs",
15
+ ".spec.cjsx",
16
+ ".spec.mts",
17
+ ".spec.mtsx",
18
+ ".spec.mjs",
19
+ ".spec.mjsx"
20
+ ]);
21
+ function isSpecFile(filename) {
22
+ const basename = path.basename(filename);
23
+ return Array.from(SPEC_FILE_SUFFIXES).some((suffix) => basename.endsWith(suffix));
24
+ }
25
+ function isFunctionExpression(node) {
26
+ return node != null && typeof node === "object" && "type" in node && (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression");
27
+ }
28
+ function getCallbackArgIndex(callee) {
29
+ if (callee.type === "Identifier" && DIRECT_TEST_CALLS.has(callee.name))
30
+ return 1;
31
+ if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && DIRECT_TEST_CALLS.has(callee.object.name) && callee.property.type === "Identifier") {
32
+ if (TEST_WRAPPER_CALLS.has(callee.property.name))
33
+ return 1;
34
+ if (TEST_HOOK_CALLS.has(callee.property.name))
35
+ return 0;
36
+ }
37
+ return null;
38
+ }
39
+ function getPageFixtureProperty(param) {
40
+ if (!param || param.type !== "ObjectPattern")
41
+ return null;
42
+ for (const property of param.properties) {
43
+ if (property.type !== "Property" || property.computed)
44
+ continue;
45
+ if (property.key.type === "Identifier" && property.key.name === "page")
46
+ return property;
47
+ }
48
+ return null;
49
+ }
50
+ const noPageFixtureInSpecsRule = {
51
+ meta: {
52
+ type: "problem",
53
+ docs: {
54
+ description: "Disallow Playwright's default `page` fixture in spec callbacks. Prefer generated fixtures and POMs instead."
55
+ },
56
+ messages: {
57
+ noPageFixture: "Do not destructure the default `page` fixture in spec callbacks. Use generated fixtures and POMs instead."
58
+ },
59
+ schema: []
60
+ },
61
+ create(context) {
62
+ const filename = context.getFilename();
63
+ if (!isSpecFile(filename))
64
+ return {};
65
+ return {
66
+ CallExpression(node) {
67
+ const callbackArgIndex = getCallbackArgIndex(node.callee);
68
+ if (callbackArgIndex == null)
69
+ return;
70
+ const callback = node.arguments[callbackArgIndex];
71
+ if (!isFunctionExpression(callback))
72
+ return;
73
+ const pageFixtureProperty = getPageFixtureProperty(callback.params[0]);
74
+ if (!pageFixtureProperty)
75
+ return;
76
+ context.report({
77
+ node: pageFixtureProperty,
78
+ messageId: "noPageFixture"
79
+ });
80
+ }
81
+ };
82
+ }
83
+ };
3
84
  function isVueTemplateFile(filename) {
4
85
  return filename.endsWith(".vue");
5
86
  }
@@ -105,10 +186,14 @@ const LOCATOR_ACTIONS = /* @__PURE__ */ new Set([
105
186
  "selectText"
106
187
  ]);
107
188
  const CHAIN_METHODS = /* @__PURE__ */ new Set(["last", "first", "nth", "filter"]);
189
+ function startsWithUppercaseLetter(value) {
190
+ const first = value.charCodeAt(0);
191
+ return first >= 65 && first <= 90;
192
+ }
108
193
  function getPomGetterName(node) {
109
194
  if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
110
195
  const name = node.property.name;
111
- if (/^[A-Z]/.test(name)) return name;
196
+ if (startsWithUppercaseLetter(name)) return name;
112
197
  }
113
198
  if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && CHAIN_METHODS.has(node.callee.property.name)) {
114
199
  return getPomGetterName(node.callee.object);
@@ -147,10 +232,12 @@ const noRawLocatorActionRule = {
147
232
  };
148
233
  const plugin = {
149
234
  rules: {
235
+ "no-page-fixture-in-specs": noPageFixtureInSpecsRule,
150
236
  "no-raw-locator-action": noRawLocatorActionRule,
151
237
  "remove-existing-test-id-attributes": removeExistingTestIdAttributesRule
152
238
  }
153
239
  };
240
+ exports.noPageFixtureInSpecsRule = noPageFixtureInSpecsRule;
154
241
  exports.noRawLocatorActionRule = noRawLocatorActionRule;
155
242
  exports.plugin = plugin;
156
243
  exports.removeExistingTestIdAttributesRule = removeExistingTestIdAttributesRule;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","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.cjs","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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"}
@@ -1,11 +1,14 @@
1
1
  import type { Rule } from "eslint";
2
+ import { noPageFixtureInSpecsRule } from "./no-page-fixture-in-specs";
2
3
  import { removeExistingTestIdAttributesRule } from "./remove-existing-test-id-attributes";
3
4
  export declare const noRawLocatorActionRule: Rule.RuleModule;
4
5
  export declare const plugin: {
5
6
  rules: {
7
+ "no-page-fixture-in-specs": Rule.RuleModule;
6
8
  "no-raw-locator-action": Rule.RuleModule;
7
9
  "remove-existing-test-id-attributes": Rule.RuleModule;
8
10
  };
9
11
  };
12
+ export { noPageFixtureInSpecsRule };
10
13
  export { removeExistingTestIdAttributesRule };
11
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../eslint/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AA0D1F,eAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC,UAmCzC,CAAC;AAEF,eAAO,MAAM,MAAM;;;;;CAKmC,CAAC;AAEvD,OAAO,EAAE,kCAAkC,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../eslint/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAGnC,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAC;AA+D1F,eAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC,UAmCzC,CAAC;AAEF,eAAO,MAAM,MAAM;;;;;;CAMmC,CAAC;AAEvD,OAAO,EAAE,wBAAwB,EAAE,CAAC;AACpC,OAAO,EAAE,kCAAkC,EAAE,CAAC"}
@@ -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");