Testing Playbook
Use the smallest test surface that can fail for the change you are making, then widen only if the change crosses boundaries.
Main Commands
From repo root:
bun run test:core
bun run test:web
bun run test:backend
bun run test:scripts
bun run type-check
Avoid defaulting to bun run test unless you really need the full suite.
CI Unit Test Workflow
Source of truth:
.github/workflows/test-unit.yml.github/workflows/test-e2e.yml
Unit workflow (test-unit.yml):
- triggers on
push - runs a matrix across
core,web,backend, andscripts - uses
fail-fast: false, so one failing lane does not cancel the others - runs
bun run test:<project>in each lane after dependency install - passes timezone through
TZ: ${{ vars.TZ }}
Local parity commands:
bun run test:core
bun run test:web
bun run test:backend
bun run test:scripts
E2E workflow (test-e2e.yml) is separate and runs on pull requests to main via bun run test:e2e.
Current Test Strategy
bun run test:coreusesbun testwith a small compatibility preload for the core BSON mock setup.bun run test:web,bun run test:backend, andbun run test:scriptsintentionally retain the existing Jest harness while their hoist-heavy module-mocking patterns are migrated.bun run test:<project>is the stable CI-facing entrypoint for every package; the root dispatcher chooses the correct runner per project.
Retained Jest Layout
Source:
jest.config.js
Projects:
corewebbackendscripts
Each project has its own setup files and module alias mapping.
What To Run By Change Type
Shared type or schema change
Run:
bun run test:core && bun run test:web && bun run test:backend
bun run type-check
Web-only UI or behavior change
Run:
bun run test:web
Add bun run test:core if the change touched shared utilities.
Backend route or service change
Run:
bun run test:backend
Add bun run test:core if a shared type or mapper changed.
CLI, migration, or seeder change
Run:
bun run test:scripts
Web Test Style
Preferred style:
- React Testing Library
- semantic queries by role/name/text
user-eventfor real interactions
Avoid:
- CSS selectors
- implementation-detail assertions
- unnecessary module-wide mocks
Web Jest Harness Defaults (MSW + Globals)
Primary setup files:
packages/web/src/__tests__/web.test.start.tspackages/web/src/__tests__/__mocks__/server/mock.handlers.ts
Current defaults worth knowing:
- MSW runs in strict mode:
server.listen({ onUnhandledRequest: "error" }) - unhandled HTTP requests fail the test (instead of silently passing)
- IndexedDB is provided by
fake-indexeddb/auto structuredCloneis polyfilled for test environments that do not provide it- SuperTokens session existence is reset to
trueinbeforeEach
Important built-in handlers include:
GET http://localhost/version.json(used byuseVersionCheck)- event and user profile/metadata routes under
ENV_WEB.API_BASEURL POST /session/refreshwith both token headers and token cookies
When a component/hook introduces a new request, add a handler in the test (or shared handlers) rather than disabling strict mode.
Example per-test override:
import { rest } from "msw";
import { server } from "@web/__tests__/__mocks__/server/mock.server";
server.use(
rest.get("http://localhost/version.json", (_req, res, ctx) => {
return res(ctx.json({ version: "1.2.3" }));
}),
);
Warning-Free React Updates
When a test drives React state updates outside simple one-off interactions, wrap the update sequence in act imported from react.
import { act } from "react";
Use this pattern for:
- grouped
user-eventinteractions that trigger multiple updates - manual callback triggers (for example
matchMediachange handlers) - awaiting async values returned from spies before asserting final UI state
Example:
await act(async () => {
await user.type(screen.getByLabelText(/email/i), "invalid");
await user.tab();
});
Testing Responsive Sidebar State (useSidebarState)
Files:
packages/web/src/common/hooks/useSidebarState.tspackages/web/src/views/Day/components/ShortcutsSidebar/ShortcutsSidebar.tsxpackages/web/src/views/Day/view/DayViewContent.tsxpackages/web/src/views/Now/view/NowView.tsx
Reliable setup pattern:
- set
window.innerWidthexplicitly in each test scenario (>= 1280for open,< 1280for closed) - mock
window.matchMediawithaddEventListener/removeEventListenersupport - expose a small test helper to trigger media-query changes and wrap the trigger in
act
Assertions to prefer:
- query the sidebar by landmark role and label (
role="complementary",name: "Shortcuts sidebar") - when asserting presence in JSDOM for desktop-only markup (
hidden xl:flex), use role queries that allow hidden elements where needed - verify both pathways for toggle behavior:
- user interaction (header toggle button)
- keyboard interaction (
[shortcut via view shortcut hooks)
Route-Aware Component Tests
For components that depend on routing context (Outlet, nested routes, route transitions), prefer the shared memory-router helper:
packages/web/src/__tests__/utils/providers/MemoryRouter.tsx
Pass initialEntries when asserting nested or non-root routes.
Global And Console Cleanup
If a test overrides globals (for example window.location or window.indexedDB) or spies on console.*, always restore them in teardown (afterEach/afterAll) to prevent cross-test leakage and noisy output.
Floating UI-Dependent Tests
If a test exercises components that rely on @floating-ui/react refs/styles (for example Day view task/context-menu interactions), import the shared setup:
@web/__tests__/floating-ui.setup
This keeps tests on production code paths while avoiding brittle layout coupling in JSDOM.
Jest Unbound-Method Rule In Tests
Test linting enforces jest/unbound-method. If you need to assert method calls on non-mock objects, spy on the method first so assertions are bound to a Jest mock/spy.
Useful anchors:
packages/web/src/__tests__packages/web/src/views/**/*.test.tsxpackages/web/src/sse/**/*.test.tsx
Backend Test Style
Preferred style:
- controller/service behavior tests
- realistic request flows when possible
- mock only external services, not internal business logic
Do not import mongoService (or other persistence implementations) directly in tests. Use test drivers instead (e.g. UserDriver, GoogleWatchDriver in packages/backend/src/__tests__/drivers/). Drivers encapsulate persistence so that switching away from Mongo (or another store) in the future does not require changing test code.
Useful anchors:
packages/backend/src/__tests__packages/backend/src/__tests__/drivers/packages/backend/src/event/services/*.test.tspackages/backend/src/sync/**/*.test.ts
Core Test Style
Preferred style:
- pure function coverage
- edge cases and schema validation
- date and recurrence invariants
Useful anchors:
packages/core/src/util/**/*.test.tspackages/core/src/types/*.test.tspackages/core/src/validators/*.test.ts
E2E Notes
E2E tests live in e2e.
Use them for:
- critical user flows
- integration between auth, UI, and persistence
- regressions that unit tests cannot model cleanly
Testing Realtime And Sync Changes
For SSE or sync work:
- test backend emitters/handlers where possible
- test web SSE hooks for listener registration and dispatch behavior
- test event sagas if refetch or optimistic behavior changed
Common Gaps To Watch
- optimistic event ids
- recurring event scope handling
- local-only versus authenticated repository behavior
- storage migration paths
- date parsing around all-day events and UTC formatting