eslint-plugin-test-governance 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,48 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-10-22
10
+
11
+ ### Added
12
+
13
+ - `node:test` support. `configs['node-test']`, or `configure({ framework: 'node-test' })`, checks tests
14
+ written with Node's built-in runner: `test`, `it`, `describe` and `suite` imported or required from
15
+ `node:test`, their `skip`, `todo` and `expectFailure` members, which `node:test` also exports by name,
16
+ and the same options. Node counts an option set to any value but `false` or `undefined`, so
17
+ `{ skip: 'broker sandbox is down' }` always skips and needs a ticket, while `{ skip: isWindows }` is a
18
+ conditional skip. `t.skip()` and `t.todo()` on a test's context, or `this.skip()` and `this.todo()` in a
19
+ `function`, count in a test, `beforeEach` and `afterEach`, and subtests declared with `t.test()` are
20
+ checked like tests. `expectFailure` is the `fail` state, which is off by default.
21
+
22
+ ### Fixed
23
+
24
+ - A marker in the wrong case directly above a test in that state, as `// todo:` above `test.todo(...)`, is
25
+ reported once, by `require-ticket`, which asks for uppercase. `require-ticket-in-comments` reported it too.
26
+
27
+ ## [0.7.0] - 2026-10-20
28
+
29
+ ### Added
30
+
31
+ - Mocha, Cypress and WebdriverIO support. `configs.mocha`, `configs.cypress` and `configs.webdriverio`,
32
+ or `configure({ framework })`, check those frameworks' tests with the same rules and markers:
33
+ `it.skip`, `describe.skip`, `context.skip`, `xit`, `xdescribe` and the other `x` forms, the TDD
34
+ interface's `suite.skip` and `test.skip`, with globals or imports from `mocha`, and `this.skip()` in a
35
+ test or hook written as a `function`. Under a condition, as in `if (!broker) this.skip()`, it is a
36
+ conditional skip, so it needs no ticket by default. Cypress and WebdriverIO run their tests on Mocha,
37
+ so their configs read the same calls; `webdriverio` is for WebdriverIO's Mocha framework.
38
+ - A Mocha test with only a title, `it('settles at T+1')`, which Mocha runs as pending, is in the `todo`
39
+ state and needs a `// TODO:` marker, like Jest's `test.todo`. `it.skip('title')` with no body is a
40
+ skip.
41
+ - In Cypress files, tags from @cypress/grep's `{ tags: '@smoke' }` count like tags in the title, and
42
+ `{ browser: '!webkit' }`, which runs the test only in some browsers, is a conditional skip.
43
+
44
+ ### Fixed
45
+
46
+ - A runtime skip after other code in a `try` block that has a `catch`, as in
47
+ `try { await stat(target); test.skip(); } catch {}`, is a conditional skip: an error before it jumps to
48
+ the `catch`, so the test runs. Before, it needed a ticket like an unconditional skip. A skip that starts
49
+ the `try` block, or one in a `try` with only `finally`, still does. This covers Mocha's `this.skip()` too.
50
+
9
51
  ## [0.6.2] - 2026-10-14
10
52
 
11
53
  ### Fixed
@@ -305,7 +347,9 @@ First release.
305
347
  - Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
306
348
  fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
307
349
 
