/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import {
Component,
DebugElement,
Input,
QueryList,
ViewChild,
ViewChildren,
Injectable,
} from '@angular/core';
import { Location } from '@angular/common';
import { Router, Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
import { NbMenuModule } from './menu.module';
import { NbMenuBag, NbMenuInternalService, NbMenuItem, NbMenuService } from './menu.service';
import { NbThemeModule } from '../../theme.module';
import {
getFragmentPartOfUrl,
isFragmentContain,
isFragmentEqual,
isUrlPathContain,
isUrlPathEqual,
getPathPartOfUrl,
} from './url-matching-helpers';
import { pairwise, take } from 'rxjs/operators';
import { NbMenuComponent } from './menu.component';
import {
NbIconComponent,
NbIconLibraries,
NbLayoutDirection,
NbLayoutDirectionService,
} from '@nebular/theme';
import { SpyLocation } from '@angular/common/testing';
@Component({ template: '' })
export class NoopComponent {}
@Component({
template: ``,
})
export class SingleMenuTestComponent {
constructor (public menuPublicService: NbMenuService) {}
@Input() items: NbMenuItem[];
@Input() menuTag: string;
@ViewChild(NbMenuComponent) menuComponent: NbMenuComponent;
}
@Component({
template: `
`,
})
export class DoubleMenusTestComponent {
constructor (public menuPublicService: NbMenuService) {}
@Input() firstMenuItems: NbMenuItem[];
@Input() secondMenuItems: NbMenuItem[];
@Input() firstMenuTag: string;
@Input() secondMenuTag: string;
@ViewChildren(NbMenuComponent) menuComponent: QueryList;
}
// Overrides SpyLocation path method to take into account `includeHash` parameter.
// Original SpyLocation ignores parameters and always returns path with hash which is different
// from Location.
@Injectable()
export class SpyLocationPathParameter extends SpyLocation {
path(includeHash: boolean = false): string {
const path = super.path();
if (includeHash) {
return path;
}
return getPathPartOfUrl(path);
}
}
function createTestBed(routes: Routes = []) {
TestBed.configureTestingModule({
imports: [
NbThemeModule.forRoot(),
NbMenuModule.forRoot(),
RouterTestingModule.withRoutes(routes),
NoopAnimationsModule,
],
declarations: [SingleMenuTestComponent, DoubleMenusTestComponent, NoopComponent],
providers: [NbMenuService],
});
TestBed.overrideProvider(Location, { useValue: new SpyLocationPathParameter() });
const iconLibs: NbIconLibraries = TestBed.inject(NbIconLibraries);
iconLibs.registerSvgPack('test', { 'some-icon': '' });
iconLibs.setDefaultPack('test')
}
function createSingleMenuComponent(menuItems, menuTag = 'menu') {
createTestBed();
const fixture = TestBed.createComponent( SingleMenuTestComponent );
fixture.componentInstance.items = menuItems;
fixture.componentInstance.menuTag = menuTag;
const menuService = fixture.componentInstance.menuPublicService;
fixture.detectChanges();
return { fixture, menuService };
}
function createDoubleMenuComponent( firstMenuItems, firstMenuTag, secondMenuItems, secondMenuTag ) {
createTestBed();
const fixture = TestBed.createComponent( DoubleMenusTestComponent );
fixture.componentInstance.firstMenuItems = firstMenuItems;
fixture.componentInstance.secondMenuItems = secondMenuItems;
fixture.componentInstance.firstMenuTag = firstMenuTag;
fixture.componentInstance.secondMenuTag = secondMenuTag;
const menuService = fixture.componentInstance.menuPublicService;
fixture.detectChanges();
return { fixture, menuService };
}
function createMenuItems(items: Partial[], menuInternaleService: NbMenuInternalService): NbMenuItem[] {
menuInternaleService.prepareItems(items as NbMenuItem[]);
return items as NbMenuItem[];
}
describe('NbMenuItem', () => {
it('should set tag attribute for menu services', () => {
const { fixture } = createSingleMenuComponent([{ title: 'Home' }], 'menu');
const nbMenuTag = fixture.componentInstance.menuComponent.tag;
expect(nbMenuTag).toEqual('menu');
});
it('should set icon to menu item', () => {
const { fixture } = createSingleMenuComponent([{ title: 'Home', icon: 'some-icon' }]);
const iconWrapper = fixture.nativeElement.querySelector('.menu-icon');
expect(iconWrapper.textContent).toContain('some-icon');
});
it('should set title to menu item', () => {
const { fixture } = createSingleMenuComponent([{ title: 'Test title' }]);
const titleWrapper = fixture.nativeElement.querySelector('.menu-title').innerHTML;
expect(titleWrapper).toEqual('Test title');
});
it('should set link target to menu item', () => {
const { fixture } = createSingleMenuComponent([
{ title: 'Link with _blank target', target: '_blank' },
{ title: 'Link with _self target', target: '_self' },
{ title: 'Link with any not valid target', target: 'anyNotValid' },
]);
const menuLinks = fixture.nativeElement.querySelectorAll('a');
expect(menuLinks[0].getAttribute('target')).toEqual('_blank');
expect(menuLinks[1].getAttribute('target')).toEqual('_self');
expect(menuLinks[2].getAttribute('target')).toEqual('anyNotValid');
});
it('should have only span, without link on group element', () => {
const { fixture } = createSingleMenuComponent([{ title: 'Group item', group: true }]);
const menuItem = fixture.nativeElement.querySelector('.menu-item');
expect(menuItem.querySelector('a')).toBeNull();
expect(menuItem.querySelector('span')).not.toBeNull();
});
it('should not render hidden element', () => {
const { fixture } = createSingleMenuComponent([
{ title: 'Visible item' },
{ title: 'Hidden item', hidden: true },
{ title: 'Visible item' },
]);
const menuList = fixture.nativeElement.querySelectorAll('.menu-item');
expect(menuList.length).toEqual(2);
});
it('should set child menu items', () => {
const { fixture } = createSingleMenuComponent([
{
title: 'Parent item',
expanded: true,
children: [{ title: 'Child item' }],
},
]);
const parentItem = fixture.nativeElement.querySelector('.menu-item');
expect(parentItem.querySelector('ul.menu-items')).not.toBeNull();
});
it('should expand child menu items', () => {
const { fixture } = createSingleMenuComponent([
{ title: 'Parent item', expanded: true, children: [{ title: 'Child item' }] },
]);
const childList = fixture.nativeElement.querySelector('.menu-item > ul.menu-items');
expect(childList.classList).toContain('expanded');
});
it('should set URL', () => {
const { fixture } = createSingleMenuComponent([{ title: 'Menu Item with link', url: 'https://test.link' }]);
const menuItem = fixture.nativeElement.querySelector('.menu-item');
expect(menuItem.querySelector('a').getAttribute('href')).toEqual('https://test.link');
});
it('should set selected item', () => {
const selectedItem = { title: 'Menu item selected', selected: true };
const { fixture } = createSingleMenuComponent([
{ title: 'Menu item not selected' }, selectedItem,
]);
const activeItem = fixture.nativeElement.querySelector('a.active');
expect(activeItem.querySelector('span').innerHTML).toEqual(selectedItem.title);
});
it('should change arrow direction when document direction changes', () => {
const menuItems = [{ title: '', children: [{ title: '' }] }];
const { fixture } = createSingleMenuComponent(menuItems);
const iconComponent = fixture.debugElement.query(By.directive(NbIconComponent)) as DebugElement;
const directionService: NbLayoutDirectionService = TestBed.inject(NbLayoutDirectionService);
expect(iconComponent.componentInstance.icon).toEqual('chevron-left-outline');
directionService.setDirection(NbLayoutDirection.RTL);
fixture.detectChanges();
expect(iconComponent.componentInstance.icon).toEqual('chevron-right-outline');
});
});
describe('menu services', () => {
it('should operate with menu by tag', () => {
const { fixture, menuService } = createDoubleMenuComponent(
[{ title: 'Home'}],
'menuFirst',
[{ title: 'Home'}],
'menuSecond' );
const itemToAdd = { title: 'Added item' };
const initialFirstMenuItemsCount = fixture.nativeElement
.querySelector('nb-menu:first-child')
.querySelectorAll('.menu-item')
.length;
const initialSecondMenuItemsCount = fixture.nativeElement
.querySelector('nb-menu:last-child')
.querySelectorAll('.menu-item')
.length;
menuService.addItems([itemToAdd], 'menuFirst');
fixture.detectChanges();
const afterAddFirstMenuItemsCount = fixture.nativeElement
.querySelector('nb-menu:first-child')
.querySelectorAll('.menu-item')
.length;
const afterAddSecondMenuItemsCount = fixture.nativeElement
.querySelector('nb-menu:last-child')
.querySelectorAll('.menu-item')
.length;
expect(afterAddFirstMenuItemsCount).toEqual(initialFirstMenuItemsCount + 1);
expect(afterAddSecondMenuItemsCount).toEqual(initialSecondMenuItemsCount);
});
it('should add new items to DOM', () => {
const { fixture, menuService } = createSingleMenuComponent([{ title: 'Existing item' }]);
const itemToAdd = { title: 'Added item' };
const menuListOnInit = fixture.nativeElement.querySelectorAll('li').length;
menuService.addItems([itemToAdd], 'menu');
fixture.detectChanges();
const menuListItemAdded = fixture.nativeElement.querySelectorAll('li').length;
expect(menuListItemAdded).toEqual(menuListOnInit + 1);
});
it('should get selected menu item', (done) => {
const selectedItem = { title: 'Menu item selected', selected: true };
const { menuService } = createSingleMenuComponent([{ title: 'Menu item not selected' }, selectedItem ]);
menuService.getSelectedItem('menu')
.pipe(take(1))
.subscribe((menuBag: NbMenuBag) => {
expect(menuBag.item.title).toEqual(selectedItem.title);
done();
});
}, 1000);
it('should hide all expanded menu items', (done) => {
const { fixture, menuService } = createSingleMenuComponent([
{
title: 'Menu item collapsed',
children: [{ title: 'Menu item inner' }],
},
{
title: 'Menu item expanded 1',
expanded: true,
children: [{ title: 'Menu item inner' }],
},
{
title: 'Menu item expanded 2',
expanded: true,
children: [{ title: 'Menu item inner' }],
},
]);
menuService.onSubmenuToggle()
.pipe(pairwise(), take(1))
.subscribe(([menuBagFirstCollapsed, menuBagSecondCollapsed]: NbMenuBag[]) => {
expect(menuBagFirstCollapsed.item.title).toEqual('Menu item expanded 1');
expect(menuBagSecondCollapsed.item.title).toEqual('Menu item expanded 2');
done();
});
menuService.collapseAll();
fixture.detectChanges();
}, 1000);
});
describe('NbMenuInternalService', () => {
let router: Router;
let menuInternalService: NbMenuInternalService;
beforeEach(() => {
const routes = [
{ path: 'menu-1', component: NoopComponent },
{ path: 'menu-1/2', component: NoopComponent },
{
path: 'menu-2',
component: NoopComponent,
children: [{ path: 'menu-2-level-2', component: NoopComponent }],
},
];
createTestBed(routes);
router = TestBed.inject(Router);
menuInternalService = TestBed.inject(NbMenuInternalService);
});
describe('selectFromUrl pathMatch full', () => {
it('should select menu item with matching path', (done) => {
const items: Partial[] = [{ link: '/menu-1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});
it('should select menu item with matching path and fragment', (done) => {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});
it('should select child menu item and its parent', (done) => {
const items: Partial[] = [{
link: '/menu-2',
children: [{ link: '/menu-2/menu-2-level-2' }] as NbMenuItem[],
}];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const parentMenuItem: NbMenuItem = menuItems[0];
const childMenuItem: NbMenuItem = parentMenuItem.children[0];
expect(parentMenuItem.selected).toBeFalsy();
expect(childMenuItem.selected).toBeFalsy();
router.navigate([childMenuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(parentMenuItem.selected).toEqual(true);
expect(childMenuItem.selected).toEqual(true);
done();
});
});
it('should select child menu item with fragment', (done) => {
const items: Partial[] = [{
link: '/menu-2',
children: [{ link: '/menu-2/menu-2-level-2', fragment: '22' }] as NbMenuItem[],
}];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const parentMenuItem: NbMenuItem = menuItems[0];
const childMenuItem: NbMenuItem = parentMenuItem.children[0];
expect(parentMenuItem.selected).toBeFalsy();
expect(childMenuItem.selected).toBeFalsy();
router.navigate([childMenuItem.link], { fragment: childMenuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(parentMenuItem.selected).toEqual(true);
expect(childMenuItem.selected).toEqual(true);
done();
});
});
it('should not select menu item with matching path if fragment doesn\'t match', function(done) {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link], { fragment: menuItem.fragment + 'random-fragment' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
it('should not select menu item with matching fragment if path doesn\'t match', function(done) {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
const url = menuItem.link + '/2';
router.navigate([url], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
it('should not select menu item with fragment if no fragment in url', (done) => {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
it('should not select menu item if path not matches fully', (done) => {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
const url = menuItem.link + '/2';
router.navigate([url], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
it('should not select menu item if path and fragment not matches fully', (done) => {
const items: Partial[] = [{ link: '/menu-1', fragment: '1' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link], { fragment: menuItem.fragment + '1' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
});
describe('selectFromUrl pathMatch prefix', () => {
it('should select menu item if url contains menu link', function(done) {
const items: Partial[] = [{ link: '/menu-1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
const url = menuItem.link + '/2';
router.navigate([url])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});
it('should select menu item if url contains menu link and fragment', function(done) {
const items: Partial[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link], { fragment: menuItem.fragment + '1' })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toEqual(true);
done();
});
});
it('should not select menu item if url contains link without fragment', function(done) {
const items: Partial[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate([menuItem.link])
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
it('should not select menu item if url contains fragment without link', function(done) {
const items: Partial[] = [{ link: '/menu-1', fragment: '1', pathMatch: 'prefix' }];
const menuItems: NbMenuItem[] = createMenuItems(items, menuInternalService);
const menuItem: NbMenuItem = menuItems[0];
expect(menuItem.selected).toBeFalsy();
router.navigate(['menu-2'], { fragment: menuItem.fragment })
.then(() => {
menuInternalService.selectFromUrl(menuItems, '');
expect(menuItem.selected).toBeFalsy();
done();
});
});
});
});
describe('menu URL helpers', () => {
it('isUrlPathContain should work by url segments', () => {
expect(isUrlPathContain('/a/ba', '/a/b')).toBeFalsy();
expect(isUrlPathContain('/a/b/c', '/a/b')).toBeTruthy();
});
it('isUrlPathContain should work for url with fragments', () => {
expect(isUrlPathContain('/a/b#fragment', '/a/b')).toBeTruthy();
});
it('isUrlPathContain should work for url with query strings', () => {
expect(isUrlPathContain('/a/b?a=1;b=2&c=3', '/a/b')).toBeTruthy();
});
it('isUrlPathEqual should work for identical paths', () => {
expect(isUrlPathEqual('/a/b/c', '/a/b')).toBeFalsy();
expect(isUrlPathEqual('/a/b/c', '/a/b/c')).toBeTruthy();
});
it('isUrlPathEqual should work for url with fragments', () => {
expect(isUrlPathEqual('/a/b/c#fragment', '/a/b/c')).toBeTruthy();
});
it('isUrlPathEqual should work for url with query strings', () => {
expect(isUrlPathEqual('/a/b/c?a=1;b=2&c=3', '/a/b/c')).toBeTruthy();
});
it('getFragmentPartOfUrl should return empty string for path without fragment', () => {
expect(getFragmentPartOfUrl('/a/b')).toBeFalsy();
expect(getFragmentPartOfUrl('/a/b/c?a=1;b=2&c=3')).toBeFalsy();
});
it('getFragmentPartOfUrl should return fragment part when it presented', () => {
expect(getFragmentPartOfUrl('/a/b#f')).toEqual('f');
expect(getFragmentPartOfUrl('/a/b/c?a=1;b=2&c=3#fragment')).toEqual('fragment');
});
it('isFragmentEqual should return false for path without fragments', () => {
expect(isFragmentEqual('/a/b', 'fragment')).toBeFalsy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3', 'fragment')).toBeFalsy();
});
it('isFragmentEqual should return false for path with different fragments', () => {
expect(isFragmentEqual('/a/b#f', 'fragment')).toBeFalsy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3#f', 'fragment')).toBeFalsy();
});
it('isFragmentEqual should return true for path with same fragments', () => {
expect(isFragmentEqual('/a/b#fragment', 'fragment')).toBeTruthy();
expect(isFragmentEqual('/a/b/c?a=1;b=2&c=3#fragment', 'fragment')).toBeTruthy();
});
it('isFragmentContain should return true for url with exact fragment', () => {
expect(isFragmentContain('/a/b#1', '1')).toBeTruthy();
expect(isFragmentContain('/#2', '2')).toBeTruthy();
});
it('isFragmentContain should return true for url containing fragments', () => {
expect(isFragmentContain('/a/b#12', '1')).toBeTruthy();
expect(isFragmentContain('/a/b?a=1;b=2&c=3#21', '1')).toBeTruthy();
});
it('isFragmentContain should return false for url without fragment', () => {
expect(isFragmentContain('/a1/b', '1')).toBeFalsy();
expect(isFragmentContain('/a1/b?a=1;b=2&c=3', '1')).toBeFalsy();
});
it('isFragmentContain should return false for url with different fragment', () => {
expect(isFragmentContain('/a1/b#222', '1')).toBeFalsy();
expect(isFragmentContain('/a1/b?a=1;b=2&c=3#222', '1')).toBeFalsy();
});
});