playwright-toolbox 1.0.0
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/.changeset/README.md +9 -0
- package/.changeset/config.json +11 -0
- package/README.md +90 -0
- package/package.json +26 -0
- package/packages/playwright-config/CHANGELOG.md +21 -0
- package/packages/playwright-config/README.md +22 -0
- package/packages/playwright-config/package.json +47 -0
- package/packages/playwright-config/src/index.ts +21 -0
- package/packages/playwright-config/tsconfig.json +19 -0
- package/packages/playwright-history-dashboard/CHANGELOG.md +21 -0
- package/packages/playwright-history-dashboard/README.md +216 -0
- package/packages/playwright-history-dashboard/RELEASING.md +249 -0
- package/packages/playwright-history-dashboard/dashboard/index.html +2825 -0
- package/packages/playwright-history-dashboard/package-lock.json +105 -0
- package/packages/playwright-history-dashboard/package.json +56 -0
- package/packages/playwright-history-dashboard/pw-dashboard.config.js +22 -0
- package/packages/playwright-history-dashboard/scripts/init.ts +95 -0
- package/packages/playwright-history-dashboard/src/reporter.ts +376 -0
- package/packages/playwright-history-dashboard/tsconfig.json +19 -0
- package/packages/pw-standard/.eslintrc.js +23 -0
- package/packages/pw-standard/CHANGELOG.md +31 -0
- package/packages/pw-standard/README.md +50 -0
- package/packages/pw-standard/jest.config.js +28 -0
- package/packages/pw-standard/package.json +86 -0
- package/packages/pw-standard/src/base/index.ts +19 -0
- package/packages/pw-standard/src/eslint/index.ts +91 -0
- package/packages/pw-standard/src/eslint/rules/no-brittle-selectors.ts +53 -0
- package/packages/pw-standard/src/eslint/rules/no-focused-tests.ts +61 -0
- package/packages/pw-standard/src/eslint/rules/no-page-pause.ts +37 -0
- package/packages/pw-standard/src/eslint/rules/no-wait-for-timeout.ts +34 -0
- package/packages/pw-standard/src/eslint/rules/prefer-web-first-assertions.ts +90 -0
- package/packages/pw-standard/src/eslint/rules/require-test-description.ts +159 -0
- package/packages/pw-standard/src/eslint/types.ts +20 -0
- package/packages/pw-standard/src/eslint/utils/ast.ts +59 -0
- package/packages/pw-standard/src/index.ts +13 -0
- package/packages/pw-standard/src/playwright/index.ts +6 -0
- package/packages/pw-standard/src/tsconfig/base.json +21 -0
- package/packages/pw-standard/src/tsconfig/strict.json +11 -0
- package/packages/pw-standard/tests/eslint/no-brittle-selectors.test.ts +34 -0
- package/packages/pw-standard/tests/eslint/no-page-pause-and-focused.test.ts +41 -0
- package/packages/pw-standard/tests/eslint/no-wait-for-timeout.test.ts +30 -0
- package/packages/pw-standard/tests/eslint/prefer-web-first-assertions.test.ts +25 -0
- package/packages/pw-standard/tests/eslint/require-test-description.test.ts +49 -0
- package/packages/pw-standard/tsconfig.json +24 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @acahet/pw-standard
|
|
2
|
+
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 51cd994: feat: publish history dashboard to npm, extract playwright-config as standalone package, add missing .eslintrc.js to pw-standard
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [51cd994]
|
|
12
|
+
- @acahet/playwright-config@1.2.0
|
|
13
|
+
|
|
14
|
+
## 2.0.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 4329be3: Convert repository to npm workspaces with three independent packages and introduce `@acahet/playwright-config`.
|
|
19
|
+
|
|
20
|
+
Summary:
|
|
21
|
+
|
|
22
|
+
- migrate to `packages/*` workspace structure
|
|
23
|
+
- split Playwright config into dedicated package
|
|
24
|
+
- keep a compatibility bridge export at `@acahet/pw-standard/playwright`
|
|
25
|
+
- update dashboard package metadata/docs for new workspace location
|
|
26
|
+
- add root orchestration scripts and Changesets configuration
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [4329be3]
|
|
31
|
+
- @acahet/playwright-config@1.1.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @acahet/pw-standard
|
|
2
|
+
|
|
3
|
+
Shared Playwright standards package:
|
|
4
|
+
|
|
5
|
+
- ESLint plugin and custom rules
|
|
6
|
+
- base classes/fixtures entrypoint
|
|
7
|
+
- shared tsconfig presets
|
|
8
|
+
- compatibility bridge export for Playwright config (`@acahet/pw-standard/playwright`)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D @acahet/pw-standard
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### ESLint plugin
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import plugin from '@acahet/pw-standard/eslint';
|
|
22
|
+
|
|
23
|
+
export default [
|
|
24
|
+
{
|
|
25
|
+
plugins: {
|
|
26
|
+
'playwright-standards': plugin,
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
'playwright-standards/no-wait-for-timeout': 'error',
|
|
30
|
+
'playwright-standards/no-brittle-selectors': 'error',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### TSConfig preset
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"extends": "@acahet/pw-standard/tsconfig/base"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Playwright config compatibility export
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import * as configPreset from '@acahet/pw-standard/playwright';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
New projects should import Playwright config presets from `@acahet/playwright-config` directly.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const config = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
roots: ['<rootDir>/tests'],
|
|
6
|
+
testMatch: ['**/*.test.ts'],
|
|
7
|
+
moduleNameMapper: {
|
|
8
|
+
'^@acahet/pw-standard/eslint$': '<rootDir>/src/eslint/index.ts',
|
|
9
|
+
'^@acahet/pw-standard/playwright$': '<rootDir>/src/playwright/index.ts',
|
|
10
|
+
'^@acahet/pw-standard/base$': '<rootDir>/src/base/index.ts',
|
|
11
|
+
},
|
|
12
|
+
collectCoverageFrom: [
|
|
13
|
+
'src/**/*.ts',
|
|
14
|
+
'!src/**/*.d.ts',
|
|
15
|
+
'!src/**/index.ts', // barrel files — covered transitively
|
|
16
|
+
'!src/tsconfig/**',
|
|
17
|
+
],
|
|
18
|
+
coverageThreshold: {
|
|
19
|
+
global: {
|
|
20
|
+
branches: 80,
|
|
21
|
+
functions: 80,
|
|
22
|
+
lines: 80,
|
|
23
|
+
statements: 80,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports = config;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acahet/pw-standard",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Shared Playwright + TypeScript standards: ESLint rules, configs, base classes and fixtures",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./eslint": {
|
|
14
|
+
"import": "./dist/eslint/index.js",
|
|
15
|
+
"require": "./dist/eslint/index.js",
|
|
16
|
+
"types": "./dist/eslint/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./playwright": {
|
|
19
|
+
"import": "./dist/playwright/index.js",
|
|
20
|
+
"require": "./dist/playwright/index.js",
|
|
21
|
+
"types": "./dist/playwright/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./tsconfig/base": "./src/tsconfig/base.json",
|
|
24
|
+
"./tsconfig/strict": "./src/tsconfig/strict.json",
|
|
25
|
+
"./base": {
|
|
26
|
+
"import": "./dist/base/index.js",
|
|
27
|
+
"require": "./dist/base/index.js",
|
|
28
|
+
"types": "./dist/base/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src/tsconfig",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc --project tsconfig.json",
|
|
38
|
+
"build:watch": "tsc --project tsconfig.json --watch",
|
|
39
|
+
"test": "jest --config jest.config.js",
|
|
40
|
+
"test:watch": "jest --config jest.config.js --watch",
|
|
41
|
+
"test:coverage": "jest --config jest.config.js --coverage",
|
|
42
|
+
"lint": "eslint src --ext .ts",
|
|
43
|
+
"prepublishOnly": "npm run build && npm test"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"eslint",
|
|
47
|
+
"eslint-plugin",
|
|
48
|
+
"playwright",
|
|
49
|
+
"typescript",
|
|
50
|
+
"testing",
|
|
51
|
+
"e2e",
|
|
52
|
+
"standards"
|
|
53
|
+
],
|
|
54
|
+
"author": "",
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/acahet-automation-org/playwright-toolbox.git",
|
|
59
|
+
"directory": "packages/pw-standard"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"eslint": ">=8.0.0",
|
|
63
|
+
"@playwright/test": ">=1.40.0",
|
|
64
|
+
"@acahet/playwright-config": ">=1.2.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@playwright/test": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@playwright/test": "^1.42.0",
|
|
73
|
+
"@types/eslint": "^8.56.0",
|
|
74
|
+
"@types/jest": "^29.5.12",
|
|
75
|
+
"@types/node": "^20.11.0",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
77
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
78
|
+
"eslint": "^8.56.0",
|
|
79
|
+
"jest": "^29.7.0",
|
|
80
|
+
"ts-jest": "^29.1.2",
|
|
81
|
+
"typescript": "^5.3.3"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18.0.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @acahet/pw-standard/base
|
|
3
|
+
*
|
|
4
|
+
* Shared base classes and fixtures for Playwright test suites.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* import { BasePage } from '@acahet/pw-standard/base';
|
|
9
|
+
*
|
|
10
|
+
* export class LoginPage extends BasePage {
|
|
11
|
+
* async goto() { await this.page.goto('/login'); }
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Base classes will be added here — e.g.:
|
|
16
|
+
// export { BasePage } from './BasePage';
|
|
17
|
+
// export { BaseFixture } from './BaseFixture';
|
|
18
|
+
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RuleMap, ConfigRules } from './types';
|
|
2
|
+
import noWaitForTimeout from './rules/no-wait-for-timeout';
|
|
3
|
+
import noBrittleSelectors from './rules/no-brittle-selectors';
|
|
4
|
+
import requireTestDescription from './rules/require-test-description';
|
|
5
|
+
import noPagePause from './rules/no-page-pause';
|
|
6
|
+
import noFocusedTests from './rules/no-focused-tests';
|
|
7
|
+
import preferWebFirstAssertions from './rules/prefer-web-first-assertions';
|
|
8
|
+
|
|
9
|
+
// ─── Rule registry ────────────────────────────────────────────────────────────
|
|
10
|
+
// Add new rules here — they are automatically picked up by all configs below.
|
|
11
|
+
|
|
12
|
+
const rules: RuleMap = {
|
|
13
|
+
'no-wait-for-timeout': noWaitForTimeout,
|
|
14
|
+
'no-brittle-selectors': noBrittleSelectors,
|
|
15
|
+
'require-test-description': requireTestDescription,
|
|
16
|
+
'no-page-pause': noPagePause,
|
|
17
|
+
'no-focused-tests': noFocusedTests,
|
|
18
|
+
'prefer-web-first-assertions': preferWebFirstAssertions,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ─── Shared parser options ────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const PARSER_OPTIONS = {
|
|
24
|
+
ecmaVersion: 2020,
|
|
25
|
+
sourceType: 'module' as const,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ─── Helper: prefix all rule keys with the plugin name ───────────────────────
|
|
29
|
+
|
|
30
|
+
function prefixedRules(severity: 'error' | 'warn'): ConfigRules {
|
|
31
|
+
return Object.keys(rules).reduce<ConfigRules>((acc, key) => {
|
|
32
|
+
const meta = rules[key].meta;
|
|
33
|
+
if (meta.docs?.recommended) {
|
|
34
|
+
acc[`playwright-standards/${key}`] = severity;
|
|
35
|
+
}
|
|
36
|
+
return acc;
|
|
37
|
+
}, {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Configs ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const configs = {
|
|
43
|
+
/**
|
|
44
|
+
* `recommended` — all recommended rules as errors.
|
|
45
|
+
* Best for CI enforcement.
|
|
46
|
+
*/
|
|
47
|
+
recommended: {
|
|
48
|
+
plugins: ['playwright-standards'],
|
|
49
|
+
parserOptions: PARSER_OPTIONS,
|
|
50
|
+
rules: prefixedRules('error'),
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `strict` — all rules (including non-recommended) as errors.
|
|
55
|
+
*/
|
|
56
|
+
strict: {
|
|
57
|
+
plugins: ['playwright-standards'],
|
|
58
|
+
parserOptions: PARSER_OPTIONS,
|
|
59
|
+
rules: Object.keys(rules).reduce<ConfigRules>((acc, key) => {
|
|
60
|
+
acc[`playwright-standards/${key}`] = 'error';
|
|
61
|
+
return acc;
|
|
62
|
+
}, {}),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `warn` — all recommended rules as warnings.
|
|
67
|
+
* Useful when adopting the plugin incrementally.
|
|
68
|
+
*/
|
|
69
|
+
warn: {
|
|
70
|
+
plugins: ['playwright-standards'],
|
|
71
|
+
parserOptions: PARSER_OPTIONS,
|
|
72
|
+
rules: prefixedRules('warn'),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ─── Plugin export ────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const plugin = {
|
|
79
|
+
meta: {
|
|
80
|
+
name: '@acahet/pw-standard',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
},
|
|
83
|
+
rules,
|
|
84
|
+
configs,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default plugin;
|
|
88
|
+
|
|
89
|
+
// Named exports for consumers who import rules individually
|
|
90
|
+
export { rules, configs };
|
|
91
|
+
export type { RuleMap, ConfigRules };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression, Literal } from 'estree';
|
|
3
|
+
import { getMethodName, isBrittleSelector } from '../utils/ast';
|
|
4
|
+
import { RuleModule } from '../types';
|
|
5
|
+
|
|
6
|
+
const LOCATOR_METHODS = new Set([
|
|
7
|
+
'locator',
|
|
8
|
+
'$',
|
|
9
|
+
'$$',
|
|
10
|
+
'waitForSelector',
|
|
11
|
+
'querySelector',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const rule: RuleModule = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
docs: {
|
|
18
|
+
description:
|
|
19
|
+
'Discourage brittle CSS / XPath selectors — prefer getByTestId, getByRole, getByLabel, etc.',
|
|
20
|
+
category: 'Best Practices',
|
|
21
|
+
recommended: true,
|
|
22
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-brittle-selectors.md',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
brittleSelector:
|
|
26
|
+
'Brittle selector "{{selector}}". Prefer getByTestId(), getByRole(), getByLabel(), or getByText() instead.',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
32
|
+
return {
|
|
33
|
+
CallExpression(node: CallExpression) {
|
|
34
|
+
const method = getMethodName(node);
|
|
35
|
+
if (!method || !LOCATOR_METHODS.has(method)) return;
|
|
36
|
+
|
|
37
|
+
const firstArg = node.arguments[0];
|
|
38
|
+
if (!firstArg || firstArg.type !== 'Literal') return;
|
|
39
|
+
|
|
40
|
+
const selector = String((firstArg as Literal).value);
|
|
41
|
+
if (isBrittleSelector(selector)) {
|
|
42
|
+
context.report({
|
|
43
|
+
node: firstArg,
|
|
44
|
+
messageId: 'brittleSelector',
|
|
45
|
+
data: { selector },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default rule;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression, MemberExpression, Identifier } from 'estree';
|
|
3
|
+
import { RuleModule } from '../types';
|
|
4
|
+
|
|
5
|
+
const FOCUSED_METHODS = new Set(['only', 'skip']);
|
|
6
|
+
const TEST_DESCRIBE_OBJECTS = new Set(['test', 'it', 'describe']);
|
|
7
|
+
|
|
8
|
+
const rule: RuleModule = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Disallow test.only() / describe.only() — remove before committing',
|
|
13
|
+
category: 'Best Practices',
|
|
14
|
+
recommended: true,
|
|
15
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-focused-tests.md',
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
noOnly: '"{{parent}}.only()" found. Remove it before committing to avoid blocking the full test suite.',
|
|
19
|
+
noSkip: '"{{parent}}.skip()" found. Remove or resolve the skip before committing.',
|
|
20
|
+
},
|
|
21
|
+
schema: [
|
|
22
|
+
{
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
allowSkip: { type: 'boolean' },
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
33
|
+
const options = context.options[0] ?? {};
|
|
34
|
+
const allowSkip: boolean = options.allowSkip ?? false;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
CallExpression(node: CallExpression) {
|
|
38
|
+
if (node.callee.type !== 'MemberExpression') return;
|
|
39
|
+
const callee = node.callee as MemberExpression;
|
|
40
|
+
|
|
41
|
+
if (callee.object.type !== 'Identifier') return;
|
|
42
|
+
const parentName = (callee.object as Identifier).name;
|
|
43
|
+
if (!TEST_DESCRIBE_OBJECTS.has(parentName)) return;
|
|
44
|
+
|
|
45
|
+
if (callee.property.type !== 'Identifier') return;
|
|
46
|
+
const methodName = (callee.property as Identifier).name;
|
|
47
|
+
if (!FOCUSED_METHODS.has(methodName)) return;
|
|
48
|
+
|
|
49
|
+
if (methodName === 'skip' && allowSkip) return;
|
|
50
|
+
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: methodName === 'only' ? 'noOnly' : 'noSkip',
|
|
54
|
+
data: { parent: parentName },
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default rule;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression, MemberExpression, Identifier } from 'estree';
|
|
3
|
+
import { RuleModule } from '../types';
|
|
4
|
+
|
|
5
|
+
const rule: RuleModule = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow page.pause() — remove before committing',
|
|
10
|
+
category: 'Best Practices',
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-page-pause.md',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
noPagePause:
|
|
16
|
+
'page.pause() is for local debugging only. Remove it before committing.',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node: CallExpression) {
|
|
24
|
+
if (node.callee.type !== 'MemberExpression') return;
|
|
25
|
+
const callee = node.callee as MemberExpression;
|
|
26
|
+
if (
|
|
27
|
+
callee.property.type === 'Identifier' &&
|
|
28
|
+
(callee.property as Identifier).name === 'pause'
|
|
29
|
+
) {
|
|
30
|
+
context.report({ node, messageId: 'noPagePause' });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default rule;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression } from 'estree';
|
|
3
|
+
import { isWaitForTimeout } from '../utils/ast';
|
|
4
|
+
import { RuleModule } from '../types';
|
|
5
|
+
|
|
6
|
+
const rule: RuleModule = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
'Disallow waitForTimeout() — use explicit wait conditions instead',
|
|
12
|
+
category: 'Best Practices',
|
|
13
|
+
recommended: true,
|
|
14
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-wait-for-timeout.md',
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
noWaitForTimeout:
|
|
18
|
+
'Avoid waitForTimeout(). Use waitFor(), waitForResponse(), or expect(...).toBeVisible() instead.',
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
24
|
+
return {
|
|
25
|
+
CallExpression(node: CallExpression) {
|
|
26
|
+
if (isWaitForTimeout(node)) {
|
|
27
|
+
context.report({ node, messageId: 'noWaitForTimeout' });
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default rule;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression, Identifier } from 'estree';
|
|
3
|
+
import { RuleModule } from '../types';
|
|
4
|
+
|
|
5
|
+
const EAGER_METHODS = new Set([
|
|
6
|
+
'innerText',
|
|
7
|
+
'textContent',
|
|
8
|
+
'getAttribute',
|
|
9
|
+
'isVisible',
|
|
10
|
+
'isHidden',
|
|
11
|
+
'isEnabled',
|
|
12
|
+
'isDisabled',
|
|
13
|
+
'isChecked',
|
|
14
|
+
'inputValue',
|
|
15
|
+
'innerHTML',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const PREFERRED: Record<string, string> = {
|
|
19
|
+
innerText: 'toHaveText()',
|
|
20
|
+
textContent: 'toHaveText()',
|
|
21
|
+
getAttribute: 'toHaveAttribute()',
|
|
22
|
+
isVisible: 'toBeVisible()',
|
|
23
|
+
isHidden: 'toBeHidden()',
|
|
24
|
+
isEnabled: 'toBeEnabled()',
|
|
25
|
+
isDisabled: 'toBeDisabled()',
|
|
26
|
+
isChecked: 'toBeChecked()',
|
|
27
|
+
inputValue: 'toHaveValue()',
|
|
28
|
+
innerHTML: 'toContainText() or toHaveText()',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const rule: RuleModule = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
'Prefer Playwright web-first assertions over awaited property calls',
|
|
37
|
+
category: 'Best Practices',
|
|
38
|
+
recommended: true,
|
|
39
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/prefer-web-first-assertions.md',
|
|
40
|
+
},
|
|
41
|
+
messages: {
|
|
42
|
+
preferWebFirst:
|
|
43
|
+
'Avoid awaiting "{{method}}()" inside expect(). Use the web-first assertion "{{preferred}}" instead — it auto-retries.',
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
49
|
+
const sourceCode = context.sourceCode;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
'CallExpression > AwaitExpression > CallExpression'(
|
|
53
|
+
node: CallExpression,
|
|
54
|
+
) {
|
|
55
|
+
if (node.callee.type !== 'MemberExpression') return;
|
|
56
|
+
const prop = node.callee.property;
|
|
57
|
+
if (prop.type !== 'Identifier') return;
|
|
58
|
+
|
|
59
|
+
const method = (prop as Identifier).name;
|
|
60
|
+
if (!EAGER_METHODS.has(method)) return;
|
|
61
|
+
|
|
62
|
+
const ancestors = sourceCode.getAncestors(node);
|
|
63
|
+
const awaitExpr = ancestors[ancestors.length - 1];
|
|
64
|
+
if (!awaitExpr || awaitExpr.type !== 'AwaitExpression') return;
|
|
65
|
+
|
|
66
|
+
const outerCall = ancestors[ancestors.length - 2];
|
|
67
|
+
if (
|
|
68
|
+
!outerCall ||
|
|
69
|
+
outerCall.type !== 'CallExpression' ||
|
|
70
|
+
(outerCall as CallExpression).callee.type !==
|
|
71
|
+
'Identifier' ||
|
|
72
|
+
((outerCall as CallExpression).callee as Identifier)
|
|
73
|
+
.name !== 'expect'
|
|
74
|
+
)
|
|
75
|
+
return;
|
|
76
|
+
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'preferWebFirst',
|
|
80
|
+
data: {
|
|
81
|
+
method,
|
|
82
|
+
preferred: PREFERRED[method] ?? 'a web-first assertion',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default rule;
|