chore(test): create shortcut integration test

This commit is contained in:
Adam M. Szalkowski 2026-09-06 07:33:05 +02:00 • committed by Adam
parent b032c486db
commit ff6370f999
3 changed files with 209 additions and 1 deletions

View file

@ -0,0 +1,187 @@
package de.szalkowski.activitylauncher
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.UiSelector
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import de.szalkowski.activitylauncher.domain.packages.PackageRepository
import de.szalkowski.activitylauncher.domain.settings.SettingsRepository
import de.szalkowski.activitylauncher.entrypoint.MainActivity
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.regex.Pattern
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class CreateShortcutIntegrationTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val disableAnimationsRule = DisableAnimationsRule()
@Inject
lateinit var systemRepository: FakeSystemPackageRepository
@Inject
lateinit var packageRepository: PackageRepository
@Inject
lateinit var settingsRepository: SettingsRepository
private lateinit var device: UiDevice
@Before
fun setup() {
hiltRule.inject()
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
settingsRepository.disclaimerAccepted = true
val context = ApplicationProvider.getApplicationContext<Context>()
context.getSharedPreferences("al_recent_activities", Context.MODE_PRIVATE).edit().clear().commit()
context.getSharedPreferences("al_favorites", Context.MODE_PRIVATE).edit().clear().commit()
// Enable using real system package manager to query actual system packages
systemRepository.useReal = true
packageRepository.sync()
val startTime = System.currentTimeMillis()
while ((!packageRepository.isLoaded) && (System.currentTimeMillis() - startTime < 10000)) {
Thread.sleep(200)
}
}
@Test
fun testCreateShortcutAndLaunchFromHomeScreen() {
val targetPackage = "com.android.settings"
val shortcutName = "System Settings Shortcut"
ActivityScenario.launch(MainActivity::class.java).use {
TestUtils.dismissSystemDialogs()
TestUtils.waitForWindowFocus()
Thread.sleep(1000)
// Search specifically for com.android.settings using the search bar
onView(withId(R.id.tiSearch))
.perform(typeText(targetPackage), pressImeActionButton())
Thread.sleep(1500)
// Click on the Settings package item in the package list
onView(withId(R.id.rvPackages))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
Thread.sleep(1500)
// Click on the first activity item in the activity list
onView(withId(R.id.rvActivities))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
Thread.sleep(1500)
// Set a unique name for the shortcut in ActivityDetailsFragment
onView(withId(R.id.tiName)).perform(replaceText(shortcutName))
Thread.sleep(500)
// Click "Create shortcut" in activity details
onView(withId(R.id.btCreateShortcut)).perform(click())
Thread.sleep(1000)
// Confirm pinning shortcut in system dialog
acceptPinShortcutDialog(device)
// Press Home to return to launcher
device.pressHome()
device.waitForIdle()
// Find and click the created shortcut on home screen
val shortcut = findShortcutOnHomeScreen(device, shortcutName)
assertNotNull("Shortcut with label '$shortcutName' should exist on Home screen", shortcut)
shortcut?.click()
// Validate that the Settings app was launched
TestUtils.waitForWindowFocus(targetPackage, 10000)
assertEquals(targetPackage, device.currentPackageName)
}
}
private fun acceptPinShortcutDialog(device: UiDevice) {
Thread.sleep(1000)
val tosButtonPattern = Pattern.compile("(?i)(accept & continue|accept all|i agree|no thanks|got it)")
val tosObj = device.findObject(By.text(tosButtonPattern))
tosObj?.click()
val pinPattern = Pattern.compile("(?i)(add automatically|add to home screen|add|allow|ok)")
var pinButton = device.findObject(UiSelector().textMatches(pinPattern.pattern()))
if (!pinButton.waitForExists(5000)) {
pinButton = device.findObject(UiSelector().resourceId("android:id/button1"))
}
if (pinButton.exists()) {
pinButton.click()
device.waitForIdle()
Thread.sleep(1500)
}
}
private fun findShortcutOnHomeScreen(device: UiDevice, label: String): UiObject2? {
val launcherPkg = device.launcherPackageName
fun isLauncherObject(obj: UiObject2): Boolean {
return obj.applicationPackage != "com.android.systemui" &&
(launcherPkg == null || obj.applicationPackage == launcherPkg)
}
fun searchOnCurrentPage(): UiObject2? {
val objects = device.findObjects(By.text(label))
for (obj in objects) {
if (isLauncherObject(obj)) return obj
}
val descObjects = device.findObjects(By.desc(label))
for (obj in descObjects) {
if (isLauncherObject(obj)) return obj
}
return null
}
var obj = searchOnCurrentPage()
if (obj != null) return obj
val width = device.displayWidth
val height = device.displayHeight
repeat(3) {
device.swipe(width * 3 / 4, height / 2, width / 4, height / 2, 10)
device.waitForIdle()
obj = searchOnCurrentPage()
if (obj != null) return obj
}
repeat(6) {
device.swipe(width / 4, height / 2, width * 3 / 4, height / 2, 10)
device.waitForIdle()
obj = searchOnCurrentPage()
if (obj != null) return obj
}
return null
}
}

View file

@ -1,5 +1,6 @@
package de.szalkowski.activitylauncher
import de.szalkowski.activitylauncher.data.packages.SystemPackageRepositoryImpl
import de.szalkowski.activitylauncher.domain.model.MyActivityInfo
import de.szalkowski.activitylauncher.domain.model.SystemPackage
import de.szalkowski.activitylauncher.domain.packages.SystemPackageRepository
@ -7,29 +8,44 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FakeSystemPackageRepository @Inject constructor() : SystemPackageRepository {
class FakeSystemPackageRepository @Inject constructor(
private val realRepository: SystemPackageRepositoryImpl,
) : SystemPackageRepository {
private val packages = mutableMapOf<String, SystemPackage>()
private val activities = mutableMapOf<String, List<MyActivityInfo>>()
var useReal: Boolean = false
fun addPackage(pkg: SystemPackage, pkgActivities: List<MyActivityInfo>) {
useReal = false
packages[pkg.packageName] = pkg
activities[pkg.packageName] = pkgActivities
}
fun clear() {
useReal = false
packages.clear()
activities.clear()
}
override fun getInstalledPackages(): List<SystemPackage> {
if (useReal || packages.isEmpty()) {
return realRepository.getInstalledPackages()
}
return packages.values.toList()
}
override fun getPackageDetails(packageName: String): SystemPackage? {
if (useReal || !packages.containsKey(packageName)) {
return realRepository.getPackageDetails(packageName)
}
return packages[packageName]
}
override fun getActivities(packageName: String): List<MyActivityInfo> {
if (useReal || !activities.containsKey(packageName)) {
return realRepository.getActivities(packageName)
}
return activities[packageName] ?: emptyList()
}
}

View file

@ -40,8 +40,13 @@ object TestUtils {
"Close",
"OK",
"Accept",
"Accept all",
"Allow",
"Wait",
"No thanks",
"I agree",
"Got it",
"Continue",
)
val regex = "(?i)" + commonButtons.joinToString("|")