Skip to content

Testing Overview

Dejan Strbac edited this page Nov 12, 2026 · 1 revision

ALPS has three test tiers: Go tests for the backend, frontend unit tests that run in Node.js under jsdom, and browser tests that drive the built application in Chromium against a real mail stack. This page covers what each tier catches and how to run all three before you open a pull request. The repository has no CI, so these local runs are the only testing a change gets. The details are on Go Tests, Frontend Unit Tests, Browser Tests and in docs/E2E.md.

The three tiers

Tier Run with Needs Tests live in
Go tests go test ./... or make test Go 1.25.0 or later, nothing else *_test.go beside the sources
Frontend unit tests cd frontend && npm test (a typecheck, then Vitest) Node.js, and npm install in frontend/ frontend/test/**/*.test.ts
Browser tests scripts/e2e.sh or make test-e2e Go, Node.js, PostgreSQL, a sora checkout, Playwright's Chromium frontend/e2e/*.spec.ts

Use Node.js 20 (20.19 or later), 22 (22.12 or later), or 24 and later. On Node.js 23, npm warns during install that the locked Vitest does not support it (EBADENGINE). The README has the version details.

What each tier catches

Go tests

The Go tests cover the backend: HTTP handlers, configuration loading and startup (cmd/alps), the imap, maildir and multi providers, sessions, protocol handling, the cluster, the TLS manager, the Go code of all six plugins, the browser harness's test stack (internal/e2estack), and how scripts/e2e.sh keeps the record of the last browser run (scripts). Every package has tests except four. internal/netguard and internal/serviceurl are tested through the packages that use them, and provider/providertest and tlsmanager/storage/storagetest hold the shared test doubles.

They need no outside server and no setup. The servers a test talks to run inside the test process on loopback, on ports the system picks: in-memory IMAP servers, SMTP servers, HTTP test servers, and, in the cluster package, gossip members. The base plugin's tests run the real server over a Maildir in a temporary directory or over an in-memory IMAP server. Eight cmd/alps tests build the alps binary and run it, and the scripts test runs the first part of scripts/e2e.sh with stand-in tools. No Go test runs frontend code, but the root package embeds the checked-in frontend/dist, and static_test.go checks how the server serves it. See Go Tests.

Frontend unit tests

npm test first typechecks the tests (npm run typecheck:test), then runs vitest run under jsdom. These tests check logic and render decisions: what a component shows for given data, what it does on an event, what a store keeps. jsdom does no layout or painting, and no ALPS backend runs behind the tests.

Only files under frontend/test/ that end in .test.ts run, and the tests for plugin frontends live there too. A *.test.ts file next to a source file never runs. See Frontend Unit Tests.

Browser tests

scripts/e2e.sh builds the frontend, ALPS, the test stack (internal/e2estack) and sora, and creates a throwaway PostgreSQL database for the run. It then starts three things in order:

  1. the test stack: an object store for sora's message bodies, the SMTP submission server ALPS sends through, a relay for sora's own outgoing mail, a CalDAV/CardDAV server and a control API for the tests;
  2. sora, with IMAP, ManageSieve and its admin API;
  3. ALPS, with every plugin enabled and a fixed test login_key.

Last, it runs Playwright in Chromium against ALPS.

This tier finds what the other two cannot see:

  • a component that reads a field the backend does not send;
  • a request the browser refuses;
  • a page that logs an error and renders nothing;
  • a flow that breaks only when a real IMAP server, a real SMTP submission and a real browser are all involved.

A browser test fails on any console error or uncaught exception on its page, as well as on its own assertions. Chromium logs a console error for every response that is not 2xx, so an unexpected refusal from the server fails the test too. A test that provokes an error on purpose allows that one line with allowConsoleErrors.

Some things the browser tests leave out, or test only when you ask for them:

  • Restoring a session, linked accounts and account switching. The harness sets a fixed test login_key, which the DAV connection tests need, so every sign-in also gets the alps_login_token cookie. No spec tests restoring a session from it, linked accounts or account switching (see Sessions and Sign-in). The Go tests cover these features.
  • Two ALPS nodes. e2e/cluster-recovery.spec.ts runs only with E2E_CLUSTER=1, which starts a second ALPS clustered with the first. Without that variable, the spec skips itself.
  • Message previews. The default run checks that ALPS shows no previews when the server offers none. The test that previews do appear runs only with E2E_PREVIEW=1, which needs a SORA_DIR checkout that supports FETCH PREVIEW.
  • A DAV connection with no default server. The default run tests the CalDAV and CardDAV connections users set in Settings against the harness's default servers. E2E_DAV_PERSONAL=1 leaves those servers out, so that e2e/dav-connections.spec.ts tests setting up a connection from nothing.

See Browser Tests.

Running everything before a pull request

Set up once:

cd frontend
npm install
npx playwright install chromium
cd ..

Then, before each pull request, from the repository root:

