eslint-plugin-test-governance 0.13.2 → 0.14.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 (38) hide show
  1. package/CHANGELOG.md +90 -1
  2. package/README.md +118 -445
  3. package/dist/index.d.ts +3 -3
  4. package/dist/index.js +11 -3
  5. package/dist/rules/{no-conflicting-states.d.ts → consistent-tag-names.d.ts} +1 -1
  6. package/dist/rules/{no-conflicting-states.js → consistent-tag-names.js} +2 -29
  7. package/dist/rules/limit-retries.d.ts +2 -0
  8. package/dist/rules/limit-retries.js +28 -0
  9. package/dist/rules/marker-matches-state.js +4 -1
  10. package/dist/rules/require-ticket-for-commented-out-tests.js +4 -4
  11. package/dist/rules/require-ticket.js +5 -5
  12. package/dist/utils/analyze.d.ts +5 -5
  13. package/dist/utils/analyze.js +514 -205
  14. package/dist/utils/constants.d.ts +1 -4
  15. package/dist/utils/constants.js +1 -3
  16. package/dist/utils/create-rule.d.ts +1 -1
  17. package/dist/utils/fix.js +7 -6
  18. package/dist/utils/frameworks.d.ts +2 -0
  19. package/dist/utils/frameworks.js +7 -6
  20. package/dist/utils/options.d.ts +7 -10
  21. package/dist/utils/options.js +3 -6
  22. package/dist/utils/schema.js +12 -11
  23. package/dist/utils/tickets.js +10 -5
  24. package/dist/utils/validate.d.ts +2 -0
  25. package/dist/utils/validate.js +3 -0
  26. package/docs/configuration.md +400 -0
  27. package/docs/frameworks.md +228 -0
  28. package/docs/rules/consistent-tag-names.md +79 -0
  29. package/docs/rules/limit-retries.md +94 -0
  30. package/docs/rules/marker-matches-state.md +28 -11
  31. package/docs/rules/no-orphaned-marker.md +33 -14
  32. package/docs/rules/require-ticket-for-commented-out-tests.md +30 -12
  33. package/docs/rules/require-ticket-in-comments.md +19 -17
  34. package/docs/rules/require-ticket.md +130 -41
  35. package/docs/troubleshooting.md +77 -0
  36. package/docs/upgrading.md +103 -31
  37. package/package.json +1 -1
  38. package/docs/rules/no-conflicting-states.md +0 -85
