forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-file-transformer.ts
More file actions
270 lines (243 loc) · 9.15 KB
/
test-file-transformer.ts
File metadata and controls
270 lines (243 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* @fileoverview This is the main entry point for the Jasmine to Vitest transformation.
* It orchestrates the application of various AST transformers to convert Jasmine test
* syntax and APIs to their Vitest equivalents. It also handles import management,
* blank line preservation, and reporting of transformation details.
*/
import ts from '../../third_party/typescript';
import {
transformDoneCallback,
transformFocusedAndSkippedTests,
transformPending,
} from './transformers/jasmine-lifecycle';
import {
transformArrayWithExactContents,
transformAsymmetricMatchers,
transformCalledOnceWith,
transformComplexMatchers,
transformExpectAsync,
transformExpectNothing,
transformSyntacticSugarMatchers,
transformToHaveClass,
transformWithContext,
transformtoHaveBeenCalledBefore,
} from './transformers/jasmine-matcher';
import {
transformDefaultTimeoutInterval,
transformFail,
transformGlobalFunctions,
transformTimerMocks,
transformUnknownJasmineProperties,
transformUnsupportedJasmineCalls,
} from './transformers/jasmine-misc';
import {
transformCreateSpyObj,
transformSpies,
transformSpyCallInspection,
transformSpyReset,
} from './transformers/jasmine-spy';
import { transformJasmineTypes } from './transformers/jasmine-type';
import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers';
import { RefactorContext } from './utils/refactor-context';
import { RefactorReporter } from './utils/refactor-reporter';
/**
* A placeholder used to temporarily replace blank lines in the source code.
* This is necessary because TypeScript's printer removes blank lines by default.
*/
const BLANK_LINE_PLACEHOLDER = '// __PRESERVE_BLANK_LINE__';
/**
* Vitest function names that should be imported when using the --add-imports option.
*/
const VITEST_FUNCTION_NAMES = new Set([
'describe',
'it',
'expect',
'beforeEach',
'afterEach',
'beforeAll',
'afterAll',
]);
/**
* Replaces blank lines in the content with a placeholder to prevent TypeScript's printer
* from removing them. This ensures that the original formatting of blank lines is preserved.
* @param content The source code content.
* @returns The content with blank lines replaced by placeholders.
*/
function preserveBlankLines(content: string): string {
return content
.split('\n')
.map((line) => (line.trim() === '' ? BLANK_LINE_PLACEHOLDER : line))
.join('\n');
}
/**
* Restores blank lines in the content by replacing the placeholder with actual blank lines.
* This is called after TypeScript's printer has processed the file.
* @param content The content with blank line placeholders.
* @returns The content with blank lines restored.
*/
function restoreBlankLines(content: string): string {
const regex = /^\s*\/\/ __PRESERVE_BLANK_LINE__\s*$/gm;
return content.replace(regex, '');
}
/**
* A collection of transformers that operate on `ts.CallExpression` nodes.
* These are applied in stages to ensure correct order of operations:
* 1. High-Level & Context-Sensitive: Transformations that fundamentally change the call.
* 2. Core Matcher & Spy: Bulk conversions for `expect(...)` and `spyOn(...)`.
* 3. Global Functions & Cleanup: Handles global Jasmine functions and unsupported APIs.
*/
const callExpressionTransformers = [
// **Stage 1: High-Level & Context-Sensitive Transformations**
// These transformers often wrap or fundamentally change the nature of the call,
// so they need to run before more specific matchers.
transformWithContext,
transformExpectAsync,
transformFocusedAndSkippedTests,
transformPending,
transformDoneCallback,
// **Stage 2: Core Matcher & Spy Transformations**
// This is the bulk of the `expect(...)` and `spyOn(...)` conversions.
transformSyntacticSugarMatchers,
transformComplexMatchers,
transformSpies,
transformCreateSpyObj,
transformSpyReset,
transformSpyCallInspection,
transformtoHaveBeenCalledBefore,
transformToHaveClass,
// **Stage 3: Global Functions & Cleanup**
// These handle global Jasmine functions and catch-alls for unsupported APIs.
transformTimerMocks,
transformGlobalFunctions,
transformUnsupportedJasmineCalls,
];
/**
* A collection of transformers that operate on `ts.PropertyAccessExpression` nodes.
* These primarily handle `jasmine.any()` and other `jasmine.*` properties.
*/
const propertyAccessExpressionTransformers = [
// These transformers handle `jasmine.any()` and other `jasmine.*` properties.
transformAsymmetricMatchers,
transformSpyCallInspection,
transformUnknownJasmineProperties,
];
/**
* A collection of transformers that operate on `ts.ExpressionStatement` nodes.
* These are mutually exclusive; the first one that matches will be applied.
*/
const expressionStatementTransformers = [
transformCalledOnceWith,
transformArrayWithExactContents,
transformExpectNothing,
transformFail,
transformDefaultTimeoutInterval,
];
/**
* Transforms a string of Jasmine test code to Vitest test code.
* This is the main entry point for the transformation.
* @param filePath The path to the file being transformed.
* @param content The source code to transform.
* @param reporter The reporter to track TODOs.
* @param options Transformation options, including whether to add Vitest API imports.
* @returns The transformed code.
*/
export function transformJasmineToVitest(
filePath: string,
content: string,
reporter: RefactorReporter,
options: { addImports: boolean; browserMode: boolean },
): string {
const contentWithPlaceholders = preserveBlankLines(content);
const sourceFile = ts.createSourceFile(
filePath,
contentWithPlaceholders,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const pendingVitestValueImports = new Set<string>();
const pendingVitestTypeImports = new Set<string>();
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
const refactorCtx: RefactorContext = {
sourceFile,
reporter,
tsContext: context,
pendingVitestValueImports,
pendingVitestTypeImports,
};
const visitor: ts.Visitor = (node) => {
let transformedNode: ts.Node | readonly ts.Node[] = node;
// Transform the node itself based on its type
if (ts.isCallExpression(transformedNode)) {
if (options.addImports && ts.isIdentifier(transformedNode.expression)) {
const name = transformedNode.expression.text;
if (VITEST_FUNCTION_NAMES.has(name)) {
addVitestValueImport(pendingVitestValueImports, name);
}
}
for (const transformer of callExpressionTransformers) {
if (!(options.browserMode && transformer === transformToHaveClass)) {
transformedNode = transformer(transformedNode, refactorCtx);
}
}
} else if (ts.isPropertyAccessExpression(transformedNode)) {
for (const transformer of propertyAccessExpressionTransformers) {
transformedNode = transformer(transformedNode, refactorCtx);
}
} else if (ts.isExpressionStatement(transformedNode)) {
// Statement-level transformers are mutually exclusive. The first one that
// matches will be applied, and then the visitor will stop for this node.
for (const transformer of expressionStatementTransformers) {
const result = transformer(transformedNode, refactorCtx);
if (result !== transformedNode) {
transformedNode = result;
break;
}
}
} else if (ts.isQualifiedName(transformedNode) || ts.isTypeReferenceNode(transformedNode)) {
transformedNode = transformJasmineTypes(transformedNode, refactorCtx);
}
// Visit the children of the node to ensure they are transformed
if (Array.isArray(transformedNode)) {
return transformedNode.map((node) => ts.visitEachChild(node, visitor, context));
} else {
return ts.visitEachChild(transformedNode as ts.Node, visitor, context);
}
};
return (node) => ts.visitEachChild(node, visitor, context);
};
const result = ts.transform(sourceFile, [transformer]);
let transformedSourceFile = result.transformed[0];
const hasPendingValueImports = pendingVitestValueImports.size > 0;
const hasPendingTypeImports = pendingVitestTypeImports.size > 0;
if (
transformedSourceFile === sourceFile &&
!reporter.hasTodos &&
!hasPendingValueImports &&
!hasPendingTypeImports
) {
return content;
}
if (hasPendingTypeImports || (options.addImports && hasPendingValueImports)) {
const vitestImport = getVitestAutoImports(
options.addImports ? pendingVitestValueImports : new Set(),
pendingVitestTypeImports,
);
if (vitestImport) {
transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [
vitestImport,
...transformedSourceFile.statements,
]);
}
}
const printer = ts.createPrinter();
const transformedContentWithPlaceholders = printer.printFile(transformedSourceFile);
return restoreBlankLines(transformedContentWithPlaceholders);
}