@@ -29,16 +29,31 @@ function expectImportsToContain(content: string, ...importNames: string[]): void
2929
3030// Helper function to check import declarations with flexible formatting
3131function expectImportDeclarationToContain ( content : string , ...importNames : string [ ] ) : void {
32- // Create individual patterns for each import name with flexible spacing
33- const sortedImports = importNames
34- . sort ( )
35- . map ( ( name ) => `\\s*${ name } \\s*` )
36- . join ( ',' ) ;
37- // Create regex pattern that matches import with flexible spacing: import { NgIf } or import {NgIf}
38- const importPattern = new RegExp (
39- `import\\s*\\{${ sortedImports } \\}\\s*from\\s*['"]@angular\\/common['"]` ,
40- ) ;
41- expect ( content ) . toMatch ( importPattern ) ;
32+ // Sort the import names to match the sorted order in the actual imports
33+ const sortedImports = [ ...importNames ] . sort ( ) ;
34+
35+ // Match both single-line and multi-line import formats
36+ // Pattern matches: import { A, B, C } from '@angular/common'
37+ // or: import {\n A,\n B,\n C\n} from '@angular/common'
38+ const importRegex = / i m p o r t \s * \{ ( [ ^ } ] + ) \} \s * f r o m \s * [ ' " ] @ a n g u l a r \/ c o m m o n [ ' " ] / ;
39+ const match = content . match ( importRegex ) ;
40+
41+ expect ( match ) . toBeTruthy ( 'Should have an import from @angular/common' ) ;
42+
43+ // Extract and normalize the imported names
44+ const importedNames = match ! [ 1 ]
45+ . split ( ',' )
46+ . map ( ( name ) => name . trim ( ) )
47+ . filter ( ( name ) => name . length > 0 )
48+ . sort ( ) ;
49+
50+ // Check that all expected imports are present
51+ for ( const expectedImport of sortedImports ) {
52+ expect ( importedNames ) . toContain ( expectedImport ) ;
53+ }
54+
55+ // Check that we have the exact same imports (no extras)
56+ expect ( importedNames ) . toEqual ( sortedImports ) ;
4257}
4358
4459describe ( 'Common → standalone imports migration' , ( ) => {
@@ -1493,6 +1508,41 @@ describe('Common → standalone imports migration', () => {
14931508 expect ( content ) . not . toContain ( 'CommonModule' ) ;
14941509 } ) ;
14951510 } ) ;
1511+ it ( 'should not create duplicate imports when imports already exist' , async ( ) => {
1512+ writeFile (
1513+ '/comp.ts' ,
1514+ dedent `
1515+ import {Component} from '@angular/core';
1516+ import {CommonModule, AsyncPipe} from '@angular/common';
1517+ import {SomeOtherModule} from './some-other-module';
1518+
1519+ @Component({
1520+ selector: 'app-duplicate-test',
1521+ imports: [CommonModule, AsyncPipe, SomeOtherModule],
1522+ template: \`<div>{{ data$ | async }}</div>\`
1523+ })
1524+ export class DuplicateTestComponent {
1525+ data$ = Promise.resolve('test');
1526+ }
1527+ ` ,
1528+ ) ;
1529+
1530+ await runMigration ( ) ;
1531+ const content = tree . readContent ( '/comp.ts' ) ;
1532+
1533+ // Should remove CommonModule and keep AsyncPipe without duplicating it
1534+ expect ( content ) . not . toContain ( 'CommonModule' ) ;
1535+ expectImportsToContain ( content , 'AsyncPipe' , 'SomeOtherModule' ) ;
1536+
1537+ // Verify AsyncPipe appears only once in the imports array
1538+ const importsMatch = content . match ( / i m p o r t s : \s * \[ ( [ \s \S ] * ?) \] / ) ;
1539+ expect ( importsMatch ) . toBeTruthy ( ) ;
1540+ const importsArray = importsMatch ! [ 1 ] ;
1541+ const asyncPipeMatches = importsArray . match ( / A s y n c P i p e / g) ;
1542+ expect ( asyncPipeMatches ?. length ) . toBe ( 1 ) ;
1543+
1544+ expectImportDeclarationToContain ( content , 'AsyncPipe' ) ;
1545+ } ) ;
14961546
14971547 describe ( 'Robustness and edge cases' , ( ) => {
14981548 it ( 'should handle very large templates without performance issues' , async ( ) => {
0 commit comments