@flint.fyi/rule-tester 0.13.1 → 0.14.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @flint/rule-tester
2
2
 
3
+ ## 0.14.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b58d145: corrected logic and types for current-file suggestions
8
+
9
+ ## 0.14.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 7d0d873: add // flint-\* comment directives
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [b48f4a9]
18
+ - Updated dependencies [7d0d873]
19
+ - Updated dependencies [79f15da]
20
+ - @flint.fyi/core@0.15.0
21
+
3
22
  ## 0.13.1
4
23
 
5
24
  ### Patch Changes
@@ -1,16 +1,20 @@
1
- import { AnyOptionalSchema, AnyRule, InferredObject, InvalidTestCase, RuleAbout, ValidTestCase } from "@flint.fyi/core";
1
+ import { AnyOptionalSchema, AnyRule, InferredObject, RuleAbout } from "@flint.fyi/core";
2
+ import { InvalidTestCase, ValidTestCase } from "./types.js";
2
3
  export interface RuleTesterOptions {
3
- describe?: TesterSetup;
4
- it?: TesterSetup;
4
+ describe?: TesterSetupDescribe;
5
+ it?: TesterSetupIt;
6
+ only?: TesterSetupIt;
5
7
  scope?: Record<string, unknown>;
8
+ skip?: TesterSetupIt;
6
9
  }
7
10
  export interface TestCases<Options extends object | undefined> {
8
11
  invalid: InvalidTestCase<Options>[];
9
12
  valid: ValidTestCase<Options>[];
10
13
  }
11
- export type TesterSetup = (description: string, setup: () => void) => void;
14
+ export type TesterSetupDescribe = (description: string, setup: () => void) => void;
15
+ export type TesterSetupIt = (description: string, setup: () => Promise<void>) => void;
12
16
  export declare class RuleTester {
13
17
  #private;
14
- constructor({ describe, it, scope }?: RuleTesterOptions);
18
+ constructor({ describe, it, only, scope, skip, }?: RuleTesterOptions);
15
19
  describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<RuleAbout, OptionsSchema>, { invalid, valid }: TestCases<InferredObject<OptionsSchema>>): void;
16
20
  }
package/lib/RuleTester.js CHANGED
@@ -1,16 +1,33 @@
1
1
  import { CachedFactory } from "cached-factory";
2
2
  import assert from "node:assert";
3
3
  import { createReportSnapshot } from "./createReportSnapshot.js";
4
+ import { normalizeTestCase } from "./normalizeTestCase.js";
5
+ import { resolveReportedSuggestions } from "./resolveReportedSuggestions.js";
4
6
  import { runTestCaseRule } from "./runTestCaseRule.js";
5
7
  export class RuleTester {
6
8
  #fileFactories;
7
9
  #testerOptions;
8
- constructor({ describe, it, scope = globalThis } = {}) {
10
+ constructor({ describe, it, only, scope = globalThis, skip, } = {}) {
9
11
  this.#fileFactories = new CachedFactory((language) => language.prepare());
12
+ it = defaultTo(it, scope, "it");
13
+ if (!skip && "skip" in it && typeof it.skip === "function") {
14
+ skip = it.skip;
15
+ }
16
+ if (!only && "only" in it && typeof it.only === "function") {
17
+ only = it.only;
18
+ }
19
+ if (!skip) {
20
+ throw new TypeError("RuleTester needs a `skip` function");
21
+ }
22
+ if (!only) {
23
+ throw new TypeError("RuleTester needs a `only` function");
24
+ }
10
25
  this.#testerOptions = {
11
26
  describe: defaultTo(describe, scope, "describe"),
12
- it: defaultTo(it, scope, "it"),
27
+ it,
28
+ only,
13
29
  scope,
30
+ skip,
14
31
  };
15
32
  }
16
33
  describe(rule, { invalid, valid }) {
@@ -28,26 +45,41 @@ export class RuleTester {
28
45
  });
29
46
  }
