Unit and rendered tests
One Vitest run at the repository root tests everything written in TypeScript outside the contracts: the service layer, the cryptography and format code in src/lib, the Next.js API route handlers, the React pages and components, the SDK in packages/sdk, the scripts in scripts/ and the E2E harness's own helpers. It runs without a chain, a browser or a network: chain access goes through fakes, and pages render in jsdom.
Configuration
vitest.config.ts:
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
test: {
environment: 'node',
exclude: [...configDefaults.exclude, 'blockchain/**', 'interop/**', '.next/**', '.next-e2e/**', 'e2e/journeys/**'],
coverage: {
provider: 'v8',
include: ['src/services/**/*.ts', 'src/lib/**/*.ts'],
exclude: ['**/*.test.ts', '**/__fixtures__/**', 'src/services/testing/**'],
reporter: ['text', 'json-summary'],
thresholds: { lines: 80, statements: 80, functions: 80, branches: 80 },
},
},
});
What it means:
| Setting | Effect |
|---|---|
alias '@' | @/services/... resolves to src/services/..., as in the app |
environment: 'node' | Tests run in Node by default. A file that renders React opts into jsdom with a first-line comment |
| Default include | Vitest's default pattern, so any *.test.ts, *.test.tsx (or .spec) file is picked up |
exclude | Leaves out the contract tests (blockchain/, run by Hardhat), the interop tests (interop/, run by node:test), Next.js build folders and the Playwright journeys. node_modules folders are excluded by the defaults |
| Coverage | V8 coverage over src/services and src/lib only, with an 80% gate on all four measures |
vitest.shims.d.ts holds a type reference to the @vitest/browser Playwright provider. Browser mode is not configured in vitest.config.ts; the rendered tests use jsdom.
Commands
| Command | What it does |
|---|---|
npm run test:run | One run of every Vitest file (vitest run --passWithNoTests) |
npm run test:coverage | One run with coverage and the 80% gate (what CI runs) |
npm test | Watch mode, for local work only |
npx vitest run src/lib/eip712.test.ts | One file |
npm run sdk:test | Only packages/sdk |
Two environments
| Environment | How a file chooses it | Used for | Count |
|---|---|---|---|
node | Default | Services, src/lib, route handlers, scripts, SDK, source scans | the rest |
jsdom | // @vitest-environment jsdom on the first line | Rendered pages and components | 74 files |
Rendered tests use @testing-library/react (render, screen, fireEvent, waitFor, cleanup) and the matchers from @testing-library/jest-dom/vitest. They mock next/navigation and wrap the component in ServicesProvider with fake services, so a page sees exactly the service answers the test chooses. A shortened example from src/components/layout/Header.test.tsx:
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
const route = vi.hoisted(() => ({ pathname: "/" }));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => route.pathname,
}));
Fakes for the chain
Services take their contract access through a gateway interface (see Service layer), so tests hand them a fake instead of a provider.
| File | Exports | Use |
|---|---|---|
src/services/testing/fake-gateway.ts | fakeGateway(methods, options), asContract(methods), txResult(receipt) | One fake contract behind every accessor of a ContractGateway; the writer runs an optional guard first (for example a wrong-network check). txResult returns a mock transaction whose wait() resolves to a receipt |
src/services/testing/fake-credential-gateway.ts | fakeCredentialGateway(...), FAKE_REGISTRY, FAKE_ISSUER_REGISTRY, FAKE_STATUS_REGISTRY, FAKE_RECORD_TYPE_REGISTRY, FAKE_HOLDER_REGISTRY | The same for the v0.3 registries, with fixed fake addresses |
These files are excluded from coverage. Route handlers are tested by calling the exported handler with a constructed Request and injected stores and fetchers; for example src/app/api/upload/route.test.ts and src/lib/server/ops/handler.test.ts.
Shared vectors
Where the browser and a contract must compute the same bytes, both test suites read one committed vector file, so a change on either side fails a test.
| Vector file | Read by | Pins |
|---|---|---|
blockchain/test/fixtures/credential-vector.json | src/lib/eip712.test.ts and contract tests such as blockchain/test/v03/CredentialIssuance.test.ts | The EIP-712 credential digest and id |
blockchain/test/fixtures/commitment-vectors.json | src/lib/commitments.test.ts and blockchain/test/v03/NoPlaintext.test.ts | Salted subject and metadata commitments |
blockchain/test/fixtures/invitation-vector.json | src/lib/invitation.test.ts, src/lib/invitation-file.test.ts and blockchain/test/v03/HolderRegistry.test.ts | The enrolment invitation digest |
blockchain/test/zk/fixtures/access-scope-vector.json | src/lib/zk/scope.test.ts and blockchain/test/zk/AccessGate.test.ts | The access proof scope and message |
vc/test-vectors/golden-vc.json | src/lib/vc/golden.test.ts | A complete verifiable credential |
src/lib/disclosure/vectors.json | src/lib/disclosure/vectors.test.ts | Selective disclosure field tree |
Frozen strings in src/lib/present/vectors.test.ts | the same file | Version 1 compact codes (v1., d1., c1.) must keep decoding to the same facts |
src/lib/doc-crypto/fixtures/rfc9180-x25519-aes256gcm.json | src/lib/doc-crypto/wrap.test.ts | Published HPKE test vectors for key wrapping |
Fixture builders live in __fixtures__ folders, for example src/lib/vc/__fixtures__/make-package.ts and make-badge-signer.ts.
Property tests
Some format and cryptography tests use fast-check to generate many inputs, for example src/lib/doc-crypto/container.test.ts, payload.test.ts, encoding.test.ts, src/lib/vc/parse.test.ts, src/lib/import/csv.test.ts and adapters.test.ts. The contract invariant suite uses the same library; see Invariants and gas.
Source scans
A few tests read source files instead of running them, to pin a rule about the code itself. Fourteen test files under src/ do this, including:
| Test | Rule it pins |
|---|---|
src/app/data-exposure.test.ts | No page except the record detail and add pages references ipfsHash; pages read records through the permission-aware call; the public verify pages look credentials up by id only |
src/lib/zk/proof-boundary.test.ts | The proving package is imported only by the access proof module, which only the worker loads |
src/lib/doc-crypto/boundary.test.ts | Which document crypto modules are safe for the server |
src/app/fonts.test.ts | Fonts are self-hosted and not requested from a third-party host |
src/compliance/dpia.test.ts | The data protection impact assessment draft exists and covers every required section |
src/lib/vc/schema.test.ts, src/lib/vc/openbadges/schema.test.ts | Exported documents pass the vendored schemas |
Where the tests are
Tests sit next to the module they test (src/lib/eip712.ts and src/lib/eip712.test.ts). Counted on 2026-10-24:
| Folder | Test files |
|---|---|
src/lib | 167 |
src/app | 74 |
src/services | 59 |
src/components | 19 |
packages/sdk | 6 |
e2e/support | 4 |
src/contracts | 2 |
src/compliance | 1 |
scripts (including scripts/lib) | 2 |
| Total | 334 |
The last full run recorded on 2026-10-24 passed 3,369 tests in these 334 files.
Counting tests
find . \( -path '*/node_modules' -o -path ./blockchain -o -path ./interop -o -path ./.next \
-o -path ./.next-e2e -o -path ./e2e/journeys -o -path ./.git \) -prune -o -type f \
\( -name "*.test.*" -o -name "*.spec.*" \) -print | wc -l
npm run test:run # prints the number of tests that ran
Conventions
- Test names read as sentences about behaviour:
describe("the package stands on its own"),it("imports nothing from the app"). - Prefer injecting a dependency over mocking a module; module mocks are kept for framework edges such as
next/navigation. - Read failures get their own tests, so a failure is never shown as a negative answer: for example
src/app/student/MyCredentialsPanel.test.tsx("shows a retryable error when the history could not be read, not an empty list") andsrc/app/dashboard/IssuedCredentialsPanel.test.tsx("says a failed load is not proof of an empty ledger and retries on request"). - No
.only, no skipped tests, no tests underdocs/.
Source files
vitest.config.ts,vitest.shims.d.tssrc/services/testing/fake-gateway.ts,src/services/testing/fake-credential-gateway.tssrc/lib/vc/__fixtures__/blockchain/test/fixtures/,blockchain/test/zk/fixtures/access-scope-vector.json,vc/test-vectors/golden-vc.json
Tests
The tests named on this page, for example src/components/layout/Header.test.tsx, src/lib/eip712.test.ts, src/lib/commitments.test.ts, src/app/data-exposure.test.ts and src/lib/present/vectors.test.ts.