Skip to content

Commit 6ec3e48

Browse files
committed
fix(material/tabs): avoid timeouts in background tabs
The tabs have a couple of `requestAnimationFrame` calls when checking for the size/position of elements which can cause tests to hang when they're in a background tab. The problem is that browsers block `requestAnimationFrame` when the browser is out of focus and test harnesses will wait for the call to resolve. These changes switch to `NgZone.onStable` in an attempt to resolve the issue. Fixes #23964.
1 parent f91b98f commit 6ec3e48

File tree

5 files changed

+26
-55
lines changed

5 files changed

+26
-55
lines changed

src/material-experimental/mdc-tabs/ink-bar.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export class MatInkBarFoundation {
8080
}
8181
},
8282
setContentStyleProperty: (propName, value) => {
83-
this._inkBarContentElement.style.setProperty(propName, value);
83+
if (!this._destroyed) {
84+
this._inkBarContentElement.style.setProperty(propName, value);
85+
}
8486
},
8587
computeContentClientRect: () => {
8688
// `getBoundingClientRect` isn't available on the server.

src/material-experimental/mdc-tabs/tab-header.spec.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Direction, Directionality} from '@angular/cdk/bidi';
1+
import {Direction} from '@angular/cdk/bidi';
22
import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
33
import {PortalModule} from '@angular/cdk/portal';
44
import {ScrollingModule, ViewportRuler} from '@angular/cdk/scrolling';
@@ -23,25 +23,18 @@ import {MatRippleModule} from '@angular/material-experimental/mdc-core';
2323
import {By} from '@angular/platform-browser';
2424
import {MatTabHeader} from './tab-header';
2525
import {MatTabLabelWrapper} from './tab-label-wrapper';
26-
import {Subject} from 'rxjs';
2726
import {ObserversModule, MutationObserverFactory} from '@angular/cdk/observers';
2827

