This community package renders Angular components in Vitest Browser Mode.
import { Component, input } from '@angular/core';
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-angular';
@Component({
selector: 'app-hello-world',
template: '<h1>Hello, {{ name() }}!</h1>',
})
export class HelloWorld {
name = input.required<string>();
}
test('renders name', async () => {
const { locator } = await render(HelloWorld, {
inputs: {
name: 'World',
},
});
await expect.element(locator).toHaveTextContent('Hello, World!');
});There are currently two ways to set up Vitest for Angular:
- Analog's
vitest-angularplugin (community). - Angular CLI's
unit-testbuilder (official).
While Angular CLI's unit-test builder is the official way to set up Vitest for Angular, it has some limitations. Analog's vitest-angular plugin provides more Vitest features and greater flexibility.
- Set up Vitest
npm add -D @analogjs/platform vitest-browser-angular
ng g @analogjs/platform:setup-vitest- Activate browser mode in the generated Vitest configuration by following the browser mode configuration instructions.
- Configure your Angular project to use the
@angular/build:unit-testbuilder, and add the browsers of your choice.
{
...,
"projects": {
"my-app": {
...,
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"browsers": ["Chromium", "Firefox", "Webkit"]
}
}
}
}
}
}Since Angular v21, Vitest is the default runner so you don't need to set the runner option.
- Install the browser provider of your choice using
ng add
# With Playwright
ng add @vitest/browser-playwright
# or with WebdriverIO
ng add @vitest/browser-webdriverio- Add the
vitest-browser-angularpackage to your project.
npm add -D vitest-browser-angularAngular CLI will automatically set up the test environment for you depending on the presence of zone.js in your project's polyfills.
When using the Analog plugin, you can control the behavior using the zoneless option of setupTestBed() in test-setup.ts:
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
setupTestBed({
zoneless: true,
});For detailed setup instructions for both Zone.js and Zoneless configurations, please refer to the Analog Vitest documentation.
To preview, debug and interact with a component in the browser after the test, you can prevent Angular from destroying it.
In Angular CLI, enable this using the --debug option.
With the Analog plugin, enable this using the browserMode option of setupTestBed() in test-setup.ts:
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
setupTestBed({
browserMode: true,
});The render function supports two query patterns:
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';
@Component({
template: ` <h1>Welcome</h1> `,
})
export class MyComponent {}
test('query elements', async () => {
// Pattern 1: Use locator to query within the component element
const { locator } = await render(MyComponent);
await expect.element(locator.getByText('Welcome')).toBeVisible();
// Pattern 2: Use screen to query from document.body (useful for portals/overlays)
const screen = await render(MyComponent);
await expect.element(screen.getByText('Welcome')).toBeVisible();
await expect.element(screen.getByText('Some Popover Content')).toBeVisible();
});Both locator and screen provide the following query methods:
getByRole- Locate by ARIA role and accessible namegetByText- Locate by text contentgetByLabelText- Locate by associated label textgetByPlaceholder- Locate by placeholder textgetByAltText- Locate by alt text (images)getByTitle- Locate by title attributegetByTestId- Locate by data-testid attribute
When to use which pattern:
locator: (full name: "Component Locator") - queries are scoped to the component's host element. Best for most component tests.screen: Queries start frombaseElement(defaults todocument.body). Use when testing components that render content outside their host element (modals, tooltips, portals).
Access the component's host element directly via container (shortcut for fixture.nativeElement):
const { container, locator } = await render(MyComponent);
expect(container).toBe(locator.element());Customize the root element for screen queries (useful for portal/overlay testing):
const customContainer = document.querySelector('#modal-root');
const screen = await render(ModalComponent, {
baseElement: customContainer,
});
// screen queries now start from customContainer instead of document.bodyPass input values to components using the inputs option:
import { Component, input } from '@angular/core';
@Component({
template: '<h2>{{ name() }}</h2><p>Price: ${{ price() }}</p>',
standalone: true,
})
export class ProductComponent {
name = input('Unknown Product');
price = input(0);
}
test('render with inputs', async () => {
const screen = await render(ProductComponent, {
inputs: {
name: 'Laptop',
price: 1299.99,
},
});
await expect.element(screen.getByText('Laptop')).toBeVisible();
await expect.element(screen.getByText(/Price: \$1299\.99/)).toBeVisible();
});Works with both signal-based inputs (input()) and @Input() decorators.
Enable routing with withRouting: true for components that use routing features but don't require specific route configuration:
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
template: `
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>
`,
imports: [RouterLink, RouterOutlet],
})
export class RoutedComponent {}
test('render with simple routing', async () => {
const screen = await render(RoutedComponent, {
withRouting: true,
});
await expect.element(screen.getByText('Home')).toBeVisible();
await expect.element(screen.getByText('About')).toBeVisible();
});Configure specific routes and optionally set an initial route:
import { test, expect } from 'vitest';
import { render } from 'vitest-browser-angular';
import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterOutlet, Routes } from '@angular/router';
@Component({
template: '<h1>Home Page</h1>',
})
export class HomeComponent {}
@Component({
template: '<h1>About Page</h1>',
standalone: true,
})
export class AboutComponent {}
@Component({
template: `
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>
`,
imports: [RouterLink, RouterOutlet],
standalone: true,
})
export class AppComponent {
router = inject(Router);
}
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
];
test('render with route configuration', async () => {
const { locator, routerHarness, router } = await render(AppComponent, {
withRouting: {
routes,
initialRoute: '/home',
},
});
await expect.element(locator).toHaveTextContent('Home Page');
// Navigate programmatically (prefer routerHarness over router)
await routerHarness.navigateByUrl('/about');
await expect.element(locator).toHaveTextContent('About Page');
// Use router to inspect state
expect(router.url).toBe('/about');
});When rendering a routed component, componentClassInstance provides access to the actual component instance with full routing context:
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Routes } from '@angular/router';
@Component({
template: '<h1>User: {{ userId }}</h1>',
})
export class UserComponent {
private route = inject(ActivatedRoute);
userId = this.route.snapshot.params['id'];
}
test('access route params', async () => {
const routes: Routes = [{ path: 'user/:id', component: UserComponent }];
const { componentClassInstance } = await render(UserComponent, {
withRouting: {
routes,
initialRoute: '/user/42',
},
});
expect(componentClassInstance.userId).toBe('42');
});By default, withComponentInputBinding() is enabled, which automatically binds route data, route params, and query params to matching component inputs. This works with both signal inputs (input()) and @Input() decorators:
import { Component, input } from '@angular/core';
import { Routes } from '@angular/router';
@Component({
template: `
<h2>{{ name() }}</h2>
<p>Age: {{ age() }}</p>
<p>Role: {{ role() }}</p>
`,
})
export class ProfileComponent {
name = input('Guest');
age = input(0);
role = input('user');
}
test('pass inputs via route data', async () => {
const routes: Routes = [
{
path: 'profile',
component: ProfileComponent,
data: {
name: 'Jane Doe',
age: 30,
role: 'admin',
},
},
];
const { locator, componentClassInstance } = await render(ProfileComponent, {
withRouting: {
routes,
initialRoute: '/profile',
},
});
// Inputs are automatically bound from route data
expect(componentClassInstance.name()).toBe('Jane Doe');
expect(componentClassInstance.age()).toBe(30);
expect(componentClassInstance.role()).toBe('admin');
await expect.element(locator.getByText('Jane Doe')).toBeVisible();
});If you need to manually handle route data via ActivatedRoute instead of automatic input binding, use disableInputBinding:
test('disable automatic input binding', async () => {
const routes: Routes = [
{
path: 'profile',
component: ProfileComponent,
data: { name: 'Jane Doe' },
},
];
const { componentClassInstance } = await render(ProfileComponent, {
withRouting: {
routes,
initialRoute: '/profile',
disableInputBinding: true, // Inputs will NOT be bound from route data
},
});
// Inputs retain their default values
expect(componentClassInstance.name()).toBe('Guest');
});If you need to add or override component providers, you can use the componentProviders option.
@Component({
template: '<h1>{{ title }}</h1>',
providers: [GreetingService],
})
export class HelloWorldComponent {
title = 'Hello World';
}
test('renders component with service provider', async () => {
const screen = await render(ServiceConsumerComponent, {
componentProviders: [
{ provide: GreetingService, useClass: FakeGreetingService },
],
});
await expect.element(screen.getByText('Fake Greeting')).toBeVisible();
});Want to contribute? Yayy! ๐
Please read and follow our Contributing Guidelines to learn what are the right steps to take before contributing your time, effort and code.
Thanks ๐
Be kind to each other and please read our code of conduct.
This project is inspired by the following projects:
vitest-browser-vue angular-testing-library
MIT