Merge pull request #4445 from abpframework/feat/2445

Improved Routes Handling and the New Config Modules
pull/4459/head
Mehmet Erim 5 years ago committed by GitHub
commit b4f08ead0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,7 +6,7 @@ import { ABP } from '../models';
import { ReplaceableComponents } from '../models/replaceable-components';
import { RoutesService } from '../services/routes.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
import { getRoutePath } from '../utils/route-utils';
import { findRoute, getRoutePath } from '../utils/route-utils';
import { takeUntilDestroy } from '../utils/rxjs-utils';
import { TreeNode } from '../utils/tree-utils';
@ -23,13 +23,10 @@ import { TreeNode } from '../utils/tree-utils';
export class DynamicLayoutComponent implements OnDestroy {
layout: Type<any>;
constructor(
injector: Injector,
private route: ActivatedRoute,
private routes: RoutesService,
private store: Store,
) {
constructor(injector: Injector, private store: Store) {
const route = injector.get(ActivatedRoute);
const router = injector.get(Router);
const routes = injector.get(RoutesService);
const layouts = {
application: this.getComponent('Theme.ApplicationLayoutComponent'),
account: this.getComponent('Theme.AccountLayoutComponent'),
@ -38,11 +35,12 @@ export class DynamicLayoutComponent implements OnDestroy {
router.events.pipe(takeUntilDestroy(this)).subscribe(event => {
if (event instanceof NavigationEnd) {
let expectedLayout = (this.route.snapshot.data || {}).layout;
const path = getRoutePath(router);
let expectedLayout = (route.snapshot.data || {}).layout;
if (!expectedLayout) {
let node = { parent: this.routes.search({ path }) } as TreeNode<ABP.Route>;
let node = findRoute(routes, getRoutePath(router));
node = { parent: node } as TreeNode<ABP.Route>;
while (node.parent) {
node = node.parent;

@ -21,6 +21,7 @@ import { PermissionDirective } from './directives/permission.directive';
import { ReplaceableTemplateDirective } from './directives/replaceable-template.directive';
import { StopPropagationDirective } from './directives/stop-propagation.directive';
import { VisibilityDirective } from './directives/visibility.directive';
import { RoutesHandler } from './handlers/routes.handler';
import { ApiInterceptor } from './interceptors/api.interceptor';
import { LocalizationModule } from './localization.module';
import { ABP } from './models/common';
@ -196,6 +197,12 @@ export class CoreModule {
deps: [LocalizationService],
useFactory: noop,
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [RoutesHandler],
useFactory: noop,
},
{ provide: OAuthStorage, useFactory: storageFactory },
],
};

@ -0,0 +1 @@
export * from './routes.handler';

@ -0,0 +1,45 @@
import { Injectable, Optional } from '@angular/core';
import { Router } from '@angular/router';
import { ABP } from '../models';
import { RoutesService } from '../services/routes.service';
@Injectable({
providedIn: 'root',
})
export class RoutesHandler {
constructor(private routes: RoutesService, @Optional() private router: Router) {
this.addRoutes();
}
addRoutes() {
this.router?.config?.forEach(({ path = '', data }) => {
if (!data?.routes) return;
if (Array.isArray(data.routes)) {
this.routes.add(data.routes);
return;
}
const routes = flatRoutes([{ ...data.routes, path }], { path: '' });
this.routes.add(routes);
});
}
}
function flatRoutes(routes: RouteDef[], parent: any) {
if (!routes) return [];
return routes.reduce((acc, route) => {
const current = {
...route,
parentName: parent.name,
path: parent.path + '/' + route.path,
};
acc.push(current, ...flatRoutes(current.children, current));
return acc;
}, []);
}
type RouteDef = ABP.Route & { children: RouteDef[] };

@ -30,19 +30,16 @@ export namespace ABP {
maxResultCount?: number;
}
export interface Node {
export interface Nav {
name: string;
parentName?: string;
requiredPolicy?: string;
order?: number;
invisible?: boolean;
}
export interface Nav extends Node {
path: string;
requiredPolicy?: string;
}
export interface Route extends Nav {
path: string;
layout?: eLayoutType;
iconClass?: string;
}

@ -1,8 +1,10 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { Injectable, OnDestroy } from '@angular/core';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { BehaviorSubject, Observable } from 'rxjs';
import { GetAppConfiguration } from '../actions/config.actions';
import { ABP } from '../models/common';
import { ConfigState } from '../states/config.state';
import { takeUntilDestroy } from '../utils/rxjs-utils';
import { pushValueTo } from '../utils/array-utils';
import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils';
@ -75,6 +77,13 @@ export abstract class AbstractTreeService<T extends object> {
return this.publish(flatItems, visibleItems);
}
find(predicate: (item: TreeNode<T>) => boolean, tree = this.tree): TreeNode<T> | null {
return tree.reduce(
(acc, node) => (acc ? acc : predicate(node) ? node : this.find(predicate, node.children)),
null,
);
}
patch(identifier: string, props: Partial<T>): T[] | false {
const flatItems = this._flat$.value;
const index = flatItems.findIndex(item => item[this.id] === identifier);
@ -88,6 +97,10 @@ export abstract class AbstractTreeService<T extends object> {
return this.publish(flatItems, visibleItems);
}
refresh(): T[] {
return this.add([]);
}
remove(identifiers: string[]): T[] {
const set = new Set<string>();
identifiers.forEach(id => set.add(id));
@ -98,7 +111,7 @@ export abstract class AbstractTreeService<T extends object> {
return this.publish(flatItems, visibleItems);
}
search(params: Partial<T>, tree = this.tree): TreeNode<T> {
search(params: Partial<T>, tree = this.tree): TreeNode<T> | null {
const searchKeys = Object.keys(params);
return tree.reduce(
@ -107,38 +120,42 @@ export abstract class AbstractTreeService<T extends object> {
? acc
: searchKeys.every(key => node[key] === params[key])
? node
: node.children
? this.search(params, node.children)
: acc,
: this.search(params, node.children),
null,
);
}
}
@Injectable({
providedIn: 'root',
})
export class RoutesService extends AbstractTreeService<ABP.Route> {
export abstract class AbstractNavTreeService<T extends ABP.Nav> extends AbstractTreeService<T>
implements OnDestroy {
readonly id = 'name';
readonly parentId = 'parentName';
readonly hide = (item: ABP.Route) => item.invisible;
readonly sort = (a: ABP.Route, b: ABP.Route) => a.order - b.order;
}
readonly hide = (item: T) => item.invisible || !this.isGranted(item);
readonly sort = (a: T, b: T) => a.order - b.order;
@Injectable({
providedIn: 'root',
})
export class SettingTabsService extends AbstractTreeService<ABP.Tab> {
readonly id = 'name';
readonly parentId = 'parentName';
readonly hide = (setting: ABP.Tab) => setting.invisible || !this.isGranted(setting);
readonly sort = (a: ABP.Tab, b: ABP.Tab) => a.order - b.order;
constructor(private store: Store) {
constructor(protected actions: Actions, protected store: Store) {
super();
this.actions
.pipe(takeUntilDestroy(this), ofActionSuccessful(GetAppConfiguration))
.subscribe(() => this.refresh());
}
protected isGranted({ requiredPolicy }: T): boolean {
return this.store.selectSnapshot(ConfigState.getGrantedPolicy(requiredPolicy));
}
private isGranted(setting: ABP.Tab): boolean {
return this.store.selectSnapshot(ConfigState.getGrantedPolicy(setting.requiredPolicy));
hasInvisibleChild(identifier: string): boolean {
const node = this.find(item => item[this.id] === identifier);
return node?.children?.some(child => child.invisible);
}
/* istanbul ignore next */
ngOnDestroy() {}
}
@Injectable({ providedIn: 'root' })
export class RoutesService extends AbstractNavTreeService<ABP.Route> {}
@Injectable({ providedIn: 'root' })
export class SettingTabsService extends AbstractNavTreeService<ABP.Tab> {}

@ -93,23 +93,24 @@ export class ConfigState {
static getGrantedPolicy(key: string) {
const selector = createSelector([ConfigState], (state: Config.State): boolean => {
if (!key) return true;
const getPolicy = k => snq(() => state.auth.grantedPolicies[k], false);
const getPolicy = (k: string) => snq(() => state.auth.grantedPolicies[k], false);
const orRegexp = /\|\|/g;
const andRegexp = /&&/g;
// TODO: Allow combination of ANDs & ORs
if (orRegexp.test(key)) {
const keys = key.split('||').filter(k => !!k);
const keys = key.split('||').filter(Boolean);
if (keys.length !== 2) return false;
if (keys.length < 2) return false;
return getPolicy(keys[0].trim()) || getPolicy(keys[1].trim());
return keys.some(k => getPolicy(k.trim()));
} else if (andRegexp.test(key)) {
const keys = key.split('&&').filter(k => !!k);
const keys = key.split('&&').filter(Boolean);
if (keys.length !== 2) return false;
if (keys.length < 2) return false;
return getPolicy(keys[0].trim()) && getPolicy(keys[1].trim());
return keys.every(k => getPolicy(k.trim()));
}
return getPolicy(key);

@ -2,7 +2,8 @@ import { HttpClient } from '@angular/common/http';
import { Component, NgModule } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store';
import { Actions, NgxsModule, Store } from '@ngxs/store';
import { NEVER } from 'rxjs';
import { DynamicLayoutComponent, RouterOutletComponent } from '../components';
import { eLayoutType } from '../enums/common';
import { ABP } from '../models';
@ -97,11 +98,24 @@ const storeData = {
};
describe('DynamicLayoutComponent', () => {
const mockActions: Actions = NEVER;
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
const createComponent = createRoutingFactory({
component: RouterOutletComponent,
stubsEnabled: false,
declarations: [DummyComponent, DynamicLayoutComponent],
mocks: [ApplicationConfigurationService, HttpClient],
providers: [
{
provide: RoutesService,
useFactory: () => new RoutesService(mockActions, mockStore),
},
],
imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])],
routes: [
{ path: '', component: RouterOutletComponent },

@ -2,13 +2,35 @@ import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { RouterOutletComponent } from '../components';
import { getRoutePath } from '../utils/route-utils';
import { RoutesService } from '../services/routes.service';
import { findRoute, getRoutePath } from '../utils/route-utils';
// tslint:disable-next-line
@Component({ template: '' })
class DummyComponent {}
describe('Route Utils', () => {
describe('#findRoute', () => {
const node = { path: '/foo' };
test.each`
path | expected | count
${'/foo/bar/baz'} | ${node} | ${3}
${'/foo/bar'} | ${node} | ${2}
${'/foo'} | ${node} | ${1}
${'/'} | ${null} | ${1}
`(
'should find $expected in $count turns when path is $path',
async ({ path, expected, count }) => {
const find = jest.fn(cb => (cb(node) ? node : null));
const routes = ({ find } as any) as RoutesService;
const route = findRoute(routes, path);
expect(route).toBe(expected);
expect(find).toHaveBeenCalledTimes(count);
},
);
});
describe('#getRoutePath', () => {
let spectator: SpectatorRouting<RouterOutletComponent>;
const createRouting = createRoutingFactory({

@ -0,0 +1,40 @@
import { Router } from '@angular/router';
import { RoutesHandler } from '../handlers';
import { RoutesService } from '../services';
describe('Routes Handler', () => {
describe('#add', () => {
it('should add routes from router config', () => {
const config = [
{ path: 'x' },
{ path: 'y', data: {} },
{ path: '', data: { routes: { name: 'Foo' } } },
{ path: 'bar', data: { routes: { name: 'Bar' } } },
{ data: { routes: [{ path: '/baz', name: 'Baz' }] } },
];
const foo = [{ path: '/', name: 'Foo' }];
const bar = [{ path: '/bar', name: 'Bar' }];
const baz = [{ path: '/baz', name: 'Baz' }];
const routes = [];
const add = jest.fn(routes.push.bind(routes));
const mockRoutesService = ({ add } as unknown) as RoutesService;
const mockRouter = ({ config } as unknown) as Router;
const handler = new RoutesHandler(mockRoutesService, mockRouter);
expect(add).toHaveBeenCalledTimes(3);
expect(routes).toEqual([foo, bar, baz]);
});
it('should not add routes when there is no router', () => {
const routes = [];
const add = jest.fn(routes.push.bind(routes));
const mockRoutesService = ({ add } as unknown) as RoutesService;
const handler = new RoutesHandler(mockRoutesService, null);
expect(add).not.toHaveBeenCalled();
});
});
});

@ -1,17 +1,31 @@
import { Store } from '@ngxs/store';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { GetAppConfiguration } from '../actions';
import { RoutesService } from '../services';
const routes = [
{ path: '/foo', name: 'foo' },
{ path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 },
{ path: '/foo/bar/baz', name: 'baz', parentName: 'bar', order: 1 },
{ path: '/foo/x', name: 'x', parentName: 'foo', order: 1 },
];
describe('Routes Service', () => {
let service: RoutesService;
const routes = [
{ path: '/foo', name: 'foo' },
{ path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 },
{ path: '/foo/bar/baz', name: 'baz', parentName: 'bar', order: 1 },
{ path: '/foo/x', name: 'x', parentName: 'foo', order: 1 },
];
const mockActions = new Subject();
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
beforeEach(() => {
service = new RoutesService(mockActions, mockStore);
});
describe('#add', () => {
it('should add given routes as flat$, tree$, and visible$', async () => {
const service = new RoutesService();
service.add(routes);
const flat = await service.flat$.pipe(take(1)).toPromise();
@ -38,9 +52,34 @@ describe('Routes Service', () => {
});
});
describe('#find', () => {
it('should return node found based on query', () => {
service.add(routes);
const result = service.find(route => route.invisible);
expect(result.name).toBe('bar');
expect(result.children.length).toBe(1);
expect(result.children[0].name).toBe('baz');
});
it('should return null when query is not found', () => {
service.add(routes);
const result = service.find(route => route.requiredPolicy === 'X');
expect(result).toBe(null);
});
});
describe('#hasInvisibleChild', () => {
it('should return if node has invisible child', () => {
service.add(routes);
expect(service.hasInvisibleChild('foo')).toBe(true);
expect(service.hasInvisibleChild('bar')).toBe(false);
expect(service.hasInvisibleChild('baz')).toBe(false);
});
});
describe('#remove', () => {
it('should remove routes based on given routeNames', () => {
const service = new RoutesService();
service.add(routes);
service.remove(['bar']);
@ -66,9 +105,9 @@ describe('Routes Service', () => {
describe('#patch', () => {
it('should patch propeties of routes based on given routeNames', () => {
const service = new RoutesService();
service['isGranted'] = jest.fn(route => route.requiredPolicy !== 'X');
service.add(routes);
service.patch('x', { invisible: true });
service.patch('x', { requiredPolicy: 'X' });
const flat = service.flat;
const tree = service.tree;
@ -93,21 +132,40 @@ describe('Routes Service', () => {
});
it('should return false when route name is not found', () => {
const service = new RoutesService();
service.add(routes);
const result = service.patch('A man has no name.', { invisible: true });
expect(result).toBe(false);
});
});
describe('#refresh', () => {
it('should call add once with empty array', () => {
const add = jest.spyOn(service, 'add');
service.refresh();
expect(add).toHaveBeenCalledTimes(1);
expect(add).toHaveBeenCalledWith([]);
});
it('should be called upon successful GetAppConfiguration action', () => {
const refresh = jest.spyOn(service, 'refresh');
mockActions.next({ action: new GetAppConfiguration(), status: 'SUCCESSFUL' });
expect(refresh).toHaveBeenCalledTimes(1);
});
});
describe('#search', () => {
it('should return node found when route name is not found', () => {
const service = new RoutesService();
it('should return node found based on query', () => {
service.add(routes);
const result = service.search({ invisible: true });
expect(result.name).toBe('bar');
expect(result.children.length).toBe(1);
expect(result.children[0].name).toBe('baz');
});
it('should return null when query is not found', () => {
service.add(routes);
const result = service.search({ requiredPolicy: 'X' });
expect(result).toBe(null);
});
});
});

@ -1,4 +1,21 @@
import { PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
import { ABP } from '../models/common';
import { RoutesService } from '../services/routes.service';
import { TreeNode } from './tree-utils';
export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> {
const node = routes.find(route => route.path === path);
return node || path === '/'
? node
: findRoute(
routes,
path
.split('/')
.slice(0, -1)
.join('/'),
);
}
export function getRoutePath(router: Router) {
const emptyGroup = { segments: [] } as UrlSegmentGroup;

@ -1 +1,2 @@
export * from './policy-names';
export * from './route-names';

@ -0,0 +1,5 @@
export const enum eIdentityPolicyNames {
IdentityManagement = 'AbpIdentity.Roles || AbpIdentity.Users',
Roles = 'AbpIdentity.Roles',
Users = 'AbpIdentity.Users',
}

@ -1,6 +1,7 @@
import { eLayoutType, RoutesService } from '@abp/ng.core';
import { eThemeSharedRouteNames } from '@abp/ng.theme.shared';
import { APP_INITIALIZER } from '@angular/core';
import { eIdentityPolicyNames } from '../enums/policy-names';
import { eIdentityRouteNames } from '../enums/route-names';
export const IDENTITY_ROUTE_PROVIDERS = [
@ -14,6 +15,7 @@ export function configureRoutes(routes: RoutesService) {
path: '/identity',
name: eIdentityRouteNames.IdentityManagement,
parentName: eThemeSharedRouteNames.Administration,
requiredPolicy: eIdentityPolicyNames.IdentityManagement,
iconClass: 'fa fa-id-card-o',
layout: eLayoutType.application,
order: 1,
@ -22,14 +24,14 @@ export function configureRoutes(routes: RoutesService) {
path: '/identity/roles',
name: eIdentityRouteNames.Roles,
parentName: eIdentityRouteNames.IdentityManagement,
requiredPolicy: 'AbpIdentity.Roles',
requiredPolicy: eIdentityPolicyNames.Roles,
order: 1,
},
{
path: '/identity/users',
name: eIdentityRouteNames.Users,
parentName: eIdentityRouteNames.IdentityManagement,
requiredPolicy: 'AbpIdentity.Users',
requiredPolicy: eIdentityPolicyNames.Users,
order: 2,
},
]);

@ -1 +1,2 @@
export * from './policy-names';
export * from './route-names';

@ -0,0 +1,3 @@
export const enum eSettingManagementPolicyNames {
Settings = 'AbpAccount.SettingManagement',
}

@ -2,6 +2,7 @@ import { eLayoutType, RoutesService, SettingTabsService } from '@abp/ng.core';
import { eThemeSharedRouteNames } from '@abp/ng.theme.shared';
import { APP_INITIALIZER } from '@angular/core';
import { debounceTime, map } from 'rxjs/operators';
import { eSettingManagementPolicyNames } from '../enums/policy-names';
import { eSettingManagementRouteNames } from '../enums/route-names';
export const SETTING_MANAGEMENT_ROUTE_PROVIDERS = [
@ -21,7 +22,7 @@ export function configureRoutes(routes: RoutesService) {
name: eSettingManagementRouteNames.Settings,
path: '/setting-management',
parentName: eThemeSharedRouteNames.Administration,
requiredPolicy: 'AbpAccount.SettingManagement',
requiredPolicy: eSettingManagementPolicyNames.Settings,
layout: eLayoutType.application,
order: 6,
iconClass: 'fa fa-cog',
@ -37,6 +38,6 @@ export function hideRoutes(routes: RoutesService, tabs: SettingTabsService) {
debounceTime(0),
map(nodes => !nodes.length),
)
.subscribe(invisible => routes.patch('AbpSettingManagement::Settings', { invisible }));
.subscribe(invisible => routes.patch(eSettingManagementRouteNames.Settings, { invisible }));
};
}

@ -1 +1,2 @@
export * from './policy-names';
export * from './route-names';

@ -0,0 +1,4 @@
export const enum eTenantManagementPolicyNames {
TenantManagement = 'AbpTenantManagement.Tenants',
Tenants = 'AbpTenantManagement.Tenants',
}

@ -1,6 +1,7 @@
import { eLayoutType, RoutesService } from '@abp/ng.core';
import { eThemeSharedRouteNames } from '@abp/ng.theme.shared';
import { APP_INITIALIZER } from '@angular/core';
import { eTenantManagementPolicyNames } from '../enums/policy-names';
import { eTenantManagementRouteNames } from '../enums/route-names';
export const TENANT_MANAGEMENT_ROUTE_PROVIDERS = [
@ -14,6 +15,7 @@ export function configureRoutes(routes: RoutesService) {
path: '/tenant-management',
name: eTenantManagementRouteNames.TenantManagement,
parentName: eThemeSharedRouteNames.Administration,
requiredPolicy: eTenantManagementPolicyNames.TenantManagement,
layout: eLayoutType.application,
iconClass: 'fa fa-users',
order: 2,
@ -22,7 +24,7 @@ export function configureRoutes(routes: RoutesService) {
path: '/tenant-management/tenants',
name: eTenantManagementRouteNames.Tenants,
parentName: eTenantManagementRouteNames.TenantManagement,
requiredPolicy: 'AbpTenantManagement.Tenants',
requiredPolicy: eTenantManagementPolicyNames.Tenants,
order: 1,
},
]);

@ -1,7 +1,7 @@
<ul class="navbar-nav">
<ng-container
*ngFor="let route of routes.visible$ | async; trackBy: trackByFn"
[ngTemplateOutlet]="route?.children?.length ? dropdownLink : defaultLink"
[ngTemplateOutlet]="isDropdown(route) ? dropdownLink : defaultLink"
[ngTemplateOutletContext]="{ $implicit: route }"
>
</ng-container>

@ -11,4 +11,8 @@ export class RoutesComponent {
trackByFn: TrackByFunction<TreeNode<ABP.Route>> = (_, item) => item.name;
constructor(public readonly routes: RoutesService) {}
isDropdown(node: TreeNode<ABP.Route>) {
return !node.isLeaf || this.routes.hasInvisibleChild(node.name);
}
}

@ -8,6 +8,7 @@ import {
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs/operators';
import { eThemeSharedRouteNames } from '../../enums';
@Component({
selector: 'abp-breadcrumb',
@ -42,7 +43,7 @@ export class BreadcrumbComponent implements OnDestroy, OnInit {
while (node.parent) {
node = node.parent;
const { parent, children, isLeaf, ...segment } = node;
this.segments.unshift(segment);
if (!isAdministration(segment)) this.segments.unshift(segment);
}
this.cdRef.detectChanges();
@ -50,3 +51,7 @@ export class BreadcrumbComponent implements OnDestroy, OnInit {
});
}
}
function isAdministration(route: ABP.Route) {
return route.name === eThemeSharedRouteNames.Administration;
}

@ -10,7 +10,7 @@ export function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '',
path: '/',
name: eThemeSharedRouteNames.Administration,
iconClass: 'fa fa-wrench',
order: 1,

@ -2,8 +2,15 @@ import { ABP, LocalizationPipe, RouterOutletComponent, RoutesService } from '@ab
import { RouterModule } from '@angular/router';
import { createRoutingFactory, SpectatorRouting, SpyObject } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store';
import { Subject } from 'rxjs';
import { BreadcrumbComponent } from '../components/breadcrumb/breadcrumb.component';
const mockActions = new Subject();
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
const mockRoutes: ABP.Route[] = [
{ name: 'Identity', path: '/identity' },
{ name: 'Users', path: '/identity/users', parentName: 'Identity' },
@ -19,6 +26,12 @@ describe('BreadcrumbComponent', () => {
stubsEnabled: false,
detectChanges: false,
mocks: [Store],
providers: [
{
provide: RoutesService,
useFactory: () => new RoutesService(mockActions, mockStore),
},
],
declarations: [LocalizationPipe, BreadcrumbComponent],
imports: [RouterModule],
routes: [

Loading…
Cancel
Save