eslint-plugin-test-governance 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +166 -64
  3. package/dist/index.d.ts +14 -12
  4. package/dist/index.js +22 -12
  5. package/dist/rules/marker-matches-state.d.ts +1 -2
  6. package/dist/rules/marker-matches-state.js +7 -7
  7. package/dist/rules/no-conflicting-states.d.ts +1 -2
  8. package/dist/rules/no-conflicting-states.js +12 -20
  9. package/dist/rules/no-orphaned-marker.d.ts +1 -2
  10. package/dist/rules/no-orphaned-marker.js +15 -10
  11. package/dist/rules/require-ticket-in-comments.d.ts +1 -2
  12. package/dist/rules/require-ticket-in-comments.js +13 -18
  13. package/dist/rules/require-ticket.d.ts +1 -2
  14. package/dist/rules/require-ticket.js +18 -10
  15. package/dist/utils/analyze.d.ts +17 -14
  16. package/dist/utils/analyze.js +370 -153
  17. package/dist/utils/constants.d.ts +5 -3
  18. package/dist/utils/constants.js +4 -3
  19. package/dist/utils/create-rule.d.ts +9 -10
  20. package/dist/utils/create-rule.js +5 -11
  21. package/dist/utils/distance.d.ts +0 -1
  22. package/dist/utils/distance.js +0 -1
  23. package/dist/utils/errors.d.ts +0 -1
  24. package/dist/utils/errors.js +0 -1
  25. package/dist/utils/fix.d.ts +0 -3
  26. package/dist/utils/fix.js +0 -8
  27. package/dist/utils/frameworks.d.ts +59 -0
  28. package/dist/utils/frameworks.js +102 -0
  29. package/dist/utils/options.d.ts +26 -11
  30. package/dist/utils/options.js +13 -9
  31. package/dist/utils/schema.d.ts +1 -2
  32. package/dist/utils/schema.js +2 -2
  33. package/dist/utils/tickets.d.ts +0 -1
  34. package/dist/utils/tickets.js +4 -4
  35. package/dist/utils/validate.d.ts +0 -1
  36. package/dist/utils/validate.js +3 -3
  37. package/docs/rules/marker-matches-state.md +18 -10
  38. package/docs/rules/no-conflicting-states.md +14 -8
  39. package/docs/rules/no-orphaned-marker.md +31 -17
  40. package/docs/rules/require-ticket-in-comments.md +26 -22
  41. package/docs/rules/require-ticket.md +107 -47
  42. package/package.json +7 -10
@@ -2,9 +2,11 @@
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
- export declare const BUILTIN_STATE_NAMES: readonly ["skip", "fixme", "fail", "slow", "new", "unstable"];
5
+ export declare const BUILTIN_STATE_NAMES: readonly ["skip", "fixme", "fail", "slow", "todo", "new", "unstable"];
6
6
  export type BuiltinStateName = (typeof BUILTIN_STATE_NAMES)[number];
7
+ /** The test frameworks the rules understand. */
8
+ export declare const FRAMEWORK_NAMES: readonly ["playwright", "jest", "vitest"];
9
+ export type FrameworkName = (typeof FRAMEWORK_NAMES)[number];
7
10
  export declare const TICKET_PRESETS: readonly ["any", "jira", "github", "gitlab", "linear", "azure-devops", "numeric", "pattern"];
8
- /** Comment keywords that must reference a ticket by default (`// TODO: WEB-123`). */
11
+ /** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
9
12
  export declare const DEFAULT_COMMENT_KEYWORDS: string[];
10
- //# sourceMappingURL=constants.d.ts.map
@@ -2,7 +2,9 @@
2
2
  export const MARKER_RE = /^[A-Z][A-Z0-9_-]*$/;
3
3
  /** A tag as written in options: `@` followed by letters, digits, `-` and `_`. */
4
4
  export const TAG_RE = /^@[\w-]+$/;
