eslint-plugin-test-governance 0.11.1 → 0.12.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 +39 -1
- package/README.md +59 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/rules/marker-matches-state.js +10 -3
- package/dist/rules/no-orphaned-marker.js +2 -2
- package/dist/rules/require-ticket-for-commented-out-tests.d.ts +2 -0
- package/dist/rules/require-ticket-for-commented-out-tests.js +67 -0
- package/dist/rules/require-ticket.d.ts +1 -1
- package/dist/rules/require-ticket.js +43 -2
- package/dist/utils/analyze.d.ts +28 -1
- package/dist/utils/analyze.js +205 -24
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/fix.d.ts +16 -2
- package/dist/utils/fix.js +57 -2
- package/dist/utils/frameworks.d.ts +8 -1
- package/dist/utils/frameworks.js +10 -4
- package/dist/utils/options.d.ts +9 -4
- package/dist/utils/options.js +10 -1
- package/dist/utils/schema.js +5 -1
- package/dist/utils/tickets.js +11 -3
- package/docs/rules/require-ticket-for-commented-out-tests.md +81 -0
- package/docs/rules/require-ticket-in-comments.md +2 -0
- package/docs/rules/require-ticket.md +3 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-11-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `annotations: { only: true }` for Playwright and Vitest: a test or describe must carry its ticket in the
|
|
14
|
+
annotation, which reaches the test report, and a marker comment above one is reported with a suggestion that moves
|
|
15
|
+
its tickets into `annotation` or `meta`. Runtime skips and steps, which can't carry an annotation, keep their
|
|
16
|
+
comments. With any other framework, `only` is a configuration error.
|
|
17
|
+
- A new rule, `require-ticket-for-commented-out-tests`, set to warn in every config: a test commented out by hand,
|
|
18
|
+
such as `// test('places an order', ...)`, needs the `// SKIP: <ticket>` marker a skipped test needs, anywhere in
|
|
19
|
+
its block of comments. It reads comment text, so the configs warn rather than fail. A marker in such a block
|
|
20
|
+
belongs to it, not to the test below the block.
|
|
21
|
+
- A `retry` state, off by default: with `states: { retry: true }`, retries need a `// RETRY: <ticket>` marker, as a
|
|
22
|
+
skip needs `// SKIP:`. It reads Playwright's `test.describe.configure({ retries })`, Jest's `jest.retryTimes()`,
|
|
23
|
+
Vitest's `{ retry }`, Mocha's `this.retries()` in a describe, test or hook written as a `function`, and Cypress's
|
|
24
|
+
`{ retries }`. Any count but `0` counts, including one known only when the tests run, such as
|
|
25
|
+
`process.env.CI ? 2 : 0`, since retries on CI are where flaky tests hide. node:test and Jasmine have no retries.
|
|
26
|
+
- A marker can be the description of an ESLint directive, as in
|
|
27
|
+
`// eslint-disable-next-line jest/no-disabled-tests -- SKIP: TRADE-123`, so a skip that also turns off a rule
|
|
28
|
+
banning skipped tests needs one comment, not two. It works in `eslint-disable-next-line` above the test,
|
|
29
|
+
`eslint-disable-line` on its first line, and an `eslint-disable` block comment, which covers the tests up to the
|
|
30
|
+
`eslint-enable` of one of its rules or of every rule. Removing such a marker keeps the directive, and a marker
|
|
31
|
+
added next to it goes above it.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `require-ticket-in-comments` now reads the description of an ESLint directive, the text after `--`, as it
|
|
36
|
+
reads any comment, so `// eslint-disable-next-line rule -- TODO: migrate this` in a test file needs a ticket:
|
|
37
|
+
`-- TODO: TRADE-123`. Directives weren't read before, so a test file with such descriptions can get new reports on
|
|
38
|
+
upgrade. Add the tickets, or set `comments: false` to turn the check off.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- The `clickup` preset no longer takes digits alone as a task in `#4821` or `https://app.clickup.com/t/9015210959`:
|
|
43
|
+
the first is an issue number in GitHub, GitLab or Redmine, and the second is a workspace link. Write an
|
|
44
|
+
all-digit task ID as `CU-4821` or `/t/<workspace>/4821`.
|
|
45
|
+
|
|
9
46
|
## [0.11.1] - 2026-11-03
|
|
10
47
|
|
|
11
48
|
### Fixed
|
|
@@ -447,7 +484,8 @@ First release.
|
|
|
447
484
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
448
485
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
449
486
|
|
|
450
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
487
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.12.0...HEAD
|
|
488
|
+
[0.12.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.11.1...v0.12.0
|
|
451
489
|
[0.11.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.11.0...v0.11.1
|
|
452
490
|
[0.11.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.10.1...v0.11.0
|
|
453
491
|
[0.10.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.10.0...v0.10.1
|
package/README.md
CHANGED
|
@@ -194,13 +194,14 @@ condition. A function or variable of your own called `pending` is not Jasmine's.
|
|
|
194
194
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
195
195
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
196
196
|
|
|
197
|
-
| Name
|
|
198
|
-
|
|
|
199
|
-
| [marker-matches-state](docs/rules/marker-matches-state.md)
|
|
200
|
-
| [no-conflicting-states](docs/rules/no-conflicting-states.md)
|
|
201
|
-
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md)
|
|
202
|
-
| [require-ticket](docs/rules/require-ticket.md)
|
|
203
|
-
| [require-ticket-
|
|
197
|
+
| Name | Description | 💼 | ⚠️ | 🔧 | 💡 |
|
|
198
|
+
| :--------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :---------------------- | :---------------------- | :-- | :-- |
|
|
199
|
+
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
200
|
+
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | 🔧 | |
|
|
201
|
+
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
202
|
+
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
203
|
+
| [require-ticket-for-commented-out-tests](docs/rules/require-ticket-for-commented-out-tests.md) | Require a ticket marker on commented-out tests | | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | 💡 |
|
|
204
|
+
| [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
204
205
|
|
|
205
206
|
<!-- end auto-generated rules list -->
|
|
206
207
|
|
|
@@ -208,20 +209,21 @@ condition. A function or variable of your own called `pending` is not Jasmine's.
|
|
|
208
209
|
|
|
209
210
|
The defaults follow how most teams already work, so each framework config is useful with no options.
|
|
210
211
|
|
|
211
|
-
| Setting | Default
|
|
212
|
-
| --------------------------------------------------------------------------------------------------------- |
|
|
213
|
-
| States that need a ticket | `skip`, `fixme`; in the other frameworks `skip`, `todo`
|
|
214
|
-
| `@new`, `@quarantined` | off; `lifecycleTags: true` turns both on
|
|
215
|
-
| Conditional skips (`test.skip(browserName === 'webkit', ...)`, `if (...) test.skip()`, a skip in `catch`) | no ticket needed
|
|
216
|
-
| `test.fail`, `test.slow`, Jest's `test.failing`, Vitest's `test.fails` | off
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
|
|
|
212
|
+
| Setting | Default | Why |
|
|
213
|
+
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
214
|
+
| States that need a ticket | `skip`, `fixme`; in the other frameworks `skip`, `todo` | Every team skips tests. Chromium DevTools, GitLab and Slack all require a bug link on disabled tests. |
|
|
215
|
+
| `@new`, `@quarantined` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that say `@flaky` instead [rename the tag](#tag-names). |
|
|
216
|
+
| 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. |
|
|
217
|
+
| `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 }`. |
|
|
218
|
+
| Retries: `describe.configure({ retries })`, `jest.retryTimes()`, `retry`, `this.retries()` | off | Retries hide flaky tests. `states: { retry: true }` needs a ticket wherever tests retry, even only on CI. |
|
|
219
|
+
| Marker format | `// SKIP: TRADE-123` or `// SKIP: TRADE-123, TRADE-124` | Uppercase keyword and colon, like `TODO:`. Easy to grep. |
|
|
220
|
+
| 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. |
|
|
221
|
+
| `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. |
|
|
222
|
+
| Placement | comment block directly above; other comments allowed; a blank line breaks it | Survives `eslint-disable-next-line` and Prettier. |
|
|
223
|
+
| 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. |
|
|
224
|
+
| 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. |
|
|
225
|
+
| Describe blocks | a marker on a skipped or tagged describe covers its tests | One ticket per skipped suite. |
|
|
226
|
+
| Severity | `no-conflicting-states` and `require-ticket-for-commented-out-tests`: 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, and commented-out tests are read from comment text. |
|
|
225
227
|
|
|
226
228
|
## Options
|
|
227
229
|
|
|
@@ -230,21 +232,21 @@ yourself. `framework` is required, and every other option has a default. The rul
|
|
|
230
232
|
of their own: they all read these shared settings, so they always agree on ticket formats, states
|
|
231
233
|
and markers. To turn a rule off, set it to `'off'` as usual.
|
|
232
234
|
|
|
233
|
-
| Option | Type | Default | Description
|
|
234
|
-
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
235
|
-
| `framework` | `'playwright' \| 'jest' \| 'vitest' \| 'mocha' \| 'cypress' \| 'webdriverio' \| 'node-test' \| 'jasmine'` | required | The test framework of the files the config applies to. Each framework's config, such as `configs.playwright`, sets it, and `configure()` needs it. See [Frameworks](#frameworks).
|
|
236
|
-
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets).
|
|
237
|
-
| `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.
|
|
238
|
-
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states.
|
|
239
|
-
| `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `new`, `quarantined`) on or off, renames their tag or marker, or gives them their own ticket format. `fail` and `
|
|
240
|
-
| `customStates` | `{ [name]: { when, marker, ticket? } }` | `{}` | Adds a state for a tag, e.g. `{ 'needs-data': { when: '@needs-data', marker: 'NEEDS-DATA' } }`. `when` is a tag and `marker` an uppercase keyword; neither can be shared with another state.
|
|
241
|
-
| `requireTicketForConditional` | `boolean` | `false` | Require a ticket for conditional skips too: `test.skip(condition, ...)`, `if (...) test.skip()`, `(condition ? test.skip : test)(...)`, a skip in a `catch` block, after other code in a `try` block with a `catch`, or after an early `return` or `throw`.
|
|
242
|
-
| `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test.
|
|
243
|
-
| `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.
|
|
244
|
-
| `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.
|
|
245
|
-
| `annotations` | `false \| { type?: string }`
|
|
246
|
-
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`, Mocha, Cypress and WebdriverIO to `['it', 'specify', 'test']`, and Jasmine to `['it']`. Your list replaces the default one, so include those names if you still need them. See below.
|
|
247
|
-
| `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.
|
|
235
|
+
| Option | Type | Default | Description |
|
|
236
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
237
|
+
| `framework` | `'playwright' \| 'jest' \| 'vitest' \| 'mocha' \| 'cypress' \| 'webdriverio' \| 'node-test' \| 'jasmine'` | required | The test framework of the files the config applies to. Each framework's config, such as `configs.playwright`, sets it, and `configure()` needs it. See [Frameworks](#frameworks). |
|
|
238
|
+
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
239
|
+
| `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. |
|
|
240
|
+
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
|
|
241
|
+
| `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `retry`, `new`, `quarantined`) on or off, renames their tag or marker, or gives them their own ticket format. `fail`, `slow` and `retry` are off by default; a setting for `new` or `quarantined` here wins over `lifecycleTags`. Only `new` and `quarantined` have a tag to rename; see [Tag names](#tag-names). `fixme` and `slow` exist only in Playwright, `todo` in every framework but Playwright, `fail` in Playwright, Jest and Vitest, and `retry` in every framework but node:test and Jasmine, so turning one on elsewhere has no effect. |
|
|
242
|
+
| `customStates` | `{ [name]: { when, marker, ticket? } }` | `{}` | Adds a state for a tag, e.g. `{ 'needs-data': { when: '@needs-data', marker: 'NEEDS-DATA' } }`. `when` is a tag and `marker` an uppercase keyword; neither can be shared with another state. |
|
|
243
|
+
| `requireTicketForConditional` | `boolean` | `false` | Require a ticket for conditional skips too: `test.skip(condition, ...)`, `if (...) test.skip()`, `(condition ? test.skip : test)(...)`, a skip in a `catch` block, after other code in a `try` block with a `catch`, or after an early `return` or `throw`. |
|
|
244
|
+
| `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
|
|
245
|
+
| `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. |
|
|
246
|
+
| `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. |
|
|
247
|
+
| `annotations` | `false \| { type?: string, only?: boolean }` | `{ type: 'issue' }` | Accept the ticket from a test annotation as well: Playwright's `annotation` of the given type or Vitest's `meta` key of that name. One valid annotation covers every state the test is in, and a describe's covers its tests. `only: true` makes it the only place for a test's or describe's ticket. See [Tickets from annotations](#tickets-from-annotations). |
|
|
248
|
+
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`, Mocha, Cypress and WebdriverIO to `['it', 'specify', 'test']`, and Jasmine to `['it']`. Your list replaces the default one, so include those names if you still need them. See below. |
|
|
249
|
+
| `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. |
|
|
248
250
|
|
|
249
251
|
### Tag names
|
|
250
252
|
|
|
@@ -286,7 +288,7 @@ a `customStates` entry.
|
|
|
286
288
|
| `trello` | card links, `https://trello.com/c/nqPiDKmw` with or without the `/9-title` after it | |
|
|
287
289
|
| `notion` | Unique IDs such as `TASK-123`, `https://www.notion.so/TASK-123`, page links that end in the page ID (also `https://app.notion.com/p/...`), database links with `?p=<page ID>` | `prefixes`; `host` for a custom domain (`notion.so`, `www.notion.so`, `app.notion.com` and `*.notion.site` by default) |
|
|
288
290
|
| `asana` | task links, `https://app.asana.com/0/<project>/<task>` and the newer `https://app.asana.com/1/<workspace>/.../task/<task>` | |
|
|
289
|
-
| `clickup` | `CU-86abc123`, `#86abc123
|
|
291
|
+
| `clickup` | `CU-86abc123`, `#86abc123` (not `#123`), custom IDs like `DEV-123`, `https://app.clickup.com/t/86abc123` or `/t/<workspace>/DEV-123`, view links `?p=<task>` | `prefixes` for custom IDs |
|
|
290
292
|
| `redmine` | `#123`, `#123-4` and `#123#note-4` (a note), `##123`; with `host`, `https://<host>/issues/123` (also under a path) | `host`, needed for links |
|
|
291
293
|
| `monday` | item links, `https://<account>.monday.com/boards/<board>/pulses/<item>`, and custom keys such as `TIX-001` | `prefixes` for custom keys |
|
|
292
294
|
| `youtrack` | `KT-123`, `https://<host>/issue/KT-123/...` or `/projects/KT/issues/KT-123` (also under a path, as in `/youtrack/issue/KT-123`) | `projects`; `host` for Server or a custom domain (`youtrack.jetbrains.com`, `*.youtrack.cloud` and `*.myjetbrains.com` by default) |
|
|
@@ -369,6 +371,26 @@ left alone, and an annotation whose ticket isn't static text is reported with `r
|
|
|
369
371
|
both must be valid. Jest, Mocha, Jasmine, node:test and Cypress have no annotations, so for them it is
|
|
370
372
|
comments only.
|
|
371
373
|
|
|
374
|
+
With `annotations: { only: true }`, a test or describe in Playwright or Vitest must carry its ticket in the
|
|
375
|
+
annotation, and a marker comment above it is reported, with a suggestion that moves its tickets into
|
|
376
|
+
`annotation` or `meta`. A runtime skip such as `test.skip()` in a body, and a step, can't carry an
|
|
377
|
+
annotation, so their comments still count. `only` with another framework is a configuration error.
|
|
378
|
+
|
|
379
|
+
### Markers in `eslint-disable` comments
|
|
380
|
+
|
|
381
|
+
If you also run a rule that bans skipped tests, such as `jest/no-disabled-tests`, the comment that turns it off
|
|
382
|
+
can hold the marker in its description, after `--`:
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
// eslint-disable-next-line jest/no-disabled-tests -- SKIP: TRADE-123
|
|
386
|
+
test.skip('rejects a margin order above the buying power', () => {});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The description is read as a marker comment, with the same tickets and notes rules. It works in an
|
|
390
|
+
`eslint-disable-next-line` above the test, an `eslint-disable-line` on the line the test starts on, and an
|
|
391
|
+
`eslint-disable` block comment, whose marker covers every test up to the `eslint-enable` of one of its rules, or of
|
|
392
|
+
every rule. A suggestion that removes or adds a marker keeps the directive working.
|
|
393
|
+
|
|
372
394
|
### Which functions count as `test`
|
|
373
395
|
|
|
374
396
|
In Playwright files:
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type GovernanceOptions } from './utils/options.ts';
|
|
|
4
4
|
export type { FrameworkName } from './utils/constants.ts';
|
|
5
5
|
export type { CustomState, GovernanceOptions, StateOverride } from './utils/options.ts';
|
|
6
6
|
export type { TicketSpec } from './utils/tickets.ts';
|
|
7
|
-
export type RuleName = 'require-ticket' | 'marker-matches-state' | 'no-orphaned-marker' | 'no-conflicting-states' | 'require-ticket-in-comments';
|
|
7
|
+
export type RuleName = 'require-ticket' | 'marker-matches-state' | 'no-orphaned-marker' | 'no-conflicting-states' | 'require-ticket-in-comments' | 'require-ticket-for-commented-out-tests';
|
|
8
8
|
export declare const rules: Record<RuleName, Rule.RuleModule>;
|
|
9
9
|
export interface FlatConfig {
|
|
10
10
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import markerMatchesState from "./rules/marker-matches-state.js";
|
|
3
3
|
import noConflictingStates from "./rules/no-conflicting-states.js";
|
|
4
4
|
import noOrphanedMarker from "./rules/no-orphaned-marker.js";
|
|
5
|
+
import requireTicketForCommentedOutTests from "./rules/require-ticket-for-commented-out-tests.js";
|
|
5
6
|
import requireTicketInComments from "./rules/require-ticket-in-comments.js";
|
|
6
7
|
import requireTicket from "./rules/require-ticket.js";
|
|
7
8
|
import { FRAMEWORK_NAMES } from "./utils/constants.js";
|
|
@@ -16,6 +17,7 @@ const ruleModules = {
|
|
|
16
17
|
'no-orphaned-marker': noOrphanedMarker,
|
|
17
18
|
'no-conflicting-states': noConflictingStates,
|
|
18
19
|
'require-ticket-in-comments': requireTicketInComments,
|
|
20
|
+
'require-ticket-for-commented-out-tests': requireTicketForCommentedOutTests,
|
|
19
21
|
};
|
|
20
22
|
// Public types come from `eslint` itself, so users don't need typescript-eslint installed.
|
|
21
23
|
export const rules = ruleModules;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appliesTo, describeSubject, isStateMarker, missingStates, strayMarkers } from "../utils/analyze.js";
|
|
1
|
+
import { appliesTo, describeSubject, directiveOf, isStateMarker, missingStates, strayMarkers, } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
3
|
import { indentOf, keywordRange } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
@@ -53,8 +53,13 @@ export default createRule({
|
|
|
53
53
|
.find((m) => m.state !== missing[0] && appliesTo(subject, m.state.name));
|
|
54
54
|
if (!present)
|
|
55
55
|
continue;
|
|
56
|
+
// A marker added after an `eslint-disable-next-line` would take the line it disables, so it goes above.
|
|
57
|
+
// One on the test's own line, in an `eslint-disable-line`, has no line of its own to go next to.
|
|
58
|
+
const directive = directiveOf(present.comment);
|
|
59
|
+
const indent = indentOf(sourceCode, present.comment);
|
|
56
60
|
for (const state of missing) {
|
|
57
61
|
const tickets = present.tickets.map((t) => t.text).join(', ');
|
|
62
|
+
const added = `// ${state.marker}: ${tickets}`;
|
|
58
63
|
context.report({
|
|
59
64
|
loc: present.comment.loc,
|
|
60
65
|
messageId: 'sharedMarker',
|
|
@@ -65,12 +70,14 @@ export default createRule({
|
|
|
65
70
|
state: state.name,
|
|
66
71
|
subject: subjectName,
|
|
67
72
|
},
|
|
68
|
-
suggest: present.comment.type === 'Line' && tickets !== ''
|
|
73
|
+
suggest: present.comment.type === 'Line' && tickets !== '' && directive?.kind !== 'eslint-disable-line'
|
|
69
74
|
? [
|
|
70
75
|
{
|
|
71
76
|
messageId: 'addMarker',
|
|
72
77
|
data: { expected: state.marker, tickets },
|
|
73
|
-
fix: (fixer) =>
|
|
78
|
+
fix: (fixer) => directive
|
|
79
|
+
? fixer.insertTextBeforeRange(present.comment.range, `${added}\n${indent}`)
|
|
80
|
+
: fixer.insertTextAfterRange(present.comment.range, `\n${indent}${added}`),
|
|
74
81
|
},
|
|
75
82
|
]
|
|
76
83
|
: [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describeSubject, isProse, missingStates, strayMarkers } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
|
-
import {
|
|
3
|
+
import { removeMarkerComment } from "../utils/fix.js";
|
|
4
4
|
export default createRule({
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'problem',
|
|
@@ -50,7 +50,7 @@ export default createRule({
|
|
|
50
50
|
loc: marker.comment.loc,
|
|
51
51
|
messageId,
|
|
52
52
|
data: { marker: marker.keyword, state: marker.state.name, subject },
|
|
53
|
-
suggest: [{ messageId: 'removeMarker', fix: () =>
|
|
53
|
+
suggest: [{ messageId: 'removeMarker', fix: () => removeMarkerComment(sourceCode, marker.comment) }],
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createRule } from "../utils/create-rule.js";
|
|
2
|
+
export default createRule({
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Require a ticket marker on commented-out tests',
|
|
7
|
+
recommended: 'warn',
|
|
8
|
+
url: 'https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/rules/require-ticket-for-commented-out-tests.md',
|
|
9
|
+
},
|
|
10
|
+
hasSuggestions: true,
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
missingMarker: 'This commented-out test has no `// {{marker}}: <ticket>` comment, e.g. `// {{marker}}: {{example}}`. Track it like a skipped test, or delete it.',
|
|
14
|
+
missingTicket: '`{{marker}}:` needs a ticket right after the colon, e.g. `// {{marker}}: {{example}}`.',
|
|
15
|
+
invalidTicket: "'{{ticket}}' is not a valid ticket for the 'skip' state. Expected {{expected}}.",
|
|
16
|
+
placeholderTicket: "'{{ticket}}' is a placeholder, not a real ticket. Link the ticket that tracks this test.",
|
|
17
|
+
extraText: 'Only ticket IDs are allowed after `{{marker}}:`, separated by commas. Put details in the ticket instead of `{{text}}`.',
|
|
18
|
+
removeExtraText: 'Remove `{{text}}`.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context, analysis, options) {
|
|
22
|
+
// Commenting a test out skips it by hand, so it takes the skip state's marker and ticket format.
|
|
23
|
+
const skip = options.states.find((state) => state.name === 'skip');
|
|
24
|
+
if (!analysis.isTestFile || skip === undefined)
|
|
25
|
+
return;
|
|
26
|
+
const { sourceCode } = context;
|
|
27
|
+
for (const { range, markers } of analysis.commentedOutTests) {
|
|
28
|
+
const own = markers.filter((marker) => marker.state === skip);
|
|
29
|
+
const data = { marker: skip.marker, example: skip.ticket.example };
|
|
30
|
+
if (own.length === 0) {
|
|
31
|
+
const loc = { start: sourceCode.getLocFromIndex(range[0]), end: sourceCode.getLocFromIndex(range[1]) };
|
|
32
|
+
context.report({ loc, messageId: 'missingMarker', data });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const valid = own.find((m) => m.tickets.length > 0 && m.tickets.every((t) => t.result === 'ok'));
|
|
36
|
+
if (valid) {
|
|
37
|
+
const { extra } = valid;
|
|
38
|
+
if (extra === undefined || options.allowNotes)
|
|
39
|
+
continue;
|
|
40
|
+
context.report({
|
|
41
|
+
loc: { start: sourceCode.getLocFromIndex(extra.range[0]), end: sourceCode.getLocFromIndex(extra.range[1]) },
|
|
42
|
+
messageId: 'extraText',
|
|
43
|
+
data: { marker: skip.marker, text: extra.text },
|
|
44
|
+
suggest: [
|
|
45
|
+
{
|
|
46
|
+
messageId: 'removeExtraText',
|
|
47
|
+
data: { text: extra.text },
|
|
48
|
+
fix: (fixer) => fixer.removeRange(extra.range),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const [marker] = own;
|
|
55
|
+
const bad = marker.tickets.find((t) => t.result !== 'ok');
|
|
56
|
+
if (bad === undefined) {
|
|
57
|
+
context.report({ loc: marker.comment.loc, messageId: 'missingTicket', data });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
context.report({
|
|
61
|
+
loc: marker.comment.loc,
|
|
62
|
+
messageId: bad.result === 'placeholder' ? 'placeholderTicket' : 'invalidTicket',
|
|
63
|
+
data: { ticket: bad.text, expected: skip.ticket.expected },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("../utils/create-rule.ts").Rule<"missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "
|
|
1
|
+
declare const _default: import("../utils/create-rule.ts").Rule<"missingMarker" | "missingTicket" | "invalidTicket" | "placeholderTicket" | "extraText" | "removeExtraText" | "markerCase" | "dynamicTitle" | "dynamicTags" | "invalidAnnotation" | "useAnnotation" | "missingAnnotation" | "moveToAnnotation" | "unreadAnnotation">;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { annotationTicketsOf, appliesTo, describeSubject, evaluate, hasAnnotationTicket, headLoc, unreadAnnotationsOf, } from "../utils/analyze.js";
|
|
2
2
|
import { createRule } from "../utils/create-rule.js";
|
|
3
|
+
import { moveToAnnotation } from "../utils/fix.js";
|
|
3
4
|
export default createRule({
|
|
4
5
|
meta: {
|
|
5
6
|
type: 'problem',
|
|
@@ -21,6 +22,9 @@ export default createRule({
|
|
|
21
22
|
dynamicTitle: "This title isn't static text, so its tags can't be checked. Use a string or template literal.",
|
|
22
23
|
dynamicTags: "These tags can't be read in this file, so they can't be checked. Use strings, or constants declared in this file.",
|
|
23
24
|
invalidAnnotation: "'{{ticket}}' is not a valid ticket for the '{{state}}' state. Expected {{expected}}.",
|
|
25
|
+
useAnnotation: 'Put the ticket in an `{{type}}` annotation instead of a comment, so it reaches the test report, e.g. `{{example}}`.',
|
|
26
|
+
missingAnnotation: "{{subject}} is in the '{{state}}' state but has no `{{type}}` annotation with a ticket, e.g. `{{example}}`.",
|
|
27
|
+
moveToAnnotation: 'Move the ticket into an `{{type}}` annotation.',
|
|
24
28
|
unreadAnnotation: "This annotation's ticket isn't static text, so it can't be checked. Use a string, or a constant declared in this file.",
|
|
25
29
|
},
|
|
26
30
|
},
|
|
@@ -29,6 +33,38 @@ export default createRule({
|
|
|
29
33
|
// A broken marker on a describe can be reached from several tests; report it once.
|
|
30
34
|
const reported = new Set();
|
|
31
35
|
const reportOnce = (key) => !reported.has(key) && Boolean(reported.add(key));
|
|
36
|
+
// With `annotations.only`, a test or describe takes its ticket from an annotation only; runtime skips
|
|
37
|
+
// and steps can't carry one, so their comments still count.
|
|
38
|
+
const only = options.annotations?.only === true && options.framework.annotations !== undefined
|
|
39
|
+
? { source: options.framework.annotations, type: options.annotations.type }
|
|
40
|
+
: undefined;
|
|
41
|
+
/** Reports the comment markers that should be annotations, or the missing annotation when there is none. */
|
|
42
|
+
const reportAnnotationOnly = (subject, state, { source, type }) => {
|
|
43
|
+
const example = source.shape === 'meta'
|
|
44
|
+
? `meta: { ${type}: '${state.ticket.example}' }`
|
|
45
|
+
: `annotation: { type: '${type}', description: '${state.ticket.example}' }`;
|
|
46
|
+
const markers = [...subject.markers, ...subject.regionMarkers].filter((m) => m.state === state);
|
|
47
|
+
for (const marker of markers) {
|
|
48
|
+
if (!reportOnce(`only:${marker.comment.range[0]}`))
|
|
49
|
+
continue;
|
|
50
|
+
const fix = subject.markers.includes(marker)
|
|
51
|
+
? moveToAnnotation(context.sourceCode, subject.node, marker, source, type)
|
|
52
|
+
: undefined;
|
|
53
|
+
context.report({
|
|
54
|
+
loc: marker.comment.loc,
|
|
55
|
+
messageId: 'useAnnotation',
|
|
56
|
+
data: { type, example },
|
|
57
|
+
suggest: fix ? [{ messageId: 'moveToAnnotation', data: { type }, fix }] : [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (markers.length > 0 || hasAnnotationTicket(subject, state, options))
|
|
61
|
+
return;
|
|
62
|
+
context.report({
|
|
63
|
+
loc: headLoc(subject),
|
|
64
|
+
messageId: 'missingAnnotation',
|
|
65
|
+
data: { subject: describeSubject(subject), state: state.name, type, example },
|
|
66
|
+
});
|
|
67
|
+
};
|
|
32
68
|
for (const subject of analysis.subjects) {
|
|
33
69
|
const needsTicket = subject.states.some((s) => s.required);
|
|
34
70
|
if (options.reportUnreadableTags && hasTagStates) {
|
|
@@ -45,9 +81,14 @@ export default createRule({
|
|
|
45
81
|
}
|
|
46
82
|
}
|
|
47
83
|
}
|
|
84
|
+
const annotationOnly = only && (subject.kind === 'test' || subject.kind === 'describe') ? only : undefined;
|
|
48
85
|
for (const { state, required } of subject.states) {
|
|
49
86
|
if (!required)
|
|
50
87
|
continue;
|
|
88
|
+
if (annotationOnly) {
|
|
89
|
+
reportAnnotationOnly(subject, state, annotationOnly);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
51
92
|
const result = evaluate(subject, state);
|
|
52
93
|
// Annotations are checked whenever a ticket is needed, whatever the markers say.
|
|
53
94
|
for (const ticket of annotationTicketsOf(subject)) {
|
|
@@ -106,9 +147,9 @@ export default createRule({
|
|
|
106
147
|
}
|
|
107
148
|
// Markers are ticket-only unless notes are allowed. A marker with a broken ticket is
|
|
108
149
|
// already reported above, so its extra text waits until the ticket is fixed.
|
|
109
|
-
if (options.allowNotes)
|
|
150
|
+
if (options.allowNotes || annotationOnly)
|
|
110
151
|
continue;
|
|
111
|
-
for (const marker of subject.markers) {
|
|
152
|
+
for (const marker of [...subject.markers, ...subject.regionMarkers]) {
|
|
112
153
|
const { extra, state } = marker;
|
|
113
154
|
if (!extra || !state || !appliesTo(subject, state.name))
|
|
114
155
|
continue;
|
package/dist/utils/analyze.d.ts
CHANGED
|
@@ -28,6 +28,18 @@ export type StateMarker = Marker & {
|
|
|
28
28
|
state: StateDef;
|
|
29
29
|
};
|
|
30
30
|
export declare function isStateMarker(marker: Marker): marker is StateMarker;
|
|
31
|
+
export interface Directive {
|
|
32
|
+
kind: 'eslint-disable-next-line' | 'eslint-disable-line' | 'eslint-disable' | 'eslint-enable';
|
|
33
|
+
/** The rules it names; none means every rule. */
|
|
34
|
+
rules: string[];
|
|
35
|
+
/** Where ` -- ` starts in the comment's value and where the description after it starts. */
|
|
36
|
+
description?: {
|
|
37
|
+
separator: number;
|
|
38
|
+
start: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The ESLint directive a comment holds, read the way ESLint reads it, or undefined for any other comment. */
|
|
42
|
+
export declare function directiveOf(comment: Comment): Directive | undefined;
|
|
31
43
|
export interface TagOccurrence {
|
|
32
44
|
tag: string;
|
|
33
45
|
/** Where to report the tag: the text it is written in, or the name it comes from, as `Tags.NEW`. */
|
|
@@ -72,6 +84,8 @@ export interface Subject {
|
|
|
72
84
|
skipped: boolean;
|
|
73
85
|
ownSkip: boolean;
|
|
74
86
|
markers: Marker[];
|
|
87
|
+
/** Markers of the `eslint-disable` regions this node is in, other than those in its own block. */
|
|
88
|
+
regionMarkers: Marker[];
|
|
75
89
|
/** Tickets read from annotations of the configured type on this call, with the node to report at. */
|
|
76
90
|
annotationTickets: {
|
|
77
91
|
text: string;
|
|
@@ -83,10 +97,19 @@ export interface Subject {
|
|
|
83
97
|
/** For a runtime function such as Jasmine's `pending()`, its name; messages call the others by their state. */
|
|
84
98
|
functionName?: string;
|
|
85
99
|
}
|
|
100
|
+
/** A block of comments holding a test or describe written out as code, as `// test('places an order', ...)`. */
|
|
101
|
+
export interface CommentedOutTest {
|
|
102
|
+
/** The first line that is a commented-out test or describe, without the comment's leading space or `*`. */
|
|
103
|
+
range: TSESTree.Range;
|
|
104
|
+
/** The markers anywhere in the block. */
|
|
105
|
+
markers: Marker[];
|
|
106
|
+
}
|
|
86
107
|
export interface Analysis {
|
|
87
108
|
subjects: Subject[];
|
|
88
109
|
/** Markers that are not in the comment block of any test, describe or runtime call. */
|
|
89
110
|
detachedMarkers: StateMarker[];
|
|
111
|
+
/** Commented-out tests; their comments are left out of the comment blocks of the tests around them. */
|
|
112
|
+
commentedOutTests: CommentedOutTest[];
|
|
90
113
|
/** Whether the file uses the test framework at all: an import from it or a call to a test function. */
|
|
91
114
|
isTestFile: boolean;
|
|
92
115
|
}
|
|
@@ -105,12 +128,16 @@ export type Evaluation = {
|
|
|
105
128
|
marker: Marker;
|
|
106
129
|
ticket: MarkerTicket;
|
|
107
130
|
};
|
|
131
|
+
export declare function isFunction(node: Node | undefined): node is TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;
|
|
108
132
|
/**
|
|
109
133
|
* Parses `TICKET[, TICKET...]` from the text after a marker's colon. `offset` is the source position
|
|
110
134
|
* of `text[0]`. Anything after the last ticket, including a trailing comma, is returned as `extra`.
|
|
111
135
|
*/
|
|
112
136
|
export declare function parseTickets(text: string, offset: number, matcher: TicketMatcher): Pick<Marker, 'tickets' | 'extra'>;
|
|
113
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* The lines of a comment and where each starts in the source, split at every line break JavaScript has.
|
|
139
|
+
* In an ESLint directive, only the description after ` -- ` is comment text.
|
|
140
|
+
*/
|
|
114
141
|
export declare function commentLines(comment: Comment): {
|
|
115
142
|
text: string;
|
|
116
143
|
start: number;
|