eslint-plugin-test-governance 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +166 -64
  3. package/dist/index.d.ts +14 -12
  4. package/dist/index.js +22 -12
  5. package/dist/rules/marker-matches-state.d.ts +1 -2
  6. package/dist/rules/marker-matches-state.js +7 -7
  7. package/dist/rules/no-conflicting-states.d.ts +1 -2
  8. package/dist/rules/no-conflicting-states.js +12 -20
  9. package/dist/rules/no-orphaned-marker.d.ts +1 -2
  10. package/dist/rules/no-orphaned-marker.js +15 -10
  11. package/dist/rules/require-ticket-in-comments.d.ts +1 -2
  12. package/dist/rules/require-ticket-in-comments.js +13 -18
  13. package/dist/rules/require-ticket.d.ts +1 -2
  14. package/dist/rules/require-ticket.js +18 -10
  15. package/dist/utils/analyze.d.ts +17 -14
  16. package/dist/utils/analyze.js +370 -153
  17. package/dist/utils/constants.d.ts +5 -3
  18. package/dist/utils/constants.js +4 -3
  19. package/dist/utils/create-rule.d.ts +9 -10
  20. package/dist/utils/create-rule.js +5 -11
  21. package/dist/utils/distance.d.ts +0 -1
  22. package/dist/utils/distance.js +0 -1
  23. package/dist/utils/errors.d.ts +0 -1
  24. package/dist/utils/errors.js +0 -1
  25. package/dist/utils/fix.d.ts +0 -3
  26. package/dist/utils/fix.js +0 -8
  27. package/dist/utils/frameworks.d.ts +59 -0
  28. package/dist/utils/frameworks.js +102 -0
  29. package/dist/utils/options.d.ts +26 -11
  30. package/dist/utils/options.js +13 -9
  31. package/dist/utils/schema.d.ts +1 -2
  32. package/dist/utils/schema.js +2 -2
  33. package/dist/utils/tickets.d.ts +0 -1
  34. package/dist/utils/tickets.js +4 -4
  35. package/dist/utils/validate.d.ts +0 -1
  36. package/dist/utils/validate.js +3 -3
  37. package/docs/rules/marker-matches-state.md +18 -10
  38. package/docs/rules/no-conflicting-states.md +14 -8
  39. package/docs/rules/no-orphaned-marker.md +31 -17
  40. package/docs/rules/require-ticket-in-comments.md +26 -22
  41. package/docs/rules/require-ticket.md +107 -47
  42. package/package.json +7 -10
package/CHANGELOG.md CHANGED
@@ -6,6 +6,79 @@ 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
+
69
+ ## [0.3.1] - 2026-10-06
70
+
71
+ ### Fixed
72
+
73
+ - A `test` bound to another test runner without an import, as in `const test = require('node:test')`,
74
+ `const test = require('ava')` or `const test = anyTest as TestFn<Context>`, is no longer mistaken
75
+ for Playwright's `test`. Before, skipped tests in those files were reported as missing a ticket.
76
+ - `require-ticket-in-comments` reads a ticket in parentheses after a space, as in `// TODO (WEB-123)`.
77
+ Before, such comments were reported as having no ticket at all.
78
+ - A test or describe whose body is not written inline, such as `test.skip('pays', payWithCard)` or
79
+ `test('pays @new', withPage(async () => {}))`, is checked like any other. Before, it was ignored,
80
+ so a skipped test written this way needed no ticket.
81
+
9
82
  ## [0.3.0] - 2026-10-06
10
83
 
11
84
  ### Added
@@ -121,7 +194,9 @@ First release.
121
194
  - Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
122
195
  fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
123
196
 
