GioJSdocs
On this page

renderPage

Render a page in your test process through the worker's own pipeline, and inspect its status, HTML, props, cookies, redirect and cacheability.

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

const page = await renderPage('/posts/1');

Reference

renderPage(path, options?). path is an absolute path, query string allowed (/posts?page=2).

OptionTypeDefaultDescription
appDirstringGIO_APP_DIR, else ./appThe app/ directory, resolved from the working directory.
method'GET' | 'HEAD''GET'Other methods throw - use callRoute to post to a page action.
headersRecord<string, string>-Request headers; names are case-insensitive. host defaults to localhost.
cookiesRecord<string, string>-Sent as the Cookie header, after any cookie in headers. Values are sent as given.
queryRecord<string, string>-Merged over the query string in path.
localestring''The locale [i18n] detection would have picked. No detection runs: for /fr/about, pass renderPage('/about', { locale: 'fr' }).

Returns

A Promise<RenderPageResult>:

FieldTypeDefaultDescription
statusnumber-The status the worker answered with.
headersRecord<string, string>-The worker's response headers, lowercase. The Rust server adds its own (security headers, Cache-Control, X-Gio-Cache) on top - those are not here.
setCookiesstring[]-Every Set-Cookie value, in order.
htmlstring-The rendered document, with its hydration envelope and stylesheet links; '' for HEAD.
propsRecord<string, unknown> | null-The hydration props as serialized into the page. null for a redirect, a 404, an error, or props that are not JSON-serializable.
cacheableboolean-Whether the server would store the response in its shared page cache: revalidate set, no cookies sent, no credentials read.
cacheMaxAgenumber-Seconds it would be kept; 0 when not cacheable.
cacheTagsstring[]-The tags it would be stored under for revalidateTag(): export const tags plus the ones getServerSideProps returned, validated and de-duplicated. Empty when not cacheable.
redirect{ destination: string; permanent: boolean } | undefined-Set for a 3xx answer; permanent for 301 and 308.
error{ message: string; digest?: string; stack?: string } | undefined-Set when the render failed and no error.tsx answered (status 500). message is generic in production; stack is set in development only.

Behavior

  • On the first call per app directory it discovers your routes, layouts, route.ts handlers, not-found and error files, metadata routes and gio.config.ts plugins (running their onStartup), and loads the project's .env files - never over a variable already set. A route conflict fails the call like it fails the worker's boot.
  • getServerSideProps, layouts, notFound(), redirect() and error.tsx files answer exactly as on the server. Nothing is cached between calls.
  • Not applied, because the Rust server does them: gio.toml and middleware.ts rules and guards, CSRF checks, rate limits, security headers, the page cache, compression and locale detection. Test those with createTestServer.
  • The test runs in production mode unless NODE_ENV=development (vitest sets test): error pages show only a digest, and the message is on the ssr render failed log line.
  • With no GIO_SESSION_SECRET in the environment or a .env file, discovery sets a random one for the test process, so session modules load.

Errors

  • A method other than GET or HEAD: TypeError.
  • A page that answers with an event stream: TypeError - read it with callRoute.
  • A path that does not start with /, or one the server would refuse with 400 before any page sees it (a . or .. segment, a stray %): TypeError.
  • An invalid cookie name, or a cookie value with ;, CR or LF: TypeError.

Examples

Props, tags and cacheability

For the cached, tagged /posts/[id] page from revalidateTag:

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

it('renders a cached, tagged post', async () => {
  const page = await renderPage('/posts/1');
  expect(page.status).toBe(200);
  expect(page.props).toEqual({ post: { id: '1', title: 'Hello', slug: 'hello' } });
  expect(page.cacheable).toBe(true);
  expect(page.cacheMaxAge).toBe(60);
  expect(page.cacheTags).toEqual(['posts', 'post:1']);
});

it('404s an unknown post', async () => {
  const page = await renderPage('/posts/999');
  expect(page.status).toBe(404);
  expect(page.props).toBeNull();
});

For the /dashboard guard from redirect and the /welcome page from Cookie helpers:

ts
it('sends visitors without a session to /login', async () => {
  const page = await renderPage('/dashboard');
  expect(page.status).toBe(302);
  expect(page.redirect).toEqual({ destination: '/login?next=%2Fdashboard', permanent: false });
});

it('greets a returning visitor', async () => {
  const page = await renderPage('/welcome', { cookies: { seen: '1' } });
  expect(page.props).toEqual({ firstVisit: false });
  expect(page.cacheable).toBe(false);   // it read and set cookies
});

Good to know

  • Modules are imported once per process. resetTestApp re-runs discovery for added or removed files, but an edit to an imported module shows up only in a new test process.
  • Assert on props rather than on HTML text: React separates adjacent text with <!-- --> in server HTML.
  • CSS Modules render the server's class names under node:test; under vitest add gioVitest().
  • No client bundles are built. The page's bootstrap script points at TEST_ENTRY_SCRIPT (/_gio/testing/entry.js, also exported from @gio.js/core/testing) instead of a real chunk; the hydration envelope that props is read from is the real one.
  • Server-only. @gio.js/core/testing imports the server-only marker: a page or component that imports it has its client bundle refused.

Version history

VersionChanges
v0.1.0-beta.8Introduced.