package/README.md CHANGED
@@ -4,513 +4,186 @@
4
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
5
  [![License: MIT](https://img.shields.io/npm/l/eslint-plugin-test-governance)](LICENSE)
6
6
 
7
- ESLint rules that keep a Playwright, Jest, Vitest, Mocha, Cypress, WebdriverIO, `node:test` or Jasmine suite honest. Every skipped, fixme, todo, `@new`
8
- or `@quarantined` test must point at the ticket that tracks it:
7
+ Require tracking tickets for disabled tests and TODO/FIXME comments with ESLint. Supports Playwright,
8
+ Jest, Vitest, Mocha, Cypress, WebdriverIO, `node:test` and Jasmine. Custom tag policies and retry tracking
9
+ are optional.
9
10
 
10
11
  <!-- example: valid -->
11
12
 
12
- ```ts
13
+ ```js
13
14
  // SKIP: TRADE-123
14
- test.skip('rejects a margin order above the buying power', async ({ page }) => {
15
- // ...
16
- });
15
+ test.skip('cancels an open order', () => {});
17
16
  ```
18
17
 
19
- Without the comment, `test-governance/require-ticket` fails the lint. When the test is fixed and the
20
- skip removed, `no-orphaned-marker` flags the leftover comment. The markers are plain
21
- comments, so `grep -rn "SKIP:" tests/` lists every disabled test and its ticket.
18
+ Removing the skip leaves a stale marker, which the plugin reports too. Tickets can also come from
19
+ [Playwright annotations or Vitest metadata](docs/configuration.md#tickets-from-annotations).
20
+ Ticket formats are checked locally; the plugin does not verify that an issue exists or is open.
22
21
 
23
- The same check covers tests commented out by hand, `FIXME` and `TODO` comments in test files, and, once
24
- turned on, retried tests. The ticket can also go in the description of an `eslint-disable` comment, or in
25
- a Playwright annotation or Vitest `meta`, which reach the test report.
22
+ **Start here:** [Install](#install) · [First lint run](#usage) · [Choose a framework](#frameworks) ·
23
+ [Rules](#rules) · [Configuration](docs/configuration.md) · [Troubleshooting](docs/troubleshooting.md) ·
24
+ [Upgrading](docs/upgrading.md)
26
25
 
27
- > **Status:** early release (0.x). Options may still change before 1.0. Coming from 0.5?
28
- > [Upgrading to 0.6](docs/upgrading.md#upgrading-to-06) shows what to change.
26
+ > Early release (0.x): options may change before 1.0. Check the [upgrade guide](docs/upgrading.md)
27
+ > when updating an existing configuration.
29
28
 
30
29
  ## Install
31
30
 
32
- ```sh
33
- npm install --save-dev eslint-plugin-test-governance
34
- ```
31
+ Requires ESLint 9 or 10 with flat config. The plugin requires Node.js 22.12 or later; ESLint may
32
+ require a newer version:
35
33
 
36
- Requires ESLint 9 or 10 with flat config, and Node.js 22.12 or later. For TypeScript specs, also install
37
- `@typescript-eslint/parser` (or `typescript-eslint`).
34
+ | ESLint | Node.js requirement for this plugin and ESLint together |
35
+ | ------ | ------------------------------------------------------- |
36
+ | 9 | 22.12 or later |
37
+ | 10 | 22.13 or later within Node 22, or Node 24 and later |
38
38
 
39
- ## Usage
39
+ See [ESLint's prerequisites](https://eslint.org/docs/latest/use/getting-started#prerequisites).
40
+ Use a project that already has a supported test runner. Install the linter and plugin:
40
41
 
41
- Pick the config for the framework your tests use, `configs.playwright`, `configs.jest`,
42
- `configs.vitest`, `configs.mocha`, `configs.cypress`, `configs.webdriverio`, `configs['node-test']` for
43
- Node's built-in runner or `configs.jasmine`, and point it at those test files with `files`:
42
+ ```sh
43
+ npm install --save-dev eslint eslint-plugin-test-governance
44
+ ```
44
45
 
45
- ```js
46
- // eslint.config.js
47
- import tsParser from '@typescript-eslint/parser';
48
- import testGovernance from 'eslint-plugin-test-governance';
46
+ For TypeScript tests, follow the [TypeScript setup](docs/configuration.md#typescript), which also
47
+ installs and configures a parser. The JavaScript quick start below needs no TypeScript parser.
49
48
 
50
- export default [
51
- { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
52
- {
53
- files: ['tests/**/*.{js,ts}'],
54
- ...testGovernance.configs.playwright,
55
- },
56
- ];
57
- ```
49
+ ## Usage
58
50
 
59
- There is no default config. Each framework declares, skips and tags tests its own way, so the rules
60
- need to know which one a file uses. In a Jest, Vitest, Mocha or Jasmine config, the framework's functions,
61
- such as `it` and `describe`, belong to it, whether they are globals or imported from `@jest/globals`,
62
- `vitest`, `mocha` or `jasmine-core`, the way the Jest, Vitest and Mocha ESLint plugins are set up. `node:test` has no globals, so in its config
63
- they are the names imported or required from `node:test`, and `test` and `it` from `testFunctions`.
51
+ This example uses Playwright and JavaScript tests in `tests/`. Choose your runner in the
52
+ [framework table](#frameworks) and adjust `files` to match your test locations and extensions.
53
+ If you already have an ESLint config, add this object to its array rather than replacing the file.
64
54
 
65
- Cypress and WebdriverIO run their tests on Mocha, so `configs.cypress` and `configs.webdriverio` read the
66
- same calls as `configs.mocha`. `webdriverio` means WebdriverIO's Mocha framework; for its Jasmine
67
- framework, use `configs.jasmine`. Karma runs Jasmine or Mocha tests, so use `configs.jasmine` with
68
- karma-jasmine, which Angular's karma builder sets up, and `configs.mocha` with karma-mocha. Puppeteer has
69
- no test functions of its own, so use the config of the runner its tests run on: `configs.jest` for
70
- jest-puppeteer, or `configs.vitest`, `configs.mocha`, `configs['node-test']` or `configs.jasmine`.
55
+ 1. Create `eslint.config.mjs` at the project root:
71
56
 
72
- With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
57
+ <!-- config: javascript -->
73
58
 
74
59
  ```js
75
- // eslint.config.js
76
- import { defineConfig } from 'eslint/config';
77
- import tsParser from '@typescript-eslint/parser';
60
+ // eslint.config.mjs
78
61
  import testGovernance from 'eslint-plugin-test-governance';
79
62
 
80
- export default defineConfig([
81
- { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
82
- {
83
- files: ['tests/**/*.{js,ts}'],
84
- extends: [testGovernance.configs.playwright],
85
- },
86
- ]);
63
+ export default [{ files: ['tests/**/*.js'], ...testGovernance.configs.playwright }];
87
64
  ```
88
65
 
89
- Any flat config file works. The package is ESM, and `require('eslint-plugin-test-governance')` in an
90
- `eslint.config.cjs` returns the plugin itself. Inside `defineConfig()`, once the plugin is registered
91
- under `plugins`, the string form `extends: ['test-governance/playwright']` works too. ESLint loads
92
- an `eslint.config.ts` through [`jiti`](https://github.com/unjs/jiti), so install it as a dev
93
- dependency too.
66
+ 2. In `tests/order.spec.js`, a skipped test without a ticket is reported:
94
67
 
95
- To change the defaults, use `configure()`. It takes the framework and your options, and returns that
96
- framework's config with the options stored in `settings['test-governance']`, which all the rules read:
68
+ <!-- example: invalid -->
97
69
 
98
70
  ```js
99
- export default [
100
- {
101
- files: ['tests/**/*.{js,ts}'],
102
- ...testGovernance.configure({
103
- framework: 'playwright',
104
- // Only accept Jira keys from these projects (or Jira URLs).
105
- ticket: { preset: 'jira', projects: ['TRADE', 'RISK'] },
106
- // Turn on the @new and @quarantined states.
107
- lifecycleTags: true,
108
- // Any tag your team uses, with the same rules as the built-ins.
109
- customStates: {
110
- 'needs-data': { when: '@needs-data', marker: 'NEEDS-DATA' },
111
- },
112
- }),
113
- },
114
- ];
71
+ import { test } from '@playwright/test';
72
+
73
+ test.skip('cancels an open order', () => {});
115
74
  ```
116
75
 
117
- `configure()` validates the options when the config file loads, so a mistake fails immediately
118
- with a clear message rather than being ignored:
76
+ 3. Run ESLint from the project root:
119
77
 
120
- ```text
121
- eslint-plugin-test-governance: invalid options:
122
- - lifecycleTag is not a known option; did you mean "lifecycleTags"?
123
- - ticket.preset must be one of "any", "jira", "github", … (got "jria")
78
+ ```sh
79
+ npx eslint tests/order.spec.js
124
80
  ```
125
81
 
126
- ### Frameworks
82
+ ESLint reports `test-governance/require-ticket` and exits with status 1. Add the marker directly above
83
+ the test, then run the same command again:
127
84
 
128
- A project with tests in more than one framework gives each framework its own config, pointed at its
129
- files. To share options between them, pass the same options to each `configure()`:
85
+ <!-- example: valid -->
130
86
 
131
87
  ```js
132
- const options = { ticket: { preset: 'jira', projects: ['TRADE'] } };
88
+ import { test } from '@playwright/test';
133
89
 
134
- export default [
135
- // The e2e specs run on Playwright, the unit tests on Vitest.
136
- { files: ['e2e/**/*.spec.ts'], ...testGovernance.configure({ framework: 'playwright', ...options }) },
137
- { files: ['src/**/*.test.ts'], ...testGovernance.configure({ framework: 'vitest', ...options }) },
138
- ];
90
+ // SKIP: TRADE-123
91
+ test.skip('cancels an open order', () => {});
139
92
  ```
140
93
 
141
- This matters most for Jasmine next to Jest or Mocha, whose `it`, `xit` and `describe` look the same:
142
- only the `files` of each config tell them apart.
143
-
144
- Each framework's config, such as `configs.playwright`, sets only the framework, so one listed after a
145
- `configure()` for the same files keeps that config's options and changes only the framework. They all
146
- turn on the same rules. What puts a test in a state in each framework:
147
-
148
- | State (marker) | Playwright | Jest | Vitest | Mocha | Cypress | WebdriverIO | node-test | Jasmine |
149
- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
150
- | `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()`, `context.skip('note')` or `skip()` in a body or in `beforeEach` | `it.skip`, `specify.skip`, `describe.skip`, `context.skip`, `xit`, `xspecify`, `xdescribe`, `xcontext`, TDD `test.skip` and `suite.skip`, `this.skip()` in a test or hook | as Mocha | as Mocha | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `skip()`, `{ skip: true }` or `{ skip: 'reason' }`, `t.skip()`, `t.skip('reason')` or `this.skip()` in a test, `beforeEach` or `afterEach` | `xit`, `xdescribe`, `pending()`, `pending('reason')` or `notApplicable('reason')` in a test, `beforeEach` or `afterEach` |
151
- | `todo` (`// TODO:`) | | `test.todo`, `it.todo` | `test.todo`, `describe.todo`, `{ todo: true }` | a test with only a title, `it('title')`, which Mocha runs as pending | as Mocha | as Mocha | `test.todo`, `it.todo`, `describe.todo`, `suite.todo`, `todo()`, `{ todo: true }` or `{ todo: 'reason' }`, `t.todo()`, `this.todo()` | a test with only a title, `it('title')`, which Jasmine reports as pending |
152
- | `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | | | | | | |
153
- | `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` | | | | `test.expectFailure`, `expectFailure()`, `{ expectFailure }` | |
154
- | `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | | | | | | |
155
- | `retry` (`// RETRY:`), off by default | `test.describe.configure({ retries })` | `jest.retryTimes()` | `{ retry }` | `this.retries()` in a describe, test or hook written as a `function` | as Mocha, and `{ retries }` | as Mocha | | |
156
- | conditional skip, no ticket by default | `test.skip(condition)`, `if (...) test.skip()`, `(condition ? test.skip : test)(...)` | `(condition ? it.skip : it)(...)` | `skipIf(condition)`, `runIf(condition)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()`, `(condition ? it.skip : it)(...)` | `if (...) this.skip()`, `(condition ? it.skip : it)(...)` | as Mocha, and `{ browser: '!webkit' }` | as Mocha | `{ skip: condition }`, `{ todo: condition }`, `if (...) t.skip()`, `(condition ? it.skip : it)(...)` | `if (...) pending()`, `(condition ? xit : it)(...)` |
157
- | tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }`, where `'new'` and `'@new'` both mean `@new` | the title | the title, `{ tags }` from @cypress/grep | the title | the title, `{ tags }`, as in Vitest | the title |
158
-
159
- Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
160
- A test or describe whose function a condition picks, as in `(isMobile ? test.skip : test)('title', fn)`,
161
- is a conditional skip in every framework.
162
- A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
163
- `runIf(true)` and `{ skip: false }` never do.
164
- In Vitest, Mocha and `node:test`, `test.skip('title')` and `describe.skip('title')` with no body are skipped too,
165
- as is `xit('title')` in Jasmine.
166
- In Mocha, `this.skip()` counts in a test or hook written as a `function`; an arrow function gets no
167
- Mocha context, so its `this` is not the test's. `this.slow(ms)` only sets Mocha's speed threshold, so
168
- it isn't the `slow` state.
169
- Vitest, and `node:test` since Node 26.2, write tag names without the `@`, as in `{ tags: ['flaky'] }`, so a
170
- name in `tags` counts as the tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with
171
- `when: '@flaky'` matches it.
172
- In `node:test`, an option counts with any value but `false` or `undefined`, as Node reads it, so
173
- `{ skip: 'broker sandbox is down' }` always skips. `t.skip()` and `t.todo()` take a message, not a
174
- condition, and in a test or hook written as a `function`, `this.skip()` and `this.todo()` do the same.
175
- Subtests declared with `t.test('title', options, fn)` are checked like tests, and a test or describe can
176
- leave out its title and start with its options or body, as in `test({ skip: true }, fn)`.
177
- A test with no function, `test('title')`, passes in Node, so it isn't a todo. A describe's body, and
178
- `before` and `after` inside it, get the suite's context, which can't skip. Node reports a test in one
179
- state only, skip before todo before `expectFailure`, so `describe.skip('title', { todo: 'why' }, fn)`
180
- needs only a `// SKIP:` marker.
181
- In Jasmine, `pending()` and Jasmine 7's `notApplicable()` stop the running test, so they count in a test,
182
- `beforeEach` or `afterEach`; in `beforeAll`, `afterAll` or a describe's body Jasmine fails the suite
183
- instead, so they are left alone there. Like `t.skip('reason')`, `pending('reason')` takes a reason, not a
184
- condition. A function or variable of your own called `pending` is not Jasmine's.
94
+ With this config and file, ESLint prints nothing and exits with status 0. Use a real ticket from your
95
+ tracker. The default accepts `PROJ-123`, `#123`, `owner/repo#123` or an http(s) URL; a
96
+ [ticket preset](docs/configuration.md#ticket-presets) narrows the accepted format.
185
97
 
186
- ## Rules
187
-
188
- <!-- begin auto-generated rules list -->
189
-
190
- 💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
191
- ⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
192
- 🌲 Set in the `cypress` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
193
- 🌼 Set in the `jasmine` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
194
- 🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
195
- ☕ Set in the `mocha` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
196
- 🟢 Set in the `node-test` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
197
- 🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
198
- ⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
199
- 🤖 Set in the `webdriverio` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
200
- 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
201
- 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
202
-
203
- | Name                                   | Description | 💼 | ⚠️ | 🔧 | 💡 |
204
- | :--------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :---------------------- | :---------------------- | :-- | :-- |
205
- | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
206
- | [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | 🔧 | |
207
- | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
208
- | [require-ticket](docs/rules/require-ticket.md) | Require a ticket for skipped, fixme, todo, retried and tagged tests | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
209
- | [require-ticket-for-commented-out-tests](docs/rules/require-ticket-for-commented-out-tests.md) | Require a ticket marker on commented-out tests | | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | 💡 |
210
- | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
211
-
212
- <!-- end auto-generated rules list -->
213
-
214
- ## Defaults
215
-
216
- The defaults follow how most teams already work, so each framework config is useful with no options.
217
-
218
- | Setting | Default | Why |
219
- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
220
- | 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. |
221
- | `@new`, `@quarantined` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that say `@flaky` instead [rename the tag](#tag-names). |
222
- | 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` asks for a ticket, `requireReasonForConditional: true` for a reason. |
223
- | `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 }`. |
224
- | 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. |
225
- | Marker format | `// SKIP: TRADE-123` or `// SKIP: TRADE-123, TRADE-124` | Uppercase keyword and colon, like `TODO:`. Easy to grep. |
226
- | 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. |
227
- | `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. |
228
- | Placement | comment block directly above; other comments allowed; a blank line breaks it | Survives `eslint-disable-next-line` and Prettier. |
229
- | 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. |
230
- | 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. |
231
- | Describe blocks | a marker on a skipped or tagged describe covers its tests | One ticket per skipped suite. |
232
- | 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. |
233
-
234
- ## Options
98
+ ### Frameworks
235
99
 
236
- All options go in `configure({...})`, or in `settings['test-governance']` if you build the config
237
- yourself. `framework` is required, and every other option has a default. The rules have no options
238
- of their own: they all read these shared settings, so they always agree on ticket formats, states
239
- and markers. To turn a rule off, set it to `'off'` as usual.
240
-
241
- | Option | Type | Default | Description |
242
- | ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
243
- | `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). |
244
- | `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
245
- | `placeholders` | `string[]` | see above | Tickets rejected as placeholders. `*` matches any characters. Case-insensitive, and a leading `#` on the ticket or the placeholder is ignored. Your list replaces the default one, so repeat the defaults you want to keep. Tickets numbered 0 are always rejected. |
246
- | `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
247
- | `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, Vitest and node:test, and `retry` in every framework but node:test and Jasmine, so turning one on elsewhere has no effect. |
248
- | `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. |
249
- | `knownTags` | `string[]` | `[]` | Your team's other tags, such as `['@smoke', '@regression']`, written with the `@`. `no-conflicting-states` reports them in the wrong case or misspelled, as it does state tags, and never reports a listed tag as a typo of a state tag. |
250
- | `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`. |
251
- | `requireReasonForConditional` | `boolean` | `false` | Require a reason, but no ticket, for conditional skips: a comment above the skip or at the end of its line, above the `if` around it or above its hook, step or test, or the reason the call takes, as `test.skip(condition, 'why')`. See [require-ticket](docs/rules/require-ticket.md#reasons-for-conditional-skips). |
252
- | `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
253
- | `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. |
254
- | `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. |
255
- | `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). |
256
- | `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest, Vitest and node:test 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. |
257
- | `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. |
258
-
259
- ### Tag names
260
-
261
- Teams name the stages of a test differently, so `states` can rename the tag of `new` and
262
- `quarantined`. The marker keeps its name unless you rename it too:
100
+ Each config sets the same rule defaults, adapted to the runner. There is no `recommended` or default config.
101
+ See [Framework syntax](docs/frameworks.md) for imports, supported calls and version-specific features.
263
102
 
264
- ```js
265
- testGovernance.configure({
266
- framework: 'playwright',
267
- lifecycleTags: true,
268
- states: { quarantined: { tag: '@flaky', marker: 'FLAKY' } },
269
- });
270
- ```
103
+ | Runner | Config |
104
+ | ------------------------------------------- | ---------------------- |
105
+ | Playwright | `configs.playwright` |
106
+ | Jest | `configs.jest` |
107
+ | Vitest | `configs.vitest` |
108
+ | Mocha | `configs.mocha` |
109
+ | Cypress | `configs.cypress` |
110
+ | WebdriverIO with Mocha | `configs.webdriverio` |
111
+ | Node's built-in runner | `configs['node-test']` |
112
+ | Jasmine, including WebdriverIO with Jasmine | `configs.jasmine` |
271
113
 
272
- <!-- example: valid settings={"lifecycleTags":true,"states":{"quarantined":{"tag":"@flaky","marker":"FLAKY"}}} -->
114
+ For mixed suites, give each framework its own `files` pattern. See
115
+ [multiple frameworks](docs/configuration.md#multiple-frameworks).
273
116
 
274
- ```ts
275
- // FLAKY: TRADE-311
276
- test('streams level 2 quotes @flaky', async ({ page }) => {
277
- // ...
278
- });
279
- ```
280
-
281
- The new tag replaces the default one, so `@quarantined` no longer puts a test in the state, and the
282
- rules check `@flaky` as they checked `@quarantined`: `@new` together with `@flaky` is reported, and so
283
- is a near-miss such as `@flakey`. For a tag that isn't one of these stages, such as `@needs-data`, add
284
- a `customStates` entry.
285
-
286
- ### Ticket presets
287
-
288
- | Preset | Accepts | Options |
289
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
290
- | `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL | |
291
- | `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) |
292
- | `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`) | `host` for GitHub Enterprise (`github.com` by default) |
293
- | `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123` | `host` for self-managed (`gitlab.com` by default) |
294
- | `linear` | `RISK-123`, `https://linear.app/<workspace>/issue/RISK-123/...` | `teams` |
295
- | `azure-devops` | `AB#123`, `#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123` | `host` for Azure DevOps Server (`dev.azure.com` and `*.visualstudio.com` by default) |
296
- | `trello` | card links, `https://trello.com/c/nqPiDKmw` with or without the `/9-title` after it | |
297
- | `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) |
298
- | `asana` | task links, `https://app.asana.com/0/<project>/<task>` and the newer `https://app.asana.com/1/<workspace>/.../task/<task>` | |
299
- | `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 |
300
- | `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 |
301
- | `monday` | item links, `https://<account>.monday.com/boards/<board>/pulses/<item>`, and custom keys such as `TIX-001` | `prefixes` for custom keys |
302
- | `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) |
303
- | `basecamp` | to-do and card links, `https://3.basecamp.com/<account>/buckets/<project>/todos/<id>` or `/card_tables/cards/<id>` | |
304
- | `shortcut` | `sc-123`, `[sc-123]`, `https://app.shortcut.com/<workspace>/story/123/...` | |
305
- | `openproject` | `#4821` and per-project IDs such as `AGILE-1`; with `host`, `https://<host>/projects/<identifier>/work_packages/AGILE-1` (also `/work_packages/4821` and the short `/wp/4821`) | `host`, needed for links |
306
- | `bugzilla` | bug numbers `4821`; with `host`, `https://<host>/show_bug.cgi?id=4821`, also under a path as in `/bugzilla/show_bug.cgi` | `host`, needed for links |
307
- | `phabricator` | task monograms `T4821`; with `host`, `https://<host>/T4821`. Phorge, Phabricator's maintained fork, shares the format | `host`, needed for links |
308
- | `rally` | formatted IDs `US4821`, `DE4821`, `TA4821`, `TC4821`, never zero-padded (so MITRE ATT&CK's `TA0006` isn't one) | |
309
- | `servicenow` | record numbers `STRY0010001` for the prefixes you list, any digit count | `prefixes`, required: record-number prefixes such as `STRY` |
310
- | `plane` | `WEB-142`; `https://app.plane.so/<workspace>/<project>/issues/WEB-142` (or a UUID), and the same on a `host` for self-hosted | `projects`; `host` for self-hosted (`app.plane.so` by default) |
311
- | `url` | links to the hosts you list, and nothing else | `hosts`, required: host names such as `tasks.example.com`, with no scheme, port or path; `*.example.com` matches its subdomains |
312
- | `numeric` | `4821` | `minLength` (1), `maxLength` (20) |
313
- | `pattern` | anything matching your regex, anchored at both ends | `pattern`, `flags` |
314
-
315
- `projects` (Jira, YouTrack, Plane), `teams` (Linear) and `prefixes` (Notion, ClickUp, monday.com,
316
- ServiceNow) are keys as they appear in tickets, in uppercase: `TRADE` for `TRADE-123`, `STRY` for
317
- `STRY0010001`. YouTrack project IDs may also start with a digit, as `4DM`. Without them, any key passes;
318
- ServiceNow's list is required. `host` is a host name such as `jira.example.com`, or `*.example.com` for
319
- every subdomain, as in `{ preset: 'jira', host: '*.atlassian.net' }`. Every preset that takes `host`
320
- requires a plain host name: a value with a scheme, port or path never matched a link, so it is an error.
321
- Each preset
322
- takes only its own options, so `host` on the `any` preset fails with a clear message instead of being
323
- ignored. A `pattern` always matches the whole ticket, so the `g` and `y` flags make no difference and are
324
- ignored.
325
-
326
- A preset rejects links that aren't one ticket in its tracker, such as a Trello board, an Asana project or
327
- a Notion database view. Redmine runs on your own server, and `/issues/123` is also GitHub's path, so its
328
- links pass only once `host` is set. The same holds for OpenProject, Bugzilla and Phabricator, which also
329
- run on their own servers.
330
-
331
- For a tracker without a preset, use `url` with its host, for example
332
- `{ preset: 'url', hosts: ['www.wrike.com'] }` for Wrike links such as
333
- `https://www.wrike.com/open.htm?id=4821`; a link with `?id=0`, or a path that ends in `/0`, is a
334
- placeholder. It works the same for Microsoft Planner (`tasks.office.com`), Airtable (`airtable.com`),
335
- Smartsheet (`app.smartsheet.com`) and trackers of your own. For another
336
- tracker's keys, such as `WEB-12`, `any` or `pattern` works. Gitea, Forgejo and Codeberg use GitHub's link layout,
337
- so the `github` preset with their `host` covers them. Bitbucket has no issue tracker since August 2026,
338
- so it has no preset.
339
-
340
- Presets combine, and each state can have its own format. For example, bugs in Jira and flaky-test
341
- reports in GitHub:
117
+ ## Rules
342
118
 
343
- ```js
344
- testGovernance.configure({
345
- framework: 'playwright',
346
- ticket: { preset: 'jira', projects: ['TRADE'] },
347
- lifecycleTags: true,
348
- states: { quarantined: { ticket: { preset: 'github' } } },
349
- });
350
- ```
119
+ All framework configs use the severities in [Defaults](#defaults).
351
120
 
352
- The built-in presets are written to run in linear time. A `pattern` preset runs your regex as given,
353
- so keep it free of nested quantifiers.
121
+ <!-- begin auto-generated rules list -->
354
122
 
355
- ### Tickets from annotations
123
+ | Name | Description |
124
+ | :--------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- |
125
+ | [consistent-tag-names](docs/rules/consistent-tag-names.md) | Disallow tag names that differ in case or look like typos |
126
+ | [limit-retries](docs/rules/limit-retries.md) | Enforce a maximum retry count for tests |
127
+ | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state |
128
+ | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state |
129
+ | [require-ticket](docs/rules/require-ticket.md) | Require a ticket for skipped, fixme, todo, retried and tagged tests |
130
+ | [require-ticket-for-commented-out-tests](docs/rules/require-ticket-for-commented-out-tests.md) | Require a ticket marker on commented-out tests |
131
+ | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket |
356
132
 
357
- A comment never reaches a test report. Playwright and Vitest can carry the ticket in data that does, so
358
- `annotations` accepts it from there as well:
133
+ <!-- end auto-generated rules list -->
359
134
 
360
- ```js
361
- test.skip(
362
- 'rejects a margin order above the buying power',
363
- {
364
- annotation: { type: 'issue', description: 'TRADE-123' },
365
- },
366
- async () => {},
367
- );
368
- ```
135
+ ## Defaults
369
136
 
370
- ```js
371
- // Vitest: the ticket is the `issue` key of `meta`.
372
- test.skip('rejects a margin order above the buying power', { meta: { issue: 'TRADE-123' } }, () => {});
373
- ```
137
+ | Setting | Default |
138
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
139
+ | States that need a ticket | `skip`, `fixme`; in the other frameworks `skip`, `todo` |
140
+ | Tag ticket requirements | none by default; configure your own with `customStates` |
141
+ | Conditional skips | no ticket or reason required; opt in with `requireTicketForConditional` or `requireReasonForConditional` |
142
+ | `fail`, `slow`, `retry` | off; enable the states your framework supports with `states` |
143
+ | Marker format | uppercase keyword and colon, then comma-separated tickets: `// SKIP: TRADE-123, TRADE-124` |
144
+ | Notes after the ticket | not allowed; `allowNotes: true` allows them |
145
+ | `FIXME` / `TODO` comments | require tickets in test files; `comments: false` turns this check off |
146
+ | Placement | comment block directly above the test or skip call; a blank line breaks it |
147
+ | Placeholder tickets | rejected: `TODO`, `TBD`, `XXX-*`, and any ticket numbered 0 |
148
+ | Describe blocks | a marker on a skipped or tagged describe covers its tests |
149
+ | Severity | `consistent-tag-names` and `require-ticket-for-commented-out-tests`: warn; every other rule except the opt-in retry limit: error |
150
+ | Retry limit | `limit-retries`: off; enable the rule explicitly, then set `maxRetries` (default `2`) |
151
+
152
+ Tags have no built-in meaning. Configure `customStates` for tags that need ticket markers, or
153
+ `knownTags` for spelling checks alone. Other tags remain allowed. See [custom tag policies](docs/configuration.md#tag-names).
154
+ Warnings allow a successful lint run unless your command uses `--max-warnings 0`.
374
155
 
375
- The `type` names Playwright's annotation type and Vitest's `meta` key; `'issue'` by default, matched
376
- exactly. The value must be the ticket and nothing else, as in a marker. One valid annotation covers every
377
- state the test is in, and a describe's annotation covers its tests. An annotation of another type is
378
- left alone, and an annotation whose ticket isn't static text is reported with `reportUnreadableTags`.
379
- `annotations: false` turns it off. A marker comment still works everywhere, and when both are written,
380
- both must be valid. Jest, Mocha, Jasmine, node:test and Cypress have no annotations, so for them it is
381
- comments only.
156
+ ## Options
382
157
 
383
- With `annotations: { only: true }`, a test or describe in Playwright or Vitest must carry its ticket in the
384
- annotation, and a marker comment above it is reported, with a suggestion that moves its tickets into
385
- `annotation` or `meta`. A runtime skip such as `test.skip()` in a body, and a step, can't carry an
386
- annotation, so their comments still count. `only` with another framework is a configuration error.
158
+ Use [`configure()`](docs/configuration.md#change-the-shared-policy) to change shared settings, or
159
+ [override rule severity](docs/troubleshooting.md#adopt-in-an-existing-suite). Options belong to the
160
+ plugin's shared settings, not individual rules.
387
161
 
388
- ### Markers in `eslint-disable` comments
162
+ <a name="tag-names"></a>
163
+ <a name="ticket-presets"></a>
164
+ <a name="tickets-from-annotations"></a>
165
+ <a name="markers-in-eslint-disable-comments"></a>
389
166
 
390
- If you also run a rule that bans skipped tests, such as `jest/no-disabled-tests`, the comment that turns it off
391
- can hold the marker in its description, after `--`:
167
+ - [Custom tags and spelling checks](docs/configuration.md#tag-names)
168
+ - [Ticket presets](docs/configuration.md#ticket-presets)
169
+ - [Tickets from annotations](docs/configuration.md#tickets-from-annotations)
170
+ - [Markers in ESLint directives](docs/configuration.md#markers-in-eslint-disable-comments)
392
171
 
393
- ```js
394
- // eslint-disable-next-line jest/no-disabled-tests -- SKIP: TRADE-123
395
- test.skip('rejects a margin order above the buying power', () => {});
396
- ```
172
+ <a name="which-functions-count-as-test"></a>
173
+ <a name="syntax-the-rules-understand"></a>
397
174
 
398
- The description is read as a marker comment, with the same tickets and notes rules. It works in an
399
- `eslint-disable-next-line` above the test, an `eslint-disable-line` on the line the test starts on, and an
400
- `eslint-disable` block comment, whose marker covers every test up to the `eslint-enable` of one of its rules, or of
401
- every rule. A suggestion that removes or adds a marker keeps the directive working.
402
-
403
- ### Which functions count as `test`
404
-
405
- In Playwright files:
406
-
407
- - `test` imported from `@playwright/test`, `playwright/test` or a component-testing package such as
408
- `@playwright/experimental-ct-react`, including the default import, aliases such as
409
- `import { test as it }` or `const it = test`, and `const { test } = require('@playwright/test')`.
410
- - `pw.test` on the whole module, after `import * as pw from '@playwright/test'`. The default import
411
- and `const pw = require('@playwright/test')` are `test` itself, so `pw.skip(...)` and
412
- `pw.test.skip(...)` both count.
413
- - Fixtures made with `.extend()` or `mergeTests()` in the same file, such as
414
- `const test = base.extend({...})`.
415
- - Any name in `testFunctions`, for fixtures imported from your own modules. The default `['test']`
416
- covers the common `import { test } from './fixtures'`.
417
-
418
- In Jest and Vitest files:
419
-
420
- - The globals `test`, `it` and `describe`, Jest's `xit`, `xtest`, `fit`, `xdescribe` and
421
- `fdescribe`, and Vitest's `suite`, and the same names imported or required from `@jest/globals` or
422
- `vitest`, including aliases and `import * as vi from 'vitest'`.
423
- - Vitest fixtures made with `test.extend()` in the same file.
424
- - Any name in `testFunctions`, which defaults to `['test', 'it']`.
425
-
426
- In Mocha, Cypress and WebdriverIO files:
427
-
428
- - The globals `it`, `specify`, `describe` and `context`, their `x` forms, the TDD names `suite` and
429
- `test`, and the hooks `before`, `beforeEach`, `after`, `afterEach`, `suiteSetup`, `setup`,
430
- `suiteTeardown` and `teardown`.
431
- - The names imported or required from `mocha`, including aliases, and the whole module, as `mocha.it`
432
- after `import * as mocha from 'mocha'`, `import mocha from 'mocha'` or `const mocha = require('mocha')`.
433
- - Any name in `testFunctions`, which defaults to `['it', 'specify', 'test']`.
434
-
435
- In `node:test` files:
436
-
437
- - `test`, `it`, `describe`, `suite`, the hooks `before`, `beforeEach`, `after` and `afterEach`, and
438
- the members exported by name, `skip`, `todo`, `only` and `expectFailure`, imported or required from
439
- `node:test`, including aliases and `import * as nodeTest from 'node:test'`. The module is `test`
440
- itself, so after `import test from 'node:test'` or `const test = require('node:test')`,
441
- `test.describe(...)` is a describe.
442
- - Subtests: `t.test(...)` on the context a test's body receives.
443
- - Any name in `testFunctions`, which defaults to `['test', 'it']`. `node:test` has no globals.
444
-
445
- In Jasmine files:
446
-
447
- - The globals `it`, `xit`, `fit`, `describe`, `xdescribe`, `fdescribe`, and the hooks `beforeEach`,
448
- `afterEach`, `beforeAll` and `afterAll`. With the runner's `globals: false`, Jasmine 7 takes the same
449
- names from `jasmine-core`, so names imported or required from it count too, including aliases and
450
- the whole module, as `core.xit` after `const core = require('jasmine-core')`.
451
- - `pending()` and `notApplicable()`, as globals or imported by name.
452
- - Any name in `testFunctions`, which defaults to `['it']`. Jasmine has no `test`.
453
-
454
- A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
455
- `node:test`, `jasmine-core`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
456
- `const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
457
- variable or function declared inside a function that happens to be called `test`.
458
-
459
- ### Syntax the rules understand
460
-
461
- The [Frameworks](#frameworks) table lists what each framework's tests can do. In Playwright:
462
-
463
- - `test.skip('title', fn)`, `test.fixme(...)`, `test.fail(...)`, with or without a details object,
464
- and with a body defined elsewhere, as in `test.skip('places a limit order', placeLimitOrder)`. The
465
- title can also be a `const` declared in the same file.
466
- - `test.describe.skip(...)` and `test.describe.fixme(...)`, including `serial` and `parallel` variants,
467
- and `describe.skip(...)` after `const { describe } = test`. The state covers every test inside.
468
- - Runtime calls inside a test, a describe or a hook such as `test.beforeEach`: `test.skip()`,
469
- `test.skip(true)`, `test.fixme()`, `test.fail()`, `test.slow()`, the same methods on `testInfo`,
470
- and `test.info().skip()`.
471
- - Skipped steps: `test.step.skip('title', fn)`, and `step.skip()` on the `TestStepInfo` a step's body
472
- receives. Only the step is skipped, so a marker above the step or above a skipped test around it
473
- covers it.
474
- - Tags in titles (string or template literal) and in `{ tag: '@new' }` / `{ tag: [...] }`, inherited
475
- from enclosing describes. Tags, titles and details can also come from a `const` or an enum declared
476
- in the same file, as in `{ tag: TAGS.NEW }` after `const TAGS = { NEW: '@new' } as const`, including
477
- arrays and spreads of them.
478
-
479
- Not covered in this version: tests generated in loops with dynamic titles, and tags or details imported
480
- from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
481
- `testFunctions`, a Vitest test with only a title, which Vitest runs as a todo but which looks the same as a
482
- helper called `test`, Cypress tests skipped with @cypress/skip-test's `skipOn()` and `onlyOn()`, `node:test`'s
483
- `getTestContext()`, Jasmine's `pending()` in a helper function that tests call, Jasmine 5 and 6's
484
- `require('jasmine-core').noGlobals()`, and other runners such as Bun.
175
+ See [recognized test functions and syntax](docs/frameworks.md), including
176
+ [static analysis limits](docs/frameworks.md#static-analysis-limits).
485
177
 
486
178
  ## TypeScript
487
179
 
488
- The package ships its own type declarations, which work with TypeScript 5.0 or later. Besides the
489
- plugin, it exports these types: `GovernanceOptions` (the options of `configure()`), `TicketSpec`,
490
- `CustomState`, `StateOverride`, `FrameworkName`, `RuleName`, `ConfigName`, `FlatConfig` and
491
- `TestGovernancePlugin`.
492
-
493
- ```ts
494
- // eslint.config.ts
495
- import tsParser from '@typescript-eslint/parser';
496
- import testGovernance, { type GovernanceOptions } from 'eslint-plugin-test-governance';
497
-
498
- const options: GovernanceOptions = {
499
- framework: 'playwright',
500
- lifecycleTags: true,
501
- ticket: { preset: 'jira', projects: ['TRADE'] },
502
- };
503
-
504
- export default [
505
- { files: ['**/*.ts'], languageOptions: { parser: tsParser } },
506
- { files: ['tests/**/*.ts'], ...testGovernance.configure(options) },
507
- ];
508
- ```
180
+ See [TypeScript setup and exported types](docs/configuration.md#typescript). TypeScript test files
181
+ need a parser; TypeScript config files also need an appropriate config loader.
509
182
 
510
183
  ## Contributing
511
184
 
512
- Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) explains the setup and
513
- the checks every pull request runs, and [CHANGELOG.md](CHANGELOG.md) lists the changes in each release.
185
+ [CONTRIBUTING.md](CONTRIBUTING.md) covers setup, checks and documentation maintenance.
186
+ [CHANGELOG.md](CHANGELOG.md) lists releases; [SECURITY.md](SECURITY.md) explains private vulnerability reports.
514
187
 
515
188
  ## License
516
189