eslint-plugin-test-governance 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/README.md +202 -83
  3. package/dist/index.d.ts +12 -11
  4. package/dist/index.js +28 -14
  5. package/dist/rules/marker-matches-state.d.ts +0 -1
  6. package/dist/rules/marker-matches-state.js +4 -4
  7. package/dist/rules/no-conflicting-states.d.ts +0 -1
  8. package/dist/rules/no-conflicting-states.js +17 -19
  9. package/dist/rules/no-orphaned-marker.d.ts +0 -1
  10. package/dist/rules/no-orphaned-marker.js +12 -7
  11. package/dist/rules/require-ticket-in-comments.d.ts +0 -1
  12. package/dist/rules/require-ticket-in-comments.js +8 -13
  13. package/dist/rules/require-ticket.d.ts +1 -2
  14. package/dist/rules/require-ticket.js +22 -10
  15. package/dist/utils/analyze.d.ts +29 -12
  16. package/dist/utils/analyze.js +526 -181
  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 +7 -8
  20. package/dist/utils/create-rule.js +3 -9
  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 +68 -0
  28. package/dist/utils/frameworks.js +108 -0
  29. package/dist/utils/options.d.ts +33 -12
  30. package/dist/utils/options.js +9 -5
  31. package/dist/utils/schema.d.ts +0 -1
  32. package/dist/utils/schema.js +9 -3
  33. package/dist/utils/tickets.d.ts +0 -1
  34. package/dist/utils/tickets.js +3 -3
  35. package/dist/utils/validate.d.ts +4 -1
  36. package/dist/utils/validate.js +9 -4
  37. package/docs/rules/marker-matches-state.md +19 -11
  38. package/docs/rules/no-conflicting-states.md +18 -10
  39. package/docs/rules/no-orphaned-marker.md +59 -18
  40. package/docs/rules/require-ticket-in-comments.md +27 -23
  41. package/docs/rules/require-ticket.md +135 -49
  42. package/docs/upgrading.md +131 -0
  43. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -6,6 +6,117 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-10-08