30
47
  #itInvalidCase(rule, testCase) {
31
- this.#testerOptions.it(testCase.code, () => {
32
- const reports = runTestCaseRule(this.#fileFactories, {
48
+ const testCaseNormalized = normalizeTestCase(testCase);
49
+ let test = testCase.only
50
+ ? this.#testerOptions.only
51
+ : this.#testerOptions.it;
52
+ if (testCase.skip) {
53
+ if ("skip" in test && typeof test.skip === "function") {
54
+ test = test.skip;
55
+ }
56
+ else {
57
+ test = this.#testerOptions.skip;
58
+ }
59
+ }
60
+ test(testCase.code, async () => {
61
+ const reports = await runTestCaseRule(this.#fileFactories, {
33
62
  // TODO: Figure out a way around the type assertion...
34
- options: (testCase.options ?? {}),
63
+ options: testCase.options ?? {},
35
64
  rule,
36
- }, testCase);
37
- const actual = createReportSnapshot(testCase.code, reports);
38
- assert.equal(actual, testCase.snapshot);
65
+ }, testCaseNormalized);
66
+ const actualSnapshot = createReportSnapshot(testCase.code, reports);
67
+ assert.equal(actualSnapshot, testCase.snapshot);
68
+ const actualSuggestions = resolveReportedSuggestions(reports, testCaseNormalized);
69
+ assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
39
70
  });
40
71
  }
41
72
  #itValidCase(rule, testCaseRaw) {
42
73
  const testCase = typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
43
- this.#testerOptions.it(testCase.code, () => {
44
- const reports = runTestCaseRule(this.#fileFactories, {
74
+ const testCaseNormalized = normalizeTestCase(testCase);
75
+ this.#testerOptions.it(testCase.code, async () => {
76
+ const reports = await runTestCaseRule(this.#fileFactories, {
45
77
  // TODO: Figure out a way around the type assertion...
46
78
  options: (testCase.options ?? {}),
47
79
  rule,
48
- }, testCase);
80
+ }, testCaseNormalized);
49
81
  if (reports.length) {
50
- assert.deepStrictEqual(createReportSnapshot(testCase.code, reports), testCase.code);
82
+ assert.deepStrictEqual(createReportSnapshot(testCaseNormalized.code, reports), testCaseNormalized.code);
51
83
  }
52
84
  });
53
85
  }