2928
describe('MDC-based MatTabHeader', () => {
30-
let dir: Direction = 'ltr';
31-
let change = new Subject();
3229
let fixture: ComponentFixture<SimpleTabHeaderApp>;
3330
let appComponent: SimpleTabHeaderApp;
3431

3532
beforeEach(
3633
waitForAsync(() => {
37-
dir = 'ltr';
3834
TestBed.configureTestingModule({
3935
imports: [CommonModule, PortalModule, MatRippleModule, ScrollingModule, ObserversModule],
4036
declarations: [MatTabHeader, MatTabLabelWrapper, SimpleTabHeaderApp],
41-
providers: [
42-
ViewportRuler,
43-
{provide: Directionality, useFactory: () => ({value: dir, change: change})},
44-
],
37+
providers: [ViewportRuler],
4538
});
4639

4740
TestBed.compileComponents();
@@ -238,11 +231,10 @@ describe('MDC-based MatTabHeader', () => {
238231
describe('pagination', () => {
239232
describe('ltr', () => {
240233
beforeEach(() => {
241-
dir = 'ltr';
242234
fixture = TestBed.createComponent(SimpleTabHeaderApp);
243-
fixture.detectChanges();
244-
245235
appComponent = fixture.componentInstance;
236+
appComponent.dir = 'ltr';
237+
fixture.detectChanges();
246238
});
247239

248240
it('should show width when tab list width exceeds container', () => {
@@ -322,11 +314,9 @@ describe('MDC-based MatTabHeader', () => {
322314

323315
describe('rtl', () => {
324316
beforeEach(() => {
325-
dir = 'rtl';
326317
fixture = TestBed.createComponent(SimpleTabHeaderApp);
327318
appComponent = fixture.componentInstance;
328319
appComponent.dir = 'rtl';
329-
330320
fixture.detectChanges();
331321
});
332322

@@ -607,9 +597,9 @@ describe('MDC-based MatTabHeader', () => {
607597

608598
fixture.detectChanges();
609599

610-
change.next();
600+
fixture.componentInstance.dir = 'rtl';
611601
fixture.detectChanges();
612-
tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
602+
tick();
613603

614604
expect(inkBar.alignToElement).toHaveBeenCalled();
615605
}));

src/material/tabs/ink-bar.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Directive, ElementRef, Inject, InjectionToken, NgZone, Optional} from '@angular/core';
1010
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
11+
import {take} from 'rxjs/operators';
1112

1213
/**
1314
* Interface for a a MatInkBar positioner method, defining the positioning and width of the ink
@@ -65,14 +66,12 @@ export class MatInkBar {
6566
*/
6667
alignToElement(element: HTMLElement) {
6768
this.show();
68-
69-
if (typeof requestAnimationFrame !== 'undefined') {
70-
this._ngZone.runOutsideAngular(() => {
71-
requestAnimationFrame(() => this._setStyles(element));
72-
});
73-
} else {
74-
this._setStyles(element);
75-
}
69+
this._ngZone.onStable.pipe(take(1)).subscribe(() => {
70+
const positions = this._inkBarPositioner(element);
71+
const inkBar: HTMLElement = this._elementRef.nativeElement;
72+
inkBar.style.left = positions.left;
73+
inkBar.style.width = positions.width;
74+
});
7675
}
7776

7877
/** Shows the ink bar. */
@@ -84,16 +83,4 @@ export class MatInkBar {
8483
hide(): void {
8584
this._elementRef.nativeElement.style.visibility = 'hidden';
8685
}
87-
88-
/**
89-
* Sets the proper styles to the ink bar element.
90-
* @param element
91-
*/
92-
private _setStyles(element: HTMLElement) {
93-
const positions = this._inkBarPositioner(element);
94-
const inkBar: HTMLElement = this._elementRef.nativeElement;
95-
96-
inkBar.style.left = positions.left;
97-
inkBar.style.width = positions.width;
98-
}
9986
}

src/material/tabs/paginated-tab-header.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {ViewportRuler} from '@angular/cdk/scrolling';
3232
import {FocusKeyManager, FocusableOption} from '@angular/cdk/a11y';
3333
import {ENTER, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
3434
import {merge, of as observableOf, Subject, timer, fromEvent} from 'rxjs';
35-
import {takeUntil} from 'rxjs/operators';
35+
import {take, takeUntil} from 'rxjs/operators';
3636
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
3737
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
3838

@@ -212,7 +212,9 @@ export abstract class MatPaginatedTabHeader
212212

213213
// Defer the first call in order to allow for slower browsers to lay out the elements.
214214
// This helps in cases where the user lands directly on a page with paginated tabs.
215-
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign();
215+
// Note that we use `onStable` instead of `requestAnimationFrame`, because the latter
216+
// can hold up tests that are in a background tab.
217+
this._ngZone.onStable.pipe(take(1)).subscribe(realign);
216218

217219
// On dir change or window resize, realign the ink bar and update the orientation of
218220
// the key manager if the direction has changed.

src/material/tabs/tab-header.spec.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Direction, Directionality} from '@angular/cdk/bidi';
1+
import {Direction} from '@angular/cdk/bidi';
22
import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
33
import {PortalModule} from '@angular/cdk/portal';
44
import {ScrollingModule, ViewportRuler} from '@angular/cdk/scrolling';
@@ -24,25 +24,18 @@ import {By} from '@angular/platform-browser';
2424
import {MatInkBar} from './ink-bar';
2525
import {MatTabHeader} from './tab-header';
2626
import {MatTabLabelWrapper} from './tab-label-wrapper';
27-
import {Subject} from 'rxjs';
2827
import {ObserversModule, MutationObserverFactory} from '@angular/cdk/observers';
2928

3029
describe('MatTabHeader', () => {
31-
let dir: Direction = 'ltr';
32-
let change = new Subject();
3330
let fixture: ComponentFixture<SimpleTabHeaderApp>;
3431
let appComponent: SimpleTabHeaderApp;
3532

3633
beforeEach(
3734
waitForAsync(() => {
38-
dir = 'ltr';
3935
TestBed.configureTestingModule({
4036
imports: [CommonModule, PortalModule, MatRippleModule, ScrollingModule, ObserversModule],
4137
declarations: [MatTabHeader, MatInkBar, MatTabLabelWrapper, SimpleTabHeaderApp],
42-
providers: [
43-
ViewportRuler,
44-
{provide: Directionality, useFactory: () => ({value: dir, change: change})},
45-
],
38+
providers: [ViewportRuler],
4639
});
4740

4841
TestBed.compileComponents();
@@ -239,11 +232,10 @@ describe('MatTabHeader', () => {
239232
describe('pagination', () => {
240233
describe('ltr', () => {
241234
beforeEach(() => {
242-
dir = 'ltr';
243235
fixture = TestBed.createComponent(SimpleTabHeaderApp);
244-
fixture.detectChanges();
245-
246236
appComponent = fixture.componentInstance;
237+
appComponent.dir = 'ltr';
238+
fixture.detectChanges();
247239
});
248240

249241
it('should show width when tab list width exceeds container', () => {
@@ -319,11 +311,9 @@ describe('MatTabHeader', () => {
319311

320312
describe('rtl', () => {
321313
beforeEach(() => {
322-
dir = 'rtl';
323314
fixture = TestBed.createComponent(SimpleTabHeaderApp);
324315
appComponent = fixture.componentInstance;
325316
appComponent.dir = 'rtl';
326-
327317
fixture.detectChanges();
328318
});
329319

@@ -603,9 +593,9 @@ describe('MatTabHeader', () => {
603593

604594
fixture.detectChanges();
605595

606-
change.next();
596+
fixture.componentInstance.dir = 'rtl';
607597
fixture.detectChanges();
608-
tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
598+
tick();
609599

610600
expect(inkBar.alignToElement).toHaveBeenCalled();
611601
}));

0 commit comments

Comments
 (0)