308
- [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.2...HEAD
350
+ [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.8.0...HEAD
351
+ [0.8.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.7.0...v0.8.0
352
+ [0.7.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.2...v0.7.0
309
353
  [0.6.2]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.1...v0.6.2
310
354
  [0.6.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.0...v0.6.1
311
355
  [0.6.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.5.1...v0.6.0
package/README.md CHANGED
@@ -4,7 +4,7 @@
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 or Vitest suite honest. Every skipped, fixme, todo, `@new`
7
+ ESLint rules that keep a Playwright, Jest, Vitest, Mocha, Cypress, WebdriverIO or `node:test` suite honest. Every skipped, fixme, todo, `@new`
8
8
  or `@quarantined` test must point at the ticket that tracks it:
9
9
 
10
10
  <!-- example: valid -->
@@ -34,8 +34,9 @@ Requires ESLint 9 or 10 with flat config, and Node.js 22.12 or later. For TypeSc
34
34
 
35
35
  ## Usage
36
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`:
37
+ Pick the config for the framework your tests use, `configs.playwright`, `configs.jest`,
38
+ `configs.vitest`, `configs.mocha`, `configs.cypress`, `configs.webdriverio` or `configs['node-test']` for
39
+ Node's built-in runner, and point it at those test files with `files`:
39
40
 
40
41
  ```js
41
42
  // eslint.config.js
@@ -52,9 +53,16 @@ export default [
52
53
  ```
53
54
 
54
55
  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.
56
+ need to know which one a file uses. In a Jest, Vitest or Mocha config, `test`, `it` and `describe` belong
57
+ to that framework, whether they are globals or imported from `@jest/globals`, `vitest` or `mocha`, the
58
+ way the Jest, Vitest and Mocha ESLint plugins are set up. `node:test` has no globals, so in its config
59
+ they are the names imported or required from `node:test`, and `test` and `it` from `testFunctions`.
60
+
61
+ Cypress and WebdriverIO run their tests on Mocha, so `configs.cypress` and `configs.webdriverio` read the
62
+ same calls as `configs.mocha`. `webdriverio` means WebdriverIO's Mocha framework; its Jasmine framework
63
+ isn't supported yet. Puppeteer has no test functions of its own, so use the config of the runner its
64
+ tests run on: `configs.jest` for jest-puppeteer, or `configs.vitest`, `configs.mocha` or
65
+ `configs['node-test']`.
58
66
 
59
67
  With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
60
68
 
@@ -125,28 +133,40 @@ export default [
125
133
  ];
126
134
  ```
127
135
 
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
136
+ Each framework's config, such as `configs.playwright`, sets only the framework, so one listed after a
137
+ `configure()` for the same files keeps that config's options and changes only the framework. They all
130
138
  turn on the same rules. What puts a test in a state in each framework:
131
139
 
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()`, `context.skip('note')` 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()`, `(condition ? test.skip : test)(...)` | `(condition ? it.skip : it)(...)` | `skipIf(condition)`, `runIf(condition)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()`, `(condition ? it.skip : it)(...)` |
140
- | tags such as `@new` (`// NEW:`) | the title, `{ tag }` | the title | the title, `{ tags }`, where `'new'` and `'@new'` both mean `@new` |
140
+ | State (marker) | Playwright | Jest | Vitest | Mocha | Cypress | WebdriverIO | node-test |
141
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142
+ | `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` |
143
+ | `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()` |
144
+ | `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | | | | | |
145
+ | `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` | | | | `test.expectFailure`, `expectFailure()`, `{ expectFailure }` |
146
+ | `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | | | | | |
147
+ | 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)(...)` |
148
+ | 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 |
141
149
 
142
150
  Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
143
151
  A test or describe whose function a condition picks, as in `(isMobile ? test.skip : test)('title', fn)`,
144
152
  is a conditional skip in every framework.
145
153
  A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
146
154
  `runIf(true)` and `{ skip: false }` never do.
147
- In Vitest, `test.skip('title')` and `describe.skip('title')` with no body are skipped too.
155
+ In Vitest, Mocha and `node:test`, `test.skip('title')` and `describe.skip('title')` with no body are skipped too.
156
+ In Mocha, `this.skip()` counts in a test or hook written as a `function`; an arrow function gets no
157
+ Mocha context, so its `this` is not the test's. `this.slow(ms)` only sets Mocha's speed threshold, so
158
+ it isn't the `slow` state.
148
159
  Vitest writes tag names without the `@`, as in `{ tags: ['flaky'] }`, so a name in `tags` counts as the
149
160
  tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when: '@flaky'` matches it.
161
+ In `node:test`, an option counts with any value but `false` or `undefined`, as Node reads it, so
162
+ `{ skip: 'broker sandbox is down' }` always skips. `t.skip()` and `t.todo()` take a message, not a
163
+ condition, and in a test or hook written as a `function`, `this.skip()` and `this.todo()` do the same.
164
+ Subtests declared with `t.test('title', options, fn)` are checked like tests, and a test or describe can
165
+ leave out its title and start with its options or body, as in `test({ skip: true }, fn)`.
166
+ A test with no function, `test('title')`, passes in Node, so it isn't a todo. A describe's body, and
167
+ `before` and `after` inside it, get the suite's context, which can't skip. Node reports a test in one
168
+ state only, skip before todo before `expectFailure`, so `describe.skip('title', { todo: 'why' }, fn)`
169
+ needs only a `// SKIP:` marker.
150
170
 
151
171
  ## Rules
152
172
 
@@ -154,19 +174,23 @@ tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when:
154
174
 
155
175
  💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
156
176
  ⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
177
+ 🌲 Set in the `cypress` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
157
178
  🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
179
+ ☕ Set in the `mocha` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
180
+ 🟢 Set in the `node-test` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
158
181
  🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
159
182
  ⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
183
+ 🤖 Set in the `webdriverio` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
160
184
  🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
161
185
  💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
162
186
 
163
- | Name                       | Description | 💼 | ⚠️ | 🔧 | 💡 |
164
- | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------- | :------- | :-- | :-- |
165
- | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🃏 🎭 ⚡ | | | 💡 |
166
- | [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🃏 🎭 ⚡ | 🔧 | |
167
- | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🃏 🎭 ⚡ | | | 💡 |
168
- | [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🃏 🎭 ⚡ | | | 💡 |
169
- | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🃏 🎭 ⚡ | | | 💡 |
187
+ | Name                       | Description | 💼 | ⚠️ | 🔧 | 💡 |
188
+ | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :------------------- | :------------------- | :-- | :-- |
189
+ | [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
190
+ | [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | 🔧 | |
191
+ | [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
192
+ | [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
193
+ | [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
170
194
 
171
195
  <!-- end auto-generated rules list -->
172
196
 
@@ -176,7 +200,7 @@ The defaults follow how most teams already work, so each framework config is use
176
200
 
177
201
  | Setting | Default | Why |
178
202
  | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
179
- | 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. |
203
+ | 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. |
180
204
  | `@new`, `@quarantined` | off; `lifecycleTags: true` turns both on | A promotion workflow is not universal. Teams that say `@flaky` instead [rename the tag](#tag-names). |
181
205
  | 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. |
182
206
  | `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 }`. |
@@ -196,20 +220,20 @@ yourself. `framework` is required, and every other option has a default. The rul
196
220
  of their own: they all read these shared settings, so they always agree on ticket formats, states
197
221
  and markers. To turn a rule off, set it to `'off'` as usual.
198
222
 
199
- | Option | Type | Default | Description |
200
- | ----------------------------- | -------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
201
- | `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). |
202
- | `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
203
- | `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. |
204
- | `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
205
- | `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `new`, `quarantined`) on or off, renames their tag or marker, or gives them their own ticket format. `fail` and `slow` 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 and `todo` only in Jest and Vitest, so turning one on elsewhere has no effect. |
206
- | `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. |
207
- | `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 or after an early `return` or `throw`. |
208
- | `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
209
- | `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. |
210
- | `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. |
211
- | `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. |
212
- | `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. |
223
+ | Option | Type | Default | Description |
224
+ | ----------------------------- | ----------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
225
+ | `framework` | `'playwright' \| 'jest' \| 'vitest' \| 'mocha' \| 'cypress' \| 'webdriverio'` | 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). |
226
+ | `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
227
+ | `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. |
228
+ | `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
229
+ | `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }` | `skip`, `fixme` and `todo` on | Turns built-in states (`skip`, `fixme`, `fail`, `slow`, `todo`, `new`, `quarantined`) on or off, renames their tag or marker, or gives them their own ticket format. `fail` and `slow` 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, and `fail` in Playwright, Jest and Vitest, so turning one on elsewhere has no effect. |
230
+ | `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. |
231
+ | `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`. |
232
+ | `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
233
+ | `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. |
234
+ | `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. |
235
+ | `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`, and Mocha, Cypress and WebdriverIO to `['it', 'specify', 'test']`. Your list replaces the default one, so include those names if you still need them. See below. |
236
+ | `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. |
213
237
 
214
238
  ### Tag names
215
239
 
@@ -293,6 +317,24 @@ In Jest and Vitest files:
293
317
  - Vitest fixtures made with `test.extend()` in the same file.
294
318
  - Any name in `testFunctions`, which defaults to `['test', 'it']`.
295
319
 
320
+ In Mocha, Cypress and WebdriverIO files:
321
+
322
+ - The globals `it`, `specify`, `describe` and `context`, their `x` forms, the TDD names `suite` and
323
+ `test`, and the hooks `before`, `beforeEach`, `after`, `afterEach`, `suiteSetup`, `setup`,
324
+ `suiteTeardown` and `teardown`.
325
+ - The names imported or required from `mocha`, including aliases and `import * as mocha from 'mocha'`.
326
+ - Any name in `testFunctions`, which defaults to `['it', 'specify', 'test']`.
327
+
328
+ In `node:test` files:
329
+
330
+ - `test`, `it`, `describe`, `suite`, the hooks `before`, `beforeEach`, `after` and `afterEach`, and
331
+ the members exported by name, `skip`, `todo`, `only` and `expectFailure`, imported or required from
332
+ `node:test`, including aliases and `import * as nodeTest from 'node:test'`. The module is `test`
333
+ itself, so after `import test from 'node:test'` or `const test = require('node:test')`,
334
+ `test.describe(...)` is a describe.
335
+ - Subtests: `t.test(...)` on the context a test's body receives.
336
+ - Any name in `testFunctions`, which defaults to `['test', 'it']`. `node:test` has no globals.
337
+
296
338
  A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
297
339
  `node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
298
340
  `const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
@@ -321,7 +363,9 @@ The [Frameworks](#frameworks) table lists what each framework's tests can do. In
321
363
  Not covered in this version: tests generated in loops with dynamic titles, and tags or details imported
322
364
  from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
323
365
  `testFunctions`, a Vitest test with only a title, which Vitest runs as a todo but which looks the same as a
324
- helper called `test`, Playwright annotations as markers, and other runners such as Mocha, Bun and `node:test`.
366
+ helper called `test`, Playwright annotations as markers, Cypress tests skipped with @cypress/skip-test's
367
+ `skipOn()` and `onlyOn()`, `node:test`'s experimental `tags` option and `getTestContext()`, and other
368
+ runners such as Jasmine and Bun.
325
369
 
326
370
  ## TypeScript
327
371
 
package/dist/index.d.ts CHANGED
@@ -13,8 +13,8 @@ export interface FlatConfig {
13
13
  settings?: Record<string, unknown>;
14
14
  }
15
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`.
16
+ * One config per framework. Each turns on the same rules for that framework's tests; point each one at its
17
+ * test files with `files`.
18
18
  */
19
19
  export type ConfigName = FrameworkName;
20
20
  export interface TestGovernancePlugin extends ESLint.Plugin {
@@ -33,5 +33,5 @@ declare const plugin: TestGovernancePlugin;
33
33
  * so all rules read the same ticket formats and states.
34
34
  */
35
35
  export declare function configure(options: GovernanceOptions): FlatConfig;
36
- export declare const configs: Record<"playwright" | "jest" | "vitest", FlatConfig>;
36
+ export declare const configs: Record<"playwright" | "jest" | "vitest" | "mocha" | "cypress" | "webdriverio" | "node-test", FlatConfig>;
37
37
  export default plugin;
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ for (const framework of FRAMEWORK_NAMES) {
52
52
  // tools that list the configs don't see it.
53
53
  Object.defineProperty(plugin.configs, 'recommended', {
54
54
  get() {
55
- throw new ConfigError('configs.recommended was removed in 0.5.0. Use the config for your framework: configs.playwright, configs.jest or configs.vitest. See https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md');
55
+ throw new ConfigError("configs.recommended was removed in 0.5.0. Use the config for your framework: configs.playwright, configs.jest, configs.vitest, configs.mocha, configs.cypress, configs.webdriverio or configs['node-test']. See https://github.com/Huldoser/eslint-plugin-test-governance/blob/main/docs/upgrading.md");
56
56
  },
57
57
  });
58
58
  export const configs = plugin.configs;
@@ -1,4 +1,4 @@
1
- import { appliesTo, commentLines, isStateMarker, parseTickets } from "../utils/analyze.js";
1
+ import { appliesTo, commentLines, parseTickets } from "../utils/analyze.js";
2
2
  import { createRule } from "../utils/create-rule.js";
3
3
  import { compileTicketSpec } from "../utils/tickets.js";
4
4
  /** Recognises anything shaped like a ticket, to tell a wrong-format ticket from plain prose. */
@@ -30,11 +30,13 @@ export default createRule({
30
30
  return;
31
31
  const { sourceCode } = context;
32
32
  const matcher = options.commentTicket;
33
- // A marker directly above a test in that state belongs to require-ticket.
33
+ // A marker directly above a test in that state belongs to require-ticket, which also reports one
34
+ // in the wrong case.
34
35
  const owned = new Set();
35
36
  for (const subject of analysis.subjects) {
36
37
  for (const marker of subject.markers) {
37
- if (isStateMarker(marker) && appliesTo(subject, marker.state.name))
38
+ const state = marker.state ?? marker.caseOf;
39
+ if (state && appliesTo(subject, state.name))
38
40
  owned.add(marker.comment);
39
41
  }
40
42
  }