Skip to content

Commit 72128f5

Browse files
fix(breakpoint-observer): fix the breakpoint observer emit count and accuracy
The breakpoint observer emits multiple and incorrect states when more than one query changes. Debounce the observer emissions to eliminate the incorrect states and emit once. References #10925
1 parent 03a9a39 commit 72128f5

File tree

5 files changed

+55
-22
lines changed

5 files changed

+55
-22
lines changed

src/cdk/layout/breakpoints-observer.spec.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import {LayoutModule} from './layout-module';
1010
import {BreakpointObserver, BreakpointState} from './breakpoints-observer';
1111
import {MediaMatcher} from './media-matcher';
12-
import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing';
12+
import {fakeAsync, TestBed, inject, flush, tick} from '@angular/core/testing';
1313
import {Injectable} from '@angular/core';
1414
import {Subscription} from 'rxjs';
15-
import {take} from 'rxjs/operators';
15+
import {skip, take} from 'rxjs/operators';
1616

1717
describe('BreakpointObserver', () => {
1818
let breakpointObserver: BreakpointObserver;
@@ -93,10 +93,10 @@ describe('BreakpointObserver', () => {
9393
queryMatchState = state.matches;
9494
});
9595

96-
flush();
96+
tick();
9797
expect(queryMatchState).toBeTruthy();
9898
mediaMatcher.setMatchesQuery(query, false);
99-
flush();
99+
tick();
100100
expect(queryMatchState).toBeFalsy();
101101
}));
102102

@@ -111,33 +111,50 @@ describe('BreakpointObserver', () => {
111111

112112
mediaMatcher.setMatchesQuery(queryOne, false);
113113
mediaMatcher.setMatchesQuery(queryTwo, false);
114-
flush();
114+
tick();
115115
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});
116116

117117
mediaMatcher.setMatchesQuery(queryOne, true);
118118
mediaMatcher.setMatchesQuery(queryTwo, false);
119-
flush();
119+
tick();
120120
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
121121
}));
122122

123123
it('emits a true matches state when the query is matched', fakeAsync(() => {
124124
const query = '(width: 999px)';
125125
breakpointObserver.observe(query).subscribe();
126126
mediaMatcher.setMatchesQuery(query, true);
127+
tick();
127128
expect(breakpointObserver.isMatched(query)).toBeTruthy();
128129
}));
129130

130131
it('emits a false matches state when the query is not matched', fakeAsync(() => {
131132
const query = '(width: 999px)';
132133
breakpointObserver.observe(query).subscribe();
133134
mediaMatcher.setMatchesQuery(query, false);
135+
tick();
134136
expect(breakpointObserver.isMatched(query)).toBeFalsy();
135137
}));
136138

