From 478a2e42699915eccd9c56f2d382612596e02711 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 9 Feb 2017 00:02:24 +0100 Subject: [PATCH] chore(focus-classes): fix failing tests when browser is blurred (#2935) * chore(focus-classes): fix failing tests when browser is blurred On Saucelabs, browsers will run simultaneously and therefore can't focus all browser windows at the same time. This is problematic when testing focus states. Chrome and Firefoxonly fire FocusEvents when the window is focused. This issue also appears locally. Fixes #2903. * Extract patch logic into extra function --- src/lib/core/style/focus-classes.spec.ts | 69 +++++++++++------------- test/karma-test-shim.js | 63 ++++++++++------------ 2 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index e3e86b2004c..bb7a735b611 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -4,26 +4,16 @@ import {StyleModule} from './index'; import {By} from '@angular/platform-browser'; import {TAB} from '../keyboard/keycodes'; import {FocusOriginMonitor} from './focus-classes'; -import {PlatformModule} from '../platform/index'; -import {Platform} from '../platform/platform'; - - -// NOTE: Firefox only fires focus & blur events when it is the currently active window. -// This is not always the case on our CI setup, therefore we disable tests that depend on these -// events firing for Firefox. We may be able to fix this by configuring our CI to start Firefox with -// the following preference: focusmanager.testmode = true - describe('FocusOriginMonitor', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; let buttonRenderer: Renderer; let focusOriginMonitor: FocusOriginMonitor; - let platform: Platform; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StyleModule, PlatformModule], + imports: [StyleModule], declarations: [ PlainButton, ], @@ -32,21 +22,21 @@ describe('FocusOriginMonitor', () => { TestBed.compileComponents(); })); - beforeEach(inject([FocusOriginMonitor, Platform], (fom: FocusOriginMonitor, pfm: Platform) => { + beforeEach(inject([FocusOriginMonitor], (fom: FocusOriginMonitor) => { fixture = TestBed.createComponent(PlainButton); fixture.detectChanges(); buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; buttonRenderer = fixture.componentInstance.renderer; focusOriginMonitor = fom; - platform = pfm; focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer); + + // Patch the element focus to properly emit focus events when the browser is blurred. + patchElementFocus(buttonElement); })); it('manually registered element should receive focus classes', async(() => { - if (platform.FIREFOX) { return; } - buttonElement.focus(); fixture.detectChanges(); @@ -59,8 +49,6 @@ describe('FocusOriginMonitor', () => { })); it('should detect focus via keyboard', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); @@ -79,8 +67,6 @@ describe('FocusOriginMonitor', () => { })); it('should detect focus via mouse', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -99,8 +85,6 @@ describe('FocusOriginMonitor', () => { })); it('should detect programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); @@ -118,8 +102,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia keyboard should simulate keyboard focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard'); fixture.detectChanges(); @@ -136,8 +118,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia mouse should simulate mouse focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse'); fixture.detectChanges(); @@ -154,8 +134,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia program should simulate programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program'); fixture.detectChanges(); @@ -176,11 +154,10 @@ describe('FocusOriginMonitor', () => { describe('cdkFocusClasses', () => { let fixture: ComponentFixture; let buttonElement: HTMLElement; - let platform: Platform; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StyleModule, PlatformModule], + imports: [StyleModule], declarations: [ ButtonWithFocusClasses, ], @@ -189,21 +166,21 @@ describe('cdkFocusClasses', () => { TestBed.compileComponents(); })); - beforeEach(inject([Platform], (pfm: Platform) => { + beforeEach(() => { fixture = TestBed.createComponent(ButtonWithFocusClasses); fixture.detectChanges(); buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; - platform = pfm; - })); + + // Patch the element focus to properly emit focus events when the browser is blurred. + patchElementFocus(buttonElement); + }); it('should initially not be focused', () => { expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes'); }); it('should detect focus via keyboard', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via keyboard. dispatchKeydownEvent(document, TAB); buttonElement.focus(); @@ -222,8 +199,6 @@ describe('cdkFocusClasses', () => { })); it('should detect focus via mouse', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -242,8 +217,6 @@ describe('cdkFocusClasses', () => { })); it('should detect programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); @@ -271,6 +244,7 @@ class PlainButton { @Component({template: ``}) class ButtonWithFocusClasses {} +// TODO(devversion): move helper functions into a global utility file. See #2902 /** Dispatches a mousedown event on the specified element. */ function dispatchMousedownEvent(element: Node) { @@ -291,3 +265,22 @@ function dispatchKeydownEvent(element: Node, keyCode: number) { }); element.dispatchEvent(event); } + +/** Dispatches a focus event on the specified element. */ +function dispatchFocusEvent(element: Node, type = 'focus') { + let event = document.createEvent('Event'); + event.initEvent(type, true, true); + element.dispatchEvent(event); +} + +/** Patches an elements focus method to properly emit focus events when the browser is blurred. */ +function patchElementFocus(element: HTMLElement) { + // On Saucelabs, browsers will run simultaneously and therefore can't focus all browser windows + // at the same time. This is problematic when testing focus states. Chrome and Firefox + // only fire FocusEvents when the window is focused. This issue also appears locally. + let _nativeButtonFocus = element.focus.bind(element); + + element.focus = () => { + document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(element); + }; +} diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index d19e7eca1ac..71c79ce7544 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -2,34 +2,37 @@ Error.stackTraceLimit = Infinity; jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; -__karma__.loaded = function () { -}; +__karma__.loaded = function () {}; -var distPath = '/base/dist/'; +var baseDir = '/base/dist/'; +var configFile = baseDir + '@angular/material/system-config-spec.js'; +var specFiles = Object.keys(window.__karma__.files).filter(isMaterialSpecFile); -function isJsFile(path) { - return path.slice(-3) == '.js'; +// Configure the base path for dist/ +System.config({baseURL: baseDir}); + +// Load the spec SystemJS configuration file. +System.import(configFile) + .then(configureTestBed) + .then(runMaterialSpecs) + .then(__karma__.start, __karma__.error); + + +/** Runs the Angular Material specs in Karma. */ +function runMaterialSpecs() { + // By importing all spec files, Karma will run the tests directly. + return Promise.all(specFiles.map(function(fileName) { + return System.import(fileName); + })); } -function isSpecFile(path) { - return path.slice(-8) == '.spec.js'; +/** Whether the specified file is part of Angular Material. */ +function isMaterialSpecFile(path) { + return path.slice(-8) === '.spec.js' && path.indexOf('vendor') === -1; } -function isMaterialFile(path) { - return isJsFile(path) && path.indexOf('vendor') == -1; -} - -var allSpecFiles = Object.keys(window.__karma__.files) - .filter(isSpecFile) - .filter(isMaterialFile); - -// Load our SystemJS configuration. -System.config({ - baseURL: distPath -}); - -System.import(distPath + '@angular/material/system-config-spec.js').then(function() { - // Load and configure the TestComponentBuilder. +/** Configures Angular's TestBed. */ +function configureTestBed() { return Promise.all([ System.import('@angular/core/testing'), System.import('@angular/platform-browser-dynamic/testing') @@ -40,16 +43,8 @@ System.import(distPath + '@angular/material/system-config-spec.js').then(functio jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; testing.TestBed.initTestEnvironment( - testingBrowser.BrowserDynamicTestingModule, - testingBrowser.platformBrowserDynamicTesting()); + testingBrowser.BrowserDynamicTestingModule, + testingBrowser.platformBrowserDynamicTesting() + ); }); -}).then(function() { - // Finally, load all spec files. - // This will run the tests directly. - return Promise.all( - allSpecFiles.map(function (moduleName) { - return System.import(moduleName).then(function(module) { - return module; - }); - })); -}).then(__karma__.start, __karma__.error); +} \ No newline at end of file