This project uses a unified server-side rendering helper renderSSR for all SSR-only tests.
React 19 changes (notably around hydration and render APIs) made a bespoke ServerRenderer abstraction harder to maintain. renderSSR:
- Wraps
renderToStringand sets up a JSDOM document with a fixed URL (enables storage APIs) - Binds all
@testing-library/domqueries to the server-generatedcontainer - Preserves leading
<link>preload tags for hydration fidelity - Returns a familiar query API (getByRole, getByText, queryByTestId, etc.)
const { container, getByRole, getByText } = renderSSR(<MyComponent />);
expect(getByRole('heading', { level: 2 })).toBeTruthy();interface SSRRenderResult {
container: HTMLElement; // <body> element containing server markup
html: string; // Raw server HTML string
prettyDOM: (...args) => string;// Pretty-printer wrapper
// Bound testing-library queries (getByRole, queryByText, etc.)
}Unlike client @testing-library/react render, there is no unmount and no rerender (SSR output is static). For re-renders just call renderSSR again.
Server output often splits inline formatting across nodes (<strong>, <em>, etc.). Exact getByText('Full sentence with bold word') calls can fail because the text is not a single text node. Prefer one of:
// 1. Substring match
expect(container.textContent).toContain('Full sentence');
// 2. Regex match
expect(getByText(/Full sentence/)).toBeTruthy();
// 3. Target the formatted segment explicitly
aexpect(getByText('bold word').tagName).toBe('STRONG');For repetitive substring assertions you can import the lightweight helper:
import { assertContainsText } from '../../test-utils/assertContainsText';
assertContainsText(container, 'Important notice');The helper normalises whitespace and gives a clearer error message.
Hydration validation should be in dedicated *.hydration.test.tsx files. Pattern:
const { html, container } = renderSSR(<HeaderSSR {...props} />);
const wrapper = document.createElement('div');
wrapper.innerHTML = html; // initial server markup
hydrateRoot(wrapper, <HeaderSSR {...props} />);Keep preload <link> tags intact at the start of html, as they are part of the fidelity contract.
Do:
- Use bound queries (getByRole) for accessibility-oriented assertions
- Use
container.querySelectorfor structural hooks (.nhsuk-* classes) - Prefer substring / regex for long or mixed-format text
Avoid:
- Using legacy
ServerRenderer(removed) - Depending on
unmountorrerender - Over-destructuring unused queries (keep it minimal)
// Print nicely formatted DOM
console.log(prettyDOM());
// Raw HTML for diffing
console.log(html);If a getByText fails, inspect container.innerHTML and adjust to a substring/regex or target inner element.
All tests now use renderSSR; legacy ServerRenderer was removed. The transitional alias export render was also dropped.
Feel free to extend this guide with streaming SSR or hydration mismatch troubleshooting in future iterations.