@@ -1,2 +1,2 @@
1
- import { NormalizedRuleReport } from "@flint.fyi/core";
2
- export declare function createReportSnapshot(sourceText: string, reports: NormalizedRuleReport[]): string;
1
+ import { NormalizedReport } from "@flint.fyi/core";
2
+ export declare function createReportSnapshot(sourceText: string, reports: NormalizedReport[]): string;
@@ -10,7 +10,7 @@ function createReportSnapshotAt(sourceText, report) {
10
10
  const range = report.range;
11
11
  const lineEndIndex = ifNegative(sourceText.indexOf("\n", range.begin.raw), sourceText.length);
12
12
  const lineStartIndex = ifNegative(sourceText.lastIndexOf("\n", range.begin.raw), 0);
13
- const column = ifNegative(range.begin.raw - lineStartIndex - 2, 0);
13
+ const column = ifNegative(range.begin.raw - lineStartIndex - 1, 0);
14
14
  const width = range.end.raw - range.begin.raw;
15
15
  const injectionPrefix = " ".repeat(column);
16
16
  const injectedLines = [
package/lib/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { RuleTester } from "./RuleTester.js";
2
+ export type * from "./types.js";
@@ -0,0 +1,5 @@
1
+ import { TestCase } from "./types.js";
2
+ export interface TestCaseNormalized extends TestCase {
3
+ fileName: string;
4
+ }
5
+ export declare function normalizeTestCase<T extends TestCase>(testCase: T): T & TestCaseNormalized;
@@ -0,0 +1,6 @@
1
+ export function normalizeTestCase(testCase) {
2
+ return {
3
+ fileName: "file.ts",
4
+ ...testCase,
5
+ };
6
+ }
@@ -0,0 +1,2 @@
1
+ import { TestSuggestion, TestSuggestionForFiles } from "./types.js";
2
+ export declare function isTestSuggestionForFiles(suggestion: TestSuggestion): suggestion is TestSuggestionForFiles;
@@ -0,0 +1,3 @@
1
+ export function isTestSuggestionForFiles(suggestion) {
2
+ return "files" in suggestion;
3
+ }
@@ -0,0 +1,14 @@
1
+ import { NormalizedReport } from "@flint.fyi/core";
2
+ import { TestCaseNormalized } from "./normalizeTestCase.js";
3
+ import { InvalidTestCase, TestSuggestionFileCase } from "./types.js";
4
+ export declare function resolveReportedSuggestions(reports: NormalizedReport[], testCaseNormalized: InvalidTestCase & TestCaseNormalized): ({
5
+ files: {
6
+ [k: string]: TestSuggestionFileCase[];
7
+ };
8
+ id: string;
9
+ updated?: undefined;
10
+ } | {
11
+ id: string;
12
+ updated: string;
13
+ files?: undefined;
14
+ })[] | undefined;
@@ -0,0 +1,46 @@
1
+ import { applyChangesToText, isSuggestionForFiles, } from "@flint.fyi/core";
2
+ import { isTruthy } from "@flint.fyi/utils";
3
+ import { isTestSuggestionForFiles } from "./predicates.js";
4
+ export function resolveReportedSuggestions(reports, testCaseNormalized) {
5
+ const suggestionsReported = reports
6
+ .flatMap((report) => report.suggestions)
7
+ .filter(isTruthy);
8
+ if (!suggestionsReported.length) {
9
+ return undefined;
10
+ }
11
+ return suggestionsReported.map((suggestionReported) => isSuggestionForFiles(suggestionReported)
12
+ ? {
13
+ files: resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized),
14
+ id: suggestionReported.id,
15
+ }
16
+ : {
17
+ id: suggestionReported.id,
18
+ updated: applyChangesToText([suggestionReported], testCaseNormalized.code),
19
+ });
20
+ }
21
+ function resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized) {
22
+ if (!testCaseNormalized.suggestions) {
23
+ return {};
24
+ }
25
+ if (!testCaseNormalized.suggestions.every(isTestSuggestionForFiles)) {
26
+ throw new Error("This test case describes suggestions across files, but the rule is only reporting changes to its own file.");
27
+ }
28
+ return Object.fromEntries(testCaseNormalized.suggestions
29
+ .map((suggestionExpected) => {
30
+ return Object.entries(suggestionExpected.files).map(([filePath, suggestionCasesExpected]) => {
31
+ return [
32
+ filePath,
33
+ suggestionCasesExpected.map((suggestionCaseExpected) => {
34
+ const changes = suggestionReported.files[filePath]?.(suggestionCaseExpected.original);
35
+ return {
36
+ original: suggestionCaseExpected.original,
37
+ updated: changes
38
+ ? applyChangesToText(changes, suggestionCaseExpected.original)
39
+ : suggestionCaseExpected.original,
40
+ };
41
+ }),
42
+ ];
43
+ });
44
+ })
45
+ .flat());
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveReportedSuggestions } from "./resolveReportedSuggestions.js";
3
+ const mockReport = {
4
+ message: { primary: "", secondary: [], suggestions: [] },
5
+ range: {
6
+ begin: { column: 0, line: 1, raw: 0 },
7
+ end: { column: 3, line: 1, raw: 3 },
8
+ },
9
+ };
10
+ const mockTestCaseNormalized = {
11
+ code: "xyz",
12
+ fileName: "file.ts",
13
+ snapshot: "",
14
+ };
15
+ describe("resolveReportedSuggestions", () => {
16
+ it("returns undefined when reports is empty", () => {
17
+ const result = resolveReportedSuggestions([], mockTestCaseNormalized);
18
+ expect(result).toEqual(undefined);
19
+ });
20
+ it("returns undefined when given one report with no suggestions", () => {
21
+ const report = {
22
+ ...mockReport,
23
+ suggestions: [],
24
+ };
25
+ const result = resolveReportedSuggestions([report], mockTestCaseNormalized);
26
+ expect(result).toEqual(undefined);
27
+ });
28
+ it("returns id and updated text when given a single file suggestion", () => {
29
+ const suggestion = {
30
+ id: "suggestion",
31
+ range: { begin: 0, end: 3 },
32
+ text: "abc",
33
+ };
34
+ const report = {
35
+ ...mockReport,
36
+ suggestions: [suggestion],
37
+ };
38
+ const result = resolveReportedSuggestions([report], mockTestCaseNormalized);
39
+ expect(result).toEqual([
40
+ {
41
+ id: suggestion.id,
42
+ updated: suggestion.text,
43
+ },
44
+ ]);
45
+ });
46
+ it("throws when given a test case with no suggestions", () => {
47
+ const report = {
48
+ ...mockReport,
49
+ suggestions: [
50
+ {
51
+ files: {
52
+ "file.ts": () => [{ range: { begin: 0, end: 3 }, text: "def" }],
53
+ },
54
+ id: "suggestion-report",
55
+ },
56
+ ],
57
+ };
58
+ expect(() => resolveReportedSuggestions([report], {
59
+ ...mockTestCaseNormalized,
60
+ suggestions: [
61
+ {
62
+ id: "suggestion-result",
63
+ updated: "...",
64
+ },
65
+ ],
66
+ })).toThrowErrorMatchingInlineSnapshot(`[Error: This test case describes suggestions across files, but the rule is only reporting changes to its own file.]`);
67
+ });
68
+ it("throws when given a test case that doesn't have cross-file suggestions", () => {
69
+ const report = {
70
+ ...mockReport,
71
+ suggestions: [
72
+ {
73
+ files: {
74
+ "file.ts": () => [{ range: { begin: 0, end: 3 }, text: "def" }],
75
+ },
76
+ id: "suggestion-report",
77
+ },
78
+ ],
79
+ };
80
+ expect(() => resolveReportedSuggestions([report], {
81
+ ...mockTestCaseNormalized,
82
+ suggestions: [
83
+ {
84
+ id: "suggestion-result",
85
+ updated: "...",
86
+ },
87
+ ],
88
+ })).toThrowErrorMatchingInlineSnapshot(`[Error: This test case describes suggestions across files, but the rule is only reporting changes to its own file.]`);
89
+ });
90
+ it("returns id and a files object when given multi-file suggestions with a single file", () => {
91
+ const report = {
92
+ ...mockReport,
93
+ suggestions: [
94
+ {
95
+ files: {
96
+ "file.ts": () => [{ range: { begin: 0, end: 3 }, text: "def" }],
97
+ },
98
+ id: "suggestion-report",
99
+ },
100
+ ],
101
+ };
102
+ const result = resolveReportedSuggestions([report], {
103
+ ...mockTestCaseNormalized,
104
+ suggestions: [
105
+ {
106
+ files: {
107
+ "file.ts": [{ original: "abc", updated: "def" }],
108
+ },
109
+ id: "suggestion-result",
110
+ },
111
+ ],
112
+ });
113
+ expect(result).toEqual([
114
+ {
115
+ files: {
116
+ "file.ts": [
117
+ {
118
+ original: "abc",
119
+ updated: "def",
120
+ },
121
+ ],
122
+ },
123
+ id: "suggestion-report",
124
+ },
125
+ ]);
126
+ });
127
+ it("returns id and a files object when given multi-file suggestions with multiple file", () => {
128
+ const report = {
129
+ ...mockReport,
130
+ suggestions: [
131
+ {
132
+ files: {
133
+ "fileA.ts": () => [{ range: { begin: 0, end: 5 }, text: "def-A" }],
134
+ "fileB.ts": () => [{ range: { begin: 0, end: 5 }, text: "def-B" }],
135
+ },
136
+ id: "suggestion-report",
137
+ },
138
+ ],
139
+ };
140
+ const result = resolveReportedSuggestions([report], {
141
+ ...mockTestCaseNormalized,
142
+ suggestions: [
143
+ {
144
+ files: {
145
+ "fileA.ts": [{ original: "abc-A", updated: "def-A" }],
146
+ "fileB.ts": [{ original: "abc-B", updated: "def-B" }],
147
+ },
148
+ id: "suggestion-result",
149
+ },
150
+ ],
151
+ });
152
+ expect(result).toEqual([
153
+ {
154
+ files: {
155
+ "fileA.ts": [
156
+ {
157
+ original: "abc-A",
158
+ updated: "def-A",
159
+ },
160
+ ],
161
+ "fileB.ts": [
162
+ {
163
+ original: "abc-B",
164
+ updated: "def-B",
165
+ },
166
+ ],
167
+ },
168
+ id: "suggestion-report",
169
+ },
170
+ ]);
171
+ });
172
+ });
@@ -1,11 +1,8 @@
1
1
  import { AnyLanguage, AnyOptionalSchema, AnyRule, InferredObject, LanguageFileFactory, RuleAbout } from "@flint.fyi/core";
