Testing the Actual Processing Path in a Browser-First Image Tool
In the first two articles of Building Piclume: Browser-First Image Tools, I described the product decision to process common image jobs in the browser and the routing contract that sends compatibility-heavy jobs to Sharp on the server.
That architecture creates a testing problem that a simple upload test does not solve.
An image can reach a successful download through more than one engine. A page can show a “browser-first” capability badge while the uploaded file actually uses the server. A mixed batch can legitimately use both paths. And a conversion can succeed even when its output is larger than the input.
For Piclume, the useful question is not only:
Did the user get a file?
It is also:
Did the file take the processing path that the application intended, and does the downloaded result have the format and metadata that path promised?
This article shows how the current test suite answers that question.
Test the contract at the workspace boundary
The application has two related but different pieces of processing information:
- a route-level description, such as “Browser + server”;
- the engine used for a particular completed result, such as “Browser” or “Server”.
The route-level description helps explain what a tool is designed to support before a file is selected. The result-level label is evidence about what happened for this upload. The distinction matters for a hybrid route.
For example, /compress-image accepts common browser-friendly formats as well as AVIF. A JPG can finish in the browser, while an AVIF file can use the server compatibility path. The route is hybrid, but an individual result still has one actual processor.
The client router keeps that information in the result object:
return {
processor: "browser",
blob: processed.blob,
filename: buildFileName(options.file.name, processed.outputFormat),
originalSize: options.file.size,
processedSize: processed.blob.size,
outputFormat: processed.outputFormat,
originalFormat: processed.originalFormat,
width: processed.width,
height: processed.height,
};
The server path returns the same kind of metadata through response headers. The workspace can therefore render one result UI while the tests assert the engine that actually produced each file.
Build fixtures at test time
The Playwright suite generates image fixtures with Sharp inside each test's output directory. The repository does not need to store binary test images just to verify the upload workflow.
That choice makes the fixture intent visible in the test and gives format-sensitive assertions a reliable input. The current suite creates JPG, PNG, WebP, AVIF, and an AVIF fixture with alpha data.
The important property is not that the fixture resembles a real photograph. It is that its format, dimensions, and transparency characteristics are deliberate. When the test downloads a result, Sharp can inspect the output instead of relying on a browser preview.
The automated test flow
The happy path still matters, but it should include the processing contract and the downloaded artifact:
The shared helper encodes the UI part of this contract:
export async function expectConversionResult(page, expectedEngine, expectedDownloads) {
await expect(page.getByTestId("processing-status")).toHaveText("Ready to download");
await expect(page.getByTestId("processing-engine")).toHaveText(expectedEngine);
await expect(page.getByTestId("result-download")).toHaveCount(expectedDownloads);
await expect(page.getByTestId("result-download").first()).toBeVisible();
}
This helper deliberately checks three things: the final state, the engine label, and the number of results. A visible download button by itself is too weak an assertion for a hybrid pipeline.
Cover one browser path and one server path
The browser tests use routes where the policy allows local processing. A JPG-to-PNG test verifies that a JPG input can be decoded, drawn into Canvas, exported as PNG, and downloaded from the browser session. A PNG-to-WebP test checks the same shape while also asserting that the output remains four-channel when the source is transparent.
The test does not need to inspect internal browser functions. It observes the result contract and then checks the downloaded file:
await expectConversionResult(page, "Browser", 1);
const outputPath = testInfo.outputPath("png-to-webp.webp");
await saveDownloadResult(page, page.getByTestId("result-download").first(), outputPath);
await expect(sharp(outputPath).metadata()).resolves.toMatchObject({
format: "webp",
width: 160,
height: 120,
channels: 4,
hasAlpha: true,
});
The server test uses AVIF-to-PNG. AVIF is outside the browser-first input set, so the client sends a FormData request to the Next.js route. The route validates the file, calls the shared processImage function, and returns the processed binary with Cache-Control: no-store and metadata headers.
That test asserts both Server and the PNG alpha channel. A preview that looks transparent is not enough evidence that the downloaded PNG has retained an alpha channel.
Mixed batches are a first-class case
The most revealing test is a mixed batch. The user selects an AVIF with alpha and a JPG for a route that can accept both. The application evaluates each file independently:
The test checks the aggregate label and the individual result labels:
await expectConversionResult(page, "Browser + server", 2);
await expect(page.getByTestId("result-engine")).toHaveText(["Server", "Browser"]);
This catches a class of regression that single-file tests miss. A change that accidentally sends every file through the server could still produce valid outputs, but it would violate the browser-first routing policy. A change that labels every batch as Browser could hide an actual compatibility upload.
Test boundaries instead of file extensions alone
The routing decision depends on more than the input extension. shouldPreferBrowserProcessing checks whether the code is running in a browser, whether the input MIME type is in the browser-supported set, whether the requested output can be exported by the current browser, and whether product policy keeps a route on the server.
The policy intentionally keeps PNG compression on the server. Canvas can export a PNG, but a generic canvas re-export is not the same as a controlled PNG compression strategy. The test matrix therefore treats compress-png as a server case even though PNG is a browser-readable format.
There is also a runtime fallback. A capability check can pass while decoding or export still fails for a particular file or device. The client catches that failure and retries through the server path. A complete test strategy should eventually include a forced browser failure to verify that the fallback changes the result engine rather than leaving the user at an error state.
Keep real-browser checks for the hard formats
The automated Playwright suite intentionally does not replace every manual check. HEIC upload behavior depends on the browser surface and the local file chooser, so the release checklist still uses Chrome with the Codex extension's file access permission enabled.
That split is useful:
- Playwright gives repeatable format, engine, download, and metadata assertions.
- Real Chrome checks file chooser behavior and the HEIC compatibility boundary.
The two surfaces answer different questions. Treating them as interchangeable would either weaken the automated assertions or pretend that browser-specific upload behavior is fully covered when it is not.
What the test suite does not claim
The current tests do not claim that every conversion reduces file size. Converting AVIF to JPG or JPG to PNG can produce a larger file, and that can still be a correct conversion. The tests prioritize status, engine, output format, dimensions, transparency where relevant, and download availability.
They also do not claim that a successful server response creates a persistent file library. The API returns the current job's binary result and uses no-store response headers. The product intentionally keeps the upload → process → download loop small: no login, database, history, or background queue is required for the normal workflow.
The larger lesson
A hybrid image tool needs tests that understand its routing policy. “The button appeared” is not enough, and “the file downloaded” is only the beginning.
The useful test contract is:
- The workspace reaches
Ready to download. - The displayed processor matches the actual path selected for that file.
- A download exists for every accepted input.
- Sharp confirms the format, dimensions, and important channel metadata.
- Mixed batches preserve per-file engine information while exposing an aggregate status.
Those assertions make the architecture observable. They also make future changes easier to evaluate: if a new format, encoder, or routing rule changes the processing path, the failure points to the contract that changed instead of leaving the team to infer what happened from a screenshot.
You can try the current workflow at Piclume, or start with Part 1, Why I Built a Browser-First Image Tool with Next.js, for the architectural constraints behind the product.


Top comments (0)