eslint-plugin-test-governance 0.8.1 → 0.10.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 +42 -1
- package/README.md +103 -59
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/analyze.d.ts +2 -0
- package/dist/utils/analyze.js +68 -13
- package/dist/utils/constants.d.ts +4 -2
- package/dist/utils/constants.js +15 -0
- package/dist/utils/frameworks.d.ts +5 -0
- package/dist/utils/frameworks.js +64 -1
- package/dist/utils/options.d.ts +3 -3
- package/dist/utils/options.js +37 -2
- package/dist/utils/schema.js +25 -3
- package/dist/utils/tickets.d.ts +28 -0
- package/dist/utils/tickets.js +288 -41
- 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 +1 -1
- package/docs/rules/require-ticket.md +25 -3
- package/docs/upgrading.md +2 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.0] - 2026-10-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Ticket presets for more trackers: `trello`, `notion`, `asana`, `clickup`, `redmine`, `monday`,
|
|
14
|
+
`youtrack`, `basecamp` and `shortcut`. Each accepts its tracker's ticket keys and links to one ticket,
|
|
15
|
+
and rejects links to boards, projects, views and other sites. `prefixes` limits the keys Notion,
|
|
16
|
+
ClickUp and monday.com accept, and `projects` the YouTrack projects; `host` sets a custom domain for
|
|
17
|
+
Notion, a server for YouTrack, and the server whose links Redmine accepts.
|
|
18
|
+
- A `url` preset for any other tracker: it accepts links to the `hosts` you list, and `*.example.com`
|
|
19
|
+
matches subdomains.
|
|
20
|
+
- The `azure-devops` preset accepts `#123`, which Azure Repos links to work item 123 in commit messages
|
|
21
|
+
and pull requests.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A link whose title ends in `-0`, as in `https://linear.app/acme/issue/ENG-1/upgrade-to-node-0`, was
|
|
26
|
+
reported as a placeholder ticket. Only the ticket's own ID makes it a placeholder now, which also
|
|
27
|
+
catches links where a title, suffix or query follows a zero ID, as in
|
|
28
|
+
`https://linear.app/acme/issue/ENG-0/fix-rounding`.
|
|
29
|
+
- `host` takes `*.example.com` to match every subdomain, as in `{ preset: 'jira', host: '*.atlassian.net' }`.
|
|
30
|
+
Before, such a host matched no link.
|
|
31
|
+
- `projects: []` (Jira) and `teams: []` (Linear) are config errors. An empty list rejected every key, and
|
|
32
|
+
the messages read "in project ,".
|
|
33
|
+
- Links whose host ends in a dot, as in `https://dev.azure.com./acme/web/_workitems/edit/1`, are read like
|
|
34
|
+
the same link without it.
|
|
35
|
+
|
|
36
|
+
## [0.9.0] - 2026-10-27
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Jasmine support. `configs.jasmine`, or `configure({ framework: 'jasmine' })`, checks tests run by
|
|
41
|
+
Jasmine on its own, on Karma with karma-jasmine (as Angular's karma builder sets up) or by WebdriverIO's
|
|
42
|
+
Jasmine framework: the globals `it`, `xit`, `fit`, `describe`, `xdescribe` and `fdescribe`, or the same
|
|
43
|
+
names from `jasmine-core`. `xit` and `xdescribe` skip a test, and `it('title')` with no function is a
|
|
44
|
+
todo. `pending()` and Jasmine 7's `notApplicable()` skip the running test in a test, `beforeEach` or
|
|
45
|
+
`afterEach`; like `t.skip('reason')`, `pending('reason')` needs a ticket unless a condition guards it.
|
|
46
|
+
`testFunctions` defaults to `['it']`, since Jasmine has no `test`.
|
|
47
|
+
|
|
9
48
|
## [0.8.1] - 2026-10-22
|
|
10
49
|
|
|
11
50
|
### Fixed
|
|
@@ -357,7 +396,9 @@ First release.
|
|
|
357
396
|
- Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
|
|
358
397
|
fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
|
|
359
398
|
|
|
360
|
-
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.
|
|
399
|
+
[Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.10.0...HEAD
|
|
400
|
+
[0.10.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.9.0...v0.10.0
|
|
401
|
+
[0.9.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.8.1...v0.9.0
|
|
361
402
|
[0.8.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.8.0...v0.8.1
|
|
362
403
|
[0.8.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.7.0...v0.8.0
|
|
363
404
|
[0.7.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.6.2...v0.7.0
|
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, WebdriverIO
|
|
7
|
+
ESLint rules that keep a Playwright, Jest, Vitest, Mocha, Cypress, WebdriverIO, `node:test` or Jasmine 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`, `configs.webdriverio
|
|
39
|
-
Node's built-in runner
|
|
38
|
+
`configs.vitest`, `configs.mocha`, `configs.cypress`, `configs.webdriverio`, `configs['node-test']` for
|
|
39
|
+
Node's built-in runner or `configs.jasmine`, and point it at those test files with `files`:
|
|
40
40
|
|
|
41
41
|
```js
|
|
42
42
|
// eslint.config.js
|
|
@@ -53,16 +53,17 @@ export default [
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
There is no default config. Each framework declares, skips and tags tests its own way, so the rules
|
|
56
|
-
need to know which one a file uses. In a Jest, Vitest or
|
|
57
|
-
to
|
|
58
|
-
way the Jest, Vitest and Mocha ESLint plugins are set up. `node:test` has no globals, so in its config
|
|
56
|
+
need to know which one a file uses. In a Jest, Vitest, Mocha or Jasmine config, the framework's functions,
|
|
57
|
+
such as `it` and `describe`, belong to it, whether they are globals or imported from `@jest/globals`,
|
|
58
|
+
`vitest`, `mocha` or `jasmine-core`, the way the Jest, Vitest and Mocha ESLint plugins are set up. `node:test` has no globals, so in its config
|
|
59
59
|
they are the names imported or required from `node:test`, and `test` and `it` from `testFunctions`.
|
|
60
60
|
|
|
61
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`configs
|
|
62
|
+
same calls as `configs.mocha`. `webdriverio` means WebdriverIO's Mocha framework; for its Jasmine
|
|
63
|
+
framework, use `configs.jasmine`. Karma runs Jasmine or Mocha tests, so use `configs.jasmine` with
|
|
64
|
+
karma-jasmine, which Angular's karma builder sets up, and `configs.mocha` with karma-mocha. Puppeteer has
|
|
65
|
+
no test functions of its own, so use the config of the runner its tests run on: `configs.jest` for
|
|
66
|
+
jest-puppeteer, or `configs.vitest`, `configs.mocha`, `configs['node-test']` or `configs.jasmine`.
|
|
66
67
|
|
|
67
68
|
With ESLint's `defineConfig()` (ESLint 9.22 or later), put the config in `extends`:
|
|
68
69
|
|
|
@@ -133,26 +134,30 @@ export default [
|
|
|
133
134
|
];
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
This matters most for Jasmine next to Jest or Mocha, whose `it`, `xit` and `describe` look the same:
|
|
138
|
+
only the `files` of each config tell them apart.
|
|
139
|
+
|
|
136
140
|
Each framework's config, such as `configs.playwright`, sets only the framework, so one listed after a
|
|
137
141
|
`configure()` for the same files keeps that config's options and changes only the framework. They all
|
|
138
142
|
turn on the same rules. What puts a test in a state in each framework:
|
|
139
143
|
|
|
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 |
|
|
144
|
+
| State (marker) | Playwright | Jest | Vitest | Mocha | Cypress | WebdriverIO | node-test | Jasmine |
|
|
145
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
146
|
+
| `skip` (`// SKIP:`) | `test.skip(...)`, `test.describe.skip`, `test.skip()` in a body, `testInfo.skip()`, `test.step.skip`, `step.skip()` | `test.skip`, `it.skip`, `describe.skip`, `xit`, `xtest`, `xdescribe` | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `skipIf(true)`, `runIf(false)`, `{ skip: true }`, `context.skip()`, `context.skip('note')` or `skip()` in a body or in `beforeEach` | `it.skip`, `specify.skip`, `describe.skip`, `context.skip`, `xit`, `xspecify`, `xdescribe`, `xcontext`, TDD `test.skip` and `suite.skip`, `this.skip()` in a test or hook | as Mocha | as Mocha | `test.skip`, `it.skip`, `describe.skip`, `suite.skip`, `skip()`, `{ skip: true }` or `{ skip: 'reason' }`, `t.skip()`, `t.skip('reason')` or `this.skip()` in a test, `beforeEach` or `afterEach` | `xit`, `xdescribe`, `pending()`, `pending('reason')` or `notApplicable('reason')` in a test, `beforeEach` or `afterEach` |
|
|
147
|
+
| `todo` (`// TODO:`) | | `test.todo`, `it.todo` | `test.todo`, `describe.todo`, `{ todo: true }` | a test with only a title, `it('title')`, which Mocha runs as pending | as Mocha | as Mocha | `test.todo`, `it.todo`, `describe.todo`, `suite.todo`, `todo()`, `{ todo: true }` or `{ todo: 'reason' }`, `t.todo()`, `this.todo()` | a test with only a title, `it('title')`, which Jasmine reports as pending |
|
|
148
|
+
| `fixme` (`// FIXME:`) | `test.fixme(...)`, `test.describe.fixme`, `testInfo.fixme()` | | | | | | | |
|
|
149
|
+
| `fail` (`// FAIL:`), off by default | `test.fail(...)`, `testInfo.fail()` | `test.failing` | `test.fails`, `{ fails: true }` | | | | `test.expectFailure`, `expectFailure()`, `{ expectFailure }` | |
|
|
150
|
+
| `slow` (`// SLOW:`), off by default | `test.slow()`, `testInfo.slow()` | | | | | | | |
|
|
151
|
+
| conditional skip, no ticket by default | `test.skip(condition)`, `if (...) test.skip()`, `(condition ? test.skip : test)(...)` | `(condition ? it.skip : it)(...)` | `skipIf(condition)`, `runIf(condition)`, `{ skip: condition }`, `context.skip(condition)`, `if (...) context.skip()`, `(condition ? it.skip : it)(...)` | `if (...) this.skip()`, `(condition ? it.skip : it)(...)` | as Mocha, and `{ browser: '!webkit' }` | as Mocha | `{ skip: condition }`, `{ todo: condition }`, `if (...) t.skip()`, `(condition ? it.skip : it)(...)` | `if (...) pending()`, `(condition ? xit : it)(...)` |
|
|
152
|
+
| 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 | the title |
|
|
149
153
|
|
|
150
154
|
Table-driven tests count too: `test.skip.each(table)(...)`, `describe.skip.each` and Vitest's `.for`.
|
|
151
155
|
A test or describe whose function a condition picks, as in `(isMobile ? test.skip : test)('title', fn)`,
|
|
152
156
|
is a conditional skip in every framework.
|
|
153
157
|
A literal condition decides on its own: `skipIf(true)` and `runIf(false)` always skip, and `skipIf(false)`,
|
|
154
158
|
`runIf(true)` and `{ skip: false }` never do.
|
|
155
|
-
In Vitest, Mocha and `node:test`, `test.skip('title')` and `describe.skip('title')` with no body are skipped too
|
|
159
|
+
In Vitest, Mocha and `node:test`, `test.skip('title')` and `describe.skip('title')` with no body are skipped too,
|
|
160
|
+
as is `xit('title')` in Jasmine.
|
|
156
161
|
In Mocha, `this.skip()` counts in a test or hook written as a `function`; an arrow function gets no
|
|
157
162
|
Mocha context, so its `this` is not the test's. `this.slow(ms)` only sets Mocha's speed threshold, so
|
|
158
163
|
it isn't the `slow` state.
|
|
@@ -167,6 +172,10 @@ A test with no function, `test('title')`, passes in Node, so it isn't a todo. A
|
|
|
167
172
|
`before` and `after` inside it, get the suite's context, which can't skip. Node reports a test in one
|
|
168
173
|
state only, skip before todo before `expectFailure`, so `describe.skip('title', { todo: 'why' }, fn)`
|
|
169
174
|
needs only a `// SKIP:` marker.
|
|
175
|
+
In Jasmine, `pending()` and Jasmine 7's `notApplicable()` stop the running test, so they count in a test,
|
|
176
|
+
`beforeEach` or `afterEach`; in `beforeAll`, `afterAll` or a describe's body Jasmine fails the suite
|
|
177
|
+
instead, so they are left alone there. Like `t.skip('reason')`, `pending('reason')` takes a reason, not a
|
|
178
|
+
condition. A function or variable of your own called `pending` is not Jasmine's.
|
|
170
179
|
|
|
171
180
|
## Rules
|
|
172
181
|
|
|
@@ -175,6 +184,7 @@ needs only a `// SKIP:` marker.
|
|
|
175
184
|
💼 [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) enabled in.\
|
|
176
185
|
⚠️ [Configurations](https://github.com/Huldoser/eslint-plugin-test-governance#usage) set to warn in.\
|
|
177
186
|
🌲 Set in the `cypress` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
187
|
+
🌼 Set in the `jasmine` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
178
188
|
🃏 Set in the `jest` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
179
189
|
☕ Set in the `mocha` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
180
190
|
🟢 Set in the `node-test` [configuration](https://github.com/Huldoser/eslint-plugin-test-governance#usage).\
|
|
@@ -184,13 +194,13 @@ needs only a `// SKIP:` marker.
|
|
|
184
194
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
185
195
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
186
196
|
|
|
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 | 🌲 🃏 ☕ 🟢 🎭 ⚡ 🤖 |
|
|
197
|
+
| Name | Description | 💼 | ⚠️ | 🔧 | 💡 |
|
|
198
|
+
| :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------- | :---------------------- | :---------------------- | :-- | :-- |
|
|
199
|
+
| [marker-matches-state](docs/rules/marker-matches-state.md) | Require the marker keyword to match the test's state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
200
|
+
| [no-conflicting-states](docs/rules/no-conflicting-states.md) | Disallow state tags that contradict each other, differ in case or look like typos | | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | 🔧 | |
|
|
201
|
+
| [no-orphaned-marker](docs/rules/no-orphaned-marker.md) | Disallow marker comments that no longer match a test state | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
202
|
+
| [require-ticket](docs/rules/require-ticket.md) | Require a ticket marker comment above skipped, fixme and tagged tests | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
203
|
+
| [require-ticket-in-comments](docs/rules/require-ticket-in-comments.md) | Require FIXME and TODO comments in test files to start with a ticket | 🌲 🌼 🃏 ☕ 🟢 🎭 ⚡ 🤖 | | | 💡 |
|
|
194
204
|
|
|
195
205
|
<!-- end auto-generated rules list -->
|
|
196
206
|
|
|
@@ -220,20 +230,20 @@ yourself. `framework` is required, and every other option has a default. The rul
|
|
|
220
230
|
of their own: they all read these shared settings, so they always agree on ticket formats, states
|
|
221
231
|
and markers. To turn a rule off, set it to `'off'` as usual.
|
|
222
232
|
|
|
223
|
-
| Option | Type
|
|
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[]`
|
|
227
|
-
| `placeholders` | `string[]`
|
|
228
|
-
| `lifecycleTags` | `boolean`
|
|
229
|
-
| `states` | `{ [state]: boolean \| { enabled?, tag?, marker?, ticket? } }`
|
|
230
|
-
| `customStates` | `{ [name]: { when, marker, ticket? } }`
|
|
231
|
-
| `requireTicketForConditional` | `boolean`
|
|
232
|
-
| `allowBlankLine` | `boolean`
|
|
233
|
-
| `allowNotes` | `boolean`
|
|
234
|
-
| `comments` | `false \| { keywords?: string[] }`
|
|
235
|
-
| `testFunctions` | `string[]`
|
|
236
|
-
| `reportUnreadableTags` | `boolean`
|
|
233
|
+
| Option | Type | Default | Description |
|
|
234
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
235
|
+
| `framework` | `'playwright' \| 'jest' \| 'vitest' \| 'mocha' \| 'cypress' \| 'webdriverio' \| 'node-test' \| 'jasmine'` | required | The test framework of the files the config applies to. Each framework's config, such as `configs.playwright`, sets it, and `configure()` needs it. See [Frameworks](#frameworks). |
|
|
236
|
+
| `ticket` | `TicketSpec \| TicketSpec[]` | `{ preset: 'any' }` | Accepted ticket formats. An array accepts a ticket that matches any entry. See [Ticket presets](#ticket-presets). |
|
|
237
|
+
| `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. |
|
|
238
|
+
| `lifecycleTags` | `boolean` | `false` | Turns on the `new` (`@new`) and `quarantined` (`@quarantined`) states. |
|
|
239
|
+
| `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. |
|
|
240
|
+
| `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. |
|
|
241
|
+
| `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`. |
|
|
242
|
+
| `allowBlankLine` | `boolean` | `false` | Let blank lines separate a marker from its test. |
|
|
243
|
+
| `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. |
|
|
244
|
+
| `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. |
|
|
245
|
+
| `testFunctions` | `string[]` | `['test']` | Names treated as the framework's `test`. Jest and Vitest default to `['test', 'it']`, Mocha, Cypress and WebdriverIO to `['it', 'specify', 'test']`, and Jasmine to `['it']`. Your list replaces the default one, so include those names if you still need them. See below. |
|
|
246
|
+
| `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. |
|
|
237
247
|
|
|
238
248
|
### Tag names
|
|
239
249
|
|
|
@@ -264,21 +274,45 @@ a `customStates` entry.
|
|
|
264
274
|
|
|
265
275
|
### Ticket presets
|
|
266
276
|
|
|
267
|
-
| Preset | Accepts
|
|
268
|
-
| -------------- |
|
|
269
|
-
| `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL
|
|
270
|
-
| `jira` | `TRADE-123`, `https://<host>/browse/TRADE-123` (also under a path, as in `/jira/browse/TRADE-123`), board URLs with `selectedIssue=TRADE-123`
|
|
271
|
-
| `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`)
|
|
272
|
-
| `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123`
|
|
273
|
-
| `linear` | `RISK-123`, `https://linear.app/<workspace>/issue/RISK-123/...`
|
|
274
|
-
| `azure-devops` | `AB#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123`
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
|
|
278
|
-
`
|
|
279
|
-
`
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
| Preset | Accepts | Options |
|
|
278
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
279
|
+
| `any` | `PROJ-123`, `#123`, `owner/repo#123`, any http(s) URL | |
|
|
280
|
+
| `jira` | `TRADE-123`, `https://<host>/browse/TRADE-123` (also under a path, as in `/jira/browse/TRADE-123`), board URLs with `selectedIssue=TRADE-123` | `projects`; `host` (any host by default) |
|
|
281
|
+
| `github` | `#123`, `owner/repo#123`, `https://github.com/owner/repo/issues/123` (or `/pull/`) | `host` for GitHub Enterprise (`github.com` by default) |
|
|
282
|
+
| `gitlab` | `#123`, `group/project#123`, `https://gitlab.com/group/project/-/issues/123` | `host` for self-managed (`gitlab.com` by default) |
|
|
283
|
+
| `linear` | `RISK-123`, `https://linear.app/<workspace>/issue/RISK-123/...` | `teams` |
|
|
284
|
+
| `azure-devops` | `AB#123`, `#123`, `https://dev.azure.com/<org>/<project>/_workitems/edit/123` | `host` for Azure DevOps Server (`dev.azure.com` and `*.visualstudio.com` by default) |
|
|
285
|
+
| `trello` | card links, `https://trello.com/c/nqPiDKmw` with or without the `/9-title` after it | |
|
|
286
|
+
| `notion` | Unique IDs such as `TASK-123`, `https://www.notion.so/TASK-123`, page links that end in the page ID (also `https://app.notion.com/p/...`), database links with `?p=<page ID>` | `prefixes`; `host` for a custom domain (`notion.so`, `www.notion.so`, `app.notion.com` and `*.notion.site` by default) |
|
|
287
|
+
| `asana` | task links, `https://app.asana.com/0/<project>/<task>` and the newer `https://app.asana.com/1/<workspace>/.../task/<task>` | |
|
|
288
|
+
| `clickup` | `CU-86abc123`, `#86abc123`, custom IDs such as `DEV-123`, `https://app.clickup.com/t/86abc123` or `/t/<workspace>/DEV-123`, view links with `?p=<task>` | `prefixes` for custom IDs |
|
|
289
|
+
| `redmine` | `#123`, `#123-4` and `#123#note-4` (a note), `##123`; with `host`, `https://<host>/issues/123` (also under a path) | `host`, needed for links |
|
|
290
|
+
| `monday` | item links, `https://<account>.monday.com/boards/<board>/pulses/<item>`, and custom keys such as `TIX-001` | `prefixes` for custom keys |
|
|
291
|
+
| `youtrack` | `KT-123`, `https://<host>/issue/KT-123/...` or `/projects/KT/issues/KT-123` (also under a path, as in `/youtrack/issue/KT-123`) | `projects`; `host` for Server or a custom domain (`youtrack.jetbrains.com`, `*.youtrack.cloud` and `*.myjetbrains.com` by default) |
|
|
292
|
+
| `basecamp` | to-do and card links, `https://3.basecamp.com/<account>/buckets/<project>/todos/<id>` or `/card_tables/cards/<id>` | |
|
|
293
|
+
| `shortcut` | `sc-123`, `[sc-123]`, `https://app.shortcut.com/<workspace>/story/123/...` | |
|
|
294
|
+
| `url` | links to the hosts you list, and nothing else | `hosts`, required: host names such as `tasks.example.com`, with no scheme, port or path; `*.example.com` matches its subdomains |
|
|
295
|
+
| `numeric` | `4821` | `minLength` (1), `maxLength` (20) |
|
|
296
|
+
| `pattern` | anything matching your regex, anchored at both ends | `pattern`, `flags` |
|
|
297
|
+
|
|
298
|
+
`projects` (Jira, YouTrack), `teams` (Linear) and `prefixes` (Notion, ClickUp, monday.com) are keys as
|
|
299
|
+
they appear in tickets, in uppercase: `TRADE` for `TRADE-123`. YouTrack project IDs may also start with a digit, as `4DM`. Without them, any key passes. `host` is a host name such as `jira.example.com`, or
|
|
300
|
+
`*.example.com` for every subdomain, as in `{ preset: 'jira', host: '*.atlassian.net' }`. Each preset
|
|
301
|
+
takes only its own options, so `host` on the `any` preset fails with a clear message instead of being
|
|
302
|
+
ignored. A `pattern` always matches the whole ticket, so the `g` and `y` flags make no difference and are
|
|
303
|
+
ignored.
|
|
304
|
+
|
|
305
|
+
A preset rejects links that aren't one ticket in its tracker, such as a Trello board, an Asana project or
|
|
306
|
+
a Notion database view. Redmine runs on your own server, and `/issues/123` is also GitHub's path, so its
|
|
307
|
+
links pass only once `host` is set.
|
|
308
|
+
|
|
309
|
+
For a tracker without a preset, use `url` with its host, for example
|
|
310
|
+
`{ preset: 'url', hosts: ['www.wrike.com'] }` for Wrike links such as
|
|
311
|
+
`https://www.wrike.com/open.htm?id=4821`. It works the same for Microsoft Planner (`tasks.office.com`),
|
|
312
|
+
Airtable (`airtable.com`), Smartsheet (`app.smartsheet.com`) and trackers of your own. For another
|
|
313
|
+
tracker's keys, such as `WEB-12`, `any` or `pattern` works. Gitea, Forgejo and Codeberg use GitHub's link layout,
|
|
314
|
+
so the `github` preset with their `host` covers them. Bitbucket has no issue tracker since August 2026,
|
|
315
|
+
so it has no preset.
|
|
282
316
|
|
|
283
317
|
Presets combine, and each state can have its own format. For example, bugs in Jira and flaky-test
|
|
284
318
|
reports in GitHub:
|
|
@@ -337,8 +371,17 @@ In `node:test` files:
|
|
|
337
371
|
- Subtests: `t.test(...)` on the context a test's body receives.
|
|
338
372
|
- Any name in `testFunctions`, which defaults to `['test', 'it']`. `node:test` has no globals.
|
|
339
373
|
|
|
374
|
+
In Jasmine files:
|
|
375
|
+
|
|
376
|
+
- The globals `it`, `xit`, `fit`, `describe`, `xdescribe`, `fdescribe`, and the hooks `beforeEach`,
|
|
377
|
+
`afterEach`, `beforeAll` and `afterAll`. With the runner's `globals: false`, Jasmine 7 takes the same
|
|
378
|
+
names from `jasmine-core`, so names imported or required from it count too, including aliases and
|
|
379
|
+
the whole module, as `core.xit` after `const core = require('jasmine-core')`.
|
|
380
|
+
- `pending()` and `notApplicable()`, as globals or imported by name.
|
|
381
|
+
- Any name in `testFunctions`, which defaults to `['it']`. Jasmine has no `test`.
|
|
382
|
+
|
|
340
383
|
A name imported or required from another test runner (Playwright, `vitest`, `@jest/globals`,
|
|
341
|
-
`node:test`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
|
|
384
|
+
`node:test`, `jasmine-core`, `bun:test`, `mocha`, `ava`, `tap`, `uvu`), or a variable set from one such as
|
|
342
385
|
`const test = require('ava')`, is never treated as the framework's `test`, and neither is a local
|
|
343
386
|
variable or function declared inside a function that happens to be called `test`.
|
|
344
387
|
|
|
@@ -366,8 +409,9 @@ Not covered in this version: tests generated in loops with dynamic titles, and t
|
|
|
366
409
|
from other modules (see `reportUnreadableTags`), fixtures imported from other modules under a name not in
|
|
367
410
|
`testFunctions`, a Vitest test with only a title, which Vitest runs as a todo but which looks the same as a
|
|
368
411
|
helper called `test`, Playwright annotations as markers, Cypress tests skipped with @cypress/skip-test's
|
|
369
|
-
`skipOn()` and `onlyOn()`, `node:test`'s experimental `tags` option and `getTestContext()`,
|
|
370
|
-
|
|
412
|
+
`skipOn()` and `onlyOn()`, `node:test`'s experimental `tags` option and `getTestContext()`, Jasmine's
|
|
413
|
+
`pending()` in a helper function that tests call, Jasmine 5 and 6's `require('jasmine-core').noGlobals()`,
|
|
414
|
+
and other runners such as Bun.
|
|
371
415
|
|
|
372
416
|
## TypeScript
|
|
373
417
|
|
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" | "node-test", FlatConfig>;
|
|
36
|
+
export declare const configs: Record<"playwright" | "jest" | "vitest" | "mocha" | "cypress" | "webdriverio" | "node-test" | "jasmine", 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, configs.vitest, configs.mocha, configs.cypress, configs.webdriverio
|
|
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, configs['node-test'] or configs.jasmine. 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;
|
package/dist/utils/analyze.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface Subject {
|
|
|
73
73
|
ownSkip: boolean;
|
|
74
74
|
markers: Marker[];
|
|
75
75
|
dynamicTitle: boolean;
|
|
76
|
+
/** For a runtime function such as Jasmine's `pending()`, its name; messages call the others by their state. */
|
|
77
|
+
functionName?: string;
|
|
76
78
|
}
|
|
77
79
|
export interface Analysis {
|
|
78
80
|
subjects: Subject[];
|
package/dist/utils/analyze.js
CHANGED
|
@@ -82,6 +82,7 @@ function collectTestNames(program, framework, configured) {
|
|
|
82
82
|
const roots = new Map(framework.globals);
|
|
83
83
|
for (const name of configured)
|
|
84
84
|
roots.set(name, TEST);
|
|
85
|
+
const runtime = new Map();
|
|
85
86
|
const modules = new Set();
|
|
86
87
|
const mergeTests = new Set();
|
|
87
88
|
/** Names bound to another test runner, such as `ava` in `import ava from 'ava'`. */
|
|
@@ -123,6 +124,12 @@ function collectTestNames(program, framework, configured) {
|
|
|
123
124
|
roots.delete(name);
|
|
124
125
|
others.add(name);
|
|
125
126
|
};
|
|
127
|
+
/** Takes `name` for the runtime function `imported`, as in `const { pending: pend } = require('jasmine-core')`. */
|
|
128
|
+
const bindRuntime = (name, imported) => {
|
|
129
|
+
const modifier = imported === undefined ? undefined : framework.runtimeFunctions.get(imported);
|
|
130
|
+
if (imported !== undefined && modifier !== undefined)
|
|
131
|
+
runtime.set(name, { name: imported, modifier });
|
|
132
|
+
};
|
|
126
133
|
for (const statement of program.body) {
|
|
127
134
|
if (statement.type === 'ImportDeclaration') {
|
|
128
135
|
const source = statement.source.value;
|
|
@@ -144,6 +151,7 @@ function collectTestNames(program, framework, configured) {
|
|
|
144
151
|
}
|
|
145
152
|
else {
|
|
146
153
|
const root = framework.exports.get(importedName(spec));
|
|
154
|
+
bindRuntime(local, importedName(spec));
|
|
147
155
|
if (root)
|
|
148
156
|
roots.set(local, root);
|
|
149
157
|
else if (importedName(spec) === 'mergeTests')
|
|
@@ -200,6 +208,7 @@ function collectTestNames(program, framework, configured) {
|
|
|
200
208
|
setOther(prop.value.name);
|
|
201
209
|
else if (framework.ownsModule(source)) {
|
|
202
210
|
const root = framework.exports.get(propertyName(prop) ?? '');
|
|
211
|
+
bindRuntime(prop.value.name, propertyName(prop));
|
|
203
212
|
if (root)
|
|
204
213
|
roots.set(prop.value.name, root);
|
|
205
214
|
}
|
|
@@ -207,7 +216,7 @@ function collectTestNames(program, framework, configured) {
|
|
|
207
216
|
}
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
|
-
return { roots, modules };
|
|
219
|
+
return { roots, modules, runtime };
|
|
211
220
|
}
|
|
212
221
|
/** The name a property key spells out: `test` in `{ test }`, `{ 'test': t }` or `{ ['test']: t }`. */
|
|
213
222
|
function propertyName(prop) {
|
|
@@ -225,6 +234,21 @@ function requireSource(node) {
|
|
|
225
234
|
const first = node.arguments.at(0);
|
|
226
235
|
return first?.type === 'Literal' && typeof first.value === 'string' ? first.value : undefined;
|
|
227
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Where the file declares what `id` refers to: `inside` a function or block, a parameter included, at the
|
|
239
|
+
* `top` level, or nowhere, for a `global`.
|
|
240
|
+
*/
|
|
241
|
+
function declaredWhere(sourceCode, id) {
|
|
242
|
+
for (let scope = sourceCode.getScope(id); scope; scope = scope.upper) {
|
|
243
|
+
const variable = scope.set.get(id.name);
|
|
244
|
+
if (!variable)
|
|
245
|
+
continue;
|
|
246
|
+
if (scope.block.type !== 'Program')
|
|
247
|
+
return 'inside';
|
|
248
|
+
return variable.defs.length > 0 ? 'top' : 'global';
|
|
249
|
+
}
|
|
250
|
+
return 'global';
|
|
251
|
+
}
|
|
228
252
|
/**
|
|
229
253
|
* Whether `id` refers to a variable or function declared inside a function or block, such as a local
|
|
230
254
|
* helper that happens to be called `test`. Top-level variables, imports, parameters and globals still count.
|
|
@@ -816,14 +840,31 @@ function runAnalysis(sourceCode, options) {
|
|
|
816
840
|
if (body?.type === 'FunctionExpression')
|
|
817
841
|
thisBodies.add(body);
|
|
818
842
|
};
|
|
819
|
-
/**
|
|
843
|
+
/**
|
|
844
|
+
* What a body can change the test's state with: Playwright's `testInfo`, Vitest's test context, or
|
|
845
|
+
* Jasmine's `pending()`.
|
|
846
|
+
*/
|
|
820
847
|
const bodyInfos = (args, path) => {
|
|
821
848
|
const body = args.findLast(isFunction);
|
|
822
|
-
|
|
823
|
-
if (!framework.info || !body || path.includes('each()'))
|
|
849
|
+
if (!body)
|
|
824
850
|
return [];
|
|
851
|
+
const functions = [
|
|
852
|
+
...[...framework.runtimeFunctions].map(([name, modifier]) => ({
|
|
853
|
+
name,
|
|
854
|
+
method: modifier,
|
|
855
|
+
runtimeFunction: { name, imported: false },
|
|
856
|
+
})),
|
|
857
|
+
...[...testNames.runtime].map(([local, fn]) => ({
|
|
858
|
+
name: local,
|
|
859
|
+
method: fn.modifier,
|
|
860
|
+
runtimeFunction: { name: fn.name, imported: true },
|
|
861
|
+
})),
|
|
862
|
+
];
|
|
863
|
+
// `test.each(table)` passes the row to the body, and `test.for(cases)` passes it before the context.
|
|
864
|
+
if (!framework.info || path.includes('each()'))
|
|
865
|
+
return functions;
|
|
825
866
|
const param = body.params.at(framework.info.param + (path.includes('for()') ? 1 : 0));
|
|
826
|
-
return infoNames(param, framework.info.methods);
|
|
867
|
+
return [...functions, ...infoNames(param, framework.info.methods)];
|
|
827
868
|
};
|
|
828
869
|
/**
|
|
829
870
|
* What a callee declares and with which members: `describe` and `['skip']` for `test.describe.skip`.
|
|
@@ -834,12 +875,16 @@ function runAnalysis(sourceCode, options) {
|
|
|
834
875
|
return undefined;
|
|
835
876
|
const playwrightDescribe = framework.playwright && path[0] === 'describe';
|
|
836
877
|
const kind = playwrightDescribe ? 'describe' : root.kind;
|
|
837
|
-
|
|
878
|
+
const written = playwrightDescribe ? path.slice(1) : path;
|
|
879
|
+
return { kind, members: [...root.modifiers, ...written], written, args };
|
|
838
880
|
};
|
|
839
|
-
/**
|
|
840
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Whether a call with these members and arguments declares a test or describe of this kind. Only the
|
|
883
|
+
* members written after the name must exist: Jasmine's `xit` skips, but it has no `it.skip`.
|
|
884
|
+
*/
|
|
885
|
+
const declares = ({ kind, members, written }, args) => {
|
|
841
886
|
const allowed = kind === 'test' ? framework.testMembers : framework.describeMembers;
|
|
842
|
-
return
|
|
887
|
+
return written.every((m) => allowed.has(m)) && isDeclaration(sourceCode, framework, kind, args, members);
|
|
843
888
|
};
|
|
844
889
|
/**
|
|
845
890
|
* `(isCI ? describe.skip : describe)('order entry', ...)`: a test or describe whose function is picked by
|
|
@@ -893,7 +938,7 @@ function runAnalysis(sourceCode, options) {
|
|
|
893
938
|
const { object } = callee;
|
|
894
939
|
if (object.type !== 'Identifier' || !infos.findLast((info) => info.name === object.name)?.methods)
|
|
895
940
|
return undefined;
|
|
896
|
-
const declaration = { kind: 'test', members: [], args: new Map() };
|
|
941
|
+
const declaration = { kind: 'test', members: [], written: [], args: new Map() };
|
|
897
942
|
return declares(declaration, call.arguments)
|
|
898
943
|
? declare(call, declaration, bodyInfos(call.arguments, []))
|
|
899
944
|
: undefined;
|
|
@@ -938,7 +983,7 @@ function runAnalysis(sourceCode, options) {
|
|
|
938
983
|
};
|
|
939
984
|
/**
|
|
940
985
|
* `testInfo.skip()`, `step.skip()`, `test.info().skip()`, Vitest's `context.skip()` or `skip()`,
|
|
941
|
-
* Mocha's `this.skip()`,
|
|
986
|
+
* Mocha's `this.skip()`, node:test's `t.skip()` and `t.todo()`, and Jasmine's `pending()`.
|
|
942
987
|
*/
|
|
943
988
|
const runtimeCall = (call) => {
|
|
944
989
|
const { callee } = call;
|
|
@@ -946,7 +991,17 @@ function runAnalysis(sourceCode, options) {
|
|
|
946
991
|
const innermost = (name) => infos.findLast((info) => info.name === name);
|
|
947
992
|
if (callee.type === 'Identifier') {
|
|
948
993
|
const info = innermost(callee.name);
|
|
949
|
-
|
|
994
|
+
if (info?.method === undefined)
|
|
995
|
+
return undefined;
|
|
996
|
+
// A global such as Jasmine's `pending` counts unless the file declares the name itself; an import of
|
|
997
|
+
// one counts unless a function or block declares the name again.
|
|
998
|
+
const fn = info.runtimeFunction;
|
|
999
|
+
if (fn && declaredWhere(sourceCode, callee) !== (fn.imported ? 'top' : 'global'))
|
|
1000
|
+
return undefined;
|
|
1001
|
+
const subject = runtimeSubject(call, info.method, info.step);
|
|
1002
|
+
if (subject)
|
|
1003
|
+
subject.functionName = fn?.name;
|
|
1004
|
+
return subject;
|
|
950
1005
|
}
|
|
951
1006
|
if (callee.type !== 'MemberExpression')
|
|
952
1007
|
return undefined;
|
|
@@ -1105,6 +1160,6 @@ export function describeSubject(subject) {
|
|
|
1105
1160
|
if (subject.kind === 'step')
|
|
1106
1161
|
return 'This step';
|
|
1107
1162
|
if (subject.kind === 'runtime')
|
|
1108
|
-
return `This ${subject.states[0].state.modifier} call`;
|
|
1163
|
+
return `This ${subject.functionName ?? subject.states[0].state.modifier} call`;
|
|
1109
1164
|
return 'This test';
|
|
1110
1165
|
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
export declare const MARKER_RE: RegExp;
|
|
3
3
|
/** A tag as written in options: `@` followed by letters, digits, `-` and `_`. */
|
|
4
4
|
export declare const TAG_RE: RegExp;
|
|
5
|
+
/** A host with no scheme, port or path: a host name, or `*.` and a domain of two or more labels for its subdomains. */
|
|
6
|
+
export declare const HOST_ENTRY_RE: RegExp;
|
|
5
7
|
export declare const BUILTIN_STATE_NAMES: readonly ["skip", "fixme", "fail", "slow", "todo", "new", "quarantined"];
|
|
6
8
|
export type BuiltinStateName = (typeof BUILTIN_STATE_NAMES)[number];
|
|
7
9
|
/** The built-in states a tag puts a test in, so the only ones whose tag can be renamed. */
|
|
8
10
|
export declare const TAG_STATE_NAMES: readonly ["new", "quarantined"];
|
|
9
11
|
export type TagStateName = (typeof TAG_STATE_NAMES)[number];
|
|
10
12
|
/** The test frameworks the rules understand. */
|
|
11
|
-
export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest", "mocha", "cypress", "webdriverio", "node-test"];
|
|
13
|
+
export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest", "mocha", "cypress", "webdriverio", "node-test", "jasmine"];
|
|
12
14
|
export type FrameworkName = (typeof FRAMEWORK_NAMES)[number];
|
|
13
|
-
export declare const TICKET_PRESETS: readonly ["any", "jira", "github", "gitlab", "linear", "azure-devops", "numeric", "pattern"];
|
|
15
|
+
export declare const TICKET_PRESETS: readonly ["any", "jira", "github", "gitlab", "linear", "azure-devops", "trello", "notion", "asana", "clickup", "redmine", "monday", "youtrack", "basecamp", "shortcut", "url", "numeric", "pattern"];
|
|
14
16
|
/** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
|
|
15
17
|
export declare const DEFAULT_COMMENT_KEYWORDS: string[];
|