2
2
  import { CachedFactory } from "cached-factory";
3
- export interface NormalizedTestCase {
4
- code: string;
5
- fileName?: string;
6
- }
3
+ import { TestCaseNormalized } from "./normalizeTestCase.js";
7
4
  export interface TestCaseRuleConfiguration<OptionsSchema extends AnyOptionalSchema | undefined> {
8
5
  options?: InferredObject<OptionsSchema>;
9
6
  rule: AnyRule<RuleAbout, OptionsSchema>;
10
7
  }
11
- export declare function runTestCaseRule<OptionsSchema extends AnyOptionalSchema | undefined>(fileFactories: CachedFactory<AnyLanguage, LanguageFileFactory>, { options, rule }: Required<TestCaseRuleConfiguration<OptionsSchema>>, { code, fileName }: NormalizedTestCase): import("@flint.fyi/core").NormalizedRuleReport[];
8
+ export declare function runTestCaseRule<OptionsSchema extends AnyOptionalSchema | undefined>(fileFactories: CachedFactory<AnyLanguage, LanguageFileFactory>, { options, rule }: Required<TestCaseRuleConfiguration<OptionsSchema>>, { code, fileName }: TestCaseNormalized): import("@flint.fyi/core/lib/types/promises.js").PromiseOrSync<import("@flint.fyi/core").NormalizedReport[]>;
@@ -1,9 +1,9 @@
1
- export function runTestCaseRule(fileFactories, { options, rule }, { code, fileName = "file.ts" }) {
1
+ export function runTestCaseRule(fileFactories, { options, rule }, { code, fileName }) {
2
2
  using file = fileFactories
3
3
  // TODO: How to make types more permissive around assignability?
4
4
  // See AnyRule's any
5
5
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
6
6
  .get(rule.language)
7
- .prepareFileVirtually(fileName, code);
7
+ .prepareFromVirtual(fileName, code).file;
8
8
  return file.runRule(rule, options);
9
9
  }
package/lib/types.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ export interface InvalidTestCase<Options extends object | undefined = object | undefined> extends TestCase<Options> {
2
+ output?: string;
3
+ snapshot: string;
4
+ suggestions?: TestSuggestion[];
5
+ }
6
+ export interface TestCase<Options extends object | undefined = object | undefined> {
7
+ code: string;
8
+ fileName?: string;
9
+ /**
10
+ * Run only this test case. Useful for debugging.
11
+ *
12
+ * Do not commit code with this flag set.
13
+ */
14
+ only?: boolean;
15
+ options?: Options;
16
+ /**
17
+ * Skip running this test case. Useful for work-in-progress tests.
18
+ *
19
+ * Do not commit code with this flag set.
20
+ */
21
+ skip?: boolean;
22
+ }
23
+ export type TestSuggestion = TestSuggestionForFile | TestSuggestionForFiles;
24
+ export interface TestSuggestionFileCase {
25
+ original: string;
26
+ updated: string;
27
+ }
28
+ export interface TestSuggestionForFile {
29
+ id: string;
30
+ updated: string;
31
+ }
32
+ export interface TestSuggestionForFiles {
33
+ files: Record<string, TestSuggestionFileCase[]>;
34
+ id: string;
35
+ }
36
+ export type ValidTestCase<Options extends object | undefined> = string | ValidTestCaseObject<Options>;
37
+ export type ValidTestCaseObject<Options extends object | undefined> = TestCase<Options>;
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flint.fyi/rule-tester",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "[Experimental] Rule unit tests for Flint.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,6 +16,7 @@
16
16
  "main": "./lib/index.js",
17
17
  "dependencies": {
18
18
  "@flint.fyi/core": "",
19
+ "@flint.fyi/utils": "",
19
20
  "cached-factory": "^0.1.0"
20
21
  },
21
22
  "engines": {
package/src/RuleTester.ts CHANGED
@@ -3,21 +3,24 @@ import {
3
3
  AnyOptionalSchema,
4
4
  AnyRule,
5
5
  InferredObject,
6
- InvalidTestCase,
7
6
  LanguageFileFactory,
8
7
  RuleAbout,
9
- ValidTestCase,
10
8
  } from "@flint.fyi/core";
11
9
  import { CachedFactory } from "cached-factory";
12
10
  import assert from "node:assert";
13
11
 
14
12
  import { createReportSnapshot } from "./createReportSnapshot.js";
13
+ import { normalizeTestCase } from "./normalizeTestCase.js";
14
+ import { resolveReportedSuggestions } from "./resolveReportedSuggestions.js";
15
15
  import { runTestCaseRule } from "./runTestCaseRule.js";
16
+ import { InvalidTestCase, ValidTestCase } from "./types.js";
16
17
 
17
18
  export interface RuleTesterOptions {
18
- describe?: TesterSetup;
19
- it?: TesterSetup;
19
+ describe?: TesterSetupDescribe;
20
+ it?: TesterSetupIt;
21
+ only?: TesterSetupIt;
20
22
  scope?: Record<string, unknown>;
23
+ skip?: TesterSetupIt;
21
24
  }
22
25
 
23
26
  export interface TestCases<Options extends object | undefined> {
@@ -25,20 +28,52 @@ export interface TestCases<Options extends object | undefined> {
25
28
  valid: ValidTestCase<Options>[];
26
29
  }
27
30
 
28
- export type TesterSetup = (description: string, setup: () => void) => void;
31
+ export type TesterSetupDescribe = (
32
+ description: string,
33
+ setup: () => void,
34
+ ) => void;
35
+
36
+ export type TesterSetupIt = (
37
+ description: string,
38
+ setup: () => Promise<void>,
39
+ ) => void;
29
40
 
30
41
  export class RuleTester {
31
42
  #fileFactories: CachedFactory<AnyLanguage, LanguageFileFactory>;
32
43
  #testerOptions: Required<RuleTesterOptions>;
33
44
 
34
- constructor({ describe, it, scope = globalThis }: RuleTesterOptions = {}) {
45
+ constructor({
46
+ describe,
47
+ it,
48
+ only,
49
+ scope = globalThis,
50
+ skip,
51
+ }: RuleTesterOptions = {}) {
35
52
  this.#fileFactories = new CachedFactory((language: AnyLanguage) =>
36
53
  language.prepare(),
37
54
  );
55
+
56
+ it = defaultTo(it, scope, "it");
57
+
58
+ if (!skip && "skip" in it && typeof it.skip === "function") {
59
+ skip = it.skip as TesterSetupIt;
60
+ }
61
+ if (!only && "only" in it && typeof it.only === "function") {
62
+ only = it.only as TesterSetupIt;
63
+ }
64
+ if (!skip) {
65
+ throw new TypeError("RuleTester needs a `skip` function");
66
+ }
67
+ if (!only) {
68
+ throw new TypeError("RuleTester needs a `only` function");
69
+ }
70
+
38
71
  this.#testerOptions = {
39
72
  describe: defaultTo(describe, scope, "describe"),
40
- it: defaultTo(it, scope, "it"),
73
+ it,
74
+ only,
41
75
  scope,
76
+ skip,
42
77
  };
43
78
  }
44
79
 
@@ -65,19 +100,39 @@ export class RuleTester {
65
100
  rule: AnyRule<RuleAbout, OptionsSchema>,
66
101
  testCase: InvalidTestCase<InferredObject<OptionsSchema>>,
67
102
  ) {
68
- this.#testerOptions.it(testCase.code, () => {
69
- const reports = runTestCaseRule(
103
+ const testCaseNormalized = normalizeTestCase(testCase);
104
+
105
+ let test = testCase.only
106
+ ? this.#testerOptions.only
107
+ : this.#testerOptions.it;
108
+
109
+ if (testCase.skip) {
110
+ if ("skip" in test && typeof test.skip === "function") {
111
+ test = test.skip as TesterSetupIt;
112
+ } else {
113
+ test = this.#testerOptions.skip;
114
+ }
115
+ }
116
+
117
+ test(testCase.code, async () => {
118
+ const reports = await runTestCaseRule(
70
119
  this.#fileFactories,
71
120
  {
72
121
  // TODO: Figure out a way around the type assertion...
73
- options: (testCase.options ?? {}) as InferredObject<OptionsSchema>,
122
+ options: testCase.options ?? ({} as InferredObject<OptionsSchema>),
74
123
  rule,
75
124
  },
76
- testCase,
125
+ testCaseNormalized,
77
126
  );
78
- const actual = createReportSnapshot(testCase.code, reports);
127
+ const actualSnapshot = createReportSnapshot(testCase.code, reports);
79
128
 
80
- assert.equal(actual, testCase.snapshot);
129
+ assert.equal(actualSnapshot, testCase.snapshot);
130
+
131
+ const actualSuggestions = resolveReportedSuggestions(
132
+ reports,
133
+ testCaseNormalized,
134
+ );
135
+ assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
81
136
  });
82
137
  }
83
138
 
@@ -87,29 +142,30 @@ export class RuleTester {
87
142
  ) {
88
143
  const testCase =
89
144
  typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
145
+ const testCaseNormalized = normalizeTestCase(testCase);
90
146
 
91
- this.#testerOptions.it(testCase.code, () => {
92
- const reports = runTestCaseRule(
147
+ this.#testerOptions.it(testCase.code, async () => {
148
+ const reports = await runTestCaseRule(
93
149
  this.#fileFactories,
94
150
  {
95
151
  // TODO: Figure out a way around the type assertion...
96
152
  options: (testCase.options ?? {}) as InferredObject<OptionsSchema>,
97
153
  rule,
98
154
  },
99
- testCase,
155
+ testCaseNormalized,
100
156
  );
101
157
 
102
158
  if (reports.length) {
103
159
  assert.deepStrictEqual(
104
- createReportSnapshot(testCase.code, reports),
105
- testCase.code,
160
+ createReportSnapshot(testCaseNormalized.code, reports),
161
+ testCaseNormalized.code,
106
162
  );
107
163
  }
108
164
  });
109
165
  }
110
166
  }
111
167
 
112
- function defaultTo(
168
+ function defaultTo<TesterSetup extends TesterSetupDescribe | TesterSetupIt>(
113
169
  provided: TesterSetup | undefined,
114
170
  scope: Record<string, unknown>,
115
171
  scopeKey: string,