eslint-plugin-test-governance 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,77 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.13.3] - 2026-11-09
10
+
11
+ ### Fixed
12
+
13
+ - Vitest conditional factories can clear an earlier skip in a modifier chain. Runtime context methods
14
+ retain their state through TypeScript wrappers on the receiver or method.
15
+ - Mutation detection follows wrapped references and aliases, and recognizes writes through the global
16
+ `Object` and `Reflect` mutation methods without treating shadowed helpers as those built-ins.
17
+ - Retry counts are read through local constants and object members, with effective properties after
18
+ spreads and overrides. Unread spreads remain possible retries unless every relevant count is known.
19
+ - Commented-out tests no longer hide a broken marker or a disallowed note behind another valid marker.
20
+ - Negative numeric and bigint template interpolations, `void`, and unshadowed `undefined`, `NaN` and
21
+ `Infinity` retain their rendered text when checking tag boundaries.
22
+ - Mutable constant objects and arrays no longer supply stale tag or annotation values after detected
23
+ property writes, deletion, array mutation or writes through a local alias.
24
+ - Runtime context methods keep their original test owner inside nested subtests. Vitest skip notes
25
+ supplied through constants remain unconditional skips.
26
+ - Test callees retain their meaning through TypeScript assertions and type instantiations, sequence
27
+ expressions and defaulted local destructuring. Repeated conditional factories use the final condition.
28
+ - Nested conditional returns and throws, and conditional loop breaks and continues, guard later runtime
29
+ skips. Jest retry calls are recognized through CommonJS object bindings and method aliases, including
30
+ bound arguments.
31
+ - Primitive template interpolations retain their rendered text when checking tag boundaries. Object
32
+ prototype setters do not count as own tag or metadata properties, and suggestions use computed keys
33
+ for metadata named `__proto__`.
34
+ - Test functions are read through aliases of whole modules, local destructuring, bound functions and
35
+ nested conditional selections. Reassigning an alias to a helper no longer leaves it treated as a test.
36
+ - Runtime test contexts and their methods keep their meaning through chains of aliases, `bind()` and
37
+ default parameter values. Arguments supplied by `bind()` retain their condition and reason, and a
38
+ bound title no longer gives a diagnostic whose end is before its start.
39
+ - A runtime skip in a conditional loop, or after a conditional early exit inside a nested block, is
40
+ read as conditional. A TypeScript assertion around a literal boolean keeps that boolean's meaning.
41
+ - With the retry state on, Jest's retry calls are read through renamed and namespace imports, and a
42
+ Mocha describe chosen by a condition still supplies its suite context for `this.retries()`.
43
+ - Chains of constants retain their tags, details and annotation tickets. An unread computed property
44
+ no longer leaves an earlier member of a tag object treated as certain.
45
+ - Tags from literal and nested template interpolations are checked using the rendered text's boundaries,
46
+ so joining text around an `@` doesn't invent a state tag. Plain Vitest tags written as templates count
47
+ like quoted strings. Cyclic constant and template definitions remain unread instead of recursing.
48
+ - `annotations.only` checks every supplied issue ticket, including an invalid one next to a valid one.
49
+ - The commented-out-test rule recognizes renamed test imports.
50
+ - Removing an orphaned marker removes its complete line with CR or Unicode line endings too.
51
+ - A suggestion to add another state's marker no longer copies a ticket that state's format rejects.
52
+ - Suggestions that move a ticket into annotations escape line breaks in the annotation type or key,
53
+ keeping the suggested JavaScript valid.
54
+
55
+ ## [0.13.2] - 2026-11-08
56
+
57
+ ### Fixed
58
+
59
+ - Local aliases such as `const check = it; check.skip(...)`, and aliases of conditional test functions,
60
+ are checked like the functions they keep.
61
+ - A helper's parameter or block variable named like a test context no longer makes its `skip()` or
62
+ `test()` calls count as runtime skips or subtests.
63
+ - Runtime calls with a literal `false` condition, such as Playwright's `test.skip(false)`, need neither
64
+ a ticket nor a reason.
65
+ - Details, options and annotations use the last value of a property, including values from object
66
+ spreads. An unread spread or computed key no longer leaves an earlier value treated as certain.
67
+ - A tag read beyond an array's bounds no longer crashes linting. An index after a spread, or a string
68
+ such as `'01'` that isn't an array index, is left unread instead of using the wrong tag.
69
+ - Circular object spreads no longer overflow the stack during analysis.
70
+ - Playwright annotation arrays read tickets from spreads of arrays declared in the same file, and
71
+ report unread spreads with `reportUnreadableTags`.
72
+ - A ticket marker in a block comment no longer hides unrelated `TODO` or `FIXME` work comments on
73
+ other lines of that block.
74
+ - The `pattern` preset matches the whole ticket even with the multiline flag, and no longer accepts
75
+ trailing line breaks outside the pattern.
76
+ - Moving a ticket into an annotation preserves comments inside an empty details object.
77
+ - node:test's `todo` state no longer makes `no-conflicting-states` call a new test skipped: Node still
78
+ runs its body.
79
+
9
80
  ## [0.13.1] - 2026-11-06
