import {Platform} from '@angular/cdk/platform'; import {Component, PLATFORM_ID, ViewChild} from '@angular/core'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {A11yModule, FocusTrap, CdkTrapFocus} from '../index'; describe('FocusTrap', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [A11yModule], declarations: [ FocusTrapWithBindings, SimpleFocusTrap, FocusTrapTargets, FocusTrapWithSvg, FocusTrapWithoutFocusableElements, FocusTrapWithAutoCapture, FocusTrapUnfocusableTarget, ], }); TestBed.compileComponents(); })); describe('with default element', () => { let fixture: ComponentFixture; let focusTrapInstance: FocusTrap; beforeEach(() => { fixture = TestBed.createComponent(SimpleFocusTrap); fixture.detectChanges(); focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap; }); it('wrap focus from end to start', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. const result = focusTrapInstance.focusFirstTabbableElement(); expect(document.activeElement!.nodeName.toLowerCase()) .toBe('input', 'Expected input element to be focused'); expect(result).toBe(true, 'Expected return value to be true if focus was shifted.'); }); it('should wrap focus from start to end', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. const result = focusTrapInstance.focusLastTabbableElement(); const platformId = TestBed.get(PLATFORM_ID); // In iOS button elements are never tabbable, so the last element will be the input. const lastElement = new Platform(platformId).IOS ? 'input' : 'button'; expect(document.activeElement!.nodeName.toLowerCase()) .toBe(lastElement, `Expected ${lastElement} element to be focused`); expect(result).toBe(true, 'Expected return value to be true if focus was shifted.'); }); it('should return false if it did not manage to find a focusable element', () => { fixture.destroy(); const newFixture = TestBed.createComponent(FocusTrapWithoutFocusableElements); newFixture.detectChanges(); const focusTrap = newFixture.componentInstance.focusTrapDirective.focusTrap; const result = focusTrap.focusFirstTabbableElement(); expect(result).toBe(false); }); it('should be enabled by default', () => { expect(focusTrapInstance.enabled).toBe(true); }); }); describe('with bindings', () => { let fixture: ComponentFixture; beforeEach(() => { fixture = TestBed.createComponent(FocusTrapWithBindings); fixture.detectChanges(); }); it('should clean up its anchor sibling elements on destroy', () => { const rootElement = fixture.debugElement.nativeElement as HTMLElement; expect(rootElement.querySelectorAll('div.cdk-visually-hidden').length).toBe(2); fixture.componentInstance.renderFocusTrap = false; fixture.detectChanges(); expect(rootElement.querySelectorAll('div.cdk-visually-hidden').length).toBe(0); }); it('should set the appropriate tabindex on the anchors, based on the disabled state', () => { const anchors = Array.from( fixture.debugElement.nativeElement.querySelectorAll('div.cdk-visually-hidden') ) as HTMLElement[]; expect(anchors.every(current => current.getAttribute('tabindex') === '0')).toBe(true); expect(anchors.every(current => current.getAttribute('aria-hidden') === 'true')).toBe(true); fixture.componentInstance._isFocusTrapEnabled = false; fixture.detectChanges(); expect(anchors.every(current => !current.hasAttribute('tabindex'))).toBe(true); }); }); describe('with focus targets', () => { let fixture: ComponentFixture; let focusTrapInstance: FocusTrap; beforeEach(() => { fixture = TestBed.createComponent(FocusTrapTargets); fixture.detectChanges(); focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap; }); it('should be able to set initial focus target', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. focusTrapInstance.focusInitialElement(); expect(document.activeElement!.id).toBe('middle'); }); it('should be able to prioritize the first focus target', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. focusTrapInstance.focusFirstTabbableElement(); expect(document.activeElement!.id).toBe('first'); }); it('should be able to prioritize the last focus target', () => { // Because we can't mimic a real tab press focus change in a unit test, just call the // focus event handler directly. focusTrapInstance.focusLastTabbableElement(); expect(document.activeElement!.id).toBe('last'); }); it('should warn if the initial focus target is not focusable', () => { const alternateFixture = TestBed.createComponent(FocusTrapUnfocusableTarget); alternateFixture.detectChanges(); focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap; spyOn(console, 'warn'); focusTrapInstance.focusInitialElement(); expect(console.warn).toHaveBeenCalled(); }); }); describe('special cases', () => { it('should not throw when it has a SVG child', () => { let fixture = TestBed.createComponent(FocusTrapWithSvg); fixture.detectChanges(); let focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap; expect(() => focusTrapInstance.focusFirstTabbableElement()).not.toThrow(); expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow(); }); }); describe('with autoCapture', () => { it('should automatically capture and return focus on init / destroy', async(() => { const fixture = TestBed.createComponent(FocusTrapWithAutoCapture); fixture.detectChanges(); const buttonOutsideTrappedRegion = fixture.nativeElement.querySelector('button'); buttonOutsideTrappedRegion.focus(); expect(document.activeElement).toBe(buttonOutsideTrappedRegion); fixture.componentInstance.showTrappedRegion = true; fixture.detectChanges(); fixture.whenStable().then(() => { expect(document.activeElement!.id).toBe('auto-capture-target'); fixture.destroy(); expect(document.activeElement).toBe(buttonOutsideTrappedRegion); }); })); }); }); @Component({ template: `
` }) class SimpleFocusTrap { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; } @Component({ template: `
` }) class FocusTrapWithAutoCapture { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; showTrappedRegion = false; } @Component({ template: `
` }) class FocusTrapWithBindings { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; renderFocusTrap = true; _isFocusTrapEnabled = true; } @Component({ template: `
` }) class FocusTrapTargets { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; } @Component({ template: `
` }) class FocusTrapUnfocusableTarget { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; } @Component({ template: `
` }) class FocusTrapWithSvg { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; } @Component({ template: `

Hello

` }) class FocusTrapWithoutFocusableElements { @ViewChild(CdkTrapFocus, {static: false}) focusTrapDirective: CdkTrapFocus; }