我正在尝试根据 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都不会触发
组件的构造函数在调用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();
});
});