10
+
11
+ This release changes how the plugin is configured, ahead of 1.0.
12
+ [Upgrading to 0.5](https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md#upgrading-to-05)
13
+ shows what to change, with a before and after for each one.
14
+
15
+ ### Changed
16
+
17
+ - **Breaking:** `configure()` needs a `framework`, as in
18
+ `configure({ framework: 'playwright', ticket: { preset: 'jira' } })`, and so does
19
+ `settings['test-governance']` in a config you build yourself. Before, a config without one checked
20
+ files as Playwright tests. Now `configure()` fails when the config file loads, and hand-written
21
+ settings fail on the first lint, with a message that names the three frameworks. To share options
22
+ between frameworks, pass the same options to a `configure()` for each one.
23
+ - **Breaking:** `reportDynamicTitles` is renamed to `reportUnreadableTags`, because it also reports
24
+ tags and details objects imported from another module, not only titles. It works the same way, and
25
+ a config that still uses the old name fails with a message that names the new one.
26
+ - Vitest's `skipIf(true)` and `runIf(false)` always skip, so they need a ticket, like `{ skip: true }`
27
+ and Playwright's `test.skip(true)`. `skipIf(false)` and `runIf(true)` never skip, so a `// SKIP:`
28
+ marker above one is reported as left over. Any other condition is still a conditional skip. Before,
29
+ a literal condition was treated like any other.
30
+
31
+ ### Removed
32
+
33
+ - **Breaking:** `configs.recommended` is gone. It checked every file as Playwright tests, so pick the
34
+ config for your framework instead: `configs.playwright`, `configs.jest` or `configs.vitest`. For
35
+ Playwright, replace `configs.recommended` with `configs.playwright`, and
36
+ `extends: ['test-governance/recommended']` with `extends: ['test-governance/playwright']`. The
37
+ rules and their severities are the same. A config that still reads `configs.recommended` fails when
38
+ it loads, with a message that points to the upgrade guide, rather than spreading nothing and turning
39
+ the rules off.
40
+
41
+ ### Fixed
42
+
43
+ - Tags kept in a `const` or an enum in the same file count, as in `{ tag: TAGS.NEW }` after
44
+ `const TAGS = { NEW: '@new' } as const`, with spreads, `as const` and `satisfies`, and so do titles
45
+ and details objects kept in a constant. Before, a test tagged this way needed no ticket, and a
46
+ correct `// NEW:` marker above it was reported as left over.
47
+ - Tags and details imported from another module can't be read, so a marker for a tag state above such
48
+ a test is no longer reported as left over, and neither is a Vitest state marker such as `// SKIP:`
49
+ above a test whose options object is imported. With `reportUnreadableTags: true`, the unreadable
50
+ tags are reported, like titles that aren't static text.
51
+ - Vitest tag names, which are written without the `@` (`{ tags: ['flaky'] }`), match the states:
52
+ `'new'` is `@new`, and a custom state with `when: '@flaky'` matches `'flaky'`. Before, they never
53
+ matched. `no-conflicting-states` shows and fixes them without the `@`.
54
+ - `context.skip()` in Vitest's `beforeEach`, global or imported, needs a ticket, like the one in
55
+ `test.beforeEach`. Before, it wasn't checked.
56
+ - A marker in the wrong case with a ticket, such as `// skip: TRADE-1`, is reported by
57
+ `no-orphaned-marker` once the test is no longer skipped, and by `marker-matches-state` above a test in
58
+ another state. Before, it was only reported while the test was still in that state.
59
+
60
+ ## [0.4.0] - 2026-10-08
61
+
62
+ ### Added
63
+
64
+ - Jest and Vitest support. `configs.jest` and `configs.vitest`, or `configure({ framework })`, check
65
+ those frameworks' tests with the same rules and markers: `test.skip`, `it.skip`, `describe.skip`,
66
+ Jest's `xit`, `xtest` and `xdescribe`, Vitest's `{ skip: true }` and `context.skip()`, and their
67
+ `.each` tables, with globals or imports from `@jest/globals` and `vitest`. Vitest's `skipIf()`,
68
+ `runIf()` and `{ skip: condition }` are conditional skips, so they need no ticket by default.
69
+ Point each config at its framework's files with `files`; Playwright files keep using
70
+ `recommended` or `configs.playwright`.
71
+ - A `todo` state for Jest's and Vitest's `test.todo`, with the `// TODO:` marker, on by default. A
72
+ `// TODO: TRADE-123` above a test that isn't a todo is left alone as a tracked work comment. Jest's
73
+ `test.failing` and Vitest's `test.fails` use the `fail` state, which stays off by default.
74
+ - Skipped steps need a ticket, like skipped tests: `test.step.skip('title', fn)` and `step.skip()`
75
+ on the `TestStepInfo` a step's body receives. The marker goes above the call, above the step or
76
+ above a skipped test around it. A marker above a step that isn't skipped is reported as left over.
77
+ Before, skipped steps were not checked.
78
+
79
+ ### Changed
80
+
81
+ - `#123`, `#1234` and `#12345` are real tickets now, not placeholders, since young repositories
82
+ have issues with small numbers. Add them to `placeholders` to keep rejecting them. `TODO`, `TBD`,
83
+ `XXX-*` and tickets numbered 0 are still rejected.
84
+
85
+ ### Fixed
86
+
87
+ - The type declarations load in TypeScript 5.0 to 5.5. Before, they included a string export name
88
+ that only TypeScript 5.6 and later can parse, so older versions failed on the plugin's types even
89
+ with `skipLibCheck`.
90
+ - Tests inside a node type that a custom parser gives no visitor keys for are now checked. ESLint
91
+ reads such nodes' children from the node itself, and the rules now do the same. Before, everything
92
+ inside such a node was skipped.
93
+ - Test files that ESLint parses as CommonJS, such as `.cjs` files, are now checked. Before, their
94
+ top-level variables were taken for local helpers, so a `test` from `require('@playwright/test')`
95
+ was ignored by every rule.
96
+ - The `jira` preset accepts issue URLs on Jira Server and Data Center sites that run under a path,
97
+ such as `https://issues.example.com/jira/browse/TRADE-123`. Before, only `/browse/...` right
98
+ after the host was accepted.
99
+ - A plain alias of a Playwright test, as in `const orderTest = base`, is now followed like
100
+ `base.extend()` and `mergeTests()` are. Before, tests declared through it were not checked.
101
+ - A quoted key in a destructured `require()` is read like a plain one. Before,
102
+ `const { 'test': test } = require('node:test')` still counted as Playwright's `test`, so its
103
+ skipped tests were reported, and `const { 'test': t } = require('@playwright/test')` was not
104
+ followed.
105
+ - Tests written through the whole Playwright module, as in `import * as pw from '@playwright/test'`
106
+ or `const pw = require('@playwright/test')` followed by `pw.test.skip(...)`, are now checked,
107
+ including `pw.test.extend()` and `pw.mergeTests()`. Before, they were ignored.
108
+ - The `no-conflicting-states` fix for a tag in the wrong case changes only that tag. Before, an
109
+ escape earlier in the title could make it change another one: in `'@New\u002Dbeta @New'` it
110
+ rewrote the start of `@New-beta` and left `@New` as it was. A tag after an escape in its title is
111
+ still reported, without a fix.
112
+ - Block comments with lone CR line breaks, or with the Unicode line and paragraph separators
113
+ (U+2028, U+2029), are read line by line. Before, only LF and CRLF ended a line, so a marker or a
114
+ `TODO` after the first line of such a comment was missed.
115
+ - A marker with only punctuation after the colon, such as `// SKIP: .`, is reported as missing its
116
+ ticket. Before, the message was `'' is not a valid ticket`.
117
+ - A regex passed where an option expects a string is shown as written in the error, as in
118
+ `ticket.pattern must be a string (got /TRADE-\d+/)`. Before, it was shown as `{}`.
119
+
9
120
  ## [0.3.1] - 2026-10-06
10
121
 
11
122
  ### Fixed
@@ -134,7 +245,9 @@ First release.
134
245
  - Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
135
246
  fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
136
247
 
137
- [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.1...HEAD
248
+ [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.5.0...HEAD
249
+ [0.5.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.4.0...v0.5.0
250
+ [0.4.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.1...v0.4.0
138
251
  [0.3.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.3.0...v0.3.1
139
252
  [0.3.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.2.1...v0.3.0
140
253
  [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,17 @@
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:
9
+
10
+ <!-- example: valid -->
5
11
 
6
12
  ```ts
7
- // SKIP: WEB-123
8
- test.skip('pays with PayPal', async ({ page }) => {
13
+ // SKIP: TRADE-123
14
+ test.skip('rejects a margin order above the buying power', async ({ page }) => {
9
15
  // ...
10
16
  });
11
17
  ```
@@ -14,7 +20,8 @@ Without the comment, `test-governance/require-ticket` fails the lint. When the t
14
20
  skip removed, `no-orphaned-marker` flags the leftover comment. The markers are plain
15
21
  comments, so `grep -rn "SKIP:" tests/` lists every disabled test and its ticket.
16
22
 
17
- > **Status:** early release (0.x). Options may still change before 1.0.
23
+ > **Status:** early release (0.x). Options may still change before 1.0. Coming from 0.4?
24
+ > [Upgrading to 0.5](docs/upgrading.md#upgrading-to-05) shows what to change.
18
25
 
19
26
  ## Install
20
27
 
@@ -27,6 +34,9 @@ Requires ESLint 9 or 10 with flat config, and Node.js 22.12 or later. For TypeSc
27
34
 
28
35
  ## Usage
29
36
 
37
+ Pick the config for the framework your tests use, `configs.playwright`, `configs.jest` or
38
+ `configs.vitest`, and point it at those test files with `files`:
39
+
30
40
  ```js
31
41
  // eslint.config.js
32
42
  import tsParser from '@typescript-eslint/parser';
@@ -36,26 +46,50 @@ export default [
36
46
  { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
37
47
  {
38
48
  files: ['tests/**/*.{js,ts}'],
39
- ...testGovernance.configs.recommended,
49
+ ...testGovernance.configs.playwright,
40
50
  },
41
51
  ];
42
52
  ```
43
53
 
54
+ There is no default config. Each framework declares, skips and tags tests its own way, so the rules
55
+ need to know which one a file uses. In a Jest or Vitest config, `test`, `it` and `describe` belong to
56
+ that framework, whether they are globals or imported from `@jest/globals` or `vitest`, the way the
57
+ Jest and Vitest ESLint plugins are set up.
58
+
59
+ With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
60
+
61
+ ```js
62
+ // eslint.config.js
63
+ import { defineConfig } from 'eslint/config';
64
+ import tsParser from '@typescript-eslint/parser';
65
+ import testGovernance from 'eslint-plugin-test-governance';
66
+
67
+ export default defineConfig([
68
+ { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
69
+ {
70
+ files: ['tests/**/*.{js,ts}'],
71
+ extends: [testGovernance.configs.playwright],
72
+ },
73
+ ]);
74
+ ```
75
+
44
76
  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.
77
+ `eslint.config.cjs` returns the plugin itself. Inside `defineConfig()`, once the plugin is registered
78
+ under `plugins`, the string form `extends: ['test-governance/playwright']` works too. ESLint loads
79
+ an `eslint.config.ts` through [`jiti`](https://github.com/unjs/jiti), so install it as a dev
80
+ dependency too.
48
81
 
49
- To change the defaults, use `configure()`. It returns the same recommended config with your options
50
- stored in `settings['test-governance']`, which all the rules read:
82
+ To change the defaults, use `configure()`. It takes the framework and your options, and returns that
83
+ framework's config with the options stored in `settings['test-governance']`, which all the rules read:
51
84
 
52
85
  ```js
53
86
  export default [
54
87
  {
55
88
  files: ['tests/**/*.{js,ts}'],
56
89
  ...testGovernance.configure({
90
+ framework: 'playwright',
57
91
  // Only accept Jira keys from these projects (or Jira URLs).
58
- ticket: { preset: 'jira', projects: ['WEB', 'QA'] },
92
+ ticket: { preset: 'jira', projects: ['TRADE', 'RISK'] },
59
93
  // Turn on the @new and @unstable states.
60
94
  lifecycleTags: true,
61
95
  // Any tag your team uses, with the same rules as the built-ins.
@@ -76,84 +110,129 @@ eslint-plugin-test-governance: invalid options:
76
110
  - ticket.preset must be one of "any", "jira", "github", … (got "jria")
77
111
  ```
78
112
 
113
+ ### Frameworks
114
+
115
+ A project with tests in more than one framework gives each framework its own config, pointed at its
116
+ files. To share options between them, pass the same options to each `configure()`:
117
+
118
+ ```js
119
+ const options = { ticket: { preset: 'jira', projects: ['TRADE'] } };
120
+
121
+ export default [
122
+ // The e2e specs run on Playwright, the unit tests on Vitest.
123
+ { files: ['e2e/**/*.spec.ts'], ...testGovernance.configure({ framework: 'playwright', ...options }) },
124
+ { files: ['src/**/*.test.ts'], ...testGovernance.configure({ framework: 'vitest', ...options }) },
125
+ ];
126
+ ```
127
+
128
+ `configs.playwright`, `configs.jest` and `configs.vitest` set only the framework, so one listed after a
129
+ `configure()` for the same files keeps that config's options and changes only the framework. All three
130
+ turn on the same rules. What puts a test in a state in each framework:
131
+
132
+ | State (marker) | Playwright | Jest | Vitest |
133
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
134
+ | `skip` (`// SKIP:`) | `test.skip(...)`, `test.describe.skip`, `test.skip()` in a body, `testInfo.skip()`, `test.step.skip`, `step.skip()` | `test.skip`, `it.skip`, `describe.skip`, `xit`, `xtest`, `xdescribe` | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `skipIf(true)`, `runIf(false)`, `{ skip: true }`, `context.skip()` or `skip()` in a body or in `beforeEach` |
135
+ | `todo` (`// TODO:`) | | `test.todo`, `it.todo` | `test.todo`, `describe.todo`, `{ todo: true }` |
136
+ | `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | |
137
+ | `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` |
138
+ | `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | |
139
+ | conditional skip, no ticket by default | `test.skip(condition)`, `if (...) test.skip()` | | `skipIf(condition)`, `runIf(condition)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()` |
140
+ | tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }`, where `'new'` and `'@new'` both mean `@new` |
141
+
142
+ Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
143
+ A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
144
+ `runIf(true)` and `{ skip: false }` never do.
145
+ Vitest writes tag names without the `@`, as in `{ tags: ['flaky'] }`, so a name in `tags` counts as the
146
+ tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when: '@flaky'` matches it.
147
+
79
148
  ## Rules
80
149
 
81
150
  <!-- begin auto-generated rules list -->
82
151
 
83
152
  💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
84
153
  ⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
85
- ✅ Set in the `recommended` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
154
+ 🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
155
+ 🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
156
+ ⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
86
157
  🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
87
158
  💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
88
159
 
89
- | Name                       | Description | 💼 | ⚠️ | 🔧 | 💡 |
90
- | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
91
- | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | ✅ | | | 💡 |
92
- | [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | ✅ | 🔧 | |
93
- | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | ✅ | | | 💡 |
94
- | [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | ✅ | | | 💡 |
95
- | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | ✅ | | | 💡 |
160
+ | Name                       | Description | 💼 | ⚠️ | 🔧 | 💡 |
161
+ | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------- | :------- | :-- | :-- |
162
+ | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🃏 🎭 ⚡ | | | 💡 |
163
+ | [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🃏 🎭 ⚡ | 🔧 | |
164
+ | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🃏 🎭 ⚡ | | | 💡 |
165
+ | [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🃏 🎭 ⚡ | | | 💡 |
166
+ | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🃏 🎭 ⚡ | | | 💡 |
96
167
 
97
168
  <!-- end auto-generated rules list -->
98
169
 
99
170
  ## Defaults
100
171
 
101
- The defaults follow how most teams already work, so `recommended` is useful with no options.
102
-
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. |
172
+ The defaults follow how most teams already work, so each framework config is useful with no options.
173
+
174
+ | Setting | Default | Why |
175
+ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
176
+ | States that need a ticket | `skip`, `fixme`; in Jest and Vitest `skip`, `todo` | Every team skips tests. Chromium DevTools, GitLab and Slack all require a bug link on disabled tests. |
177
+ | `@new`, `@unstable` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that call it `@flaky` or `@quarantine` use `customStates`. |
178
+ | Conditional skips (`test.skip(browserName === 'webkit', ...)`, `if (...) test.skip()`, a skip in `catch`) | no ticket needed | These are usually permanent platform limits, not bugs. `requireTicketForConditional: true` changes this. |
179
+ | `test.fail`, `test.slow`, Jest's `test.failing`, Vitest's `test.fails` | off | They don't remove coverage. Turn them on with `states: { fail: true, slow: true }`. |
180
+ | Marker format | `// SKIP: TRADE-123` or `// SKIP: TRADE-123, TRADE-124` | Uppercase keyword and colon, like `TODO:`. Easy to grep. |
181
+ | 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. |
182
+ | `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. |
183
+ | Placement | comment block directly above; other comments allowed; a blank line breaks it | Survives `eslint-disable-next-line` and Prettier. |
184
+ | 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. |
185
+ | 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. |
186
+ | Describe blocks | a marker on a skipped or tagged describe covers its tests | One ticket per skipped suite. |
187
+ | 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
188
 
118
189
  ## Options
119
190
 
120
191
  All options go in `configure({...})`, or in `settings['test-governance']` if you build the config
121
- yourself. The rules have no options of their own: they all read these shared settings, so they
122
- always agree on ticket formats, states and markers. To turn a rule off, set it to `'off'` as usual.
123
-
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. |
192
+ yourself. `framework` is required, and every other option has a default. The rules have no options
193
+ of their own: they all read these shared settings, so they always agree on ticket formats, states
194
+ and markers. To turn a rule off, set it to `'off'` as usual.
195
+
196
+ | Option | Type | Default | Description |
197
+ | ----------------------------- | -------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
+ | `framework` | `'playwright' \| 'jest' \| 'vitest'` | required | The test framework of the files the config applies to. `configs.playwright`, `configs.jest` and `configs.vitest` set it, and `configure()` needs it. See [Frameworks](#frameworks). |
199
+ | `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
200
+ | `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive; a leading `#` is ignored. Your list replaces the default one, so repeat the defaults you want to keep. Tickets numbered 0 are always rejected. |
201
+ | `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `unstable` (`@unstable`) states. |
202
+ | `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. |
203
+ | `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. |
204
+ | `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`. |
205
+ | `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
206
+ | `allowNotes` | `boolean` | `false` | Allow free text after the tickets, e.g. `// SKIP: TRADE-123 flaky on CI`. Applies to markers and to `FIXME` / `TODO` comments. |
207
+ | `comments` | `false \| { keywords?: string[] }` | `{ keywords: ['FIXME', 'TODO'] }` | Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: TRADE-123`. Your keywords replace the default ones. `false` allows free-form comments. |
208
+ | `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`. Your list replaces the default one, so include those names if you still need them. See below. |
209
+ | `reportUnreadableTags` | `boolean` | `false` | When a tag state is on, report titles that aren't static text, and tags or details that can't be read in the file, such as ones imported from another module, since their tags can't be checked. |
137
210
 
138
211
  ### Ticket presets
139
212
 
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` |
213
+ | Preset | Accepts | Options |
214
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
215
+ | `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL | |
216
+ | `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) |
217
+ | `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`) | `host` for GitHub Enterprise (`github.com` by default) |
218
+ | `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123` | `host` for self-managed (`gitlab.com` by default) |
219
+ | `linear` | `RISK-123`, `https://linear.app/<workspace>/issue/RISK-123/...` | `teams` |
220
+ | `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) |
221
+ | `numeric` | `4821` | `minLength` (1), `maxLength` (20) |
222
+ | `pattern` | anything matching your regex, anchored at both ends | `pattern`, `flags` |
223
+
224
+ `projects` (Jira) and `teams` (Linear) are keys as they appear in tickets, in uppercase: `TRADE` for
225
+ `TRADE-123`. Each preset takes only its own options, so `host` on the `any` preset fails with a clear
226
+ message instead of being ignored. A `pattern` always matches the whole ticket, so the `g` and `y`
227
+ flags make no difference and are ignored.
150
228
 
151
229
  Presets combine, and each state can have its own format. For example, bugs in Jira and flaky-test
152
230
  reports in GitHub:
153
231
 
154
232
  ```js
155
233
  testGovernance.configure({
156
- ticket: { preset: 'jira', projects: ['WEB'] },
234
+ framework: 'playwright',
235
+ ticket: { preset: 'jira', projects: ['TRADE'] },
157
236
  lifecycleTags: true,
158
237
  states: { unstable: { ticket: { preset: 'github' } } },
159
238
  });
@@ -164,43 +243,83 @@ so keep it free of nested quantifiers.
164
243
 
165
244
  ### Which functions count as `test`
166
245
 
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')`.
246
+ In Playwright files:
247
+
248
+ - `test` imported from `@playwright/test`, `playwright/test` or a component-testing package such as
249
+ `@playwright/experimental-ct-react`, including the default import, aliases such as
250
+ `import { test as it }` or `const it = test`, and `const { test } = require('@playwright/test')`.
251
+ - `pw.test` on the whole module, after `import * as pw from '@playwright/test'` or
252
+ `const pw = require('@playwright/test')`.
170
253
  - Fixtures made with `.extend()` or `mergeTests()` in the same file, such as
171
254
  `const test = base.extend({...})`.
172
255
  - Any name in `testFunctions`, for fixtures imported from your own modules. The default `['test']`
173
256
  covers the common `import { test } from './fixtures'`.
174
257
 
175
- A name imported or required from another test runner (`vitest`, `@jest/globals`, `node:test`,
176
- `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
177
- `const test = require('ava')`, is never treated as Playwright's `test`, and neither is a local
258
+ In Jest and Vitest files:
259
+
260
+ - The globals `test`, `it` and `describe`, Jest's `xit`, `xtest`, `fit`, `xdescribe` and
261
+ `fdescribe`, and Vitest's `suite`, and the same names imported or required from `@jest/globals` or
262
+ `vitest`, including aliases and `import * as vi from 'vitest'`.
263
+ - Vitest fixtures made with `test.extend()` in the same file.
264
+ - Any name in `testFunctions`, which defaults to `['test', 'it']`.
265
+
266
+ A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
267
+ `node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
268
+ `const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
178
269
  variable declared inside a function that happens to be called `test`.
179
270
 
180
271
  ### Syntax the rules understand
181
272
 
182
- - `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, and the same with a details object.
273
+ The [Frameworks](#frameworks) table lists what each framework's tests can do. In Playwright:
274
+
275
+ - `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, with or without a details object,
276
+ and with a body defined elsewhere, as in `test.skip('places a limit order', placeLimitOrder)`.
183
277
  - `test.describe.skip(...)` and `test.describe.fixme(...)`, including `serial` and `parallel` variants.
184
278
  The state covers every test inside.
185
- - `test.skip()`, `test.skip(true)`, `test.fixme()` inside a test or hook; `testInfo.skip()`,
186
- `test.info().skip()`.
279
+ - Runtime calls inside a test, a describe or a hook such as `test.beforeEach`: `test.skip()`,
280
+ `test.skip(true)`, `test.fixme()`, `test.fail()`, `test.slow()`, the same methods on `testInfo`,
281
+ and `test.info().skip()`.
282
+ - Skipped steps: `test.step.skip('title', fn)`, and `step.skip()` on the `TestStepInfo` a step's body
283
+ receives. Only the step is skipped, so a marker above the step or above a skipped test around it
284
+ covers it.
187
285
  - Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
188
- from enclosing describes.
286
+ from enclosing describes. Tags, titles and details can also come from a `const` or an enum declared
287
+ in the same file, as in `{ tag: TAGS.NEW }` after `const TAGS = { NEW: '@new' } as const`, including
288
+ arrays and spreads of them.
189
289
 
190
- Not covered in this version: tests generated in loops with dynamic titles, fixtures imported from
191
- other modules under a name not in `testFunctions`, and Playwright annotations as markers.
290
+ Not covered in this version: tests generated in loops with dynamic titles, and tags or details imported
291
+ from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
292
+ `testFunctions`, Playwright annotations as markers, and other runners such as Mocha, Bun and `node:test`.
192
293
 
193
- ## Development
294
+ ## TypeScript
194
295
 
195
- ```sh
196
- npm install
197
- npm test # rule tests under espree and @typescript-eslint/parser
198
- npm run test:coverage # fails below 100% coverage of src/rules and src/utils
199
- npm run typecheck
200
- npm run test:pack # pack the tarball and lint tests/fixtures/sample-project with it
201
- npm run docs # regenerate the rules list and rule doc headers
296
+ The package ships its own type declarations, which work with TypeScript 5.0 or later. Besides the
297
+ plugin, it exports these types: `GovernanceOptions` (the options of `configure()`), `TicketSpec`,
298
+ `CustomState`, `StateOverride`, `FrameworkName`, `RuleName`, `ConfigName`, `FlatConfig` and
299
+ `TestGovernancePlugin`.
300
+
301
+ ```ts
302
+ // eslint.config.ts
303
+ import tsParser from '@typescript-eslint/parser';
304
+ import testGovernance, { type GovernanceOptions } from 'eslint-plugin-test-governance';
305
+
306
+ const options: GovernanceOptions = {
307
+ framework: 'playwright',
308
+ lifecycleTags: true,
309
+ ticket: { preset: 'jira', projects: ['TRADE'] },
310
+ };
311
+
312
+ export default [
313
+ { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
314
+ { files: ['tests/**/*.ts'], ...testGovernance.configure(options) },
315
+ ];
202
316
  ```
203
317
 
318
+ ## Contributing
319
+
320
+ Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) explains the setup and
321
+ the checks every pull request runs, and [CHANGELOG.md](CHANGELOG.md) lists the changes in each release.
322
+
204
323
  ## License
205
324
 
206
325
  [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
+ * One config per framework. `playwright`, `jest` and `vitest` turn on the same rules for that framework's
17
+ * tests; point each one at its test files with `files`.
18
+ */
19
+ export type ConfigName = FrameworkName;
13
20
  export interface TestGovernancePlugin extends ESLint.Plugin {
14
21
  meta: {
15
22
  name: string;
@@ -17,20 +24,14 @@ 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
- };
23
- configure(options?: GovernanceOptions): FlatConfig;
27
+ configs: Record<ConfigName, FlatConfig>;
28
+ configure(options: GovernanceOptions): FlatConfig;
24
29
  }
25
30
  declare const plugin: TestGovernancePlugin;
26
31
  /**
27
- * Returns the recommended config with shared options in `settings['test-governance']`,
32
+ * Returns the config of `options.framework` with shared options in `settings['test-governance']`,
28
33
  * so all rules read the same ticket formats and states.
29
34
  */
30
- export declare function configure(options?: GovernanceOptions): FlatConfig;
31
- export declare const configs: {
32
- recommended: FlatConfig;
33
- };
35
+ export declare function configure(options: GovernanceOptions): FlatConfig;
36
+ export declare const configs: Record<"playwright" | "jest" | "vitest", FlatConfig>;
34
37
  export default plugin;
35
- export { plugin as 'module.exports' };
36
- //# sourceMappingURL=index.d.ts.map