139+
it('emits one event when multiple queries change', fakeAsync(() => {
140+
const observer = jasmine.createSpy('observer');
141+
const queryOne = '(width: 700px)';
142+
const queryTwo = '(width: 999px)';
143+
breakpointObserver.observe([queryOne, queryTwo])
144+
.pipe(skip(1))
145+
.subscribe(observer);
146+
147+
mediaMatcher.setMatchesQuery(queryOne, true);
148+
mediaMatcher.setMatchesQuery(queryTwo, false);
149+
150+
tick();
151+
expect(observer).toHaveBeenCalledTimes(1);
152+
}));
153+
137154
it('should not complete other subscribers when preceding subscriber completes', fakeAsync(() => {
138155
const queryOne = '(width: 700px)';
139156
const queryTwo = '(width: 999px)';
140-
const breakpoint = breakpointObserver.observe([queryOne, queryTwo]);
157+
const breakpoint = breakpointObserver.observe([queryOne, queryTwo]).pipe(skip(1));
141158
const subscriptions: Subscription[] = [];
142159
let emittedValues: number[] = [];
143160

@@ -148,14 +165,14 @@ describe('BreakpointObserver', () => {
148165

149166
mediaMatcher.setMatchesQuery(queryOne, true);
150167
mediaMatcher.setMatchesQuery(queryTwo, false);
151-
flush();
168+
tick();
152169

153170
expect(emittedValues).toEqual([1, 2, 3, 4]);
154171
emittedValues = [];
155172

156173
mediaMatcher.setMatchesQuery(queryOne, false);
157174
mediaMatcher.setMatchesQuery(queryTwo, true);
158-
flush();
175+
tick();
159176

160177
expect(emittedValues).toEqual([1, 3, 4]);
161178

@@ -172,7 +189,11 @@ export class FakeMediaQueryList {
172189
/** Toggles the matches state and "emits" a change event. */
173190
setMatches(matches: boolean) {
174191
this.matches = matches;
175-
this._listeners.forEach(listener => listener(this as any));
192+
193+
/** Simulate an asynchronous task. */
194+
setTimeout(() => {
195+
this._listeners.forEach(listener => listener(this as any));
196+
});
176197
}
177198

178199
/** Registers a callback method for change events. */

src/cdk/layout/breakpoints-observer.ts

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

99
import {Injectable, NgZone, OnDestroy} from '@angular/core';
1010
import {MediaMatcher} from './media-matcher';
11-
import {asapScheduler, combineLatest, Observable, Subject, Observer} from 'rxjs';
11+
import {combineLatest, Observable, Subject, Observer} from 'rxjs';
1212
import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators';
1313
import {coerceArray} from '@angular/cdk/coercion';
1414

@@ -76,7 +76,7 @@ export class BreakpointObserver implements OnDestroy {
7676
const observables = queries.map(query => this._registerQuery(query).observable);
7777

7878
return combineLatest(observables).pipe(
79-
debounceTime(0, asapScheduler),
79+
debounceTime(0),
8080
map((breakpointStates: InternalBreakpointState[]) => {
8181
const response: BreakpointState = {
8282
matches: false,

src/material/bottom-sheet/bottom-sheet.spec.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ describe('MatBottomSheet', () => {
119119
// callback should not be called before animation is complete
120120
expect(spy).not.toHaveBeenCalled();
121121

122-
flushMicrotasks();
122+
tick();
123123
expect(spy).toHaveBeenCalled();
124124
}));
125125

@@ -212,6 +212,8 @@ describe('MatBottomSheet', () => {
212212
dispatchKeyboardEvent(document.body, 'keydown', A, container);
213213

214214
expect(spy).toHaveBeenCalledTimes(3);
215+
216+
tick(); // Tick for the breakpoint observer
215217
}));
216218

217219
it('should allow setting the layout direction', () => {
@@ -296,7 +298,7 @@ describe('MatBottomSheet', () => {
296298
viewContainerFixture.detectChanges();
297299

298300
// Wait for the dismiss animation to finish.
299-
flush();
301+
tick();
300302
bottomSheetRef = bottomSheet.open(TacoMsg, config);
301303
viewContainerFixture.detectChanges();
302304

@@ -400,7 +402,7 @@ describe('MatBottomSheet', () => {
400402

401403
mockLocation.simulateUrlPop('');
402404
viewContainerFixture.detectChanges();
403-
flush();
405+
tick();
404406

405407
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
406408
}));
@@ -440,7 +442,7 @@ describe('MatBottomSheet', () => {
440442
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
441443
backdrop.click();
442444
viewContainerFixture.detectChanges();
443-
flush();
445+
tick();
444446

445447
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
446448
}));
@@ -454,7 +456,7 @@ describe('MatBottomSheet', () => {
454456
viewContainerFixture.detectChanges();
455457
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE);
456458
viewContainerFixture.detectChanges();
457-
flush();
459+
tick();
458460

459461
expect(overlayContainerElement.querySelector('mat-bottom-sheet-container')).toBeTruthy();
460462
}));
@@ -541,7 +543,7 @@ describe('MatBottomSheet', () => {
541543
});
542544

543545
viewContainerFixture.detectChanges();
544-
flushMicrotasks();
546+
tick();
545547

546548
expect(document.activeElement!.tagName).toBe('MAT-BOTTOM-SHEET-CONTAINER',
547549
'Expected bottom sheet container to be focused.');
@@ -554,22 +556,22 @@ describe('MatBottomSheet', () => {
554556
});
555557

556558
viewContainerFixture.detectChanges();
557-
flushMicrotasks();
559+
tick();
558560

559561
const focusTrapAnchors = overlayContainerElement.querySelectorAll('.cdk-focus-trap-anchor');
560562

561563
expect(focusTrapAnchors.length).toBeGreaterThan(0);
562564
}));
563565

564-
it('should focus the first tabbable element of the bottom sheet on open when' +
566+
it('should focus the first tabbable element of the bottom sheet on open when ' +
565567
'autoFocus is enabled', fakeAsync(() => {
566568
bottomSheet.open(PizzaMsg, {
567569
viewContainerRef: testViewContainerRef,
568570
autoFocus: true
569571
});
570572

571573
viewContainerFixture.detectChanges();
572-
flushMicrotasks();
574+
tick();
573575

574576
expect(document.activeElement!.tagName).toBe('INPUT',
575577
'Expected first tabbable element (input) in the sheet to be focused.');
@@ -582,7 +584,7 @@ describe('MatBottomSheet', () => {
582584
});
583585

584586
viewContainerFixture.detectChanges();
585-
flushMicrotasks();
587+
tick();
586588

587589
expect(document.activeElement!.tagName).not.toBe('INPUT');
588590
}));

src/material/snack-bar/snack-bar.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ describe('MatSnackBar', () => {
198198
// Expect the live announcer to have been called with the display message and some
199199
// string for the politeness. We do not want to test for the default politeness here.
200200
expect(liveAnnouncer.announce).toHaveBeenCalledWith(simpleMessage, jasmine.any(String));
201+
202+
tick(); // Tick for the breakpoint observer
201203
}));
202204

203205
it('should be able to specify a custom announcement message', fakeAsync(() => {
@@ -213,6 +215,8 @@ describe('MatSnackBar', () => {
213215
.toBe(1, 'Expected the overlay with a custom `announcementMessage` to be added');
214216

215217
expect(liveAnnouncer.announce).toHaveBeenCalledWith('Custom announcement', 'assertive');
218+
219+
tick(); // Tick for the breakpoint observer
216220
}));
217221

218222
it('should be able to get dismissed through the service', fakeAsync(() => {

src/material/tooltip/tooltip.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ describe('MatTooltip', () => {
401401

402402
fixture.detectChanges();
403403
expect(overlayContainerElement.textContent).toContain(newMessage);
404+
405+
tick(); // Tick for the breakpoint observer
404406
}));
405407

406408
it('should allow extra classes to be set on the tooltip', fakeAsync(() => {
@@ -427,6 +429,8 @@ describe('MatTooltip', () => {
427429
'Expected to have the class after enabling matTooltipClass');
428430
expect(tooltipElement.classList).toContain('custom-two',
429431
'Expected to have the class after enabling matTooltipClass');
432+
433+
tick(); // Tick for the breakpoint observer
430434
}));
431435

432436
it('should be removed after parent destroyed', fakeAsync(() => {
@@ -558,6 +562,8 @@ describe('MatTooltip', () => {
558562

559563
expect(tooltipWrapper).toBeTruthy('Expected tooltip to be shown.');
560564
expect(tooltipWrapper.getAttribute('dir')).toBe('rtl', 'Expected tooltip to be in RTL mode.');
565+
566+
tick(); // Tick for the breakpoint observer
561567
}));
562568

563569
it('should keep the overlay direction in sync with the trigger direction', fakeAsync(() => {

0 commit comments

Comments
 (0)