Documentation Index
Fetch the complete documentation index at: https://mintlify.com/bitwarden/clients/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Bitwarden clients repository uses Jest as the primary testing framework for unit tests. Tests are co-located with source files using the .spec.ts naming convention.
Jest Configuration
Root Configuration
The main Jest configuration is located at jest.config.js:
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig.base");
module.exports = {
reporters: ["default", "jest-junit"],
collectCoverage: true,
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",
}),
// Multi-project configuration
projects: [
"<rootDir>/apps/browser/jest.config.js",
"<rootDir>/apps/cli/jest.config.js",
"<rootDir>/libs/common/jest.config.js",
// ... and more
],
// Performance optimization
maxWorkers: 3,
};
Library-Level Configuration
Each library has its own Jest configuration (e.g., libs/common/jest.config.js):
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("../../tsconfig.base");
const sharedConfig = require("../shared/jest.config.ts");
module.exports = {
...sharedConfig,
displayName: "libs/common tests",
preset: "ts-jest",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/../../",
}),
};
Shared Configuration
The shared Jest configuration (libs/shared/jest.config.ts.js) provides common settings:
module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"],
maxWorkers: 3, // Memory leak workaround
setupFiles: ["<rootDir>/../../libs/shared/polyfill-node-globals.ts"],
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: "<rootDir>/tsconfig.spec.json",
isolatedModules: true, // Performance optimization
astTransformers: {
before: ["<rootDir>/../../libs/shared/es2020-transformer.ts"],
},
},
],
},
};
Test Commands
Running Tests
# Run all tests
npm test
# Watch mode (clear cache first)
npm run test:watch
# Watch all tests
npm run test:watch:all
# Run tests for specific project
npm test -- --project=libs/common
Additional Test Commands
# Type checking
npm run test:types
# Locale testing
npm run test:locales
# Storybook tests
npm run test-stories
npm run test-stories:watch
Test Patterns
Basic Service Test
Example from libs/vault/src/services/copy-cipher-field.service.spec.ts:
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CopyCipherFieldService } from "@bitwarden/vault";
describe("CopyCipherFieldService", () => {
let service: CopyCipherFieldService;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let toastService: MockProxy<ToastService>;
beforeEach(() => {
platformUtilsService = mock<PlatformUtilsService>();
toastService = mock<ToastService>();
service = new CopyCipherFieldService(
platformUtilsService,
toastService,
// ... other dependencies
);
});
describe("copy", () => {
it("should copy value to clipboard", async () => {
const result = await service.copy("test", "username", cipher);
expect(result).toBeTruthy();
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("test");
});
it("should return early when valueToCopy is null", async () => {
const result = await service.copy(null, "username", cipher);
expect(result).toBeFalsy();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
});
});
Angular Component Test
Example from libs/vault/src/components/totp-countdown/totp-countdown.component.spec.ts:
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { BitTotpCountdownComponent } from "./totp-countdown.component";
describe("BitTotpCountdownComponent", () => {
let component: BitTotpCountdownComponent;
let fixture: ComponentFixture<BitTotpCountdownComponent>;
let totpService: jest.Mocked<TotpService>;
const mockCipher = {
id: "cipher-id",
name: "Test Cipher",
login: { totp: "totp-secret" },
} as CipherView;
beforeEach(async () => {
totpService = mock<TotpService>({
getCode$: jest.fn().mockReturnValue(of({ code: "123456", period: 30 })),
});
await TestBed.configureTestingModule({
providers: [
{ provide: TotpService, useValue: totpService }
],
}).compileComponents();
fixture = TestBed.createComponent(BitTotpCountdownComponent);
component = fixture.componentInstance;
component.cipher = mockCipher;
fixture.detectChanges();
});
it("initializes totpInfo$ observable", (done) => {
component.totpInfo$?.subscribe((info) => {
expect(info.totpCode).toBe("123456");
expect(info.totpCodeFormatted).toBe("123 456");
done();
});
});
});
Mocking Strategies
Using jest-mock-extended
The repository uses jest-mock-extended for type-safe mocking:
import { mock, MockProxy } from "jest-mock-extended";
// Create a mock with all methods
const mockService = mock<MyService>();
// Configure specific methods
mockService.myMethod.mockResolvedValue("result");
mockService.anotherMethod.mockReturnValue(42);
Mocking Observables
import { of } from "rxjs";
// Mock observable return value
accountService.activeAccount$ = of({ id: userId } as Account);
// Mock method returning observable
totpService.getCode$.mockReturnValue(of({ code: "123456", period: 30 }));
Test Utilities
The repository provides test utilities in dedicated packages:
@bitwarden/core-test-utils - Core testing utilities
@bitwarden/state-test-utils - State management test helpers
@bitwarden/storage-test-utils - Storage mocking utilities
Example from libs/common/spec/fake-account-service.ts:
import { mockAccountServiceWith } from "@bitwarden/common/spec/fake-account-service";
// Create a mock account service with predefined data
const accountService = mockAccountServiceWith(
userId,
{ name: "Test User", email: "test@example.com" }
);
Test Setup Files
Each library can have a test.setup.ts file for custom configuration:
// libs/common/test.setup.ts
import "core-js/proposals/explicit-resource-management";
import { webcrypto } from "crypto";
import { addCustomMatchers } from "./spec";
// Polyfill for crypto in Node environment
Object.defineProperty(window, "crypto", {
value: webcrypto,
});
// Add custom Jest matchers
addCustomMatchers();
Coverage
Coverage is configured to:
- Collect from all
.ts files in src/ directories
- Generate HTML and LCOV reports
- Output to
coverage/ directory
- Use
jest-junit reporter for CI integration
# View coverage after running tests
open coverage/index.html
Best Practices
1. Test Structure
- Use descriptive
describe blocks to group related tests
- Name tests with “should” statements:
it("should return null when...")
- Follow Arrange-Act-Assert pattern
2. Mocking
- Mock all external dependencies
- Use
jest-mock-extended for type safety
- Reset mocks between tests with
beforeEach
3. Async Testing
// For observables
it("should emit value", (done) => {
observable$.subscribe((value) => {
expect(value).toBe(expected);
done();
});
});
// For promises
it("should resolve", async () => {
const result = await service.myMethod();
expect(result).toBe(expected);
});
4. Test Organization
- Co-locate tests with source files:
my-service.ts → my-service.spec.ts
- Use nested
describe blocks for complex test suites
- Group related test cases together
- Tests run with
maxWorkers: 3 to prevent memory issues
- Use
isolatedModules: true for faster compilation
- Avoid unnecessary setup in
beforeEach
Common Patterns
Testing Password Reprompt
describe("password reprompt", () => {
beforeEach(() => {
cipher.reprompt = CipherRepromptType.Password;
});
it("should show password prompt when required", async () => {
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await service.copy("value", "password", cipher);
expect(result).toBeTruthy();
expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled();
});
it("should return early when prompt is cancelled", async () => {
passwordRepromptService.showPasswordPrompt.mockResolvedValue(false);
const result = await service.copy("value", "password", cipher);
expect(result).toBeFalsy();
});
});
Testing Event Collection
it("should collect an event", async () => {
await service.performAction(cipher);
expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientCopiedPassword,
cipher.id,
false,
cipher.organizationId
);
});
Troubleshooting
Memory Issues
If tests crash due to memory:
- Check
maxWorkers is set to 3
- Verify
isolatedModules: true in ts-jest config
- Clear Jest cache:
npm run test:watch (includes --clearCache)
Type Errors
If TypeScript types aren’t working:
- Ensure
tsconfig.spec.json is properly configured
- Check
moduleNameMapper paths align with tsconfig.base.json
- Verify all type dependencies are installed
Import Resolution
If imports fail:
- Check
pathsToModuleNameMapper configuration
- Verify the
prefix matches your directory structure
- Ensure barrel exports (
index.ts) are present