From c9f45cf4a29bf94ce00556f25da776a83490b1fe Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sat, 4 Feb 2017 22:41:39 +0100 Subject: [PATCH 1/2] 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. --- src/lib/core/style/focus-classes.spec.ts | 67 ++++++++++-------------- test/karma-test-shim.js | 61 ++++++++++----------- 2 files changed, 57 insertions(+), 71 deletions(-) diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index e3e86b2004c3..cfbd83c97156 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,27 @@ 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); + + // 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 = buttonElement.focus.bind(buttonElement); + buttonElement.focus = () => { + document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(buttonElement); + }; + })); it('manually registered element should receive focus classes', async(() => { - if (platform.FIREFOX) { return; } - buttonElement.focus(); fixture.detectChanges(); @@ -59,8 +55,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 +73,6 @@ describe('FocusOriginMonitor', () => { })); it('should detect focus via mouse', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -99,8 +91,6 @@ describe('FocusOriginMonitor', () => { })); it('should detect programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); @@ -118,8 +108,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia keyboard should simulate keyboard focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard'); fixture.detectChanges(); @@ -136,8 +124,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia mouse should simulate mouse focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse'); fixture.detectChanges(); @@ -154,8 +140,6 @@ describe('FocusOriginMonitor', () => { })); it('focusVia program should simulate programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program'); fixture.detectChanges(); @@ -176,11 +160,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 +172,26 @@ 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; - })); + + // 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 = buttonElement.focus.bind(buttonElement); + buttonElement.focus = () => { + document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(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 +210,6 @@ describe('cdkFocusClasses', () => { })); it('should detect focus via mouse', async(() => { - if (platform.FIREFOX) { return; } - // Simulate focus via mouse. dispatchMousedownEvent(document); buttonElement.focus(); @@ -242,8 +228,6 @@ describe('cdkFocusClasses', () => { })); it('should detect programmatic focus', async(() => { - if (platform.FIREFOX) { return; } - // Programmatically focus. buttonElement.focus(); fixture.detectChanges(); @@ -291,3 +275,10 @@ 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); +} diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index d19e7eca1acf..71c79ce75448 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}); -function isSpecFile(path) { - return path.slice(-8) == '.spec.js'; -} +// Load the spec SystemJS configuration file. +System.import(configFile) + .then(configureTestBed) + .then(runMaterialSpecs) + .then(__karma__.start, __karma__.error); -function isMaterialFile(path) { - return isJsFile(path) && path.indexOf('vendor') == -1; -} -var allSpecFiles = Object.keys(window.__karma__.files) - .filter(isSpecFile) - .filter(isMaterialFile); +/** 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); + })); +} -// Load our SystemJS configuration. -System.config({ - baseURL: distPath -}); +/** Whether the specified file is part of Angular Material. */ +function isMaterialSpecFile(path) { + return path.slice(-8) === '.spec.js' && path.indexOf('vendor') === -1; +} -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 From bf488cd08747b7bafd238d7ef66c3962d237590a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 5 Feb 2017 11:44:32 +0100 Subject: [PATCH 2/2] Extract patch logic into extra function --- src/lib/core/style/focus-classes.spec.ts | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/lib/core/style/focus-classes.spec.ts b/src/lib/core/style/focus-classes.spec.ts index cfbd83c97156..bb7a735b6115 100644 --- a/src/lib/core/style/focus-classes.spec.ts +++ b/src/lib/core/style/focus-classes.spec.ts @@ -32,14 +32,8 @@ describe('FocusOriginMonitor', () => { focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer); - // 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 = buttonElement.focus.bind(buttonElement); - buttonElement.focus = () => { - document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(buttonElement); - }; - + // 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(() => { @@ -178,13 +172,8 @@ describe('cdkFocusClasses', () => { buttonElement = fixture.debugElement.query(By.css('button')).nativeElement; - // 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 = buttonElement.focus.bind(buttonElement); - buttonElement.focus = () => { - document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(buttonElement); - }; + // Patch the element focus to properly emit focus events when the browser is blurred. + patchElementFocus(buttonElement); }); it('should initially not be focused', () => { @@ -255,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) { @@ -282,3 +272,15 @@ function dispatchFocusEvent(element: Node, type = 'focus') { 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); + }; +}