skip to content
walterra.dev

I spent some time on a side project called node-html2img-render-server, a node server using Playwright to convert HTML to images. The primary use case is for consistent visual regression testing. I started using it for my react-milestones-vis component library.

Visual regression testing can be painful: Browser inconsistencies and environment differences make comparing snapshots flaky. node-html2img-render-server tries to solve this by offloading image rendering to a service so you’re rendering always in the same environment. The server takes HTML, CSS, and optional JavaScript via a simple REST API, renders it using Playwright, and returns a screenshot.

// Example: Testing a component with Jest + jest-image-snapshot
test('component renders correctly', async () => {
const response = await axios.post(
`http://localhost:3000/render?apiKey=${apiKey}`,
{
html: '<div class="card">Product Title</div>',
css: '.card { border: 1px solid #ccc; }',
viewport: { width: 500, height: 300 }
},
{ responseType: 'arraybuffer' }
);
const buffer = Buffer.from(response.data);
expect(buffer).toMatchImageSnapshot();
});

So far it offers a simple API key authentication. For my react-milestones-vis library, this server now provides consistent visual testing across different environments. That visualization requires a more advanced setup which I was also able to solve with the renderer: The layout algorithm to remove label overlap requires a fully working DOM, that’s why I never could create tests with just jsdom. For the visual regression tests, I’m passing on a custom rollup UMD build of the library that renders different examples. So under the hood Playwright runs the whole thing and returns the screenshot once done. This will finally allow me to work on layout improvements with more confidence to avoid breaking existing edge cases.

The render service supports Elastic’s OpenTelemetry SDK too: https://github.com/walterra/node-html2img-render-server/blob/main/docs/observability.md

You can run the service via npx node-html2img-render-server or Docker. It’s actually the first time I published a docker image: https://hub.docker.com/r/walterra/node-html2img-render-server

Currently I’m running the service for my own needs on a small Hetzner VM (Docker CE), they have a nice CLI to manage servers: https://github.com/hetznercloud/cli.

To use it for regression testing on a project and run it via CI, I set its URL and API-KEY as secret environment variables for Github Actions like this: https://github.com/walterra/react-milestones-vis/blob/main/.github/workflows/tests.yml

- name: Run visual tests
env:
HTML2IMG_RENDER_URL: ${{ secrets.HTML2IMG_RENDER_URL }}
HTML2IMG_API_KEY: ${{ secrets.HTML2IMG_API_KEY }}
run: yarn test:visual

Here’s a screenshot after capturing some telemetry in Elastic’s APM UI:

Screenshot of an Elastic Observability latency distribution dashboard monitoring a node-html2img-render-server on Hetzner Docker CE. The graph shows 33 total transactions with most response times under 700ms and a 95th percentile around 800ms. The trace sample details show a 670ms POST /render request with middleware components including jsonParser (442μs), query (102μs), expressInit (92μs), and helmetMiddleware (73μs). The transaction received a 200 OK response.

Why do this anyway? I found some older similar projects, but they used Puppeteer. And I wanted something with telemetry instrumentation ootb. It was also a good opportunity to give Claude Code a try for a new project. Definitely an interesting learning experience. Claude Code was great for creating the overall boilerplate, like custom scaffolding on steroids. But it struggled with some stuff and went in circles (or back and forth) munching tokens on the trickier parts.

Here’s again the links to the mentioned repo’s, leave a star if you want to keep track of updates: