Testing
The vitest smoke suite that ships with the boilerplate, how Turbo wires it, and how to add your own tests.
GlassKit ships a small, pure-unit test suite. Three test files, ~18 assertions, no Convex deployment or external service required to run them. The point isn't comprehensive coverage; it's signal in CI on a fresh clone, before you've done any setup.
What's covered
| Package | File | Tests |
|---|---|---|
@glasskit/glasses-ui | src/dpad.test.ts | The scoreRect spatial-focus algorithm: six directional + edge cases |
@glasskit/glasses-ui | src/sensors.test.ts | orientationEqual / motionEqual pure equality helpers |
@glasskit/backend | convex/lib/priceToPlan.test.ts | The Stripe PRICE_TO_PLAN mapping in entitlements.ts: known prices, unknown prices, env-driven changes |
All three are pure: no DOM, no Convex client, no network. They test the bits of the codebase where logic actually lives, not the integration points (where Convex's own test harness would be needed).
Running the suite
From the repo root:
pnpm testTurbo invokes the test script in every workspace that declares
one. Currently that's @glasskit/glasses-ui and @glasskit/backend.
The app/ and companion/ workspaces have no test script, so
Turbo skips them silently.
Run a single package:
pnpm --filter @glasskit/glasses-ui test
pnpm --filter @glasskit/backend testWatch mode (per package, since Turbo doesn't pipe vitest watch cleanly):
cd packages/glasses-ui && pnpm exec vitestHow CI uses the suite
.github/workflows/ci.yml runs pnpm test before typecheck +
build. It runs unconditionally, even on a fresh clone where the
Convex _generated/ directory doesn't exist yet, because the
suite doesn't depend on _generated/. Fresh clones get test
signal on PR #1 before they've configured anything.
The Convex-dependent typecheck + build steps are gated on
_generated/ being present, so they only run once you've
committed it.
The convention
Pure unit tests live alongside source
packages/backend/convex/lib/priceToPlan.test.ts follows the same
convention: pure helper is extracted to lib/priceToPlan.ts and
the test sits next to it.
Extract pure functions to make them testable
The pattern is visible in dpad.tsx:
// Pure — no DOM. Exported separately for testing.
export function scoreRect(
current: RectLike,
candidate: RectLike,
dir: Dir,
): number | null {
// ... pure scoring logic ...
}
// Stateful — uses pure scoreRect internally.
function moveFocus(dir: Dir) {
const els = focusables();
// ...
for (const el of els) {
const score = scoreRect(currentRect, el.getBoundingClientRect(), dir);
// ...
}
}The expensive integration logic (DOM queries, event listeners) is untested. The bug-prone pure logic (the geometric scoring) is exhaustively tested. Cheap tests, high signal.
Adding a new test
For a glasses-ui helper
Add packages/glasses-ui/src/X.test.ts next to the source.
vitest auto-discovers *.test.ts and *.test.tsx.
import { describe, it, expect } from "vitest";
import { myHelper } from "./X";
describe("myHelper", () => {
it("does the thing", () => {
expect(myHelper(1)).toBe(2);
});
});For a Convex backend helper
Add packages/backend/convex/lib/X.test.ts. Extract the pure bit
of any Convex function you want to test into convex/lib/X.ts
first (a function that takes plain values and returns plain
values, not a function that takes ctx).
Don't try to test Convex queries/mutations/actions directly with
vitest; they need the Convex runtime. Convex has its own test
harness (convex-test) for that; we don't ship it because the
pure-logic split keeps coverage where it matters without the
heavier setup.
Tests that need _generated/
Sometimes a test legitimately needs to import from
@glasskit/backend/convex/_generated/api (e.g. an end-to-end
smoke that exercises the whole Convex API surface). Those tests
must be gated separately because they fail on a fresh clone
before codegen runs.
The convention if you add such tests: put them under an
__integration/ directory:
Then exclude __integration/** from the default vitest run
config in that package's vitest.config.ts, and add a separate
test:integration script:
{
"scripts": {
"test": "vitest run --exclude '**/__integration/**'",
"test:integration": "vitest run __integration/"
}
}CI runs pnpm test (skipping integration) unconditionally + gates
pnpm turbo run test:integration behind _generated/ existing,
mirroring the typecheck/build gating.
There are no integration tests in the boilerplate today; this is just the place for them when you add them.
What's NOT tested
By design, not by accident
- React components: no Testing Library, no jsdom. The glasses-ui components are thin enough that visual review on device (or in the browser preview) catches more than RTL would.
- Convex actions end-to-end: would need a real Convex deployment. Not in scope for the smoke suite.
- The full pairing flow: the same-network register → claim →
on-glasses confirm →
mintAndAttachpath needs a real Convex deployment and real Clerk infrastructure (the Sign-In Ticket is minted server-side). See Pairing for the manual verification path. - AI calls: would burn API tokens on every CI run. Manual verification via the demos.
For end-to-end coverage you'd add Playwright tests at the
companion/ level (Playwright can drive both apps + assert on
real network responses). That's an explicit "add this if you
need it" choice, not shipped because the value depends entirely
on your product's flows.
CI run summary
What you'll see in a GitHub Actions run for a new PR:
✓ Setup pnpm
✓ Setup Node
✓ Install dependencies
✓ Detect Convex codegen
- if _generated/ exists: ready=true
- if not: ready=false (notice posted, CI stays green)
✓ Typecheck (only if ready=true)
✓ Test (always runs)
✓ Build (only if ready=true, with shape-valid env placeholders)Two-stage gating means a fresh clone's first PR (before you've run
pnpm exec convex dev) still passes CI; they just see the
notice. Once they commit _generated/, full typecheck + build
turn on automatically.