Files
offscale/web/ServersView.test.tsx
pastilhas 58fbf12e4b the picker's register link opens the form
reported as "I click register a server and nothing happens", and nothing was broken. three controls on
that screen carry the label. two are buttons that open the form; the one in the server picker — top left,
where you look first — was a plain <Link> to the servers section.

/offscale and /offscale/servers render the same view (useOffscaleSection defaults to servers when there is
no param), so from either one the click navigated to something identical. the URL moved and nothing else
did, and the only visible result was a second button with the same label further down the page.

so the link now carries ?register=1 and ServersView consumes it. the flag is stripped with replace on the
way in, which is not tidiness: left in the URL, closing the form and clicking the picker again would be a
navigation to the URL already showing, and nothing would happen — the same bug, restored.

ServersView.test.tsx pins all three: the form opens on arrival, the flag is consumed, and no flag means no
form. removing the effect fails the first two.
2026-08-17 12:58:47 +00:00

85 lines
3.3 KiB
TypeScript

import type { ReactNode } from 'react';
import { afterEach, describe, expect, test } from 'bun:test';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter, useLocation } from 'react-router';
import { render, screen, waitFor } from '@testing-library/react';
import { ServersView } from './ServersView';
import { offscaleRegisterPath } from './shared';
/**
* Whether "Register a server" opens the form — from every place that offers it.
*
* The bug this pins was reported as "I click register a server and nothing happens", and nothing was
* broken: three controls carry that label and the one in the server picker was a plain link to the Servers
* section. `/offscale` and `/offscale/servers` render the same view, so from the section you were already
* on it changed the URL and nothing else — a control that looks like the primary action and is a no-op.
*
* So the test is about the link's DESTINATION doing something, which is why it renders the view at that
* destination rather than clicking the picker: the picker lives in another panel, and what has to be true
* is that arriving there opens the form.
*
* `fetch` is stubbed rather than `mock.module('hooks/useClient')` — Bun has one module registry per run, so
* that mock is process-wide and permanent, and other files already own it.
*/
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
const stubServers = (servers: unknown[] = []) => {
globalThis.fetch = (async () =>
new Response(JSON.stringify({ servers }), {
status: 200,
headers: { 'content-type': 'application/json' },
})) as unknown as typeof fetch;
};
/** Reports the live URL, so "the flag was consumed" is an assertion rather than an inference. */
const Search = () => <span data-testid="search">{useLocation().search}</span>;
const Wrap = ({ at, children }: { at: string; children: ReactNode }) => (
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
<MemoryRouter initialEntries={[at]}>
{children}
<Search />
</MemoryRouter>
</QueryClientProvider>
);
const mountAt = (at: string) =>
render(
<Wrap at={at}>
<ServersView />
</Wrap>,
);
const formHeading = () => screen.queryByText('Register a Headscale server');
const search = () => screen.getByTestId('search').textContent;
describe('ServersView registration form', () => {
test('the picker link lands with the form already open', async () => {
stubServers([]);
mountAt(offscaleRegisterPath());
await waitFor(() => expect(formHeading()).not.toBeNull());
});
test('the flag is consumed, so clicking the picker link again still works', async () => {
stubServers([]);
mountAt(offscaleRegisterPath());
// Left in the URL, a second click on the picker would be a navigation to the URL already showing —
// which is the original bug, restored.
await waitFor(() => expect(search()).toBe(''));
});
test('without the flag the form stays closed', async () => {
stubServers([]);
mountAt('/offscale/servers');
// The empty state has to have rendered before "no form" means anything.
await waitFor(() => expect(screen.queryByText('No Headscale servers yet')).not.toBeNull());
expect(formHeading()).toBeNull();
});
});