@flint.fyi/rule-tester 0.14.0 → 0.14.2
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 +14 -0
- package/lib/RuleTester.d.ts +5 -3
- package/lib/RuleTester.js +34 -5
- package/lib/createReportSnapshot.js +1 -1
- package/lib/predicates.d.ts +2 -0
- package/lib/predicates.js +3 -0
- package/lib/resolveReportedSuggestions.d.ts +8 -5
- package/lib/resolveReportedSuggestions.js +13 -10
- package/lib/resolveReportedSuggestions.test.d.ts +1 -0
- package/lib/resolveReportedSuggestions.test.js +172 -0
- package/lib/runTestCaseRule.d.ts +2 -2
- package/lib/types.d.ts +21 -4
- package/package.json +1 -1
- package/src/RuleTester.ts +52 -10
- package/src/createReportSnapshot.ts +1 -1
- package/src/predicates.ts +7 -0
- package/src/resolveReportedSuggestions.test.ts +201 -0
- package/src/resolveReportedSuggestions.ts +24 -13
- package/src/runTestCaseRule.ts +2 -2
- package/src/types.ts +26 -4
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @flint/rule-tester
|
|
2
2
|
|
|
3
|
+
## 0.14.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a3f9043: fix: respect \`only\` and \`skip\` options in valid RuleTester cases
|
|
8
|
+
- Updated dependencies [3d19082]
|
|
9
|
+
- @flint.fyi/core@0.15.1
|
|
10
|
+
|
|
11
|
+
## 0.14.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- b58d145: corrected logic and types for current-file suggestions
|
|
16
|
+
|
|
3
17
|
## 0.14.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/lib/RuleTester.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { AnyOptionalSchema, AnyRule,
|
|
1
|
+
import { AnyOptionalSchema, AnyRule, InferredObject, RuleAbout } from "@flint.fyi/core";
|
|
2
2
|
import { InvalidTestCase, ValidTestCase } from "./types.js";
|
|
3
3
|
export interface RuleTesterOptions {
|
|
4
4
|
describe?: TesterSetupDescribe;
|
|
5
5
|
it?: TesterSetupIt;
|
|
6
|
+
only?: TesterSetupIt;
|
|
6
7
|
scope?: Record<string, unknown>;
|
|
8
|
+
skip?: TesterSetupIt;
|
|
7
9
|
}
|
|
8
10
|
export interface TestCases<Options extends object | undefined> {
|
|
9
11
|
invalid: InvalidTestCase<Options>[];
|
|
@@ -13,6 +15,6 @@ export type TesterSetupDescribe = (description: string, setup: () => void) => vo
|
|
|
13
15
|
export type TesterSetupIt = (description: string, setup: () => Promise<void>) => void;
|
|
14
16
|
export declare class RuleTester {
|
|
15
17
|
#private;
|
|
16
|
-
constructor({ describe, it, scope }?: RuleTesterOptions);
|
|
17
|
-
describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<
|
|
18
|
+
constructor({ describe, it, only, scope, skip, }?: RuleTesterOptions);
|
|
19
|
+
describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<RuleAbout, OptionsSchema>, { invalid, valid }: TestCases<InferredObject<OptionsSchema>>): void;
|
|
18
20
|
}
|
package/lib/RuleTester.js
CHANGED
|
@@ -7,12 +7,27 @@ import { runTestCaseRule } from "./runTestCaseRule.js";
|
|
|
7
7
|
export class RuleTester {
|
|
8
8
|
#fileFactories;
|
|
9
9
|
#testerOptions;
|
|
10
|
-
constructor({ describe, it, scope = globalThis } = {}) {
|
|
10
|
+
constructor({ describe, it, only, scope = globalThis, skip, } = {}) {
|
|
11
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
|
+
}
|
|
12
25
|
this.#testerOptions = {
|
|
13
26
|
describe: defaultTo(describe, scope, "describe"),
|
|
14
|
-
it
|
|
27
|
+
it,
|
|
28
|
+
only,
|
|
15
29
|
scope,
|
|
30
|
+
skip,
|
|
16
31
|
};
|
|
17
32
|
}
|
|
18
33
|
describe(rule, { invalid, valid }) {
|
|
@@ -31,10 +46,10 @@ export class RuleTester {
|
|
|
31
46
|
}
|
|
32
47
|
#itInvalidCase(rule, testCase) {
|
|
33
48
|
const testCaseNormalized = normalizeTestCase(testCase);
|
|
34
|
-
this.#
|
|
49
|
+
this.#itTestCase(testCaseNormalized, async () => {
|
|
35
50
|
const reports = await runTestCaseRule(this.#fileFactories, {
|
|
36
51
|
// TODO: Figure out a way around the type assertion...
|
|
37
|
-
options:
|
|
52
|
+
options: testCase.options ?? {},
|
|
38
53
|
rule,
|
|
39
54
|
}, testCaseNormalized);
|
|
40
55
|
const actualSnapshot = createReportSnapshot(testCase.code, reports);
|
|
@@ -43,10 +58,24 @@ export class RuleTester {
|
|
|
43
58
|
assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
|
|
44
59
|
});
|
|
45
60
|
}
|
|
61
|
+
#itTestCase(testCase, setup) {
|
|
62
|
+
let test = testCase.only
|
|
63
|
+
? this.#testerOptions.only
|
|
64
|
+
: this.#testerOptions.it;
|
|
65
|
+
if (testCase.skip) {
|
|
66
|
+
if ("skip" in test && typeof test.skip === "function") {
|
|
67
|
+
test = test.skip;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
test = this.#testerOptions.skip;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
test(testCase.code, setup);
|
|
74
|
+
}
|
|
46
75
|
#itValidCase(rule, testCaseRaw) {
|
|
47
76
|
const testCase = typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
|
|
48
77
|
const testCaseNormalized = normalizeTestCase(testCase);
|
|
49
|
-
this.#
|
|
78
|
+
this.#itTestCase(testCaseNormalized, async () => {
|
|
50
79
|
const reports = await runTestCaseRule(this.#fileFactories, {
|
|
51
80
|
// TODO: Figure out a way around the type assertion...
|
|
52
81
|
options: (testCase.options ?? {}),
|
|
@@ -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 -
|
|
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 = [
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { NormalizedReport } from "@flint.fyi/core";
|
|
2
2
|
import { TestCaseNormalized } from "./normalizeTestCase.js";
|
|
3
3
|
import { InvalidTestCase, TestSuggestionFileCase } from "./types.js";
|
|
4
|
-
export declare function resolveReportedSuggestions(reports: NormalizedReport[], testCaseNormalized: InvalidTestCase & TestCaseNormalized): {
|
|
4
|
+
export declare function resolveReportedSuggestions(reports: NormalizedReport[], testCaseNormalized: InvalidTestCase & TestCaseNormalized): ({
|
|
5
5
|
files: {
|
|
6
6
|
[k: string]: TestSuggestionFileCase[];
|
|
7
|
-
}
|
|
8
|
-
[testCaseNormalized.fileName]: import("@flint.fyi/core").SuggestionForFile;
|
|
9
|
-
}[];
|
|
7
|
+
};
|
|
10
8
|
id: string;
|
|
11
|
-
|
|
9
|
+
updated?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
id: string;
|
|
12
|
+
updated: string;
|
|
13
|
+
files?: undefined;
|
|
14
|
+
})[] | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyChangesToText, isSuggestionForFiles, } from "@flint.fyi/core";
|
|
2
2
|
import { isTruthy } from "@flint.fyi/utils";
|
|
3
|
+
import { isTestSuggestionForFiles } from "./predicates.js";
|
|
3
4
|
export function resolveReportedSuggestions(reports, testCaseNormalized) {
|
|
4
5
|
const suggestionsReported = reports
|
|
5
6
|
.flatMap((report) => report.suggestions)
|
|
@@ -7,21 +8,23 @@ export function resolveReportedSuggestions(reports, testCaseNormalized) {
|
|
|
7
8
|
if (!suggestionsReported.length) {
|
|
8
9
|
return undefined;
|
|
9
10
|
}
|
|
10
|
-
return suggestionsReported.map((suggestionReported) => (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
],
|
|
18
|
-
|
|
19
|
-
}));
|
|
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
20
|
}
|
|
21
21
|
function resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized) {
|
|
22
22
|
if (!testCaseNormalized.suggestions) {
|
|
23
23
|
return {};
|
|
24
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
|
+
}
|
|
25
28
|
return Object.fromEntries(testCaseNormalized.suggestions
|
|
26
29
|
.map((suggestionExpected) => {
|
|
27
30
|
return Object.entries(suggestionExpected.files).map(([filePath, suggestionCasesExpected]) => {
|
|
@@ -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
|
+
});
|
package/lib/runTestCaseRule.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { AnyLanguage, AnyOptionalSchema, AnyRule,
|
|
1
|
+
import { AnyLanguage, AnyOptionalSchema, AnyRule, InferredObject, LanguageFileFactory, RuleAbout } from "@flint.fyi/core";
|
|
2
2
|
import { CachedFactory } from "cached-factory";
|
|
3
3
|
import { TestCaseNormalized } from "./normalizeTestCase.js";
|
|
4
4
|
export interface TestCaseRuleConfiguration<OptionsSchema extends AnyOptionalSchema | undefined> {
|
|
5
5
|
options?: InferredObject<OptionsSchema>;
|
|
6
|
-
rule: AnyRule<
|
|
6
|
+
rule: AnyRule<RuleAbout, OptionsSchema>;
|
|
7
7
|
}
|
|
8
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[]>;
|
package/lib/types.d.ts
CHANGED
|
@@ -6,15 +6,32 @@ export interface InvalidTestCase<Options extends object | undefined = object | u
|
|
|
6
6
|
export interface TestCase<Options extends object | undefined = object | undefined> {
|
|
7
7
|
code: string;
|
|
8
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;
|
|
9
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;
|
|
10
22
|
}
|
|
11
|
-
export
|
|
12
|
-
files: Record<string, TestSuggestionFileCase[]>;
|
|
13
|
-
id: string;
|
|
14
|
-
}
|
|
23
|
+
export type TestSuggestion = TestSuggestionForFile | TestSuggestionForFiles;
|
|
15
24
|
export interface TestSuggestionFileCase {
|
|
16
25
|
original: string;
|
|
17
26
|
updated: string;
|
|
18
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
|
+
}
|
|
19
36
|
export type ValidTestCase<Options extends object | undefined> = string | ValidTestCaseObject<Options>;
|
|
20
37
|
export type ValidTestCaseObject<Options extends object | undefined> = TestCase<Options>;
|
package/package.json
CHANGED
package/src/RuleTester.ts
CHANGED
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
AnyLanguage,
|
|
3
3
|
AnyOptionalSchema,
|
|
4
4
|
AnyRule,
|
|
5
|
-
BaseAbout,
|
|
6
5
|
InferredObject,
|
|
7
6
|
LanguageFileFactory,
|
|
7
|
+
RuleAbout,
|
|
8
8
|
} from "@flint.fyi/core";
|
|
9
9
|
import { CachedFactory } from "cached-factory";
|
|
10
10
|
import assert from "node:assert";
|
|
@@ -13,12 +13,14 @@ import { createReportSnapshot } from "./createReportSnapshot.js";
|
|
|
13
13
|
import { normalizeTestCase } from "./normalizeTestCase.js";
|
|
14
14
|
import { resolveReportedSuggestions } from "./resolveReportedSuggestions.js";
|
|
15
15
|
import { runTestCaseRule } from "./runTestCaseRule.js";
|
|
16
|
-
import { InvalidTestCase, ValidTestCase } from "./types.js";
|
|
16
|
+
import { InvalidTestCase, TestCase, ValidTestCase } from "./types.js";
|
|
17
17
|
|
|
18
18
|
export interface RuleTesterOptions {
|
|
19
19
|
describe?: TesterSetupDescribe;
|
|
20
20
|
it?: TesterSetupIt;
|
|
21
|
+
only?: TesterSetupIt;
|
|
21
22
|
scope?: Record<string, unknown>;
|
|
23
|
+
skip?: TesterSetupIt;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface TestCases<Options extends object | undefined> {
|
|
@@ -40,19 +42,43 @@ export class RuleTester {
|
|
|
40
42
|
#fileFactories: CachedFactory<AnyLanguage, LanguageFileFactory>;
|
|
41
43
|
#testerOptions: Required<RuleTesterOptions>;
|
|
42
44
|
|
|
43
|
-
constructor({
|
|
45
|
+
constructor({
|
|
46
|
+
describe,
|
|
47
|
+
it,
|
|
48
|
+
only,
|
|
49
|
+
scope = globalThis,
|
|
50
|
+
skip,
|
|
51
|
+
}: RuleTesterOptions = {}) {
|
|
44
52
|
this.#fileFactories = new CachedFactory((language: AnyLanguage) =>
|
|
45
53
|
language.prepare(),
|
|
46
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
|
+
|
|
47
71
|
this.#testerOptions = {
|
|
48
72
|
describe: defaultTo(describe, scope, "describe"),
|
|
49
|
-
it
|
|
73
|
+
it,
|
|
74
|
+
only,
|
|
50
75
|
scope,
|
|
76
|
+
skip,
|
|
51
77
|
};
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
describe<OptionsSchema extends AnyOptionalSchema | undefined>(
|
|
55
|
-
rule: AnyRule<
|
|
81
|
+
rule: AnyRule<RuleAbout, OptionsSchema>,
|
|
56
82
|
{ invalid, valid }: TestCases<InferredObject<OptionsSchema>>,
|
|
57
83
|
) {
|
|
58
84
|
this.#testerOptions.describe(rule.about.id, () => {
|
|
@@ -71,17 +97,17 @@ export class RuleTester {
|
|
|
71
97
|
}
|
|
72
98
|
|
|
73
99
|
#itInvalidCase<OptionsSchema extends AnyOptionalSchema | undefined>(
|
|
74
|
-
rule: AnyRule<
|
|
100
|
+
rule: AnyRule<RuleAbout, OptionsSchema>,
|
|
75
101
|
testCase: InvalidTestCase<InferredObject<OptionsSchema>>,
|
|
76
102
|
) {
|
|
77
103
|
const testCaseNormalized = normalizeTestCase(testCase);
|
|
78
104
|
|
|
79
|
-
this.#
|
|
105
|
+
this.#itTestCase(testCaseNormalized, async () => {
|
|
80
106
|
const reports = await runTestCaseRule(
|
|
81
107
|
this.#fileFactories,
|
|
82
108
|
{
|
|
83
109
|
// TODO: Figure out a way around the type assertion...
|
|
84
|
-
options:
|
|
110
|
+
options: testCase.options ?? ({} as InferredObject<OptionsSchema>),
|
|
85
111
|
rule,
|
|
86
112
|
},
|
|
87
113
|
testCaseNormalized,
|
|
@@ -98,15 +124,31 @@ export class RuleTester {
|
|
|
98
124
|
});
|
|
99
125
|
}
|
|
100
126
|
|
|
127
|
+
#itTestCase(testCase: TestCase, setup: () => Promise<void>) {
|
|
128
|
+
let test = testCase.only
|
|
129
|
+
? this.#testerOptions.only
|
|
130
|
+
: this.#testerOptions.it;
|
|
131
|
+
|
|
132
|
+
if (testCase.skip) {
|
|
133
|
+
if ("skip" in test && typeof test.skip === "function") {
|
|
134
|
+
test = test.skip as TesterSetupIt;
|
|
135
|
+
} else {
|
|
136
|
+
test = this.#testerOptions.skip;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test(testCase.code, setup);
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
#itValidCase<OptionsSchema extends AnyOptionalSchema | undefined>(
|
|
102
|
-
rule: AnyRule<
|
|
144
|
+
rule: AnyRule<RuleAbout, OptionsSchema>,
|
|
103
145
|
testCaseRaw: ValidTestCase<InferredObject<OptionsSchema>>,
|
|
104
146
|
) {
|
|
105
147
|
const testCase =
|
|
106
148
|
typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
|
|
107
149
|
const testCaseNormalized = normalizeTestCase(testCase);
|
|
108
150
|
|
|
109
|
-
this.#
|
|
151
|
+
this.#itTestCase(testCaseNormalized, async () => {
|
|
110
152
|
const reports = await runTestCaseRule(
|
|
111
153
|
this.#fileFactories,
|
|
112
154
|
{
|
|
@@ -25,7 +25,7 @@ function createReportSnapshotAt(sourceText: string, report: NormalizedReport) {
|
|
|
25
25
|
0,
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
const column = ifNegative(range.begin.raw - lineStartIndex -
|
|
28
|
+
const column = ifNegative(range.begin.raw - lineStartIndex - 1, 0);
|
|
29
29
|
const width = range.end.raw - range.begin.raw;
|
|
30
30
|
|
|
31
31
|
const injectionPrefix = " ".repeat(column);
|