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,159 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { CallExpression, Literal, TemplateLiteral } from 'estree';
|
|
3
|
+
import { RuleModule } from '../types';
|
|
4
|
+
|
|
5
|
+
const TEST_FUNCTIONS = new Set(['test', 'it']);
|
|
6
|
+
|
|
7
|
+
const VAGUE_PATTERNS: RegExp[] = [
|
|
8
|
+
/^test\s*\d*$/i,
|
|
9
|
+
/^it\s*\d*$/i,
|
|
10
|
+
/^spec\s*\d*$/i,
|
|
11
|
+
/^(todo|fixme|wip)$/i,
|
|
12
|
+
/^\d+$/,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const MIN_DESCRIPTION_LENGTH = 10;
|
|
16
|
+
const TEST_ID_AT_END_PATTERN = /\bnrt-\d+\s*$/i;
|
|
17
|
+
|
|
18
|
+
function getMeaningfulDescription(description: string): string {
|
|
19
|
+
return description
|
|
20
|
+
.replace(TEST_ID_AT_END_PATTERN, '')
|
|
21
|
+
.replace(/\s+/g, ' ')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rule: RuleModule = {
|
|
26
|
+
meta: {
|
|
27
|
+
type: 'suggestion',
|
|
28
|
+
docs: {
|
|
29
|
+
description: 'Require meaningful, descriptive test names',
|
|
30
|
+
category: 'Best Practices',
|
|
31
|
+
recommended: true,
|
|
32
|
+
url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/require-test-description.md',
|
|
33
|
+
},
|
|
34
|
+
messages: {
|
|
35
|
+
missingDescription:
|
|
36
|
+
'test() must have a description as the first argument.',
|
|
37
|
+
missingTestId:
|
|
38
|
+
'Test description "{{name}}" must end with a test ID in the format nrt-123.',
|
|
39
|
+
tooShort:
|
|
40
|
+
'Test description "{{name}}" is too short (min {{min}} characters). Describe observable behaviour.',
|
|
41
|
+
vagueDescription:
|
|
42
|
+
'Test description "{{name}}" is too vague. Describe what the user sees or does.',
|
|
43
|
+
},
|
|
44
|
+
schema: [
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
minLength: { type: 'number', minimum: 1 },
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
56
|
+
const options = context.options[0] ?? {};
|
|
57
|
+
const minLength: number = options.minLength ?? MIN_DESCRIPTION_LENGTH;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
CallExpression(node: CallExpression) {
|
|
61
|
+
if (node.callee.type !== 'Identifier') return;
|
|
62
|
+
const name = (node.callee as { name: string }).name;
|
|
63
|
+
if (!TEST_FUNCTIONS.has(name)) return;
|
|
64
|
+
|
|
65
|
+
const firstArg = node.arguments[0];
|
|
66
|
+
|
|
67
|
+
if (!firstArg) {
|
|
68
|
+
context.report({ node, messageId: 'missingDescription' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (firstArg.type === 'TemplateLiteral') {
|
|
73
|
+
const cooked = (firstArg as TemplateLiteral).quasis
|
|
74
|
+
.map((q) => q.value.cooked ?? '')
|
|
75
|
+
.join('');
|
|
76
|
+
const description = cooked.trim();
|
|
77
|
+
const meaningfulDescription =
|
|
78
|
+
getMeaningfulDescription(description);
|
|
79
|
+
|
|
80
|
+
if (!TEST_ID_AT_END_PATTERN.test(description)) {
|
|
81
|
+
context.report({
|
|
82
|
+
node: firstArg,
|
|
83
|
+
messageId: 'missingTestId',
|
|
84
|
+
data: { name: description },
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
VAGUE_PATTERNS.some((pattern) =>
|
|
91
|
+
pattern.test(meaningfulDescription),
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
context.report({
|
|
95
|
+
node: firstArg,
|
|
96
|
+
messageId: 'vagueDescription',
|
|
97
|
+
data: { name: meaningfulDescription },
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (meaningfulDescription.length < minLength) {
|
|
103
|
+
context.report({
|
|
104
|
+
node: firstArg,
|
|
105
|
+
messageId: 'tooShort',
|
|
106
|
+
data: {
|
|
107
|
+
name: meaningfulDescription,
|
|
108
|
+
min: String(minLength),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (firstArg.type !== 'Literal') return;
|
|
116
|
+
|
|
117
|
+
const description = String((firstArg as Literal).value).trim();
|
|
118
|
+
const meaningfulDescription =
|
|
119
|
+
getMeaningfulDescription(description);
|
|
120
|
+
|
|
121
|
+
if (!TEST_ID_AT_END_PATTERN.test(description)) {
|
|
122
|
+
context.report({
|
|
123
|
+
node: firstArg,
|
|
124
|
+
messageId: 'missingTestId',
|
|
125
|
+
data: { name: description },
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
VAGUE_PATTERNS.some((pattern) =>
|
|
132
|
+
pattern.test(meaningfulDescription),
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
context.report({
|
|
136
|
+
node: firstArg,
|
|
137
|
+
messageId: 'vagueDescription',
|
|
138
|
+
data: { name: meaningfulDescription },
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (meaningfulDescription.length < minLength) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: firstArg,
|
|
146
|
+
messageId: 'tooShort',
|
|
147
|
+
data: {
|
|
148
|
+
name: meaningfulDescription,
|
|
149
|
+
min: String(minLength),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export default rule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
export interface RuleModule extends Rule.RuleModule {
|
|
4
|
+
meta: Rule.RuleMetaData & {
|
|
5
|
+
docs: {
|
|
6
|
+
description: string;
|
|
7
|
+
category: string;
|
|
8
|
+
recommended: boolean;
|
|
9
|
+
url?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type RuleMap = Record<string, RuleModule>;
|
|
15
|
+
|
|
16
|
+
export type Severity = 'error' | 'warn' | 'off';
|
|
17
|
+
|
|
18
|
+
export interface ConfigRules {
|
|
19
|
+
[ruleName: string]: Severity | [Severity, ...unknown[]];
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
import { Node, CallExpression, MemberExpression, Identifier } from 'estree';
|
|
3
|
+
|
|
4
|
+
export function isPlaywrightFile(filename: string): boolean {
|
|
5
|
+
return (
|
|
6
|
+
/\.(spec|test)\.[jt]sx?$/.test(filename) ||
|
|
7
|
+
/[/\\](e2e|tests?)[/\\]/.test(filename)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isWaitForTimeout(node: CallExpression): boolean {
|
|
12
|
+
if (node.callee.type !== 'MemberExpression') return false;
|
|
13
|
+
const callee = node.callee as MemberExpression;
|
|
14
|
+
return (
|
|
15
|
+
callee.property.type === 'Identifier' &&
|
|
16
|
+
(callee.property as Identifier).name === 'waitForTimeout'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMethodName(node: CallExpression): string | null {
|
|
21
|
+
if (node.callee.type !== 'MemberExpression') return null;
|
|
22
|
+
const prop = (node.callee as MemberExpression).property;
|
|
23
|
+
return prop.type === 'Identifier' ? (prop as Identifier).name : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getObjectName(node: CallExpression): string | null {
|
|
27
|
+
if (node.callee.type !== 'MemberExpression') return null;
|
|
28
|
+
const obj = (node.callee as MemberExpression).object;
|
|
29
|
+
return obj.type === 'Identifier' ? (obj as Identifier).name : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function findAncestor(
|
|
33
|
+
node: Node,
|
|
34
|
+
context: Rule.RuleContext,
|
|
35
|
+
predicate: (n: Node) => boolean,
|
|
36
|
+
): Node | null {
|
|
37
|
+
const ancestors = context.getAncestors();
|
|
38
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
39
|
+
if (predicate(ancestors[i])) return ancestors[i];
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const BRITTLE_SELECTOR_PATTERNS: RegExp[] = [
|
|
45
|
+
/^\.[\w-]+/,
|
|
46
|
+
/^#[\w-]+/,
|
|
47
|
+
/nth-child/,
|
|
48
|
+
/nth-of-type/,
|
|
49
|
+
/^\/\//,
|
|
50
|
+
/\s*>\s*/,
|
|
51
|
+
/\s*\+\s*/,
|
|
52
|
+
/\s*~\s*/,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export function isBrittleSelector(selector: string): boolean {
|
|
56
|
+
return BRITTLE_SELECTOR_PATTERNS.some((pattern) =>
|
|
57
|
+
pattern.test(selector.trim()),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @acahet/pw-standard
|
|
3
|
+
*
|
|
4
|
+
* Root barrel. Prefer importing from the specific entry point
|
|
5
|
+
* to keep bundle size small:
|
|
6
|
+
*
|
|
7
|
+
* import plugin from '@acahet/pw-standard/eslint'
|
|
8
|
+
* import { baseConfig } from '@acahet/pw-standard/playwright'
|
|
9
|
+
* import { BasePage } from '@acahet/pw-standard/base'
|
|
10
|
+
*
|
|
11
|
+
* This barrel is available for tooling that needs everything at once.
|
|
12
|
+
*/
|
|
13
|
+
export { default as eslintPlugin } from './eslint/index';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "@acahet/pw-standard — base",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ES2020",
|
|
6
|
+
"module": "commonjs",
|
|
7
|
+
"lib": ["ES2020", "DOM"],
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noImplicitReturns": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"display": "@acahet/pw-standard — strict",
|
|
4
|
+
"extends": "./base.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"strict": true,
|
|
7
|
+
"exactOptionalPropertyTypes": true,
|
|
8
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/eslint/rules/no-brittle-selectors';
|
|
3
|
+
|
|
4
|
+
const tester = new RuleTester({
|
|
5
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
tester.run('no-brittle-selectors', rule, {
|
|
9
|
+
valid: [
|
|
10
|
+
{
|
|
11
|
+
code: `async function t() { await page.getByTestId('submit-button').click(); }`,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
code: `async function t() { await page.getByRole('button', { name: 'Submit' }).click(); }`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: `async function t() { await page.locator('[data-testid="modal"]').click(); }`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
invalid: [
|
|
21
|
+
{
|
|
22
|
+
code: `async function t() { await page.locator('.btn-primary').click(); }`,
|
|
23
|
+
errors: [{ messageId: 'brittleSelector' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: `async function t() { await page.locator('#submit-btn').click(); }`,
|
|
27
|
+
errors: [{ messageId: 'brittleSelector' }],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
code: `async function t() { await page.locator('div > span > button').click(); }`,
|
|
31
|
+
errors: [{ messageId: 'brittleSelector' }],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import noPagePause from '../../src/eslint/rules/no-page-pause';
|
|
3
|
+
import noFocusedTests from '../../src/eslint/rules/no-focused-tests';
|
|
4
|
+
|
|
5
|
+
const tester = new RuleTester({
|
|
6
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
tester.run('no-page-pause', noPagePause, {
|
|
10
|
+
valid: [{ code: `async function t() { await page.goto('/'); }` }],
|
|
11
|
+
invalid: [
|
|
12
|
+
{
|
|
13
|
+
code: `async function t() { await page.pause(); }`,
|
|
14
|
+
errors: [{ messageId: 'noPagePause' }],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
tester.run('no-focused-tests', noFocusedTests, {
|
|
20
|
+
valid: [
|
|
21
|
+
{ code: `test('a normal test', async () => {});` },
|
|
22
|
+
{
|
|
23
|
+
code: `test.skip('skipped test', async () => {});`,
|
|
24
|
+
options: [{ allowSkip: true }],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
invalid: [
|
|
28
|
+
{
|
|
29
|
+
code: `test.only('focused test', async () => {});`,
|
|
30
|
+
errors: [{ messageId: 'noOnly' }],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
code: `describe.only('focused describe', () => {});`,
|
|
34
|
+
errors: [{ messageId: 'noOnly' }],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: `test.skip('skipped test', async () => {});`,
|
|
38
|
+
errors: [{ messageId: 'noSkip' }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/eslint/rules/no-wait-for-timeout';
|
|
3
|
+
|
|
4
|
+
const tester = new RuleTester({
|
|
5
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
tester.run('no-wait-for-timeout', rule, {
|
|
9
|
+
valid: [
|
|
10
|
+
{
|
|
11
|
+
code: `async function t() { await page.locator('[data-testid="modal"]').waitFor(); }`,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
code: `async function t() { await expect(page.getByTestId('spinner')).toBeHidden(); }`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: `async function t() { await page.waitForResponse(r => r.url().includes('/api')); }`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
invalid: [
|
|
21
|
+
{
|
|
22
|
+
code: `async function t() { await page.waitForTimeout(2000); }`,
|
|
23
|
+
errors: [{ messageId: 'noWaitForTimeout' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: `async function t() { await frame.waitForTimeout(500); }`,
|
|
27
|
+
errors: [{ messageId: 'noWaitForTimeout' }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/eslint/rules/prefer-web-first-assertions';
|
|
3
|
+
|
|
4
|
+
const tester = new RuleTester({
|
|
5
|
+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
tester.run('prefer-web-first-assertions', rule, {
|
|
9
|
+
valid: [
|
|
10
|
+
{
|
|
11
|
+
code: `await expect(page.getByTestId('title')).toHaveText('Hello');`,
|
|
12
|
+
},
|
|
13
|
+
{ code: `const text = await page.locator('h1').innerText();` },
|
|
14
|
+
],
|
|
15
|
+
invalid: [
|
|
16
|
+
{
|
|
17
|
+
code: `expect(await page.locator('h1').innerText()).toBe('Hello');`,
|
|
18
|
+
errors: [{ messageId: 'preferWebFirst' }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: `expect(await page.locator('button').isVisible()).toBe(true);`,
|
|
22
|
+
errors: [{ messageId: 'preferWebFirst' }],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/eslint/rules/require-test-description';
|
|
3
|
+
|
|
4
|
+
const tester = new RuleTester({
|
|
5
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
tester.run('require-test-description', rule, {
|
|
9
|
+
valid: [
|
|
10
|
+
{
|
|
11
|
+
code: `test('user can submit the login form with valid credentials nrt-101', async () => {});`,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
code: `it('admin sees the delete button when viewing another user nrt-202', async () => {});`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
// custom minLength option
|
|
18
|
+
code: `test('okay nrt-303', async () => {});`,
|
|
19
|
+
options: [{ minLength: 3 }],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
invalid: [
|
|
23
|
+
{
|
|
24
|
+
// only test ID at the end leaves no meaningful description → tooShort
|
|
25
|
+
code: `test('nrt-1', async () => {});`,
|
|
26
|
+
errors: [{ messageId: 'tooShort' }],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
// missing nrt-xxx suffix → missingTestId
|
|
30
|
+
code: `test('user can submit the login form with valid credentials', async () => {});`,
|
|
31
|
+
errors: [{ messageId: 'missingTestId' }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// id not at the end → missingTestId
|
|
35
|
+
code: `test('nrt-404 user can submit the login form with valid credentials', async () => {});`,
|
|
36
|
+
errors: [{ messageId: 'missingTestId' }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
// matches VAGUE_PATTERNS after removing trailing test ID → vagueDescription
|
|
40
|
+
code: `test('fixme nrt-404', async () => {});`,
|
|
41
|
+
errors: [{ messageId: 'vagueDescription' }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// matches /^test\s*\d*$/i after removing trailing test ID → vagueDescription
|
|
45
|
+
code: `test('test 1 nrt-505', async () => {});`,
|
|
46
|
+
errors: [{ messageId: 'vagueDescription' }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"paths": {
|
|
17
|
+
"@acahet/pw-standard/eslint": ["./src/eslint/index.ts"],
|
|
18
|
+
"@acahet/pw-standard/playwright": ["./src/playwright/index.ts"],
|
|
19
|
+
"@acahet/pw-standard/base": ["./src/base/index.ts"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
24
|
+
}
|