使用 Jasmin 和 Karma 进行角度信号测试

问题描述 投票:0回答:1

我正在尝试根据 getScrollTop() 上的间谍操作来测试“showToolbar”的值是 true 还是 false。

我的代码:

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [
    MatToolbar,
    MatIcon,
    MatIconButton,
    NgOptimizedImage,
    MatAnchor,
    MatIconAnchor,
    RouterLink
  ],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
  animations: [
    trigger('toolbarAnimation', [
      state('open', style({ transform: 'translateY(0px)'})),
      state('close', style({ transform: 'translateY(-100px)'})),
      transition('open => close',[animate('300ms ease-out')]),
      transition('close  => open',[animate('300ms ease-in')]),
    ]),
  ],
})

export class HeaderComponent {

  protected readonly sidenavOpeningService :SidenavOpeningService = inject(SidenavOpeningService);
 
  showToolbar: Signal<boolean>;
  private readonly topLimitShowToolbar = 349;

  constructor(private scrollDispatcher: ScrollDispatcher, private viewportRuler: ViewportRuler) {
    const scrollLimit = toSignal(this.scrollLimit(scrollDispatcher,viewportRuler,this.topLimitShowToolbar),{ initialValue: true }) ;
    const directionScroll = toSignal(this.directionScroll(scrollDispatcher,viewportRuler),{ initialValue: true });
    
    this.showToolbar = computed(() => directionScroll() || scrollLimit());
    
    effect(() => this.showToolbar());
  }

  getScrollTop(scrollDispatcher:ScrollDispatcher,viewportRuler:ViewportRuler): Observable<number> {
    return scrollDispatcher.scrolled().pipe(
      map(() => viewportRuler.getViewportScrollPosition().top),
      tap((_getScrollTop) => console.log('getScrollTop',_getScrollTop))
    );
  }

  scrollLimit(scrollDispatcher:ScrollDispatcher, viewportRuler:ViewportRuler, limit:number): Observable<boolean> {
    return this.getScrollTop(scrollDispatcher,viewportRuler).pipe(

      map((top) => top < limit),
      tap((_scrollLimit) => console.log('scrollLimit',_scrollLimit))
    );
  }

  directionScroll(scrollDispatcher:ScrollDispatcher, viewportRuler:ViewportRuler): Observable<boolean> {
    return this.getScrollTop(scrollDispatcher,viewportRuler).pipe(
      scan((acc: number[], current: number) => [acc[1], current], [viewportRuler.getViewportScrollPosition().top, 0]),
      skip(1), // On a besoin d'au moins deux valeurs pour savoir si le défilement monte ou descent
      map(([prev, current]) => prev >= current),
      tap((_directionScroll) => console.log('directionScroll',_directionScroll))
    );
  }
}

describe('HeaderComponent', () => {
  let component: HeaderComponent;
  let fixture: ComponentFixture<HeaderComponent>;
  let scrollDispatcher: ScrollDispatcher;
  let viewportRuler: ViewportRuler;
  let sidenavOpeningService: SidenavOpeningService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [BrowserAnimationsModule,HeaderComponent,MatIconTestingModule],
      providers: [
        { provide: ActivatedRoute, useValue: {params: of([{id: 1}]),},},
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();

    // Mock services
    scrollDispatcher = TestBed.inject(ScrollDispatcher);
    viewportRuler = TestBed.inject(ViewportRuler);
    sidenavOpeningService = TestBed.inject(SidenavOpeningService);
  });
  it('showToolbar should emit ture when scrolling down or up before the topLimit ', async() => {

    const topLimitShowToolbar = component['topLimitShowToolbar'];
    // Simuler la position de défilement et le comportement attendu
    spyOn(component, 'getScrollTop').and.returnValues(of(topLimitShowToolbar-100),of(topLimitShowToolbar+1), of(topLimitShowToolbar-50));
    fixture.detectChanges();
    expect(component.showToolbar()).toBeTrue();
  });
});

我希望“returnValues()”触发整个编程链直到compute(),但这永远不会发生,scollLimit和directionScroll函数的console.log都不会触发

angular rxjs angular-material jasmine signals
1个回答
0
投票

组件的构造函数在调用spyOn函数之前运行。 因此您无法提供所需的模拟值。 尝试将构造函数逻辑移至 ngOnInit 生命周期方法中。

