Go to content
.

I'm currently working on an application that uses Preact and preact-iso for server-side rendering and routing. preact-iso provides the useLocation() hook to access the current URL in a component. When testing such a component I need to mock the location, but I was having troubles figuring out how I'm able to do this.

After reading the source code of LocationProvider component I found an undocumented and wrong way to achieve this, but after creating an issue Ryan Christian, one of the Preact developers, pointed me to the correct solution.

But let's start at the beginning. We have a component that needs access to the current URL, for example, to check if a given URL is currently active:

export function Link({ href, children }) {
  const { url } = useLocation();

  const isActive = url === href;
  return (
    <a href={href} className={isActive ? 'active' : ''}>
      {children}
    </a>
  );
}

To test the logic in this component, we need to write two tests: one to check if the given href is active and a negative case. I am using vitest and @testing-library/preact, but this should work with all testing frameworks.

test('Link should render active link with class', () => {
  render(
    <LocationProvider>
      <Link href="/active">My link</Link>
    </LocationProvider>
  );

  const link = await screen.findByRole('link');
  expect(link).toHaveClass('active');
});

test('Link should render not active link without class', () => {
  render(
    <LocationProvider>
      <Link href="/not-active">My link</Link>
    </LocationProvider>
  );

  const link = await screen.findByRole('link');
  expect(link).not.toHaveClass('active');
});

In order to make both tests pass, we need to mock the location by using the snippet that Ryan suggested to me:

function mockLocation(path) {
  globalThis.location = {};
  const u = new URL(path, 'http://localhost');
  for (const i in u) {
    try {
      globalThis.location[i] = /to[A-Z]/.test(i) ? u[i].bind(u) : String(u[i]);
    } catch {}
  }
}

Now we can set the location before each test:

test('Link should render active link with class', () => {
  mockLocation('/active');
  // Rest of the test...
});

test('Link should render not active link without class', () => {
  mockLocation('/active');
  // Rest of the test...
});

One thing that is super important to get this to work is that we have to wrap every component in LocationProvider since the provider is the one that actually picks up the location from the browser API. If we have a lot of tests that depend on the location and need to be wrapped in the provider, we can create a custom render function that always wraps the component in LocationProvider:

import { render } from '@testing-library/preact';

function renderWithLocation(ui, options = {}) {
  return render(ui, { wrapper: LocationProvider, ...options });
}

Our final test code is readable, stable, and allows us to test the full functionality of our application:

test('Link should render active link with class', () => {
  mockLocation('/active');
  renderWithLocation(<Link href="/active">My link</Link>);

  const link = await screen.findByRole('link');
  expect(link).toHaveClass('active');
});

test('Link should render not active link without class', () => {
  mockLocation('/active');
  renderWithLocation(<Link href="/not-active">My link</Link>);

  const link = await screen.findByRole('link');
  expect(link).not.toHaveClass('active');
});