Testing

One command runs every test of the website, the Cloud Functions, the scripts and the docs. The security rules have a second command, because they need the Firebase emulators.

Run the tests

From the repository root:

npm run test

In a terminal, Vitest keeps watching and reruns the tests you touch; press q to stop. To run everything once and stop, for example in a script:

npx vitest run

The functions folder has no test script of its own: its specs run from the root with everything else.

Run one file or one test

npx vitest run functions/src/__tests__/adminCreateUser.spec.ts

A path that contains brackets or parentheses, such as a page in a (group) folder, needs quotes in the shell. To run only the tests whose name contains some words, add -t:

npx vitest run src/app/core/features -t "resolveFeatures"

What runs

vitest.config.ts sets up one run for everything:

SettingValue
Specssrc/**/*.spec.ts, functions/src/**/*.spec.ts, scripts/**/*.spec.ts, and an app's custom/**/*.spec.ts
Environmentjsdom, with Vitest globals
Setup for every specsrc/test/setup.ts (Angular) and functions/src/__tests__/setup.ts (Cloud Functions)
Once before the runscripts/vitest-global-setup.ts writes the functions' generated feature files, which a fresh clone does not have

The rules tests in tests/rules/ are left out of this run (see Rules tests).

Type check

Vitest strips the types from each file without checking them, so code with a type error passes its specs and only fails when npm run build or a deploy compiles it. The suite closes that gap: scripts/__tests__/typecheck.spec.ts fails on any type error in the app or the functions. On its own, in about 15 seconds:

npm run typecheck
ChecksWithLike
The app: src/main.ts, every *.page.ts in src/app/pages and src/custom/pages, and everything they import, templates includedngc -p tsconfig.app.json --noEmitnpm run build
The functions: all of functions/src, their specs and functions/src/custom includedtsc -p functions/tsconfig.json --noEmitnpm run build --prefix functions

The frontend specs (src/**/*.spec.ts) are not type-checked. A file that nothing imports and that is not a page is not either; the build leaves it out too.

Write a component spec

Component specs use Angular's TestBed with Vitest (vi.fn(), vi.mock()). src/test/setup.ts already starts the Angular test environment and gives every spec Transloco with the real English strings, so a rendered template shows the text a person sees. Code that builds a message with a key, outside a template, can be checked against the same English with english(key, params) from src/test/english.ts, which also stands in for a component's translate function on a mock. To prove a screen has no English left, render it with pseudoTestingModule() from src/test/pseudo-language.ts, a pseudo language of the member keys, and expect textNotInPseudo(fixture.nativeElement) to be empty (see Member languages).

The part to plan for is Firebase. Many services reach it indirectly, and a spec must mock whatever would:

Your component usesIt pulls inDo this
<arc-page-header> (every admin and member page)The notification bell, the admin search box and AuthStateSpread ...headerTestProviders() first in providers
AuthServiceDbService, which injects FirestoreProvide a mock with the methods the component calls
AuthState (the auth store)Firebase Auth and AuthServiceProvide a mock
A signal storeIts data service and Firestore.overrideProvider(TheStore, { useValue: mockStore }), with signals for its state

headerTestProviders() is in src/test/header-test-providers.ts. Put it first, so a provider your spec declares for the same token wins. A page spec, following src/app/pages/admin/(transactions)/transactions.page.spec.ts:

import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { provideRouter } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import TransactionsPageComponent from './transactions.page';
import { TransactionsStore } from './transactions.store';
import { headerTestProviders } from '../../../../test/header-test-providers';

describe('TransactionsPageComponent', () => {
    const mockStore = { items: signal([]), isLoading: signal(false), getAll: vi.fn() };

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            imports: [TransactionsPageComponent, NoopAnimationsModule],
            providers: [...headerTestProviders(), provideRouter([])],
        })
            .overrideProvider(TransactionsStore, { useValue: mockStore })
            .compileComponents();
    });

    it('loads the transactions', () => {
        const fixture = TestBed.createComponent(TransactionsPageComponent);
        fixture.detectChanges();
        expect(mockStore.getAll).toHaveBeenCalled();
    });
});

