eslint-plugin-test-governance 0.4.0 → 0.5.1
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 +72 -1
- package/README.md +75 -54
- package/dist/index.d.ts +8 -9
- package/dist/index.js +17 -13
- package/dist/rules/no-conflicting-states.js +9 -3
- package/dist/rules/no-orphaned-marker.js +1 -1
- package/dist/rules/require-ticket.d.ts +1 -1
- package/dist/rules/require-ticket.js +6 -2
- package/dist/utils/analyze.d.ts +18 -4
- package/dist/utils/analyze.js +232 -58
- package/dist/utils/frameworks.d.ts +20 -4
- package/dist/utils/frameworks.js +14 -2
- package/dist/utils/options.d.ts +11 -5
- package/dist/utils/options.js +2 -2
- package/dist/utils/schema.js +8 -2
- package/dist/utils/validate.d.ts +4 -0
- package/dist/utils/validate.js +7 -2
- package/docs/rules/marker-matches-state.md +1 -1
- package/docs/rules/no-conflicting-states.md +4 -2
- package/docs/rules/no-orphaned-marker.md +28 -1
- package/docs/rules/require-ticket-in-comments.md +1 -1
- package/docs/rules/require-ticket.md +40 -14
- package/docs/upgrading.md +131 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,75 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.1] - 2026-10-13
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- In Jest and Vitest, a test or describe whose title is a variable and whose body is defined
|
|
14
|
+
elsewhere, as in `test.skip(title, placeOrder)`, is checked. Before, it was read as Playwright's
|
|
15
|
+
runtime `test.skip(condition, description)` and left alone, though Jest and Vitest have no such call.
|
|
16
|
+
In Playwright, a title kept in a `const` in the same file now counts as a title too.
|
|
17
|
+
- Vitest's `test.skip('title')` and `describe.skip('title')` with no body need a `// SKIP:` marker, since
|
|
18
|
+
Vitest reports them as skipped. Before, only `.todo` was checked without a body.
|
|
19
|
+
- Vitest's `context.skip('note')` always skips, so it needs a `// SKIP:` marker. Before, the note was
|
|
20
|
+
read as a condition and the skip as conditional.
|
|
21
|
+
- In Playwright, `describe` taken out of `test`, as in `const { describe } = test`, works like
|
|
22
|
+
`test.describe`: `describe.skip(...)` needs a marker, and tags in its title apply to the tests inside.
|
|
23
|
+
Before, it wasn't recognized.
|
|
24
|
+
- A function declared inside another function that happens to be called `test` is no longer taken for
|
|
25
|
+
the framework's `test`, as a local variable already wasn't.
|
|
26
|
+
|
|
27
|
+
## [0.5.0] - 2026-10-08
|
|
28
|
+
|
|
29
|
+
This release changes how the plugin is configured, ahead of 1.0.
|
|
30
|
+
[Upgrading to 0.5](https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md#upgrading-to-05)
|
|
31
|
+
shows what to change, with a before and after for each one.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Breaking:** `configure()` needs a `framework`, as in
|
|
36
|
+
`configure({ framework: 'playwright', ticket: { preset: 'jira' } })`, and so does
|
|
37
|
+
`settings['test-governance']` in a config you build yourself. Before, a config without one checked
|
|
38
|
+
files as Playwright tests. Now `configure()` fails when the config file loads, and hand-written
|
|
39
|
+
settings fail on the first lint, with a message that names the three frameworks. To share options
|
|
40
|
+
between frameworks, pass the same options to a `configure()` for each one.
|
|
41
|
+
- **Breaking:** `reportDynamicTitles` is renamed to `reportUnreadableTags`, because it also reports
|
|
42
|
+
tags and details objects imported from another module, not only titles. It works the same way, and
|
|
43
|
+
a config that still uses the old name fails with a message that names the new one.
|
|
44
|
+
- Vitest's `skipIf(true)` and `runIf(false)` always skip, so they need a ticket, like `{ skip: true }`
|
|
45
|
+
and Playwright's `test.skip(true)`. `skipIf(false)` and `runIf(true)` never skip, so a `// SKIP:`
|
|
46
|
+
marker above one is reported as left over. Any other condition is still a conditional skip. Before,
|
|
47
|
+
a literal condition was treated like any other.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- **Breaking:** `configs.recommended` is gone. It checked every file as Playwright tests, so pick the
|
|
52
|
+
config for your framework instead: `configs.playwright`, `configs.jest` or `configs.vitest`. For
|
|
53
|
+
Playwright, replace `configs.recommended` with `configs.playwright`, and
|
|
54
|
+
`extends: ['test-governance/recommended']` with `extends: ['test-governance/playwright']`. The
|
|
55
|
+
rules and their severities are the same. A config that still reads `configs.recommended` fails when
|
|
56
|
+
it loads, with a message that points to the upgrade guide, rather than spreading nothing and turning
|
|
57
|
+
the rules off.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Tags kept in a `const` or an enum in the same file count, as in `{ tag: TAGS.NEW }` after
|
|
62
|
+
`const TAGS = { NEW: '@new' } as const`, with spreads, `as const` and `satisfies`, and so do titles
|
|
63
|
+
and details objects kept in a constant. Before, a test tagged this way needed no ticket, and a
|
|
64
|
+
correct `// NEW:` marker above it was reported as left over.
|
|
65
|
+
- Tags and details imported from another module can't be read, so a marker for a tag state above such
|
|
66
|
+
a test is no longer reported as left over, and neither is a Vitest state marker such as `// SKIP:`
|
|
67
|
+
above a test whose options object is imported. With `reportUnreadableTags: true`, the unreadable
|
|
68
|
+
tags are reported, like titles that aren't static text.
|
|
69
|
+
- Vitest tag names, which are written without the `@` (`{ tags: ['flaky'] }`), match the states:
|
|
70
|
+
`'new'` is `@new`, and a custom state with `when: '@flaky'` matches `'flaky'`. Before, they never
|
|
71
|
+
matched. `no-conflicting-states` shows and fixes them without the `@`.
|
|
72
|
+
- `context.skip()` in Vitest's `beforeEach`, global or imported, needs a ticket, like the one in
|
|
73
|
+
`test.beforeEach`. Before, it wasn't checked.
|
|
74
|
+
- A marker in the wrong case with a ticket, such as `// skip: TRADE-1`, is reported by
|
|
75
|
+
`no-orphaned-marker` once the test is no longer skipped, and by `marker-matches-state` above a test in
|
|
76
|
+
another state. Before, it was only reported while the test was still in that state.
|
|
77
|
+
|
|
9
78
|
## [0.4.0] - 2026-10-08
|
|
10
79
|
|
|
11
80
|
### Added
|
|
@@ -194,7 +263,9 @@ First release.
|
|
|
194
263
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
195
264
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
196
265
|
|
|
197
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
266
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.5.1...HEAD
|
|
267
|
+
[0.5.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.5.0...v0.5.1
|
|
268
|
+
[0.5.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.4.0...v0.5.0
|
|
198
269
|
[0.4.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.1...v0.4.0
|
|
199
270
|
[0.3.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...v0.3.1
|
|
200
271
|
[0.3.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.1...v0.3.0
|
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
ESLint rules that keep a Playwright, Jest or Vitest suite honest. Every skipped, fixme, todo, `@new`
|
|
8
8
|
or `@unstable` test must point at the ticket that tracks it:
|
|
9
9
|
|
|
10
|
+
<!-- example: valid -->
|
|
11
|
+
|
|
10
12
|
```ts
|
|
11
13
|
// SKIP: TRADE-123
|
|
12
14
|
test.skip('rejects a margin order above the buying power', async ({ page }) => {
|
|
@@ -18,7 +20,8 @@ Without the comment, `test-governance/require-ticket` fails the lint. When the t
|
|
|
18
20
|
skip removed, `no-orphaned-marker` flags the leftover comment. The markers are plain
|
|
19
21
|
comments, so `grep -rn "SKIP:" tests/` lists every disabled test and its ticket.
|
|
20
22
|
|
|
21
|
-
> **Status:** early release (0.x). Options may still change before 1.0.
|
|
23
|
+
> **Status:** early release (0.x). Options may still change before 1.0. Coming from 0.4?
|
|
24
|
+
> [Upgrading to 0.5](docs/upgrading.md#upgrading-to-05) shows what to change.
|
|
22
25
|
|
|
23
26
|
## Install
|
|
24
27
|
|
|
@@ -31,6 +34,9 @@ Requires ESLint 9 or 10 with flat config, and Node.js 22.12 or later. For TypeSc
|
|
|
31
34
|
|
|
32
35
|
## Usage
|
|
33
36
|
|
|
37
|
+
Pick the config for the framework your tests use, `configs.playwright`, `configs.jest` or
|
|
38
|
+
`configs.vitest`, and point it at those test files with `files`:
|
|
39
|
+
|
|
34
40
|
```js
|
|
35
41
|
// eslint.config.js
|
|
36
42
|
import tsParser from '@typescript-eslint/parser';
|
|
@@ -40,11 +46,16 @@ export default [
|
|
|
40
46
|
{ files: ['**/*.ts'], languageOptions: { parser: tsParser } },
|
|
41
47
|
{
|
|
42
48
|
files: ['tests/**/*.{js,ts}'],
|
|
43
|
-
...testGovernance.configs.
|
|
49
|
+
...testGovernance.configs.playwright,
|
|
44
50
|
},
|
|
45
51
|
];
|
|
46
52
|
```
|
|
47
53
|
|
|
54
|
+
There is no default config. Each framework declares, skips and tags tests its own way, so the rules
|
|
55
|
+
need to know which one a file uses. In a Jest or Vitest config, `test`, `it` and `describe` belong to
|
|
56
|
+
that framework, whether they are globals or imported from `@jest/globals` or `vitest`, the way the
|
|
57
|
+
Jest and Vitest ESLint plugins are set up.
|
|
58
|
+
|
|
48
59
|
With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
|
|
49
60
|
|
|
50
61
|
```js
|
|
@@ -57,25 +68,26 @@ export default defineConfig([
|
|
|
57
68
|
{ files: ['**/*.ts'], languageOptions: { parser: tsParser } },
|
|
58
69
|
{
|
|
59
70
|
files: ['tests/**/*.{js,ts}'],
|
|
60
|
-
extends: [testGovernance.configs.
|
|
71
|
+
extends: [testGovernance.configs.playwright],
|
|
61
72
|
},
|
|
62
73
|
]);
|
|
63
74
|
```
|
|
64
75
|
|
|
65
76
|
Any flat config file works. The package is ESM, and `require('eslint-plugin-test-governance')` in an
|
|
66
77
|
`eslint.config.cjs` returns the plugin itself. Inside `defineConfig()`, once the plugin is registered
|
|
67
|
-
under `plugins`, the string form `extends: ['test-governance/
|
|
78
|
+
under `plugins`, the string form `extends: ['test-governance/playwright']` works too. ESLint loads
|
|
68
79
|
an `eslint.config.ts` through [`jiti`](https://github.com/unjs/jiti), so install it as a dev
|
|
69
80
|
dependency too.
|
|
70
81
|
|
|
71
|
-
To change the defaults, use `configure()`. It
|
|
72
|
-
stored in `settings['test-governance']`, which all the rules read:
|
|
82
|
+
To change the defaults, use `configure()`. It takes the framework and your options, and returns that
|
|
83
|
+
framework's config with the options stored in `settings['test-governance']`, which all the rules read:
|
|
73
84
|
|
|
74
85
|
```js
|
|
75
86
|
export default [
|
|
76
87
|
{
|
|
77
88
|
files: ['tests/**/*.{js,ts}'],
|
|
78
89
|
...testGovernance.configure({
|
|
90
|
+
framework: 'playwright',
|
|
79
91
|
// Only accept Jira keys from these projects (or Jira URLs).
|
|
80
92
|
ticket: { preset: 'jira', projects: ['TRADE', 'RISK'] },
|
|
81
93
|
// Turn on the @new and @unstable states.
|
|
@@ -98,40 +110,41 @@ eslint-plugin-test-governance: invalid options:
|
|
|
98
110
|
- ticket.preset must be one of "any", "jira", "github", … (got "jria")
|
|
99
111
|
```
|
|
100
112
|
|
|
101
|
-
###
|
|
113
|
+
### Frameworks
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
In those files, `test`, `it` and `describe` belong to that framework, whether they are globals or
|
|
106
|
-
imported from `@jest/globals` or `vitest`. Each framework config sets only the framework, so the
|
|
107
|
-
options of an earlier `configure()` still apply to its files:
|
|
115
|
+
A project with tests in more than one framework gives each framework its own config, pointed at its
|
|
116
|
+
files. To share options between them, pass the same options to each `configure()`:
|
|
108
117
|
|
|
109
118
|
```js
|
|
119
|
+
const options = { ticket: { preset: 'jira', projects: ['TRADE'] } };
|
|
120
|
+
|
|
110
121
|
export default [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
115
|
-
// Unit tests run on Vitest; the e2e specs stay on Playwright.
|
|
116
|
-
{ files: ['src/**/*.test.ts'], ...testGovernance.configs.vitest },
|
|
122
|
+
// The e2e specs run on Playwright, the unit tests on Vitest.
|
|
123
|
+
{ files: ['e2e/**/*.spec.ts'], ...testGovernance.configure({ framework: 'playwright', ...options }) },
|
|
124
|
+
{ files: ['src/**/*.test.ts'], ...testGovernance.configure({ framework: 'vitest', ...options }) },
|
|
117
125
|
];
|
|
118
126
|
```
|
|
119
127
|
|
|
120
|
-
`
|
|
121
|
-
`
|
|
122
|
-
|
|
128
|
+
`configs.playwright`, `configs.jest` and `configs.vitest` set only the framework, so one listed after a
|
|
129
|
+
`configure()` for the same files keeps that config's options and changes only the framework. All three
|
|
130
|
+
turn on the same rules. What puts a test in a state in each framework:
|
|
123
131
|
|
|
124
|
-
| State (marker) | Playwright | Jest | Vitest
|
|
125
|
-
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
126
|
-
| `skip` (`// SKIP:`) | `test.skip(...)`, `test.describe.skip`, `test.skip()` in a body, `testInfo.skip()`, `test.step.skip`, `step.skip()` | `test.skip`, `it.skip`, `describe.skip`, `xit`, `xtest`, `xdescribe` | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `{ skip: true }`, `context.skip()` or `skip()` in a body |
|
|
127
|
-
| `todo` (`// TODO:`) | | `test.todo`, `it.todo` | `test.todo`, `describe.todo`, `{ todo: true }`
|
|
128
|
-
| `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | |
|
|
129
|
-
| `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }`
|
|
130
|
-
| `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | |
|
|
131
|
-
| conditional skip, no ticket by default | `test.skip(condition)`, `if (...) test.skip()` | | `skipIf(
|
|
132
|
-
| tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }`
|
|
132
|
+
| State (marker) | Playwright | Jest | Vitest |
|
|
133
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
134
|
+
| `skip` (`// SKIP:`) | `test.skip(...)`, `test.describe.skip`, `test.skip()` in a body, `testInfo.skip()`, `test.step.skip`, `step.skip()` | `test.skip`, `it.skip`, `describe.skip`, `xit`, `xtest`, `xdescribe` | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `skipIf(true)`, `runIf(false)`, `{ skip: true }`, `context.skip()`, `context.skip('note')` or `skip()` in a body or in `beforeEach` |
|
|
135
|
+
| `todo` (`// TODO:`) | | `test.todo`, `it.todo` | `test.todo`, `describe.todo`, `{ todo: true }` |
|
|
136
|
+
| `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | |
|
|
137
|
+
| `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` |
|
|
138
|
+
| `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | |
|
|
139
|
+
| conditional skip, no ticket by default | `test.skip(condition)`, `if (...) test.skip()` | | `skipIf(condition)`, `runIf(condition)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()` |
|
|
140
|
+
| tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }`, where `'new'` and `'@new'` both mean `@new` |
|
|
133
141
|
|
|
134
142
|
Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
|
|
143
|
+
A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
|
|
144
|
+
`runIf(true)` and `{ skip: false }` never do.
|
|
145
|
+
In Vitest, `test.skip('title')` and `describe.skip('title')` with no body are skipped too.
|
|
146
|
+
Vitest writes tag names without the `@`, as in `{ tags: ['flaky'] }`, so a name in `tags` counts as the
|
|
147
|
+
tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when: '@flaky'` matches it.
|
|
135
148
|
|
|
136
149
|
## Rules
|
|
137
150
|
|
|
@@ -139,27 +152,29 @@ Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each`
|
|
|
139
152
|
|
|
140
153
|
💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
|
|
141
154
|
⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
|
|
142
|
-
|
|
155
|
+
🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
156
|
+
🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
157
|
+
⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
143
158
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
144
159
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
145
160
|
|
|
146
|
-
| Name | Description | 💼
|
|
147
|
-
| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- |
|
|
148
|
-
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state |
|
|
149
|
-
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos |
|
|
150
|
-
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state |
|
|
151
|
-
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests |
|
|
152
|
-
| [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket |
|
|
161
|
+
| Name | Description | 💼 | ⚠️ | 🔧 | 💡 |
|
|
162
|
+
| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------- | :------- | :-- | :-- |
|
|
163
|
+
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🃏 🎭 ⚡ | | | 💡 |
|
|
164
|
+
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🃏 🎭 ⚡ | 🔧 | |
|
|
165
|
+
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🃏 🎭 ⚡ | | | 💡 |
|
|
166
|
+
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🃏 🎭 ⚡ | | | 💡 |
|
|
167
|
+
| [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🃏 🎭 ⚡ | | | 💡 |
|
|
153
168
|
|
|
154
169
|
<!-- end auto-generated rules list -->
|
|
155
170
|
|
|
156
171
|
## Defaults
|
|
157
172
|
|
|
158
|
-
The defaults follow how most teams already work, so
|
|
173
|
+
The defaults follow how most teams already work, so each framework config is useful with no options.
|
|
159
174
|
|
|
160
175
|
| Setting | Default | Why |
|
|
161
176
|
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
162
|
-
| States that need a ticket | `skip`, `fixme`; in Jest and Vitest `skip`, `todo` | Every team skips tests. Chromium, GitLab and Slack all require a bug link on disabled tests.
|
|
177
|
+
| States that need a ticket | `skip`, `fixme`; in Jest and Vitest `skip`, `todo` | Every team skips tests. Chromium DevTools, GitLab and Slack all require a bug link on disabled tests. |
|
|
163
178
|
| `@new`, `@unstable` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that call it `@flaky` or `@quarantine` use `customStates`. |
|
|
164
179
|
| Conditional skips (`test.skip(browserName === 'webkit', ...)`, `if (...) test.skip()`, a skip in `catch`) | no ticket needed | These are usually permanent platform limits, not bugs. `requireTicketForConditional: true` changes this. |
|
|
165
180
|
| `test.fail`, `test.slow`, Jest's `test.failing`, Vitest's `test.fails` | off | They don't remove coverage. Turn them on with `states: { fail: true, slow: true }`. |
|
|
@@ -175,12 +190,13 @@ The defaults follow how most teams already work, so `recommended` is useful with
|
|
|
175
190
|
## Options
|
|
176
191
|
|
|
177
192
|
All options go in `configure({...})`, or in `settings['test-governance']` if you build the config
|
|
178
|
-
yourself.
|
|
179
|
-
|
|
193
|
+
yourself. `framework` is required, and every other option has a default. The rules have no options
|
|
194
|
+
of their own: they all read these shared settings, so they always agree on ticket formats, states
|
|
195
|
+
and markers. To turn a rule off, set it to `'off'` as usual.
|
|
180
196
|
|
|
181
197
|
| Option | Type | Default | Description |
|
|
182
198
|
| ----------------------------- | -------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
-
| `framework` | `'playwright' \| 'jest' \| 'vitest'` |
|
|
199
|
+
| `framework` | `'playwright' \| 'jest' \| 'vitest'` | required | The test framework of the files the config applies to. `configs.playwright`, `configs.jest` and `configs.vitest` set it, and `configure()` needs it. See [Frameworks](#frameworks). |
|
|
184
200
|
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
185
201
|
| `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive; a leading `#` is ignored. Your list replaces the default one, so repeat the defaults you want to keep. Tickets numbered 0 are always rejected. |
|
|
186
202
|
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `unstable` (`@unstable`) states. |
|
|
@@ -191,7 +207,7 @@ always agree on ticket formats, states and markers. To turn a rule off, set it t
|
|
|
191
207
|
| `allowNotes` | `boolean` | `false` | Allow free text after the tickets, e.g. `// SKIP: TRADE-123 flaky on CI`. Applies to markers and to `FIXME` / `TODO` comments. |
|
|
192
208
|
| `comments` | `false \| { keywords?: string[] }` | `{ keywords: ['FIXME', 'TODO'] }` | Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: TRADE-123`. Your keywords replace the default ones. `false` allows free-form comments. |
|
|
193
209
|
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`. Your list replaces the default one, so include those names if you still need them. See below. |
|
|
194
|
-
| `
|
|
210
|
+
| `reportUnreadableTags` | `boolean` | `false` | When a tag state is on, report titles that aren't static text, and tags or details that can't be read in the file, such as ones imported from another module, since their tags can't be checked. |
|
|
195
211
|
|
|
196
212
|
### Ticket presets
|
|
197
213
|
|
|
@@ -216,6 +232,7 @@ reports in GitHub:
|
|
|
216
232
|
|
|
217
233
|
```js
|
|
218
234
|
testGovernance.configure({
|
|
235
|
+
framework: 'playwright',
|
|
219
236
|
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
220
237
|
lifecycleTags: true,
|
|
221
238
|
states: { unstable: { ticket: { preset: 'github' } } },
|
|
@@ -250,16 +267,17 @@ In Jest and Vitest files:
|
|
|
250
267
|
A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
|
|
251
268
|
`node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
|
|
252
269
|
`const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
|
|
253
|
-
variable declared inside a function that happens to be called `test`.
|
|
270
|
+
variable or function declared inside a function that happens to be called `test`.
|
|
254
271
|
|
|
255
272
|
### Syntax the rules understand
|
|
256
273
|
|
|
257
|
-
The [
|
|
274
|
+
The [Frameworks](#frameworks) table lists what each framework's tests can do. In Playwright:
|
|
258
275
|
|
|
259
276
|
- `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, with or without a details object,
|
|
260
|
-
and with a body defined elsewhere, as in `test.skip('places a limit order', placeLimitOrder)`.
|
|
261
|
-
|
|
262
|
-
|
|
277
|
+
and with a body defined elsewhere, as in `test.skip('places a limit order', placeLimitOrder)`. The
|
|
278
|
+
title can also be a `const` declared in the same file.
|
|
279
|
+
- `test.describe.skip(...)` and `test.describe.fixme(...)`, including `serial` and `parallel` variants,
|
|
280
|
+
and `describe.skip(...)` after `const { describe } = test`. The state covers every test inside.
|
|
263
281
|
- Runtime calls inside a test, a describe or a hook such as `test.beforeEach`: `test.skip()`,
|
|
264
282
|
`test.skip(true)`, `test.fixme()`, `test.fail()`, `test.slow()`, the same methods on `testInfo`,
|
|
265
283
|
and `test.info().skip()`.
|
|
@@ -267,12 +285,14 @@ The [Jest and Vitest](#jest-and-vitest) table lists what each framework's tests
|
|
|
267
285
|
receives. Only the step is skipped, so a marker above the step or above a skipped test around it
|
|
268
286
|
covers it.
|
|
269
287
|
- Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
|
|
270
|
-
from enclosing describes.
|
|
288
|
+
from enclosing describes. Tags, titles and details can also come from a `const` or an enum declared
|
|
289
|
+
in the same file, as in `{ tag: TAGS.NEW }` after `const TAGS = { NEW: '@new' } as const`, including
|
|
290
|
+
arrays and spreads of them.
|
|
271
291
|
|
|
272
|
-
Not covered in this version: tests generated in loops with dynamic titles
|
|
273
|
-
`
|
|
274
|
-
|
|
275
|
-
`test
|
|
292
|
+
Not covered in this version: tests generated in loops with dynamic titles, and tags or details imported
|
|
293
|
+
from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
|
|
294
|
+
`testFunctions`, a Vitest test with only a title, which Vitest runs as a todo but which looks the same as a
|
|
295
|
+
helper called `test`, Playwright annotations as markers, and other runners such as Mocha, Bun and `node:test`.
|
|
276
296
|
|
|
277
297
|
## TypeScript
|
|
278
298
|
|
|
@@ -287,6 +307,7 @@ import tsParser from '@typescript-eslint/parser';
|
|
|
287
307
|
import testGovernance, { type GovernanceOptions } from 'eslint-plugin-test-governance';
|
|
288
308
|
|
|
289
309
|
const options: GovernanceOptions = {
|
|
310
|
+
framework: 'playwright',
|
|
290
311
|
lifecycleTags: true,
|
|
291
312
|
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
292
313
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -13,10 +13,10 @@ export interface FlatConfig {
|
|
|
13
13
|
settings?: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* One config per framework. `playwright`, `jest` and `vitest` turn on the same rules for that framework's
|
|
17
|
+
* tests; point each one at its test files with `files`.
|
|
18
18
|
*/
|
|
19
|
-
export type ConfigName =
|
|
19
|
+
export type ConfigName = FrameworkName;
|
|
20
20
|
export interface TestGovernancePlugin extends ESLint.Plugin {
|
|
21
21
|
meta: {
|
|
22
22
|
name: string;
|
|
@@ -25,14 +25,13 @@ export interface TestGovernancePlugin extends ESLint.Plugin {
|
|
|
25
25
|
};
|
|
26
26
|
rules: Record<RuleName, Rule.RuleModule>;
|
|
27
27
|
configs: Record<ConfigName, FlatConfig>;
|
|
28
|
-
configure(options
|
|
28
|
+
configure(options: GovernanceOptions): FlatConfig;
|
|
29
29
|
}
|
|
30
30
|
declare const plugin: TestGovernancePlugin;
|
|
31
31
|
/**
|
|
32
|
-
* Returns the
|
|
33
|
-
* so all rules read the same ticket formats and states.
|
|
34
|
-
* framework's config.
|
|
32
|
+
* Returns the config of `options.framework` with shared options in `settings['test-governance']`,
|
|
33
|
+
* so all rules read the same ticket formats and states.
|
|
35
34
|
*/
|
|
36
|
-
export declare function configure(options
|
|
37
|
-
export declare const configs: Record<
|
|
35
|
+
export declare function configure(options: GovernanceOptions): FlatConfig;
|
|
36
|
+
export declare const configs: Record<"playwright" | "jest" | "vitest", FlatConfig>;
|
|
38
37
|
export default plugin;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import noOrphanedMarker from "./rules/no-orphaned-marker.js";
|
|
|
5
5
|
import requireTicketInComments from "./rules/require-ticket-in-comments.js";
|
|
6
6
|
import requireTicket from "./rules/require-ticket.js";
|
|
7
7
|
import { FRAMEWORK_NAMES } from "./utils/constants.js";
|
|
8
|
+
import { ConfigError } from "./utils/errors.js";
|
|
8
9
|
import { compileOptions, SETTINGS_KEY } from "./utils/options.js";
|
|
9
10
|
const PLUGIN_NAME = 'test-governance';
|
|
10
11
|
// Read at load time so the reported version always matches the published package.
|
|
@@ -18,7 +19,8 @@ const ruleModules = {
|
|
|
18
19
|
};
|
|
19
20
|
// Public types come from `eslint` itself, so users don't need typescript-eslint installed.
|
|
20
21
|
export const rules = ruleModules;
|
|
21
|
-
|
|
22
|
+
// Every framework config turns on all the rules, at the severity in each rule's `meta.docs.recommended`.
|
|
23
|
+
const configRules = Object.fromEntries(Object.keys(ruleModules).map((name) => [
|
|
22
24
|
`${PLUGIN_NAME}/${name}`,
|
|
23
25
|
ruleModules[name].meta.docs.recommended,
|
|
24
26
|
]));
|
|
@@ -29,34 +31,36 @@ const plugin = {
|
|
|
29
31
|
configure,
|
|
30
32
|
};
|
|
31
33
|
/**
|
|
32
|
-
* Returns the
|
|
33
|
-
* so all rules read the same ticket formats and states.
|
|
34
|
-
* framework's config.
|
|
34
|
+
* Returns the config of `options.framework` with shared options in `settings['test-governance']`,
|
|
35
|
+
* so all rules read the same ticket formats and states.
|
|
35
36
|
*/
|
|
36
|
-
export function configure(options
|
|
37
|
+
export function configure(options) {
|
|
37
38
|
compileOptions(options); // fail on a bad config at load time, not on the first lint
|
|
38
39
|
return {
|
|
39
|
-
name: `${PLUGIN_NAME}/${options.framework
|
|
40
|
+
name: `${PLUGIN_NAME}/${options.framework}`,
|
|
40
41
|
plugins: { [PLUGIN_NAME]: plugin },
|
|
41
|
-
rules: { ...
|
|
42
|
+
rules: { ...configRules },
|
|
42
43
|
settings: { [SETTINGS_KEY]: options },
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
|
-
plugin.configs.recommended = {
|
|
46
|
-
name: `${PLUGIN_NAME}/recommended`,
|
|
47
|
-
plugins: { [PLUGIN_NAME]: plugin },
|
|
48
|
-
rules: { ...recommendedRules },
|
|
49
|
-
};
|
|
50
46
|
// Each sets its framework, so it wins over a `configure()` that applies to all files when listed after it.
|
|
51
47
|
for (const framework of FRAMEWORK_NAMES) {
|
|
52
48
|
plugin.configs[framework] = configure({ framework });
|
|
53
49
|
}
|
|
50
|
+
// `configs.recommended` was removed in 0.5.0. Reading it throws, so `...configs.recommended` fails with this
|
|
51
|
+
// message instead of spreading nothing and turning the rules off without a word. It isn't enumerable, so
|
|
52
|
+
// tools that list the configs don't see it.
|
|
53
|
+
Object.defineProperty(plugin.configs, 'recommended', {
|
|
54
|
+
get() {
|
|
55
|
+
throw new ConfigError('configs.recommended was removed in 0.5.0. Use the config for your framework: configs.playwright, configs.jest or configs.vitest. See https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md');
|
|
56
|
+
},
|
|
57
|
+
});
|
|
54
58
|
export const configs = plugin.configs;
|
|
55
59
|
export default plugin;
|
|
56
60
|
/**
|
|
57
61
|
* Makes `require('eslint-plugin-test-governance')` return the plugin itself rather than the module
|
|
58
62
|
* namespace. With the namespace, a CommonJS config that registers the plugin next to
|
|
59
|
-
* `configs.
|
|
63
|
+
* `configs.playwright` fails with "Cannot redefine plugin".
|
|
60
64
|
*
|
|
61
65
|
* @internal Left out of the type declarations: TypeScript before 5.6 can't parse a string export
|
|
62
66
|
* name, so it would reject the whole file.
|
|
@@ -51,6 +51,8 @@ export default createRule({
|
|
|
51
51
|
}
|
|
52
52
|
function checkSpelling(occurrence) {
|
|
53
53
|
const { tag, range } = occurrence;
|
|
54
|
+
// A Vitest tag name is written without its `@`, so show it, and fix it, the same way.
|
|
55
|
+
const written = (name) => (occurrence.plain ? name.slice(1) : name);
|
|
54
56
|
if (tagStates.some((s) => s.tag === tag))
|
|
55
57
|
return;
|
|
56
58
|
const lower = tag.toLowerCase();
|
|
@@ -59,8 +61,8 @@ export default createRule({
|
|
|
59
61
|
context.report({
|
|
60
62
|
node: occurrence.node,
|
|
61
63
|
messageId: 'tagCase',
|
|
62
|
-
data: { found: tag, expected: sameLetters.tag },
|
|
63
|
-
fix: range && ((fixer) => fixer.replaceTextRange(range, sameLetters.tag)),
|
|
64
|
+
data: { found: written(tag), expected: written(sameLetters.tag) },
|
|
65
|
+
fix: range && ((fixer) => fixer.replaceTextRange(range, written(sameLetters.tag))),
|
|
64
66
|
});
|
|
65
67
|
return;
|
|
66
68
|
}
|
|
@@ -72,7 +74,11 @@ export default createRule({
|
|
|
72
74
|
editDistance(lower, expected) <= typoThreshold(expected.length - 1));
|
|
73
75
|
});
|
|
74
76
|
if (near) {
|
|
75
|
-
context.report({
|
|
77
|
+
context.report({
|
|
78
|
+
node: occurrence.node,
|
|
79
|
+
messageId: 'tagTypo',
|
|
80
|
+
data: { found: written(tag), expected: written(near.tag) },
|
|
81
|
+
});
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
},
|
|
@@ -37,7 +37,7 @@ export default createRule({
|
|
|
37
37
|
for (const marker of strayMarkers(subject, options)) {
|
|
38
38
|
// TODO is the everyday work-comment keyword, so `// TODO: TRADE-12` above a test that isn't a todo
|
|
39
39
|
// is a tracked note about it far more often than a todo test's forgotten marker.
|
|
40
|
-
if (marker.state.name === 'todo' && options.workCommentKeywords.has(marker.keyword))
|
|
40
|
+
if (marker.state.name === 'todo' && options.workCommentKeywords.has(marker.keyword.toUpperCase()))
|
|
41
41
|
continue;
|
|
42
42
|
markers.push({ marker, messageId: 'orphaned', subject: name });
|
|
43
43
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "missingMarker" | "markerCase" | "dynamicTitle">;
|
|
1
|
+
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "missingMarker" | "markerCase" | "dynamicTitle" | "dynamicTags">;
|
|
2
2
|
export default _default;
|
|
@@ -19,6 +19,7 @@ export default createRule({
|
|
|
19
19
|
extraText: 'Only ticket IDs are allowed after `{{marker}}:`, separated by commas. Put details in the ticket instead of `{{text}}`.',
|
|
20
20
|
removeExtraText: 'Remove `{{text}}`.',
|
|
21
21
|
dynamicTitle: "This title isn't static text, so its tags can't be checked. Use a string or template literal.",
|
|
22
|
+
dynamicTags: "These tags can't be read in this file, so they can't be checked. Use strings, or constants declared in this file.",
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
create(context, analysis, options) {
|
|
@@ -27,8 +28,11 @@ export default createRule({
|
|
|
27
28
|
const reported = new Set();
|
|
28
29
|
const reportOnce = (key) => !reported.has(key) && Boolean(reported.add(key));
|
|
29
30
|
for (const subject of analysis.subjects) {
|
|
30
|
-
if (
|
|
31
|
-
|
|
31
|
+
if (options.reportUnreadableTags && hasTagStates) {
|
|
32
|
+
if (subject.dynamicTitle)
|
|
33
|
+
context.report({ node: subject.node.arguments[0], messageId: 'dynamicTitle' });
|
|
34
|
+
for (const node of subject.unreadTags)
|
|
35
|
+
context.report({ node, messageId: 'dynamicTags' });
|
|
32
36
|
}
|
|
33
37
|
for (const { state, required } of subject.states) {
|
|
34
38
|
if (!required)
|
package/dist/utils/analyze.d.ts
CHANGED
|
@@ -30,12 +30,15 @@ export type StateMarker = Marker & {
|
|
|
30
30
|
export declare function isStateMarker(marker: Marker): marker is StateMarker;
|
|
31
31
|
export interface TagOccurrence {
|
|
32
32
|
tag: string;
|
|
33
|
-
|
|
33
|
+
/** Where to report the tag: the text it is written in, or the name it comes from, as `Tags.NEW`. */
|
|
34
|
+
node: Node;
|
|
34
35
|
/**
|
|
35
|
-
* Where the tag is written in the source. Unset when
|
|
36
|
-
* tag or inside it, since the source then no longer lines up with the title.
|
|
36
|
+
* Where the tag is written in the source. Unset when it comes from a constant, or when an escape such
|
|
37
|
+
* as `\u002D` comes before the tag or inside it, since the source then no longer lines up with the title.
|
|
37
38
|
*/
|
|
38
39
|
range?: TSESTree.Range;
|
|
40
|
+
/** Set for a Vitest tag name such as `'flaky'` in `tags`, which is written without its `@`. */
|
|
41
|
+
plain?: true;
|
|
39
42
|
}
|
|
40
43
|
interface StateSource {
|
|
41
44
|
state: StateDef;
|
|
@@ -58,6 +61,13 @@ export interface Subject {
|
|
|
58
61
|
tags: TagOccurrence[];
|
|
59
62
|
/** Tags this node inherits from enclosing describes. */
|
|
60
63
|
inheritedTags: Set<string>;
|
|
64
|
+
/** Tag values, or a whole details object, on this node that can't be read, as `{ tag: TAGS.NEW }` with `TAGS` imported. */
|
|
65
|
+
unreadTags: Node[];
|
|
66
|
+
/**
|
|
67
|
+
* States this node may be in without the code showing it: tag states when tags can't be read, and
|
|
68
|
+
* Vitest's option states when its options object can't. Includes those of enclosing describes.
|
|
69
|
+
*/
|
|
70
|
+
unknownStates: Set<string>;
|
|
61
71
|
/** True when this node is skipped or fixme'd by any means, whether or not those states are on. */
|
|
62
72
|
skipped: boolean;
|
|
63
73
|
ownSkip: boolean;
|
|
@@ -108,7 +118,11 @@ export declare function missingStates(subject: Subject): StateDef[];
|
|
|
108
118
|
* after it.
|
|
109
119
|
*/
|
|
110
120
|
export declare function isProse(marker: Marker): boolean;
|
|
111
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Markers in the subject's own block for states that don't apply to it, leaving out work comments and
|
|
123
|
+
* states its code may hide, such as a tag from a constant imported from another file. A marker in the
|
|
124
|
+
* wrong case, as `// skip: TRADE-1`, counts when it starts with a valid ticket.
|
|
125
|
+
*/
|
|
112
126
|
export declare function strayMarkers(subject: Subject, options: ResolvedOptions): StateMarker[];
|
|
113
127
|
/**
|
|
114
128
|
* Where to report a problem with a test or describe: from the callee to the end of the title
|