@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 +49 -5
- package/RELEASE_NOTES.md +25 -16
- package/class-generation/index.ts +4 -1
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/eslint/index.cjs +88 -1
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.mjs +88 -1
- package/dist/eslint/index.mjs.map +1 -1
- package/dist/eslint/no-page-fixture-in-specs.d.ts +3 -0
- package/dist/eslint/no-page-fixture-in-specs.d.ts.map +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +4 -1
- package/dist/index.mjs.map +1 -1
- package/dist/tests/vue-plugin-state.test.d.ts +2 -0
- package/dist/tests/vue-plugin-state.test.d.ts.map +1 -0
- package/package.json +1 -1
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
|
|
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: "
|
|
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()`
|
|
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: "
|
|
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
|
-
- `"
|
|
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
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
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
|
-
**
|
|
11
|
-
-
|
|
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
|
-
**
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
**
|
|
19
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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;
|
|
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"}
|
package/dist/eslint/index.cjs
CHANGED
|
@@ -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 (
|
|
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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"}
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/eslint/index.mjs
CHANGED
|
@@ -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 (
|
|
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 @@
|
|
|
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
|
-
"
|
|
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");
|