66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9+ /**
10+ * @fileoverview This is the main entry point for the Jasmine to Vitest transformation.
11+ * It orchestrates the application of various AST transformers to convert Jasmine test
12+ * syntax and APIs to their Vitest equivalents. It also handles import management,
13+ * blank line preservation, and reporting of transformation details.
14+ */
15+
916import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript' ;
1017import {
1118 transformDoneCallback ,
@@ -43,28 +50,102 @@ import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers'
4350import { RefactorContext } from './utils/refactor-context' ;
4451import { RefactorReporter } from './utils/refactor-reporter' ;
4552
53+ /**
54+ * A placeholder used to temporarily replace blank lines in the source code.
55+ * This is necessary because TypeScript's printer removes blank lines by default.
56+ */
4657const BLANK_LINE_PLACEHOLDER = '// __PRESERVE_BLANK_LINE__' ;
4758
59+ /**
60+ * Replaces blank lines in the content with a placeholder to prevent TypeScript's printer
61+ * from removing them. This ensures that the original formatting of blank lines is preserved.
62+ * @param content The source code content.
63+ * @returns The content with blank lines replaced by placeholders.
64+ */
4865function preserveBlankLines ( content : string ) : string {
4966 return content
5067 . split ( '\n' )
5168 . map ( ( line ) => ( line . trim ( ) === '' ? BLANK_LINE_PLACEHOLDER : line ) )
5269 . join ( '\n' ) ;
5370}
5471
72+ /**
73+ * Restores blank lines in the content by replacing the placeholder with actual blank lines.
74+ * This is called after TypeScript's printer has processed the file.
75+ * @param content The content with blank line placeholders.
76+ * @returns The content with blank lines restored.
77+ */
5578function restoreBlankLines ( content : string ) : string {
56- const regex = new RegExp ( `^\\s* ${ BLANK_LINE_PLACEHOLDER . replace ( / \/ / g , '\\/' ) } \\ s*$` , 'gm' ) ;
79+ const regex = / ^ \s * \ /\/ _ _ P R E S E R V E _ B L A N K _ L I N E _ _ \ s* $ / gm ;
5780
5881 return content . replace ( regex , '' ) ;
5982}
6083
84+ /**
85+ * A collection of transformers that operate on `ts.CallExpression` nodes.
86+ * These are applied in stages to ensure correct order of operations:
87+ * 1. High-Level & Context-Sensitive: Transformations that fundamentally change the call.
88+ * 2. Core Matcher & Spy: Bulk conversions for `expect(...)` and `spyOn(...)`.
89+ * 3. Global Functions & Cleanup: Handles global Jasmine functions and unsupported APIs.
90+ */
91+ const callExpressionTransformers = [
92+ // **Stage 1: High-Level & Context-Sensitive Transformations**
93+ // These transformers often wrap or fundamentally change the nature of the call,
94+ // so they need to run before more specific matchers.
95+ transformWithContext ,
96+ transformExpectAsync ,
97+ transformFocusedAndSkippedTests ,
98+ transformPending ,
99+ transformDoneCallback ,
100+
101+ // **Stage 2: Core Matcher & Spy Transformations**
102+ // This is the bulk of the `expect(...)` and `spyOn(...)` conversions.
103+ transformSyntacticSugarMatchers ,
104+ transformComplexMatchers ,
105+ transformSpies ,
106+ transformCreateSpyObj ,
107+ transformSpyReset ,
108+ transformSpyCallInspection ,
109+ transformtoHaveBeenCalledBefore ,
110+ transformToHaveClass ,
111+
112+ // **Stage 3: Global Functions & Cleanup**
113+ // These handle global Jasmine functions and catch-alls for unsupported APIs.
114+ transformTimerMocks ,
115+ transformGlobalFunctions ,
116+ transformUnsupportedJasmineCalls ,
117+ ] ;
118+
119+ /**
120+ * A collection of transformers that operate on `ts.PropertyAccessExpression` nodes.
121+ * These primarily handle `jasmine.any()` and other `jasmine.*` properties.
122+ */
123+ const propertyAccessExpressionTransformers = [
124+ // These transformers handle `jasmine.any()` and other `jasmine.*` properties.
125+ transformAsymmetricMatchers ,
126+ transformSpyCallInspection ,
127+ transformUnknownJasmineProperties ,
128+ ] ;
129+
130+ /**
131+ * A collection of transformers that operate on `ts.ExpressionStatement` nodes.
132+ * These are mutually exclusive; the first one that matches will be applied.
133+ */
134+ const expressionStatementTransformers = [
135+ transformCalledOnceWith ,
136+ transformArrayWithExactContents ,
137+ transformExpectNothing ,
138+ transformFail ,
139+ transformDefaultTimeoutInterval ,
140+ ] ;
141+
61142/**
62143 * Transforms a string of Jasmine test code to Vitest test code.
63144 * This is the main entry point for the transformation.
64145 * @param filePath The path to the file being transformed.
65146 * @param content The source code to transform.
66147 * @param reporter The reporter to track TODOs.
67- * @param options Transformation options.
148+ * @param options Transformation options, including whether to add Vitest API imports .
68149 * @returns The transformed code.
69150 */
70151export function transformJasmineToVitest (
@@ -85,6 +166,7 @@ export function transformJasmineToVitest(
85166
86167 const pendingVitestValueImports = new Set < string > ( ) ;
87168 const pendingVitestTypeImports = new Set < string > ( ) ;
169+
88170 const transformer : ts . TransformerFactory < ts . SourceFile > = ( context ) => {
89171 const refactorCtx : RefactorContext = {
90172 sourceFile,
@@ -106,59 +188,17 @@ export function transformJasmineToVitest(
106188 }
107189 }
108190
109- const transformations = [
110- // **Stage 1: High-Level & Context-Sensitive Transformations**
111- // These transformers often wrap or fundamentally change the nature of the call,
112- // so they need to run before more specific matchers.
113- transformWithContext ,
114- transformExpectAsync ,
115- transformFocusedAndSkippedTests ,
116- transformPending ,
117- transformDoneCallback ,
118-
119- // **Stage 2: Core Matcher & Spy Transformations**
120- // This is the bulk of the `expect(...)` and `spyOn(...)` conversions.
121- transformSyntacticSugarMatchers ,
122- transformComplexMatchers ,
123- transformSpies ,
124- transformCreateSpyObj ,
125- transformSpyReset ,
126- transformSpyCallInspection ,
127- transformtoHaveBeenCalledBefore ,
128- transformToHaveClass ,
129-
130- // **Stage 3: Global Functions & Cleanup**
131- // These handle global Jasmine functions and catch-alls for unsupported APIs.
132- transformTimerMocks ,
133- transformGlobalFunctions ,
134- transformUnsupportedJasmineCalls ,
135- ] ;
136-
137- for ( const transformer of transformations ) {
191+ for ( const transformer of callExpressionTransformers ) {
138192 transformedNode = transformer ( transformedNode , refactorCtx ) ;
139193 }
140194 } else if ( ts . isPropertyAccessExpression ( transformedNode ) ) {
141- const transformations = [
142- // These transformers handle `jasmine.any()` and other `jasmine.*` properties.
143- transformAsymmetricMatchers ,
144- transformSpyCallInspection ,
145- transformUnknownJasmineProperties ,
146- ] ;
147- for ( const transformer of transformations ) {
195+ for ( const transformer of propertyAccessExpressionTransformers ) {
148196 transformedNode = transformer ( transformedNode , refactorCtx ) ;
149197 }
150198 } else if ( ts . isExpressionStatement ( transformedNode ) ) {
151199 // Statement-level transformers are mutually exclusive. The first one that
152200 // matches will be applied, and then the visitor will stop for this node.
153- const statementTransformers = [
154- transformCalledOnceWith ,
155- transformArrayWithExactContents ,
156- transformExpectNothing ,
157- transformFail ,
158- transformDefaultTimeoutInterval ,
159- ] ;
160-
161- for ( const transformer of statementTransformers ) {
201+ for ( const transformer of expressionStatementTransformers ) {
162202 const result = transformer ( transformedNode , refactorCtx ) ;
163203 if ( result !== transformedNode ) {
164204 transformedNode = result ;
0 commit comments