Test a component in a real browser
Use @markless/vitest-browser to mount a component in the browser with render(), or render it on a server with renderSSR() and click the HTML.
Types pass. But a type check never clicks a button.
@markless/vitest-browser runs your component in a real browser, and your test clicks it. The package is experimental.
The compiler turns Counter.tsrx into code that renders in many places. A test is one more place. The package gives you two ways in. Mount the component in the browser. Or render it to HTML on a server, then click that HTML.
A bug can hide in one way and not in the other. So write the check once and run it both ways.
Pick a way in. Then press Run the test.
What does a test do with your component?
Pick a way in. Press Run the test, or drag the timeline. Watch each step.
Vitest opens a real browser page for the test. The page is empty.
Empty page.
import { render, renderSSR } from '@markless/vitest-browser';import { expect, test } from 'vitest';import Counter from './Counter.tsrx'; async function clickCounts(container: HTMLElement) { const button = container.querySelector('button')!; button.click(); await expect.poll(() => button.textContent).toBe('Count 1');} test('mounted in the browser: a click counts', async () => { (this test) const screen = await render(Counter); await clickCounts(screen.container as HTMLElement);}); test('rendered on a server: a click counts', async () => { const screen = await renderSSR(Counter); await clickCounts(screen.container);});- Nothing yet. The test has just started.
Simplified. Steps follow @markless/vitest-browser and the test file on this page. The test server belongs to the test run. Your app does not need a server. The package is experimental.
Notice that the last three steps are the same both ways. Only the start differs: where your component runs.
| Helper | What it does |
|---|---|
render(Counter) |
Mounts Counter in the browser, the way an app without a server does. |
renderSSR(Counter) |
Renders Counter to HTML on the test server, then puts that HTML in the page. |
Set it up
The starters do not include this setup yet, and the package has no README. This configuration comes from the Markless repo’s own test projects.
npm install -D @markless/vitest-browser vitest playwrightpnpm add -D @markless/vitest-browser vitest playwrightyarn add -D @markless/vitest-browser vitest playwrightbun add -D @markless/vitest-browser vitest playwrightnub add -D @markless/vitest-browser vitest playwrightaube add -D @markless/vitest-browser vitest playwrightnpx playwright install chromium
import { markless } from '@markless/core/vite';
import { testSSR } from '@markless/vitest-browser/ssr-plugin';
import { playwright } from 'vite-plus/test/browser-playwright';
import { defineProject } from 'vitest/config';
export default defineProject({
plugins: [testSSR(), markless()],
test: {
include: ['src/**/*.browser.ts'],
browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: 'chromium' }] },
},
});
Put testSSR() before markless(). Without it, renderSSR() throws.
One check, both ways
Take the counter from the starter app.
import { state } from '@markless/core';
export default function Counter() @{
let count = state(0);
<button onClick={() => count++}>Count {count}</button>
}
Write the check once. Then run it both ways.
import { render, renderSSR } from '@markless/vitest-browser';
import { expect, test } from 'vitest';
import Counter from './Counter.tsrx';
async function clickCounts(container: HTMLElement) {
const button = container.querySelector('button')!;
button.click();
await expect.poll(() => button.textContent).toBe('Count 1');
}
test('mounted in the browser: a click counts', async () => {
const screen = await render(Counter);
await clickCounts(screen.container as HTMLElement);
});
test('rendered on a server: a click counts', async () => {
const screen = await renderSSR(Counter);
await clickCounts(screen.container);
});
The package removes each mounted component after each test. Call cleanup() yourself only in the middle of a test.
Next: A build failed with a long code in capital letters. What is it telling you? Diagnostics →
