Skip to content

Commit 1ffcfb6

Browse files
SkyZeroZxAndrewKushnir
authored andcommitted
feat(migrations): Adds migration for deprecated router testing module (#64217)
Introduces a schematic to replace deprecated router testing imports PR Close #64217
1 parent 563dbd9 commit 1ffcfb6

File tree

14 files changed

+1956
-0
lines changed

14 files changed

+1956
-0
lines changed

adev/src/app/routing/sub-navigation-data.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,12 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
15031503
contentPath: 'reference/migrations/ngstyle-to-style',
15041504
status: 'new',
15051505
},
1506+
{
1507+
label: 'Router Testing Migration',
1508+
path: 'reference/migrations/router-testing-migration',
1509+
contentPath: 'reference/migrations/router-testing-migration',
1510+
status: 'new',
1511+
},
15061512
],
15071513
},
15081514
];

adev/src/content/reference/migrations/overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ Learn about how you can migrate your existing angular project to the latest feat
3636
<docs-card title="NgStyle to Style Bindings" link="Migrate now" href="reference/migrations/ngstyle-to-style">
3737
Convert component templates to prefer style bindings over the `NgStyle` directives when possible.
3838
</docs-card>
39+
<docs-card title="RouterTestingModule migration" link="Migrate now" href="reference/migrations/router-testing-migration">
40+
Convert `RouterTestingModule` usages to `RouterModule` in TestBed configurations and add `provideLocationMocks()` when appropriate.
41+
</docs-card>
3942
</docs-card-container>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# RouterTestingModule migration
2+
3+
This schematic migrates usages of `RouterTestingModule` inside tests to `RouterModule` and, when necessary, adds `provideLocationMocks()` to preserve behavior when `Location` or `LocationStrategy` are imported.
4+
5+
Run the schematic with:
6+
7+
<docs-code language="shell">
8+
9+
ng generate @angular/core:router-testing-migration
10+
11+
</docs-code>
12+
13+
## Options
14+
15+
| Option | Details |
16+
| :----- | :---------------------------------------------------------------------------------------------------------------------------- |
17+
| `path` | The path (relative to project root) to migrate. Defaults to `./`. Use this to incrementally migrate a subset of your project. |
18+
19+
## Examples
20+
21+
### Preserve router options
22+
23+
Before:
24+
25+
```ts
26+
TestBed.configureTestingModule({
27+
imports: [RouterTestingModule.withRoutes(routes, { initialNavigation: 'enabledBlocking' })]
28+
});
29+
```
30+
31+
After:
32+
33+
```ts
34+
TestBed.configureTestingModule({
35+
imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })]
36+
});
37+
```
38+
39+
### Add provideLocationMocks when Location is used
40+
41+
Before:
42+
43+
```ts
44+
import { Location } from '@angular/common';
45+
import { RouterTestingModule } from '@angular/router/testing';
46+
47+
describe('test', () => {
48+
let mockLocation : Location;
49+
beforeEach(() => {
50+
TestBed.configureTestingModule({
51+
imports: [RouterTestingModule]
52+
});
53+
});
54+
});
55+
```
56+
57+
After:
58+
59+
```ts
60+
import { RouterModule } from '@angular/router';
61+
import { provideLocationMocks } from '@angular/common/testing';
62+
import { Location } from '@angular/common';
63+
64+
describe('test', () => {
65+
let mockLocation : Location;
66+
beforeEach(() => {
67+
TestBed.configureTestingModule({
68+
imports: [RouterModule],
69+
providers: [provideLocationMocks()]
70+
});
71+
});
72+
});
73+
```

packages/core/schematics/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ npm_package(
4545
"//packages/core/schematics/migrations/control-flow-migration:static_files",
4646
"//packages/core/schematics/migrations/ngclass-to-class-migration:static_files",
4747
"//packages/core/schematics/migrations/ngstyle-to-style-migration:static_files",
48+
"//packages/core/schematics/migrations/router-testing-migration:static_files",
4849
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
4950
"//packages/core/schematics/ng-generate/inject-migration:static_files",
5051
"//packages/core/schematics/ng-generate/output-migration:static_files",
@@ -127,6 +128,10 @@ bundle_entrypoints = [
127128
"bootstrap-options-migration",
128129
"packages/core/schematics/migrations/bootstrap-options-migration/index.js",
129130
],
131+
[
132+
"router-testing-migration",
133+
"packages/core/schematics/migrations/router-testing-migration/index.js",
134+
],
130135
]
131136

