diff --git a/src/cdk/layout/breakpoints-observer.spec.ts b/src/cdk/layout/breakpoints-observer.spec.ts index daa9e443f2d..ef47470c7bd 100644 --- a/src/cdk/layout/breakpoints-observer.spec.ts +++ b/src/cdk/layout/breakpoints-observer.spec.ts @@ -9,10 +9,10 @@ import {LayoutModule} from './layout-module'; import {BreakpointObserver, BreakpointState} from './breakpoints-observer'; import {MediaMatcher} from './media-matcher'; -import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing'; +import {fakeAsync, TestBed, inject, flush, tick} from '@angular/core/testing'; import {Injectable} from '@angular/core'; import {Subscription} from 'rxjs'; -import {take} from 'rxjs/operators'; +import {skip, take} from 'rxjs/operators'; describe('BreakpointObserver', () => { let breakpointObserver: BreakpointObserver; @@ -93,10 +93,10 @@ describe('BreakpointObserver', () => { queryMatchState = state.matches; }); - flush(); + tick(); expect(queryMatchState).toBeTruthy(); mediaMatcher.setMatchesQuery(query, false); - flush(); + tick(); expect(queryMatchState).toBeFalsy(); })); @@ -108,15 +108,16 @@ describe('BreakpointObserver', () => { breakpointObserver.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => { state = breakpoint; }); + expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: true}); mediaMatcher.setMatchesQuery(queryOne, false); mediaMatcher.setMatchesQuery(queryTwo, false); - flush(); + tick(); expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false}); mediaMatcher.setMatchesQuery(queryOne, true); mediaMatcher.setMatchesQuery(queryTwo, false); - flush(); + tick(); expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false}); })); @@ -124,6 +125,7 @@ describe('BreakpointObserver', () => { const query = '(width: 999px)'; breakpointObserver.observe(query).subscribe(); mediaMatcher.setMatchesQuery(query, true); + tick(); expect(breakpointObserver.isMatched(query)).toBeTruthy(); })); @@ -131,13 +133,29 @@ describe('BreakpointObserver', () => { const query = '(width: 999px)'; breakpointObserver.observe(query).subscribe(); mediaMatcher.setMatchesQuery(query, false); + tick(); expect(breakpointObserver.isMatched(query)).toBeFalsy(); })); + it('emits one event when multiple queries change', fakeAsync(() => { + const observer = jasmine.createSpy('observer'); + const queryOne = '(width: 700px)'; + const queryTwo = '(width: 999px)'; + breakpointObserver.observe([queryOne, queryTwo]) + .pipe(skip(1)) + .subscribe(observer); + + mediaMatcher.setMatchesQuery(queryOne, false); + mediaMatcher.setMatchesQuery(queryTwo, false); + + tick(); + expect(observer).toHaveBeenCalledTimes(1); + })); + it('should not complete other subscribers when preceding subscriber completes', fakeAsync(() => { const queryOne = '(width: 700px)'; const queryTwo = '(width: 999px)'; - const breakpoint = breakpointObserver.observe([queryOne, queryTwo]); + const breakpoint = breakpointObserver.observe([queryOne, queryTwo]).pipe(skip(1)); const subscriptions: Subscription[] = []; let emittedValues: number[] = []; @@ -148,14 +166,14 @@ describe('BreakpointObserver', () => { mediaMatcher.setMatchesQuery(queryOne, true); mediaMatcher.setMatchesQuery(queryTwo, false); - flush(); + tick(); expect(emittedValues).toEqual([1, 2, 3, 4]); emittedValues = []; mediaMatcher.setMatchesQuery(queryOne, false); mediaMatcher.setMatchesQuery(queryTwo, true); - flush(); + tick(); expect(emittedValues).toEqual([1, 3, 4]); @@ -172,7 +190,11 @@ export class FakeMediaQueryList { /** Toggles the matches state and "emits" a change event. */ setMatches(matches: boolean) { this.matches = matches; - this._listeners.forEach(listener => listener(this as any)); + + /** Simulate an asynchronous task. */ + setTimeout(() => { + this._listeners.forEach(listener => listener(this as any)); + }); } /** Registers a callback method for change events. */ diff --git a/src/cdk/layout/breakpoints-observer.ts b/src/cdk/layout/breakpoints-observer.ts index 4555d41692b..de50cf00b8f 100644 --- a/src/cdk/layout/breakpoints-observer.ts +++ b/src/cdk/layout/breakpoints-observer.ts @@ -8,8 +8,8 @@ import {Injectable, NgZone, OnDestroy} from '@angular/core'; import {MediaMatcher} from './media-matcher'; -import {asapScheduler, combineLatest, Observable, Subject, Observer} from 'rxjs'; -import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators'; +import {combineLatest, concat, Observable, Subject, Observer} from 'rxjs'; +import {debounceTime, map, skip, startWith, take, takeUntil} from 'rxjs/operators'; import {coerceArray} from '@angular/cdk/coercion'; @@ -75,19 +75,22 @@ export class BreakpointObserver implements OnDestroy { const queries = splitQueries(coerceArray(value)); const observables = queries.map(query => this._registerQuery(query).observable); - return combineLatest(observables).pipe( - debounceTime(0, asapScheduler), - map((breakpointStates: InternalBreakpointState[]) => { - const response: BreakpointState = { - matches: false, - breakpoints: {}, - }; - breakpointStates.forEach((state: InternalBreakpointState) => { - response.matches = response.matches || state.matches; - response.breakpoints[state.query] = state.matches; - }); - return response; - })); + let stateObservable = combineLatest(observables); + // Emit the first state immediately, and then debounce the subsequent emissions. + stateObservable = concat( + stateObservable.pipe(take(1)), + stateObservable.pipe(skip(1), debounceTime(0))); + return stateObservable.pipe(map((breakpointStates: InternalBreakpointState[]) => { + const response: BreakpointState = { + matches: false, + breakpoints: {}, + }; + breakpointStates.forEach((state: InternalBreakpointState) => { + response.matches = response.matches || state.matches; + response.breakpoints[state.query] = state.matches; + }); + return response; + })); } /** Registers a specific query to be listened for. */