【问题标题】:How to mock window.screen.width in Angular Unit Test with Jasmine如何使用 Jasmine 在 Angular 单元测试中模拟 window.screen.width
【发布时间】:2018-11-08 09:32:50
【问题描述】:

我有一个 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 unit-testing jasmine karma-jasmine chromium


    【解决方案1】:

    我猜 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'));
        });
    

    【讨论】:

      【解决方案2】:

      模拟 Angular 材质 BreakpointObserver

      我猜你并不是真的想模拟 window.screen,你实际上是想模拟 BreakpointObserver。毕竟,无需测试他们的代码,您只需测试您的代码是否正确响应 BreakpointObserver.observe() 返回的不同屏幕尺寸的 observable。

      有很多不同的方法可以做到这一点。为了说明一种方法,我将STACKBLITZ 与您的代码放在一起,展示了我将如何处理这个问题。与您上面的代码不同的注意事项:

      • 您的代码在构造函数中设置了可观察对象。因此,必须在实例化服务之前更改模拟,因此您会看到对 resize() 的调用发生在 service = TestBed.get(MyService); 调用之前。
      • 我用 spyObj 嘲笑了BreakpointObserver,并称其为假 代替BreakpointObserver.observe() 方法的函数。这 假函数使用我设置的过滤器,得到我想要的结果 从各种比赛中。他们一开始都是假的,因为 值会根据所需的屏幕尺寸而改变 模拟,这是由您使用的 resize() 函数设置的 在上面的代码中。

      注意:当然还有其他方法可以解决这个问题。查看角材料自己的breakpoints-observer.spec.tson github。这是一种比我在这里概述的更好的通用方法,它只是为了测试您提供的功能。

      这是 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));
          });
        });
      });
      

      【讨论】:

      • 对我来说效果很好。我没有想到 callFake 方法。相反,我未能尝试模拟 BreakpointObserver 的依赖项
      【解决方案3】:

      如果您查看 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();
                      });
                  });
              });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多