Skip to content

Commit 478a2e4

Browse files
devversiontinayuangao
authored andcommitted
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
1 parent 86a10d2 commit 478a2e4

File tree

2 files changed

+59
-71
lines changed

2 files changed

+59
-71
lines changed

src/lib/core/style/focus-classes.spec.ts

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,16 @@ import {StyleModule} from './index';
44
import {By} from '@angular/platform-browser';
55
import {TAB} from '../keyboard/keycodes';
66
import {FocusOriginMonitor} from './focus-classes';
7-
import {PlatformModule} from '../platform/index';
8-
import {Platform} from '../platform/platform';
9-
10-
11-
// NOTE: Firefox only fires focus & blur events when it is the currently active window.
12-
// This is not always the case on our CI setup, therefore we disable tests that depend on these
13-
// events firing for Firefox. We may be able to fix this by configuring our CI to start Firefox with
14-
// the following preference: focusmanager.testmode = true
15-
167

178
describe('FocusOriginMonitor', () => {
189
let fixture: ComponentFixture<PlainButton>;
1910
let buttonElement: HTMLElement;
2011
let buttonRenderer: Renderer;
2112
let focusOriginMonitor: FocusOriginMonitor;
22-
let platform: Platform;
2313

2414
beforeEach(async(() => {
2515
TestBed.configureTestingModule({
26-
imports: [StyleModule, PlatformModule],
16+
imports: [StyleModule],
2717
declarations: [
2818
PlainButton,
2919
],
@@ -32,21 +22,21 @@ describe('FocusOriginMonitor', () => {
3222
TestBed.compileComponents();
3323
}));
3424

35-
beforeEach(inject([FocusOriginMonitor, Platform], (fom: FocusOriginMonitor, pfm: Platform) => {
25+
beforeEach(inject([FocusOriginMonitor], (fom: FocusOriginMonitor) => {
3626
fixture = TestBed.createComponent(PlainButton);
3727
fixture.detectChanges();
3828

3929
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
4030
buttonRenderer = fixture.componentInstance.renderer;
4131
focusOriginMonitor = fom;
42-
platform = pfm;
4332

4433
focusOriginMonitor.registerElementForFocusClasses(buttonElement, buttonRenderer);
34+
35+
// Patch the element focus to properly emit focus events when the browser is blurred.
36+
patchElementFocus(buttonElement);
4537
}));
4638

4739
it('manually registered element should receive focus classes', async(() => {
48-
if (platform.FIREFOX) { return; }
49-
5040
buttonElement.focus();
5141
fixture.detectChanges();
5242

@@ -59,8 +49,6 @@ describe('FocusOriginMonitor', () => {
5949
}));
6050

6151
it('should detect focus via keyboard', async(() => {
62-
if (platform.FIREFOX) { return; }
63-
6452
// Simulate focus via keyboard.
6553
dispatchKeydownEvent(document, TAB);
6654
buttonElement.focus();
@@ -79,8 +67,6 @@ describe('FocusOriginMonitor', () => {
7967
}));
8068

8169
it('should detect focus via mouse', async(() => {
82-
if (platform.FIREFOX) { return; }
83-
8470
// Simulate focus via mouse.
8571
dispatchMousedownEvent(document);
8672
buttonElement.focus();
@@ -99,8 +85,6 @@ describe('FocusOriginMonitor', () => {
9985
}));
10086

10187
it('should detect programmatic focus', async(() => {
102-
if (platform.FIREFOX) { return; }
103-
10488
// Programmatically focus.
10589
buttonElement.focus();
10690
fixture.detectChanges();
@@ -118,8 +102,6 @@ describe('FocusOriginMonitor', () => {
118102
}));
119103

120104
it('focusVia keyboard should simulate keyboard focus', async(() => {
121-
if (platform.FIREFOX) { return; }
122-
123105
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'keyboard');
124106
fixture.detectChanges();
125107

@@ -136,8 +118,6 @@ describe('FocusOriginMonitor', () => {
136118
}));
137119

138120
it('focusVia mouse should simulate mouse focus', async(() => {
139-
if (platform.FIREFOX) { return; }
140-
141121
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'mouse');
142122
fixture.detectChanges();
143123

@@ -154,8 +134,6 @@ describe('FocusOriginMonitor', () => {
154134
}));
155135

156136
it('focusVia program should simulate programmatic focus', async(() => {
157-
if (platform.FIREFOX) { return; }
158-
159137
focusOriginMonitor.focusVia(buttonElement, buttonRenderer, 'program');
160138
fixture.detectChanges();
161139

@@ -176,11 +154,10 @@ describe('FocusOriginMonitor', () => {
176154
describe('cdkFocusClasses', () => {
177155
let fixture: ComponentFixture<ButtonWithFocusClasses>;
178156
let buttonElement: HTMLElement;
179-
let platform: Platform;
180157

181158
beforeEach(async(() => {
182159
TestBed.configureTestingModule({
183-
imports: [StyleModule, PlatformModule],
160+
imports: [StyleModule],
184161
declarations: [
185162
ButtonWithFocusClasses,
186163
],
@@ -189,21 +166,21 @@ describe('cdkFocusClasses', () => {
189166
TestBed.compileComponents();
190167
}));
191168

192-
beforeEach(inject([Platform], (pfm: Platform) => {
169+
beforeEach(() => {
193170
fixture = TestBed.createComponent(ButtonWithFocusClasses);
194171
fixture.detectChanges();
195172

196173
buttonElement = fixture.debugElement.query(By.css('button')).nativeElement;
197-
platform = pfm;
198-
}));
174+
175+
// Patch the element focus to properly emit focus events when the browser is blurred.
176+
patchElementFocus(buttonElement);
177+
});
199178

200179
it('should initially not be focused', () => {
201180
expect(buttonElement.classList.length).toBe(0, 'button should not have focus classes');
202181
});
203182

204183
it('should detect focus via keyboard', async(() => {
205-
if (platform.FIREFOX) { return; }
206-
207184
// Simulate focus via keyboard.
208185
dispatchKeydownEvent(document, TAB);
209186
buttonElement.focus();
@@ -222,8 +199,6 @@ describe('cdkFocusClasses', () => {
222199
}));
223200

224201
it('should detect focus via mouse', async(() => {
225-
if (platform.FIREFOX) { return; }
226-
227202
// Simulate focus via mouse.
228203
dispatchMousedownEvent(document);
229204
buttonElement.focus();
@@ -242,8 +217,6 @@ describe('cdkFocusClasses', () => {
242217
}));
243218

244219
it('should detect programmatic focus', async(() => {
245-
if (platform.FIREFOX) { return; }
246-
247220
// Programmatically focus.
248221
buttonElement.focus();
249222
fixture.detectChanges();
@@ -271,6 +244,7 @@ class PlainButton {
271244
@Component({template: `<button cdkFocusClasses>focus me!</button>`})
272245
class ButtonWithFocusClasses {}
273246

247+
// TODO(devversion): move helper functions into a global utility file. See #2902
274248

275249
/** Dispatches a mousedown event on the specified element. */
276250
function dispatchMousedownEvent(element: Node) {
@@ -291,3 +265,22 @@ function dispatchKeydownEvent(element: Node, keyCode: number) {
291265
});
292266
element.dispatchEvent(event);
293267
}
268+
269+
/** Dispatches a focus event on the specified element. */
270+
function dispatchFocusEvent(element: Node, type = 'focus') {
271+
let event = document.createEvent('Event');
272+
event.initEvent(type, true, true);
273+
element.dispatchEvent(event);
274+
}
275+
276+
/** Patches an elements focus method to properly emit focus events when the browser is blurred. */
277+
function patchElementFocus(element: HTMLElement) {
278+
// On Saucelabs, browsers will run simultaneously and therefore can't focus all browser windows
279+
// at the same time. This is problematic when testing focus states. Chrome and Firefox
280+
// only fire FocusEvents when the window is focused. This issue also appears locally.
281+
let _nativeButtonFocus = element.focus.bind(element);
282+
283+
element.focus = () => {
284+
document.hasFocus() ? _nativeButtonFocus() : dispatchFocusEvent(element);
285+
};
286+
}

test/karma-test-shim.js

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,37 @@
22
Error.stackTraceLimit = Infinity;
33
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
44

5-
__karma__.loaded = function () {
6-
};
5+
__karma__.loaded = function () {};
76

8-
var distPath = '/base/dist/';
7+
var baseDir = '/base/dist/';
8+
var configFile = baseDir + '@angular/material/system-config-spec.js';
9+
var specFiles = Object.keys(window.__karma__.files).filter(isMaterialSpecFile);
910

10-
function isJsFile(path) {
11-
return path.slice(-3) == '.js';
12-
}
11+
// Configure the base path for dist/
12+
System.config({baseURL: baseDir});
1313

14-
function isSpecFile(path) {
15-
return path.slice(-8) == '.spec.js';
16-
}
14+
// Load the spec SystemJS configuration file.
15+
System.import(configFile)
16+
.then(configureTestBed)
17+
.then(runMaterialSpecs)
18+
.then(__karma__.start, __karma__.error);
1719

18-
function isMaterialFile(path) {
19-
return isJsFile(path) && path.indexOf('vendor') == -1;
20-
}
2120

22-
var allSpecFiles = Object.keys(window.__karma__.files)
23-
.filter(isSpecFile)
24-
.filter(isMaterialFile);
21+
/** Runs the Angular Material specs in Karma. */
22+
function runMaterialSpecs() {
23+
// By importing all spec files, Karma will run the tests directly.
24+
return Promise.all(specFiles.map(function(fileName) {
25+
return System.import(fileName);
26+
}));
27+
}
2528

26-
// Load our SystemJS configuration.
27-
System.config({
28-
baseURL: distPath
29-
});
29+
/** Whether the specified file is part of Angular Material. */
30+
function isMaterialSpecFile(path) {
31+
return path.slice(-8) === '.spec.js' && path.indexOf('vendor') === -1;
32+
}
3033

31-
System.import(distPath + '@angular/material/system-config-spec.js').then(function() {
32-
// Load and configure the TestComponentBuilder.
34+
/** Configures Angular's TestBed. */
35+
function configureTestBed() {
3336
return Promise.all([
3437
System.import('@angular/core/testing'),
3538
System.import('@angular/platform-browser-dynamic/testing')
@@ -40,16 +43,8 @@ System.import(distPath + '@angular/material/system-config-spec.js').then(functio
4043
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
4144

4245
testing.TestBed.initTestEnvironment(
43-
testingBrowser.BrowserDynamicTestingModule,
44-
testingBrowser.platformBrowserDynamicTesting());
46+
testingBrowser.BrowserDynamicTestingModule,
47+
testingBrowser.platformBrowserDynamicTesting()
48+
);
4549
});
46-
}).then(function() {
47-
// Finally, load all spec files.
48-
// This will run the tests directly.
49-
return Promise.all(
50-
allSpecFiles.map(function (moduleName) {
51-
return System.import(moduleName).then(function(module) {
52-
return module;
53-
});
54-
}));
55-
}).then(__karma__.start, __karma__.error);
50+
}

0 commit comments

Comments
 (0)