GlassKit

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

PackageFileTests
@glasskit/glasses-uisrc/dpad.test.tsThe scoreRect spatial-focus algorithm: six directional + edge cases
@glasskit/glasses-uisrc/sensors.test.tsorientationEqual / motionEqual pure equality helpers
@glasskit/backendconvex/lib/priceToPlan.test.tsThe 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 test

Turbo 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 test

Watch mode (per package, since Turbo doesn't pipe vitest watch cleanly):

cd packages/glasses-ui && pnpm exec vitest

How 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

dpad.tsx
dpad.test.ts
sensors.ts
sensors.test.ts

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:

packages/glasses-ui/src/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.

packages/glasses-ui/src/X.test.ts
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:

full-stack.test.ts

Then exclude __integration/** from the default vitest run config in that package's vitest.config.ts, and add a separate test:integration script:

packages/backend/package.json
{
  "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 → mintAndAttach path 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.

On this page