Comprehensive documentation of test concepts for Angular directives.
22 of 22 tests passing ✅
| Directive | Tests | Description |
|---|---|---|
| IconHoverDirective | 10 | HostListener (mouseenter/mouseleave), CSS classes |
| NoScrollDirective | 12 | HostListener (change), document.body manipulation |
Directives are Angular classes that add additional behavior to HTML elements:
Types:
- Attribute Directives - Change appearance/behavior (e.g., appIconHover)
- Structural Directives - Change DOM structure (e.g., *ngIf, *ngFor)
Our Directives:
// Attribute Directive - adds hover effect
<div appIconHover>Icon</div>
// Attribute Directive - blocks scrolling
<input type="checkbox" appNoScroll />@Component({
standalone: true,
imports: [DirectiveName],
template: `<div appDirective>Test</div>`,
})
class TestComponent {}Why standalone?
- Angular 21: Standalone components are the standard
- Simpler imports
- No NgModule needed
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestComponent], // ← imports, not declarations!
});
fixture = TestBed.createComponent(TestComponent);
element = fixture.debugElement.query(By.directive(DirectiveName));
fixture.detectChanges();
});Important concepts:
// ✅ Correct - finds element with directive
element = fixture.debugElement.query(By.directive(IconHoverDirective));
// ❌ Wrong - only finds CSS selector
element = fixture.debugElement.query(By.css('[appIconHover]'));Why By.directive()?
- Finds element with the directive
- Independent of selector
- Access to directive instance
const element: DebugElement = fixture.debugElement.query(...);
const nativeElement: HTMLElement = element.nativeElement;
// DebugElement - Angular wrapper (for events)
element.triggerEventHandler('click', null);
// nativeElement - Real DOM element (for checks)
expect(nativeElement.classList.contains('active')).toBe(true);- Component creation
- Directive initialization
- HostListener events
- CSS class manipulation
- Edge cases
describe('IconHoverDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestComponent],
});
fixture = TestBed.createComponent(TestComponent);
element = fixture.debugElement.query(By.directive(IconHoverDirective));
fixture.detectChanges();
});
it('should create directive', () => {
expect(element).toBeTruthy();
});
it('should add hover class on mouseenter', () => {
element.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
expect(element.nativeElement.classList.contains('icon-hover')).toBe(true);
});
it('should remove hover class on mouseleave', () => {
element.triggerEventHandler('mouseenter', null);
element.triggerEventHandler('mouseleave', null);
fixture.detectChanges();
expect(element.nativeElement.classList.contains('icon-hover')).toBe(false);
});
});The NoScrollDirective manipulates document.body, which requires special testing:
describe('NoScrollDirective', () => {
let originalOverflow: string;
beforeEach(() => {
originalOverflow = document.body.style.overflow;
});
afterEach(() => {
document.body.style.overflow = originalOverflow; // ← Important!
});
it('should block scroll when checkbox is checked', () => {
const checkbox = element.nativeElement as HTMLInputElement;
checkbox.checked = true;
element.triggerEventHandler('change', { target: checkbox });
expect(document.body.style.overflow).toBe('hidden');
});
it('should allow scroll when checkbox is unchecked', () => {
const checkbox = element.nativeElement as HTMLInputElement;
checkbox.checked = false;
element.triggerEventHandler('change', { target: checkbox });
expect(document.body.style.overflow).toBe('');
});
});Why afterEach restoration?
- Prevents conflicts between tests
- Keeps test environment clean
- Avoids side effects
Each test should be independent:
afterEach(() => {
// Reset state
document.body.style.overflow = '';
});// ✅ Correct
element.triggerEventHandler('mouseenter', null);
// ❌ Wrong (doesn't trigger Angular change detection)
element.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));it('should handle multiple rapid hover events', fakeAsync(() => {
element.triggerEventHandler('mouseenter', null);
element.triggerEventHandler('mouseleave', null);
element.triggerEventHandler('mouseenter', null);
tick();
expect(element.nativeElement.classList.contains('icon-hover')).toBe(true);
}));// ✅ Good
it('should add hover class on mouseenter', () => {});
// ❌ Bad
it('should work', () => {});Overall:
| Directive | Tests | Coverage |
|---|---|---|
| IconHoverDirective | 10/10 | 100% ✅ |
| NoScrollDirective | 12/12 | 100% ✅ |
| Total | 22 | 22/22 ✅ |
Tests: 22/22 ✅
Part of: 823 total tests in project
# All tests
npm test
# Only directive tests
npm test -- --include='**/directives/**/*.spec.ts'
# With coverage
npm test -- --no-watch --code-coverageLast Update: December 15, 2025