From f53d51844f5a8d870a92928da44a371d6668192c Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 17 Mar 2025 09:32:46 +0100 Subject: [PATCH 1/2] feat(core): remove TestBed.get `TestBed.get` isn't type safe and has been deprecated for several years now. These changes remove it from the public API and a follow-up change will add an automated migration to `TestBed.inject`. BREAKING CHANGE: * `TestBed.get` has been removed. Use `TestBed.inject` instead. --- goldens/public-api/core/testing/index.api.md | 2 -- packages/common/http/test/module_spec.ts | 6 +++-- packages/core/test/acceptance/di_spec.ts | 2 +- .../test/application_ref_integration_spec.ts | 2 +- packages/core/test/application_ref_spec.ts | 6 +++-- packages/core/test/linker/integration_spec.ts | 2 +- .../test/linker/security_integration_spec.ts | 6 ++--- packages/core/test/render3/providers_spec.ts | 2 +- packages/core/testing/src/test_bed.ts | 21 ------------------ .../test/http-client-backend-service_spec.ts | 22 +++++++++---------- .../test/dom/events/hammer_gestures_spec.ts | 10 ++++++--- .../test/testing_public_spec.ts | 6 +++-- .../test/regression_integration.spec.ts | 2 +- packages/router/test/router_preloader.spec.ts | 2 +- .../test/downgrade_component_adapter_spec.ts | 5 +++-- 15 files changed, 42 insertions(+), 54 deletions(-) diff --git a/goldens/public-api/core/testing/index.api.md b/goldens/public-api/core/testing/index.api.md index 09fc4f83a706..e7162a4b0023 100644 --- a/goldens/public-api/core/testing/index.api.md +++ b/goldens/public-api/core/testing/index.api.md @@ -124,8 +124,6 @@ export interface TestBed { // (undocumented) execute(tokens: any[], fn: Function, context?: any): any; flushEffects(): void; - // @deprecated (undocumented) - get(token: any, notFoundValue?: any): any; initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, options?: TestEnvironmentOptions): void; // (undocumented) inject(token: ProviderToken, notFoundValue: undefined, options: InjectOptions & { diff --git a/packages/common/http/test/module_spec.ts b/packages/common/http/test/module_spec.ts index 56bf39c7ac11..dd0cc20f4bb0 100644 --- a/packages/common/http/test/module_spec.ts +++ b/packages/common/http/test/module_spec.ts @@ -79,7 +79,7 @@ class ReentrantInterceptor implements HttpInterceptor { describe('HttpClientModule', () => { let injector: Injector; beforeEach(() => { - injector = TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ {provide: HTTP_INTERCEPTORS, useClass: InterceptorA, multi: true}, @@ -87,6 +87,7 @@ describe('HttpClientModule', () => { {provide: HTTP_INTERCEPTORS, useClass: InterceptorC, multi: true}, ], }); + injector = TestBed.inject(Injector); }); it('initializes HttpClient properly', (done) => { injector @@ -132,10 +133,11 @@ describe('HttpClientModule', () => { }); it('allows interceptors to inject HttpClient', (done) => { TestBed.resetTestingModule(); - injector = TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [{provide: HTTP_INTERCEPTORS, useClass: ReentrantInterceptor, multi: true}], }); + injector = TestBed.inject(Injector); injector .get(HttpClient) .get('/test') diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index 17587c72ad05..da5ec8b643da 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -2873,7 +2873,7 @@ describe('di', () => { constructor(public injector: Injector) {} } - const testBedInjector: Injector = TestBed.get(Injector); + const testBedInjector = TestBed.inject(Injector); const childInjector = Injector.create({providers: [], parent: testBedInjector}); const anyService = childInjector.get(AnyService); diff --git a/packages/core/test/application_ref_integration_spec.ts b/packages/core/test/application_ref_integration_spec.ts index 953623f6116f..54447e92c072 100644 --- a/packages/core/test/application_ref_integration_spec.ts +++ b/packages/core/test/application_ref_integration_spec.ts @@ -69,7 +69,7 @@ describe('ApplicationRef bootstrap', () => { expect(helloWorldComponent.log).toEqual(['OnInit', 'DoCheck', 'DoCheck']); // Cleanup TestabilityRegistry - const registry: TestabilityRegistry = getTestBed().get(TestabilityRegistry); + const registry = getTestBed().inject(TestabilityRegistry); registry.unregisterAllApplications(); }), ); diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index 531e6bcf77ef..2569c6b75397 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -17,10 +17,12 @@ import { Component, EnvironmentInjector, InjectionToken, + Injector, LOCALE_ID, NgModule, NgZone, PlatformRef, + ProviderToken, provideZoneChangeDetection, RendererFactory2, TemplateRef, @@ -134,7 +136,7 @@ describe('bootstrap', () => { createRootEl(); const modFactory = compiler.compileModuleSync(SomeModule); - const module = modFactory.create(TestBed); + const module = modFactory.create(TestBed.inject(Injector)); const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent); const component = app.bootstrap(cmpFactory); @@ -162,7 +164,7 @@ describe('bootstrap', () => { createRootEl('custom-selector'); const modFactory = compiler.compileModuleSync(SomeModule); - const module = modFactory.create(TestBed); + const module = modFactory.create(TestBed.inject(Injector)); const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent); const component = app.bootstrap(cmpFactory, 'custom-selector'); diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 3c48f4b16ff6..ba1f53b441ed 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1578,7 +1578,7 @@ describe('integration tests', function () { }); const template = '
hello
'; TestBed.overrideComponent(MyComp, {set: {template}}); - const anchorElement = getTestBed().get(ANCHOR_ELEMENT); + const anchorElement = getTestBed().inject(ANCHOR_ELEMENT); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); diff --git a/packages/core/test/linker/security_integration_spec.ts b/packages/core/test/linker/security_integration_spec.ts index eaeefab01b9f..318a47fdb0f5 100644 --- a/packages/core/test/linker/security_integration_spec.ts +++ b/packages/core/test/linker/security_integration_spec.ts @@ -96,7 +96,7 @@ describe('security integration tests', function () { const template = `Link Title`; TestBed.overrideComponent(SecuredComponent, {set: {template}}); const fixture = TestBed.createComponent(SecuredComponent); - const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer); + const sanitizer = getTestBed().inject(DomSanitizer); const e = fixture.debugElement.children[0].nativeElement; const ci = fixture.componentInstance; @@ -110,7 +110,7 @@ describe('security integration tests', function () { const template = `Link Title`; TestBed.overrideComponent(SecuredComponent, {set: {template}}); const fixture = TestBed.createComponent(SecuredComponent); - const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer); + const sanitizer = getTestBed().inject(DomSanitizer); const trusted = sanitizer.bypassSecurityTrustScript('javascript:alert(1)'); const ci = fixture.componentInstance; @@ -122,7 +122,7 @@ describe('security integration tests', function () { const template = `Link Title`; TestBed.overrideComponent(SecuredComponent, {set: {template}}); const fixture = TestBed.createComponent(SecuredComponent); - const sanitizer: DomSanitizer = getTestBed().get(DomSanitizer); + const sanitizer: DomSanitizer = getTestBed().inject(DomSanitizer); const e = fixture.debugElement.children[0].nativeElement; const trusted = sanitizer.bypassSecurityTrustUrl('bar/baz'); diff --git a/packages/core/test/render3/providers_spec.ts b/packages/core/test/render3/providers_spec.ts index 0ea833b21aca..ea0d22885de7 100644 --- a/packages/core/test/render3/providers_spec.ts +++ b/packages/core/test/render3/providers_spec.ts @@ -1095,7 +1095,7 @@ describe('providers', () => { const environmentInjector = createEnvironmentInjector( [{provide: String, useValue: 'From module injector'}], - TestBed.get(EnvironmentInjector), + TestBed.inject(EnvironmentInjector), ); hostComponent!.vcref.createComponent(EmbeddedComponent, { diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 9c747fef07c0..ae054cd64653 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -115,9 +115,6 @@ export interface TestBed { ): T | null; inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; - /** @deprecated from v9.0.0 use TestBed.inject */ - get(token: any, notFoundValue?: any): any; - /** * Runs the given function in the `EnvironmentInjector` context of `TestBed`. * @@ -364,17 +361,6 @@ export class TestBedImpl implements TestBed { return TestBedImpl.INSTANCE.inject(token, notFoundValue, options); } - /** @deprecated from v9.0.0 use TestBed.inject */ - static get(token: any, notFoundValue?: any): any; - /** @deprecated from v9.0.0 use TestBed.inject */ - static get( - token: any, - notFoundValue: any = Injector.THROW_IF_NOT_FOUND, - options?: InjectOptions, - ): any { - return TestBedImpl.INSTANCE.inject(token, notFoundValue, options); - } - /** * Runs the given function in the `EnvironmentInjector` context of `TestBed`. * @@ -575,13 +561,6 @@ export class TestBedImpl implements TestBed { : result; } - /** @deprecated from v9.0.0 use TestBed.inject */ - get(token: any, notFoundValue?: any): any; - /** @deprecated from v9.0.0 use TestBed.inject */ - get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND, options?: InjectOptions): any { - return this.inject(token, notFoundValue, options); - } - runInInjectionContext(fn: () => T): T { return runInInjectionContext(this.inject(EnvironmentInjector), fn); } diff --git a/packages/misc/angular-in-memory-web-api/test/http-client-backend-service_spec.ts b/packages/misc/angular-in-memory-web-api/test/http-client-backend-service_spec.ts index 471fc9dff929..983fb1f1003b 100644 --- a/packages/misc/angular-in-memory-web-api/test/http-client-backend-service_spec.ts +++ b/packages/misc/angular-in-memory-web-api/test/http-client-backend-service_spec.ts @@ -49,7 +49,7 @@ describe('HttpClient Backend Service', () => { ], }); - http = TestBed.get(HttpClient); + http = TestBed.inject(HttpClient); }); it('can get heroes', waitForAsync(() => { @@ -62,7 +62,7 @@ describe('HttpClient Backend Service', () => { })); it('GET should be a "cold" observable', waitForAsync(() => { - const httpBackend = TestBed.get(HttpBackend); + const httpBackend = TestBed.inject(HttpBackend); const spy = spyOn(httpBackend, 'collectionHandler').and.callThrough(); const get$ = http.get('api/heroes'); @@ -88,7 +88,7 @@ describe('HttpClient Backend Service', () => { })); it('Should only initialize the db once', waitForAsync(() => { - const httpBackend = TestBed.get(HttpBackend); + const httpBackend = TestBed.inject(HttpBackend); const spy = spyOn(httpBackend, 'resetDb').and.callThrough(); // Simultaneous backend.handler calls @@ -252,7 +252,7 @@ describe('HttpClient Backend Service', () => { ], }); - http = TestBed.get(HttpClient); + http = TestBed.inject(HttpClient); }); it('can get heroes', waitForAsync(() => { @@ -377,7 +377,7 @@ describe('HttpClient Backend Service', () => { let heroService: HeroService; beforeEach(() => { - heroService = TestBed.get(HeroService); + heroService = TestBed.inject(HeroService); }); it('can get heroes', waitForAsync(() => { @@ -499,9 +499,9 @@ describe('HttpClient Backend Service', () => { ], }); - http = TestBed.get(HttpClient); - httpBackend = TestBed.get(HttpBackend); - interceptors = TestBed.get(HTTP_INTERCEPTORS); + http = TestBed.inject(HttpClient); + httpBackend = TestBed.inject(HttpBackend); + interceptors = TestBed.inject(HTTP_INTERCEPTORS); }); // sanity test @@ -560,8 +560,8 @@ describe('HttpClient Backend Service', () => { ], }); - http = TestBed.get(HttpClient); - httpBackend = TestBed.get(HttpBackend); + http = TestBed.inject(HttpClient); + httpBackend = TestBed.inject(HttpBackend); createPassThruBackend = spyOn(httpBackend, 'createPassThruBackend').and.callThrough(); }); @@ -623,7 +623,7 @@ describe('HttpClient Backend Service', () => { ], }); - http = TestBed.get(HttpClient); + http = TestBed.inject(HttpClient); }); it('can get heroes (encapsulated)', waitForAsync(() => { diff --git a/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts b/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts index 7cf6a28bd0dd..cafb85177086 100644 --- a/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts +++ b/packages/platform-browser/test/dom/events/hammer_gestures_spec.ts @@ -5,7 +5,7 @@ * 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 */ -import {ApplicationRef, NgZone} from '@angular/core'; +import {ApplicationRef, Injector, NgZone} from '@angular/core'; import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; import {EventManager} from '@angular/platform-browser'; import { @@ -24,7 +24,11 @@ describe('HammerGesturesPlugin', () => { describe('with no custom loader', () => { beforeEach(() => { - plugin = new HammerGesturesPlugin(document, new HammerGestureConfig(), TestBed); + plugin = new HammerGesturesPlugin( + document, + new HammerGestureConfig(), + TestBed.inject(Injector), + ); }); it('should warn user and do nothing when Hammer.js not loaded', () => { @@ -86,7 +90,7 @@ describe('HammerGesturesPlugin', () => { const hammerConfig = new HammerGestureConfig(); spyOn(hammerConfig, 'buildHammer').and.returnValue(fakeHammerInstance); - plugin = new HammerGesturesPlugin(document, hammerConfig, TestBed, loader); + plugin = new HammerGesturesPlugin(document, hammerConfig, TestBed.inject(Injector), loader); // Use a fake EventManager that has access to the NgZone. plugin.manager = {getZone: () => ngZone} as EventManager; diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index 0903a99452e4..699e1a0c63eb 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -527,7 +527,7 @@ describe('public testing API', () => { 'resolveComponentFactory', ]); TestBed.overrideProvider(ComponentFactoryResolver, {useValue: componentFactoryMock}); - expect(TestBed.get(ComponentFactoryResolver)).toEqual(componentFactoryMock); + expect(TestBed.inject(ComponentFactoryResolver)).toEqual(componentFactoryMock); }); }); @@ -598,7 +598,9 @@ describe('public testing API', () => { const compiler = TestBed.inject(Compiler); const modFactory = compiler.compileModuleSync(MyModule); - expect(modFactory.create(getTestBed()).injector.get(aTok)).toBe('mockA: parentDepValue'); + expect(modFactory.create(TestBed.inject(Injector)).injector.get(aTok)).toBe( + 'mockA: parentDepValue', + ); }); it('should keep imported NgModules eager', () => { diff --git a/packages/router/test/regression_integration.spec.ts b/packages/router/test/regression_integration.spec.ts index ca05b6eeefe1..0845ca04dd43 100644 --- a/packages/router/test/regression_integration.spec.ts +++ b/packages/router/test/regression_integration.spec.ts @@ -218,7 +218,7 @@ describe('Integration', () => { declarations: [OnPushComponent, SimpleCmp], }); - const router: Router = TestBed.get(Router); + const router = TestBed.inject(Router); const fixture = createRoot(router, OnPushComponent); router.navigateByUrl('/simple'); advance(fixture); diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index 05cb2db5a807..85f9ea27dd84 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -60,7 +60,7 @@ describe('RouterPreloader', () => { }); it('being destroyed before expected', () => { - const preloader: RouterPreloader = TestBed.get(RouterPreloader); + const preloader = TestBed.inject(RouterPreloader); // Calling the RouterPreloader's ngOnDestroy method is done to simulate what would happen if // the containing NgModule is destroyed. expect(() => preloader.ngOnDestroy()).not.toThrow(); diff --git a/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts b/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts index 89d9b4d9d751..2fc84ac37eb4 100644 --- a/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts +++ b/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts @@ -168,9 +168,10 @@ withEachNg1Version(() => { class NewModule {} const modFactory = compiler.compileModuleSync(NewModule); - const module = modFactory.create(TestBed); + const testBedInjector = TestBed.inject(Injector); + const module = modFactory.create(testBedInjector); componentFactory = module.componentFactoryResolver.resolveComponentFactory(NewComponent)!; - parentInjector = TestBed; + parentInjector = testBedInjector; return new DowngradeComponentAdapter( element, From 2abad2fd7d373b0b41138b19c2c956ac0a1b36a5 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 17 Mar 2025 10:00:49 +0100 Subject: [PATCH 2/2] refactor(migrations): add migration for TestBed.get Adds a migration that will move users off the deprecated `TestBed.get` method. --- packages/core/schematics/BUILD.bazel | 2 + packages/core/schematics/migrations.json | 5 + .../migrations/test-bed-get/BUILD.bazel | 25 +++ .../migrations/test-bed-get/README.md | 24 +++ .../migrations/test-bed-get/index.ts | 20 +++ .../test-bed-get/test_bed_get_migration.ts | 115 ++++++++++++++ .../core/schematics/test/test_bed_get_spec.ts | 145 ++++++++++++++++++ 7 files changed, 336 insertions(+) create mode 100644 packages/core/schematics/migrations/test-bed-get/BUILD.bazel create mode 100644 packages/core/schematics/migrations/test-bed-get/README.md create mode 100644 packages/core/schematics/migrations/test-bed-get/index.ts create mode 100644 packages/core/schematics/migrations/test-bed-get/test_bed_get_migration.ts create mode 100644 packages/core/schematics/test/test_bed_get_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 44bd7969685f..c373bd7699e9 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -46,6 +46,7 @@ rollup_bundle( "//packages/core/schematics/ng-generate/output-migration:index.ts": "output-migration", "//packages/core/schematics/ng-generate/self-closing-tags-migration:index.ts": "self-closing-tags-migration", "//packages/core/schematics/migrations/inject-flags:index.ts": "inject-flags", + "//packages/core/schematics/migrations/test-bed-get:index.ts": "test-bed-get", }, format = "cjs", link_workspace_root = True, @@ -56,6 +57,7 @@ rollup_bundle( ], deps = [ "//packages/core/schematics/migrations/inject-flags", + "//packages/core/schematics/migrations/test-bed-get", "//packages/core/schematics/ng-generate/cleanup-unused-imports", "//packages/core/schematics/ng-generate/control-flow-migration", "//packages/core/schematics/ng-generate/inject-migration", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 404f08bb8ad3..d9c31aa97e0d 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -4,6 +4,11 @@ "version": "20.0.0", "description": "Replaces usages of the deprecated InjectFlags enum", "factory": "./bundles/inject-flags#migrate" + }, + "test-bed-get": { + "version": "20.0.0", + "description": "Replaces usages of the deprecated TestBed.get method with TestBed.inject", + "factory": "./bundles/test-bed-get#migrate" } } } diff --git a/packages/core/schematics/migrations/test-bed-get/BUILD.bazel b/packages/core/schematics/migrations/test-bed-get/BUILD.bazel new file mode 100644 index 000000000000..b8e354c7492e --- /dev/null +++ b/packages/core/schematics/migrations/test-bed-get/BUILD.bazel @@ -0,0 +1,25 @@ +load("//tools:defaults.bzl", "ts_library") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +ts_library( + name = "test-bed-get", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + deps = [ + "//packages/compiler-cli/private", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/core/schematics/utils", + "//packages/core/schematics/utils/tsurge", + "//packages/core/schematics/utils/tsurge/helpers/angular_devkit", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/test-bed-get/README.md b/packages/core/schematics/migrations/test-bed-get/README.md new file mode 100644 index 000000000000..9c46f0fc4d01 --- /dev/null +++ b/packages/core/schematics/migrations/test-bed-get/README.md @@ -0,0 +1,24 @@ +## Remove `TestBed.get` migration +Replaces the usages of the deprecated `TestBed.get` method with the non-deprecated `TestBed.inject`: + +### Before +```typescript +import { TestBed } from '@angular/core/testing'; + +describe('test', () => { + it('should inject', () => { + console.log(TestBed.get(SOME_TOKEN)); + }); +}); +``` + +### After +```typescript +import { TestBed } from '@angular/core/testing'; + +describe('test', () => { + it('should inject', () => { + console.log(TestBed.inject(SOME_TOKEN)); + }); +}); +``` diff --git a/packages/core/schematics/migrations/test-bed-get/index.ts b/packages/core/schematics/migrations/test-bed-get/index.ts new file mode 100644 index 000000000000..e85beb64087a --- /dev/null +++ b/packages/core/schematics/migrations/test-bed-get/index.ts @@ -0,0 +1,20 @@ +/*! + * @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 + */ + +import {Rule} from '@angular-devkit/schematics'; +import {TestBedGetMigration} from './test_bed_get_migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; + +export function migrate(): Rule { + return async (tree) => { + await runMigrationInDevkit({ + tree, + getMigration: () => new TestBedGetMigration(), + }); + }; +} diff --git a/packages/core/schematics/migrations/test-bed-get/test_bed_get_migration.ts b/packages/core/schematics/migrations/test-bed-get/test_bed_get_migration.ts new file mode 100644 index 000000000000..e252eae2dc83 --- /dev/null +++ b/packages/core/schematics/migrations/test-bed-get/test_bed_get_migration.ts @@ -0,0 +1,115 @@ +/** + * @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 + */ + +import ts from 'typescript'; +import { + confirmAsSerializable, + ProgramInfo, + ProjectFile, + projectFile, + Replacement, + Serializable, + TextUpdate, + TsurgeFunnelMigration, +} from '../../utils/tsurge'; +import {getImportSpecifier} from '../../utils/typescript/imports'; +import {isReferenceToImport} from '../../utils/typescript/symbol'; + +export interface CompilationUnitData { + locations: Location[]; +} + +/** Information about the `get` identifier in `TestBed.get`. */ +interface Location { + /** File in which the expression is defined. */ + file: ProjectFile; + + /** Start of the `get` identifier. */ + position: number; +} + +/** Name of the method being replaced. */ +const METHOD_NAME = 'get'; + +/** Migration that replaces `TestBed.get` usages with `TestBed.inject`. */ +export class TestBedGetMigration extends TsurgeFunnelMigration< + CompilationUnitData, + CompilationUnitData +> { + override async analyze(info: ProgramInfo): Promise> { + const locations: Location[] = []; + + for (const sourceFile of info.sourceFiles) { + const specifier = getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed'); + + if (specifier === null) { + continue; + } + + const typeChecker = info.program.getTypeChecker(); + sourceFile.forEachChild(function walk(node) { + if ( + ts.isPropertyAccessExpression(node) && + node.name.text === METHOD_NAME && + ts.isIdentifier(node.expression) && + isReferenceToImport(typeChecker, node.expression, specifier) + ) { + locations.push({file: projectFile(sourceFile, info), position: node.name.getStart()}); + } else { + node.forEachChild(walk); + } + }); + } + + return confirmAsSerializable({locations}); + } + + override async migrate(globalData: CompilationUnitData) { + const replacements = globalData.locations.map(({file, position}) => { + return new Replacement( + file, + new TextUpdate({ + position: position, + end: position + METHOD_NAME.length, + toInsert: 'inject', + }), + ); + }); + + return confirmAsSerializable({replacements}); + } + + override async combine( + unitA: CompilationUnitData, + unitB: CompilationUnitData, + ): Promise> { + const seen = new Set(); + const locations: Location[] = []; + const combined = [...unitA.locations, ...unitB.locations]; + + for (const location of combined) { + const key = `${location.file.id}#${location.position}`; + if (!seen.has(key)) { + seen.add(key); + locations.push(location); + } + } + + return confirmAsSerializable({locations}); + } + + override async globalMeta( + combinedData: CompilationUnitData, + ): Promise> { + return confirmAsSerializable(combinedData); + } + + override async stats() { + return {counters: {}}; + } +} diff --git a/packages/core/schematics/test/test_bed_get_spec.ts b/packages/core/schematics/test/test_bed_get_spec.ts new file mode 100644 index 000000000000..5ca6f78bd775 --- /dev/null +++ b/packages/core/schematics/test/test_bed_get_spec.ts @@ -0,0 +1,145 @@ +/** + * @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 + */ + +import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {runfiles} from '@bazel/runfiles'; +import shx from 'shelljs'; + +describe('test-bed-get migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematic('test-bed-get', {}, tree); + } + + beforeEach(() => { + runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + tmpDirPath = getSystemPath(host.root); + + writeFile('/tsconfig.json', '{}'); + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}, + }), + ); + + writeFile( + '/node_modules/@angular/core/testing/index.d.ts', + ` + export declare class TestBed { + static get(token: any): any; + } + `, + ); + + shx.cd(tmpDirPath); + }); + + it('should migrate a usage of TestBed.get', async () => { + writeFile( + '/test.ts', + ` + import { TestBed } from '@angular/core/testing'; + + const SOME_TOKEN = {}; + + describe('test', () => { + it('should inject', () => { + console.log(TestBed.get(SOME_TOKEN, null)); + }); + }); + `, + ); + + await runMigration(); + expect(tree.readContent('/test.ts')).toContain( + 'console.log(TestBed.inject(SOME_TOKEN, null));', + ); + }); + + it('should migrate a usage of an aliased TestBed.get', async () => { + writeFile( + '/test.ts', + ` + import { TestBed as Alias } from '@angular/core/testing'; + + const SOME_TOKEN = {}; + + describe('test', () => { + it('should inject', () => { + console.log(Alias.get(SOME_TOKEN, null)); + }); + }); + `, + ); + + await runMigration(); + expect(tree.readContent('/test.ts')).toContain('console.log(Alias.inject(SOME_TOKEN, null));'); + }); + + it('should migrate a usage of TestBed.get that is not in a call', async () => { + writeFile( + '/test.ts', + ` + import { TestBed } from '@angular/core/testing'; + + export const GET = TestBed.get; + `, + ); + + await runMigration(); + expect(tree.readContent('/test.ts')).toContain('export const GET = TestBed.inject;'); + }); + + it('should handle a file that is present in multiple projects', async () => { + writeFile('/tsconfig-2.json', '{}'); + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: { + a: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}, + b: {root: '', architect: {build: {options: {tsConfig: './tsconfig-2.json'}}}}, + }, + }), + ); + + writeFile( + 'test.ts', + ` + import { TestBed } from '@angular/core/testing'; + + const SOME_TOKEN = {}; + + describe('test', () => { + it('should inject', () => { + console.log(TestBed.get(SOME_TOKEN)); + }); + }); + `, + ); + + await runMigration(); + const content = tree.readContent('/test.ts'); + expect(content).toContain('console.log(TestBed.inject(SOME_TOKEN));'); + }); +});