Skip to content

Commit

Permalink
fix(migrations): add protractor support if protractor imports are det…
Browse files Browse the repository at this point in the history
…ected (#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
  • Loading branch information
crisbeto authored and AndrewKushnir committed Mar 1, 2023
1 parent 1749971 commit 6207d6f
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 3 deletions.
Expand Up @@ -8,7 +8,7 @@


import {NgtscProgram} from '@angular/compiler-cli';
import {Reference, TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
import {TemplateTypeChecker} from '@angular/compiler-cli/private/migrations';
import {dirname, join} from 'path';
import ts from 'typescript';

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

// `bootstrapApplication` doesn't include Protractor support by default
// anymore so we have to opt the app in, if we detect it being used.
const additionalProviders = hasImport(program, rootFileNames, 'protractor') ?
new Map([['provideProtractorTestingSupport', '@angular/platform-browser']]) :
null;

for (const sourceFile of sourceFiles) {
sourceFile.forEachChild(function walk(node) {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
Expand All @@ -64,7 +70,8 @@ export function toStandaloneBootstrap(

for (const call of bootstrapCalls) {
call.declarations.forEach(decl => allDeclarations.add(decl));
migrateBootstrapCall(call, tracker, referenceResolver, typeChecker, printer);
migrateBootstrapCall(
call, tracker, additionalProviders, referenceResolver, typeChecker, printer);
}

// The previous migrations explicitly skip over bootstrapped
Expand Down Expand Up @@ -135,12 +142,15 @@ function analyzeBootstrapCall(
* Converts a `bootstrapModule` call to `bootstrapApplication`.
* @param analysis Analysis result of the call.
* @param tracker Tracker in which to register the changes.
* @param additionalFeatures Additional providers, apart from the auto-detected ones, that should
* be added to the bootstrap call.
* @param referenceResolver
* @param typeChecker
* @param printer
*/
function migrateBootstrapCall(
analysis: BootstrapCallAnalysis, tracker: ChangeTracker, referenceResolver: ReferenceResolver,
analysis: BootstrapCallAnalysis, tracker: ChangeTracker,
additionalProviders: Map<string, string>|null, referenceResolver: ReferenceResolver,
typeChecker: ts.TypeChecker, printer: ts.Printer) {
const sourceFile = analysis.call.getSourceFile();
const moduleSourceFile = analysis.metadata.getSourceFile();
Expand Down Expand Up @@ -177,6 +187,13 @@ function migrateBootstrapCall(
nodesToCopy, referenceResolver, typeChecker);
}

if (additionalProviders) {
additionalProviders.forEach((moduleSpecifier, name) => {
providersInNewCall.push(ts.factory.createCallExpression(
tracker.addImport(sourceFile, name, moduleSpecifier), undefined, undefined));
});
}

if (nodesToCopy.size > 0) {
let text = '\n\n';
nodesToCopy.forEach(node => {
Expand Down Expand Up @@ -703,3 +720,27 @@ function getLastImportEnd(sourceFile: ts.SourceFile): number {

return index;
}

/** Checks if any of the program's files has an import of a specific module. */
function hasImport(program: NgtscProgram, rootFileNames: string[], moduleName: string): boolean {
const tsProgram = program.getTsProgram();
const deepImportStart = moduleName + '/';

for (const fileName of rootFileNames) {
const sourceFile = tsProgram.getSourceFile(fileName);

if (!sourceFile) {
continue;
}

for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) &&
(statement.moduleSpecifier.text === moduleName ||
statement.moduleSpecifier.text.startsWith(deepImportStart))) {
return true;
}
}
}

return false;
}
90 changes: 90 additions & 0 deletions packages/core/schematics/test/standalone_migration_spec.ts
Expand Up @@ -3647,4 +3647,94 @@ describe('standalone migration', () => {
}).catch(e => console.error(e));
`));
});

it('should add Protractor support if any tests are detected', async () => {
writeFile('main.ts', `
import {AppModule} from './app/app.module';
import {platformBrowser} from '@angular/platform-browser';
platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
`);

writeFile('./app/app.module.ts', `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'app', template: 'hello'})
export class AppComponent {}
@NgModule({declarations: [AppComponent], bootstrap: [AppComponent]})
export class AppModule {}
`);

writeFile('./app/app.e2e.spec.ts', `
import {browser, by, element} from 'protractor';
describe('app', () => {
beforeAll(async () => {
await browser.get(browser.params.testUrl);
});
it('should work', async () => {
const rootSelector = element(by.css('app'));
expect(await rootSelector.isPresent()).toBe(true);
});
});
`);

await runMigration('standalone-bootstrap');

expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(`
import {AppComponent} from './app/app.module';
import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [provideProtractorTestingSupport()]
}).catch(e => console.error(e));
`));
});

it('should add Protractor support if any tests with deep imports are detected', async () => {
writeFile('main.ts', `
import {AppModule} from './app/app.module';
import {platformBrowser} from '@angular/platform-browser';
platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
`);

writeFile('./app/app.module.ts', `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'app', template: 'hello'})
export class AppComponent {}
@NgModule({declarations: [AppComponent], bootstrap: [AppComponent]})
export class AppModule {}
`);

writeFile('./app/app.e2e.spec.ts', `
import {browser, by, element} from 'protractor/some/deep-import';
describe('app', () => {
beforeAll(async () => {
await browser.get(browser.params.testUrl);
});
it('should work', async () => {
const rootSelector = element(by.css('app'));
expect(await rootSelector.isPresent()).toBe(true);
});
});
`);

await runMigration('standalone-bootstrap');

expect(stripWhitespace(tree.readContent('main.ts'))).toBe(stripWhitespace(`
import {AppComponent} from './app/app.module';
import {platformBrowser, provideProtractorTestingSupport, bootstrapApplication} from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [provideProtractorTestingSupport()]
}).catch(e => console.error(e));
`));
});
});

0 comments on commit 6207d6f

Please sign in to comment.