|
6 | 6 | * found in the LICENSE file at https://angular.dev/license |
7 | 7 | */ |
8 | 8 |
|
9 | | -import { exec } from 'child_process'; |
10 | | -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises'; |
| 9 | +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises'; |
11 | 10 | import { tmpdir } from 'os'; |
12 | 11 | import { join } from 'path'; |
13 | | -import { promisify } from 'util'; |
14 | 12 | import { ModernizeOutput, runModernization } from './modernize'; |
15 | | - |
16 | | -const execAsync = promisify(exec); |
| 13 | +import * as processExecutor from './process-executor'; |
17 | 14 |
|
18 | 15 | describe('Modernize Tool', () => { |
| 16 | + let execAsyncSpy: jasmine.Spy; |
19 | 17 | let projectDir: string; |
20 | | - let originalPath: string | undefined; |
21 | 18 |
|
22 | 19 | beforeEach(async () => { |
23 | | - originalPath = process.env.PATH; |
| 20 | + // Create a temporary directory and a fake angular.json to satisfy the tool's project root search. |
24 | 21 | projectDir = await mkdtemp(join(tmpdir(), 'angular-modernize-test-')); |
| 22 | + await writeFile(join(projectDir, 'angular.json'), JSON.stringify({ version: 1, projects: {} })); |
25 | 23 |
|
26 | | - // Create a dummy Angular project structure. |
27 | | - await writeFile( |
28 | | - join(projectDir, 'angular.json'), |
29 | | - JSON.stringify( |
30 | | - { |
31 | | - version: 1, |
32 | | - projects: { |
33 | | - app: { |
34 | | - root: '', |
35 | | - projectType: 'application', |
36 | | - architect: { |
37 | | - build: { |
38 | | - options: { |
39 | | - tsConfig: 'tsconfig.json', |
40 | | - }, |
41 | | - }, |
42 | | - }, |
43 | | - }, |
44 | | - }, |
45 | | - }, |
46 | | - null, |
47 | | - 2, |
48 | | - ), |
49 | | - ); |
50 | | - await writeFile( |
51 | | - join(projectDir, 'package.json'), |
52 | | - JSON.stringify( |
53 | | - { |
54 | | - dependencies: { |
55 | | - '@angular/core': 'latest', |
56 | | - }, |
57 | | - devDependencies: { |
58 | | - '@angular/cli': 'latest', |
59 | | - '@angular-devkit/schematics': 'latest', |
60 | | - typescript: 'latest', |
61 | | - }, |
62 | | - }, |
63 | | - null, |
64 | | - 2, |
65 | | - ), |
66 | | - ); |
67 | | - await writeFile( |
68 | | - join(projectDir, 'tsconfig.base.json'), |
69 | | - JSON.stringify( |
70 | | - { |
71 | | - compilerOptions: { |
72 | | - strict: true, |
73 | | - forceConsistentCasingInFileNames: true, |
74 | | - skipLibCheck: true, |
75 | | - }, |
76 | | - }, |
77 | | - null, |
78 | | - 2, |
79 | | - ), |
80 | | - ); |
81 | | - await writeFile( |
82 | | - join(projectDir, 'tsconfig.json'), |
83 | | - JSON.stringify( |
84 | | - { |
85 | | - extends: './tsconfig.base.json', |
86 | | - compilerOptions: { |
87 | | - outDir: './dist/out-tsc', |
88 | | - }, |
89 | | - }, |
90 | | - null, |
91 | | - 2, |
92 | | - ), |
93 | | - ); |
94 | | - |
95 | | - // Symlink the node_modules directory from the runfiles to the temporary project. |
96 | | - const nodeModulesPath = require |
97 | | - .resolve('@angular/core/package.json') |
98 | | - .replace(/\/@angular\/core\/package\.json$/, ''); |
99 | | - await execAsync(`ln -s ${nodeModulesPath} ${join(projectDir, 'node_modules')}`); |
100 | | - |
101 | | - // Prepend the node_modules/.bin path to the PATH environment variable |
102 | | - // so that `ng` can be found by `execAsync` calls. |
103 | | - process.env.PATH = `${join(nodeModulesPath, '.bin')}:${process.env.PATH}`; |
| 24 | + // Spy on the execAsync function from our new module. |
| 25 | + execAsyncSpy = spyOn(processExecutor, 'execAsync').and.resolveTo({ stdout: '', stderr: '' }); |
104 | 26 | }); |
105 | 27 |
|
106 | 28 | afterEach(async () => { |
107 | | - process.env.PATH = originalPath; |
108 | 29 | await rm(projectDir, { recursive: true, force: true }); |
109 | 30 | }); |
110 | 31 |
|
111 | | - async function modernize( |
112 | | - dir: string, |
113 | | - file: string, |
114 | | - transformations: string[], |
115 | | - ): Promise<{ structuredContent: ModernizeOutput; newContent: string }> { |
116 | | - const structuredContent = ( |
117 | | - (await runModernization({ directories: [dir], transformations })) as { |
118 | | - structuredContent: ModernizeOutput; |
119 | | - } |
120 | | - ).structuredContent; |
121 | | - const newContent = await readFile(file, 'utf8'); |
122 | | - |
123 | | - return { structuredContent, newContent }; |
124 | | - } |
| 32 | + it('should return instructions if no transformations are provided', async () => { |
| 33 | + const { structuredContent } = (await runModernization({})) as { |
| 34 | + structuredContent: ModernizeOutput; |
| 35 | + }; |
125 | 36 |
|
126 | | - it('can run a single transformation', async () => { |
127 | | - const componentPath = join(projectDir, 'test.component.ts'); |
128 | | - const componentContent = ` |
129 | | - import { Component } from '@angular/core'; |
130 | | -
|
131 | | - @Component({ |
132 | | - selector: 'app-foo', |
133 | | - template: '<app-bar></app-bar>', |
134 | | - }) |
135 | | - export class FooComponent {} |
136 | | - `; |
137 | | - await writeFile(componentPath, componentContent); |
138 | | - |
139 | | - const { structuredContent, newContent } = await modernize(projectDir, componentPath, [ |
140 | | - 'self-closing-tag', |
| 37 | + expect(execAsyncSpy).not.toHaveBeenCalled(); |
| 38 | + expect(structuredContent?.instructions).toEqual([ |
| 39 | + 'See https://angular.dev/best-practices for Angular best practices. ' + |
| 40 | + 'You can call this tool if you have specific transformation you want to run.', |
141 | 41 | ]); |
| 42 | + }); |
142 | 43 |
|
143 | | - expect(structuredContent?.stderr).toBe(''); |
144 | | - expect(newContent).toContain('<app-bar />'); |
| 44 | + it('should return instructions if no directories are provided', async () => { |
| 45 | + const { structuredContent } = (await runModernization({ |
| 46 | + transformations: ['control-flow'], |
| 47 | + })) as { |
| 48 | + structuredContent: ModernizeOutput; |
| 49 | + }; |
| 50 | + |
| 51 | + expect(execAsyncSpy).not.toHaveBeenCalled(); |
145 | 52 | expect(structuredContent?.instructions).toEqual([ |
146 | | - 'Migration self-closing-tag on directory . completed successfully.', |
| 53 | + 'Provide this tool with a list of directory paths in your workspace ' + |
| 54 | + 'to run the modernization on.', |
147 | 55 | ]); |
148 | 56 | }); |
149 | 57 |
|
150 | | - it('can run multiple transformations', async () => { |
151 | | - const componentPath = join(projectDir, 'test.component.ts'); |
152 | | - const componentContent = ` |
153 | | - import { Component } from '@angular/core'; |
154 | | -
|
155 | | - @Component({ |
156 | | - selector: 'app-foo', |
157 | | - template: '<app-bar *ngIf="show"></app-bar>', |
158 | | - }) |
159 | | - export class FooComponent { |
160 | | - show = true; |
161 | | - } |
162 | | - `; |
163 | | - await writeFile(componentPath, componentContent); |
164 | | - |
165 | | - const { structuredContent, newContent } = await modernize(projectDir, componentPath, [ |
166 | | - 'control-flow', |
167 | | - 'self-closing-tag', |
| 58 | + it('can run a single transformation', async () => { |
| 59 | + const { structuredContent } = (await runModernization({ |
| 60 | + directories: [projectDir], |
| 61 | + transformations: ['self-closing-tag'], |
| 62 | + })) as { structuredContent: ModernizeOutput }; |
| 63 | + |
| 64 | + expect(execAsyncSpy).toHaveBeenCalledOnceWith( |
| 65 | + `ng generate @angular/core:self-closing-tag --path ${projectDir}`, |
| 66 | + { cwd: projectDir }, |
| 67 | + ); |
| 68 | + expect(structuredContent?.stderr).toBeUndefined(); |
| 69 | + expect(structuredContent?.instructions).toEqual([ |
| 70 | + `Migration self-closing-tag on directory ${projectDir} completed successfully.`, |
168 | 71 | ]); |
| 72 | + }); |
169 | 73 |
|
170 | | - expect(structuredContent?.stderr).toBe(''); |
171 | | - expect(newContent).toContain('@if (show) {<app-bar />}'); |
| 74 | + it('can run multiple transformations', async () => { |
| 75 | + const { structuredContent } = (await runModernization({ |
| 76 | + directories: [projectDir], |
| 77 | + transformations: ['control-flow', 'self-closing-tag'], |
| 78 | + })) as { structuredContent: ModernizeOutput }; |
| 79 | + |
| 80 | + expect(execAsyncSpy).toHaveBeenCalledTimes(2); |
| 81 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 82 | + `ng generate @angular/core:control-flow --path ${projectDir}`, |
| 83 | + { cwd: projectDir }, |
| 84 | + ); |
| 85 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 86 | + `ng generate @angular/core:self-closing-tag --path ${projectDir}`, |
| 87 | + { cwd: projectDir }, |
| 88 | + ); |
| 89 | + expect(structuredContent?.stderr).toBeUndefined(); |
| 90 | + expect(structuredContent?.instructions).toEqual([ |
| 91 | + `Migration control-flow on directory ${projectDir} completed successfully.`, |
| 92 | + `Migration self-closing-tag on directory ${projectDir} completed successfully.`, |
| 93 | + ]); |
172 | 94 | }); |
173 | 95 |
|
174 | 96 | it('can run multiple transformations across multiple directories', async () => { |
175 | 97 | const subfolder1 = join(projectDir, 'subfolder1'); |
176 | | - await mkdir(subfolder1); |
177 | | - const componentPath1 = join(subfolder1, 'test.component.ts'); |
178 | | - const componentContent1 = ` |
179 | | - import { Component } from '@angular/core'; |
180 | | -
|
181 | | - @Component({ |
182 | | - selector: 'app-foo', |
183 | | - template: '<app-bar *ngIf="show"></app-bar>', |
184 | | - }) |
185 | | - export class FooComponent { |
186 | | - show = true; |
187 | | - } |
188 | | - `; |
189 | | - await writeFile(componentPath1, componentContent1); |
190 | | - |
191 | 98 | const subfolder2 = join(projectDir, 'subfolder2'); |
| 99 | + await mkdir(subfolder1); |
192 | 100 | await mkdir(subfolder2); |
193 | | - const componentPath2 = join(subfolder2, 'test.component.ts'); |
194 | | - const componentContent2 = ` |
195 | | - import { Component } from '@angular/core'; |
196 | | -
|
197 | | - @Component({ |
198 | | - selector: 'app-bar', |
199 | | - template: '<app-baz></app-baz>', |
200 | | - }) |
201 | | - export class BarComponent {} |
202 | | - `; |
203 | | - await writeFile(componentPath2, componentContent2); |
204 | | - |
205 | | - const structuredContent = ( |
206 | | - (await runModernization({ |
207 | | - directories: [subfolder1, subfolder2], |
208 | | - transformations: ['control-flow', 'self-closing-tag'], |
209 | | - })) as { structuredContent: ModernizeOutput } |
210 | | - ).structuredContent; |
211 | | - const newContent1 = await readFile(componentPath1, 'utf8'); |
212 | | - const newContent2 = await readFile(componentPath2, 'utf8'); |
213 | | - |
214 | | - expect(structuredContent?.stderr).toBe(''); |
215 | | - expect(newContent1).toContain('@if (show) {<app-bar />}'); |
216 | | - expect(newContent2).toContain('<app-baz />'); |
| 101 | + |
| 102 | + const { structuredContent } = (await runModernization({ |
| 103 | + directories: [subfolder1, subfolder2], |
| 104 | + transformations: ['control-flow', 'self-closing-tag'], |
| 105 | + })) as { structuredContent: ModernizeOutput }; |
| 106 | + |
| 107 | + expect(execAsyncSpy).toHaveBeenCalledTimes(4); |
| 108 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 109 | + `ng generate @angular/core:control-flow --path ${subfolder1}`, |
| 110 | + { cwd: projectDir }, |
| 111 | + ); |
| 112 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 113 | + `ng generate @angular/core:self-closing-tag --path ${subfolder1}`, |
| 114 | + { cwd: projectDir }, |
| 115 | + ); |
| 116 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 117 | + `ng generate @angular/core:control-flow --path ${subfolder2}`, |
| 118 | + { cwd: projectDir }, |
| 119 | + ); |
| 120 | + expect(execAsyncSpy).toHaveBeenCalledWith( |
| 121 | + `ng generate @angular/core:self-closing-tag --path ${subfolder2}`, |
| 122 | + { cwd: projectDir }, |
| 123 | + ); |
| 124 | + expect(structuredContent?.stderr).toBeUndefined(); |
| 125 | + expect(structuredContent?.instructions).toEqual([ |
| 126 | + `Migration control-flow on directory ${subfolder1} completed successfully.`, |
| 127 | + `Migration self-closing-tag on directory ${subfolder1} completed successfully.`, |
| 128 | + `Migration control-flow on directory ${subfolder2} completed successfully.`, |
| 129 | + `Migration self-closing-tag on directory ${subfolder2} completed successfully.`, |
| 130 | + ]); |
| 131 | + }); |
| 132 | + |
| 133 | + it('should report errors from transformations', async () => { |
| 134 | + // Simulate a failed execution |
| 135 | + execAsyncSpy.and.rejectWith(new Error('Command failed with error')); |
| 136 | + |
| 137 | + const { structuredContent } = (await runModernization({ |
| 138 | + directories: [projectDir], |
| 139 | + transformations: ['self-closing-tag'], |
| 140 | + })) as { structuredContent: ModernizeOutput }; |
| 141 | + |
| 142 | + expect(execAsyncSpy).toHaveBeenCalledOnceWith( |
| 143 | + `ng generate @angular/core:self-closing-tag --path ${projectDir}`, |
| 144 | + { cwd: projectDir }, |
| 145 | + ); |
| 146 | + expect(structuredContent?.stderr).toContain('Command failed with error'); |
| 147 | + expect(structuredContent?.instructions).toEqual([ |
| 148 | + `Migration self-closing-tag on directory ${projectDir} failed.`, |
| 149 | + ]); |
217 | 150 | }); |
218 | 151 | }); |
0 commit comments