Signal-driven effects with setTimeout-based debouncing are notoriously hard to test because the effect's cleanup function must actually cancel the stale timer, not just let it run and get overwritten. This test uses fakeAsync and tick to deterministically control virtual time: it advances 100ms (before the debounce fires), changes the query again, then advances the remaining 300ms, and asserts only the final query's results survive — proving the onCleanup callback truly cleared the first setTimeout rather than merely racing it.
import { Component, signal, effect, computed } from '@angular/core';import { TestBed, fakeAsync, tick } from '@angular/core/testing';@Component({ selector: 'app-debounced-search', template: '' })class DebouncedSearchComponent {readonly query = signal('');readonly results = signal<string[]>([]);private searchCallCount = 0;constructor() {effect((onCleanup) => {const term = this.query();const handle = setTimeout(() => {this.searchCallCount++;this.results.set(term ? [`${term}-1`, `${term}-2`] : []);}, 300);onCleanup(() => clearTimeout(handle));});}}describe('DebouncedSearchComponent', () => {it('cancels the pending search when the query changes before the debounce fires', fakeAsync(() => {const fixture = TestBed.createComponent(DebouncedSearchComponent);const component = fixture.componentInstance;fixture.detectChanges();component.query.set('ang');tick(100);component.query.set('angular');tick(300);fixture.detectChanges();expect(component.results()).toEqual(['angular-1', 'angular-2']);}));});