---
url: /blog/2026/v0.6.9.md
description: >-
  v0.6.9 adds a frontend.test.ssr option, so the test harness can serve
  server-rendered HTML, and tests can assert on it without driving a browser.
---

One addition in `v0.6.9`, for frontend tests.

## The harness used to need a browser

The frontend test harness serves pages from the dev server.
And the dev server always renders on the client:
it sends a shell, and the page appears only once its JavaScript runs.

So a frontend test had nothing to look at without a real browser,
Playwright or Puppeteer, to fetch the shell and render the page.
Even for a test that only wanted to check a heading.

## frontend.test.ssr

Now a folder with SSR enabled can ask the harness to serve it with SSR:

```ts [kosmo.config.ts]
export default defineConfig({
  frontend: {
    stack: "react",
    base: "/",
    ssr: true,
    test: {
      ssr: true, // [!code ++]
    },
  },
});
```

The response is then the server-rendered HTML itself,
so a test can fetch it and parse it with something light like Cheerio:

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

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

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

describe(route, () => {
  test(route, async () => {
    const $ = load(await fetch(page.href()).then((r) => r.text()));
    expect($("h1").text()).toBe("Account");
  });
});
```

No browser to install, launch or wait for.

## Off by default

`test.ssr` defaults to `false`, and that is the right default for most suites.

The dev server starts much faster,
and a browser renders the same page from either server,
so a test that drives Playwright gains nothing from SSR.

Turn it on when the server-rendered HTML is what you want to assert on.
It only has an effect on a folder with SSR enabled.

## Vue: hydration that did not hydrate

A server-rendered page is meant to be hydrated in the browser:
Vue takes over the markup the server sent, instead of rendering it again.

That only happens for an app created with `createSSRApp`.
The client router created it with `createApp`,
and for such an app `mount(root, true)` quietly ignores the `true` and renders from scratch.

So every SSR page in a Vue folder was rendered twice:
once on the server, then again in the browser, throwing the server's markup away.
The page looked right, which is why it went unnoticed,
but the work SSR saved was done anyway, and slow devices could see the content flash.

The client router now uses `createSSRApp`, so hydration actually hydrates.

## Vue: a root route made of optional params

A top-level route with only optional params, say `pages/{filter}/index.vue`,
should match both `/` and `/active`.

With no params given, its path compiles to an empty string.
The Vue router runs with `strict: true`, and a strict route with an empty path does not match `/`.
So `/active` worked, and the home page did not.

Strictness is now relaxed for that one route,
so it matches `/` again, and every other route is ranked as strictly as before.
