nx/packages/angular/src/utils/nx-devkit/ast-utils.spec.ts
Leosvel Pérez Espinosa 3eb9f6a822
feat(angular): remove deprecated functionalities for v21 (#30769)
Remove the deprecated functionalities scheduled to be removed in Nx v21.

BREAKING CHANGE: Remove the deprecated data persistence operators
previously exported in `@nx/angular` and the deprecated testing utils
previously exported in `@nx/angular/testing`.
2025-04-17 09:12:32 -04:00

552 lines
15 KiB
TypeScript

import { updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { createSourceFile, ScriptTarget } from 'typescript';
import {
addImportToComponent,
addImportToDirective,
addImportToModule,
addImportToPipe,
addProviderToAppConfig,
addProviderToBootstrapApplication,
addViewProviderToComponent,
isStandalone,
} from './ast-utils';
describe('Angular AST Utils', () => {
it('should correctly add the imported symbol to the NgModule', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToModule = `my.module.ts`;
const originalContents = `import { NgModule } from '@angular/core';
@NgModule({})
export class MyModule {}
`;
tree.write(pathToModule, originalContents);
const symbolToAdd = `CommonModule`;
const sourceText = tree.read(pathToModule, 'utf-8');
const tsSourceFile = createSourceFile(
pathToModule,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
addImportToModule(tree, tsSourceFile, pathToModule, symbolToAdd);
// ASSERT
expect(tree.read(pathToModule, 'utf-8')).toMatchInlineSnapshot(`
"import { NgModule } from '@angular/core';
@NgModule({ imports: [CommonModule]
})
export class MyModule {}
"
`);
});
it('should correctly add the imported symbol to the Component', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.component.ts`;
const originalContents = `import { Component } from '@angular/core';
@Component({})
export class MyComponent {}
`;
tree.write(pathToFile, originalContents);
const symbolToAdd = `CommonModule`;
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
addImportToComponent(tree, tsSourceFile, pathToFile, symbolToAdd);
// ASSERT
expect(tree.read(pathToFile, 'utf-8')).toMatchInlineSnapshot(`
"import { Component } from '@angular/core';
@Component({ imports: [CommonModule]
})
export class MyComponent {}
"
`);
});
it('should correctly add the imported symbol to the Directive', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.directive.ts`;
const originalContents = `import { Directive } from '@angular/core';
@Directive({})
export class MyDirective {}
`;
tree.write(pathToFile, originalContents);
const symbolToAdd = `CommonModule`;
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
addImportToDirective(tree, tsSourceFile, pathToFile, symbolToAdd);
// ASSERT
expect(tree.read(pathToFile, 'utf-8')).toMatchInlineSnapshot(`
"import { Directive } from '@angular/core';
@Directive({ imports: [CommonModule]
})
export class MyDirective {}
"
`);
});
it('should correctly add the imported symbol to the Pipe', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.pipe.ts`;
const originalContents = `import { Pipe } from '@angular/core';
@Pipe({})
export class MyPipe {}
`;
tree.write(pathToFile, originalContents);
const symbolToAdd = `CommonModule`;
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
addImportToPipe(tree, tsSourceFile, pathToFile, symbolToAdd);
// ASSERT
expect(tree.read(pathToFile, 'utf-8')).toMatchInlineSnapshot(`
"import { Pipe } from '@angular/core';
@Pipe({ imports: [CommonModule]
})
export class MyPipe {}
"
`);
});
describe('isStandalone', () => {
it('should return true for a component when "standalone: true" is set', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.component.ts`;
const originalContents = `import { Component } from '@angular/core';
@Component({
standalone: true
})
export class MyComponent {}
`;
tree.write(pathToFile, originalContents);
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
// ASSERT
expect(isStandalone(tree, tsSourceFile, 'Component')).toBeTruthy();
});
it('should return true for a component when the "standalone" prop is not set and the angular version is 19 or above', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^19.0.0';
return json;
});
const componentSourceText = `import { Component } from '@angular/core';
@Component({})
export class MyComponent {}
`;
const tsSourceFile = createSourceFile(
'my.component.ts',
componentSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Component')).toBe(true);
});
it('should return false for a component when "standalone: false" is set', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.component.ts`;
const originalContents = `import { Component } from '@angular/core';
@Component({
standalone: false
})
export class MyComponent {}
`;
tree.write(pathToFile, originalContents);
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
// ASSERT
expect(isStandalone(tree, tsSourceFile, 'Component')).not.toBeTruthy();
});
it('should return false for a component when the "standalone" prop is not set and the angular version is 18 or below', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^18.0.0';
return json;
});
const componentSourceText = `import { Component } from '@angular/core';
@Component({})
export class MyComponent {}
`;
const tsSourceFile = createSourceFile(
'my.component.ts',
componentSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Component')).toBe(false);
});
it('should return true for a directive when "standalone: true" is set', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.directive.ts`;
const originalContents = `import { Directive } from '@angular/core';
@Directive({
standalone: true
})
export class MyDirective {}
`;
tree.write(pathToFile, originalContents);
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
// ASSERT
expect(isStandalone(tree, tsSourceFile, 'Directive')).toBeTruthy();
});
it('should return true for a directive when the "standalone" prop is not set and the angular version is 19 or above', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^19.0.0';
return json;
});
const directiveSourceText = `import { Directive } from '@angular/core';
@Directive({})
export class MyDirective {}
`;
const tsSourceFile = createSourceFile(
'my.directive.ts',
directiveSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Directive')).toBe(true);
});
it('should return false for a directive when "standalone: false" is set', () => {
const tree = createTreeWithEmptyWorkspace({});
const directiveSourceText = `import { Directive } from '@angular/core';
@Directive({
standalone: false
})
export class MyDirective {}
`;
const tsSourceFile = createSourceFile(
'my.directive.ts',
directiveSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Directive')).toBe(false);
});
it('should return false for a directive when the "standalone" prop is not set and the angular version is 18 or below', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^18.0.0';
return json;
});
const directiveSourceText = `import { Directive } from '@angular/core';
@Directive({})
export class MyDirective {}
`;
const tsSourceFile = createSourceFile(
'my.component.ts',
directiveSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Directive')).toBe(false);
});
it('should return true for a pipe when "standalone: true" is set', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const pathToFile = `my.pipe.ts`;
const originalContents = `import { Pipe } from '@angular/core';
@Pipe({
standalone: true
})
export class MyPipe {}
`;
tree.write(pathToFile, originalContents);
const sourceText = tree.read(pathToFile, 'utf-8');
const tsSourceFile = createSourceFile(
pathToFile,
sourceText,
ScriptTarget.Latest,
true
);
// ACT
// ASSERT
expect(isStandalone(tree, tsSourceFile, 'Pipe')).toBeTruthy();
});
it('should return true for a pipe when the "standalone" prop is not set and the angular version is 19 or above', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^19.0.0';
return json;
});
const pipeSourceText = `import { Pipe } from '@angular/core';
@Pipe({})
export class MyPipe {}
`;
const tsSourceFile = createSourceFile(
'my.pipe.ts',
pipeSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Pipe')).toBe(true);
});
it('should return false for a pipe when "standalone: false" is set', () => {
const tree = createTreeWithEmptyWorkspace({});
const pipeSourceText = `import { Pipe } from '@angular/core';
@Pipe({
standalone: false
})
export class MyPipe {}
`;
const tsSourceFile = createSourceFile(
'my.pipe.ts',
pipeSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Pipe')).toBe(false);
});
it('should return false for a pipe when the "standalone" prop is not set and the angular version is 18 or below', () => {
const tree = createTreeWithEmptyWorkspace({});
updateJson(tree, 'package.json', (json) => {
json.dependencies['@angular/core'] = '^18.0.0';
return json;
});
const pipeSourceText = `import { Pipe } from '@angular/core';
@Pipe({})
export class MyPipe {}
`;
const tsSourceFile = createSourceFile(
'my.pipe.ts',
pipeSourceText,
ScriptTarget.Latest,
true
);
expect(isStandalone(tree, tsSourceFile, 'Pipe')).toBe(false);
});
});
it('should add a provider to the bootstrapApplication call', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write(
'main.ts',
`import { bootstrapApplication } from '@angular/platform-browser';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { AppComponent } from './app/app.component';
import { appRoutes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
],
}).catch((err) => console.error(err));`
);
// ACT
addProviderToBootstrapApplication(tree, 'main.ts', 'provideStore()');
// ASSERT
expect(tree.read('main.ts', 'utf-8')).toMatchInlineSnapshot(`
"import { bootstrapApplication } from '@angular/platform-browser';
import {
provideRouter,
withEnabledBlockingInitialNavigation,
} from '@angular/router';
import { AppComponent } from './app/app.component';
import { appRoutes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideStore(),
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
],
}).catch((err) => console.error(err));"
`);
});
it('should add a provider to the appConfig', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write(
'app.config.ts',
`import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes) ]
};`
);
// ACT
addProviderToAppConfig(tree, 'app.config.ts', 'provideStore()');
// ASSERT
expect(tree.read('app.config.ts', 'utf-8')).toMatchInlineSnapshot(`
"import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideStore(),provideRouter(routes) ]
};"
`);
});
it('should add view provider to a component', () => {
// ARRANGE
const pathToComponent = 'app.component.ts';
const componentOriginal = `import { Component } from '@angular/core';
@Component({
selector: 'app-app',
template: ''
})
export class AppComponent {}
`;
const providerName = 'MyViewProvider';
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write(pathToComponent, componentOriginal);
const tsSourceFile = createSourceFile(
pathToComponent,
componentOriginal,
ScriptTarget.Latest,
true
);
// ACT
addViewProviderToComponent(
tree,
tsSourceFile,
pathToComponent,
providerName
);
// ASSERT
expect(tree.read(pathToComponent, 'utf-8')).toMatchInlineSnapshot(`
"import { Component } from '@angular/core';
@Component({
selector: 'app-app',
template: '',
viewProviders: [MyViewProvider]
})
export class AppComponent {}
"
`);
});
});