eslint-plugin-test-governance 0.7.0 → 0.8.1
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 +31 -1
- package/README.md +51 -26
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/rules/require-ticket-in-comments.js +5 -3
- package/dist/utils/analyze.js +121 -38
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +9 -1
- package/dist/utils/frameworks.d.ts +29 -2
- package/dist/utils/frameworks.js +79 -4
- package/dist/utils/options.d.ts +1 -1
- package/dist/utils/schema.js +3 -1
- package/docs/rules/marker-matches-state.md +1 -1
- package/docs/rules/no-conflicting-states.md +1 -1
- package/docs/rules/no-orphaned-marker.md +1 -1
- package/docs/rules/require-ticket-in-comments.md +3 -2
- package/docs/rules/require-ticket.md +26 -3
- package/docs/upgrading.md +2 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.1] - 2026-10-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- In Playwright files, `const pw = require('@playwright/test')` and `import pw from '@playwright/test'`
|
|
14
|
+
are `test` itself, so `pw.skip(...)`, `pw.describe.fixme(...)` and `pw.test.skip(...)` are checked.
|
|
15
|
+
They were missed.
|
|
16
|
+
- In Mocha, Cypress and WebdriverIO files, `mocha.it.skip(...)` after `import mocha from 'mocha'` is
|
|
17
|
+
checked, as it already was after `import * as mocha from 'mocha'` and `require('mocha')`.
|
|
18
|
+
|
|
19
|
+
## [0.8.0] - 2026-10-22
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `node:test` support. `configs['node-test']`, or `configure({ framework: 'node-test' })`, checks tests
|
|
24
|
+
written with Node's built-in runner: `test`, `it`, `describe` and `suite` imported or required from
|
|
25
|
+
`node:test`, their `skip`, `todo` and `expectFailure` members, which `node:test` also exports by name,
|
|
26
|
+
and the same options. Node counts an option set to any value but `false` or `undefined`, so
|
|
27
|
+
`{ skip: 'broker sandbox is down' }` always skips and needs a ticket, while `{ skip: isWindows }` is a
|
|
28
|
+
conditional skip. `t.skip()` and `t.todo()` on a test's context, or `this.skip()` and `this.todo()` in a
|
|
29
|
+
`function`, count in a test, `beforeEach` and `afterEach`, and subtests declared with `t.test()` are
|
|
30
|
+
checked like tests. `expectFailure` is the `fail` state, which is off by default.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- A marker in the wrong case directly above a test in that state, as `// todo:` above `test.todo(...)`, is
|
|
35
|
+
reported once, by `require-ticket`, which asks for uppercase. `require-ticket-in-comments` reported it too.
|
|
36
|
+
|
|
9
37
|
## [0.7.0] - 2026-10-20
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -329,7 +357,9 @@ First release.
|
|
|
329
357
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
330
358
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
331
359
|
|
|
332
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
360
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.8.1...HEAD
|
|
361
|
+
[0.8.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.8.0...v0.8.1
|
|
362
|
+
[0.8.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.7.0...v0.8.0
|
|
333
363
|
[0.7.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.2...v0.7.0
|
|
334
364
|
[0.6.2]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.1...v0.6.2
|
|
335
365
|
[0.6.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.0...v0.6.1
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/Huldoser/eslint-plugin-test-governance/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
ESLint rules that keep a Playwright, Jest, Vitest, Mocha, Cypress or
|
|
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 -->
|
|
@@ -35,8 +35,8 @@ Requires ESLint 9 or 10 with flat config, and Node.js 22.12 or later. For TypeSc
|
|
|
35
35
|
## Usage
|
|
36
36
|
|
|
37
37
|
Pick the config for the framework your tests use, `configs.playwright`, `configs.jest`,
|
|
38
|
-
`configs.vitest`, `configs.mocha`, `configs.cypress
|
|
39
|
-
test files with `files`:
|
|
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`:
|
|
40
40
|
|
|
41
41
|
```js
|
|
42
42
|
// eslint.config.js
|
|
@@ -55,12 +55,14 @@ export default [
|
|
|
55
55
|
There is no default config. Each framework declares, skips and tags tests its own way, so the rules
|
|
56
56
|
need to know which one a file uses. In a Jest, Vitest or Mocha config, `test`, `it` and `describe` belong
|
|
57
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.
|
|
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`.
|
|
59
60
|
|
|
60
61
|
Cypress and WebdriverIO run their tests on Mocha, so `configs.cypress` and `configs.webdriverio` read the
|
|
61
62
|
same calls as `configs.mocha`. `webdriverio` means WebdriverIO's Mocha framework; its Jasmine framework
|
|
62
63
|
isn't supported yet. Puppeteer has no test functions of its own, so use the config of the runner its
|
|
63
|
-
tests run on: `configs.jest` for jest-puppeteer, or `configs.vitest
|
|
64
|
+
tests run on: `configs.jest` for jest-puppeteer, or `configs.vitest`, `configs.mocha` or
|
|
65
|
+
`configs['node-test']`.
|
|
64
66
|
|
|
65
67
|
With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
|
|
66
68
|
|
|
@@ -135,27 +137,36 @@ Each framework's config, such as `configs.playwright`, sets only the framework,
|
|
|
135
137
|
`configure()` for the same files keeps that config's options and changes only the framework. They all
|
|
136
138
|
turn on the same rules. What puts a test in a state in each framework:
|
|
137
139
|
|
|
138
|
-
| State (marker) | Playwright | Jest | Vitest | Mocha | Cypress | WebdriverIO |
|
|
139
|
-
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- |
|
|
140
|
-
| `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 |
|
|
141
|
-
| `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 |
|
|
142
|
-
| `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | | | | |
|
|
143
|
-
| `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` | | | |
|
|
144
|
-
| `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | | | | |
|
|
145
|
-
| 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 |
|
|
146
|
-
| 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 |
|
|
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 |
|
|
147
149
|
|
|
148
150
|
Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
|
|
149
151
|
A test or describe whose function a condition picks, as in `(isMobile ? test.skip : test)('title', fn)`,
|
|
150
152
|
is a conditional skip in every framework.
|
|
151
153
|
A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
|
|
152
154
|
`runIf(true)` and `{ skip: false }` never do.
|
|
153
|
-
In Vitest and
|
|
155
|
+
In Vitest, Mocha and `node:test`, `test.skip('title')` and `describe.skip('title')` with no body are skipped too.
|
|
154
156
|
In Mocha, `this.skip()` counts in a test or hook written as a `function`; an arrow function gets no
|
|
155
157
|
Mocha context, so its `this` is not the test's. `this.slow(ms)` only sets Mocha's speed threshold, so
|
|
156
158
|
it isn't the `slow` state.
|
|
157
159
|
Vitest writes tag names without the `@`, as in `{ tags: ['flaky'] }`, so a name in `tags` counts as the
|
|
158
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.
|
|
159
170
|
|
|
160
171
|
## Rules
|
|
161
172
|
|
|
@@ -166,19 +177,20 @@ tag with an `@` in front: `'flaky'` is `@flaky`, and a custom state with `when:
|
|
|
166
177
|
🌲 Set in the `cypress` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
167
178
|
🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
168
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).\
|
|
169
181
|
🎭 Set in the `playwright` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
170
182
|
⚡ Set in the `vitest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
171
183
|
🤖 Set in the `webdriverio` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
172
184
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
173
185
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
174
186
|
|
|
175
|
-
| Name | Description | 💼
|
|
176
|
-
| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- |
|
|
177
|
-
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🌲 🃏 ☕ 🎭 ⚡ 🤖 |
|
|
178
|
-
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos |
|
|
179
|
-
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🌲 🃏 ☕ 🎭 ⚡ 🤖 |
|
|
180
|
-
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🌲 🃏 ☕ 🎭 ⚡ 🤖 |
|
|
181
|
-
| [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 | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
182
194
|
|
|
183
195
|
<!-- end auto-generated rules list -->
|
|
184
196
|
|
|
@@ -290,8 +302,9 @@ In Playwright files:
|
|
|
290
302
|
- `test` imported from `@playwright/test`, `playwright/test` or a component-testing package such as
|
|
291
303
|
`@playwright/experimental-ct-react`, including the default import, aliases such as
|
|
292
304
|
`import { test as it }` or `const it = test`, and `const { test } = require('@playwright/test')`.
|
|
293
|
-
- `pw.test` on the whole module, after `import * as pw from '@playwright/test'
|
|
294
|
-
`const pw = require('@playwright/test')
|
|
305
|
+
- `pw.test` on the whole module, after `import * as pw from '@playwright/test'`. The default import
|
|
306
|
+
and `const pw = require('@playwright/test')` are `test` itself, so `pw.skip(...)` and
|
|
307
|
+
`pw.test.skip(...)` both count.
|
|
295
308
|
- Fixtures made with `.extend()` or `mergeTests()` in the same file, such as
|
|
296
309
|
`const test = base.extend({...})`.
|
|
297
310
|
- Any name in `testFunctions`, for fixtures imported from your own modules. The default `['test']`
|
|
@@ -310,9 +323,20 @@ In Mocha, Cypress and WebdriverIO files:
|
|
|
310
323
|
- The globals `it`, `specify`, `describe` and `context`, their `x` forms, the TDD names `suite` and
|
|
311
324
|
`test`, and the hooks `before`, `beforeEach`, `after`, `afterEach`, `suiteSetup`, `setup`,
|
|
312
325
|
`suiteTeardown` and `teardown`.
|
|
313
|
-
- The names imported or required from `mocha`, including aliases and
|
|
326
|
+
- The names imported or required from `mocha`, including aliases, and the whole module, as `mocha.it`
|
|
327
|
+
after `import * as mocha from 'mocha'`, `import mocha from 'mocha'` or `const mocha = require('mocha')`.
|
|
314
328
|
- Any name in `testFunctions`, which defaults to `['it', 'specify', 'test']`.
|
|
315
329
|
|
|
330
|
+
In `node:test` files:
|
|
331
|
+
|
|
332
|
+
- `test`, `it`, `describe`, `suite`, the hooks `before`, `beforeEach`, `after` and `afterEach`, and
|
|
333
|
+
the members exported by name, `skip`, `todo`, `only` and `expectFailure`, imported or required from
|
|
334
|
+
`node:test`, including aliases and `import * as nodeTest from 'node:test'`. The module is `test`
|
|
335
|
+
itself, so after `import test from 'node:test'` or `const test = require('node:test')`,
|
|
336
|
+
`test.describe(...)` is a describe.
|
|
337
|
+
- Subtests: `t.test(...)` on the context a test's body receives.
|
|
338
|
+
- Any name in `testFunctions`, which defaults to `['test', 'it']`. `node:test` has no globals.
|
|
339
|
+
|
|
316
340
|
A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
|
|
317
341
|
`node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
|
|
318
342
|
`const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
|
|
@@ -342,7 +366,8 @@ Not covered in this version: tests generated in loops with dynamic titles, and t
|
|
|
342
366
|
from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
|
|
343
367
|
`testFunctions`, a Vitest test with only a title, which Vitest runs as a todo but which looks the same as a
|
|
344
368
|
helper called `test`, Playwright annotations as markers, Cypress tests skipped with @cypress/skip-test's
|
|
345
|
-
`skipOn()` and `onlyOn()`,
|
|
369
|
+
`skipOn()` and `onlyOn()`, `node:test`'s experimental `tags` option and `getTestContext()`, and other
|
|
370
|
+
runners such as Jasmine and Bun.
|
|
346
371
|
|
|
347
372
|
## TypeScript
|
|
348
373
|
|
package/dist/index.d.ts
CHANGED
|
@@ -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" | "mocha" | "cypress" | "webdriverio", 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(
|
|
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,
|
|
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
|
-
|
|
38
|
+
const state = marker.state ?? marker.caseOf;
|
|
39
|
+
if (state && appliesTo(subject, state.name))
|
|
38
40
|
owned.add(marker.comment);
|
|
39
41
|
}
|
|
40
42
|
}
|
package/dist/utils/analyze.js
CHANGED
|
@@ -138,6 +138,9 @@ function collectTestNames(program, framework, configured) {
|
|
|
138
138
|
else if (spec.type === 'ImportDefaultSpecifier') {
|
|
139
139
|
if (framework.defaultExport)
|
|
140
140
|
roots.set(local, framework.defaultExport);
|
|
141
|
+
// `import test from 'node:test'` also has `test.describe`, and `import mocha from 'mocha'` has `mocha.it`.
|
|
142
|
+
if (framework.moduleIsDefault)
|
|
143
|
+
modules.add(local);
|
|
141
144
|
}
|
|
142
145
|
else {
|
|
143
146
|
const root = framework.exports.get(importedName(spec));
|
|
@@ -167,10 +170,14 @@ function collectTestNames(program, framework, configured) {
|
|
|
167
170
|
else if (isOtherFramework(init))
|
|
168
171
|
setOther(name);
|
|
169
172
|
else {
|
|
170
|
-
// `const
|
|
173
|
+
// `const mocha = require('mocha')`, used as `mocha.it`, or `const pw = require('@playwright/test')`,
|
|
174
|
+
// which is `test` itself, as `require('node:test')` is.
|
|
171
175
|
const source = requireSource(init);
|
|
172
|
-
if (source !== undefined && framework.ownsModule(source))
|
|
176
|
+
if (source !== undefined && framework.ownsModule(source)) {
|
|
173
177
|
modules.add(name);
|
|
178
|
+
if (framework.moduleIsDefault && framework.defaultExport)
|
|
179
|
+
roots.set(name, framework.defaultExport);
|
|
180
|
+
}
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
else if (declarator.id.type === 'ObjectPattern') {
|
|
@@ -443,10 +450,10 @@ function readTexts(sourceCode, node, out, via, path = []) {
|
|
|
443
450
|
/**
|
|
444
451
|
* The details or options object of a declaration, the second argument in
|
|
445
452
|
* `test('title', { tag: '@smoke' }, fn)` or Vitest's `test('title', { skip: true }, fn)`, also when it
|
|
446
|
-
* is a constant declared in this file.
|
|
453
|
+
* is a constant declared in this file. With `optionsFirst`, the first: node:test's `test({ skip: true }, fn)`.
|
|
447
454
|
*/
|
|
448
|
-
function detailsOf(sourceCode, call) {
|
|
449
|
-
const details = call.arguments.at(1);
|
|
455
|
+
function detailsOf(sourceCode, call, optionsFirst) {
|
|
456
|
+
const details = call.arguments.at(optionsFirst ? 0 : 1);
|
|
450
457
|
if (details === undefined || isFunction(details))
|
|
451
458
|
return { entries: [] };
|
|
452
459
|
const target = unwrap(details);
|
|
@@ -455,7 +462,9 @@ function detailsOf(sourceCode, call) {
|
|
|
455
462
|
if (object?.type !== 'ObjectExpression') {
|
|
456
463
|
// A name that can't be read is the details object only when a body comes after it:
|
|
457
464
|
// `test.skip('places an order', placeOrder)` has a body defined elsewhere and no details.
|
|
458
|
-
return call.arguments.slice(2).some(isFunction)
|
|
465
|
+
return call.arguments.slice(optionsFirst ? 1 : 2).some(isFunction)
|
|
466
|
+
? { entries: [], unread: details }
|
|
467
|
+
: { entries: [] };
|
|
459
468
|
}
|
|
460
469
|
const entries = object.properties.flatMap((prop) => {
|
|
461
470
|
if (prop.type !== 'Property' || prop.computed)
|
|
@@ -492,6 +501,20 @@ function collectTags(sourceCode, call, hasTitle, framework, details) {
|
|
|
492
501
|
unread.push(details.unread);
|
|
493
502
|
return { tags, dynamic: title.unread.size > 0, unread, opaqueDetails: details.unread !== undefined };
|
|
494
503
|
}
|
|
504
|
+
/** Whether `node` is an object written out or held in a constant declared in this file. */
|
|
505
|
+
function isObject(sourceCode, node) {
|
|
506
|
+
const target = unwrap(node);
|
|
507
|
+
const value = target.type === 'ObjectExpression' ? target : constantValue(sourceCode, target);
|
|
508
|
+
return value !== undefined && unwrap(value).type === 'ObjectExpression';
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Whether a declaration leaves out its title: a describe with only a body, or in node:test any test or
|
|
512
|
+
* describe that starts with its body or its options.
|
|
513
|
+
*/
|
|
514
|
+
function leavesOutTitle(sourceCode, framework, args) {
|
|
515
|
+
const [first] = args;
|
|
516
|
+
return isFunction(first) || (framework.optionalTitle && isObject(sourceCode, first));
|
|
517
|
+
}
|
|
495
518
|
/** Whether a title is text, written out or held in a constant declared in this file. */
|
|
496
519
|
function isTextTitle(sourceCode, node) {
|
|
497
520
|
const title = unwrap(node);
|
|
@@ -501,17 +524,21 @@ function isTextTitle(sourceCode, node) {
|
|
|
501
524
|
/**
|
|
502
525
|
* Whether a call declares a test or describe block: a title and a body, as in `test('title', fn)`,
|
|
503
526
|
* `test('title', details, fn)` or `test.skip(title, payWithCard)` with the body defined elsewhere, a
|
|
504
|
-
* describe with only a body, or a `todo` with only a title, in Vitest and
|
|
505
|
-
* title,
|
|
527
|
+
* describe with only a body, or a `todo` with only a title, in Vitest, Mocha and node:test a `skip` with
|
|
528
|
+
* only a title, in Mocha a test with only a title written out, which it runs as pending, and in node:test
|
|
529
|
+
* a test or describe with no title.
|
|
506
530
|
* Only Playwright has a runtime `test.skip(isMobile, 'why')`; there a title, written out or held in a
|
|
507
531
|
* constant, or a body written in place tells the two apart.
|
|
508
532
|
*/
|
|
509
533
|
function isDeclaration(sourceCode, framework, kind, args, members) {
|
|
510
534
|
const first = args.at(0);
|
|
511
535
|
if (isFunction(first))
|
|
512
|
-
return kind === 'describe';
|
|
536
|
+
return kind === 'describe' || framework.optionalTitle;
|
|
513
537
|
if (first === undefined)
|
|
514
538
|
return false;
|
|
539
|
+
// node:test's `test({ skip: true }, fn)`, or `test({ name, fn })` with everything in the options.
|
|
540
|
+
if (framework.optionalTitle && isObject(sourceCode, first))
|
|
541
|
+
return true;
|
|
515
542
|
if (args.length === 1) {
|
|
516
543
|
if (members.includes('todo') || (framework.skipWithoutBody && members.includes('skip')))
|
|
517
544
|
return true;
|
|
@@ -533,6 +560,29 @@ function thisFunction(node) {
|
|
|
533
560
|
function booleanOf(node) {
|
|
534
561
|
return node?.type === 'Literal' && typeof node.value === 'boolean' ? node.value : undefined;
|
|
535
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Whether a node:test option applies: Node counts any value but `false` and `undefined`, so a reason such
|
|
565
|
+
* as `{ skip: 'flaky' }`, `''`, `0`, `null`, and an `expectFailure` pattern or matcher always do. Undefined
|
|
566
|
+
* for a value only known when the test runs, such as `{ skip: isWindows }`.
|
|
567
|
+
*/
|
|
568
|
+
function optionApplies(node) {
|
|
569
|
+
const value = unwrap(node);
|
|
570
|
+
if (value.type === 'Identifier' && value.name === 'undefined')
|
|
571
|
+
return false;
|
|
572
|
+
if (value.type === 'Literal')
|
|
573
|
+
return value.value !== false;
|
|
574
|
+
return OPTION_VALUES.has(value.type) ? true : undefined;
|
|
575
|
+
}
|
|
576
|
+
/** Values that are never `false` or `undefined`. */
|
|
577
|
+
const OPTION_VALUES = new Set([
|
|
578
|
+
'TemplateLiteral',
|
|
579
|
+
'ObjectExpression',
|
|
580
|
+
'ArrayExpression',
|
|
581
|
+
'ArrowFunctionExpression',
|
|
582
|
+
'FunctionExpression',
|
|
583
|
+
'ClassExpression',
|
|
584
|
+
'NewExpression',
|
|
585
|
+
]);
|
|
536
586
|
/**
|
|
537
587
|
* Whether a runtime call always applies: it has no condition, or `true`, or with `notes` only a note, as
|
|
538
588
|
* Vitest's `context.skip('Broker sandbox is down')`, and nothing around it makes it conditional.
|
|
@@ -623,8 +673,10 @@ function runAnalysis(sourceCode, options) {
|
|
|
623
673
|
const { root, path, args } = chain;
|
|
624
674
|
let base = testNames.roots.get(root.name);
|
|
625
675
|
let rest = path;
|
|
626
|
-
|
|
627
|
-
|
|
676
|
+
// `pw.test.skip`, or node:test's `test.describe`, whose module is also its `test`.
|
|
677
|
+
const member = testNames.modules.has(root.name) ? framework.exports.get(path[0]) : undefined;
|
|
678
|
+
if (member !== undefined || base === undefined) {
|
|
679
|
+
base = member;
|
|
628
680
|
rest = path.slice(1);
|
|
629
681
|
}
|
|
630
682
|
return base !== undefined && !isLocalVariable(sourceCode, root) ? { root: base, path: rest, args } : undefined;
|
|
@@ -738,10 +790,14 @@ function runAnalysis(sourceCode, options) {
|
|
|
738
790
|
// `{ skip: true }` always skips, `{ skip: isMobile }` only sometimes, `{ skip: false }` never.
|
|
739
791
|
for (const [key, value] of details) {
|
|
740
792
|
const modifier = framework.optionEffects.get(key);
|
|
741
|
-
const
|
|
742
|
-
if (modifier &&
|
|
743
|
-
add(modifier,
|
|
793
|
+
const always = framework.anyOptionValue ? optionApplies(value) : booleanOf(value);
|
|
794
|
+
if (modifier && always !== false)
|
|
795
|
+
add(modifier, always === true);
|
|
744
796
|
}
|
|
797
|
+
const first = framework.exclusiveStates.findIndex((state) => modifiers.get(state) === true);
|
|
798
|
+
if (first !== -1)
|
|
799
|
+
for (const state of framework.exclusiveStates.slice(first + 1))
|
|
800
|
+
modifiers.delete(state);
|
|
745
801
|
return modifiers;
|
|
746
802
|
};
|
|
747
803
|
/**
|
|
@@ -754,10 +810,10 @@ function runAnalysis(sourceCode, options) {
|
|
|
754
810
|
}
|
|
755
811
|
return modifiers;
|
|
756
812
|
};
|
|
757
|
-
/** Keeps the body of a
|
|
813
|
+
/** Keeps the body of a test or hook written as a `function`, whose `this` can skip it, as in Mocha. */
|
|
758
814
|
const addThisBody = (args) => {
|
|
759
815
|
const body = args.findLast(isFunction);
|
|
760
|
-
if (
|
|
816
|
+
if (body?.type === 'FunctionExpression')
|
|
761
817
|
thisBodies.add(body);
|
|
762
818
|
};
|
|
763
819
|
/** What a body can change the test's state with: Playwright's `testInfo` or Vitest's test context. */
|
|
@@ -803,8 +859,7 @@ function runAnalysis(sourceCode, options) {
|
|
|
803
859
|
if (yes.kind !== no.kind || !declares(yes, args) || !declares(no, args))
|
|
804
860
|
return undefined;
|
|
805
861
|
usesTest = true;
|
|
806
|
-
const details =
|
|
807
|
-
const tagInfo = collectTags(sourceCode, call, !isFunction(args[0]), framework, details);
|
|
862
|
+
const { details, tagInfo } = headOf(call);
|
|
808
863
|
const [ifYes, ifNo] = [yes, no].map(({ members, args: conditions }) => declarationModifiers(yes.kind, args, modifiersOf(members, details.entries, conditions)));
|
|
809
864
|
if (yes.kind === 'test')
|
|
810
865
|
addThisBody(args);
|
|
@@ -812,12 +867,41 @@ function runAnalysis(sourceCode, options) {
|
|
|
812
867
|
for (const modifier of new Set([...ifYes.keys(), ...ifNo.keys()])) {
|
|
813
868
|
modifiers.set(modifier, ifYes.get(modifier) === true && ifNo.get(modifier) === true);
|
|
814
869
|
}
|
|
815
|
-
|
|
870
|
+
const infos = yes.kind === 'test' ? bodyInfos(args, yes.members) : [];
|
|
871
|
+
return { subject: makeSubject(yes.kind, call, modifiers, tagInfo), infos };
|
|
872
|
+
};
|
|
873
|
+
/** The details and tags of a declaration; node:test lets it leave out its title. */
|
|
874
|
+
const headOf = (call) => {
|
|
875
|
+
const args = call.arguments;
|
|
876
|
+
const untitled = leavesOutTitle(sourceCode, framework, args);
|
|
877
|
+
const details = detailsOf(sourceCode, call, untitled && !isFunction(args[0]));
|
|
878
|
+
return { details, tagInfo: collectTags(sourceCode, call, !untitled, framework, details) };
|
|
879
|
+
};
|
|
880
|
+
const declare = (call, { kind, members, args: conditions }, infos) => {
|
|
881
|
+
const args = call.arguments;
|
|
882
|
+
const { details, tagInfo } = headOf(call);
|
|
883
|
+
const modifiers = declarationModifiers(kind, args, modifiersOf(members, details.entries, conditions));
|
|
884
|
+
if (kind === 'test')
|
|
885
|
+
addThisBody(args);
|
|
886
|
+
return { subject: makeSubject(kind, call, modifiers, tagInfo), infos };
|
|
887
|
+
};
|
|
888
|
+
/** node:test's `t.test('title', fn)`: a subtest declared through the context of the test around it. */
|
|
889
|
+
const subtestOf = (call) => {
|
|
890
|
+
const { callee } = call;
|
|
891
|
+
if (!framework.subtests || callee.type !== 'MemberExpression' || memberName(callee) !== 'test')
|
|
892
|
+
return undefined;
|
|
893
|
+
const { object } = callee;
|
|
894
|
+
if (object.type !== 'Identifier' || !infos.findLast((info) => info.name === object.name)?.methods)
|
|
895
|
+
return undefined;
|
|
896
|
+
const declaration = { kind: 'test', members: [], args: new Map() };
|
|
897
|
+
return declares(declaration, call.arguments)
|
|
898
|
+
? declare(call, declaration, bodyInfos(call.arguments, []))
|
|
899
|
+
: undefined;
|
|
816
900
|
};
|
|
817
901
|
const visitCall = (call) => {
|
|
818
902
|
const callee = target(chainOf(call.callee, framework.factories));
|
|
819
903
|
if (callee === undefined)
|
|
820
|
-
return pickedDeclaration(call) ?? { subject: runtimeCall(call) };
|
|
904
|
+
return subtestOf(call) ?? pickedDeclaration(call) ?? { subject: runtimeCall(call) };
|
|
821
905
|
usesTest = true;
|
|
822
906
|
const args = call.arguments;
|
|
823
907
|
const { path } = callee;
|
|
@@ -825,6 +909,8 @@ function runAnalysis(sourceCode, options) {
|
|
|
825
909
|
// Vitest's `beforeEach((context) => context.skip())` and Mocha's `before(function () { this.skip(); })`
|
|
826
910
|
// skip the tests they run for.
|
|
827
911
|
if (declaration === undefined) {
|
|
912
|
+
if (callee.root.noContext)
|
|
913
|
+
return { infos: [] };
|
|
828
914
|
addThisBody(args);
|
|
829
915
|
return { infos: bodyInfos(args, path) };
|
|
830
916
|
}
|
|
@@ -839,17 +925,11 @@ function runAnalysis(sourceCode, options) {
|
|
|
839
925
|
return { subject, infos: infoNames(param, STEP_INFO_METHODS, subject) };
|
|
840
926
|
}
|
|
841
927
|
}
|
|
842
|
-
const bodyInfo = bodyInfos(args, path);
|
|
843
928
|
const { kind, members } = declaration;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const modifiers = declarationModifiers(kind, args, modifiersOf(members, details.entries, callee.args));
|
|
849
|
-
if (kind === 'test')
|
|
850
|
-
addThisBody(args);
|
|
851
|
-
return { subject: makeSubject(kind, call, modifiers, tagInfo), infos: bodyInfo };
|
|
852
|
-
}
|
|
929
|
+
// A describe's body gets no test context: node:test passes one that can't skip.
|
|
930
|
+
const bodyInfo = kind === 'describe' ? [] : bodyInfos(args, path);
|
|
931
|
+
if (declares(declaration, args))
|
|
932
|
+
return declare(call, declaration, bodyInfo);
|
|
853
933
|
// test.skip(), test.fixme(), test.fail() and test.slow() in a test, a describe or a hook.
|
|
854
934
|
if (framework.playwright && members.length === 1 && RUNTIME_MODIFIERS.has(members[0])) {
|
|
855
935
|
return { subject: runtimeSubject(call, members[0]), infos: bodyInfo };
|
|
@@ -857,8 +937,8 @@ function runAnalysis(sourceCode, options) {
|
|
|
857
937
|
return { infos: bodyInfo };
|
|
858
938
|
};
|
|
859
939
|
/**
|
|
860
|
-
* `testInfo.skip()`, `step.skip()`, `test.info().skip()`, Vitest's `context.skip()` or `skip()`,
|
|
861
|
-
* Mocha's `this.skip()`.
|
|
940
|
+
* `testInfo.skip()`, `step.skip()`, `test.info().skip()`, Vitest's `context.skip()` or `skip()`,
|
|
941
|
+
* Mocha's `this.skip()`, and node:test's `t.skip()` and `t.todo()`.
|
|
862
942
|
*/
|
|
863
943
|
const runtimeCall = (call) => {
|
|
864
944
|
const { callee } = call;
|
|
@@ -871,19 +951,21 @@ function runAnalysis(sourceCode, options) {
|
|
|
871
951
|
if (callee.type !== 'MemberExpression')
|
|
872
952
|
return undefined;
|
|
873
953
|
const name = memberName(callee);
|
|
874
|
-
if (name === undefined
|
|
954
|
+
if (name === undefined)
|
|
875
955
|
return undefined;
|
|
876
956
|
const { object } = callee;
|
|
877
|
-
// Mocha's `this.skip()`, in a test or hook whose `function` gives it the
|
|
957
|
+
// Mocha's `this.skip()` or node:test's `this.todo()`, in a test or hook whose `function` gives it the
|
|
958
|
+
// test's `this`.
|
|
878
959
|
if (object.type === 'ThisExpression') {
|
|
879
|
-
const owner = name
|
|
960
|
+
const owner = framework.thisMethods.has(name) ? thisFunction(call) : undefined;
|
|
880
961
|
return owner !== undefined && thisBodies.has(owner) ? runtimeSubject(call, name) : undefined;
|
|
881
962
|
}
|
|
882
963
|
if (object.type === 'Identifier') {
|
|
883
964
|
const info = innermost(object.name);
|
|
884
965
|
return info?.methods?.has(name) ? runtimeSubject(call, name, info.step) : undefined;
|
|
885
966
|
}
|
|
886
|
-
const isTestInfo =
|
|
967
|
+
const isTestInfo = RUNTIME_MODIFIERS.has(name) &&
|
|
968
|
+
object.type === 'CallExpression' &&
|
|
887
969
|
target(chainOf(object.callee, framework.factories))?.path.join('.') === 'info';
|
|
888
970
|
return isTestInfo ? runtimeSubject(call, name) : undefined;
|
|
889
971
|
};
|
|
@@ -1011,9 +1093,10 @@ export function headLoc(subject) {
|
|
|
1011
1093
|
const { node } = subject;
|
|
1012
1094
|
if (subject.kind === 'runtime')
|
|
1013
1095
|
return node.loc;
|
|
1014
|
-
// A describe
|
|
1096
|
+
// A describe, or in node:test any declaration, can start with its body or options and have no title. A
|
|
1097
|
+
// todo test has only a title.
|
|
1015
1098
|
const [first] = node.arguments;
|
|
1016
|
-
const title = isFunction(first) ? undefined : first;
|
|
1099
|
+
const title = isFunction(first) || first.type === 'ObjectExpression' ? undefined : first;
|
|
1017
1100
|
return { start: node.callee.loc.start, end: (title ?? node.callee).loc.end };
|
|
1018
1101
|
}
|
|
1019
1102
|
export function describeSubject(subject) {
|
|
@@ -8,7 +8,7 @@ export type BuiltinStateName = (typeof BUILTIN_STATE_NAMES)[number];
|
|
|
8
8
|
export declare const TAG_STATE_NAMES: readonly ["new", "quarantined"];
|
|
9
9
|
export type TagStateName = (typeof TAG_STATE_NAMES)[number];
|
|
10
10
|
/** The test frameworks the rules understand. */
|
|
11
|
-
export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest", "mocha", "cypress", "webdriverio"];
|
|
11
|
+
export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest", "mocha", "cypress", "webdriverio", "node-test"];
|
|
12
12
|
export type FrameworkName = (typeof FRAMEWORK_NAMES)[number];
|
|
13
13
|
export declare const TICKET_PRESETS: readonly ["any", "jira", "github", "gitlab", "linear", "azure-devops", "numeric", "pattern"];
|
|
14
14
|
/** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
|
package/dist/utils/constants.js
CHANGED
|
@@ -6,7 +6,15 @@ export const BUILTIN_STATE_NAMES = ['skip', 'fixme', 'fail', 'slow', 'todo', 'ne
|
|
|
6
6
|
/** The built-in states a tag puts a test in, so the only ones whose tag can be renamed. */
|
|
7
7
|
export const TAG_STATE_NAMES = ['new', 'quarantined'];
|
|
8
8
|
/** The test frameworks the rules understand. */
|
|
9
|
-
export const FRAMEWORK_NAMES = [
|
|
9
|
+
export const FRAMEWORK_NAMES = [
|
|
10
|
+
'playwright',
|
|
11
|
+
'jest',
|
|
12
|
+
'vitest',
|
|
13
|
+
'mocha',
|
|
14
|
+
'cypress',
|
|
15
|
+
'webdriverio',
|
|
16
|
+
'node-test',
|
|
17
|
+
];
|
|
10
18
|
export const TICKET_PRESETS = [
|
|
11
19
|
'any',
|
|
12
20
|
'jira',
|
|
@@ -8,6 +8,8 @@ export type Modifier = 'skip' | 'fixme' | 'fail' | 'slow' | 'todo';
|
|
|
8
8
|
export interface Root {
|
|
9
9
|
kind: 'test' | 'describe' | 'hook';
|
|
10
10
|
modifiers: readonly string[];
|
|
11
|
+
/** Set for a hook whose body gets no test context, as node:test's `before` in a describe. */
|
|
12
|
+
noContext?: true;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* The state a member puts a test in. A member called with a condition, like Vitest's `skipIf(...)`, puts it
|
|
@@ -27,6 +29,11 @@ export interface Framework {
|
|
|
27
29
|
exports: ReadonlyMap<string, Root>;
|
|
28
30
|
/** What a default import from the framework's module is. */
|
|
29
31
|
defaultExport?: Root;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the default export is the module itself, with the other functions as members: node:test's and
|
|
34
|
+
* Playwright's are `test`, which has `test.describe`, and Mocha's has `mocha.it`.
|
|
35
|
+
*/
|
|
36
|
+
moduleIsDefault: boolean;
|
|
30
37
|
/** Test functions that exist without an import, besides `testFunctions`. */
|
|
31
38
|
globals: ReadonlyMap<string, Root>;
|
|
32
39
|
/** The default `testFunctions`. */
|
|
@@ -43,6 +50,21 @@ export interface Framework {
|
|
|
43
50
|
effects: ReadonlyMap<string, Effect>;
|
|
44
51
|
/** Keys of an options object that put a test in a state, as in Vitest's `{ skip: true }`. */
|
|
45
52
|
optionEffects: ReadonlyMap<string, Modifier>;
|
|
53
|
+
/**
|
|
54
|
+
* Whether an option counts whatever its value, unless it is `false` or `undefined`, as node:test's
|
|
55
|
+
* `{ skip: 'reason' }`. Otherwise only `true` always counts and other values are conditions.
|
|
56
|
+
*/
|
|
57
|
+
anyOptionValue: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* States a test is in only one of: the first that always applies, as node:test reports a skipped todo
|
|
60
|
+
* test as skipped only. A state that applies only sometimes leaves the later ones for the other runs.
|
|
61
|
+
*/
|
|
62
|
+
exclusiveStates: readonly Modifier[];
|
|
63
|
+
/**
|
|
64
|
+
* Whether the title can be left out, as node:test's `test(fn)` and `test({ skip: true }, fn)`, and an
|
|
65
|
+
* options object then comes first.
|
|
66
|
+
*/
|
|
67
|
+
optionalTitle: boolean;
|
|
46
68
|
/** The key of the details or options object that holds tags. */
|
|
47
69
|
tagKey?: string;
|
|
48
70
|
/** Whether that key holds names without their `@`, as Vitest's `tags: ['flaky']`, which stand for `@flaky`. */
|
|
@@ -55,6 +77,8 @@ export interface Framework {
|
|
|
55
77
|
param: number;
|
|
56
78
|
methods: ReadonlySet<string>;
|
|
57
79
|
};
|
|
80
|
+
/** Whether that parameter declares subtests with `t.test(...)`, as in node:test. */
|
|
81
|
+
subtests: boolean;
|
|
58
82
|
/** Whether `skip` with only a title declares a skipped test or describe, as Vitest's `test.skip('title')`. */
|
|
59
83
|
skipWithoutBody: boolean;
|
|
60
84
|
/**
|
|
@@ -62,8 +86,11 @@ export interface Framework {
|
|
|
62
86
|
* `context.skip('why')`. In Playwright the first argument is always a condition.
|
|
63
87
|
*/
|
|
64
88
|
skipNotes: boolean;
|
|
65
|
-
/**
|
|
66
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The methods a test or hook written as a `function` can change its state with at runtime through `this`,
|
|
91
|
+
* as Mocha's `this.skip()`. Empty when `this` can't.
|
|
92
|
+
*/
|
|
93
|
+
thisMethods: ReadonlySet<string>;
|
|
67
94
|
/** Whether a test with only a title written out is a todo, as Mocha's pending `it('title')`. */
|
|
68
95
|
todoWithoutBody: boolean;
|
|
69
96
|
/** Built-in states a test can be put in with this framework. */
|
package/dist/utils/frameworks.js
CHANGED
|
@@ -16,6 +16,7 @@ const playwright = {
|
|
|
16
16
|
ownsModule: isPlaywrightModule,
|
|
17
17
|
exports: new Map([['test', TEST]]),
|
|
18
18
|
defaultExport: TEST,
|
|
19
|
+
moduleIsDefault: true,
|
|
19
20
|
globals: new Map(),
|
|
20
21
|
testFunctions: ['test'],
|
|
21
22
|
testMembers: new Set(['only', 'skip', 'fixme', 'fail']),
|
|
@@ -27,12 +28,16 @@ const playwright = {
|
|
|
27
28
|
['fail', 'fail'],
|
|
28
29
|
]),
|
|
29
30
|
optionEffects: new Map(),
|
|
31
|
+
anyOptionValue: false,
|
|
32
|
+
exclusiveStates: [],
|
|
33
|
+
optionalTitle: false,
|
|
30
34
|
tagKey: 'tag',
|
|
31
35
|
plainTags: false,
|
|
32
36
|
info: { param: 1, methods: RUNTIME_MODIFIERS },
|
|
37
|
+
subtests: false,
|
|
33
38
|
skipWithoutBody: false,
|
|
34
39
|
skipNotes: false,
|
|
35
|
-
|
|
40
|
+
thisMethods: new Set(),
|
|
36
41
|
todoWithoutBody: false,
|
|
37
42
|
states: new Set(['skip', 'fixme', 'fail', 'slow', 'new', 'quarantined']),
|
|
38
43
|
playwright: true,
|
|
@@ -51,6 +56,7 @@ const jest = {
|
|
|
51
56
|
name: 'jest',
|
|
52
57
|
ownsModule: (source) => source === '@jest/globals',
|
|
53
58
|
exports: JEST_FUNCTIONS,
|
|
59
|
+
moduleIsDefault: false,
|
|
54
60
|
globals: new Map([...JEST_FUNCTIONS].filter(([name]) => name !== 'test' && name !== 'it')),
|
|
55
61
|
testFunctions: ['test', 'it'],
|
|
56
62
|
testMembers: new Set(['only', 'skip', 'todo', 'failing', 'concurrent', 'each()']),
|
|
@@ -62,10 +68,14 @@ const jest = {
|
|
|
62
68
|
['failing', 'fail'],
|
|
63
69
|
]),
|
|
64
70
|
optionEffects: new Map(),
|
|
71
|
+
anyOptionValue: false,
|
|
72
|
+
exclusiveStates: [],
|
|
73
|
+
optionalTitle: false,
|
|
65
74
|
plainTags: false,
|
|
75
|
+
subtests: false,
|
|
66
76
|
skipWithoutBody: false,
|
|
67
77
|
skipNotes: false,
|
|
68
|
-
|
|
78
|
+
thisMethods: new Set(),
|
|
69
79
|
todoWithoutBody: false,
|
|
70
80
|
states: new Set(['skip', 'todo', 'fail', 'new', 'quarantined']),
|
|
71
81
|
playwright: false,
|
|
@@ -81,6 +91,7 @@ const vitest = {
|
|
|
81
91
|
['suite', DESCRIBE],
|
|
82
92
|
['beforeEach', HOOK],
|
|
83
93
|
]),
|
|
94
|
+
moduleIsDefault: false,
|
|
84
95
|
globals: new Map([
|
|
85
96
|
['describe', DESCRIBE],
|
|
86
97
|
['suite', DESCRIBE],
|
|
@@ -102,12 +113,16 @@ const vitest = {
|
|
|
102
113
|
['todo', 'todo'],
|
|
103
114
|
['fails', 'fail'],
|
|
104
115
|
]),
|
|
116
|
+
anyOptionValue: false,
|
|
117
|
+
exclusiveStates: [],
|
|
118
|
+
optionalTitle: false,
|
|
105
119
|
tagKey: 'tags',
|
|
106
120
|
plainTags: true,
|
|
107
121
|
info: { param: 0, methods: new Set(['skip']) },
|
|
122
|
+
subtests: false,
|
|
108
123
|
skipWithoutBody: true,
|
|
109
124
|
skipNotes: true,
|
|
110
|
-
|
|
125
|
+
thisMethods: new Set(),
|
|
111
126
|
todoWithoutBody: false,
|
|
112
127
|
states: new Set(['skip', 'todo', 'fail', 'new', 'quarantined']),
|
|
113
128
|
playwright: false,
|
|
@@ -128,6 +143,7 @@ const MOCHA_EXPORTS = new Map([
|
|
|
128
143
|
const mocha = {
|
|
129
144
|
ownsModule: (source) => source === 'mocha',
|
|
130
145
|
exports: MOCHA_EXPORTS,
|
|
146
|
+
moduleIsDefault: true,
|
|
131
147
|
globals: new Map([
|
|
132
148
|
...[...MOCHA_EXPORTS].filter(([name]) => name !== 'it' && name !== 'test'),
|
|
133
149
|
['context', DESCRIBE],
|
|
@@ -141,16 +157,74 @@ const mocha = {
|
|
|
141
157
|
effects: effects([['skip', 'skip']]),
|
|
142
158
|
// Cypress's `{ browser: '!webkit' }` runs the test only in some browsers.
|
|
143
159
|
optionEffects: new Map([['browser', 'skip']]),
|
|
160
|
+
anyOptionValue: false,
|
|
161
|
+
exclusiveStates: [],
|
|
162
|
+
optionalTitle: false,
|
|
144
163
|
// @cypress/grep's `{ tags: '@smoke' }`, written with the `@`.
|
|
145
164
|
tagKey: 'tags',
|
|
146
165
|
plainTags: false,
|
|
166
|
+
subtests: false,
|
|
147
167
|
skipWithoutBody: true,
|
|
148
168
|
skipNotes: false,
|
|
149
|
-
|
|
169
|
+
thisMethods: new Set(['skip']),
|
|
150
170
|
todoWithoutBody: true,
|
|
151
171
|
states: new Set(['skip', 'todo', 'new', 'quarantined']),
|
|
152
172
|
playwright: false,
|
|
153
173
|
};
|
|
174
|
+
const NODE_TEST_MEMBERS = new Set(['only', 'skip', 'todo', 'expectFailure']);
|
|
175
|
+
/** What a test's context can do to it, as `t.skip()`, or as `this.skip()` in a `function`. */
|
|
176
|
+
const NODE_TEST_CONTEXT = new Set(['skip', 'todo']);
|
|
177
|
+
/**
|
|
178
|
+
* Node's built-in runner. `test` and `it` are one function, as are `describe` and `suite`, and the state
|
|
179
|
+
* members are exported by name too. Only `beforeEach` and `afterEach` get the test's context, as a test
|
|
180
|
+
* does: as their first parameter, and as `this` when written as a `function`. `before`, `after` and the
|
|
181
|
+
* describe itself get the suite's, which can't skip.
|
|
182
|
+
*/
|
|
183
|
+
const nodeTest = {
|
|
184
|
+
name: 'node-test',
|
|
185
|
+
ownsModule: (source) => source === 'node:test',
|
|
186
|
+
exports: new Map([
|
|
187
|
+
['test', TEST],
|
|
188
|
+
['it', TEST],
|
|
189
|
+
['describe', DESCRIBE],
|
|
190
|
+
['suite', DESCRIBE],
|
|
191
|
+
...[...NODE_TEST_MEMBERS].map((member) => [member, { kind: 'test', modifiers: [member] }]),
|
|
192
|
+
['before', { kind: 'hook', modifiers: [], noContext: true }],
|
|
193
|
+
['after', { kind: 'hook', modifiers: [], noContext: true }],
|
|
194
|
+
['beforeEach', HOOK],
|
|
195
|
+
['afterEach', HOOK],
|
|
196
|
+
]),
|
|
197
|
+
defaultExport: TEST,
|
|
198
|
+
moduleIsDefault: true,
|
|
199
|
+
globals: new Map(),
|
|
200
|
+
testFunctions: ['test', 'it'],
|
|
201
|
+
testMembers: NODE_TEST_MEMBERS,
|
|
202
|
+
describeMembers: NODE_TEST_MEMBERS,
|
|
203
|
+
factories: new Set(),
|
|
204
|
+
effects: effects([
|
|
205
|
+
['skip', 'skip'],
|
|
206
|
+
['todo', 'todo'],
|
|
207
|
+
['expectFailure', 'fail'],
|
|
208
|
+
]),
|
|
209
|
+
optionEffects: new Map([
|
|
210
|
+
['skip', 'skip'],
|
|
211
|
+
['todo', 'todo'],
|
|
212
|
+
['expectFailure', 'fail'],
|
|
213
|
+
]),
|
|
214
|
+
anyOptionValue: true,
|
|
215
|
+
exclusiveStates: ['skip', 'todo', 'fail'],
|
|
216
|
+
optionalTitle: true,
|
|
217
|
+
plainTags: false,
|
|
218
|
+
info: { param: 0, methods: NODE_TEST_CONTEXT },
|
|
219
|
+
subtests: true,
|
|
220
|
+
skipWithoutBody: true,
|
|
221
|
+
// `t.skip('why')` takes a message, not a condition.
|
|
222
|
+
skipNotes: true,
|
|
223
|
+
thisMethods: NODE_TEST_CONTEXT,
|
|
224
|
+
todoWithoutBody: false,
|
|
225
|
+
states: new Set(['skip', 'todo', 'fail', 'new', 'quarantined']),
|
|
226
|
+
playwright: false,
|
|
227
|
+
};
|
|
154
228
|
export const FRAMEWORKS = {
|
|
155
229
|
playwright,
|
|
156
230
|
jest,
|
|
@@ -158,6 +232,7 @@ export const FRAMEWORKS = {
|
|
|
158
232
|
mocha: { name: 'mocha', ...mocha },
|
|
159
233
|
cypress: { name: 'cypress', ...mocha },
|
|
160
234
|
webdriverio: { name: 'webdriverio', ...mocha },
|
|
235
|
+
'node-test': nodeTest,
|
|
161
236
|
};
|
|
162
237
|
/** Whether `source` belongs to a test runner other than `framework`, so its names are never tests here. */
|
|
163
238
|
export function isOtherRunner(framework, source) {
|
package/dist/utils/options.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface CustomState {
|
|
|
21
21
|
export interface GovernanceOptions {
|
|
22
22
|
/**
|
|
23
23
|
* The test framework of the files the config applies to: `'playwright'`, `'jest'`, `'vitest'`, `'mocha'`,
|
|
24
|
-
* `'cypress'` or `'
|
|
24
|
+
* `'cypress'`, `'webdriverio'` or `'node-test'`. Required; each framework's config, such as `configs.mocha`, sets it.
|
|
25
25
|
*/
|
|
26
26
|
framework: FrameworkName;
|
|
27
27
|
/**
|
package/dist/utils/schema.js
CHANGED
|
@@ -6,7 +6,9 @@ const KEY_PATTERN = '^[A-Z][A-Z0-9_]+$';
|
|
|
6
6
|
const KEY_HINT = 'an uppercase key as it appears in tickets, such as WEB for WEB-123';
|
|
7
7
|
/** `['"a"', '"b"', '"c"']` as `"a", "b" or "c"`. */
|
|
8
8
|
const orList = (items) => `${items.slice(0, -1).join(', ')} or ${items.slice(-1).join('')}`;
|
|
9
|
-
|
|
9
|
+
/** How code reads a framework's config: `configs.mocha`, or `configs['node-test']` for a name with a dash. */
|
|
10
|
+
const configAccess = (name) => (/^\w+$/.test(name) ? `configs.${name}` : `configs['${name}']`);
|
|
11
|
+
const FRAMEWORK_HINT = `set it to ${orList(FRAMEWORK_NAMES.map((name) => `"${name}"`))}, or use ${orList(FRAMEWORK_NAMES.map(configAccess))}`;
|
|
10
12
|
const ticketSpecSchema = {
|
|
11
13
|
type: 'object',
|
|
12
14
|
properties: {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
📝 Require the marker keyword to match the test's state.
|
|
4
4
|
|
|
5
|
-
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
5
|
+
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🟢 `node-test`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
6
6
|
|
|
7
7
|
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
📝 Disallow state tags that contradict each other, differ in case or look like typos.
|
|
4
4
|
|
|
5
|
-
⚠️ This rule _warns_ in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
5
|
+
⚠️ This rule _warns_ in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🟢 `node-test`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
6
6
|
|
|
7
7
|
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
📝 Disallow marker comments that no longer match a test state.
|
|
4
4
|
|
|
5
|
-
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
5
|
+
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🟢 `node-test`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
6
6
|
|
|
7
7
|
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
📝 Require FIXME and TODO comments in test files to start with a ticket.
|
|
4
4
|
|
|
5
|
-
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
5
|
+
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🟢 `node-test`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
6
6
|
|
|
7
7
|
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
8
8
|
|
|
@@ -32,7 +32,8 @@ How comments are read:
|
|
|
32
32
|
rejected, and no text after the tickets unless `allowNotes` is set.
|
|
33
33
|
- A `// FIXME:` marker directly above a `test.fixme(...)`, or a `// TODO:` marker above a todo test,
|
|
34
34
|
such as Jest's `test.todo(...)` or Mocha's `it('title')`, is checked by [`require-ticket`](require-ticket.md) instead, so it is
|
|
35
|
-
reported once.
|
|
35
|
+
reported once. That includes one in the wrong case, `// fixme:` or `// Todo:`, which `require-ticket` asks to write in
|
|
36
|
+
uppercase.
|
|
36
37
|
- Only test files (a test framework import or a call to a test function) are checked.
|
|
37
38
|
Application code is left alone, even if your config applies the plugin to it.
|
|
38
39
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
📝 Require a ticket marker comment above skipped, fixme and tagged tests.
|
|
4
4
|
|
|
5
|
-
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
5
|
+
💼 This rule is enabled in the following [configs](https://github.com/Huldoser/eslint-plugin-test-governance#usage): 🌲 `cypress`, 🃏 `jest`, ☕ `mocha`, 🟢 `node-test`, 🎭 `playwright`, ⚡ `vitest`, 🤖 `webdriverio`.
|
|
6
6
|
|
|
7
7
|
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ A test is in a state when it is:
|
|
|
17
17
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------- |
|
|
18
18
|
| `skip` | `test.skip('title', fn)`, `test.describe.skip`, `test.skip()` in a body, `testInfo.skip()`, `test.step.skip`, `step.skip()` in a step | `// SKIP:` | yes |
|
|
19
19
|
| `fixme` | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | `// FIXME:` | yes |
|
|
20
|
-
| `todo` | Jest's and
|
|
20
|
+
| `todo` | Jest's, Vitest's and `node:test`'s `test.todo`, and a Mocha test with only a title, `it('title')` | `// TODO:` | yes, except in Playwright |
|
|
21
21
|
| `new` | the `@new` tag | `// NEW:` | with `lifecycleTags: true` |
|
|
22
22
|
| `quarantined` | the `@quarantined` tag | `// QUARANTINED:` | with `lifecycleTags: true` |
|
|
23
23
|
| `fail`, `slow` | `test.fail(...)`, `test.slow()`, `testInfo.fail()`, `testInfo.slow()` | `// FAIL:`, `// SLOW:` | no |
|
|
@@ -26,7 +26,9 @@ A test is in a state when it is:
|
|
|
26
26
|
In Jest and Vitest, `xit`, `xdescribe`, Vitest's `{ skip: true }`, `skipIf(true)`, `context.skip()` and
|
|
27
27
|
`context.skip('note')`, in a test or in `beforeEach`, skip a test too, and `test.failing` and `test.fails` put it in the `fail` state. In Mocha, Cypress and WebdriverIO, `xit`, `xdescribe` and
|
|
28
28
|
`this.skip()` in a test or hook skip a test too, and Cypress's `{ browser: '!webkit' }` skips it in other
|
|
29
|
-
browsers, which counts as a conditional skip.
|
|
29
|
+
browsers, which counts as a conditional skip. In `node:test`, `{ skip: 'reason' }`, `t.skip()` and `this.skip()` skip a test, `{ todo: true }`,
|
|
30
|
+
`t.todo()` and `this.todo()` make it a todo, `t.test()` subtests count as tests, and `test.expectFailure` puts it in the `fail` state. The
|
|
31
|
+
[README](../../README.md#frameworks) lists every form.
|
|
30
32
|
|
|
31
33
|
Tags are read from the title (`'places a limit order @new'`) and from `{ tag: '@new' }` or `{ tag: ['@new', '@smoke'] }`,
|
|
32
34
|
also when they come from a `const` or an enum declared in the same file, as in `{ tag: TAGS.NEW }`.
|
|
@@ -203,6 +205,14 @@ it('settles a trade at T+1', function () {
|
|
|
203
205
|
});
|
|
204
206
|
```
|
|
205
207
|
|
|
208
|
+
<!-- example: invalid settings={"framework":"node-test"} -->
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { test } from 'node:test';
|
|
212
|
+
|
|
213
|
+
test('liquidates below maintenance', { skip: 'broker sandbox is down' }, () => {});
|
|
214
|
+
```
|
|
215
|
+
|
|
206
216
|
<!-- example: invalid settings={"framework":"cypress","lifecycleTags":true} -->
|
|
207
217
|
|
|
208
218
|
```js
|
|
@@ -276,6 +286,19 @@ describe('settlement', function () {
|
|
|
276
286
|
});
|
|
277
287
|
```
|
|
278
288
|
|
|
289
|
+
<!-- example: valid settings={"framework":"node-test"} -->
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import { test } from 'node:test';
|
|
293
|
+
|
|
294
|
+
// SKIP: TRADE-92
|
|
295
|
+
test('liquidates below maintenance', { skip: 'broker sandbox is down' }, () => {});
|
|
296
|
+
|
|
297
|
+
test('nets margin across accounts', (t) => {
|
|
298
|
+
if (!process.env.RISK_URL) t.skip();
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
279
302
|
## Options
|
|
280
303
|
|
|
281
304
|
This rule has no options of its own. It reads the shared options described in the
|
package/docs/upgrading.md
CHANGED
|
@@ -90,7 +90,7 @@ becomes `extends: ['test-governance/playwright']`.
|
|
|
90
90
|
A config that still reads `configs.recommended` fails when it loads:
|
|
91
91
|
|
|
92
92
|
```text
|
|
93
|
-
eslint-plugin-test-governance: configs.recommended was removed in 0.5.0. Use the config for your framework: configs.playwright, configs.jest, configs.vitest, configs.mocha, configs.cypress or configs.
|
|
93
|
+
eslint-plugin-test-governance: 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
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
### The framework option
|
|
@@ -121,7 +121,7 @@ Without it, the config fails when it loads:
|
|
|
121
121
|
|
|
122
122
|
```text
|
|
123
123
|
eslint-plugin-test-governance: invalid options:
|
|
124
|
-
- framework is required: set it to "playwright", "jest", "vitest", "mocha", "cypress" or "
|
|
124
|
+
- framework is required: set it to "playwright", "jest", "vitest", "mocha", "cypress", "webdriverio" or "node-test", or use configs.playwright, configs.jest, configs.vitest, configs.mocha, configs.cypress, configs.webdriverio or configs['node-test']
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
If you write `settings['test-governance']` yourself instead of calling `configure()`, add `framework`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-test-governance",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ESLint rules that require a ticket comment above skipped, fixme, todo and tagged Playwright, Jest, Vitest, Mocha, Cypress and
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "ESLint rules that require a ticket comment above skipped, fixme, todo and tagged Playwright, Jest, Vitest, Mocha, Cypress, WebdriverIO and node:test tests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
7
7
|
"eslintplugin",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"mocha",
|
|
13
13
|
"cypress",
|
|
14
14
|
"webdriverio",
|
|
15
|
+
"node-test",
|
|
15
16
|
"testing"
|
|
16
17
|
],
|
|
17
18
|
"homepage": "https://github.com/Huldoser/eslint-plugin-test-governance#readme",
|