diff --git a/briar-desktop/build.gradle.kts b/briar-desktop/build.gradle.kts
index a556646720367fdcf0dcbf4793f66065d115bbbb..a02b157bf1622691ff1a455bdaf86feca0fea11e 100644
--- a/briar-desktop/build.gradle.kts
+++ b/briar-desktop/build.gradle.kts
@@ -122,6 +122,9 @@ dependencies {
     testImplementation(project(path = ":bramble-core", configuration = "testOutput"))
     testImplementation("commons-io:commons-io:2.11.0")
     kaptTest("com.google.dagger:dagger-compiler:$daggerVersion")
+
+    @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+    testImplementation(compose.uiTestJUnit4)
 }
 
 // hacky fix for upstream issue when selecting skiko in gradle
@@ -134,7 +137,9 @@ configurations.all {
 }
 
 tasks.test {
-    useTestNG()
+    // todo: both cannot be used at once, we probably have to split tests into UI (Compose, JUnit) and Kotlin code (testNG)
+    useJUnit()
+    // useTestNG()
 }
 
 tasks.withType<KotlinCompile> {
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/expiration/ExpirationBanner.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/expiration/ExpirationBanner.kt
index 78a179ea7829394a66f4b71d8c8bcb6ab7f99d69..b7502bb51928b511048c98061c17bde4168fd3f2 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/expiration/ExpirationBanner.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/expiration/ExpirationBanner.kt
@@ -42,6 +42,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.expiration.ExpirationUtils.periodicallyCheckIfExpired
 import org.briarproject.briar.desktop.theme.warningBackground
@@ -112,7 +113,7 @@ fun ExpirationBanner(
             icon = Icons.Filled.Close,
             contentDescription = i18n("hide"),
             onClick = hide,
-            modifier = Modifier.padding(vertical = 4.dp)
+            modifier = Modifier.padding(vertical = 4.dp).testTag("close_expiration")
         )
     }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index e528ff6c69c61f72bd10be5f1191d047a4418abe..917d5f25595693a5be333b535c7b0e229208248f 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -83,6 +83,9 @@ interface BriarUi {
     fun start(onClose: () -> Unit)
 
     fun stop()
+
+    @Composable
+    fun content()
 }
 
 val LocalWindowScope = staticCompositionLocalOf<FrameWindowScope?> { null }
@@ -117,7 +120,6 @@ constructor(
         }
     }
 
-    @OptIn(ExperimentalFoundationApi::class)
     @Composable
     override fun start(onClose: () -> Unit) {
         val focusState = remember { WindowFocusState() }
@@ -208,10 +210,6 @@ constructor(
             CompositionLocalProvider(
                 LocalWindowScope provides this,
                 LocalWindowFocusState provides focusState,
-                LocalViewModelProvider provides viewModelProvider,
-                LocalAvatarManager provides avatarManager,
-                LocalConfiguration provides configuration,
-                LocalTextContextMenu provides BriarTextContextMenu,
             ) {
                 // invalidate whole application window in case the theme, language or UI scale
                 // setting is changed
@@ -222,16 +220,29 @@ constructor(
                 window.minimumSize = DensityDimension(800, 600, configuration)
                 window.preferredSize = DensityDimension(800, 600, configuration)
 
-                val isDarkTheme = configuration.theme == DARK ||
-                    (configuration.theme == AUTO && isSystemInDarkTheme())
-                BriarTheme(isDarkTheme, configuration.uiScale) {
-                    Column(Modifier.fillMaxSize()) {
-                        ExpirationBanner { screenState = EXPIRED; stop() }
-                        when (screenState) {
-                            STARTUP -> StartupScreen()
-                            MAIN -> MainScreen()
-                            EXPIRED -> ErrorScreen(i18n("startup.failed.expired"))
-                        }
+                content()
+            }
+        }
+    }
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Composable
+    override fun content() {
+        CompositionLocalProvider(
+            LocalViewModelProvider provides viewModelProvider,
+            LocalConfiguration provides configuration,
+            LocalAvatarManager provides avatarManager,
+            LocalTextContextMenu provides BriarTextContextMenu,
+        ) {
+            val isDarkTheme = configuration.theme == DARK ||
+                (configuration.theme == AUTO && isSystemInDarkTheme())
+            BriarTheme(isDarkTheme) {
+                Column(Modifier.fillMaxSize()) {
+                    ExpirationBanner { screenState = EXPIRED; stop() }
+                    when (screenState) {
+                        STARTUP -> StartupScreen()
+                        MAIN -> MainScreen()
+                        EXPIRED -> ErrorScreen(i18n("startup.failed.expired"))
                     }
                 }
             }
diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b24d4cb7fe4081fec4b86daebbc60981e49d5f66
--- /dev/null
+++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/ScreenshotTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runDesktopComposeUiTest
+import org.briarproject.bramble.BrambleCoreEagerSingletons
+import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT
+import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_PORT
+import org.briarproject.briar.BriarCoreEagerSingletons
+import org.briarproject.briar.desktop.TestUtils.getDataDir
+import org.jetbrains.skia.Image
+import org.junit.Test
+import java.io.FileOutputStream
+
+@OptIn(ExperimentalTestApi::class)
+class ScreenshotTest {
+    @Test
+    fun makeScreenshot() = runDesktopComposeUiTest(700, 500) {
+        val dataDir = getDataDir()
+        val app =
+            DaggerBriarDesktopTestApp.builder().desktopCoreModule(
+                DesktopCoreModule(dataDir, DEFAULT_SOCKS_PORT, DEFAULT_CONTROL_PORT)
+            ).build()
+        // We need to load the eager singletons directly after making the
+        // dependency graphs
+        BrambleCoreEagerSingletons.Helper.injectEagerSingletons(app)
+        BriarCoreEagerSingletons.Helper.injectEagerSingletons(app)
+
+        val ui = app.getBriarUi()
+
+        setContent {
+            ui.content()
+        }
+        captureToImage().save("before-click.png")
+        onNodeWithTag("close_expiration").performClick()
+        captureToImage().save("after-click.png")
+    }
+}
+
+private fun Image.save(file: String) {
+    encodeToData()?.bytes?.let { bytes ->
+        FileOutputStream(file).use { out ->
+            out.write(bytes)
+        }
+    }
+}