124
- [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...HEAD
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
199
+ [0.3.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...v0.3.1
125
200
  [0.3.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.1...v0.3.0
126
201
  [0.2.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.0...v0.2.1
127
202
  [0.2.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.1.1...v0.2.0
package/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # eslint-plugin-test-governance
2
2
 
3
- ESLint rules that keep a Playwright suite honest. Every skipped, fixme, `@new` or `@unstable` test
4
- must point at the ticket that tracks it:
3
+ [![npm](https://img.shields.io/npm/v/eslint-plugin-test-governance)](https://www.npmjs.com/package/eslint-plugin-test-governance)
4
+ [![CI](https://github.com/Huldoser/eslint-plugin-test-governance/actions/workflows/ci.yml/badge.svg)](https://github.com/Huldoser/eslint-plugin-test-governance/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/npm/l/eslint-plugin-test-governance)](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: WEB-123
8
- test.skip('pays with PayPal', async ({ page }) => {
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. With `defineConfig`, register the plugin and use
46
- `extends: ['test-governance/recommended']`. ESLint loads an `eslint.config.ts` through
47
- [`jiti`](https://github.com/unjs/jiti), so install it as a dev dependency too.
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: ['WEB', 'QA'] },
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 | Why |
104
- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
105
- | States that need a ticket | `skip`, `fixme` | Every team skips tests. Chromium, GitLab and Slack all require a bug link on disabled tests. |
106
- | `@new`, `@unstable` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that call it `@flaky` or `@quarantine` use `customStates`. |
107
- | 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. |
108
- | `test.fail`, `test.slow` | off | They don't remove coverage. Turn them on with `states: { fail: true, slow: true }`. |
109
- | Marker format | `// SKIP: WEB-123` or `// SKIP: WEB-123, WEB-124` | Uppercase keyword and colon, like `TODO:`. Easy to grep. |
110
- | 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. |
111
- | `FIXME` / `TODO` comments | must start with a ticket, e.g. `// TODO: WEB-123` or `// TODO(WEB-123)` | The ticket is the source of truth; an untracked `TODO` is never done. `comments: false` turns this off. |
112
- | Placement | comment block directly above; other comments allowed; a blank line breaks it | Survives `eslint-disable-next-line` and Prettier. |
113
- | 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. |
114
- | Placeholder tickets | rejected: `TODO`, `TBD`, `XXX-*`, `0`, `123`, `1234`, `12345`, and any ticket numbered 0 (`PROJ-0`, `#0`) | A fake ticket is worse than none, and no tracker issues number 0. |
115
- | Describe blocks | a marker on a skipped or tagged describe covers its tests | One ticket per skipped suite. |
116
- | Severity | `require-ticket`, `marker-matches-state`, `no-orphaned-marker`: error; `no-conflicting-states`: warn | A marker must always match a real state, so a missing, wrong or leftover marker fails the lint. Tag spelling is cleanup. |
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
- | `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. |
127
- | `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive; a leading `#` is ignored. Tickets numbered 0 are always rejected. |
128
- | `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `unstable` (`@unstable`) states. |
129
- | `states` | `{ [state]: boolean \| { enabled?, marker?, ticket? } }` | | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `new`, `unstable`) on or off, renames their marker, or gives them their own ticket format. |
130
- | `customStates` | `{ [name]: { when, marker, ticket? } }` | `{}` | Adds a state for a tag, e.g. `{ quarantine: { when: '@quarantine', marker: 'QUARANTINE' } }`. |
131
- | `requireTicketForConditional` | `boolean` | `false` | Require a ticket for `test.skip(condition, ...)` and `if (...) test.skip()` too. |
132
- | `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
133
- | `allowNotes` | `boolean` | `false` | Allow free text after the tickets, e.g. `// SKIP: WEB-123 flaky on CI`. |
134
- | `comments` | `false \| { keywords?: string[] }` | `{ keywords: ['FIXME', 'TODO'] }` | Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: WEB-123`. `false` allows free-form comments. |
135
- | `testFunctions` | `string[]` | `['test']` | Names treated as Playwright's `test`. See below. |
136
- | `reportDynamicTitles` | `boolean` | `false` | When a tag state is on, report titles that aren't static text, since their tags can't be read. |
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 | Options |
141
- | -------------- | ----------------------------------------------------------------------------------- | --------------------------------- |
142
- | `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL | |
143
- | `jira` | `WEB-123`, `https://<host>/browse/WEB-123`, board URLs with `selectedIssue=WEB-123` | `projects`, `host` |
144
- | `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`) | `host` for GitHub Enterprise |
145
- | `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123` | `host` for self-managed |
146
- | `linear` | `ENG-123`, `https://linear.app/<workspace>/issue/ENG-123/...` | `teams` |
147
- | `azure-devops` | `AB#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123` | `host` for Azure DevOps Server |
148
- | `numeric` | `4821` | `minLength` (1), `maxLength` (20) |
149
- | `pattern` | anything matching your regex, anchored at both ends | `pattern`, `flags` |
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: ['WEB'] },
219
+ ticket: { preset: 'jira', projects: ['TRADE'] },
157
220
  lifecycleTags: true,
158
221
  states: { unstable: { ticket: { preset: 'github' } } },
159
222
  });
@@ -164,42 +227,81 @@ so keep it free of nested quantifiers.
164
227
 
165
228
  ### Which functions count as `test`
166
229
 
167
- - `test` imported from `@playwright/test` or a component-testing package such as
168
- `@playwright/experimental-ct-react`, including aliases such as `import { test as it }` and
169
- `const { test } = require('@playwright/test')`.
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
- A name imported from another test runner (`vitest`, `@jest/globals`, `node:test`, `bun:test`,
176
- `mocha`, `ava`, `tap`, `uvu`) is never treated as Playwright's `test`, and neither is a local
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
177
253
  variable declared inside a function that happens to be called `test`.
178
254
 
179
255
  ### Syntax the rules understand
180
256
 
181
- - `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, and the same with a details object.
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)`.
182
261
  - `test.describe.skip(...)` and `test.describe.fixme(...)`, including `serial` and `parallel` variants.
183
262
  The state covers every test inside.
184
- - `test.skip()`, `test.skip(true)`, `test.fixme()` inside a test or hook; `testInfo.skip()`,
185
- `test.info().skip()`.
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.
186
269
  - Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
187
270
  from enclosing describes.
188
271
 
189
- Not covered in this version: tests generated in loops with dynamic titles, fixtures imported from
190
- other modules under a name not in `testFunctions`, and Playwright annotations as markers.
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`.
191
276
 
192
- ## Development
277
+ ## TypeScript
193
278
 
194
- ```sh
195
- npm install
196
- npm test # rule tests under espree and @typescript-eslint/parser
197
- npm run test:coverage # fails below 100% coverage of src/rules and src/utils
198
- npm run typecheck
199
- npm run test:pack # pack the tarball and lint tests/fixtures/sample-project with it
200
- npm run docs # regenerate the rules list and rule doc headers
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
+ ];
201
298
  ```
202
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
+
203
305
  ## License
204
306
 
205
307
  [MIT](LICENSE)
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { ESLint, Rule } from 'eslint';
2
- import { type GovernanceOptions } from './utils/options.js';
3
- export type { CustomState, GovernanceOptions, StateOverride } from './utils/options.js';
4
- export type { TicketSpec } from './utils/tickets.js';
2
+ import { type FrameworkName } from './utils/constants.ts';
3
+ import { type GovernanceOptions } from './utils/options.ts';
4
+ export type { FrameworkName } from './utils/constants.ts';
5
+ export type { CustomState, GovernanceOptions, StateOverride } from './utils/options.ts';
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';
6
8
  export declare const rules: Record<RuleName, Rule.RuleModule>;
7
9
  export interface FlatConfig {
@@ -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
@@ -1,10 +1,11 @@
1
1
  import { createRequire } from 'node:module';
2
- import markerMatchesState from './rules/marker-matches-state.js';
3
- import noConflictingStates from './rules/no-conflicting-states.js';
4
- import noOrphanedMarker from './rules/no-orphaned-marker.js';
5
- import requireTicketInComments from './rules/require-ticket-in-comments.js';
6
- import requireTicket from './rules/require-ticket.js';
7
- import { compileOptions, SETTINGS_KEY } from './utils/options.js';
2
+ import markerMatchesState from "./rules/marker-matches-state.js";
3
+ import noConflictingStates from "./rules/no-conflicting-states.js";
4
+ import noOrphanedMarker from "./rules/no-orphaned-marker.js";
5
+ import requireTicketInComments from "./rules/require-ticket-in-comments.js";
6
+ import requireTicket from "./rules/require-ticket.js";
7
+ import { FRAMEWORK_NAMES } from "./utils/constants.js";
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.
10
11
  const { version: VERSION } = createRequire(import.meta.url)('../package.json');
@@ -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}/recommended`,
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
- // Makes `require('eslint-plugin-test-governance')` return the plugin itself rather than the module
51
- // namespace. With the namespace, a CommonJS config that registers the plugin next to
52
- // `configs.recommended` fails with "Cannot redefine plugin".
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
@@ -1,3 +1,2 @@
1
- declare const _default: import("../utils/create-rule.js").Rule<"wrongMarker" | "sharedMarker" | "renameMarker" | "addMarker">;
1
+ declare const _default: import("../utils/create-rule.ts").Rule<"wrongMarker" | "sharedMarker" | "renameMarker" | "addMarker">;
2
2
  export default _default;
3
- //# sourceMappingURL=marker-matches-state.d.ts.map
@@ -1,15 +1,16 @@
1
- import { appliesTo, describeSubject, isStateMarker, missingStates, strayMarkers } from '../utils/analyze.js';
2
- import { createRule } from '../utils/create-rule.js';
3
- import { indentOf, keywordRange } from '../utils/fix.js';
1
+ import { appliesTo, describeSubject, isStateMarker, missingStates, strayMarkers } from "../utils/analyze.js";
2
+ import { createRule } from "../utils/create-rule.js";
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
- check(context, analysis, options) {
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
@@ -1,3 +1,2 @@
1
- declare const _default: import("../utils/create-rule.js").Rule<"newAndUnstable" | "newSkipped" | "tagCase" | "tagTypo">;
1
+ declare const _default: import("../utils/create-rule.ts").Rule<"newAndUnstable" | "newSkipped" | "tagCase" | "tagTypo">;
2
2
  export default _default;
3
- //# sourceMappingURL=no-conflicting-states.d.ts.map