# Automate device renders in CI

Run device rendering after the application has produced and delivered its email. Keep the `emailId` from the same test so your screenshots show the message your application actually sent.

## Set up the test

Install a current `mailslurp-client`, configure its API key on the test runner and select native target IDs from [the catalog](/docs/device-rendering-profiles/). Give the job enough time for message delivery, rendering and artifact downloads. Set the SDK's HTTP timeout above the requested long-wait duration.

Create a private artifact directory before the run. In Node.js, pass an async `saveArtifact(name, data)` function that writes into that directory using `node:fs/promises.writeFile`. Playwright users can attach these files to `testInfo`; other runners can publish the directory after the test, including on failure.

## Render, save evidence and assert completion

```javascript
async function renderEmailInCi(mailslurp, emailId, nativeTargets, saveArtifact) {
  const controller = mailslurp.devicePreviewsController;
  const created = await controller.createDevicePreviewRun({
    emailId,
    createDevicePreviewOptions: { nativeTargets },
  });
  const waited = await controller.waitForDevicePreviewRun({
    runId: created.run.runId,
    timeoutMillis: 10 * 60 * 1000,
    pollIntervalMillis: 5_000,
  });
  const results = await controller.getDevicePreviewRunResults({
    runId: created.run.runId,
  });

  await saveArtifact(
    'render-results.json',
    Buffer.from(JSON.stringify(results, null, 2)),
  );
  for (const screenshot of results.screenshots) {
    if (!screenshot.accessUrl) continue;
    const response = await fetch(screenshot.accessUrl);
    if (!response.ok) throw new Error(`Screenshot download failed: ${response.status}`);
    await saveArtifact(
      `${screenshot.screenshotId}.png`,
      new Uint8Array(await response.arrayBuffer()),
    );
  }

  const incomplete = results.targets.filter((target) => target.status !== 'READY');
  if (waited.timedOut) throw new Error('Device preview timed out');
  if (waited.run.status !== 'COMPLETE' || incomplete.length) {
    throw new Error(`Device preview incomplete: ${incomplete.length} target(s) not ready`);
  }
  return results;
}
```

Call `renderEmailInCi(mailslurp, email.id, nativeTargets, saveArtifact)`. This example treats every requested target as required. A `PARTIAL_COMPLETE` result saves available screenshots and fails the check. A timed-out wait also fails, even when some screenshots exist.

[API endpoint: `waitForDevicePreviewRun`](/docs/api/#waitForDevicePreviewRun)

[API endpoint: `getDevicePreviewRunResults`](/docs/api/#getDevicePreviewRunResults)

## Diagnose a failed target

Inspect target status, failure code and run warnings in `render-results.json`. Differentiate a layout defect in a completed screenshot from a client capture that never completed. Rerun selected failed targets when appropriate and keep the original result with the retry evidence.

[API endpoint: `rerunDevicePreviewTargets`](/docs/api/#rerunDevicePreviewTargets)

If your job is cancelled, decide whether to cancel the remote run too. A test-runner timeout does not itself prove that remote rendering stopped.

## Compare an approved baseline

Store the approved run and client configuration in your test setup. Download baseline and current screenshots, match the same client/platform/color-scheme context, and use your image comparison tool to review the difference. Run-specific target UUIDs identify screenshots within a run; do not assume they are stable across separate runs.

Mask or normalize intentional dynamic content in your comparison process. Require a person to approve a new baseline rather than replacing it automatically whenever the check fails. Use [share links and comments](/docs/device-rendering-review/) for that review, and add [Email Audit](/docs/email-audit/) for links, content and accessibility findings.
