Skip to content

Commit d9493e7

Browse files
committed
Add test to generate screenshots for PlayStore
The reason for having these as instrumented tests is that we want them to include the rendered map. However, because that requires the GitHub secret containing the TILE_PROVIDER it can't be done from any old build. We can do it from a release, so do it there and upload the artifacts to that release. We can have both screenshots for the PlayStore, and ones for including in the jekyll docs build. We don't want the time to appear on the screenshots as that would result in endlessly changing screenshots, so trim them with imagemagick before committing. Any changes will result in the docs being rebuilt and using the new images.
1 parent 936f163 commit d9493e7

7 files changed

Lines changed: 378 additions & 8 deletions

File tree

.github/workflows/build-app.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,47 @@ jobs:
125125
name: screenshot-previews
126126
path: ${{ env.main_project_module }}/src/debug/screenshotTest/reference/org/scottishtecharmy/soundscape/
127127

128+
- name: Enable KVM
129+
run: |
130+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
131+
sudo udevadm control --reload-rules
132+
sudo udevadm trigger --name-match=kvm
133+
134+
- name: Build instrumentation tests to generate screenshots for documentation
135+
id: documentation-screenshot-tests
136+
uses: reactivecircus/android-emulator-runner@v2
137+
continue-on-error: true
138+
with:
139+
api-level: 34
140+
target: default
141+
arch: x86_64
142+
profile: pixel_7
143+
script: |
144+
mkdir -p docs/documentationScreens
145+
adb logcat -c # clear logs
146+
adb shell settings put global policy_control immersive.status=* # Disable status bar at top of screen
147+
touch app/emulator.log # create log file
148+
chmod 777 app/emulator.log # allow writing to log file
149+
adb logcat >> app/emulator.log & # pipe all logcat messages into log file as a background process
150+
# Run only the DocumentationScreens tests
151+
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.scottishtecharmy.soundscape.DocumentationScreens
152+
# And then get the resulting screenshots from the emulator
153+
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/homeScreen.png docs/documentationScreens/homeScreen.png
154+
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/homeScreenWithRoute.png docs/documentationScreens/homeScreenWithRoute.png
155+
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/routeDetails.png docs/documentationScreens/routeDetails.png
156+
adb pull /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/routeEdit.png docs/documentationScreens/routeEdit.png
157+
158+
- name: ImageMagick Action to trim off status and navigation bars from screenshots
159+
uses: jruipinto/ImageMagick-action@v1
160+
with:
161+
command: mogrify -path docs/documentationScreens -crop 1080x2130+0+140 docs/documentationScreens/*.*
162+
163+
- name: Commit new screenshots
164+
uses: stefanzweifel/git-auto-commit-action@v5
165+
with:
166+
commit_message: "New documentation screenshots"
167+
file_pattern: 'docs/documentationScreens/*.png'
168+
128169
# Run Build Project
129170
- name: Build gradle project
130171
env:

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ dependencies {
249249

250250
// Screenshots for tests
251251
//screenshotTestImplementation(libs.androidx.compose.ui.tooling)
252+
androidTestImplementation(libs.androidx.uiautomator)
252253

253254
// Protobuf
254255
implementation(libs.protobuf.kotlin.lite)
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package org.scottishtecharmy.soundscape
2+
3+
import androidx.compose.foundation.layout.WindowInsets
4+
import androidx.compose.foundation.layout.safeDrawing
5+
import androidx.compose.foundation.layout.windowInsetsPadding
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.test.junit4.createComposeRule
9+
import androidx.navigation.NavController
10+
import androidx.preference.PreferenceManager
11+
import androidx.test.platform.app.InstrumentationRegistry
12+
import kotlinx.coroutines.flow.MutableStateFlow
13+
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
14+
import org.junit.Rule
15+
import org.junit.Test
16+
import org.scottishtecharmy.soundscape.MainActivity.Companion.ACCESSIBLE_MAP_KEY
17+
import org.scottishtecharmy.soundscape.database.local.model.MarkerEntity
18+
import org.scottishtecharmy.soundscape.database.local.model.RouteEntity
19+
import org.scottishtecharmy.soundscape.database.local.model.RouteWithMarkers
20+
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt
21+
import org.scottishtecharmy.soundscape.screens.home.BottomButtonFunctions
22+
import org.scottishtecharmy.soundscape.screens.home.RouteFunctions
23+
import org.scottishtecharmy.soundscape.screens.home.StreetPreviewFunctions
24+
import org.scottishtecharmy.soundscape.screens.home.data.LocationDescription
25+
import org.scottishtecharmy.soundscape.screens.home.home.Home
26+
import org.scottishtecharmy.soundscape.screens.home.placesnearby.PlacesNearbyUiState
27+
import org.scottishtecharmy.soundscape.screens.markers_routes.screens.addandeditroutescreen.AddAndEditRouteScreen
28+
import org.scottishtecharmy.soundscape.screens.markers_routes.screens.addandeditroutescreen.AddAndEditRouteUiState
29+
import org.scottishtecharmy.soundscape.screens.markers_routes.screens.routedetailsscreen.RouteDetailsScreen
30+
import org.scottishtecharmy.soundscape.screens.markers_routes.screens.routedetailsscreen.RouteDetailsUiState
31+
import org.scottishtecharmy.soundscape.services.RoutePlayerState
32+
import org.scottishtecharmy.soundscape.utils.processMaps
33+
import org.scottishtecharmy.soundscape.viewmodels.home.HomeState
34+
35+
// This is very helpful:
36+
// https://developer.android.com/develop/ui/compose/testing/testing-cheatsheet
37+
class DocumentationScreens {
38+
@get:Rule
39+
val composeTestRule = createComposeRule()
40+
41+
private val location = LngLatAlt(
42+
-4.3178027,
43+
55.9410791
44+
)
45+
46+
private val routeToShops = RouteWithMarkers(
47+
RouteEntity(
48+
name = "Route to shops",
49+
description = "",
50+
routeId = 1L
51+
),
52+
listOf(
53+
MarkerEntity(
54+
name = "Craigton Road",
55+
longitude = -4.3239319,
56+
latitude = 55.9446396,
57+
markerId = 1L
58+
),
59+
MarkerEntity(
60+
name = "Clober Road",
61+
longitude = -4.3210534,
62+
latitude = 55.9417227,
63+
markerId = 2L
64+
),
65+
MarkerEntity(
66+
name = "Douglas Street",
67+
longitude = -4.3194968,
68+
latitude = 55.9406974,
69+
markerId = 3L
70+
),
71+
MarkerEntity(
72+
name = "Underpass to shops",
73+
longitude = -4.3175668,
74+
latitude = 55.9399973,
75+
markerId = 4L
76+
)
77+
)
78+
)
79+
80+
private fun runScreenTest(
81+
screenshotFileName: String,
82+
testCode: @Composable () -> Unit) {
83+
84+
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
85+
86+
// Unpack map assets
87+
processMaps(targetContext)
88+
89+
val themeStateFlow = MutableStateFlow(ThemeState())
90+
composeTestRule.setContent {
91+
SoundscapeTheme(themeStateFlow) {
92+
testCode()
93+
}
94+
}
95+
96+
// Delay to allow the maps to load
97+
Thread.sleep(5000)
98+
99+
// Capture screenshot of the root composable
100+
ScreenshotUtils.captureAndSaveScreenshot(
101+
context = targetContext, // Use target context
102+
filename = screenshotFileName
103+
)
104+
}
105+
106+
@Test
107+
fun homeScreen(){
108+
109+
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
110+
111+
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(targetContext)
112+
// Use accessible map
113+
sharedPreferences.edit().putBoolean(ACCESSIBLE_MAP_KEY, true).apply()
114+
115+
runScreenTest("homeScreen") {
116+
Home(
117+
state = HomeState(
118+
location = location,
119+
heading = 45.0F,
120+
121+
),
122+
onNavigate = {},
123+
preferences = sharedPreferences,
124+
onMapLongClick = { false},
125+
bottomButtonFunctions = BottomButtonFunctions(null),
126+
getCurrentLocationDescription = {
127+
LocationDescription(
128+
name = "Milngavie",
129+
location = location
130+
)
131+
},
132+
searchText = "",
133+
onToggleSearch = { },
134+
onSearchTextChange = { },
135+
rateSoundscape = { },
136+
routeFunctions = RouteFunctions(viewModel = null),
137+
streetPreviewFunctions = StreetPreviewFunctions(viewModel = null),
138+
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
139+
)
140+
}
141+
}
142+
143+
@Test
144+
fun homeScreenWithRoute(){
145+
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
146+
147+
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(targetContext)
148+
// Use accessible map
149+
sharedPreferences.edit().putBoolean(ACCESSIBLE_MAP_KEY, true).apply()
150+
151+
val routePlayerState = RoutePlayerState(
152+
routeData = routeToShops,
153+
currentWaypoint = 0
154+
)
155+
156+
runScreenTest("homeScreenWithRoute") {
157+
Home(
158+
state = HomeState(
159+
location = location,
160+
heading = 45.0F,
161+
currentRouteData = routePlayerState
162+
),
163+
onNavigate = {},
164+
preferences = sharedPreferences,
165+
onMapLongClick = { false},
166+
bottomButtonFunctions = BottomButtonFunctions(null),
167+
getCurrentLocationDescription = {
168+
LocationDescription(
169+
name = "Milngavie",
170+
location = location
171+
)
172+
},
173+
searchText = "",
174+
onToggleSearch = { },
175+
onSearchTextChange = { },
176+
rateSoundscape = { },
177+
routeFunctions = RouteFunctions(viewModel = null),
178+
streetPreviewFunctions = StreetPreviewFunctions(viewModel = null),
179+
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing)
180+
)
181+
}
182+
}
183+
184+
@Test
185+
fun routeDetailsScreen(){
186+
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
187+
188+
runScreenTest("routeDetails") {
189+
RouteDetailsScreen(
190+
navController = NavController(targetContext),
191+
routeId = 1,
192+
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing),
193+
uiState = RouteDetailsUiState(
194+
route = routeToShops
195+
),
196+
routePlayerState = RoutePlayerState(
197+
null,
198+
0
199+
),
200+
getRouteById = { },
201+
startRoute = { },
202+
stopRoute = { },
203+
shareRoute = { },
204+
clearErrorMessage = { },
205+
userLocation = location,
206+
heading = 45.0F
207+
)
208+
}
209+
}
210+
@Test
211+
fun editScreen() {
212+
val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
213+
214+
val route = routeToShops
215+
val members: MutableList<LocationDescription> =
216+
emptyList<LocationDescription>().toMutableList()
217+
for ((index, marker) in route.markers.withIndex()) {
218+
members.add(
219+
LocationDescription(
220+
name = marker.name,
221+
location = LngLatAlt(marker.longitude, marker.latitude),
222+
orderId = index.toLong(),
223+
)
224+
)
225+
}
226+
227+
val uiState = AddAndEditRouteUiState(
228+
name = "To shops",
229+
description = "Route to shops",
230+
routeMembers = members,
231+
)
232+
233+
runScreenTest("routeEdit") {
234+
AddAndEditRouteScreen(
235+
navController = NavController(targetContext),
236+
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing),
237+
uiState = uiState,
238+
userLocation = location,
239+
onClearErrorMessage = { },
240+
onResetDoneAction = { },
241+
onNameChange = { },
242+
onDescriptionChange = { },
243+
onDeleteRoute = { },
244+
onEditComplete = { },
245+
onClickFolder = { _, _ -> },
246+
onClickBack = { },
247+
onSelectLocation = { },
248+
createAndAddMarker = { _, _, _ -> },
249+
editRoute = true,
250+
routeObjectId = 1,
251+
placesNearbyUiState = PlacesNearbyUiState()
252+
)
253+
}
254+
}
255+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.scottishtecharmy.soundscape
2+
3+
import android.content.Context
4+
import android.os.Environment
5+
import androidx.test.platform.app.InstrumentationRegistry
6+
import java.io.File
7+
import androidx.test.uiautomator.UiDevice
8+
9+
object ScreenshotUtils {
10+
11+
// Screenshots are stored in:
12+
// /storage/emulated/0/Android/data/org.scottishtecharmy.soundscape/files/Pictures/screenshots/$filename.png
13+
fun captureAndSaveScreenshot(
14+
context: Context,
15+
filename: String
16+
): String {
17+
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
18+
val screenshotDir = File(
19+
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
20+
"screenshots"
21+
)
22+
if (!screenshotDir.exists()) {
23+
screenshotDir.mkdirs()
24+
}
25+
26+
val file = File(screenshotDir, "$filename.png")
27+
try {
28+
device.takeScreenshot(file)
29+
println("Screenshot saved to: ${file.absolutePath}")
30+
return file.absolutePath
31+
} catch (e: Exception) {
32+
System.err.println("Error saving screenshot: ${e.message}")
33+
e.printStackTrace()
34+
throw e // Re-throw to fail the test if saving fails
35+
}
36+
}
37+
}

app/src/main/java/org/scottishtecharmy/soundscape/MainActivity.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import org.scottishtecharmy.soundscape.screens.home.HomeScreen
3737
import org.scottishtecharmy.soundscape.screens.home.Navigator
3838
import org.scottishtecharmy.soundscape.services.SoundscapeService
3939
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
40-
import org.scottishtecharmy.soundscape.utils.extractAssets
40+
import org.scottishtecharmy.soundscape.utils.processMaps
4141
import java.io.File
4242
import javax.inject.Inject
4343
import kotlin.sequences.forEach
@@ -178,14 +178,13 @@ class MainActivity : AppCompatActivity() {
178178

179179
super.onCreate(savedInstanceState)
180180

181-
// Extract the maplibre style assets
182-
Log.d("ExtractAssets", "Start extraction")
183-
extractAssets(applicationContext, "osm-liberty-accessible","osm-liberty-accessible")
184-
Log.d("ExtractAssets", "Completed extraction")
181+
println("${Build.FINGERPRINT}")
182+
println("${Build.MODEL}")
183+
println("${Build.BRAND}")
184+
println("${Build.PRODUCT}")
185185

186-
// Update extracted style.json with protomaps server URI
187-
processStyle("style.json", "processedStyle.json")
188-
processStyle("originalStyle.json", "processedOriginalStyle.json")
186+
// Unpack map assets
187+
processMaps(applicationContext)
189188

190189
// Debug - dump preferences
191190
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)

0 commit comments

Comments
 (0)