GioJSdocs
On this page

callRoute

Call a route handler or a page action in your test process, with any method and body, and read the answer like a fetch Response.

ts
import { callRoute } from '@gio.js/core/testing';

const res = await callRoute('/api/notes', { method: 'POST', body: { text: 'hi' } });

Reference

callRoute(path, options?). It takes every option of renderPage (appDir, headers, cookies, query, locale), plus:

OptionTypeDefaultDescription
methodstring'GET'Any HTTP method; upper-cased for you.
bodystring | Uint8Array | URLSearchParams | object | unknown[] | null-How it is sent, unless headers sets a content-type: a string as text/plain;charset=UTF-8; URLSearchParams as application/x-www-form-urlencoded; a Uint8Array as raw bytes with no content type; any other value as JSON with application/json.

Returns

A Promise<RouteResponse>:

FieldTypeDefaultDescription
statusnumber-The response status.
headersRecord<string, string>-Response headers, lowercase - without the ones the Rust server adds.
setCookiesstring[]-Every Set-Cookie value, in order.
text()Promise<string>-The body as text. For an event stream, it resolves once the handler closes the stream.
json()Promise<T>-The body parsed as JSON.
bytes()Promise<Uint8Array>-The raw body.
streamReadableStream<Uint8Array> | null-Event streams only: the events as a client receives them. null otherwise.
error{ message, digest?, stack? } | undefined-Set when a page render failed and no error.tsx answered.

Behavior

The request goes through the same code as on the server, so the answers match:

  • A returned value is JSON 200; null or undefined is 204; a Response passes through.
  • notFound() is a JSON 404; a throw is a JSON 500 with a digest; a method the file does not export is 405.
  • req.json() on a non-JSON body is 415; a broken form body is 400.
  • A POST to a page runs its action: you get the redirect, or the re-rendered page as HTML.
  • A GET to a page renders it, like renderPage.

Examples

JSON and form bodies

Against the /api/notes handler from Request body errors, which accepts JSON and a form:

tests/notes.test.ts
import { expect, it } from 'vitest';
import { callRoute } from '@gio.js/core/testing';

it('accepts JSON and form posts', async () => {
  const json = await callRoute('/api/notes', { method: 'POST', body: { text: 'hi' } });
  expect(json.status).toBe(201);
  expect(await json.json()).toEqual({ saved: 'hi' });

  const form = await callRoute('/api/notes', { method: 'POST', body: new URLSearchParams({ text: 'form' }) });
  expect(form.status).toBe(201);
});

it('refuses a text/plain body', async () => {
  const res = await callRoute('/api/notes', { method: 'POST', body: 'plain' });
  expect(res.status).toBe(415);
});

A page action

Against the contact page from redirect, whose action redirects with a session cookie or re-renders with a 422:

ts
it('redirects after a valid contact form', async () => {
  const res = await callRoute('/contact', {
    method: 'POST',
    body: new URLSearchParams({ email: 'ada@example.com' }),
  });
  expect(res.status).toBe(303);
  expect(res.headers.location).toBe('/contact/thanks');
  expect(res.setCookies[0]).toMatch(/^gio_session=/);
});

it('re-renders with a 422 for an invalid email', async () => {
  const res = await callRoute('/contact', { method: 'POST', body: new URLSearchParams({ email: 'nope' }) });
  expect(res.status).toBe(422);
  expect(await res.text()).toContain('Enter a valid email address.');
});

Event streams

For a handler that returns a GioEventStream, read stream piece by piece, or text() for a stream the handler closes. Cancelling the reader runs the handler's cleanup function, like a client disconnecting. Here /api/clock is the handler from GioEventStream.

ts
it('ticks', async () => {
  const res = await callRoute('/api/clock');
  expect(res.headers['content-type']).toBe('text/event-stream');
  const reader = res.stream!.getReader();
  const { value } = await reader.read();
  expect(new TextDecoder().decode(value)).toContain('event: tick');
  await reader.cancel();   // runs the cleanup
});

Good to know

  • No Rust in between. CSRF checks, rules, guards, rate limits and [server] max_body_bytes are not applied - so a cross-site POST succeeds here. Use createTestServer to test them.
  • No cache, no WebSocket server. revalidateTag and revalidatePath resolve ok: false, and broadcast returns false.
  • Sessions work. Without a configured secret, a random GIO_SESSION_SECRET is set for the test process; pass the cookie back with cookies to act as the signed-in user.
  • A route.ts that throws on import answers 500 for every method, as on the server.

Version history

VersionChanges
v0.1.0-beta.8Introduced.