Skip to content

Commit 2fbaee3

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(migrations): add protractor support if protractor imports are detected (#49274)
The new `bootstrapApplication` API doesn't include Protractor support anymore which may cause existing e2e tests to break after the migration. These changes add some logic that will provide Protractor support if any imports to `protractor` or `protractor/*` are detected. PR Close #49274
1 parent 3673ea0 commit 2fbaee3

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

packages/core/schematics/ng-generate/standalone-migration/standalone-bootstrap.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
import {NgtscProgram} from '@angular/compiler-cli';
11-
import {Reference, TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
11+
import {TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
1212
import {dirname, join} from 'path';
1313
import ts from 'typescript';
1414

@@ -45,6 +45,12 @@ export function toStandaloneBootstrap(
4545
const testObjects = new Set<ts.ObjectLiteralExpression>();
4646
const allDeclarations = new Set<ts.ClassDeclaration>();
4747

48+
// `bootstrapApplication` doesn't include Protractor support by default
49+
// anymore so we have to opt the app in, if we detect it being used.
50+
const additionalProviders = hasImport(program, rootFileNames, 'protractor') ?
51+
new Map([['provideProtractorTestingSupport', '@angular/platform-browser']]) :
52+
null;
53+
4854
for (const sourceFile of sourceFiles) {
4955
sourceFile.forEachChild(function walk(node) {
5056
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
@@ -64,7 +70,8 @@ export function toStandaloneBootstrap(
6470

6571
for (const call of bootstrapCalls) {
6672
call.declarations.forEach(decl => allDeclarations.add(decl));
67-
migrateBootstrapCall(call, tracker, referenceResolver, typeChecker, printer);
73+
migrateBootstrapCall(
74+
call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
6875
}
6976

7077
// The previous migrations explicitly skip over bootstrapped
@@ -135,12 +142,15 @@ function analyzeBootstrapCall(
135142
* Converts a `bootstrapModule` call to `bootstrapApplication`.
136143
* @param analysis Analysis result of the call.
137144
* @param tracker Tracker in which to register the changes.
145+
* @param additionalFeatures Additional providers, apart from the auto-detected ones, that should
146+
* be added to the bootstrap call.
138147
* @param referenceResolver
139148
* @param typeChecker
140149
* @param printer
141150
*/
142151
function migrateBootstrapCall(
143-
analysis: BootstrapCallAnalysis, tracker: ChangeTracker, referenceResolver: ReferenceResolver,
152+
analysis: BootstrapCallAnalysis, tracker: ChangeTracker,
153+
additionalProviders: Map<string, string>|null, referenceResolver: ReferenceResolver,
144154
typeChecker: ts.TypeChecker, printer: ts.Printer) {
145155
const sourceFile = analysis.call.getSourceFile();
146156
const moduleSourceFile = analysis.metadata.getSourceFile();
@@ -177,6 +187,13 @@ function migrateBootstrapCall(
177187
nodesToCopy, referenceResolver, typeChecker);
178188
}
179189

190+
if (additionalProviders) {
191+
additionalProviders.forEach((moduleSpecifier, name) => {
192+
providersInNewCall.push(ts.factory.createCallExpression(
193+
tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
194+
});
195+
}
196+
180197
if (nodesToCopy.size > 0) {
181198
let text = '\n\n';
182199
nodesToCopy.forEach(node => {
@@ -703,3 +720,27 @@ function getLastImportEnd(sourceFile: ts.SourceFile): number {
703720

704721
return index;
705722
}
723+
724+
/** Checks if any of the program's files has an import of a specific module. */
725+
function hasImport(program: NgtscProgram, rootFileNames: string[], moduleName: string): boolean {
726+
const tsProgram = program.getTsProgram();
727+
const deepImportStart = moduleName + '/';
728+
729+
for (const fileName of rootFileNames) {
730+
const sourceFile = tsProgram.getSourceFile(fileName);
731+
732+
if (!sourceFile) {
733+
continue;
734+
}
735+
736+
for (const statement of sourceFile.statements) {
737+
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) &&
738+
(statement.moduleSpecifier.text === moduleName ||
739+
statement.moduleSpecifier.text.startsWith(deepImportStart))) {
740+
return true;
741+
}
742+
}
743+
}
744+
745+
return false;
746+
}

packages/core/schematics/test/standalone_migration_spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3647,4 +3647,94 @@ describe('standalone migration', () => {
36473647
}).catch(e => console.error(e));
36483648
`));
36493649
});
3650+
3651+
it('should add Protractor support if any tests are detected', async () => {
3652+
writeFile('main.ts', `
3653+
import {AppModule} from './app/app.module';
3654+
import {platformBrowser} from '@angular/platform-browser';
3655+
3656+
platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
3657+
`);
3658+
3659+
writeFile('./app/app.module.ts', `
3660+
import {NgModule, Component} from '@angular/core';
3661+
3662+
@Component({selector: 'app', template: 'hello'})
3663+
export class AppComponent {}
3664+
3665+
@NgModule({declarations: [AppComponent], bootstrap: [AppComponent]})
3666+
export class AppModule {}
3667+
`);
3668+
3669+
writeFile('./app/app.e2e.spec.ts', `
3670+
import {browser, by, element} from 'protractor';
3671+
3672+
describe('app', () => {
3673+
beforeAll(async () => {
3674+
await browser.get(browser.params.testUrl);
3675+
});
3676+
3677+
it('should work', async () => {
3678+
const rootSelector = element(by.css('app'));
3679+
expect(await rootSelector.isPresent()).toBe(true);
3680+
});
3681+
});
3682+
`);
3683+
3684+
await runMigration('standalone-bootstrap');
3685+
3686+
expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(`
3687+
import {AppComponent} from './app/app.module';
3688+
import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser';
3689+
3690+
bootstrapApplication(AppComponent, {
3691+
providers: [provideProtractorTestingSupport()]
3692+
}).catch(e => console.error(e));
3693+
`));
3694+
});
3695+
3696+
it('should add Protractor support if any tests with deep imports are detected', async () => {
3697+
writeFile('main.ts', `
3698+
import {AppModule} from './app/app.module';
3699+
import {platformBrowser} from '@angular/platform-browser';
3700+
3701+
platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
3702+
`);
3703+
3704+
writeFile('./app/app.module.ts', `
3705+
import {NgModule, Component} from '@angular/core';
3706+
3707+
@Component({selector: 'app', template: 'hello'})
3708+
export class AppComponent {}
3709+
3710+
@NgModule({declarations: [AppComponent], bootstrap: [AppComponent]})
3711+
export class AppModule {}
3712+
`);
3713+
3714+
writeFile('./app/app.e2e.spec.ts', `
3715+
import {browser, by, element} from 'protractor/some/deep-import';
3716+
3717+
describe('app', () => {
3718+
beforeAll(async () => {
3719+
await browser.get(browser.params.testUrl);
3720+
});
3721+
3722+
it('should work', async () => {
3723+
const rootSelector = element(by.css('app'));
3724+
expect(await rootSelector.isPresent()).toBe(true);
3725+
});
3726+
});
3727+
`);
3728+
3729+
await runMigration('standalone-bootstrap');
3730+
3731+
expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(`
3732+
import {AppComponent} from './app/app.module';
3733+
import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser';
3734+
3735+
bootstrapApplication(AppComponent, {
3736+
providers: [provideProtractorTestingSupport()]
3737+
}).catch(e => console.error(e));
3738+
`));
3739+
});
36503740
});

0 commit comments

Comments
 (0)