youndie@kotlin.website:~/src$ cat viddik/README.md

viddik

compose multiplatform · ksp · testing
Screenshot testing for Compose Multiplatform that renders through a real Compose Desktop window, not LayoutLib. Fixtures are declared with @Preview and buy a golden-file test and a live component browser; goldens hold across macOS, Linux and Windows.

Why it exists

Android has Paparazzi, which renders through LayoutLib. Compose Multiplatform does not, because LayoutLib is Android's renderer and the code under test is not Android code. viddik renders through a real Compose Desktop window on Skiko, on a plain JVM — the same renderer that will actually draw the component.

One annotation, two outputs

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

A KSP processor collects every annotated composable into a registry, and that registry is used twice: ViddikEngine captures each one to PNG and diffs it against the golden as a JUnit 5 test, and ViddikShowroom renders the same registry as a live component gallery. One set of declarations, so the browser cannot drift from the tests.

Fixtures can be declared with @Preview

Since 0.3.0 @ViddikScreenshot also works as a bare marker, with the details read off @Preview — the same annotation the IDE preview pane and Android's own screenshot tooling read, since Compose Multiplatform 1.12 ships it in commonMain under Android's fully-qualified name.

@ViddikScreenshot
@PreviewWrapper(AppPreviewTheme::class)
@PreviewLightDark
@Composable
fun PrimaryButton() { ... }

name, group, widthDp, heightDp, uiMode, fontScale, the size in a spec: device, repeated and multipreview annotations, and @PreviewWrapper are all read. Bare @Preview is not scanned: @ViddikScreenshot stays the opt-in, because most previews in an application exist for the IDE and cannot render headless.

Install

// build.gradle.kts of the module holding the fixtures
plugins {
    id("com.google.devtools.ksp") version "<KSP_VERSION>"
    id("io.github.youndie.viddik") version "<VERSION>"
}

The plugin adds the artefacts, puts the processor on the right ksp* configuration for your module's shape — kspDesktopTest, kspJvmTest or kspTest, and the per-target coordinates where a non-KMP module needs them — and registers viddikRecord, viddikVerify, viddikDesignParity and viddikShowroom. Wiring that by hand is still possible with viddik { addDependencies = false }, but getting the configuration name wrong produces no error, only zero generated tests, which is the reason the plugin exists.

From 0.4.0 viddik is on Maven Central as io.github.youndie.viddik, so no repository has to be declared for it. Up to 0.3.3 it was ru.workinprogress on the snapshot server, and those versions stay there.

0.2.x through 0.5.x require Compose Multiplatform 1.12.x; 0.1.x is the 1.11.x line.

The browser runs on the phone too

Since 0.5.0 the same registry opens in an Android activity and an iOS view controller, not only in the desktop window — a component library is judged on a phone, and a browser that opens on the machine that ran the build and nowhere else is half a browser.

viddik { showroomTargets = true }
class ShowroomActivity : ViddikShowroomActivity() {
    override val components = GeneratedViddikRegistry.components
}

The flag is what moves the generated registry out of the test source set — which is never compiled into an application — and into commonMain, so it exists for every target the module has. The hosts come from viddik-showroom, the goldens keep working unchanged, and the list has a search field over it. The registry is passed in rather than looked up, so a fixture that stopped compiling is a build error instead of an empty gallery at launch.

And a fixture can be measured against its design

Goldens answer "did the rendering change". viddikDesignParity, also 0.5.0, answers the question that comes first while a screen is being built: how far it is from the design it was built to. Put the exported PNG beside the golden under design/, and the task reports a percentage per fixture — reports rather than fails, because a component library rarely has a reference for every state, and a number to work from is worth more than a red build. designStrict turns it into a gate for the module where matching the design is the acceptance criterion.

Why goldens travel

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