@k8ordo/router

Testing

This router mocks nothing: it uses the browser’s Navigation API and URLPattern as they are. This page covers what can be checked without a browser, and what to watch for when checking navigation inside one.

On this page

Check the route table without a browser

The route table defineRoutes returns matches a path with match. It is a plain function, so which page a path lands on, the params it takes and the order patterns match in can all be checked without a browser.

src/routes.test.ts
import { expect, it } from 'vitest';

import { routes } from './routes';

it('sends /products/new to its own page', () => {
  const found = routes.match('/products/new');
  expect(found?.pattern).toBe('/products/new');
});

it('reads the id of a product page', () => {
  const found = routes.match('/products/42');
  expect(found?.params).toStrictEqual({ id: '42' });
});

matchPath is likewise a function you can call without a browser. Code that marks a link by where you are can take the path as an argument, and be checked along with matchPath.

Change pages inside a browser

A test that renders <Router> and changes pages runs inside a browser with the Navigation API. This package’s own tests use Vitest’s browser mode on Chromium, Firefox and WebKit.

src/app.browser.test.tsx
import { navigateTo, Router } from '@k8ordo/router';
import { expect, it } from 'vitest';
import { render } from 'vitest-browser-react';

import { routes } from './routes';

it('shows the product page once finished resolves', async () => {
  await render(<Router routes={routes} />);

  await navigateTo('/products/:id', { id: '1' }).finished;

  const heading = document.querySelector('h1');
  expect(heading?.textContent).toBe('Product 1');
});

finished resolves once the new page is on screen, so after awaiting it the screen can be checked straight away, with no retrying.

A page’s useEffect runs after it is on screen, though. To check what an effect did, use a form that retries until it passes, such as expect.element or vi.waitFor.

Intercept navigations outside the table yourself

A navigation <Router> does not take becomes an ordinary page load. Inside a test, that moves the very page the tests run in somewhere else.

Moving to a URL outside the table before a test, or back to the original URL afterwards, is exactly that. For those navigations only, the test intercepts the navigate event itself.

src/app.browser.test.tsx
const interceptEverything = (event: NavigateEvent) => {
  if (event.canIntercept) event.intercept();
};

const navigateOutside = async (url: string) => {
  navigation.addEventListener('navigate', interceptEverything);
  try {
    await navigation.navigate(url, { history: 'replace' }).finished;
  } finally {
    navigation.removeEventListener('navigate', interceptEverything);
  }
};

let home: string;

beforeEach(() => {
  home = location.href;
});

afterEach(async () => {
  await navigateOutside(home);
});
Information

Note

Once <Router> is rendered, it takes navigations to paths in the table itself; the test has nothing to intercept there.

Check back and forward in a top-level page

Vitest’s browser mode runs a test inside an iframe. There, Firefox and WebKit do not restore the scroll position on back and forward, and Firefox runs a traversal’s handler twice.

This package therefore checks going back and forward only in a top-level page it opens with Playwright. To check the scroll position after going back, do the same and leave the iframe.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2