import {
  trigger,
  state,
  style,
  transition,
  animate,
} from '@angular/animations';
import { NgOptimizedImage } from '@angular/common';
import {
  Component,
  DestroyRef,
  OnInit,
  Signal,
  computed,
  effect,
  signal,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { Observable, map, tap, scan, skip, combineLatest } from 'rxjs';

import { MatToolbar } from '@angular/material/toolbar';
import { MatIcon } from '@angular/material/icon';
import {
  MatAnchor,
  MatIconAnchor,
  MatIconButton,
} from '@angular/material/button';
import { ScrollDispatcher, ViewportRuler } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [
    MatToolbar,
    MatIcon,
    MatIconButton,
    NgOptimizedImage,
    MatAnchor,
    MatIconAnchor,
    RouterLink,
  ],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss',
  animations: [
    trigger('toolbarAnimation', [
      state('open', style({ transform: 'translateY(0px)' })),
      state('close', style({ transform: 'translateY(-100px)' })),
      transition('open => close', [animate('300ms ease-out')]),
      transition('close  => open', [animate('300ms ease-in')]),
    ]),
  ],
})
export class HeaderComponent implements OnInit {
  private readonly topLimitShowToolbar = 349;

  scrollLimitSig = signal(true);
  directionScrollSig = signal(true);

  showToolbar = computed(
    () => this.directionScrollSig() || this.scrollLimitSig(),
  );

  constructor(
    private scrollDispatcher: ScrollDispatcher,
    private viewportRuler: ViewportRuler,
    private destroyRef: DestroyRef,
  ) {}

  ngOnInit() {
    combineLatest([
      this.scrollLimit(
        this.scrollDispatcher,
        this.viewportRuler,
        this.topLimitShowToolbar,
      ),
      this.directionScroll(this.scrollDispatcher, this.viewportRuler),
    ])
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(([scrollLimit, directionScroll]) => {
        this.scrollLimitSig.set(scrollLimit);
        this.directionScrollSig.set(directionScroll);
      });
  }

  getScrollTop(
    scrollDispatcher: ScrollDispatcher,
    viewportRuler: ViewportRuler,
  ): Observable<number> {
    return scrollDispatcher.scrolled().pipe(
      map(() => viewportRuler.getViewportScrollPosition().top),
      tap((_getScrollTop) => console.log('getScrollTop', _getScrollTop)),
    );
  }

  scrollLimit(
    scrollDispatcher: ScrollDispatcher,
    viewportRuler: ViewportRuler,
    limit: number,
  ): Observable<boolean> {
    return this.getScrollTop(scrollDispatcher, viewportRuler).pipe(
      map((top) => top < limit),
      tap((_scrollLimit) => console.log('scrollLimit', _scrollLimit)),
    );
  }

  directionScroll(
    scrollDispatcher: ScrollDispatcher,
    viewportRuler: ViewportRuler,
  ): Observable<boolean> {
    return this.getScrollTop(scrollDispatcher, viewportRuler).pipe(
      scan(
        (acc: number[], current: number) => [acc[1], current],
        [viewportRuler.getViewportScrollPosition().top, 0],
      ),
      skip(1), // On a besoin d'au moins deux valeurs pour savoir si le défilement monte ou descent
      map(([prev, current]) => prev >= current),
      tap((_directionScroll) =>
        console.log('directionScroll', _directionScroll),
      ),
    );
  }
}

之后,请务必从规范文件的 beforeEach 部分中删除fixture.detectChanges() 调用,因为它还会在spyOn 调用之前调用 ngOnInit 生命周期挂钩。

将规格更改为如下所示:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs';
import { HeaderComponent } from './header.component';
import { MatIconTestingModule } from '@angular/material/icon/testing';

describe('HeaderComponent', () => {
  let component: HeaderComponent;
  let fixture: ComponentFixture<HeaderComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [BrowserAnimationsModule, HeaderComponent, MatIconTestingModule],
      providers: [
        { provide: ActivatedRoute, useValue: { params: of([{ id: 1 }]) } },
      ],
    });

    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance;
  });

  it('showToolbar should emit ture when scrolling down or up before the topLimit ', async () => {
    const topLimitShowToolbar = component['topLimitShowToolbar'];
    // Simuler la position de défilement et le comportement attendu
    const getScrollTopSpy = spyOn(component, 'getScrollTop').and.returnValues(
      of(topLimitShowToolbar - 100),
      of(topLimitShowToolbar + 1),
      of(topLimitShowToolbar - 50),
    );

    fixture.detectChanges();

    expect(component.showToolbar()).toBeTrue();
    expect(getScrollTopSpy).toHaveBeenCalled();
  });
});
© www.soinside.com 2019 - 2024. All rights reserved.