5
- export const BUILTIN_STATE_NAMES = ['skip', 'fixme', 'fail', 'slow', 'new', 'unstable'];
5
+ export const BUILTIN_STATE_NAMES = ['skip', 'fixme', 'fail', 'slow', 'todo', 'new', 'unstable'];
6
+ /** The test frameworks the rules understand. */
7
+ export const FRAMEWORK_NAMES = ['playwright', 'jest', 'vitest'];
6
8
  export const TICKET_PRESETS = [
7
9
  'any',
8
10
  'jira',
@@ -13,6 +15,5 @@ export const TICKET_PRESETS = [
13
15
  'numeric',
14
16
  'pattern',
15
17
  ];
16
- /** Comment keywords that must reference a ticket by default (`// TODO: WEB-123`). */
18
+ /** Comment keywords that must reference a ticket by default (`// TODO: TRADE-123`). */
17
19
  export const DEFAULT_COMMENT_KEYWORDS = ['FIXME', 'TODO'];
18
- //# sourceMappingURL=constants.js.map
@@ -1,10 +1,9 @@
1
1
  import type { TSESLint } from '@typescript-eslint/utils';
2
- import { type Analysis } from './analyze.js';
3
- import { type ResolvedOptions } from './options.js';
4
- export declare const REPO_URL = "https://github.com/Huldoser/eslint-plugin-test-governance";
2
+ import { type Analysis } from './analyze.ts';
3
+ import { type ResolvedOptions } from './options.ts';
5
4
  /** Rules take no options of their own: everything is shared through `settings['test-governance']`. */
6
- export type RuleOptions = [];
7
- export interface RuleDocs {
5
+ type RuleOptions = [];
6
+ interface RuleDocs {
8
7
  description: string;
9
8
  recommended: 'error' | 'warn';
10
9
  url: string;
@@ -15,15 +14,15 @@ export type Rule<MessageIds extends string> = TSESLint.RuleModule<MessageIds, Ru
15
14
  };
16
15
  };
17
16
  export interface RuleDefinition<MessageIds extends string> {
18
- name: string;
19
17
  meta: Omit<TSESLint.RuleMetaData<MessageIds, RuleDocs>, 'docs' | 'schema' | 'defaultOptions'> & {
20
- docs: Omit<RuleDocs, 'url'>;
18
+ docs: RuleDocs;
19
+ schema: [];
21
20
  };
22
- check(context: Readonly<TSESLint.RuleContext<MessageIds, RuleOptions>>, analysis: Analysis, options: ResolvedOptions): void;
21
+ create(context: Readonly<TSESLint.RuleContext<MessageIds, RuleOptions>>, analysis: Analysis, options: ResolvedOptions): void;
23
22
  }
24
23
  /**
25
24
  * Builds a rule that reads its options from `settings['test-governance']`.
26
- * `check` runs once per file with the shared analysis.
25
+ * `create` runs once per file, at the end, with the shared analysis.
27
26
  */
28
27
  export declare function createRule<MessageIds extends string>(definition: RuleDefinition<MessageIds>): Rule<MessageIds>;
29
- //# sourceMappingURL=create-rule.d.ts.map
28
+ export {};
@@ -1,26 +1,20 @@
1
- import { analyze } from './analyze.js';
2
- import { resolveOptions } from './options.js';
3
- export const REPO_URL = 'https://github.com/Huldoser/eslint-plugin-test-governance';
1
+ import { analyze } from "./analyze.js";
2
+ import { resolveOptions } from "./options.js";
4
3
  /**
5
4
  * Builds a rule that reads its options from `settings['test-governance']`.
6
- * `check` runs once per file with the shared analysis.
5
+ * `create` runs once per file, at the end, with the shared analysis.
7
6
  */
8
7
  export function createRule(definition) {
9
8
  return {
10
- meta: {
11
- ...definition.meta,
12
- docs: { ...definition.meta.docs, url: `${REPO_URL}/blob/main/docs/rules/${definition.name}.md` },
13
- schema: [],
14
- },
9
+ meta: definition.meta,
15
10
  defaultOptions: [],
16
11
  create(context) {
17
12
  const options = resolveOptions(context.settings);
18
13
  return {
19
14
  'Program:exit'() {
20
- definition.check(context, analyze(context.sourceCode, options), options);
15
+ definition.create(context, analyze(context.sourceCode, options), options);
21
16
  },
22
17
  };
23
18
  },
24
19
  };
25
20
  }
26
- //# sourceMappingURL=create-rule.js.map
@@ -2,4 +2,3 @@
2
2
  export declare function editDistance(a: string, b: string): number;
3
3
  /** How many edits still count as a typo for a tag of this length (without the `@`). */
4
4
  export declare function typoThreshold(length: number): number;
5
- //# sourceMappingURL=distance.d.ts.map
@@ -20,4 +20,3 @@ export function typoThreshold(length) {
20
20
  return 2;
21
21
  return length >= 5 ? 1 : 0;
22
22
  }
23
- //# sourceMappingURL=distance.js.map
@@ -2,4 +2,3 @@
2
2
  export declare class ConfigError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
- //# sourceMappingURL=errors.d.ts.map
@@ -5,4 +5,3 @@ export class ConfigError extends Error {
5
5
  this.name = 'ConfigError';
6
6
  }
7
7
  }
