eslint-plugin-test-governance 0.4.0 → 0.5.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 +53 -1
- package/README.md +68 -50
- 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 +200 -46
- package/dist/utils/frameworks.d.ts +13 -4
- package/dist/utils/frameworks.js +8 -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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,57 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
This release changes how the plugin is configured, ahead of 1.0.
|
|
12
|
+
[Upgrading to 0.5](https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md#upgrading-to-05)
|
|
13
|
+
shows what to change, with a before and after for each one.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Breaking:** `configure()` needs a `framework`, as in
|
|
18
|
+
`configure({ framework: 'playwright', ticket: { preset: 'jira' } })`, and so does
|
|
19
|
+
`settings['test-governance']` in a config you build yourself. Before, a config without one checked
|
|
20
|
+
files as Playwright tests. Now `configure()` fails when the config file loads, and hand-written
|
|
21
|
+
settings fail on the first lint, with a message that names the three frameworks. To share options
|
|
22
|
+
between frameworks, pass the same options to a `configure()` for each one.
|
|
23
|
+
- **Breaking:** `reportDynamicTitles` is renamed to `reportUnreadableTags`, because it also reports
|
|
24
|
+
tags and details objects imported from another module, not only titles. It works the same way, and
|
|
25
|
+
a config that still uses the old name fails with a message that names the new one.
|
|
26
|
+
- Vitest's `skipIf(true)` and `runIf(false)` always skip, so they need a ticket, like `{ skip: true }`
|
|
27
|
+
and Playwright's `test.skip(true)`. `skipIf(false)` and `runIf(true)` never skip, so a `// SKIP:`
|
|
28
|
+
marker above one is reported as left over. Any other condition is still a conditional skip. Before,
|
|
29
|
+
a literal condition was treated like any other.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **Breaking:** `configs.recommended` is gone. It checked every file as Playwright tests, so pick the
|
|
34
|
+
config for your framework instead: `configs.playwright`, `configs.jest` or `configs.vitest`. For
|
|
35
|
+
Playwright, replace `configs.recommended` with `configs.playwright`, and
|
|
36
|
+
`extends: ['test-governance/recommended']` with `extends: ['test-governance/playwright']`. The
|
|
37
|
+
rules and their severities are the same. A config that still reads `configs.recommended` fails when
|
|
38
|
+
it loads, with a message that points to the upgrade guide, rather than spreading nothing and turning
|
|
39
|
+
the rules off.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Tags kept in a `const` or an enum in the same file count, as in `{ tag: TAGS.NEW }` after
|
|
44
|
+
`const TAGS = { NEW: '@new' } as const`, with spreads, `as const` and `satisfies`, and so do titles
|
|
45
|
+
and details objects kept in a constant. Before, a test tagged this way needed no ticket, and a
|
|
46
|
+
correct `// NEW:` marker above it was reported as left over.
|
|
47
|
+
- Tags and details imported from another module can't be read, so a marker for a tag state above such
|
|
48
|
+
a test is no longer reported as left over, and neither is a Vitest state marker such as `// SKIP:`
|
|
49
|
+
above a test whose options object is imported. With `reportUnreadableTags: true`, the unreadable
|
|
50
|
+
tags are reported, like titles that aren't static text.
|
|
51
|
+
- Vitest tag names, which are written without the `@` (`{ tags: ['flaky'] }`), match the states:
|
|
52
|
+
`'new'` is `@new`, and a custom state with `when: '@flaky'` matches `'flaky'`. Before, they never
|
|
53
|
+
matched. `no-conflicting-states` shows and fixes them without the `@`.
|
|
54
|
+
- `context.skip()` in Vitest's `beforeEach`, global or imported, needs a ticket, like the one in
|
|
55
|
+
`test.beforeEach`. Before, it wasn't checked.
|
|
56
|
+
- A marker in the wrong case with a ticket, such as `// skip: TRADE-1`, is reported by
|
|
57
|
+
`no-orphaned-marker` once the test is no longer skipped, and by `marker-matches-state` above a test in
|
|
58
|
+
another state. Before, it was only reported while the test was still in that state.
|
|
59
|
+
|
|
9
60
|
## [0.4.0] - 2026-10-08
|
|
10
61
|
|
|
11
62
|
### Added
|
|
@@ -194,7 +245,8 @@ First release.
|
|
|
194
245
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
195
246
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
196
247
|
|
|
197
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
248
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.5.0...HEAD
|
|
249
|
+
[0.5.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.4.0...v0.5.0
|
|
198
250
|
[0.4.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.1...v0.4.0
|
|
199
251
|
[0.3.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...v0.3.1
|
|
200
252
|
[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,40 @@ 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()` 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
|
+
Vitest writes tag names without the `@`, as in `{ tags: ['flaky'] }`, so a name in `tags` counts as the
|
|
146
|
+
tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when: '@flaky'` matches it.
|
|
135
147
|
|
|
136
148
|
## Rules
|
|
137
149
|
|
|
@@ -139,27 +151,29 @@ Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each`
|
|
|
139
151
|
|
|
140
152
|
💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
|
|
141
153
|
⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
|
|
142
|
-
|
|
154
|
+
🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
155
|
+
🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
156
|
+
⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
143
157
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
144
158
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
145
159
|
|
|
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 |
|
|
160
|
+
| Name | Description | 💼 | ⚠️ | 🔧 | 💡 |
|
|
161
|
+
| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------- | :------- | :-- | :-- |
|
|
162
|
+
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🃏 🎭 ⚡ | | | 💡 |
|
|
163
|
+
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🃏 🎭 ⚡ | 🔧 | |
|
|
164
|
+
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🃏 🎭 ⚡ | | | 💡 |
|
|
165
|
+
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🃏 🎭 ⚡ | | | 💡 |
|
|
166
|
+
| [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
167
|
|
|
154
168
|
<!-- end auto-generated rules list -->
|
|
155
169
|
|
|
156
170
|
## Defaults
|
|
157
171
|
|
|
158
|
-
The defaults follow how most teams already work, so
|
|
172
|
+
The defaults follow how most teams already work, so each framework config is useful with no options.
|
|
159
173
|
|
|
160
174
|
| Setting | Default | Why |
|
|
161
175
|
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
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.
|
|
176
|
+
| 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
177
|
| `@new`, `@unstable` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that call it `@flaky` or `@quarantine` use `customStates`. |
|
|
164
178
|
| 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
179
|
| `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 +189,13 @@ The defaults follow how most teams already work, so `recommended` is useful with
|
|
|
175
189
|
## Options
|
|
176
190
|
|
|
177
191
|
All options go in `configure({...})`, or in `settings['test-governance']` if you build the config
|
|
178
|
-
yourself.
|
|
179
|
-
|
|
192
|
+
yourself. `framework` is required, and every other option has a default. The rules have no options
|
|
193
|
+
of their own: they all read these shared settings, so they always agree on ticket formats, states
|
|
194
|
+
and markers. To turn a rule off, set it to `'off'` as usual.
|
|
180
195
|
|
|
181
196
|
| Option | Type | Default | Description |
|
|
182
197
|
| ----------------------------- | -------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
-
| `framework` | `'playwright' \| 'jest' \| 'vitest'` |
|
|
198
|
+
| `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
199
|
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
185
200
|
| `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
201
|
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `unstable` (`@unstable`) states. |
|
|
@@ -191,7 +206,7 @@ always agree on ticket formats, states and markers. To turn a rule off, set it t
|
|
|
191
206
|
| `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
207
|
| `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
208
|
| `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
|
-
| `
|
|
209
|
+
| `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
210
|
|
|
196
211
|
### Ticket presets
|
|
197
212
|
|
|
@@ -216,6 +231,7 @@ reports in GitHub:
|
|
|
216
231
|
|
|
217
232
|
```js
|
|
218
233
|
testGovernance.configure({
|
|
234
|
+
framework: 'playwright',
|
|
219
235
|
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
220
236
|
lifecycleTags: true,
|
|
221
237
|
states: { unstable: { ticket: { preset: 'github' } } },
|
|
@@ -254,7 +270,7 @@ variable declared inside a function that happens to be called `test`.
|
|
|
254
270
|
|
|
255
271
|
### Syntax the rules understand
|
|
256
272
|
|
|
257
|
-
The [
|
|
273
|
+
The [Frameworks](#frameworks) table lists what each framework's tests can do. In Playwright:
|
|
258
274
|
|
|
259
275
|
- `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, with or without a details object,
|
|
260
276
|
and with a body defined elsewhere, as in `test.skip('places a limit order', placeLimitOrder)`.
|
|
@@ -267,12 +283,13 @@ The [Jest and Vitest](#jest-and-vitest) table lists what each framework's tests
|
|
|
267
283
|
receives. Only the step is skipped, so a marker above the step or above a skipped test around it
|
|
268
284
|
covers it.
|
|
269
285
|
- Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
|
|
270
|
-
from enclosing describes.
|
|
286
|
+
from enclosing describes. Tags, titles and details can also come from a `const` or an enum declared
|
|
287
|
+
in the same file, as in `{ tag: TAGS.NEW }` after `const TAGS = { NEW: '@new' } as const`, including
|
|
288
|
+
arrays and spreads of them.
|
|
271
289
|
|
|
272
|
-
Not covered in this version: tests generated in loops with dynamic titles
|
|
273
|
-
`
|
|
274
|
-
Playwright annotations as markers,
|
|
275
|
-
`test.beforeEach` is covered), and other runners such as Mocha, Bun and `node:test`.
|
|
290
|
+
Not covered in this version: tests generated in loops with dynamic titles, and tags or details imported
|
|
291
|
+
from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
|
|
292
|
+
`testFunctions`, Playwright annotations as markers, and other runners such as Mocha, Bun and `node:test`.
|
|
276
293
|
|
|
277
294
|
## TypeScript
|
|
278
295
|
|
|
@@ -287,6 +304,7 @@ import tsParser from '@typescript-eslint/parser';
|
|
|
287
304
|
import testGovernance, { type GovernanceOptions } from 'eslint-plugin-test-governance';
|
|
288
305
|
|
|
289
306
|
const options: GovernanceOptions = {
|
|
307
|
+
framework: 'playwright',
|
|
290
308
|
lifecycleTags: true,
|
|
291
309
|
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
292
310
|
};
|
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
|