import { Component, DebugElement, } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; import { HostWindowService } from '../../shared/host-window.service'; import { MenuService } from '../../shared/menu/menu.service'; import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model'; import { MenuSection } from '../../shared/menu/menu-section.model'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { MenuServiceStub } from '../../shared/testing/menu-service.stub'; import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive'; import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component'; describe('ExpandableNavbarSectionComponent', () => { let component: ExpandableNavbarSectionComponent; let fixture: ComponentFixture<ExpandableNavbarSectionComponent>; const menuService = new MenuServiceStub(); describe('on larger screens', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ ExpandableNavbarSectionComponent, HoverOutsideDirective, NoopAnimationsModule, TestComponent, ], providers: [ { provide: 'sectionDataProvider', useValue: {} }, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, TestComponent, ], }).compileComponents(); })); beforeEach(() => { spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([])); fixture = TestBed.createComponent(ExpandableNavbarSectionComponent); component = fixture.componentInstance; spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent); fixture.detectChanges(); }); describe('when the mouse enters the section header (while inactive)', () => { beforeEach(() => { spyOn(component, 'onMouseEnter').and.callThrough(); spyOn(component, 'activateSection').and.callThrough(); spyOn(menuService, 'activateSection'); // Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false)); component.ngOnInit(); fixture.detectChanges(); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]')); sidebarToggler.triggerEventHandler('mouseenter', { preventDefault: () => {/**/ }, }); }); it('should call onMouseEnter', () => { expect(component.onMouseEnter).toHaveBeenCalled(); }); it('should activate the section', () => { expect(component.activateSection).toHaveBeenCalled(); expect(menuService.activateSection).toHaveBeenCalled(); }); }); describe('when the mouse leaves the section header (while active)', () => { beforeEach(() => { spyOn(component, 'onMouseLeave').and.callThrough(); spyOn(component, 'deactivateSection').and.callThrough(); spyOn(menuService, 'deactivateSection'); // Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true)); component.ngOnInit(); component.mouseEntered = true; fixture.detectChanges(); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]')); sidebarToggler.triggerEventHandler('mouseleave', { preventDefault: () => {/**/ }, }); }); it('should call onMouseLeave', () => { expect(component.onMouseLeave).toHaveBeenCalled(); }); it('should deactivate the section', () => { expect(component.deactivateSection).toHaveBeenCalled(); expect(menuService.deactivateSection).toHaveBeenCalled(); }); }); describe('when Enter key is pressed on section header (while inactive)', () => { beforeEach(() => { spyOn(component, 'toggleSection').and.callThrough(); spyOn(menuService, 'toggleActiveSection'); // Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false)); component.ngOnInit(); fixture.detectChanges(); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); // dispatch the (keyup.enter) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); }); it('should call activateSection on the menuService', () => { expect(component.toggleSection).toHaveBeenCalled(); expect(menuService.toggleActiveSection).toHaveBeenCalled(); }); }); describe('when Enter key is pressed on section header (while active)', () => { beforeEach(() => { spyOn(component, 'toggleSection').and.callThrough(); spyOn(menuService, 'toggleActiveSection'); // Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true)); component.ngOnInit(); fixture.detectChanges(); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); // dispatch the (keyup.enter) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })); }); it('should call toggleSection on the menuService', () => { expect(component.toggleSection).toHaveBeenCalled(); expect(menuService.toggleActiveSection).toHaveBeenCalled(); }); }); describe('when spacebar is pressed on section header (while inactive)', () => { let sidebarToggler: DebugElement; beforeEach(() => { spyOn(component, 'toggleSection').and.callThrough(); spyOn(menuService, 'toggleActiveSection'); // Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false)); component.ngOnInit(); fixture.detectChanges(); sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); }); it('should call toggleSection on the menuService', () => { // dispatch the (keyup.space) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { code: 'Space', key: ' ' })); expect(component.toggleSection).toHaveBeenCalled(); expect(menuService.toggleActiveSection).toHaveBeenCalled(); }); // Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/ it('should not do anything on keydown space', () => { const event: Event = new KeyboardEvent('keydown', { code: 'Space', key: ' ' }); spyOn(event, 'preventDefault').and.callThrough(); // dispatch the (keyup.space) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(event); expect(event.preventDefault).toHaveBeenCalled(); }); }); describe('when spacebar is pressed on section header (while active)', () => { beforeEach(() => { spyOn(component, 'toggleSection').and.callThrough(); spyOn(menuService, 'toggleActiveSection'); // Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true)); component.ngOnInit(); fixture.detectChanges(); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); // dispatch the (keyup.space) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' })); }); it('should call toggleSection on the menuService', () => { expect(component.toggleSection).toHaveBeenCalled(); expect(menuService.toggleActiveSection).toHaveBeenCalled(); }); }); describe('when enter is pressed on section header (while inactive)', () => { let sidebarToggler: DebugElement; beforeEach(() => { spyOn(component, 'toggleSection').and.callThrough(); spyOn(menuService, 'toggleActiveSection'); // Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property. spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false)); component.ngOnInit(); fixture.detectChanges(); sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); }); // Should not do anything in order to work correctly with NVDA: https://www.nvaccess.org/ it('should not do anything on keydown space', () => { const event: Event = new KeyboardEvent('keydown', { code: 'Enter' }); spyOn(event, 'preventDefault').and.callThrough(); // dispatch the (keyup.space) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(event); expect(event.preventDefault).toHaveBeenCalled(); }); }); describe('when arrow down is pressed on section header', () => { it('should call activateSection', () => { spyOn(component, 'activateSection').and.callThrough(); const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); // dispatch the (keydown.ArrowDown) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown' })); expect(component.focusOnFirstChildSection).toBe(true); expect(component.activateSection).toHaveBeenCalled(); }); }); describe('when tab is pressed on section header', () => { it('should call deactivateSection', () => { spyOn(component, 'deactivateSection').and.callThrough(); const sidebarToggler: DebugElement = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); // dispatch the (keydown.ArrowDown) action used in our component HTML sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' })); expect(component.deactivateSection).toHaveBeenCalled(); }); }); describe('navigateDropdown', () => { beforeEach(fakeAsync(() => { jasmine.getEnv().allowRespy(true); spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([ Object.assign(new MenuSection(), { id: 'subSection1', model: Object.assign(new LinkMenuItemModel(), { type: 'TEST_LINK', }), parentId: component.section.id, }), Object.assign(new MenuSection(), { id: 'subSection2', model: Object.assign(new LinkMenuItemModel(), { type: 'TEST_LINK', }), parentId: component.section.id, }), ])); component.ngOnInit(); flush(); fixture.detectChanges(); component.focusOnFirstChildSection = true; component.active$.next(true); fixture.detectChanges(); })); it('should close the modal on Tab', () => { spyOn(menuService, 'deactivateSection').and.callThrough(); const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0]; firstSubsection.nativeElement.focus(); firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Tab' })); expect(menuService.deactivateSection).toHaveBeenCalled(); }); it('should close the modal on Escape', () => { spyOn(menuService, 'deactivateSection').and.callThrough(); const firstSubsection: DebugElement = fixture.debugElement.queryAll(By.css('.dropdown-menu a[role="menuitem"]'))[0]; firstSubsection.nativeElement.focus(); firstSubsection.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape' })); expect(menuService.deactivateSection).toHaveBeenCalled(); }); }); }); describe('on smaller, mobile screens', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ ExpandableNavbarSectionComponent, HoverOutsideDirective, NoopAnimationsModule, TestComponent, ], providers: [ { provide: 'sectionDataProvider', useValue: {} }, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(300) }, ], }).compileComponents(); })); beforeEach(() => { spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([])); fixture = TestBed.createComponent(ExpandableNavbarSectionComponent); component = fixture.componentInstance; spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent); fixture.detectChanges(); }); describe('when the mouse enters the section header', () => { beforeEach(() => { spyOn(menuService, 'activateSection'); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]')); sidebarToggler.triggerEventHandler('mouseenter', { preventDefault: () => {/**/ }, }); }); it('should not call activateSection on the menuService', () => { expect(menuService.activateSection).not.toHaveBeenCalled(); }); }); describe('when the mouse leaves the section header', () => { beforeEach(() => { spyOn(menuService, 'deactivateSection'); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]')); sidebarToggler.triggerEventHandler('mouseleave', { preventDefault: () => {/**/ }, }); }); it('should not call deactivateSection on the menuService', () => { expect(menuService.deactivateSection).not.toHaveBeenCalled(); }); }); describe('when a click occurs on the section header link', () => { beforeEach(() => { spyOn(menuService, 'toggleActiveSection'); const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]')); sidebarToggler.triggerEventHandler('click', { preventDefault: () => {/**/ }, }); }); it('should call toggleActiveSection on the menuService', () => { expect(menuService.toggleActiveSection).toHaveBeenCalled(); }); }); }); }); // declare a test component @Component({ selector: 'ds-test-cmp', template: ` <a role="menuitem">link</a> `, standalone: true, }) class TestComponent { }