End-to-end journeys
The end-to-end (E2E) suite drives the production build of the app in Chromium against a fresh local Hardhat chain with real v0.3 contracts, two contract sets and the access gate. Wallet prompts are answered by an injected test wallet backed by the node's unlocked development accounts. Only the upload and storage services are replaced by an in-memory stand-in, so no Pinata account, Redis or key is needed.
How the harness is built
Global setup
e2e/global-setup.ts runs once before the journeys:
- Refuses to start if anything already answers JSON-RPC on port 8545 or HTTP on port 3100 ("Stop it so the E2E run owns a fresh local chain").
- Refuses to start if
src/contracts/v03/deployment.31337.jsonorsrc/contracts/zk/deployment.31337.jsonalready exist, because the run deletes the files it creates and these would belong to someone's own local deployment (assertNoForeignFilesine2e/support/guards.ts). - Starts
npx hardhat node --port 8545and checks the chain id is 31337 (assertLocalChainrefuses any other chain, because the harness signs with publicly known development keys). - Deploys the first v0.3 set with
GUARDIAN_ADDRESSset to the guardian account, registers the issuer (seedIssuer) and issues one credential in that set (seedOlderCredential). - Deploys a second set sharing the same issuer registry (
ADD_SET=1,ACTIVATE_SET=1,SET_LABEL=next), so new credentials go to the second set while the first stays readable. - Deploys the access gate and runs
npm run zk:public. - Builds and starts the app (
startAppine2e/support/app-server.ts). - Writes
e2e/.generated/state.jsonwith the chain id, addresses, the older credential id and its registry.
The teardown stops every process and removes the generated build, manifests and state.
The app server
startApp builds with next build into .next-e2e (so a running dev server is left alone) and serves it with next start -p 3100 -H 127.0.0.1. The environment sets NEXT_PUBLIC_CONTRACT_VERSION=v03, chain 31337, the local RPC for browser and server, NEXT_PUBLIC_START_BLOCK=0, and turns on the optional features with throwaway values that are public on purpose and protect nothing: a status list signer key, an API signer key, one API key (its hash in API_KEYS), cache times of 0 and API_ALLOW_LOCAL_LIMITS=true.
Accounts
e2e/support/accounts.ts names Hardhat development accounts by role:
| Name | Role in the journeys |
|---|---|
ADMIN | Deployer and admin of the registries |
ISSUER | Registered as "Tribhuvan University" |
STUDENT | The holder |
GUARDIAN | Holds the guardian role |
NEW_ISSUER | The successor key in the key retirement journey ("Tribhuvan University (new key)") |
Fixtures
e2e/support/fixtures.ts extends Playwright's test:
| Fixture | What it gives a journey |
|---|---|
chainReset (automatic) | Takes evm_snapshot before the journey and evm_revert after it; fails loudly if the revert does not succeed, so the next journey never starts from dirty state |
walletPage(account) | A new browser context with the test wallet installed for that account, the upload API mocked, downloads accepted |
visitorPage() | A new browser context with no wallet at all, only the storage gateway mock, as a verifier would have |
The test wallet
installTestWallet (e2e/support/test-wallet.ts) injects an EIP-1193 window.ethereum (isE2ETestWallet: true). Account requests answer with the one account; every other request is forwarded to the local node, which signs for its unlocked accounts. A signing or sending request for any other account is refused ("The test wallet is ... but ... was requested for ..."). No key is held in the browser or the harness.
Upload and storage stand-in
mockUploadApi (e2e/support/mock-upload.ts) answers in the browser context:
| Route | Stand-in behaviour |
|---|---|
/api/auth/nonce, /api/auth/verify | A fixed nonce and { ok: true } |
/api/upload | Pins the uploaded file in a shared in-memory MockStorage; like the real route it refuses anything that is not an encrypted container (PLAINTEXT_NOT_ACCEPTED) |
/api/documents | Lists or removes copies by document hash, refusing to remove when the copy to keep is unknown (KEEP_NOT_FOUND) |
**/ipfs/* | Serves back exactly the bytes that were pinned, so a visitor can open what an issuer stored |
The real upload route, with Pinata, Redis and the role check, is covered by src/app/api/upload/route.test.ts.
Other helpers
| File | Purpose |
|---|---|
e2e/support/journey-steps.ts | Shared steps and sample documents (for example a diploma for "Asha Rai") |
e2e/support/admin.ts | Admin actions the site does not offer: unpauseAsAdmin, replaceIssuerKey |
e2e/support/access-members.ts | admitFillerMembers for the deep access group journey |
e2e/support/older-set.ts, seed.ts, state.ts, paths.ts, processes.ts, local-chain.ts | Seeding, state, paths, process control and chain checks |
The helpers have their own Vitest tests: guards.test.ts, local-chain.test.ts, mock-storage.test.ts, test-wallet.test.ts.
Running
npm ci && npm --prefix blockchain ci
npx playwright install chromium
npm run test:e2e # all journeys
npx playwright test e2e/journeys/issue-and-verify.journey.ts # one file
Ports 8545 and 3100 must be free and the two local manifests must not exist. playwright.config.ts runs one worker (the chain is shared), a 90 second timeout per journey, 15 seconds per assertion, no retries, traces and screenshots kept on failure, and forbidOnly in CI. Reports go to playwright-report/, failure traces to test-results/.
The journeys
22 files with 38 journeys (counted with grep -cE "^\s*test\(" e2e/journeys/*.ts on 2026-10-24). The descriptions are the journey titles.
| File | Journeys |
|---|---|
issue-and-verify.journey.ts | An issuer issues a credential of its own type and a visitor with no wallet verifies it from the QR code |
revoke-and-reverify.journey.ts | An issuer revokes a credential and a visitor then sees it revoked |
dashboard-actions.journey.ts (4) | Revoke from the dashboard after confirming; re-issue marks the old one superseded; a revoked credential cannot be re-issued as a replacement; a rejected second step of a re-issue across sets is reminded and finished |
delivery.journey.ts (2) | Issuing without delivery asks first, and going back lets the issuer tick it; credentials issued with delivery before and after the invitation reach the student once they accept, undelivered ones never do |
invite-and-inbox.journey.ts | A student accepts an invitation and finds the credential in their inbox with its QR code |
manage-student-link.journey.ts (2) | An issuer unlinks a wallet and the student no longer sees the credential; an issuer cancels an invitation and the student can no longer accept it |
encrypted-documents.journey.ts (2) | A document is stored only as ciphertext and the holder and issuer can each open it; a package with the wrong key cannot open it |
share-and-replace.journey.ts | A holder shares by file and by link, and the issuer's key replacement shuts the old key out |
replace-shared-file.journey.ts | An issuer who sent one file to two students replaces a key and is not offered the removal that would break the other copy |
selective-disclosure.journey.ts (2) | A holder shows only the programme and a visitor verifies it, and a revoke voids it; an earlier credential gets disclosure data from the dashboard |
vc-export.journey.ts (2) | An issuer downloads a VC, a visitor checks it, a revoke voids it; a VC for another credential is refused |
open-badge.journey.ts (3) | An Open Badge is checked without a wallet and voided by a revoke; a badge for another credential is refused; a badge with the standard proof is checked and a changed claim fails it |
status-lists.journey.ts | The served lists follow suspend, reinstate and revoke, and a visitor checks them against the chain |
verification-api.journey.ts | A relying party's backend checks a credential through the signed API, before and after a revoke |
service-probe.journey.ts | The canary probe passes, reports how fresh the API's view is, and sees a revocation within one run |
mobile-wallet.journey.ts (2) | A phone QR is checked offline and online and voided by a revoke; a verifier's challenge is signed and cannot be replayed |
access-proof.journey.ts (2) | A requester proves access with a browser proof and the issuer sees the receipt; a never-admitted requester is refused and nothing is sent |
access-proof-depth6.journey.ts | A proof for the deepest group the app serves is made in the browser and accepted by the gate |
bulk-import.journey.ts (3) | Import, stop and carry on without duplicates, then verify; a file with mistakes is refused and nothing is signed; a corrected row is flagged and can replace the earlier credential in the same transaction |
contract-sets.journey.ts (3) | A credential of the older set is found, named and managed in its own set; a new credential lands in the active set and an unlisted id is found in no set; the dashboard lists and revokes the older set's credential |
pause-and-restart.journey.ts | A guardian pauses: issuing stops, verification and revocation keep working, the admin restarts |
key-retirement.journey.ts | An admin retires an issuer key: old credentials stay valid, the new key takes over, the old key can do nothing |
What the suite does not cover
Pinata, Redis and the real upload route (unit tested instead), MetaMask and other wallet extensions, Sepolia, the v0.2 contract (not deployed by the harness), browsers other than Chromium. These are covered by the Manual checks.
Source files
playwright.config.ts,e2e/global-setup.tse2e/support/fixtures.ts,test-wallet.ts,mock-upload.ts,mock-storage.ts,app-server.ts,accounts.ts,local-chain.ts,guards.ts,seed.ts,older-set.ts,admin.ts,journey-steps.tse2e/journeys/*.journey.tsblockchain/scripts/deploy-v03.ts,blockchain/scripts/deploy-access-gate.ts,scripts/zk-public.mjs