# Screenshot tests without an emulator

> viddik renders Compose Multiplatform through a real Compose Desktop window instead of LayoutLib, so goldens stay the same on macOS, Linux and Windows.

Page: https://kotlin.website/blog/screenshots-without-an-emulator · Published: 2025-10-19

Screenshot testing on Android has a good answer — Paparazzi renders through LayoutLib, no
emulator, fast. Compose Multiplatform does not, because LayoutLib is Android's renderer and
the code under test is not Android code.

[viddik](https://github.com/youndie/viddik) takes the other route: render through a real
Compose Desktop window on Skiko, on a plain JVM. No emulator, no AVD, and — this is the
part that matters — the same renderer that will actually draw the component.

## One annotation, two outputs

```kotlin
@ViddikScreenshot
@Composable
fun EmptyBasket() = BasketScreen(state = Basket.empty)
```

A KSP processor collects every annotated composable into a registry, and that registry is
consumed twice.

`ViddikEngine` captures each one to PNG and diffs it against the golden, in record or
verify mode, as a JUnit 5 test. `ViddikShowroom` takes the *same* registry and renders it
as a live, browsable component gallery — the Showkase half of the idea, without a second
set of declarations to keep in sync.

That is the reason the two features live in one library. A component browser that drifts
from the tests is worse than no browser at all.

## Three artefacts, and why

The module split is not organisational tidiness; it is about what may appear on which
classpath.

| module | what it is | where it may go |
|---|---|---|
| `viddik-annotations` | the `@ViddikScreenshot` marker, `ViddikComponent`, `ViddikShowroom` | anywhere, including `android()` |
| `viddik-processor` | KSP codegen — registry and generated tests | a KSP configuration |
| `viddik-testing-core` | capture, diff and record, on Compose Desktop + JUnit 5 | `test` only, never `main` |

The annotations artefact is deliberately tiny because it is the one that has to be safe to
depend on from production source sets. The capture engine drags in Compose Desktop and
JUnit, and has no business anywhere near an app's `main`.

## The one that catches everyone

Wiring KSP for a KMP target is where the time goes, and the failure is quiet — the
processor simply does not run and your registry is empty.

```kotlin
dependencies {
    testImplementation("ru.workinprogress:viddik-annotations:<VERSION>")
    testImplementation("ru.workinprogress:viddik-testing-core:<VERSION>")
    add("kspDesktopTest", "ru.workinprogress:viddik-processor:<VERSION>")
}
```

The `kspDesktopTest` configuration name is derived from your target name. Name your JVM
target something else and that string changes with it; use a plain `kotlin("jvm")` project
and it is `kspTest`, with per-target artifact coordinates instead of the base ones. There
is no error for getting this wrong, only zero generated tests — which is exactly the shape
of failure worth knowing about in advance.

## Goldens that travel

The reason to care about the renderer is that goldens get committed, and then somebody
else runs the suite.

Because the capture goes through Compose Desktop rather than a platform toolkit, the same
golden holds on macOS, Linux and Windows. That is what makes goldens reviewable in a pull
request instead of a source of noise every time CI changes machines.

---

Source: [github.com/youndie/viddik](https://github.com/youndie/viddik)