10
81
 
11
82
  ### Fixed
@@ -573,7 +644,9 @@ First release.
573
644
  - Support for ESLint 9 and 10 flat config, JavaScript and TypeScript test files, `.extend()`
574
645
  fixtures, describe blocks, runtime skips and tags in titles or the `tag` option.
575
646
 
576
- [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.1...HEAD
647
+ [Unreleased]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.3...HEAD
648
+ [0.13.3]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.2...v0.13.3
649
+ [0.13.2]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.1...v0.13.2
577
650
  [0.13.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.13.0...v0.13.1
578
651
  [0.13.0]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.12.1...v0.13.0
579
652
  [0.12.1]: https://github.com/Huldoser/eslint-plugin-test-governance/compare/v0.12.0...v0.12.1
@@ -70,7 +70,10 @@ export default createRule({
70
70
  state: state.name,
71
71
  subject: subjectName,
72
72
  },
73
- suggest: present.comment.type === 'Line' && tickets !== '' && directive?.kind !== 'eslint-disable-line'
73
+ suggest: present.comment.type === 'Line' &&
74
+ tickets !== '' &&
75
+ present.tickets.every((ticket) => state.ticket.check(ticket.text) === 'ok') &&
76
+ directive?.kind !== 'eslint-disable-line'
74
77
  ? [
75
78
  {
76
79
  messageId: 'addMarker',
@@ -32,9 +32,9 @@ export default createRule({
32
32
  context.report({ loc, messageId: 'missingMarker', data });
33
33
  continue;
34
34
  }
35
- const valid = own.find((m) => m.tickets.length > 0 && m.tickets.every((t) => t.result === 'ok'));
36
- if (valid) {
37
- const { extra } = valid;
35
+ const broken = own.find((m) => m.tickets.length === 0 || m.tickets.some((t) => t.result !== 'ok'));
36
+ if (broken === undefined) {
37
+ const extra = own.find((marker) => marker.extra !== undefined)?.extra;
38
38
  if (extra === undefined || options.allowNotes)
39
39
  continue;
40
40
  context.report({
@@ -51,7 +51,7 @@ export default createRule({
51
51
  });
52
52
  continue;
53
53
  }
54
- const [marker] = own;
54
+ const marker = broken;
55
55
  const bad = marker.tickets.find((t) => t.result !== 'ok');
56
56
  if (bad === undefined) {
57
57
  context.report({ loc: marker.comment.loc, messageId: 'missingTicket', data });
@@ -37,7 +37,7 @@ export default createRule({
37
37
  for (const marker of [...subject.markers, ...subject.regionMarkers]) {
38
38
  const state = marker.state ?? marker.caseOf;
39
39
  if (state && appliesTo(subject, state.name))
40
- owned.add(marker.comment);
40
+ owned.add(marker.keywordStart);
41
41
  }
42
42
  }
43
43
  // `TODO`/`FIXME` in capitals counts with or without a colon. Other casings need the colon, so a
@@ -62,13 +62,13 @@ export default createRule({
62
62
  return { ...parsed, extra: { text: note, range: [start + close, start + line.trimEnd().length] } };
63
63
  }
64
64
  for (const comment of sourceCode.getAllComments()) {
65
- if (owned.has(comment))
66
- continue;
67
65
  for (const { text: line, start } of commentLines(comment)) {
68
66
  const match = keywordRe.exec(line);
69
67
  if (!match)
70
68
  continue;
71
69
  const [whole, indent, keyword] = match;
70
+ if (owned.has(start + indent.length))
71
+ continue;
72
72
  // The parenthesised group is optional, so it can be undefined despite RegExpExecArray's typing.
73
73
  const inParens = match[3];
74
74
  const upper = keyword.toUpperCase();
@@ -107,11 +107,6 @@ export default createRule({
107
107
  reportReason(subject, state);
108
108
  continue;
109
109
  }
110
- if (annotationOnly) {
111
- reportAnnotationOnly(subject, state, annotationOnly);
112
- continue;
113
- }
114
- const result = evaluate(subject, state);
115
110
  // Annotations are checked whenever a ticket is needed, whatever the markers say.
116
111
  for (const ticket of annotationTicketsOf(subject)) {
117
112
  if (state.ticket.check(ticket.text) !== 'ok' && reportOnce(`annotation:${ticket.node.range[0]}`)) {
@@ -122,6 +117,11 @@ export default createRule({
122
117
  });
123
118
  }
124
119
  }
120
+ if (annotationOnly) {
121
+ reportAnnotationOnly(subject, state, annotationOnly);
122
+ continue;
123
+ }
124
+ const result = evaluate(subject, state);
125
125
  if (result.kind === 'missing' && hasAnnotationTicket(subject, state, options))
126
126
  continue;
127
127
  if (result.kind !== 'ok' &&