A page that extends BaseComponent also needs an ActivatedRoute mock, as that spec shows. To render in another admin language, import translocoTestingModule({ lang: 'hi' }) from src/test/transloco-test-providers.ts.

Write a Cloud Function spec

Function specs live in functions/src/__tests__/ or beside the code. functions/src/__tests__/setup.ts mocks the Firebase Admin SDK for every spec. In your spec:

  1. Mock ../init, the module that hands out db and owner (the Admin SDK Auth), with fakes that record what the function writes. Build the fakes in vi.hoisted(), because vi.mock() runs before the imports.

  2. Mock the firebase-functions module the function registers with, so the export is the handler itself. For a callable, onCall: (...args) => args[args.length - 1].

  3. Import the function after the mocks and call it with a request ({ auth, data }) or an event.

vi.mock('../init', () => ({ db: m.db, owner: m.owner }));
vi.mock('firebase-functions/v2/https', () => ({
    onCall: (...args: any[]) => args[args.length - 1],
    HttpsError: class HttpsError extends Error { constructor(public code: string, msg: string) { super(msg); } },
}));

import { adminCreateUser } from '../users/adminCreateUser.js';

For a Firestore trigger, mock firebase-functions/v2/firestore so the trigger records its handler, and take the handler once, at the top of the file: vi.mocked(onDocumentDeleted).mock.calls.at(-1)?.[1]. functions/src/__tests__/onUserDelete.spec.ts shows the whole pattern.

Take a trigger's handler at the top of the file, not inside a test. vi.clearAllMocks() in a beforeEach empties the mock's call list, so a handler read inside a test is missing, and a test that returns early when it is missing passes without checking anything.

Code that writes claims (mergeAppClaims(), createAppAccount() and Arc CMS's own claims writes) takes a short lease in Firestore first, so a spec whose fake db has no transactions mocks the lease to simply run the work, as functions/src/__tests__/appKit.spec.ts does:

vi.mock('../users/claimLock.js', () => ({ withClaimLock: (_uid: string, work: () => Promise<unknown>) => work() }));

The custom space in specs

Core specs test Arc CMS as it ships, whatever an app puts in the custom space's starter files, so an app can run the whole suite with its own routes, menu items, home pages, member languages and feature choices:

A spec about a particular choice mocks the file itself, and its mock wins:

vi.mock('../../../custom/features', () => ({ CUSTOM_FEATURES: { on: ['pwa'] } }));
const featuresOff = new Set<string>();
vi.mock('../feature-flags.js', () => ({ isFeatureOn: (id: string) => !featuresOff.has(id) }));

The feature ids are listed in Feature ids.

Specs that meet an app's pages

The same suite runs in an app. The starter files read as shipped (above), but the rest of the app is real, so a core spec must not assume it is empty. Where a spec touches something an app can change, it reads the app's answer instead of a fixed one:

To check a change against an app, fill in a few starter files (routes with userGuard, a menu item, a member language, features turned off) and put a few files in src/custom/site/ (a header without a search box, a home page, a terms page), then run the suite: only the checks that Arc CMS ships its starter files empty and unchanged may fail, and they are skipped in an app.

Rules tests

The security rules are tested against the Firestore and Storage emulators:

npm run test:rules

It combines the core and app rules, starts the emulators with the Firebase CLI, and runs tests/rules/ with vitest.rules.config.ts. It needs the Firebase CLI and Java. Run it whenever a rules file changed. How to write a rules test is in Security rules.

Docs checks

The docs have tests too. They run with npm run test, and on their own with:

npm run check:docs

They fail on a broken link, a path or npm script that does not exist, a missing feature page, or a writing rule broken. See Writing docs.

Before you finish

A change is done when the whole suite passes, with the rules tests when rules changed, and the docs agree with the code: see Docs and tests are part of done.