eslint-plugin-test-governance 0.3.0 → 0.4.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/CHANGELOG.md +76 -1
- package/README.md +166 -64
- package/dist/index.d.ts +14 -12
- package/dist/index.js +22 -12
- package/dist/rules/marker-matches-state.d.ts +1 -2
- package/dist/rules/marker-matches-state.js +7 -7
- package/dist/rules/no-conflicting-states.d.ts +1 -2
- package/dist/rules/no-conflicting-states.js +12 -20
- package/dist/rules/no-orphaned-marker.d.ts +1 -2
- package/dist/rules/no-orphaned-marker.js +15 -10
- package/dist/rules/require-ticket-in-comments.d.ts +1 -2
- package/dist/rules/require-ticket-in-comments.js +13 -18
- package/dist/rules/require-ticket.d.ts +1 -2
- package/dist/rules/require-ticket.js +18 -10
- package/dist/utils/analyze.d.ts +17 -14
- package/dist/utils/analyze.js +370 -153
- package/dist/utils/constants.d.ts +5 -3
- package/dist/utils/constants.js +4 -3
- package/dist/utils/create-rule.d.ts +9 -10
- package/dist/utils/create-rule.js +5 -11
- package/dist/utils/distance.d.ts +0 -1
- package/dist/utils/distance.js +0 -1
- package/dist/utils/errors.d.ts +0 -1
- package/dist/utils/errors.js +0 -1
- package/dist/utils/fix.d.ts +0 -3
- package/dist/utils/fix.js +0 -8
- package/dist/utils/frameworks.d.ts +59 -0
- package/dist/utils/frameworks.js +102 -0
- package/dist/utils/options.d.ts +26 -11
- package/dist/utils/options.js +13 -9
- package/dist/utils/schema.d.ts +1 -2
- package/dist/utils/schema.js +2 -2
- package/dist/utils/tickets.d.ts +0 -1
- package/dist/utils/tickets.js +4 -4
- package/dist/utils/validate.d.ts +0 -1
- package/dist/utils/validate.js +3 -3
- package/docs/rules/marker-matches-state.md +18 -10
- package/docs/rules/no-conflicting-states.md +14 -8
- package/docs/rules/no-orphaned-marker.md +31 -17
- package/docs/rules/require-ticket-in-comments.md +26 -22
- package/docs/rules/require-ticket.md +107 -47
- package/package.json +7 -10
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
export declare const MARKER_RE: RegExp;
|
|
3
3
|
/** A tag as written in options: `@` followed by letters, digits, `-` and `_`. */
|
|
4
4
|
export declare const TAG_RE: RegExp;
|
|
5
|
-
export declare const BUILTIN_STATE_NAMES: readonly ["skip", "fixme", "fail", "slow", "new", "unstable"];
|
|
5
|
+
export declare const BUILTIN_STATE_NAMES: readonly ["skip", "fixme", "fail", "slow", "todo", "new", "unstable"];
|
|
6
6
|
export type BuiltinStateName = (typeof BUILTIN_STATE_NAMES)[number];
|
|
7
|
+
/** The test frameworks the rules understand. */
|
|
8
|
+
export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest"];
|
|
9
|
+
export type FrameworkName = (typeof FRAMEWORK_NAMES)[number];
|
|
7
10
|
export declare const TICKET_PRESETS: readonly ["any", "jira", "github", "gitlab", "linear", "azure-devops", "numeric", "pattern"];
|
|
8
|
-
/** Comment keywords that must reference a ticket by default (`// TODO:
|
|
11
|
+
/** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
|
|
9
12
|
export declare const DEFAULT_COMMENT_KEYWORDS: string[];
|
|
10
|
-
//# sourceMappingURL=constants.d.ts.map
|
package/dist/utils/constants.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
export const MARKER_RE = /^[A-Z][A-Z0-9_-]*$/;
|
|
3
3
|
/** A tag as written in options: `@` followed by letters, digits, `-` and `_`. */
|
|
4
4
|
export const TAG_RE = /^@[\w-]+$/;
|
|
5
|
-
export const BUILTIN_STATE_NAMES = ['skip', 'fixme', 'fail', 'slow', 'new', 'unstable'];
|
|
5
|
+
export const BUILTIN_STATE_NAMES = ['skip', 'fixme', 'fail', 'slow', 'todo', 'new', 'unstable'];
|
|
6
|
+
/** The test frameworks the rules understand. */
|
|
7
|
+
export const FRAMEWORK_NAMES = ['playwright', 'jest', 'vitest'];
|
|
6
8
|
export const TICKET_PRESETS = [
|
|
7
9
|
'any',
|
|
8
10
|
'jira',
|
|
@@ -13,6 +15,5 @@ export const TICKET_PRESETS = [
|
|
|
13
15
|
'numeric',
|
|
14
16
|
'pattern',
|
|
15
17
|
];
|
|
16
|
-
/** Comment keywords that must reference a ticket by default (`// TODO:
|
|
18
|
+
/** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
|
|
17
19
|
export const DEFAULT_COMMENT_KEYWORDS = ['FIXME', 'TODO'];
|
|
18
|
-
//# sourceMappingURL=constants.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
import { type Analysis } from './analyze.
|
|
3
|
-
import { type ResolvedOptions } from './options.
|
|
4
|
-
export declare const REPO_URL = "https://github.com/Huldoser/eslint-plugin-test-governance";
|
|
2
|
+
import { type Analysis } from './analyze.ts';
|
|
3
|
+
import { type ResolvedOptions } from './options.ts';
|
|
5
4
|
/** Rules take no options of their own: everything is shared through `settings['test-governance']`. */
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type RuleOptions = [];
|
|
6
|
+
interface RuleDocs {
|
|
8
7
|
description: string;
|
|
9
8
|
recommended: 'error' | 'warn';
|
|
10
9
|
url: string;
|
|
@@ -15,15 +14,15 @@ export type Rule<MessageIds extends string> = TSESLint.RuleModule<MessageIds, Ru
|
|
|
15
14
|
};
|
|
16
15
|
};
|
|
17
16
|
export interface RuleDefinition<MessageIds extends string> {
|
|
18
|
-
name: string;
|
|
19
17
|
meta: Omit<TSESLint.RuleMetaData<MessageIds, RuleDocs>, 'docs' | 'schema' | 'defaultOptions'> & {
|
|
20
|
-
docs:
|
|
18
|
+
docs: RuleDocs;
|
|
19
|
+
schema: [];
|
|
21
20
|
};
|
|
22
|
-
|
|
21
|
+
create(context: Readonly<TSESLint.RuleContext<MessageIds, RuleOptions>>, analysis: Analysis, options: ResolvedOptions): void;
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
25
24
|
* Builds a rule that reads its options from `settings['test-governance']`.
|
|
26
|
-
* `
|
|
25
|
+
* `create` runs once per file, at the end, with the shared analysis.
|
|
27
26
|
*/
|
|
28
27
|
export declare function createRule<MessageIds extends string>(definition: RuleDefinition<MessageIds>): Rule<MessageIds>;
|
|
29
|
-
|
|
28
|
+
export {};
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { analyze } from
|
|
2
|
-
import { resolveOptions } from
|
|
3
|
-
export const REPO_URL = 'https://github.com/Huldoser/eslint-plugin-test-governance';
|
|
1
|
+
import { analyze } from "./analyze.js";
|
|
2
|
+
import { resolveOptions } from "./options.js";
|
|
4
3
|
/**
|
|
5
4
|
* Builds a rule that reads its options from `settings['test-governance']`.
|
|
6
|
-
* `
|
|
5
|
+
* `create` runs once per file, at the end, with the shared analysis.
|
|
7
6
|
*/
|
|
8
7
|
export function createRule(definition) {
|
|
9
8
|
return {
|
|
10
|
-
meta:
|
|
11
|
-
...definition.meta,
|
|
12
|
-
docs: { ...definition.meta.docs, url: `${REPO_URL}/blob/main/docs/rules/${definition.name}.md` },
|
|
13
|
-
schema: [],
|
|
14
|
-
},
|
|
9
|
+
meta: definition.meta,
|
|
15
10
|
defaultOptions: [],
|
|
16
11
|
create(context) {
|
|
17
12
|
const options = resolveOptions(context.settings);
|
|
18
13
|
return {
|
|
19
14
|
'Program:exit'() {
|
|
20
|
-
definition.
|
|
15
|
+
definition.create(context, analyze(context.sourceCode, options), options);
|
|
21
16
|
},
|
|
22
17
|
};
|
|
23
18
|
},
|
|
24
19
|
};
|
|
25
20
|
}
|
|
26
|
-
//# sourceMappingURL=create-rule.js.map
|
package/dist/utils/distance.d.ts
CHANGED
package/dist/utils/distance.js
CHANGED
package/dist/utils/errors.d.ts
CHANGED
package/dist/utils/errors.js
CHANGED
package/dist/utils/fix.d.ts
CHANGED
|
@@ -6,7 +6,4 @@ export declare function keywordRange(sourceCode: SourceCode, comment: TSESTree.C
|
|
|
6
6
|
export declare function removeComment(sourceCode: SourceCode, comment: TSESTree.Comment): TSESLint.RuleFix;
|
|
7
7
|
/** Indentation of the line `comment` starts on. */
|
|
8
8
|
export declare function indentOf(sourceCode: SourceCode, comment: TSESTree.Comment): string;
|
|
9
|
-
/** Range of the `index`-th occurrence of `tag` in the source text of `node`. */
|
|
10
|
-
export declare function tagRange(sourceCode: SourceCode, node: TSESTree.Node, tag: string, index: number): TSESTree.Range | undefined;
|
|
11
9
|
export {};
|
|
12
|
-
//# sourceMappingURL=fix.d.ts.map
|
package/dist/utils/fix.js
CHANGED
|
@@ -24,11 +24,3 @@ export function indentOf(sourceCode, comment) {
|
|
|
24
24
|
const line = sourceCode.lines[comment.loc.start.line - 1];
|
|
25
25
|
return line.slice(0, line.length - line.trimStart().length);
|
|
26
26
|
}
|
|
27
|
-
/** Range of the `index`-th occurrence of `tag` in the source text of `node`. */
|
|
28
|
-
export function tagRange(sourceCode, node, tag, index) {
|
|
29
|
-
const text = sourceCode.getText(node);
|
|
30
|
-
const re = new RegExp(`(?<![\\w@])${escapeRegExp(tag)}(?![\\w-])`, 'g');
|
|
31
|
-
const match = [...text.matchAll(re)].at(index);
|
|
32
|
-
return match === undefined ? undefined : [node.range[0] + match.index, node.range[0] + match.index + tag.length];
|
|
33
|
-
}
|
|
34
|
-
//# sourceMappingURL=fix.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { BuiltinStateName, FrameworkName } from './constants.ts';
|
|
2
|
+
/** A state a test enters through its own code, as opposed to a tag. */
|
|
3
|
+
export type Modifier = 'skip' | 'fixme' | 'fail' | 'slow' | 'todo';
|
|
4
|
+
/** What a name from a framework stands for: `xit` is a test with `skip`, `suite` a describe block. */
|
|
5
|
+
export interface Root {
|
|
6
|
+
kind: 'test' | 'describe';
|
|
7
|
+
modifiers: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** The state a member puts a test in. A conditional one, like Vitest's `skipIf(...)`, may not apply. */
|
|
10
|
+
interface Effect {
|
|
11
|
+
modifier: Modifier;
|
|
12
|
+
conditional: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** How a test framework declares tests and puts them in states. */
|
|
15
|
+
export interface Framework {
|
|
16
|
+
name: FrameworkName;
|
|
17
|
+
/** Whether `source` is one of the framework's own modules. */
|
|
18
|
+
ownsModule(source: string): boolean;
|
|
19
|
+
/** Test functions the framework's modules export, by exported name. */
|
|
20
|
+
exports: ReadonlyMap<string, Root>;
|
|
21
|
+
/** What a default import from the framework's module is. */
|
|
22
|
+
defaultExport?: Root;
|
|
23
|
+
/** Test functions that exist without an import, besides `testFunctions`. */
|
|
24
|
+
globals: ReadonlyMap<string, Root>;
|
|
25
|
+
/** The default `testFunctions`. */
|
|
26
|
+
testFunctions: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Members a test or describe call may have, as in `test.skip(...)`. A member that is called to build
|
|
29
|
+
* the function, as in `test.each(table)('buys %s', fn)`, is written with parentheses: `each()`.
|
|
30
|
+
*/
|
|
31
|
+
testMembers: ReadonlySet<string>;
|
|
32
|
+
describeMembers: ReadonlySet<string>;
|
|
33
|
+
/** Members that are called to build the function that declares the tests. */
|
|
34
|
+
factories: ReadonlySet<string>;
|
|
35
|
+
/** The state each member puts a test in. */
|
|
36
|
+
effects: ReadonlyMap<string, Effect>;
|
|
37
|
+
/** Keys of an options object that put a test in a state, as in Vitest's `{ skip: true }`. */
|
|
38
|
+
optionEffects: ReadonlyMap<string, Modifier>;
|
|
39
|
+
/** The key of the details or options object that holds tags. */
|
|
40
|
+
tagKey?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The parameter of a test's body that can change its state at runtime, such as Playwright's
|
|
43
|
+
* `testInfo` or Vitest's test context, and the methods it has for that.
|
|
44
|
+
*/
|
|
45
|
+
info?: {
|
|
46
|
+
param: number;
|
|
47
|
+
methods: ReadonlySet<string>;
|
|
48
|
+
};
|
|
49
|
+
/** Built-in states a test can be put in with this framework. */
|
|
50
|
+
states: ReadonlySet<BuiltinStateName>;
|
|
51
|
+
/** Playwright's own API on `test`: `test.describe`, `test.step`, `test.info()` and runtime `test.skip()`. */
|
|
52
|
+
playwright: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Methods on `testInfo` and on `test` itself that change a test's state at runtime. */
|
|
55
|
+
export declare const RUNTIME_MODIFIERS: ReadonlySet<string>;
|
|
56
|
+
export declare const FRAMEWORKS: Record<FrameworkName, Framework>;
|
|
57
|
+
/** Whether `source` belongs to a test runner other than `framework`, so its names are never tests here. */
|
|
58
|
+
export declare function isOtherRunner(framework: Framework, source: string): boolean;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const TEST = { kind: 'test', modifiers: [] };
|
|
2
|
+
const DESCRIBE = { kind: 'describe', modifiers: [] };
|
|
3
|
+
const PLAYWRIGHT_MODULES = new Set(['@playwright/test', 'playwright/test']);
|
|
4
|
+
/** Test runners besides Playwright. A name imported from one of them belongs to that runner. */
|
|
5
|
+
const OTHER_RUNNERS = new Set(['vitest', '@jest/globals', 'node:test', 'bun:test', 'mocha', 'ava', 'tap', 'uvu']);
|
|
6
|
+
/** `@playwright/test` and the component-testing packages such as `@playwright/experimental-ct-react`. */
|
|
7
|
+
function isPlaywrightModule(source) {
|
|
8
|
+
return PLAYWRIGHT_MODULES.has(source) || source.startsWith('@playwright/experimental-ct-');
|
|
9
|
+
}
|
|
10
|
+
/** Methods on `testInfo` and on `test` itself that change a test's state at runtime. */
|
|
11
|
+
export const RUNTIME_MODIFIERS = new Set(['skip', 'fixme', 'fail', 'slow']);
|
|
12
|
+
const effects = (entries) => new Map(entries.map(([member, modifier, conditional = false]) => [member, { modifier, conditional }]));
|
|
13
|
+
const playwright = {
|
|
14
|
+
name: 'playwright',
|
|
15
|
+
ownsModule: isPlaywrightModule,
|
|
16
|
+
exports: new Map([['test', TEST]]),
|
|
17
|
+
defaultExport: TEST,
|
|
18
|
+
globals: new Map(),
|
|
19
|
+
testFunctions: ['test'],
|
|
20
|
+
testMembers: new Set(['only', 'skip', 'fixme', 'fail', 'slow']),
|
|
21
|
+
describeMembers: new Set(['only', 'skip', 'fixme', 'serial', 'parallel']),
|
|
22
|
+
factories: new Set(),
|
|
23
|
+
effects: effects([
|
|
24
|
+
['skip', 'skip'],
|
|
25
|
+
['fixme', 'fixme'],
|
|
26
|
+
['fail', 'fail'],
|
|
27
|
+
['slow', 'slow'],
|
|
28
|
+
]),
|
|
29
|
+
optionEffects: new Map(),
|
|
30
|
+
tagKey: 'tag',
|
|
31
|
+
info: { param: 1, methods: RUNTIME_MODIFIERS },
|
|
32
|
+
states: new Set(['skip', 'fixme', 'fail', 'slow', 'new', 'unstable']),
|
|
33
|
+
playwright: true,
|
|
34
|
+
};
|
|
35
|
+
const JEST_FUNCTIONS = new Map([
|
|
36
|
+
['test', TEST],
|
|
37
|
+
['it', TEST],
|
|
38
|
+
['xtest', { kind: 'test', modifiers: ['skip'] }],
|
|
39
|
+
['xit', { kind: 'test', modifiers: ['skip'] }],
|
|
40
|
+
['fit', { kind: 'test', modifiers: ['only'] }],
|
|
41
|
+
['describe', DESCRIBE],
|
|
42
|
+
['xdescribe', { kind: 'describe', modifiers: ['skip'] }],
|
|
43
|
+
['fdescribe', { kind: 'describe', modifiers: ['only'] }],
|
|
44
|
+
]);
|
|
45
|
+
const jest = {
|
|
46
|
+
name: 'jest',
|
|
47
|
+
ownsModule: (source) => source === '@jest/globals',
|
|
48
|
+
exports: JEST_FUNCTIONS,
|
|
49
|
+
globals: new Map([...JEST_FUNCTIONS].filter(([name]) => name !== 'test' && name !== 'it')),
|
|
50
|
+
testFunctions: ['test', 'it'],
|
|
51
|
+
testMembers: new Set(['only', 'skip', 'todo', 'failing', 'concurrent', 'each()']),
|
|
52
|
+
describeMembers: new Set(['only', 'skip', 'each()']),
|
|
53
|
+
factories: new Set(['each']),
|
|
54
|
+
effects: effects([
|
|
55
|
+
['skip', 'skip'],
|
|
56
|
+
['todo', 'todo'],
|
|
57
|
+
['failing', 'fail'],
|
|
58
|
+
]),
|
|
59
|
+
optionEffects: new Map(),
|
|
60
|
+
states: new Set(['skip', 'todo', 'fail', 'new', 'unstable']),
|
|
61
|
+
playwright: false,
|
|
62
|
+
};
|
|
63
|
+
const VITEST_FACTORIES = ['each()', 'for()', 'skipIf()', 'runIf()'];
|
|
64
|
+
const vitest = {
|
|
65
|
+
name: 'vitest',
|
|
66
|
+
ownsModule: (source) => source === 'vitest',
|
|
67
|
+
exports: new Map([
|
|
68
|
+
['test', TEST],
|
|
69
|
+
['it', TEST],
|
|
70
|
+
['describe', DESCRIBE],
|
|
71
|
+
['suite', DESCRIBE],
|
|
72
|
+
]),
|
|
73
|
+
globals: new Map([
|
|
74
|
+
['describe', DESCRIBE],
|
|
75
|
+
['suite', DESCRIBE],
|
|
76
|
+
]),
|
|
77
|
+
testFunctions: ['test', 'it'],
|
|
78
|
+
testMembers: new Set(['only', 'skip', 'todo', 'fails', 'concurrent', 'sequential', ...VITEST_FACTORIES]),
|
|
79
|
+
describeMembers: new Set(['only', 'skip', 'todo', 'concurrent', 'sequential', 'shuffle', ...VITEST_FACTORIES]),
|
|
80
|
+
factories: new Set(['each', 'for', 'skipIf', 'runIf']),
|
|
81
|
+
effects: effects([
|
|
82
|
+
['skip', 'skip'],
|
|
83
|
+
['todo', 'todo'],
|
|
84
|
+
['fails', 'fail'],
|
|
85
|
+
['skipIf()', 'skip', true],
|
|
86
|
+
['runIf()', 'skip', true],
|
|
87
|
+
]),
|
|
88
|
+
optionEffects: new Map([
|
|
89
|
+
['skip', 'skip'],
|
|
90
|
+
['todo', 'todo'],
|
|
91
|
+
['fails', 'fail'],
|
|
92
|
+
]),
|
|
93
|
+
tagKey: 'tags',
|
|
94
|
+
info: { param: 0, methods: new Set(['skip']) },
|
|
95
|
+
states: new Set(['skip', 'todo', 'fail', 'new', 'unstable']),
|
|
96
|
+
playwright: false,
|
|
97
|
+
};
|
|
98
|
+
export const FRAMEWORKS = { playwright, jest, vitest };
|
|
99
|
+
/** Whether `source` belongs to a test runner other than `framework`, so its names are never tests here. */
|
|
100
|
+
export function isOtherRunner(framework, source) {
|
|
101
|
+
return (isPlaywrightModule(source) || OTHER_RUNNERS.has(source)) && !framework.ownsModule(source);
|
|
102
|
+
}
|
package/dist/utils/options.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type BuiltinStateName } from './constants.
|
|
2
|
-
import { type
|
|
1
|
+
import { type BuiltinStateName, type FrameworkName } from './constants.ts';
|
|
2
|
+
import { type Framework, type Modifier } from './frameworks.ts';
|
|
3
|
+
import { type TicketMatcher, type TicketSpec } from './tickets.ts';
|
|
3
4
|
export declare const SETTINGS_KEY = "test-governance";
|
|
4
5
|
/** A state override in `states`. `false` turns a state off, `true` turns it on. */
|
|
5
6
|
export interface StateOverride {
|
|
@@ -14,36 +15,50 @@ export interface CustomState {
|
|
|
14
15
|
marker: string;
|
|
15
16
|
ticket?: TicketSpec | TicketSpec[];
|
|
16
17
|
}
|
|
17
|
-
export type { BuiltinStateName } from './constants.js';
|
|
18
18
|
/** Options shared by every rule. Set them once in `settings['test-governance']`. */
|
|
19
19
|
export interface GovernanceOptions {
|
|
20
|
-
/**
|
|
20
|
+
/** The test framework of the files the config applies to. Defaults to `'playwright'`. */
|
|
21
|
+
framework?: FrameworkName;
|
|
22
|
+
/**
|
|
23
|
+
* Names that are treated as the framework's `test` function. Defaults to `['test']` for Playwright and
|
|
24
|
+
* `['test', 'it']` for Jest and Vitest. Your list replaces the default one, so include those names if
|
|
25
|
+
* you still need them.
|
|
26
|
+
*/
|
|
21
27
|
testFunctions?: string[];
|
|
22
28
|
/** Accepted ticket formats. Defaults to `{ preset: 'any' }`. */
|
|
23
29
|
ticket?: TicketSpec | TicketSpec[];
|
|
24
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Tickets that are rejected as placeholders. `*` matches any characters. Your list replaces the default
|
|
32
|
+
* one (`TODO`, `TBD`, `XXX-*`, ...); tickets numbered 0 are always rejected.
|
|
33
|
+
*/
|
|
25
34
|
placeholders?: string[];
|
|
26
35
|
/** Turns on the `@new` and `@unstable` states. */
|
|
27
36
|
lifecycleTags?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Turns built-in states on or off, renames their marker or gives them their own ticket format. `skip`,
|
|
39
|
+
* `fixme` and `todo` are on by default, `fail` and `slow` off; a setting for `new` or `unstable` wins over
|
|
40
|
+
* `lifecycleTags`. `fixme` and `slow` exist only in Playwright and `todo` only in Jest and Vitest.
|
|
41
|
+
*/
|
|
28
42
|
states?: Partial<Record<BuiltinStateName, boolean | StateOverride>>;
|
|
43
|
+
/** Adds a state for a tag, e.g. `{ quarantine: { when: '@quarantine', marker: 'QUARANTINE' } }`. */
|
|
29
44
|
customStates?: Record<string, CustomState>;
|
|
30
45
|
/** Require a ticket for conditional skips such as `test.skip(browserName === 'webkit')`. */
|
|
31
46
|
requireTicketForConditional?: boolean;
|
|
32
47
|
/** Let blank lines separate a marker from the test it belongs to. */
|
|
33
48
|
allowBlankLine?: boolean;
|
|
34
49
|
/**
|
|
35
|
-
* Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO:
|
|
36
|
-
* Defaults to `{ keywords: ['FIXME', 'TODO'] }
|
|
50
|
+
* Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: TRADE-123`.
|
|
51
|
+
* Defaults to `{ keywords: ['FIXME', 'TODO'] }`, and your keywords replace those; `false` allows free-form
|
|
52
|
+
* comments.
|
|
37
53
|
*/
|
|
38
54
|
comments?: false | {
|
|
39
55
|
keywords?: string[];
|
|
40
56
|
};
|
|
41
|
-
/** Allow free text after the tickets, e.g. `// SKIP:
|
|
57
|
+
/** Allow free text after the tickets, e.g. `// SKIP: TRADE-1 flaky on CI`. Off by default: details belong in the ticket. */
|
|
42
58
|
allowNotes?: boolean;
|
|
43
59
|
/** Report titles that aren't static text when a tag state is on, because their tags can't be read. */
|
|
44
60
|
reportDynamicTitles?: boolean;
|
|
45
61
|
}
|
|
46
|
-
export type Modifier = 'skip' | 'fixme' | 'fail' | 'slow';
|
|
47
62
|
interface StateBase {
|
|
48
63
|
name: string;
|
|
49
64
|
marker: string;
|
|
@@ -62,6 +77,7 @@ export type TagStateDef = Extract<StateDef, {
|
|
|
62
77
|
}>;
|
|
63
78
|
export declare function isTagState(state: StateDef): state is TagStateDef;
|
|
64
79
|
export interface ResolvedOptions {
|
|
80
|
+
framework: Framework;
|
|
65
81
|
testFunctions: Set<string>;
|
|
66
82
|
states: StateDef[];
|
|
67
83
|
requireTicketForConditional: boolean;
|
|
@@ -78,11 +94,10 @@ export interface ResolvedOptions {
|
|
|
78
94
|
/** Ticket format for comments: the shared default format. */
|
|
79
95
|
commentTicket: TicketMatcher;
|
|
80
96
|
}
|
|
81
|
-
export { ConfigError } from './errors.
|
|
97
|
+
export { ConfigError } from './errors.ts';
|
|
82
98
|
/**
|
|
83
99
|
* Compiles `settings['test-governance']`. Every rule reads the same settings, so they always agree,
|
|
84
100
|
* and the result is cached per settings object, so the rules share one compile.
|
|
85
101
|
*/
|
|
86
102
|
export declare function resolveOptions(settings: unknown): ResolvedOptions;
|
|
87
103
|
export declare function compileOptions(options: GovernanceOptions): ResolvedOptions;
|
|
88
|
-
//# sourceMappingURL=options.d.ts.map
|
package/dist/utils/options.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { BUILTIN_STATE_NAMES, DEFAULT_COMMENT_KEYWORDS } from
|
|
2
|
-
import { ConfigError } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { BUILTIN_STATE_NAMES, DEFAULT_COMMENT_KEYWORDS, } from "./constants.js";
|
|
2
|
+
import { ConfigError } from "./errors.js";
|
|
3
|
+
import { FRAMEWORKS } from "./frameworks.js";
|
|
4
|
+
import { optionsSchema } from "./schema.js";
|
|
5
|
+
import { compileTicketSpec, DEFAULT_PLACEHOLDERS } from "./tickets.js";
|
|
6
|
+
import { validate } from "./validate.js";
|
|
6
7
|
export const SETTINGS_KEY = 'test-governance';
|
|
7
8
|
export function isTagState(state) {
|
|
8
9
|
return state.tag !== undefined;
|
|
@@ -12,10 +13,11 @@ const BUILTIN_STATES = {
|
|
|
12
13
|
fixme: { marker: 'FIXME', modifier: 'fixme', enabled: true },
|
|
13
14
|
fail: { marker: 'FAIL', modifier: 'fail', enabled: false },
|
|
14
15
|
slow: { marker: 'SLOW', modifier: 'slow', enabled: false },
|
|
16
|
+
todo: { marker: 'TODO', modifier: 'todo', enabled: true },
|
|
15
17
|
new: { marker: 'NEW', tag: '@new', lifecycle: true, enabled: false },
|
|
16
18
|
unstable: { marker: 'UNSTABLE', tag: '@unstable', lifecycle: true, enabled: false },
|
|
17
19
|
};
|
|
18
|
-
export { ConfigError } from
|
|
20
|
+
export { ConfigError } from "./errors.js";
|
|
19
21
|
function toSpecs(spec) {
|
|
20
22
|
return Array.isArray(spec) ? spec : [spec];
|
|
21
23
|
}
|
|
@@ -82,6 +84,7 @@ export function compileOptions(options) {
|
|
|
82
84
|
if (problems.length > 0) {
|
|
83
85
|
throw new ConfigError(`invalid options:\n${problems.map((problem) => ` - ${problem}`).join('\n')}`);
|
|
84
86
|
}
|
|
87
|
+
const framework = FRAMEWORKS[options.framework ?? 'playwright'];
|
|
85
88
|
const placeholders = options.placeholders ?? DEFAULT_PLACEHOLDERS;
|
|
86
89
|
const defaultTicket = toSpecs(options.ticket ?? { preset: 'any' });
|
|
87
90
|
const compile = (spec) => compileTicketSpec(spec === undefined ? defaultTicket : toSpecs(spec), placeholders);
|
|
@@ -92,7 +95,8 @@ export function compileOptions(options) {
|
|
|
92
95
|
const raw = overrides[name];
|
|
93
96
|
const override = typeof raw === 'boolean' ? { enabled: raw } : (raw ?? {});
|
|
94
97
|
const enabled = override.enabled ?? (def.lifecycle ? (options.lifecycleTags ?? def.enabled) : def.enabled);
|
|
95
|
-
|
|
98
|
+
// A state the framework has no way to enter, such as `fixme` in Jest, stays off.
|
|
99
|
+
if (!enabled || !framework.states.has(name))
|
|
96
100
|
continue;
|
|
97
101
|
const base = {
|
|
98
102
|
name,
|
|
@@ -123,7 +127,8 @@ export function compileOptions(options) {
|
|
|
123
127
|
}
|
|
124
128
|
const commentKeywords = options.comments === false ? [] : (options.comments?.keywords ?? DEFAULT_COMMENT_KEYWORDS);
|
|
125
129
|
return {
|
|
126
|
-
|
|
130
|
+
framework,
|
|
131
|
+
testFunctions: new Set(options.testFunctions ?? framework.testFunctions),
|
|
127
132
|
states,
|
|
128
133
|
requireTicketForConditional: options.requireTicketForConditional ?? false,
|
|
129
134
|
allowBlankLine: options.allowBlankLine ?? false,
|
|
@@ -134,4 +139,3 @@ export function compileOptions(options) {
|
|
|
134
139
|
commentTicket: defaultMatcher,
|
|
135
140
|
};
|
|
136
141
|
}
|
|
137
|
-
//# sourceMappingURL=options.js.map
|
package/dist/utils/schema.d.ts
CHANGED
package/dist/utils/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BUILTIN_STATE_NAMES, MARKER_RE, TAG_RE, TICKET_PRESETS } from
|
|
1
|
+
import { BUILTIN_STATE_NAMES, FRAMEWORK_NAMES, MARKER_RE, TAG_RE, TICKET_PRESETS } from "./constants.js";
|
|
2
2
|
const MARKER_HINT = 'an uppercase keyword such as NEEDS-DATA (letters, digits, "-" and "_")';
|
|
3
3
|
const TAG_HINT = 'a tag such as @needs-data';
|
|
4
4
|
/** Jira project and Linear team keys, as they appear in tickets: `WEB` in `WEB-123`. */
|
|
@@ -40,6 +40,7 @@ const stateOverrideSchema = {
|
|
|
40
40
|
export const optionsSchema = {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
43
|
+
framework: { type: 'string', enum: [...FRAMEWORK_NAMES] },
|
|
43
44
|
testFunctions: { type: 'array', items: { type: 'string' } },
|
|
44
45
|
ticket: ticketSchema,
|
|
45
46
|
placeholders: { type: 'array', items: { type: 'string' } },
|
|
@@ -81,4 +82,3 @@ export const optionsSchema = {
|
|
|
81
82
|
},
|
|
82
83
|
additionalProperties: false,
|
|
83
84
|
};
|
|
84
|
-
//# sourceMappingURL=schema.js.map
|
package/dist/utils/tickets.d.ts
CHANGED
package/dist/utils/tickets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ConfigError } from
|
|
2
|
-
export const DEFAULT_PLACEHOLDERS = ['TODO', 'TBD', 'XXX-*', '0'
|
|
1
|
+
import { ConfigError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_PLACEHOLDERS = ['TODO', 'TBD', 'XXX-*', '0'];
|
|
3
3
|
// Every pattern below is anchored and has no nested or overlapping quantifiers, so matching is
|
|
4
4
|
// linear in the token length. URLs are parsed with `new URL` rather than a regex.
|
|
5
5
|
const KEY_RE = /^([A-Z][A-Z0-9_]+)-\d+$/;
|
|
@@ -8,7 +8,8 @@ const GITHUB_REF_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[\w.-]+#\d+$/;
|
|
|
8
8
|
const GITLAB_REF_RE = /^[\w.-]+(?:\/[\w.-]+)+#\d+$/;
|
|
9
9
|
const AZURE_REF_RE = /^AB#\d+$/;
|
|
10
10
|
const DIGITS_RE = /^\d+$/;
|
|
11
|
-
|
|
11
|
+
// Jira Server and Data Center can run under a path, as in https://issues.example.com/jira/browse/TRADE-1.
|
|
12
|
+
const JIRA_PATH_RE = /\/browse\/([A-Z][A-Z0-9_]+-\d+)\/?$/;
|
|
12
13
|
const GITHUB_PATH_RE = /^\/[^/]+\/[^/]+\/(?:issues|pull)\/\d+\/?$/;
|
|
13
14
|
const GITLAB_PATH_RE = /\/-\/(?:issues|merge_requests|work_items)\/\d+\/?$/;
|
|
14
15
|
const LINEAR_PATH_RE = /^\/[^/]+\/issue\/([A-Z][A-Z0-9]*-\d+)(?:\/[^/]*)?$/;
|
|
@@ -171,4 +172,3 @@ export function compileTicketSpec(specs, placeholders) {
|
|
|
171
172
|
example: presets[0].example,
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
|
-
//# sourceMappingURL=tickets.js.map
|
package/dist/utils/validate.d.ts
CHANGED
package/dist/utils/validate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { editDistance } from
|
|
1
|
+
import { editDistance } from "./distance.js";
|
|
2
2
|
function kindOf(value) {
|
|
3
3
|
if (value === null)
|
|
4
4
|
return 'null';
|
|
@@ -29,7 +29,8 @@ const toJson = JSON.stringify;
|
|
|
29
29
|
function show(value) {
|
|
30
30
|
if (typeof value === 'function')
|
|
31
31
|
return 'a function';
|
|
32
|
-
|
|
32
|
+
// JSON shows a regex as `{}`.
|
|
33
|
+
const text = value instanceof RegExp ? String(value) : (toJson(value) ?? String(value));
|
|
33
34
|
return text.length > 40 ? `${text.slice(0, 37)}...` : text;
|
|
34
35
|
}
|
|
35
36
|
function join(path, key) {
|
|
@@ -119,4 +120,3 @@ export function validate(value, schema, path = '') {
|
|
|
119
120
|
}
|
|
120
121
|
return [];
|
|
121
122
|
}
|
|
122
|
-
//# sourceMappingURL=validate.js.map
|
|
@@ -22,15 +22,15 @@ Incorrect:
|
|
|
22
22
|
<!-- example: invalid -->
|
|
23
23
|
|
|
24
24
|
```js
|
|
25
|
-
// SKIP:
|
|
26
|
-
test.fixme('
|
|
25
|
+
// SKIP: TRADE-500
|
|
26
|
+
test.fixme('shows the unfilled quantity of a partly filled order', async () => {});
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
<!-- example: invalid settings={"lifecycleTags":true} -->
|
|
30
30
|
|
|
31
31
|
```js
|
|
32
|
-
// SKIP:
|
|
33
|
-
test.skip('
|
|
32
|
+
// SKIP: TRADE-500
|
|
33
|
+
test.skip('shows the unfilled quantity of a partly filled order @unstable', async () => {});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Correct:
|
|
@@ -38,18 +38,26 @@ Correct:
|
|
|
38
38
|
<!-- example: valid -->
|
|
39
39
|
|
|
40
40
|
```js
|
|
41
|
-
// FIXME:
|
|
42
|
-
test.fixme('
|
|
41
|
+
// FIXME: TRADE-500
|
|
42
|
+
test.fixme('shows the unfilled quantity of a partly filled order', async () => {});
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
<!-- example: valid settings={"lifecycleTags":true} -->
|
|
46
46
|
|
|
47
47
|
```js
|
|
48
|
-
// SKIP:
|
|
49
|
-
// UNSTABLE:
|
|
50
|
-
test.skip('
|
|
48
|
+
// SKIP: TRADE-500
|
|
49
|
+
// UNSTABLE: TRADE-500
|
|
50
|
+
test.skip('shows the unfilled quantity of a partly filled order @unstable', async () => {});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
## Options
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
This rule has no options of its own. It reads the shared options described in the
|
|
56
|
+
[README](../../README.md#options). These change what it reports:
|
|
57
|
+
|
|
58
|
+
- `lifecycleTags`, `states` and `customStates`: which states are on, and the marker keyword each
|
|
59
|
+
one expects.
|
|
60
|
+
- `requireTicketForConditional`: conditional skips such as `test.skip(browserName === 'webkit')`
|
|
61
|
+
need a marker only when this is set, so only then is a wrong marker above one reported.
|
|
62
|
+
- `allowBlankLine`: whether a marker separated from its test by a blank line still belongs to it.
|
|
63
|
+
- `framework` and `testFunctions`: which functions count as the framework's `test`.
|