test: screenshot tests from a real Open-Meteo response
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
package app.wetter
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.FormatListBulleted
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import app.wetter.data.Forecast
|
||||||
|
import app.wetter.data.OpenMeteo
|
||||||
|
import app.wetter.data.Quarter
|
||||||
|
import app.wetter.data.Settings
|
||||||
|
import app.wetter.data.ThemeMode
|
||||||
|
import app.wetter.ui.screens.DayContent
|
||||||
|
import app.wetter.ui.screens.ForecastPage
|
||||||
|
import app.wetter.ui.screens.SettingsScreen
|
||||||
|
import app.wetter.ui.theme.WeatherTheme
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/** Renders the screens on the JVM into replica/clone-screens/ for replica-diff. Data: a real Open-Meteo response. */
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
@Config(sdk = [35], qualifiers = "de-rDE-w412dp-h915dp-xxhdpi", application = android.app.Application::class)
|
||||||
|
class ScreenshotTest {
|
||||||
|
@get:Rule val compose = createComposeRule()
|
||||||
|
|
||||||
|
private fun fixture(): Forecast {
|
||||||
|
val f = javaClass.classLoader!!.getResource("berlin_forecast.json")!!.readText()
|
||||||
|
val a = javaClass.classLoader!!.getResource("berlin_air.json")!!.readText()
|
||||||
|
val parsed = OpenMeteo().parse(f, a, System.currentTimeMillis())
|
||||||
|
// pin "now" to the response's own current time so the shots never drift
|
||||||
|
val now = LocalDateTime.parse(Regex("\"current\":\\{\"time\":\"([^\"]+)\"").find(f)!!.groupValues[1])
|
||||||
|
return parsed.copy(clock = now)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Forecast.with(code: Int, isDay: Boolean, rainSoon: Boolean = false): Forecast {
|
||||||
|
val minutely = if (!rainSoon) minutely else List(12) { i ->
|
||||||
|
Quarter(now.withMinute(0).plusMinutes(15L * i), if (i in 2..7) listOf(0.2, 0.5, 0.9, 1.4, 0.8, 0.3)[i - 2] else 0.0)
|
||||||
|
}
|
||||||
|
return copy(current = current.copy(code = code, isDay = isDay), minutely = minutely)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shot(name: String, theme: ThemeMode, content: @Composable () -> Unit) {
|
||||||
|
compose.setContent { WeatherTheme(theme) { content() } }
|
||||||
|
compose.onRoot().captureRoboImage("../../replica/clone-screens/$name.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Home(f: Forecast, title: String = "Berlin", preview: Boolean = false) = ForecastPage(
|
||||||
|
title = title, isCurrent = !preview, forecast = f, settings = Settings(), animate = false,
|
||||||
|
refreshing = false, failed = false, onRefresh = {},
|
||||||
|
navigation = { IconButton(onClick = {}) { Icon(Icons.Rounded.FormatListBulleted, null) } },
|
||||||
|
actions = {
|
||||||
|
if (preview) FilledTonalButton(onClick = {}) { Text("Speichern") }
|
||||||
|
else IconButton(onClick = {}) { Icon(Icons.Rounded.Settings, null) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test fun parserReadsRealResponse() {
|
||||||
|
val f = fixture()
|
||||||
|
assertEquals(10, f.daily.size)
|
||||||
|
assertTrue(f.hourly.size >= 240)
|
||||||
|
assertTrue(f.upcomingHours(25).isNotEmpty())
|
||||||
|
assertTrue(f.air?.aqi != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun s01_real_light() = shot("S01-real-light", ThemeMode.LIGHT) { Home(fixture()) }
|
||||||
|
@Test fun s01_clear_day() = shot("S01-clear-day", ThemeMode.LIGHT) { Home(fixture().with(0, true)) }
|
||||||
|
@Test fun s01_clear_night_dark() = shot("S01-clear-night-dark", ThemeMode.DARK) { Home(fixture().with(0, false)) }
|
||||||
|
@Test fun s01_rain_nowcast() = shot("S01-rain-nowcast", ThemeMode.LIGHT) { Home(fixture().with(63, true, rainSoon = true)) }
|
||||||
|
@Test fun s01_snow() = shot("S01-snow", ThemeMode.LIGHT) { Home(fixture().with(73, true)) }
|
||||||
|
@Test fun s01_storm_dark() = shot("S01-storm-dark", ThemeMode.DARK) { Home(fixture().with(95, false)) }
|
||||||
|
|
||||||
|
@Config(qualifiers = "de-rDE-w412dp-h2600dp-xxhdpi")
|
||||||
|
@Test fun s01_full_light() = shot("S01-full-light", ThemeMode.LIGHT) { Home(fixture().with(2, true)) }
|
||||||
|
|
||||||
|
@Config(qualifiers = "de-rDE-w412dp-h2600dp-xxhdpi")
|
||||||
|
@Test fun s01_full_dark() = shot("S01-full-dark", ThemeMode.DARK) { Home(fixture().with(0, false)) }
|
||||||
|
|
||||||
|
@Config(qualifiers = "de-rDE-w412dp-h1800dp-xxhdpi")
|
||||||
|
@Test fun s04_day_dark() = shot("S04-day-dark", ThemeMode.DARK) {
|
||||||
|
val f = fixture()
|
||||||
|
DayContent(f, "Berlin", f.daily[1].date, Settings(), onBack = {})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun s15_preview() = shot("S15-preview", ThemeMode.LIGHT) { Home(fixture().with(1, true), "Zwickau", preview = true) }
|
||||||
|
@Test fun s08_settings() = shot("S08-settings", ThemeMode.LIGHT) { SettingsScreen(Settings(), onBack = {}) }
|
||||||
|
|
||||||
|
@Config(qualifiers = "de-rDE-w840dp-h900dp-xxhdpi")
|
||||||
|
@Test fun s01_tablet() = shot("S01-tablet", ThemeMode.LIGHT) { Box { Home(fixture().with(2, true)) } }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"latitude":52.5,"longitude":13.400002,"generationtime_ms":0.6562471389770508,"utc_offset_seconds":7200,"timezone":"Europe/Berlin","timezone_abbreviation":"GMT+2","elevation":38.0,"current_units":{"time":"iso8601","interval":"seconds","european_aqi":"EAQI","pm2_5":"μg/m³","pm10":"μg/m³","alder_pollen":"grains/m³","birch_pollen":"grains/m³","grass_pollen":"grains/m³","mugwort_pollen":"grains/m³","olive_pollen":"grains/m³","ragweed_pollen":"grains/m³"},"current":{"time":"2026-10-17T13:00","interval":3600,"european_aqi":20,"pm2_5":5.0,"pm10":5.1,"alder_pollen":0.0,"birch_pollen":0.0,"grass_pollen":0.0,"mugwort_pollen":0.0,"olive_pollen":0.0,"ragweed_pollen":0.0}}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user