Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% if (implements.length > 0) { %>
import { TestBed } from '@angular/core/testing';

import { <%= classify(name) %>Guard } from './<%= dasherize(name) %>.guard';
Expand All @@ -14,3 +15,24 @@ describe('<%= classify(name) %>Guard', () => {
expect(guard).toBeTruthy();
});
});
<% } %>
<% if (guardType) { %>
import { EnvironmentInjector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { <%= guardType %> } from '@angular/router';

import { <%= camelize(name) %>Guard } from './<%= dasherize(name) %>.guard';

describe('<%= camelize(name) %>Guard', () => {
const executeGuard: <%= guardType %> = (...guardParameters) =>
TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Guard(...guardParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(<%= camelize(name) %>Guard).toBeTruthy();
});
});
<% } %>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<% if (implements.length > 0) { %>
import { Injectable } from '@angular/core';
import { <%= implementationImports %> } from '@angular/router';
import { <%= routerImports %> } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
Expand Down Expand Up @@ -33,3 +34,14 @@ export class <%= classify(name) %>Guard implements <%= implementations %> {
return true;
}<% } %>
}
<% } %><% if (guardType) {
%>import { <%= guardType %> } from '@angular/router';

export const <%= camelize(name) %>Guard: <%= guardType %><% if (guardType === 'CanDeactivateFn') { %><unknown><% } %> = <%
if (guardType === 'CanMatchFn' || guardType === 'CanLoadFn') { %>(route, segments)<% }
%><% if (guardType === 'CanActivateFn') { %>(route, state)<% }
%><% if (guardType === 'CanActivateChildFn') { %>(childRoute, state)<% }
%><% if (guardType === 'CanDeactivateFn') { %>(component, currentRoute, currentState, nextState)<% } %> => {
return true;
}
<% } %>
32 changes: 28 additions & 4 deletions packages/schematics/angular/guard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,38 @@
*/

import { Rule, SchematicsException } from '@angular-devkit/schematics';

import { generateFromFiles } from '../utility/generate-from-files';

import { Implement as GuardInterface, Schema as GuardOptions } from './schema';