go test ./...
(cd frontend && npm test)
scripts/e2e.sh
git status
  1. go test ./... needs nothing running. make test runs the same tests with -v. go test -short ./... skips TestAnswerSavedWhenTheReplyCannotBeSent, which waits out the SMTP submission retries. Some caldav tests skip themselves when the system has no time zone data. make test-coverage runs the Go tests with a coverage profile, writes coverage.out and opens its HTML report.

  2. npm test in frontend/ typechecks frontend/test, then runs the whole Vitest suite. A type error in a test stops it before any test runs. Arguments after -- go to Vitest, so npm test -- test/compose-store.test.ts runs one file. npm run test:shuffle runs the same typecheck, then the suite in a random order, which shows a test that depends on what an earlier one left behind. A passing run can still print jsdom's Not implemented: navigation to another Document. That line is not a failure: the exit status and the Test Files and Tests summary lines give the result.

  3. scripts/e2e.sh needs these:

    • go, npm, psql, curl and lsof on PATH;
    • a PostgreSQL server where the pg_trgm and btree_gin extensions can be installed (sora asks for PostgreSQL 14 or later), reached as a user that can connect to the postgres database and create and drop databases (E2E_PG_HOST, E2E_PG_PORT, E2E_PG_USER, E2E_PG_PASSWORD);
    • a sora checkout at ../sora, or wherever SORA_DIR points;
    • Playwright's Chromium from the setup step, or an installed Chromium named with PW_CHROMIUM_PATH.

    By default ALPS listens on port 8900 and the other servers on the next eight ports. The script stops before it builds anything if one of those ports is already taken. To run one spec file, give its path with the e2e/ prefix, as in scripts/e2e.sh e2e/reading.spec.ts. Playwright reads a file argument as a regular expression, so a bare reading.spec.ts also runs threading.spec.ts. Running the browser tests lists every variable, and When a run fails explains each error message.

  4. git status, because the browser run rebuilds the tracked frontend/dist in place with npm run build, unless E2E_SKIP_BUILD is set. When the committed frontend/dist was built from the same sources, the rebuild leaves it unchanged. If frontend/dist shows as modified, decide whether the change belongs in your pull request.

The README asks for the Go tests and the frontend unit tests before every change, and for the browser suite as well when a change touches UI flows. A change to the frontend or to a plugin's frontend should include the rebuilt frontend/dist. Run npm run build in frontend/ (tsc, then vite build) and commit the result.

The same Contributing section says where to send patches and report bugs. The README's AI Assistance Policy asks you to disclose, in the commit messages or the pull request description, any substantive use of generative AI that materially affected the change.

Some changes need an extra check:

When you change Also run
A browser spec or a helper in frontend/e2e/, such as fixtures.ts cd frontend && npm run typecheck:e2e, which typechecks frontend/e2e/ and playwright.config.ts. It cannot tell whether a locator still matches what the page renders; only running the suite shows that.
Frontend or plugin frontend sources cd frontend && npm run build. Its tsc step typechecks frontend/src/ and plugins/*/frontend/.
The code that stores and updates security keys and recovery codes The checks in WEBAUTHN_COORDINATION.md: the Go tests named there, the E2E_CLUSTER=1 browser run, and python3 scripts/model-recovery-versions.py, which runs on Python 3.9 or later.

npm test is the step that typechecks frontend/test. It runs npm run typecheck:test first, which checks the tests, and the sources they import, through tsconfig.test.json. A type error in a test, or a fake that no longer matches the interface it stands in for, fails npm test before any test runs. Neither npm run build nor npm run typecheck:e2e checks the tests, and npx vitest run on its own skips the typecheck.

The repository has no CI

The repository has no CI configuration: no .github workflows and no other pipeline file. No tests run automatically when you open a pull request. They run only when someone runs them locally, so a reviewer knows which tiers ran only if the pull request says so.

No single command runs all three tiers. make test runs only the Go tests, make test-e2e runs only the browser suite, and no Makefile target runs Vitest.

Running tests side by side

  • Go tests. Every Go test listens on ports the system assigns, the cluster package's gossip members included, so two Go test runs can overlap on one machine, from one checkout or two.
  • Browser tests. Two runs can go side by side when their E2E_PORT values are at least 9 apart (12 apart when either uses E2E_CLUSTER=1) and they do not share an E2E_STATE, E2E_LOGS or E2E_RESULTS. The defaults of all three follow E2E_PORT, so only values you set yourself can clash. They take turns at the build step, so a second run may print waiting for another run's build. See Runs side by side.
  • One of each. Because the Go tests listen only on ports the system assigns, a Go test run does not collide with a browser run on the default ports. In one checkout, keep go test ./... from compiling while the browser run builds the frontend, because the Go tests compile in frontend/dist and that build rewrites it. Start the Go tests after the browser run prints building alps, sora and the test stack, or give the browser run E2E_SKIP_BUILD=1.

Reading the results

  • Go tests: one ok or FAIL line per package. The four packages without tests of their own (internal/netguard, internal/serviceurl, provider/providertest and tlsmanager/storage/storagetest) report [no test files].
  • Vitest: the summary at the end gives the number of test files and tests that passed or failed. When the typecheck fails, tsc prints each error with its file and line, and Vitest does not start.
  • Browser tests: Playwright's list reporter prints each test as it runs.
    • If the run ends with a server died during the run, every result after that point is void, and the run exits 1 whatever Playwright reported. Before you look at the failures, read the named server's log in the logs directory that the message prints.
    • A test that fails with the page logged errors means the application logged a console error or threw an uncaught exception. That is a finding, not noise: look at what was logged.
    • Traces and screenshots of failed tests are in E2E_RESULTS, by default test-results/ in the logs directory (${TMPDIR:-/tmp}/alps-e2e-logs-<port>/test-results). When tests fail and every server is still running, the script ends by printing both directories, as ==> server logs: and ==> traces and screenshots:. Open a trace from frontend/ with npx playwright show-trace <path to trace.zip>.
    • To run only the tests that failed, run scripts/e2e.sh --last-failed with the same E2E_PORT, and the same E2E_LOGS or E2E_RESULTS if you set them. See Browser Tests.

Clone this wiki locally