From frontend-angular
Angular testing with Jest and jest-preset-angular, Angular TestBed for component/service/pipe testing, DI mocking, signals testing, RxJS testing, Playwright E2E, and coverage requirements.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-angular:angular-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use Jest as the test runner with `jest-preset-angular` for Angular-aware transformations.
Use Jest as the test runner with jest-preset-angular for Angular-aware transformations.
npm install -D jest jest-preset-angular @types/jest ts-jest
import type { Config } from 'jest';
const config: Config = {
preset: 'jest-preset-angular',
setupFilesAfterFramework: ['<rootDir>/setup-jest.ts'],
testPathPattern: ['src/**/*.spec.ts'],
collectCoverage: true,
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 85,
functions: 85,
lines: 85,
statements: 85,
},
},
coveragePathIgnorePatterns: [
'/node_modules/',
'src/main.ts',
'src/environments/',
'.module.ts',
],
};
export default config;
import 'jest-preset-angular/setup-jest';
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jest"]
},
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}
TestBed creates an isolated Angular testing module for each test suite.
// user-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
import { User } from '../user.model';
describe('UserCardComponent', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
const mockUser: User = {
id: '1',
name: 'Jane Doe',
email: '[email protected]',
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent], // standalone component: import directly
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display user name', () => {
// Set input via componentRef
fixture.componentRef.setInput('user', mockUser);
fixture.detectChanges();
const nameEl = fixture.nativeElement.querySelector('[data-testid="user-name"]');
expect(nameEl.textContent).toContain('Jane Doe');
});
it('should emit editUser event when edit button is clicked', () => {
fixture.componentRef.setInput('user', mockUser);
fixture.detectChanges();
const emitSpy = jest.spyOn(component.editUser, 'emit');
const editBtn = fixture.nativeElement.querySelector('[data-testid="edit-btn"]');
editBtn.click();
expect(emitSpy).toHaveBeenCalledWith(mockUser);
});
});
| Method / Property | Purpose |
|---|---|
fixture.detectChanges() | Trigger change detection (required after state changes) |
fixture.nativeElement | Access the host DOM element |
fixture.debugElement | Angular's wrapper; use for querying, triggering events |
fixture.componentInstance | Reference to the component class |
fixture.componentRef.setInput('name', value) | Set signal inputs in tests |
fixture.whenStable() | Wait for async tasks to complete |
fixture.debugElement.query(By.css('.class')) | Query by CSS selector |
fixture.debugElement.queryAll(By.directive(MyDirective)) | Query by directive |
import { By } from '@angular/platform-browser';
const btn = fixture.debugElement.query(By.css('button[type="submit"]'));
btn.triggerEventHandler('click', new MouseEvent('click'));
fixture.detectChanges();
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from './user.model';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // ensure no unexpected requests
});
it('should fetch users', () => {
const mockUsers: User[] = [{ id: '1', name: 'Alice', email: '[email protected]' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should handle 500 error', () => {
service.getUsers().subscribe({
error: err => expect(err.status).toBe(500),
});
const req = httpMock.expectOne('/api/users');
req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
});
});
// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
let pipe: TruncatePipe;
beforeEach(() => {
pipe = new TruncatePipe(); // pure pipes can be tested without TestBed
});
it('should return the original string if within limit', () => {
expect(pipe.transform('Hello', 10)).toBe('Hello');
});
it('should truncate long strings', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
it('should use custom ellipsis', () => {
expect(pipe.transform('Hello World', 5, ' [more]')).toBe('Hello [more]');
});
});
describe('OrderComponent', () => {
let component: OrderComponent;
let fixture: ComponentFixture<OrderComponent>;
let orderService: jest.Mocked<OrderService>;
beforeEach(async () => {
const orderServiceMock = {
createOrder: jest.fn(),
getOrder: jest.fn().mockReturnValue(of(mockOrder)),
};
await TestBed.configureTestingModule({
imports: [OrderComponent],
providers: [
{ provide: OrderService, useValue: orderServiceMock },
],
}).compileComponents();
fixture = TestBed.createComponent(OrderComponent);
component = fixture.componentInstance;
orderService = TestBed.inject(OrderService) as jest.Mocked<OrderService>;
});
});
TestBed.overrideProvider(AuthService, {
useValue: { isAuthenticated: () => true, getToken: () => 'test-token' },
});
Signals are synchronous, so no async handling is needed for basic signal tests.
import { TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent (signals)', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should start at 0', () => {
expect(component.count()).toBe(0);
});
it('should increment on click', () => {
const btn = fixture.nativeElement.querySelector('[data-testid="increment"]');
btn.click();
expect(component.count()).toBe(1);
// Signal change → trigger Angular CD to reflect in template
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('p').textContent).toBe('1');
});
it('should compute doubled value', () => {
component.count.set(5);
expect(component.doubled()).toBe(10);
});
});
Use fakeAsync + tick to control time-based operators (debounceTime, delay, interval).
import { fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
it('should debounce search input', fakeAsync(() => {
const results: string[] = [];
const input$ = new Subject<string>();
input$.pipe(debounceTime(300)).subscribe(val => results.push(val));
input$.next('a');
input$.next('ab');
input$.next('abc');
expect(results).toHaveLength(0); // nothing emitted yet
tick(300); // advance virtual clock by 300ms
expect(results).toHaveLength(1);
expect(results[0]).toBe('abc');
}));
Use waitForAsync for Promise-based async.
it('should load data async', waitForAsync(() => {
service.getData().subscribe(data => {
expect(data).toBeDefined();
});
}));
Use TestScheduler for marble-based testing of complex observable pipelines.
import { TestScheduler } from 'rxjs/testing';
it('should retry on error', () => {
const scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
scheduler.run(({ cold, expectObservable }) => {
const source = cold('-a--#', { a: 'value' }, new Error('oops'));
const result = source.pipe(retry(1));
expectObservable(result).toBe('-a---a--#', { a: 'value' }, new Error('oops'));
});
});
Use Playwright for end-to-end testing.
npm install -D @playwright/test
npx playwright install
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
use: {
baseURL: 'http://localhost:4200',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
webServer: {
command: 'ng serve',
url: 'http://localhost:4200',
reuseExistingServer: !process.env['CI'],
},
});
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should display login form', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
});
test('should navigate to dashboard on success', async ({ page }) => {
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL('/dashboard');
});
test('should show error on invalid credentials', async ({ page }) => {
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByRole('alert')).toContainText('Invalid credentials');
});
});
*.spec.ts (co-located with source file)e2e/**/*.spec.tssrc/
app/
features/
users/
user-card.component.ts
user-card.component.spec.ts ← unit test
user.service.ts
user.service.spec.ts ← unit test
e2e/
users.spec.ts ← E2E test
Maintain >= 85% coverage across branches, functions, lines, and statements. Configure in jest.config.ts as shown above.
Exclude from coverage:
main.ts)Run coverage:
npx jest --coverage
Structure each .spec.ts file to cover positive, negative, edge, and security scenarios using Angular TestBed and Jest.
describe("LoginComponent", () => {
it("should submit form with valid credentials", () => {
const authService = TestBed.inject(AuthService) as jest.Mocked<AuthService>;
authService.login.mockReturnValue(of({ token: "abc" }));
component.email.set("[email protected]");
component.password.set("validPass123");
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type="submit"]').click();
expect(authService.login).toHaveBeenCalledWith("[email protected]", "validPass123");
});
it("should render email and password fields", () => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input[type="email"]')).toBeTruthy();
expect(fixture.nativeElement.querySelector('input[type="password"]')).toBeTruthy();
});
});
describe("LoginComponent", () => {
it("should display error when login fails", fakeAsync(() => {
const authService = TestBed.inject(AuthService) as jest.Mocked<AuthService>;
authService.login.mockReturnValue(throwError(() => new Error("Invalid credentials")));
component.email.set("[email protected]");
component.password.set("wrong");
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type="submit"]').click();
tick();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector(".error-message").textContent)
.toContain("Invalid credentials");
}));
it("should show validation error for empty email", () => {
component.email.set("");
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type="submit"]').click();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector(".email-error")).toBeTruthy();
});
});
describe("LoginComponent", () => {
it("should handle max-length email input (254 chars)", () => {
const longEmail = "a".repeat(245) + "@test.com";
component.email.set(longEmail);
fixture.detectChanges();
const emailInput = fixture.nativeElement.querySelector('input[type="email"]');
expect(emailInput.value).toBe(longEmail);
});
it("should disable submit button while request is pending", fakeAsync(() => {
const authService = TestBed.inject(AuthService) as jest.Mocked<AuthService>;
authService.login.mockReturnValue(new Observable());
component.email.set("[email protected]");
component.password.set("pass123");
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type="submit"]').click();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button[type="submit"]').disabled).toBe(true);
}));
});
describe("LoginComponent — security", () => {
it("should not render XSS payload in error message", fakeAsync(() => {
const authService = TestBed.inject(AuthService) as jest.Mocked<AuthService>;
authService.login.mockReturnValue(
throwError(() => new Error('<img src=x onerror=alert(1)>'))
);
component.email.set("[email protected]");
component.password.set("pass");
fixture.detectChanges();
fixture.nativeElement.querySelector('button[type="submit"]').click();
tick();
fixture.detectChanges();
const errorEl = fixture.nativeElement.querySelector(".error-message");
expect(errorEl.innerHTML).not.toContain("<img");
expect(errorEl.innerHTML).not.toContain("onerror");
}));
it("should mask password field input", () => {
fixture.detectChanges();
const passwordInput = fixture.nativeElement.querySelector('input[type="password"]');
expect(passwordInput).toBeTruthy();
expect(passwordInput.type).toBe("password");
});
});
describe("AuthGuard — security", () => {
it("should redirect unauthenticated users to /login", () => {
const authService = TestBed.inject(AuthService) as jest.Mocked<AuthService>;
authService.isAuthenticated.mockReturnValue(false);
const router = TestBed.inject(Router);
const navigateSpy = jest.spyOn(router, "navigate");
const guard = TestBed.inject(AuthGuard);
const result = guard.canActivate();
expect(result).toBe(false);
expect(navigateSpy).toHaveBeenCalledWith(["/login"]);
});
});
npx claudepluginhub gagandeepp/software-agent-teams --plugin frontend-angularGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.