export default function (options: GuardOptions): Rule {
if (!options.implements) {
throw new SchematicsException('Option "implements" is required.');
if (!options.implements && !options.guardType) {
throw new SchematicsException('Option "implements" or "guardType" is required.');
}
if (options.implements && options.implements.length > 0 && options.guardType) {
throw new SchematicsException(
`Options "implements" and "guardType" cannot be used together. ` +
`implements: [${options.implements}], guardType: [${options.guardType}]`,
);
}

return options.guardType ? functionalGuardRule(options) : classBasedGuardRule(options);
}

function functionalGuardRule(options: GuardOptions): Rule {
if (!options.guardType) {
throw new SchematicsException('Options "implements" and "guardType" cannot be used together.');
}
const guardType = options.guardType.replace(/^can/, 'Can') + 'Fn';

return generateFromFiles(options, { guardType });
}

function classBasedGuardRule(options: GuardOptions): Rule {
if (!options.implements) {
throw new SchematicsException('Options "implements" and "guardType" cannot be used together.');
}
const implementations = options.implements
.map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate<unknown>' : implement))
.join(', ');
Expand All @@ -36,10 +60,10 @@ export default function (options: GuardOptions): Rule {

routerNamedImports.sort();

const implementationImports = routerNamedImports.join(', ');
const routerImports = routerNamedImports.join(', ');

return generateFromFiles(options, {
implementations,
implementationImports,
routerImports,
});
}
63 changes: 51 additions & 12 deletions packages/schematics/angular/guard/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*/

import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';

import { Schema as ApplicationOptions } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';

import { Schema as GuardOptions } from './schema';

describe('Guard Schematic', () => {
Expand Down Expand Up @@ -90,6 +92,37 @@ describe('Guard Schematic', () => {
expect(fileString).not.toContain('canLoad');
});

it('should respect the guardType value', async () => {
const options = { ...defaultOptions, guardType: 'canActivate' };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain('export const fooGuard: CanActivateFn = (route, state) => {');
expect(fileString).not.toContain('CanActivateChild');
expect(fileString).not.toContain('canActivateChild');
expect(fileString).not.toContain('CanLoad');
expect(fileString).not.toContain('canLoad');
});

it('should generate a helper function to execute the guard in a test', async () => {
const options = { ...defaultOptions, guardType: 'canActivate' };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.spec.ts');
expect(fileString).toContain('const executeGuard: CanActivateFn = (...guardParameters) => ');
expect(fileString).toContain(
'TestBed.inject(EnvironmentInjector).runInContext(() => fooGuard(...guardParameters));',
);
});

it('should generate CanDeactivateFn with unknown guardType', async () => {
const options = { ...defaultOptions, guardType: 'canDeactivate' };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain(
'export const fooGuard: CanDeactivateFn<unknown> = ' +
'(component, currentRoute, currentState, nextState) => {',
);
});

it('should respect the implements values', async () => {
const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -104,18 +137,6 @@ describe('Guard Schematic', () => {
});
});

it('should use CanActivate if no implements value', async () => {
const options = { ...defaultOptions, implements: undefined };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain('CanActivate');
expect(fileString).toContain('canActivate');
expect(fileString).not.toContain('CanActivateChild');
expect(fileString).not.toContain('canActivateChild');
expect(fileString).not.toContain('CanLoad');
expect(fileString).not.toContain('canLoad');
});

it('should add correct imports based on CanLoad implementation', async () => {
const implementationOptions = ['CanLoad'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -136,6 +157,15 @@ describe('Guard Schematic', () => {
expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on canLoad guardType', async () => {
const options = { ...defaultOptions, guardType: 'canLoad' };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
const expectedImports = `import { CanLoadFn } from '@angular/router';`;

expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on CanActivate implementation', async () => {
const implementationOptions = ['CanActivate'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -146,6 +176,15 @@ describe('Guard Schematic', () => {
expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on canActivate guardType', async () => {
const options = { ...defaultOptions, guardType: 'canActivate' };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
const expectedImports = `import { CanActivateFn } from '@angular/router';`;

expect(fileString).toContain(expectedImports);
});

it('should add correct imports if multiple implementations was selected', async () => {
const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand Down
9 changes: 7 additions & 2 deletions packages/schematics/angular/guard/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@
"enum": ["CanActivate", "CanActivateChild", "CanDeactivate", "CanLoad", "CanMatch"],
"type": "string"
},
"default": ["CanActivate"],
"x-prompt": "Which interfaces would you like to implement?"
"x-deprecated": "Use '--guardType' instead."
},
"guardType": {
"type": "string",
"description": "Specifies type of guard to generate.",
"enum": ["canActivate", "canActivateChild", "canDeactivate", "canLoad", "canMatch"],
"x-prompt": "Which type of guard would you like to generate?"
}
},
"required": ["name", "project"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TestBed } from '@angular/core/testing';

<% if (!functional) { %>
import { <%= classify(name) %>Resolver } from './<%= dasherize(name) %>.resolver';

describe('<%= classify(name) %>Resolver', () => {
Expand All @@ -14,3 +14,21 @@ describe('<%= classify(name) %>Resolver', () => {
expect(resolver).toBeTruthy();
});
});
<% } else { %>
import { ResolveFn } from '@angular/router';

import { <%= camelize(name) %>Resolver } from './<%= dasherize(name) %>.resolver';

describe('<%= camelize(name) %>Resolver', () => {
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Resolver(...resolverParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(resolver).toBeTruthy();
});
});
<% } %>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
<% if (!functional) {
%>import { Injectable } from '@angular/core';
import {
Router, Resolve,
RouterStateSnapshot,
Expand All @@ -14,3 +15,9 @@ export class <%= classify(name) %>Resolver implements Resolve<boolean> {
return of(true);
}
}
<% } else { %>import { ResolveFn } from '@angular/router';

export const <%= camelize(name) %>Resolver: ResolveFn<boolean> = (route, state) => {
return true;
}
<% } %>
23 changes: 23 additions & 0 deletions packages/schematics/angular/resolver/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,27 @@ describe('resolver Schematic', () => {
.toPromise();
expect(appTree.files).toContain('/projects/bar/custom/app/foo.resolver.ts');
});

it('should create a functional resolver', async () => {
const tree = await schematicRunner
.runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree)
.toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.ts');
expect(fileString).toContain(
'export const fooResolver: ResolveFn<boolean> = (route, state) => {',
);
});

it('should create a helper function to run a functional resolver in a test', async () => {
const tree = await schematicRunner
.runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree)
.toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.spec.ts');
expect(fileString).toContain(
'const executeResolver: ResolveFn<boolean> = (...resolverParameters) => ',
);
expect(fileString).toContain(
'TestBed.inject(EnvironmentInjector).runInContext(() => fooResolver(...resolverParameters));',
);
});
});
5 changes: 5 additions & 0 deletions packages/schematics/angular/resolver/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"description": "When true (the default), creates the new files at the top level of the current project.",
"default": true
},
"functional": {
"type": "boolean",
"description": "When true, creates the resolver as a `ResolveFn`.",
"default": true
},
"path": {
"type": "string",
"format": "path",
Expand Down
2 changes: 1 addition & 1 deletion tests/legacy-cli/e2e/tests/generate/guard/guard-basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default async function () {
// Does not create a sub directory.
const guardDir = join('src', 'app');

await ng('generate', 'guard', 'test-guard');
await ng('generate', 'guard', 'test-guard', '--implements=CanActivate');
await expectFileToExist(guardDir);
await expectFileToExist(join(guardDir, 'test-guard.guard.ts'));
await expectFileToMatch(join(guardDir, 'test-guard.guard.ts'), /implements CanActivate/);
Expand Down