8
- //# sourceMappingURL=errors.js.map
@@ -6,7 +6,4 @@ export declare function keywordRange(sourceCode: SourceCode, comment: TSESTree.C
6
6
  export declare function removeComment(sourceCode: SourceCode, comment: TSESTree.Comment): TSESLint.RuleFix;
7
7
  /** Indentation of the line `comment` starts on. */
8
8
  export declare function indentOf(sourceCode: SourceCode, comment: TSESTree.Comment): string;
9
- /** Range of the `index`-th occurrence of `tag` in the source text of `node`. */
10
- export declare function tagRange(sourceCode: SourceCode, node: TSESTree.Node, tag: string, index: number): TSESTree.Range | undefined;
11
9
  export {};
12
- //# sourceMappingURL=fix.d.ts.map
package/dist/utils/fix.js CHANGED
@@ -24,11 +24,3 @@ export function indentOf(sourceCode, comment) {
24
24
  const line = sourceCode.lines[comment.loc.start.line - 1];
25
25
  return line.slice(0, line.length - line.trimStart().length);
26
26
  }
27
- /** Range of the `index`-th occurrence of `tag` in the source text of `node`. */
28
- export function tagRange(sourceCode, node, tag, index) {
29
- const text = sourceCode.getText(node);
30
- const re = new RegExp(`(?<![\\w@])${escapeRegExp(tag)}(?![\\w-])`, 'g');
31
- const match = [...text.matchAll(re)].at(index);
32
- return match === undefined ? undefined : [node.range[0] + match.index, node.range[0] + match.index + tag.length];
33
- }
34
- //# sourceMappingURL=fix.js.map
@@ -0,0 +1,59 @@
1
+ import type { BuiltinStateName, FrameworkName } from './constants.ts';
2
+ /** A state a test enters through its own code, as opposed to a tag. */
3
+ export type Modifier = 'skip' | 'fixme' | 'fail' | 'slow' | 'todo';
4
+ /** What a name from a framework stands for: `xit` is a test with `skip`, `suite` a describe block. */
5
+ export interface Root {
6
+ kind: 'test' | 'describe';
7
+ modifiers: readonly string[];
8
+ }
9
+ /** The state a member puts a test in. A conditional one, like Vitest's `skipIf(...)`, may not apply. */
10
+ interface Effect {
11
+ modifier: Modifier;
12
+ conditional: boolean;
13
+ }
14
+ /** How a test framework declares tests and puts them in states. */
15
+ export interface Framework {
16
+ name: FrameworkName;
17
+ /** Whether `source` is one of the framework's own modules. */
18
+ ownsModule(source: string): boolean;
19
+ /** Test functions the framework's modules export, by exported name. */
20
+ exports: ReadonlyMap<string, Root>;
21
+ /** What a default import from the framework's module is. */
22
+ defaultExport?: Root;
23
+ /** Test functions that exist without an import, besides `testFunctions`. */
24
+ globals: ReadonlyMap<string, Root>;
25
+ /** The default `testFunctions`. */
26
+ testFunctions: readonly string[];
27
+ /**
28
+ * Members a test or describe call may have, as in `test.skip(...)`. A member that is called to build
29
+ * the function, as in `test.each(table)('buys %s', fn)`, is written with parentheses: `each()`.
30
+ */
31
+ testMembers: ReadonlySet<string>;
32
+ describeMembers: ReadonlySet<string>;
33
+ /** Members that are called to build the function that declares the tests. */
34
+ factories: ReadonlySet<string>;
35
+ /** The state each member puts a test in. */
36
+ effects: ReadonlyMap<string, Effect>;
37
+ /** Keys of an options object that put a test in a state, as in Vitest's `{ skip: true }`. */
38
+ optionEffects: ReadonlyMap<string, Modifier>;
39
+ /** The key of the details or options object that holds tags. */
40
+ tagKey?: string;
41
+ /**
42
+ * The parameter of a test's body that can change its state at runtime, such as Playwright's
43
+ * `testInfo` or Vitest's test context, and the methods it has for that.
44
+ */
45
+ info?: {
46
+ param: number;
47
+ methods: ReadonlySet<string>;
48
+ };
49
+ /** Built-in states a test can be put in with this framework. */
50
+ states: ReadonlySet<BuiltinStateName>;
51
+ /** Playwright's own API on `test`: `test.describe`, `test.step`, `test.info()` and runtime `test.skip()`. */
52
+ playwright: boolean;
53
+ }
54
+ /** Methods on `testInfo` and on `test` itself that change a test's state at runtime. */
55
+ export declare const RUNTIME_MODIFIERS: ReadonlySet<string>;
56
+ export declare const FRAMEWORKS: Record<FrameworkName, Framework>;
57
+ /** Whether `source` belongs to a test runner other than `framework`, so its names are never tests here. */
58
+ export declare function isOtherRunner(framework: Framework, source: string): boolean;
59
+ export {};
@@ -0,0 +1,102 @@
1
+ const TEST = { kind: 'test', modifiers: [] };
2
+ const DESCRIBE = { kind: 'describe', modifiers: [] };
3
+ const PLAYWRIGHT_MODULES = new Set(['@playwright/test', 'playwright/test']);
4
+ /** Test runners besides Playwright. A name imported from one of them belongs to that runner. */
5
+ const OTHER_RUNNERS = new Set(['vitest', '@jest/globals', 'node:test', 'bun:test', 'mocha', 'ava', 'tap', 'uvu']);
6
+ /** `@playwright/test` and the component-testing packages such as `@playwright/experimental-ct-react`. */
7
+ function isPlaywrightModule(source) {
8
+ return PLAYWRIGHT_MODULES.has(source) || source.startsWith('@playwright/experimental-ct-');
9
+ }
10
+ /** Methods on `testInfo` and on `test` itself that change a test's state at runtime. */
11
+ export const RUNTIME_MODIFIERS = new Set(['skip', 'fixme', 'fail', 'slow']);
12
+ const effects = (entries) => new Map(entries.map(([member, modifier, conditional = false]) => [member, { modifier, conditional }]));
13
+ const playwright = {
14
+ name: 'playwright',
15
+ ownsModule: isPlaywrightModule,
16
+ exports: new Map([['test', TEST]]),
17
+ defaultExport: TEST,
18
+ globals: new Map(),
19
+ testFunctions: ['test'],
20
+ testMembers: new Set(['only', 'skip', 'fixme', 'fail', 'slow']),
21
+ describeMembers: new Set(['only', 'skip', 'fixme', 'serial', 'parallel']),
22
+ factories: new Set(),
23
+ effects: effects([
24
+ ['skip', 'skip'],
25
+ ['fixme', 'fixme'],
26
+ ['fail', 'fail'],
27
+ ['slow', 'slow'],
28
+ ]),
29
+ optionEffects: new Map(),
30
+ tagKey: 'tag',
31
+ info: { param: 1, methods: RUNTIME_MODIFIERS },
32
+ states: new Set(['skip', 'fixme', 'fail', 'slow', 'new', 'unstable']),
33
+ playwright: true,
34
+ };
35
+ const JEST_FUNCTIONS = new Map([
36
+ ['test', TEST],
37
+ ['it', TEST],
38
+ ['xtest', { kind: 'test', modifiers: ['skip'] }],
39
+ ['xit', { kind: 'test', modifiers: ['skip'] }],
40
+ ['fit', { kind: 'test', modifiers: ['only'] }],
41
+ ['describe', DESCRIBE],
42
+ ['xdescribe', { kind: 'describe', modifiers: ['skip'] }],
43
+ ['fdescribe', { kind: 'describe', modifiers: ['only'] }],
44
+ ]);
45
+ const jest = {
46
+ name: 'jest',
47
+ ownsModule: (source) => source === '@jest/globals',
48
+ exports: JEST_FUNCTIONS,
49
+ globals: new Map([...JEST_FUNCTIONS].filter(([name]) => name !== 'test' && name !== 'it')),
50
+ testFunctions: ['test', 'it'],
51
+ testMembers: new Set(['only', 'skip', 'todo', 'failing', 'concurrent', 'each()']),
52
+ describeMembers: new Set(['only', 'skip', 'each()']),
53
+ factories: new Set(['each']),
54
+ effects: effects([
55
+ ['skip', 'skip'],
56
+ ['todo', 'todo'],
57
+ ['failing', 'fail'],
58
+ ]),
59
+ optionEffects: new Map(),
60
+ states: new Set(['skip', 'todo', 'fail', 'new', 'unstable']),
61
+ playwright: false,
62
+ };
63
+ const VITEST_FACTORIES = ['each()', 'for()', 'skipIf()', 'runIf()'];
64
+ const vitest = {
65
+ name: 'vitest',
66
+ ownsModule: (source) => source === 'vitest',
67
+ exports: new Map([
68
+ ['test', TEST],
69
+ ['it', TEST],
70
+ ['describe', DESCRIBE],
71
+ ['suite', DESCRIBE],
72
+ ]),
73
+ globals: new Map([
74
+ ['describe', DESCRIBE],
75
+ ['suite', DESCRIBE],
76
+ ]),
77
+ testFunctions: ['test', 'it'],
78
+ testMembers: new Set(['only', 'skip', 'todo', 'fails', 'concurrent', 'sequential', ...VITEST_FACTORIES]),
79
+ describeMembers: new Set(['only', 'skip', 'todo', 'concurrent', 'sequential', 'shuffle', ...VITEST_FACTORIES]),
80
+ factories: new Set(['each', 'for', 'skipIf', 'runIf']),
81
+ effects: effects([
82
+ ['skip', 'skip'],
83
+ ['todo', 'todo'],
84
+ ['fails', 'fail'],
85
+ ['skipIf()', 'skip', true],
86
+ ['runIf()', 'skip', true],
87
+ ]),
88
+ optionEffects: new Map([
89
+ ['skip', 'skip'],
90
+ ['todo', 'todo'],
91
+ ['fails', 'fail'],
92
+ ]),
93
+ tagKey: 'tags',
94
+ info: { param: 0, methods: new Set(['skip']) },
95
+ states: new Set(['skip', 'todo', 'fail', 'new', 'unstable']),
96
+ playwright: false,
97
+ };
98
+ export const FRAMEWORKS = { playwright, jest, vitest };
99
+ /** Whether `source` belongs to a test runner other than `framework`, so its names are never tests here. */
100
+ export function isOtherRunner(framework, source) {
101
+ return (isPlaywrightModule(source) || OTHER_RUNNERS.has(source)) && !framework.ownsModule(source);
102
+ }
@@ -1,5 +1,6 @@
1
- import { type BuiltinStateName } from './constants.js';
2
- import { type TicketMatcher, type TicketSpec } from './tickets.js';
1
+ import { type BuiltinStateName, type FrameworkName } from './constants.ts';
2
+ import { type Framework, type Modifier } from './frameworks.ts';
3
+ import { type TicketMatcher, type TicketSpec } from './tickets.ts';
3
4
  export declare const SETTINGS_KEY = "test-governance";
4
5
  /** A state override in `states`. `false` turns a state off, `true` turns it on. */
5
6
  export interface StateOverride {
@@ -14,36 +15,50 @@ export interface CustomState {
14
15
  marker: string;
15
16
  ticket?: TicketSpec | TicketSpec[];
16
17
  }
17
- export type { BuiltinStateName } from './constants.js';
18
18
  /** Options shared by every rule. Set them once in `settings['test-governance']`. */
19
19
  export interface GovernanceOptions {
20
- /** Names that are treated as the Playwright `test` function. Defaults to `['test']`. */
20
+ /** The test framework of the files the config applies to. Defaults to `'playwright'`. */
21
+ framework?: FrameworkName;
22
+ /**
23
+ * Names that are treated as the framework's `test` function. Defaults to `['test']` for Playwright and
24
+ * `['test', 'it']` for Jest and Vitest. Your list replaces the default one, so include those names if
25
+ * you still need them.
26
+ */
21
27
  testFunctions?: string[];
22
28
  /** Accepted ticket formats. Defaults to `{ preset: 'any' }`. */
23
29
  ticket?: TicketSpec | TicketSpec[];
24
- /** Tickets that are rejected as placeholders. `*` matches any characters. */
30
+ /**
31
+ * Tickets that are rejected as placeholders. `*` matches any characters. Your list replaces the default
32
+ * one (`TODO`, `TBD`, `XXX-*`, ...); tickets numbered 0 are always rejected.
33
+ */
25
34
  placeholders?: string[];
26
35
  /** Turns on the `@new` and `@unstable` states. */
27
36
  lifecycleTags?: boolean;
37
+ /**
38
+ * Turns built-in states on or off, renames their marker or gives them their own ticket format. `skip`,
39
+ * `fixme` and `todo` are on by default, `fail` and `slow` off; a setting for `new` or `unstable` wins over
40
+ * `lifecycleTags`. `fixme` and `slow` exist only in Playwright and `todo` only in Jest and Vitest.
41
+ */
28
42
  states?: Partial<Record<BuiltinStateName, boolean | StateOverride>>;
43
+ /** Adds a state for a tag, e.g. `{ quarantine: { when: '@quarantine', marker: 'QUARANTINE' } }`. */
29
44
  customStates?: Record<string, CustomState>;
30
45
  /** Require a ticket for conditional skips such as `test.skip(browserName === 'webkit')`. */
31
46
  requireTicketForConditional?: boolean;
32
47
  /** Let blank lines separate a marker from the test it belongs to. */
33
48
  allowBlankLine?: boolean;
34
49
  /**
35
- * Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: WEB-123`.
36
- * Defaults to `{ keywords: ['FIXME', 'TODO'] }`; `false` allows free-form comments.
50
+ * Comment keywords that must start with a ticket anywhere in a test file, e.g. `// TODO: TRADE-123`.
51
+ * Defaults to `{ keywords: ['FIXME', 'TODO'] }`, and your keywords replace those; `false` allows free-form
52
+ * comments.
37
53
  */
38
54
  comments?: false | {
39
55
  keywords?: string[];
40
56
  };
41
- /** Allow free text after the tickets, e.g. `// SKIP: WEB-1 flaky on CI`. Off by default: details belong in the ticket. */
57
+ /** Allow free text after the tickets, e.g. `// SKIP: TRADE-1 flaky on CI`. Off by default: details belong in the ticket. */
42
58
  allowNotes?: boolean;
43
59
  /** Report titles that aren't static text when a tag state is on, because their tags can't be read. */
44
60
  reportDynamicTitles?: boolean;
45
61
  }
46
- export type Modifier = 'skip' | 'fixme' | 'fail' | 'slow';
47
62
  interface StateBase {
48
63
  name: string;
49
64
  marker: string;
@@ -62,6 +77,7 @@ export type TagStateDef = Extract<StateDef, {
62
77
  }>;
63
78
  export declare function isTagState(state: StateDef): state is TagStateDef;
64
79
  export interface ResolvedOptions {
80
+ framework: Framework;
65
81
  testFunctions: Set<string>;
66
82
  states: StateDef[];
67
83
  requireTicketForConditional: boolean;
@@ -78,11 +94,10 @@ export interface ResolvedOptions {
78
94
  /** Ticket format for comments: the shared default format. */
79
95
  commentTicket: TicketMatcher;
80
96
  }
81
- export { ConfigError } from './errors.js';
97
+ export { ConfigError } from './errors.ts';
82
98
  /**
83
99
  * Compiles `settings['test-governance']`. Every rule reads the same settings, so they always agree,
84
100
  * and the result is cached per settings object, so the rules share one compile.
85
101
  */
86
102
  export declare function resolveOptions(settings: unknown): ResolvedOptions;
87
103
  export declare function compileOptions(options: GovernanceOptions): ResolvedOptions;
88
- //# sourceMappingURL=options.d.ts.map
@@ -1,8 +1,9 @@
1
- import { BUILTIN_STATE_NAMES, DEFAULT_COMMENT_KEYWORDS } from './constants.js';
2
- import { ConfigError } from './errors.js';
3
- import { optionsSchema } from './schema.js';
4
- import { compileTicketSpec, DEFAULT_PLACEHOLDERS } from './tickets.js';
5
- import { validate } from './validate.js';
1
+ import { BUILTIN_STATE_NAMES, DEFAULT_COMMENT_KEYWORDS, } from "./constants.js";
2
+ import { ConfigError } from "./errors.js";
3
+ import { FRAMEWORKS } from "./frameworks.js";
4
+ import { optionsSchema } from "./schema.js";
5
+ import { compileTicketSpec, DEFAULT_PLACEHOLDERS } from "./tickets.js";
6
+ import { validate } from "./validate.js";
6
7
  export const SETTINGS_KEY = 'test-governance';
7
8
  export function isTagState(state) {
8
9
  return state.tag !== undefined;
@@ -12,10 +13,11 @@ const BUILTIN_STATES = {
12
13
  fixme: { marker: 'FIXME', modifier: 'fixme', enabled: true },
13
14
  fail: { marker: 'FAIL', modifier: 'fail', enabled: false },
14
15
  slow: { marker: 'SLOW', modifier: 'slow', enabled: false },
16
+ todo: { marker: 'TODO', modifier: 'todo', enabled: true },
15
17
  new: { marker: 'NEW', tag: '@new', lifecycle: true, enabled: false },
16
18
  unstable: { marker: 'UNSTABLE', tag: '@unstable', lifecycle: true, enabled: false },
17
19
  };
18
- export { ConfigError } from './errors.js';
20
+ export { ConfigError } from "./errors.js";
19
21
  function toSpecs(spec) {
20
22
  return Array.isArray(spec) ? spec : [spec];
21
23
  }
@@ -82,6 +84,7 @@ export function compileOptions(options) {
82
84
  if (problems.length > 0) {
83
85
  throw new ConfigError(`invalid options:\n${problems.map((problem) => ` - ${problem}`).join('\n')}`);
84
86
  }
87
+ const framework = FRAMEWORKS[options.framework ?? 'playwright'];
85
88
  const placeholders = options.placeholders ?? DEFAULT_PLACEHOLDERS;
86
89
  const defaultTicket = toSpecs(options.ticket ?? { preset: 'any' });
87
90
  const compile = (spec) => compileTicketSpec(spec === undefined ? defaultTicket : toSpecs(spec), placeholders);
@@ -92,7 +95,8 @@ export function compileOptions(options) {
92
95
  const raw = overrides[name];
93
96
  const override = typeof raw === 'boolean' ? { enabled: raw } : (raw ?? {});
94
97
  const enabled = override.enabled ?? (def.lifecycle ? (options.lifecycleTags ?? def.enabled) : def.enabled);
95
- if (!enabled)
98
+ // A state the framework has no way to enter, such as `fixme` in Jest, stays off.
99
+ if (!enabled || !framework.states.has(name))
96
100
  continue;
97
101
  const base = {
98
102
  name,
@@ -123,7 +127,8 @@ export function compileOptions(options) {
123
127
  }
124
128
  const commentKeywords = options.comments === false ? [] : (options.comments?.keywords ?? DEFAULT_COMMENT_KEYWORDS);
125
129
  return {
126
- testFunctions: new Set(options.testFunctions ?? ['test']),
130
+ framework,
131
+ testFunctions: new Set(options.testFunctions ?? framework.testFunctions),
127
132
  states,
128
133
  requireTicketForConditional: options.requireTicketForConditional ?? false,
129
134
  allowBlankLine: options.allowBlankLine ?? false,
@@ -134,4 +139,3 @@ export function compileOptions(options) {
134
139
  commentTicket: defaultMatcher,
135
140
  };
136
141
  }
137
- //# sourceMappingURL=options.js.map
@@ -1,4 +1,3 @@
1
- import type { Schema } from './validate.js';
1
+ import type { Schema } from './validate.ts';
2
2
  /** The shape of `settings['test-governance']`, the options of `configure()`. */
3
3
  export declare const optionsSchema: Schema;
4
- //# sourceMappingURL=schema.d.ts.map
@@ -1,4 +1,4 @@
1
- import { BUILTIN_STATE_NAMES, MARKER_RE, TAG_RE, TICKET_PRESETS } from './constants.js';
1
+ import { BUILTIN_STATE_NAMES, FRAMEWORK_NAMES, MARKER_RE, TAG_RE, TICKET_PRESETS } from "./constants.js";
2
2
  const MARKER_HINT = 'an uppercase keyword such as NEEDS-DATA (letters, digits, "-" and "_")';
3
3
  const TAG_HINT = 'a tag such as @needs-data';
4
4
  /** Jira project and Linear team keys, as they appear in tickets: `WEB` in `WEB-123`. */
@@ -40,6 +40,7 @@ const stateOverrideSchema = {
40
40
  export const optionsSchema = {
41
41
  type: 'object',
42
42
  properties: {
43
+ framework: { type: 'string', enum: [...FRAMEWORK_NAMES] },
43
44
  testFunctions: { type: 'array', items: { type: 'string' } },
44
45
  ticket: ticketSchema,
45
46
  placeholders: { type: 'array', items: { type: 'string' } },
@@ -81,4 +82,3 @@ export const optionsSchema = {
81
82
  },
82
83
  additionalProperties: false,
83
84
  };
84
- //# sourceMappingURL=schema.js.map
@@ -35,4 +35,3 @@ export interface TicketMatcher {
35
35
  }
36
36
  export declare const DEFAULT_PLACEHOLDERS: string[];
37
37
  export declare function compileTicketSpec(specs: TicketSpec[], placeholders: string[]): TicketMatcher;
38
- //# sourceMappingURL=tickets.d.ts.map
@@ -1,5 +1,5 @@
1
- import { ConfigError } from './errors.js';
2
- export const DEFAULT_PLACEHOLDERS = ['TODO', 'TBD', 'XXX-*', '0', '123', '1234', '12345'];
1
+ import { ConfigError } from "./errors.js";
2
+ export const DEFAULT_PLACEHOLDERS = ['TODO', 'TBD', 'XXX-*', '0'];
3
3
  // Every pattern below is anchored and has no nested or overlapping quantifiers, so matching is
4
4
  // linear in the token length. URLs are parsed with `new URL` rather than a regex.
5
5
  const KEY_RE = /^([A-Z][A-Z0-9_]+)-\d+$/;
@@ -8,7 +8,8 @@ const GITHUB_REF_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[\w.-]+#\d+$/;
8
8
  const GITLAB_REF_RE = /^[\w.-]+(?:\/[\w.-]+)+#\d+$/;
9
9
  const AZURE_REF_RE = /^AB#\d+$/;
10
10
  const DIGITS_RE = /^\d+$/;
11
- const JIRA_PATH_RE = /^\/browse\/([A-Z][A-Z0-9_]+-\d+)\/?$/;
11
+ // Jira Server and Data Center can run under a path, as in https://issues.example.com/jira/browse/TRADE-1.
12
+ const JIRA_PATH_RE = /\/browse\/([A-Z][A-Z0-9_]+-\d+)\/?$/;
12
13
  const GITHUB_PATH_RE = /^\/[^/]+\/[^/]+\/(?:issues|pull)\/\d+\/?$/;
13
14
  const GITLAB_PATH_RE = /\/-\/(?:issues|merge_requests|work_items)\/\d+\/?$/;
14
15
  const LINEAR_PATH_RE = /^\/[^/]+\/issue\/([A-Z][A-Z0-9]*-\d+)(?:\/[^/]*)?$/;
@@ -171,4 +172,3 @@ export function compileTicketSpec(specs, placeholders) {
171
172
  example: presets[0].example,
172
173
  };
173
174
  }
174
- //# sourceMappingURL=tickets.js.map
@@ -18,4 +18,3 @@ export interface Schema {
18
18
  * readable problems such as `ticket.preset must be one of "any", "jira", … (got "jria")`.
19
19
  */
20
20
  export declare function validate(value: unknown, schema: Schema, path?: string): string[];
21
- //# sourceMappingURL=validate.d.ts.map
@@ -1,4 +1,4 @@
1
- import { editDistance } from './distance.js';
1
+ import { editDistance } from "./distance.js";
2
2
  function kindOf(value) {
3
3
  if (value === null)
4
4
  return 'null';
@@ -29,7 +29,8 @@ const toJson = JSON.stringify;
29
29
  function show(value) {
30
30
  if (typeof value === 'function')
31
31
  return 'a function';
32
- const text = toJson(value) ?? String(value);
32
+ // JSON shows a regex as `{}`.
33
+ const text = value instanceof RegExp ? String(value) : (toJson(value) ?? String(value));
33
34
  return text.length > 40 ? `${text.slice(0, 37)}...` : text;
34
35
  }
35
36
  function join(path, key) {
@@ -119,4 +120,3 @@ export function validate(value, schema, path = '') {
119
120
  }
120
121
  return [];
121
122
  }
122
- //# sourceMappingURL=validate.js.map
@@ -22,15 +22,15 @@ Incorrect:
22
22
  <!-- example: invalid -->
23
23
 
24
24
  ```js
25
- // SKIP: WEB-500
26
- test.fixme('splits shipping', async () => {});
25
+ // SKIP: TRADE-500
26
+ test.fixme('shows the unfilled quantity of a partly filled order', async () => {});
27
27
  ```
28
28
 
29
29
  <!-- example: invalid settings={"lifecycleTags":true} -->
30
30
 
31
31
  ```js
32
- // SKIP: WEB-500
33
- test.skip('splits shipping @unstable', async () => {});
32
+ // SKIP: TRADE-500
33
+ test.skip('shows the unfilled quantity of a partly filled order @unstable', async () => {});
34
34
  ```
35
35
 
36
36
  Correct:
@@ -38,18 +38,26 @@ Correct:
38
38
  <!-- example: valid -->
39
39
 
40
40
  ```js
41
- // FIXME: WEB-500
42
- test.fixme('splits shipping', async () => {});
41
+ // FIXME: TRADE-500
42
+ test.fixme('shows the unfilled quantity of a partly filled order', async () => {});
43
43
  ```
44
44
 
45
45
  <!-- example: valid settings={"lifecycleTags":true} -->
46
46
 
47
47
  ```js
48
- // SKIP: WEB-500
49
- // UNSTABLE: WEB-500
50
- test.skip('splits shipping @unstable', async () => {});
48
+ // SKIP: TRADE-500
49
+ // UNSTABLE: TRADE-500
50
+ test.skip('shows the unfilled quantity of a partly filled order @unstable', async () => {});
51
51
  ```
52
52
 
53
53
  ## Options
54
54
 
55
- Uses the shared options described in the [README](../../README.md#options).
55
+ This rule has no options of its own. It reads the shared options described in the
56
+ [README](../../README.md#options). These change what it reports:
57
+
58
+ - `lifecycleTags`, `states` and `customStates`: which states are on, and the marker keyword each
59
+ one expects.
60
+ - `requireTicketForConditional`: conditional skips such as `test.skip(browserName === 'webkit')`
61
+ need a marker only when this is set, so only then is a wrong marker above one reported.
62
+ - `allowBlankLine`: whether a marker separated from its test by a blank line still belongs to it.
63
+ - `framework` and `testFunctions`: which functions count as the framework's `test`.