132137
rollup.rollup(
@@ -146,6 +151,7 @@ rollup.rollup(
146151
"//packages/core/schematics/migrations/ngstyle-to-style-migration",
147152
"//packages/core/schematics/migrations/router-current-navigation",
148153
"//packages/core/schematics/migrations/router-last-successful-navigation",
154+
"//packages/core/schematics/migrations/router-testing-migration",
149155
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
150156
"//packages/core/schematics/ng-generate/inject-migration",
151157
"//packages/core/schematics/ng-generate/output-migration",

packages/core/schematics/collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
"factory": "./bundles/ngstyle-to-style-migration.cjs#migrate",
7070
"schema": "./migrations/ngstyle-to-style-migration/schema.json",
7171
"aliases": ["ngstyle-to-style"]
72+
},
73+
"router-testing-migration": {
74+
"description": "Replaces deprecated RouterTestingModule with provideRouter() as recommended in the deprecation note",
75+
"factory": "./bundles/router-testing-migration.cjs#migrate",
76+
"schema": "./migrations/router-testing-migration/schema.json"
7277
}
7378
}
7479
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//tools:defaults.bzl", "copy_to_bin", "ts_project")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
copy_to_bin(
11+
name = "static_files",
12+
srcs = ["schema.json"],
13+
)
14+
15+
ts_project(
16+
name = "router-testing-migration",
17+
srcs = glob(["**/*.ts"]),
18+
deps = [
19+
"//:node_modules/@angular-devkit/schematics",
20+
"//:node_modules/typescript",
21+
"//packages/compiler-cli/private",
22+
"//packages/compiler-cli/src/ngtsc/file_system",
23+
"//packages/core/schematics/utils",
24+
"//packages/core/schematics/utils/tsurge",
25+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
26+
],
27+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# RouterTestingModule Migration
2+
3+
This migration automatically converts deprecated `RouterTestingModule` usages to the recommended modern APIs.
4+
5+
## What it does
6+
7+
- Replaces `RouterTestingModule.withRoutes([...])` with `RouterModule.forRoot([...])` for NgModule tests
8+
- Replaces `RouterTestingModule` with `RouterModule.forRoot([])` when no routes are provided
9+
- For standalone tests (detected by presence of `providers`), moves to `provideRouter([...])` instead
10+
- Updates import statements to use `@angular/router` instead of `@angular/router/testing`
11+
- Preserves other imports and test configuration
12+
13+
## Files
14+
15+
- `router_testing_module_migration.ts` - Main migration logic using TsurgeFunnelMigration
16+
- `index.ts` - Entry point for the schematic
17+
- `../../test/router_testing_to_provide_router_spec.ts` - Comprehensive test suite
18+
- `MIGRATION_NOTES.md` - Detailed documentation with examples
19+
- `BUILD.bazel` - Bazel build configuration
20+
21+
## Running the migration
22+
23+
The migration runs automatically as part of `ng update @angular/core` for v21.0.0+.
24+
25+
To run manually:
26+
```bash
27+
ng update @angular/core --migrate-only router-testing-to-provide-router
28+
```
29+
30+
## Related
31+
32+
- Issue: angular/angular#54853
33+
- Deprecation: angular/angular#54466
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
11+
import {MigrationStage, runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
import {RouterTestingModuleMigration} from './migration';
13+
14+
interface Options {
15+
path: string;
16+
analysisDir: string;
17+
}
18+
19+
export function migrate(options: Options): Rule {
20+
return async (tree, context) => {
21+
await runMigrationInDevkit({
22+
tree,
23+
getMigration: (fs: FileSystem) =>
24+
new RouterTestingModuleMigration({
25+
shouldMigrate: (file) => {
26+
return (
27+
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
28+
!/(^|\/)node_modules\//.test(file.rootRelativePath) &&
29+
/\.spec\.ts$/.test(file.rootRelativePath)
30+
);
31+
},
32+
}),
33+
beforeProgramCreation: (tsconfigPath: string, stage: MigrationStage) => {
34+
if (stage === MigrationStage.Analysis) {
35+
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
36+
} else {
37+
context.logger.info(`Running migration for: ${tsconfigPath}...`);
38+
}
39+
},
40+
beforeUnitAnalysis: (tsconfigPath: string) => {
41+
context.logger.info(`Scanning for RouterTestingModule usage: ${tsconfigPath}...`);
42+
},
43+
afterAllAnalyzed: () => {
44+
context.logger.info(``);
45+
context.logger.info(`Processing analysis data between targets...`);
46+
context.logger.info(``);
47+
},
48+
afterAnalysisFailure: () => {
49+
context.logger.error('Migration failed unexpectedly with no analysis data');
50+
},
51+
whenDone: (stats) => {
52+
context.logger.info('');
53+
context.logger.info(`Successfully migrated RouterTestingModule to RouterModule 🎉`);
54+
context.logger.info(
55+
` -> Migrated ${stats.counters.migratedUsages} RouterTestingModule usages in ${stats.counters.totalFiles} test files.`,
56+
);
57+
if (stats.counters.filesWithLocationMocks > 0) {
58+
context.logger.info(
59+
` -> Added provideLocationMocks() to ${stats.counters.filesWithLocationMocks} files with Location/LocationStrategy imports.`,
60+
);
61+
}
62+
},
63+
});
64+
};
65+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
confirmAsSerializable,
11+
ProgramInfo,
12+
projectFile,
13+
Replacement,
14+
Serializable,
15+
TsurgeFunnelMigration,
16+
} from '../../utils/tsurge';
17+
import {ImportManager} from '@angular/compiler-cli/private/migrations';
18+
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
19+
20+
import {MigrationConfig} from './types';
21+
import {
22+
findRouterTestingModuleUsages,
23+
processRouterTestingModuleUsage,
24+
RouterTestingModuleUsage,
25+
} from './utils';
26+
27+
export interface CompilationUnitData {
28+
replacements: Replacement[];
29+
migratedUsages: RouterTestingModuleUsage[];
30+
filesWithLocationMocks: Map<string, boolean>;
31+
}
32+
33+
/**
34+
* Migration that converts RouterTestingModule usages to the recommended API:
35+
* - Replace RouterTestingModule with RouterModule for all tests (respecting existing imports)
36+
* - Adds provideLocationMocks only when needed and not conflicting
37+
*/
38+
export class RouterTestingModuleMigration extends TsurgeFunnelMigration<
39+
CompilationUnitData,
40+
CompilationUnitData
41+
> {
42+
constructor(private readonly config: MigrationConfig = {}) {
43+
super();
44+
}
45+
46+
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
47+
const replacements: Replacement[] = [];
48+
const migratedUsages: RouterTestingModuleUsage[] = [];
49+
const filesWithLocationMocks = new Map<string, boolean>();
50+
const importManager = new ImportManager({
51+
shouldUseSingleQuotes: () => true,
52+
});
53+
54+
for (const sourceFile of info.sourceFiles) {
55+
const file = projectFile(sourceFile, info);
56+
57+
if (this.config.shouldMigrate && !this.config.shouldMigrate(file)) {
58+
continue;
59+
}
60+
61+
const usages = findRouterTestingModuleUsages(sourceFile);
62+
63+
for (const usage of usages) {
64+
processRouterTestingModuleUsage(usage, sourceFile, info, importManager, replacements);
65+
migratedUsages.push(usage);
66+
67+
if (usage.hasLocationOrLocationStrategyImport && !usage.hasExistingLocationProviders) {
68+
filesWithLocationMocks.set(sourceFile.fileName, true);
69+
}
70+
}
71+
}
72+
73+
applyImportManagerChanges(importManager, replacements, info.sourceFiles, info);
74+
75+
return confirmAsSerializable({
76+
replacements,
77+
migratedUsages,
78+
filesWithLocationMocks,
79+
});
80+
}
81+
82+
override async migrate(globalData: CompilationUnitData) {
83+
return {
84+
replacements: globalData.replacements,
85+
};
86+
}
87+
88+
override async combine(
89+
unitA: CompilationUnitData,
90+
unitB: CompilationUnitData,
91+
): Promise<Serializable<CompilationUnitData>> {
92+
const combinedFilesWithLocationMocks = new Map(unitA.filesWithLocationMocks);
93+
94+
for (const [fileName, hasLocationMocks] of unitB.filesWithLocationMocks) {
95+
combinedFilesWithLocationMocks.set(
96+
fileName,
97+
hasLocationMocks || combinedFilesWithLocationMocks.get(fileName) || false,
98+
);
99+
}
100+
101+
return confirmAsSerializable({
102+
replacements: [...unitA.replacements, ...unitB.replacements],
103+
migratedUsages: [...unitA.migratedUsages, ...unitB.migratedUsages],
104+
filesWithLocationMocks: combinedFilesWithLocationMocks,
105+
});
106+
}
107+
108+
override async globalMeta(
109+
combinedData: CompilationUnitData,
110+
): Promise<Serializable<CompilationUnitData>> {
111+
return confirmAsSerializable(combinedData);
112+
}
113+
114+
override async stats(globalMetadata: CompilationUnitData) {
115+
const stats = {
116+
counters: {
117+
replacements: globalMetadata.replacements.length,
118+
migratedUsages: globalMetadata.migratedUsages.length,
119+
filesWithLocationMocks: globalMetadata.filesWithLocationMocks.size,
120+
totalFiles: new Set(globalMetadata.migratedUsages.map((usage) => usage.sourceFile.fileName))
121+
.size,
122+
},
123+
};
124+
return stats as Serializable<typeof stats>;
125+
}
126+
}

0 commit comments

Comments
 (0)