I have a BreakpointService, which tells me - depending on the screen width - in which SidebarMode (closed - minified - open) I should display my Sidebar.
This is the main part of the service:
constructor(private breakpointObserver: BreakpointObserver) {
this.closed$ = this.breakpointObserver.observe(['(min-width: 1024px)']).pipe(
filter((state: BreakpointState) => !state.matches),
mapTo(SidebarMode.Closed)
);
this.opened$ = this.breakpointObserver.observe(['(min-width: 1366px)']).pipe(
filter((state: BreakpointState) => state.matches),
mapTo(SidebarMode.Open)
);
const minifiedStart$: Observable<boolean> = this.breakpointObserver.observe(['(min-width: 1024px)']).pipe(map(state => state.matches));
const minifiedEnd$: Observable<boolean> = this.breakpointObserver.observe(['(max-width: 1366px)']).pipe(map(state => state.matches));
this.minified$ = minifiedStart$.pipe(
flatMap(start => minifiedEnd$.pipe(map(end => start && end))),
distinctUntilChanged(),
filter(val => val === true),
mapTo(SidebarMode.Minified)
);
this.observer$ = merge(this.closed$, this.minified$, this.opened$);
}
with this line I can subscribe to the events:
this.breakpointService.observe().subscribe();
Now, I would like to test the different modes within a unit test, but I don't know
how to mock the window.screen.width property within a test
I tried several things, but nothing worked out for me.
This is my test-setup so far:
describe('observe()', () => {
function resize(width: number): void {
// did not work
// window.resizeTo(width, window.innerHeight);
// (<any>window).screen = { width: 700 };
// spyOn(window, 'screen').and.returnValue(...)
}
let currentMode;
beforeAll(() => {
service.observe().subscribe(mode => (currentMode = mode));
});
it('should return Observable<SidebarMode>', async () => {
resize(1000);
expect(Object.values(SidebarMode).includes(SidebarMode[currentMode])).toBeTruthy();
});
xit('should return SidebarMode.Closed', async () => {
resize(600);
expect(currentMode).toBe(SidebarMode.Closed);
});
xit('should return SidebarMode.Minified', async () => {
resize(1200);
expect(currentMode).toBe(SidebarMode.Minified);
});
xit('should return SidebarMode.Open', async () => {
resize(2000);
expect(currentMode).toBe(SidebarMode.Open);
});
});
I'm guessing the BreakPointObserver listen to the resize event so maybe you could try something like mocking the window.innerWidth / window.outerWidth with jasmine ?
spyOnProperty(window, 'innerWidth').and.returnValue(760);
Then you manually trigger a resize event :
window.dispatchEvent(new Event('resize'));
It would look like that :
it('should mock window inner width', () => {
spyOnProperty(window, 'innerWidth').and.returnValue(760);
window.dispatchEvent(new Event('resize'));
});
BreakpointObserver
I'm guessing you don't really want to mock window.screen, you actually want to mock BreakpointObserver
. After all, no need to test their code, you just want to test that your code responds properly to the observable returned by BreakpointObserver.observe()
with different screen sizes.
There are a lot of different ways to do this. To illustrate one method, I put together a STACKBLITZ with your code showing how I would approach this. Things to note that differ from what your code is above:
resize()
happens before the service = TestBed.get(MyService);
call.BreakpointObserver
with a spyObj, and called a fake
function in place of the BreakpointObserver.observe()
method. This
fake function uses a filter I had set up with the results I wanted
from the various matches. They all started as false, because the
values would change depending on what screen size is desired to be
mocked, and that is set up by the resize()
function you were using
in the code above.Note: there are certainly other ways to approach this. Check out the angular material's own
breakpoints-observer.spec.ts
on github. This is a much nicer general approach than what I outline here, which was just to test the function you provided.
Here is a snip from the StackBlitz of the new suggested describe
function:
describe(MyService.name, () => {
let service: MyService;
const matchObj = [
// initially all are false
{ matchStr: '(min-width: 1024px)', result: false },
{ matchStr: '(min-width: 1366px)', result: false },
{ matchStr: '(max-width: 1366px)', result: false },
];
const fakeObserve = (s: string[]): Observable<BreakpointState> =>
from(matchObj).pipe(
filter(match => match.matchStr === s[0]),
map(match => ({ matches: match.result, breakpoints: {} })),
);
const bpSpy = jasmine.createSpyObj('BreakpointObserver', ['observe']);
bpSpy.observe.and.callFake(fakeObserve);
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [MyService, { provide: BreakpointObserver, useValue: bpSpy }],
});
});
it('should be createable', () => {
service = TestBed.inject(MyService);
expect(service).toBeTruthy();
});
describe('observe()', () => {
function resize(width: number): void {
matchObj[0].result = width >= 1024;
matchObj[1].result = width >= 1366;
matchObj[2].result = width <= 1366;
}
it('should return Observable<SidebarMode>', () => {
resize(1000);
service = TestBed.inject(MyService);
service.observe().subscribe(mode => {
expect(
Object.values(SidebarMode).includes(SidebarMode[mode]),
).toBeTruthy();
});
});
it('should return SidebarMode.Closed', () => {
resize(600);
service = TestBed.inject(MyService);
service
.observe()
.subscribe(mode => expect(mode).toBe(SidebarMode.Closed));
});
it('should return SidebarMode.Minified', () => {
resize(1200);
service = TestBed.inject(MyService);
service
.observe()
.subscribe(mode => expect(mode).toBe(SidebarMode.Minified));
});
it('should return SidebarMode.Open', () => {
resize(2000);
service = TestBed.inject(MyService);
service.observe().subscribe(mode => expect(mode).toBe(SidebarMode.Open));
});
});
});
If you look at the tests for BreakpointObserver you will get you answer. You don't need to mock BreakpointObserver you need to mock the MediaMatcher that is injected into it. This is from one of my tests.
let mediaMatcher: FakeMediaMatcher;
class FakeMediaQueryList {
/** The callback for change events. */
private listeners: ((mql: MediaQueryListEvent) => void)[] = [];
constructor(public matches: boolean, public media: string) {}
/** Toggles the matches state and "emits" a change event. */
setMatches(matches: boolean): void {
this.matches = matches;
/** Simulate an asynchronous task. */
setTimeout(() => {
// tslint:disable-next-line: no-any
this.listeners.forEach((listener) => listener(this as any));
});
}
/** Registers a callback method for change events. */
addListener(callback: (mql: MediaQueryListEvent) => void): void {
this.listeners.push(callback);
}
/** Removes a callback method from the change events. */
removeListener(callback: (mql: MediaQueryListEvent) => void): void {
const index = this.listeners.indexOf(callback);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
}
@Injectable()
class FakeMediaMatcher {
/** A map of match media queries. */
private queries = new Map<string, FakeMediaQueryList>();
/** The number of distinct queries created in the media matcher during a test. */
get queryCount(): number {
return this.queries.size;
}
/** Fakes the match media response to be controlled in tests. */
matchMedia(query: string): FakeMediaQueryList {
const mql = new FakeMediaQueryList(true, query);
this.queries.set(query, mql);
return mql;
}
/** Clears all queries from the map of queries. */
clear(): void {
this.queries.clear();
}
/** Toggles the matching state of the provided query. */
setMatchesQuery(query: string, matches: boolean): void {
const mediaListQuery = this.queries.get(query);
if (mediaListQuery) {
mediaListQuery.setMatches(matches);
} else {
throw Error('This query is not being observed.');
}
}
}
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{ provide: MediaMatcher, useClass: FakeMediaMatcher },
],
});
});
beforeEach(inject([MediaMatcher], (mm: FakeMediaMatcher) => {
mediaMatcher = mm;
}));
afterEach(() => {
mediaMatcher.clear();
});
describe('get isSideNavClosable$', () => {
beforeEach(() => {
// (Andrew Alderson Jan 1, 2020) need to do this to register the query
component.isSideNavClosable$.subscribe();
});
it('should emit false when the media query does not match', (done) => {
mediaMatcher.setMatchesQuery('(max-width: 1280px)', false);
component.isSideNavClosable$.subscribe((closeable) => {
expect(closeable).toBeFalsy();
done();
});
});
it('should emit true when the media query does match', (done) => {
mediaMatcher.setMatchesQuery('(max-width: 1280px)', true);
component.isSideNavClosable$.subscribe((closeable) => {
expect(closeable).toBeTruthy();
done();
});
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With