eslint-plugin-test-governance 0.3.1 → 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 +62 -1
- package/README.md +166 -65
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -6
- package/dist/rules/marker-matches-state.d.ts +0 -1
- package/dist/rules/marker-matches-state.js +4 -4
- package/dist/rules/no-conflicting-states.d.ts +0 -1
- package/dist/rules/no-conflicting-states.js +8 -16
- package/dist/rules/no-orphaned-marker.d.ts +0 -1
- package/dist/rules/no-orphaned-marker.js +12 -7
- package/dist/rules/require-ticket-in-comments.d.ts +0 -1
- package/dist/rules/require-ticket-in-comments.js +8 -13
- package/dist/rules/require-ticket.d.ts +0 -1
- package/dist/rules/require-ticket.js +16 -8
- package/dist/utils/analyze.d.ts +15 -12
- package/dist/utils/analyze.js +358 -167
- package/dist/utils/constants.d.ts +5 -3
- package/dist/utils/constants.js +4 -3
- package/dist/utils/create-rule.d.ts +7 -8
- package/dist/utils/create-rule.js +3 -9
- 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 +24 -9
- package/dist/utils/options.js +8 -4
- package/dist/utils/schema.d.ts +0 -1
- package/dist/utils/schema.js +2 -2
- package/dist/utils/tickets.d.ts +0 -1
- package/dist/utils/tickets.js +3 -3
- package/dist/utils/validate.d.ts +0 -1
- package/dist/utils/validate.js +2 -2
- 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 +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,66 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Jest and Vitest support. `configs.jest` and `configs.vitest`, or `configure({ framework })`, check
|
|
14
|
+
those frameworks' tests with the same rules and markers: `test.skip`, `it.skip`, `describe.skip`,
|
|
15
|
+
Jest's `xit`, `xtest` and `xdescribe`, Vitest's `{ skip: true }` and `context.skip()`, and their
|
|
16
|
+
`.each` tables, with globals or imports from `@jest/globals` and `vitest`. Vitest's `skipIf()`,
|
|
17
|
+
`runIf()` and `{ skip: condition }` are conditional skips, so they need no ticket by default.
|
|
18
|
+
Point each config at its framework's files with `files`; Playwright files keep using
|
|
19
|
+
`recommended` or `configs.playwright`.
|
|
20
|
+
- A `todo` state for Jest's and Vitest's `test.todo`, with the `// TODO:` marker, on by default. A
|
|
21
|
+
`// TODO: TRADE-123` above a test that isn't a todo is left alone as a tracked work comment. Jest's
|
|
22
|
+
`test.failing` and Vitest's `test.fails` use the `fail` state, which stays off by default.
|
|
23
|
+
- Skipped steps need a ticket, like skipped tests: `test.step.skip('title', fn)` and `step.skip()`
|
|
24
|
+
on the `TestStepInfo` a step's body receives. The marker goes above the call, above the step or
|
|
25
|
+
above a skipped test around it. A marker above a step that isn't skipped is reported as left over.
|
|
26
|
+
Before, skipped steps were not checked.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `#123`, `#1234` and `#12345` are real tickets now, not placeholders, since young repositories
|
|
31
|
+
have issues with small numbers. Add them to `placeholders` to keep rejecting them. `TODO`, `TBD`,
|
|
32
|
+
`XXX-*` and tickets numbered 0 are still rejected.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- The type declarations load in TypeScript 5.0 to 5.5. Before, they included a string export name
|
|
37
|
+
that only TypeScript 5.6 and later can parse, so older versions failed on the plugin's types even
|
|
38
|
+
with `skipLibCheck`.
|
|
39
|
+
- Tests inside a node type that a custom parser gives no visitor keys for are now checked. ESLint
|
|
40
|
+
reads such nodes' children from the node itself, and the rules now do the same. Before, everything
|
|
41
|
+
inside such a node was skipped.
|
|
42
|
+
- Test files that ESLint parses as CommonJS, such as `.cjs` files, are now checked. Before, their
|
|
43
|
+
top-level variables were taken for local helpers, so a `test` from `require('@playwright/test')`
|
|
44
|
+
was ignored by every rule.
|
|
45
|
+
- The `jira` preset accepts issue URLs on Jira Server and Data Center sites that run under a path,
|
|
46
|
+
such as `https://issues.example.com/jira/browse/TRADE-123`. Before, only `/browse/...` right
|
|
47
|
+
after the host was accepted.
|
|
48
|
+
- A plain alias of a Playwright test, as in `const orderTest = base`, is now followed like
|
|
49
|
+
`base.extend()` and `mergeTests()` are. Before, tests declared through it were not checked.
|
|
50
|
+
- A quoted key in a destructured `require()` is read like a plain one. Before,
|
|
51
|
+
`const { 'test': test } = require('node:test')` still counted as Playwright's `test`, so its
|
|
52
|
+
skipped tests were reported, and `const { 'test': t } = require('@playwright/test')` was not
|
|
53
|
+
followed.
|
|
54
|
+
- Tests written through the whole Playwright module, as in `import * as pw from '@playwright/test'`
|
|
55
|
+
or `const pw = require('@playwright/test')` followed by `pw.test.skip(...)`, are now checked,
|
|
56
|
+
including `pw.test.extend()` and `pw.mergeTests()`. Before, they were ignored.
|
|
57
|
+
- The `no-conflicting-states` fix for a tag in the wrong case changes only that tag. Before, an
|
|
58
|
+
escape earlier in the title could make it change another one: in `'@New\u002Dbeta @New'` it
|
|
59
|
+
rewrote the start of `@New-beta` and left `@New` as it was. A tag after an escape in its title is
|
|
60
|
+
still reported, without a fix.
|
|
61
|
+
- Block comments with lone CR line breaks, or with the Unicode line and paragraph separators
|
|
62
|
+
(U+2028, U+2029), are read line by line. Before, only LF and CRLF ended a line, so a marker or a
|
|
63
|
+
`TODO` after the first line of such a comment was missed.
|
|
64
|
+
- A marker with only punctuation after the colon, such as `// SKIP: .`, is reported as missing its
|
|
65
|
+
ticket. Before, the message was `'' is not a valid ticket`.
|
|
66
|
+
- A regex passed where an option expects a string is shown as written in the error, as in
|
|
67
|
+
`ticket.pattern must be a string (got /TRADE-\d+/)`. Before, it was shown as `{}`.
|
|
68
|
+
|
|
9
69
|
## [0.3.1] - 2026-10-06
|
|
10
70
|
|
|
11
71
|
### Fixed
|
|
@@ -134,7 +194,8 @@ First release.
|
|
|
134
194
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
135
195
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
136
196
|
|
|
137
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
197
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.4.0...HEAD
|
|
198
|
+
[0.4.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.1...v0.4.0
|
|
138
199
|
[0.3.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...v0.3.1
|
|
139
200
|
[0.3.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.1...v0.3.0
|
|
140
201
|
[0.2.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# eslint-plugin-test-governance
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/eslint-plugin-test-governance)
|
|
4
|
+
[](https://github.com/Huldoser/eslint-plugin-test-governance/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
ESLint rules that keep a Playwright, Jest or Vitest suite honest. Every skipped, fixme, todo, `@new`
|
|
8
|
+
or `@unstable` test must point at the ticket that tracks it:
|
|
5
9
|
|
|
6
10
|
```ts
|
|
7
|
-
// SKIP:
|
|
8
|
-
test.skip('
|
|
11
|
+
// SKIP: TRADE-123
|
|
12
|
+
test.skip('rejects a margin order above the buying power', async ({ page }) => {
|
|
9
13
|
// ...
|
|
10
14
|
});
|
|
11
15
|
```
|
|
@@ -41,10 +45,28 @@ export default [
|
|
|
41
45
|
];
|
|
42
46
|
```
|
|
43
47
|
|
|
48
|
+
With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// eslint.config.js
|
|
52
|
+
import { defineConfig } from 'eslint/config';
|
|
53
|
+
import tsParser from '@typescript-eslint/parser';
|
|
54
|
+
import testGovernance from 'eslint-plugin-test-governance';
|
|
55
|
+
|
|
56
|
+
export default defineConfig([
|
|
57
|
+
{ files: ['**/*.ts'], languageOptions: { parser: tsParser } },
|
|
58
|
+
{
|
|
59
|
+
files: ['tests/**/*.{js,ts}'],
|
|
60
|
+
extends: [testGovernance.configs.recommended],
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
```
|
|
64
|
+
|
|
44
65
|
Any flat config file works. The package is ESM, and `require('eslint-plugin-test-governance')` in an
|
|
45
|
-
`eslint.config.cjs` returns the plugin itself.
|
|
46
|
-
`extends: ['test-governance/recommended']
|
|
47
|
-
[`jiti`](https://github.com/unjs/jiti), so install it as a dev
|
|
66
|
+
`eslint.config.cjs` returns the plugin itself. Inside `defineConfig()`, once the plugin is registered
|
|
67
|
+
under `plugins`, the string form `extends: ['test-governance/recommended']` works too. ESLint loads
|
|
68
|
+
an `eslint.config.ts` through [`jiti`](https://github.com/unjs/jiti), so install it as a dev
|
|
69
|
+
dependency too.
|
|
48
70
|
|
|
49
71
|
To change the defaults, use `configure()`. It returns the same recommended config with your options
|
|
50
72
|
stored in `settings['test-governance']`, which all the rules read:
|
|
@@ -55,7 +77,7 @@ export default [
|
|
|
55
77
|
files: ['tests/**/*.{js,ts}'],
|
|
56
78
|
...testGovernance.configure({
|
|
57
79
|
// Only accept Jira keys from these projects (or Jira URLs).
|
|
58
|
-
ticket: { preset: 'jira', projects: ['
|
|
80
|
+
ticket: { preset: 'jira', projects: ['TRADE', 'RISK'] },
|
|
59
81
|
// Turn on the @new and @unstable states.
|
|
60
82
|
lifecycleTags: true,
|
|
61
83
|
// Any tag your team uses, with the same rules as the built-ins.
|
|
@@ -76,6 +98,41 @@ eslint-plugin-test-governance: invalid options:
|
|
|
76
98
|
- ticket.preset must be one of "any", "jira", "github", … (got "jria")
|
|
77
99
|
```
|
|
78
100
|
|
|
101
|
+
### Jest and Vitest
|
|
102
|
+
|
|
103
|
+
`recommended` is for Playwright. For Jest and Vitest, use `configs.jest` or `configs.vitest` and
|
|
104
|
+
point it at those test files with `files`, the way the Jest and Vitest ESLint plugins are set up.
|
|
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:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
export default [
|
|
111
|
+
{
|
|
112
|
+
files: ['e2e/**/*.spec.ts', 'src/**/*.test.ts'],
|
|
113
|
+
...testGovernance.configure({ ticket: { preset: 'jira', projects: ['TRADE'] } }),
|
|
114
|
+
},
|
|
115
|
+
// Unit tests run on Vitest; the e2e specs stay on Playwright.
|
|
116
|
+
{ files: ['src/**/*.test.ts'], ...testGovernance.configs.vitest },
|
|
117
|
+
];
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`configure({ framework: 'jest', ... })` gives the Jest config with options in one call, and
|
|
121
|
+
`configs.playwright` is the Playwright config with its framework set, for a file list that needs it.
|
|
122
|
+
All four configs turn on the same rules. What puts a test in a state in each framework:
|
|
123
|
+
|
|
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(...)`, `runIf(...)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()` |
|
|
132
|
+
| tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }` |
|
|
133
|
+
|
|
134
|
+
Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
|
|
135
|
+
|
|
79
136
|
## Rules
|
|
80
137
|
|
|
81
138
|
<!-- begin auto-generated rules list -->
|
|
@@ -100,20 +157,20 @@ eslint-plugin-test-governance: invalid options:
|
|
|
100
157
|
|
|
101
158
|
The defaults follow how most teams already work, so `recommended` is useful with no options.
|
|
102
159
|
|
|
103
|
-
| Setting | Default
|
|
104
|
-
| --------------------------------------------------------------------------------------------------------- |
|
|
105
|
-
| States that need a ticket | `skip`, `fixme`
|
|
106
|
-
| `@new`, `@unstable` | off; `lifecycleTags: true` turns both on
|
|
107
|
-
| Conditional skips (`test.skip(browserName === 'webkit', ...)`, `if (...) test.skip()`, a skip in `catch`) | no ticket needed
|
|
108
|
-
| `test.fail`, `test.slow`
|
|
109
|
-
| Marker format | `// SKIP:
|
|
110
|
-
| Notes after the ticket | not allowed; `allowNotes: true` allows them
|
|
111
|
-
| `FIXME` / `TODO` comments | must start with a ticket, e.g. `// TODO:
|
|
112
|
-
| Placement | comment block directly above; other comments allowed; a blank line breaks it
|
|
113
|
-
| Ticket format | `any`: `PROJ-123`, `#123`, `owner/repo#123`, or an http(s) URL
|
|
114
|
-
| Placeholder tickets | rejected: `TODO`, `TBD`, `XXX-*`,
|
|
115
|
-
| Describe blocks | a marker on a skipped or tagged describe covers its tests
|
|
116
|
-
| Severity | `
|
|
160
|
+
| Setting | Default | Why |
|
|
161
|
+
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
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. |
|
|
163
|
+
| `@new`, `@unstable` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that call it `@flaky` or `@quarantine` use `customStates`. |
|
|
164
|
+
| 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
|
+
| `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 }`. |
|
|
166
|
+
| Marker format | `// SKIP: TRADE-123` or `// SKIP: TRADE-123, TRADE-124` | Uppercase keyword and colon, like `TODO:`. Easy to grep. |
|
|
167
|
+
| Notes after the ticket | not allowed; `allowNotes: true` allows them | The ticket is the source of truth. A note in a comment goes stale while the ticket stays current. |
|
|
168
|
+
| `FIXME` / `TODO` comments | must start with a ticket, e.g. `// TODO: TRADE-123` or `// TODO(TRADE-123)` | The ticket is the source of truth; an untracked `TODO` is never done. `comments: false` turns this off. |
|
|
169
|
+
| Placement | comment block directly above; other comments allowed; a blank line breaks it | Survives `eslint-disable-next-line` and Prettier. |
|
|
170
|
+
| Ticket format | `any`: `PROJ-123`, `#123`, `owner/repo#123`, or an http(s) URL | Works for Jira, Linear, GitHub and GitLab out of the box. Narrow it with a preset. |
|
|
171
|
+
| Placeholder tickets | rejected: `TODO`, `TBD`, `XXX-*`, and any ticket numbered 0 (`PROJ-0`, `#0`) | A fake ticket is worse than none, and no tracker issues number 0. |
|
|
172
|
+
| Describe blocks | a marker on a skipped or tagged describe covers its tests | One ticket per skipped suite. |
|
|
173
|
+
| Severity | `no-conflicting-states`: warn; every other rule: error | A marker must always match a real state, so a missing, wrong or leftover marker fails the lint. Tag spelling is cleanup. |
|
|
117
174
|
|
|
118
175
|
## Options
|
|
119
176
|
|
|
@@ -121,39 +178,45 @@ All options go in `configure({...})`, or in `settings['test-governance']` if you
|
|
|
121
178
|
yourself. The rules have no options of their own: they all read these shared settings, so they
|
|
122
179
|
always agree on ticket formats, states and markers. To turn a rule off, set it to `'off'` as usual.
|
|
123
180
|
|
|
124
|
-
| Option | Type | Default | Description
|
|
125
|
-
| ----------------------------- | -------------------------------------------------------- | --------------------------------- |
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
181
|
+
| Option | Type | Default | Description |
|
|
182
|
+
| ----------------------------- | -------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
183
|
+
| `framework` | `'playwright' \| 'jest' \| 'vitest'` | `'playwright'` | The test framework of the files the config applies to. `configs.jest` and `configs.vitest` set it. See [Jest and Vitest](#jest-and-vitest). |
|
|
184
|
+
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
185
|
+
| `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
|
+
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `unstable` (`@unstable`) states. |
|
|
187
|
+
| `states` | `{ [state]: boolean \| { enabled?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `new`, `unstable`) on or off, renames their marker, or gives them their own ticket format. `fail` and `slow` are off by default; a setting for `new` or `unstable` here wins over `lifecycleTags`. `fixme` and `slow` exist only in Playwright and `todo` only in Jest and Vitest, so turning one on elsewhere has no effect. |
|
|
188
|
+
| `customStates` | `{ [name]: { when, marker, ticket? } }` | `{}` | Adds a state for a tag, e.g. `{ quarantine: { when: '@quarantine', marker: 'QUARANTINE' } }`. `when` is a tag and `marker` an uppercase keyword; neither can be shared with another state. |
|
|
189
|
+
| `requireTicketForConditional` | `boolean` | `false` | Require a ticket for conditional skips too: `test.skip(condition, ...)`, `if (...) test.skip()`, a skip in a `catch` block or after an early `return` or `throw`. |
|
|
190
|
+
| `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
|
|
191
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `reportDynamicTitles` | `boolean` | `false` | When a tag state is on, report titles that aren't static text, since their tags can't be read. |
|
|
137
195
|
|
|
138
196
|
### Ticket presets
|
|
139
197
|
|
|
140
|
-
| Preset | Accepts
|
|
141
|
-
| -------------- |
|
|
142
|
-
| `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL
|
|
143
|
-
| `jira` | `
|
|
144
|
-
| `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`)
|
|
145
|
-
| `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123`
|
|
146
|
-
| `linear` | `
|
|
147
|
-
| `azure-devops` | `AB#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123`
|
|
148
|
-
| `numeric` | `4821`
|
|
149
|
-
| `pattern` | anything matching your regex, anchored at both ends
|
|
198
|
+
| Preset | Accepts | Options |
|
|
199
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
200
|
+
| `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL | |
|
|
201
|
+
| `jira` | `TRADE-123`, `https://<host>/browse/TRADE-123` (also under a path, as in `/jira/browse/TRADE-123`), board URLs with `selectedIssue=TRADE-123` | `projects`; `host` (any host by default) |
|
|
202
|
+
| `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`) | `host` for GitHub Enterprise (`github.com` by default) |
|
|
203
|
+
| `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123` | `host` for self-managed (`gitlab.com` by default) |
|
|
204
|
+
| `linear` | `RISK-123`, `https://linear.app/<workspace>/issue/RISK-123/...` | `teams` |
|
|
205
|
+
| `azure-devops` | `AB#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123` | `host` for Azure DevOps Server (`dev.azure.com` and `*.visualstudio.com` by default) |
|
|
206
|
+
| `numeric` | `4821` | `minLength` (1), `maxLength` (20) |
|
|
207
|
+
| `pattern` | anything matching your regex, anchored at both ends | `pattern`, `flags` |
|
|
208
|
+
|
|
209
|
+
`projects` (Jira) and `teams` (Linear) are keys as they appear in tickets, in uppercase: `TRADE` for
|
|
210
|
+
`TRADE-123`. Each preset takes only its own options, so `host` on the `any` preset fails with a clear
|
|
211
|
+
message instead of being ignored. A `pattern` always matches the whole ticket, so the `g` and `y`
|
|
212
|
+
flags make no difference and are ignored.
|
|
150
213
|
|
|
151
214
|
Presets combine, and each state can have its own format. For example, bugs in Jira and flaky-test
|
|
152
215
|
reports in GitHub:
|
|
153
216
|
|
|
154
217
|
```js
|
|
155
218
|
testGovernance.configure({
|
|
156
|
-
ticket: { preset: 'jira', projects: ['
|
|
219
|
+
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
157
220
|
lifecycleTags: true,
|
|
158
221
|
states: { unstable: { ticket: { preset: 'github' } } },
|
|
159
222
|
});
|
|
@@ -164,43 +227,81 @@ so keep it free of nested quantifiers.
|
|
|
164
227
|
|
|
165
228
|
### Which functions count as `test`
|
|
166
229
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
In Playwright files:
|
|
231
|
+
|
|
232
|
+
- `test` imported from `@playwright/test`, `playwright/test` or a component-testing package such as
|
|
233
|
+
`@playwright/experimental-ct-react`, including the default import, aliases such as
|
|
234
|
+
`import { test as it }` or `const it = test`, and `const { test } = require('@playwright/test')`.
|
|
235
|
+
- `pw.test` on the whole module, after `import * as pw from '@playwright/test'` or
|
|
236
|
+
`const pw = require('@playwright/test')`.
|
|
170
237
|
- Fixtures made with `.extend()` or `mergeTests()` in the same file, such as
|
|
171
238
|
`const test = base.extend({...})`.
|
|
172
239
|
- Any name in `testFunctions`, for fixtures imported from your own modules. The default `['test']`
|
|
173
240
|
covers the common `import { test } from './fixtures'`.
|
|
174
241
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
242
|
+
In Jest and Vitest files:
|
|
243
|
+
|
|
244
|
+
- The globals `test`, `it` and `describe`, Jest's `xit`, `xtest`, `fit`, `xdescribe` and
|
|
245
|
+
`fdescribe`, and Vitest's `suite`, and the same names imported or required from `@jest/globals` or
|
|
246
|
+
`vitest`, including aliases and `import * as vi from 'vitest'`.
|
|
247
|
+
- Vitest fixtures made with `test.extend()` in the same file.
|
|
248
|
+
- Any name in `testFunctions`, which defaults to `['test', 'it']`.
|
|
249
|
+
|
|
250
|
+
A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
|
|
251
|
+
`node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
|
|
252
|
+
`const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
|
|
178
253
|
variable declared inside a function that happens to be called `test`.
|
|
179
254
|
|
|
180
255
|
### Syntax the rules understand
|
|
181
256
|
|
|
182
|
-
|
|
257
|
+
The [Jest and Vitest](#jest-and-vitest) table lists what each framework's tests can do. In Playwright:
|
|
258
|
+
|
|
259
|
+
- `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)`.
|
|
183
261
|
- `test.describe.skip(...)` and `test.describe.fixme(...)`, including `serial` and `parallel` variants.
|
|
184
262
|
The state covers every test inside.
|
|
185
|
-
-
|
|
186
|
-
`test.
|
|
263
|
+
- Runtime calls inside a test, a describe or a hook such as `test.beforeEach`: `test.skip()`,
|
|
264
|
+
`test.skip(true)`, `test.fixme()`, `test.fail()`, `test.slow()`, the same methods on `testInfo`,
|
|
265
|
+
and `test.info().skip()`.
|
|
266
|
+
- Skipped steps: `test.step.skip('title', fn)`, and `step.skip()` on the `TestStepInfo` a step's body
|
|
267
|
+
receives. Only the step is skipped, so a marker above the step or above a skipped test around it
|
|
268
|
+
covers it.
|
|
187
269
|
- Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
|
|
188
270
|
from enclosing describes.
|
|
189
271
|
|
|
190
|
-
Not covered in this version: tests generated in loops with dynamic titles
|
|
191
|
-
other modules under a name not in `testFunctions`,
|
|
272
|
+
Not covered in this version: tests generated in loops with dynamic titles (see
|
|
273
|
+
`reportDynamicTitles`), fixtures imported from other modules under a name not in `testFunctions`,
|
|
274
|
+
Playwright annotations as markers, Vitest's `context.skip()` in a global `beforeEach` (the one on
|
|
275
|
+
`test.beforeEach` is covered), and other runners such as Mocha, Bun and `node:test`.
|
|
192
276
|
|
|
193
|
-
##
|
|
277
|
+
## TypeScript
|
|
194
278
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
279
|
+
The package ships its own type declarations, which work with TypeScript 5.0 or later. Besides the
|
|
280
|
+
plugin, it exports these types: `GovernanceOptions` (the options of `configure()`), `TicketSpec`,
|
|
281
|
+
`CustomState`, `StateOverride`, `FrameworkName`, `RuleName`, `ConfigName`, `FlatConfig` and
|
|
282
|
+
`TestGovernancePlugin`.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// eslint.config.ts
|
|
286
|
+
import tsParser from '@typescript-eslint/parser';
|
|
287
|
+
import testGovernance, { type GovernanceOptions } from 'eslint-plugin-test-governance';
|
|
288
|
+
|
|
289
|
+
const options: GovernanceOptions = {
|
|
290
|
+
lifecycleTags: true,
|
|
291
|
+
ticket: { preset: 'jira', projects: ['TRADE'] },
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export default [
|
|
295
|
+
{ files: ['**/*.ts'], languageOptions: { parser: tsParser } },
|
|
296
|
+
{ files: ['tests/**/*.ts'], ...testGovernance.configure(options) },
|
|
297
|
+
];
|
|
202
298
|
```
|
|
203
299
|
|
|
300
|
+
## Contributing
|
|
301
|
+
|
|
302
|
+
Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) explains the setup and
|
|
303
|
+
the checks every pull request runs, and [CHANGELOG.md](CHANGELOG.md) lists the changes in each release.
|
|
304
|
+
|
|
204
305
|
## License
|
|
205
306
|
|
|
206
307
|
[MIT](LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ESLint, Rule } from 'eslint';
|
|
2
|
+
import { type FrameworkName } from './utils/constants.ts';
|
|
2
3
|
import { type GovernanceOptions } from './utils/options.ts';
|
|
4
|
+
export type { FrameworkName } from './utils/constants.ts';
|
|
3
5
|
export type { CustomState, GovernanceOptions, StateOverride } from './utils/options.ts';
|
|
4
6
|
export type { TicketSpec } from './utils/tickets.ts';
|
|
5
7
|
export type RuleName = 'require-ticket' | 'marker-matches-state' | 'no-orphaned-marker' | 'no-conflicting-states' | 'require-ticket-in-comments';
|
|
@@ -10,6 +12,11 @@ export interface FlatConfig {
|
|
|
10
12
|
rules: Record<string, 'error' | 'warn'>;
|
|
11
13
|
settings?: Record<string, unknown>;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* `recommended` is the Playwright config. `playwright`, `jest` and `vitest` turn on the same rules
|
|
17
|
+
* for that framework; point each one at its test files with `files`.
|
|
18
|
+
*/
|
|
19
|
+
export type ConfigName = 'recommended' | FrameworkName;
|
|
13
20
|
export interface TestGovernancePlugin extends ESLint.Plugin {
|
|
14
21
|
meta: {
|
|
15
22
|
name: string;
|
|
@@ -17,20 +24,15 @@ export interface TestGovernancePlugin extends ESLint.Plugin {
|
|
|
17
24
|
namespace: string;
|
|
18
25
|
};
|
|
19
26
|
rules: Record<RuleName, Rule.RuleModule>;
|
|
20
|
-
configs:
|
|
21
|
-
recommended: FlatConfig;
|
|
22
|
-
};
|
|
27
|
+
configs: Record<ConfigName, FlatConfig>;
|
|
23
28
|
configure(options?: GovernanceOptions): FlatConfig;
|
|
24
29
|
}
|
|
25
30
|
declare const plugin: TestGovernancePlugin;
|
|
26
31
|
/**
|
|
27
32
|
* Returns the recommended config with shared options in `settings['test-governance']`,
|
|
28
|
-
* so all rules read the same ticket formats and states.
|
|
33
|
+
* so all rules read the same ticket formats and states. With `framework`, it is that
|
|
34
|
+
* framework's config.
|
|
29
35
|
*/
|
|
30
36
|
export declare function configure(options?: GovernanceOptions): FlatConfig;
|
|
31
|
-
export declare const configs:
|
|
32
|
-
recommended: FlatConfig;
|
|
33
|
-
};
|
|
37
|
+
export declare const configs: Record<ConfigName, FlatConfig>;
|
|
34
38
|
export default plugin;
|
|
35
|
-
export { plugin as 'module.exports' };
|
|
36
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import noConflictingStates from "./rules/no-conflicting-states.js";
|
|
|
4
4
|
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
|
+
import { FRAMEWORK_NAMES } from "./utils/constants.js";
|
|
7
8
|
import { compileOptions, SETTINGS_KEY } from "./utils/options.js";
|
|
8
9
|
const PLUGIN_NAME = 'test-governance';
|
|
9
10
|
// Read at load time so the reported version always matches the published package.
|
|
@@ -29,12 +30,13 @@ const plugin = {
|
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
31
32
|
* Returns the recommended config with shared options in `settings['test-governance']`,
|
|
32
|
-
* so all rules read the same ticket formats and states.
|
|
33
|
+
* so all rules read the same ticket formats and states. With `framework`, it is that
|
|
34
|
+
* framework's config.
|
|
33
35
|
*/
|
|
34
36
|
export function configure(options = {}) {
|
|
35
37
|
compileOptions(options); // fail on a bad config at load time, not on the first lint
|
|
36
38
|
return {
|
|
37
|
-
name: `${PLUGIN_NAME}
|
|
39
|
+
name: `${PLUGIN_NAME}/${options.framework ?? 'recommended'}`,
|
|
38
40
|
plugins: { [PLUGIN_NAME]: plugin },
|
|
39
41
|
rules: { ...recommendedRules },
|
|
40
42
|
settings: { [SETTINGS_KEY]: options },
|
|
@@ -45,10 +47,18 @@ plugin.configs.recommended = {
|
|
|
45
47
|
plugins: { [PLUGIN_NAME]: plugin },
|
|
46
48
|
rules: { ...recommendedRules },
|
|
47
49
|
};
|
|
50
|
+
// Each sets its framework, so it wins over a `configure()` that applies to all files when listed after it.
|
|
51
|
+
for (const framework of FRAMEWORK_NAMES) {
|
|
52
|
+
plugin.configs[framework] = configure({ framework });
|
|
53
|
+
}
|
|
48
54
|
export const configs = plugin.configs;
|
|
49
55
|
export default plugin;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Makes `require('eslint-plugin-test-governance')` return the plugin itself rather than the module
|
|
58
|
+
* namespace. With the namespace, a CommonJS config that registers the plugin next to
|
|
59
|
+
* `configs.recommended` fails with "Cannot redefine plugin".
|
|
60
|
+
*
|
|
61
|
+
* @internal Left out of the type declarations: TypeScript before 5.6 can't parse a string export
|
|
62
|
+
* name, so it would reject the whole file.
|
|
63
|
+
*/
|
|
53
64
|
export { plugin as 'module.exports' };
|
|
54
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -2,14 +2,15 @@ import { appliesTo, describeSubject, isStateMarker, missingStates, strayMarkers
|
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { indentOf, keywordRange } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
5
|
-
name: 'marker-matches-state',
|
|
6
5
|
meta: {
|
|
7
6
|
type: 'problem',
|
|
8
|
-
hasSuggestions: true,
|
|
9
7
|
docs: {
|
|
10
8
|
description: "Require the marker keyword to match the test's state",
|
|
11
9
|
recommended: 'error',
|
|
10
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/marker-matches-state.md',
|
|
12
11
|
},
|
|
12
|
+
hasSuggestions: true,
|
|
13
|
+
schema: [],
|
|
13
14
|
messages: {
|
|
14
15
|
wrongMarker: "`{{found}}:` doesn't match this code: {{subject}} is in the '{{state}}' state, so the marker should be `{{expected}}:`.",
|
|
15
16
|
sharedMarker: "`{{found}}:` covers only the '{{foundState}}' state. {{subject}} is also in the '{{state}}' state, which needs its own `// {{expected}}: <ticket>` marker.",
|
|
@@ -17,7 +18,7 @@ export default createRule({
|
|
|
17
18
|
addMarker: 'Add `// {{expected}}: {{tickets}}` with the same ticket.',
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
|
-
|
|
21
|
+
create(context, analysis, options) {
|
|
21
22
|
const { sourceCode } = context;
|
|
22
23
|
for (const subject of analysis.subjects) {
|
|
23
24
|
const missing = missingStates(subject);
|
|
@@ -78,4 +79,3 @@ export default createRule({
|
|
|
78
79
|
}
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
|
-
//# sourceMappingURL=marker-matches-state.js.map
|