Skip to content
v0.6.2: tests, built in

0.6.2 is out, and the headline of the 0.6 line is Vitest.

Tests in a framework project are usually a separate concern: their own toolchain, their own setup, their own mocks of everything the app touches. Which is also why the third route in a project often ships without one.

I wanted a new route to arrive with a test next to it, the way it arrives with a handler. So that is what happens now.

Backend: the real stack, no mocks ​

A backend test runs your route through the real thing: routing, middleware, validation, handler, error handling. No network, no running server, no mocks.

It was cheap to build, because the pieces were already there. The isomorphic fetch client from July dispatches into the API in-process during SSR. The harness points that same dispatch at the test runner instead.

ts
import { describe, expect, test } from "vitest";

import { prepareHarness } from "_/test/api";

const { client, route } = await prepareHarness("account");

describe(route, () => {
  test(route, async () => {
    const { body, response } = await client.POST([], { json: { some: "data" } });
    expect(response.status).toEqual(200);
  });
});

One difference from the client you use in the app: the harness client never throws. It hands back { body, response }, because a test wants to assert on a 400 or a 401, not catch it.

Frontend: a real browser, a real server ​

A frontend test starts a real server on a random port, so it never collides with your dev server, and hands the test a URL. No DOM shim stands in for the page.

The browser is yours: Playwright, Puppeteer, whatever the project already uses. The harness owns the server, not the client.

ts
import { describe, test } from "vitest";

import { prepareHarness } from "_/test/pages";
import { browser } from "./browser";

const { page, route } = await prepareHarness("account");

describe(route, () => {
  test(route, async () => {
    const tab = await browser.newPage();
    await tab.goto(page.href());
  });
});

Seeded, like everything else ​

Every route gets a starter index.test.ts beside it, in api/ or pages/, seeded the same way routes are.

Testing is enabled by default. Set test: false to opt out of it entirely, or test: { seed: false } to keep the harness and write the files yourself.

Running them is what you would expect, and Vitest path filters narrow a run to a source folder, one side of it, or a single route:

sh
pnpm test
pnpm test <folder>/api
pnpm test <folder>/pages/<route>

Source folders with testing enabled are loaded into your vitest.config.ts through loadConfig(). The file is yours to extend, just keep that call in place.

More in the backend and frontend testing docs.

Also in 0.6 ​

  • devPort and previewPort are replaced by kosmo.devHost and kosmo.previewHost in package.json
  • route path helpers no longer ask for a params argument on routes that have no params
  • finding a free port range got more robust

Tests are no longer a separate concern. They are files in the folder, seeded like routes, and asserted against the real app.

Released under the MIT License.