Files
HealthLog/vitest.setup.ts
T
Marc-André Bombeck b09a6d2df8 test: fail a test whose code caught and logged a missing mock export
A module mock that leaves out an export the code under test reaches throws
at the call, and where that code catches and logs (a best-effort tail, a
fail-soft read) the test went on green while the path it meant to cover
never ran. The Apple Health import tests mocked the rollup module without
its fold window, so the import tail's rollup fold and cache invalidation
never ran in any of them; four route test files mocked the logging context
or the rollups without what the measurement tail reaches.

Both setup files now watch the console and fail the test in which such a
line was logged. The ECG import test keeps the real rollup module and
stubs only the fold, and the four unit mocks gain what they lacked.
2026-10-22 23:15:43 +02:00

104 lines
4.4 KiB
TypeScript

/**
* Unit-test setup.
*
* Seed the client-side locale cache with the EN bundle. In production
* only the active locale reaches the client (boot script, then dynamic
* import on switch — see `src/lib/i18n/load-locale.ts`). Tests mount
* `I18nProvider` standalone — often with `renderToStaticMarkup`, which
* is synchronous and cannot await a dynamic import — so EN, the fallback
* floor of the t() chain, is primed up front.
*
* Only EN. Every catalog is several hundred kilobytes and this file runs
* once per test file, so priming all of them made setup the largest share
* of a unit run. A test that renders another locale primes it itself:
*
* import deMessages from "../../../messages/de.json";
* primeMessages("de", deMessages);
*
* Server-side translation (`getServerTranslator`, `shared-resolve`) holds
* every catalog on its own and needs no priming.
*/
import { afterEach, beforeEach, vi } from "vitest";
import { settleBackgroundTasks } from "@/lib/logging/background-tasks";
import { quarantineEntryFor } from "./tests/quarantine/quarantine";
import { watchSwallowedMockErrors } from "./tests/support/swallowed-mock-errors";
import { primeMessages } from "@/lib/i18n/load-locale";
import enMessages from "./messages/en.json";
primeMessages("en", enMessages);
/**
* Leave no background work in flight when a test ends.
*
* Several paths start a promise, return without awaiting it, and log on the
* detached handle so the failure is not swallowed. Vitest forwards every
* console write to the main thread as an `onUserConsoleLog` RPC call and
* rejects whatever is still pending on that channel while it tears a test file
* down, so a write that lands in that window fails the whole run with
* `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending`
* — blamed on the file the worker was tearing down, which is not the test that
* started the work and need not log anything of its own. Awaiting the
* registered tasks here removes the precondition instead of muting the
* console, which would have thrown away a real signal and left the
* non-determinism in place. See `src/lib/logging/background-tasks.ts`.
*/
const checkSwallowedMockErrors = watchSwallowedMockErrors();
afterEach(async () => {
await settleBackgroundTasks();
// A caught "missing mock export" error fails the test it happened in
// (`tests/support/swallowed-mock-errors.ts`).
checkSwallowedMockErrors();
});
/**
* Skip a test on the flaky-test quarantine list until its entry expires. See
* `tests/quarantine/quarantine.ts` and docs/contributing/testing.md.
*/
beforeEach((context) => {
const entry = quarantineEntryFor(context.task);
if (entry) {
context.skip(`quarantined until ${entry.expires} (${entry.issue})`);
}
});
/**
* The shared write ceiling is charged by every identity resolver on a write
* (`src/lib/auth/write-meter.ts`), which is a database round-trip most route
* tests have no fake for: their Prisma mock carries the models the handler
* touches and nothing else. The meter is therefore a no-op in unit tests by
* default. Its own behaviour is pinned by
* `src/lib/auth/__tests__/write-meter.test.ts`, which loads the real module,
* and against Postgres by `tests/integration/write-meter.test.ts`; a route test
* that wants the refusal mocks `meterAccountWrite` to throw.
*/
vi.mock("@/lib/auth/write-meter", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/auth/write-meter")>();
return { ...actual, meterAccountWrite: vi.fn(async () => undefined) };
});
/**
* The end of the legacy Fitbit Web API is a calendar date
* (`src/lib/fitbit/retirement.ts`). Read off the real clock, every suite that
* exercises the Fitbit sync, its jobs or its routes would turn red on that day
* and stay red until the provider code is removed. The suites therefore run
* before the turndown by default; a test about the turndown itself flips
* `isFitbitRetired` (`vi.mocked(isFitbitRetired).mockReturnValue(true)`) or
* loads the real module with `vi.importActual`, as
* `src/lib/fitbit/__tests__/retirement.test.ts` does.
*/
vi.mock("@/lib/fitbit/retirement", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/fitbit/retirement")>();
const isFitbitRetired = vi.fn((_now?: Date) => false);
return {
...actual,
isFitbitRetired,
fitbitRetirement: vi.fn((now?: Date) => ({
...actual.fitbitRetirement(now),
ended: isFitbitRetired(now),
})),
};
});