如何使用 Jasmine 在 Angular 单元测试中模拟 window.screen.width
How to mock window.screen.width in Angular Unit Test with Jasmine
我有一个 BreakpointService,它告诉我 - 根据屏幕宽度 - 我应该在哪个 SidebarMode(关闭 - 缩小 - 打开)中显示我的边栏。
这是服务的主要部分:
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$);
}
通过这条线我可以订阅事件:
this.breakpointService.observe().subscribe();
现在,我想在单元测试中测试不同的模式,但我不知道
如何在测试中模拟 window.screen.width 属性
我尝试了几种方法,但没有任何效果。
到目前为止,这是我的测试设置:
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);
});
});
嘲讽Angular MaterialBreakpointObserver
我猜你并不是真的想模拟 window.screen,你实际上是想模拟 BreakpointObserver
。毕竟,无需测试他们的代码,您只想测试您的代码是否能正确响应 BreakpointObserver.observe()
返回的具有不同屏幕尺寸的 observable。
有很多不同的方法可以做到这一点。为了说明一种方法,我将 STACKBLITZ 与您的代码放在一起,展示了我将如何处理这个问题。与上面的代码不同的注意事项:
- 您的代码在构造函数中设置了可观察对象。因此,必须在实例化服务之前更改模拟,因此您会看到对
resize()
的调用发生在 service = TestBed.get(MyService);
调用之前。
- 我用 spyObj 嘲笑了
BreakpointObserver
,并称之为假货
函数代替 BreakpointObserver.observe()
方法。这个
假函数使用我设置的过滤器和我想要的结果
从各种比赛中。它们一开始都是假的,因为
值会根据需要的屏幕尺寸而改变
嘲笑,这是由您使用的 resize()
函数设置的
在上面的代码中。
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.
这是来自 StackBlitz 的新建议 describe
函数的片段:
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));
});
});
});
我猜 BreakPointObserver 会监听 resize 事件,所以也许你可以尝试用 jasmine 模拟 window.innerWidth / window.outerWidth 之类的东西?
spyOnProperty(window, 'innerWidth').and.returnValue(760);
然后你手动触发一个resize事件:
window.dispatchEvent(new Event('resize'));
看起来像这样:
it('should mock window inner width', () => {
spyOnProperty(window, 'innerWidth').and.returnValue(760);
window.dispatchEvent(new Event('resize'));
});
如果您查看 BreakpointObserver 的测试,您会得到答案。您不需要模拟 BreakpointObserver,您需要模拟注入其中的 MediaMatcher。这是我的一项测试。
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();
});
});
});
我有一个 BreakpointService,它告诉我 - 根据屏幕宽度 - 我应该在哪个 SidebarMode(关闭 - 缩小 - 打开)中显示我的边栏。
这是服务的主要部分:
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$);
}
通过这条线我可以订阅事件:
this.breakpointService.observe().subscribe();
现在,我想在单元测试中测试不同的模式,但我不知道
如何在测试中模拟 window.screen.width 属性
我尝试了几种方法,但没有任何效果。
到目前为止,这是我的测试设置:
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);
});
});
嘲讽Angular MaterialBreakpointObserver
我猜你并不是真的想模拟 window.screen,你实际上是想模拟 BreakpointObserver
。毕竟,无需测试他们的代码,您只想测试您的代码是否能正确响应 BreakpointObserver.observe()
返回的具有不同屏幕尺寸的 observable。
有很多不同的方法可以做到这一点。为了说明一种方法,我将 STACKBLITZ 与您的代码放在一起,展示了我将如何处理这个问题。与上面的代码不同的注意事项:
- 您的代码在构造函数中设置了可观察对象。因此,必须在实例化服务之前更改模拟,因此您会看到对
resize()
的调用发生在service = TestBed.get(MyService);
调用之前。 - 我用 spyObj 嘲笑了
BreakpointObserver
,并称之为假货 函数代替BreakpointObserver.observe()
方法。这个 假函数使用我设置的过滤器和我想要的结果 从各种比赛中。它们一开始都是假的,因为 值会根据需要的屏幕尺寸而改变 嘲笑,这是由您使用的resize()
函数设置的 在上面的代码中。
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.
这是来自 StackBlitz 的新建议 describe
函数的片段:
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));
});
});
});
我猜 BreakPointObserver 会监听 resize 事件,所以也许你可以尝试用 jasmine 模拟 window.innerWidth / window.outerWidth 之类的东西?
spyOnProperty(window, 'innerWidth').and.returnValue(760);
然后你手动触发一个resize事件:
window.dispatchEvent(new Event('resize'));
看起来像这样:
it('should mock window inner width', () => {
spyOnProperty(window, 'innerWidth').and.returnValue(760);
window.dispatchEvent(new Event('resize'));
});
如果您查看 BreakpointObserver 的测试,您会得到答案。您不需要模拟 BreakpointObserver,您需要模拟注入其中的 MediaMatcher。这是我的一项测试。
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();
});
});
});