tests: add UI tests for onboarding activity

This commit is contained in:
Akash Yadav
2024-01-07 13:42:26 +05:30
parent 2a5bca1d6f
commit c30dabe50a
15 changed files with 728 additions and 308 deletions
+136
View File
@@ -3,6 +3,19 @@
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="-1176045050">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-985524882">
<value>
<AndroidTestResultsTableState>
@@ -17,6 +30,33 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-568780982">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-560165011">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="-528694390">
<value>
<AndroidTestResultsTableState>
@@ -57,6 +97,60 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="123996994">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="506209133">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="596961865">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="984985251">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1123912474">
<value>
<AndroidTestResultsTableState>
@@ -70,6 +164,35 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1418426983">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1588084549">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Realme RMX1851" value="120" />
<entry key="Tests" value="360" />
<entry key="realme RMX3312" value="120" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1679989688">
<value>
<AndroidTestResultsTableState>
@@ -96,6 +219,19 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="2089736396">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_4_XL_API_32" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
@@ -1,106 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R.string
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.shell.executeProcessAsync
import com.itsaky.androidide.testing.android.clickAndWaitForWindowUpdate
import com.itsaky.androidide.testing.android.findObjectWithText
import com.itsaky.androidide.testing.android.hasObjectWithText
import com.itsaky.androidide.testing.android.launchAndroidIDE
import com.itsaky.androidide.utils.Environment
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment
import org.junit.Test
import org.junit.runner.RunWith
/**
* A special test case which is used to install the build tools on a test device. This is used to
* setup the test device before running further tests.
*
* This class should not define any other tests.
*
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class BuildToolsInstallerTest {
companion object {
private const val TAG = "BuildToolsInstallerTest"
}
@Test
fun setupDevice() {
if (!System.getenv("IDE_INSTRUMENTATION_SETUP_BUILDTOOLS").toBoolean()) {
return
}
val device = launchAndroidIDE()
// Allow permissions
while (true) {
val obj = device.findObjectWithText("Allow")
obj?.clickAndWaitForWindowUpdate(device) ?: break
}
// Dismiss the 'AndroidIDE statistics' dialog
if (device.hasObjectWithText(string.title_androidide_statistics)
&& device.hasObjectWithText(string.msg_androidide_statistics)
) {
device.findObjectWithText(string.btn_opt_in).clickAndWaitForWindowUpdate(device)
}
// The 'Warning' dialog will be shown, no need to dismiss it.
// Start the installation process
execProcAndLogOutput("pkg upd")
execProcAndLogOutput("yes | pkg upg") // do not use -y
execProcAndLogOutput("yes | idesetup -c")
// Check if the tools were installed
assertThat(Environment.JAVA.exists()).isTrue()
assertThat(Environment.ANDROID_HOME.exists()).isTrue()
Thread.sleep(5000L)
}
private fun execProcAndLogOutput(cmd: String) {
val process = executeProcessAsync {
redirectErrorStream = true
workingDirectory = null
environment = TermuxShellEnvironment().getEnvironment(ApplicationProvider.getApplicationContext<IDEApplication>(), false)
command = listOf(Environment.SHELL.absolutePath, "-c", cmd).also {
Log.d(TAG, "setupDevice: Full command : ${it.joinToString(separator = " ")}")
}
}
logOutput(cmd, process)
process.waitFor()
}
private fun logOutput(cmd: String, process: Process) {
process.inputStream.bufferedReader().forEachLine {
Log.d(TAG, "setupDevice: '${cmd}': $it")
}
}
}
@@ -0,0 +1,231 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.activities
import android.Manifest
import android.content.Context
import android.os.Process
import android.widget.Button
import android.widget.GridLayout
import android.widget.Switch
import androidx.annotation.StringRes
import androidx.core.os.UserHandleCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import androidx.viewpager.widget.ViewPager
import com.adevinta.android.barista.rule.cleardata.ClearDatabaseRule
import com.adevinta.android.barista.rule.cleardata.ClearFilesRule
import com.adevinta.android.barista.rule.cleardata.ClearPreferencesRule
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.testing.android.clickAndWaitForNewWindow
import com.itsaky.androidide.testing.android.clickAndWaitForWindowUpdate
import com.itsaky.androidide.testing.android.findObjectWithText
import com.itsaky.androidide.testing.android.getApplicationContext
import com.itsaky.androidide.testing.android.getObjectWithText
import com.itsaky.androidide.testing.android.launchAndroidIDE
import com.itsaky.androidide.testing.android.stringRes
import com.itsaky.androidide.testing.android.uiAutomation
import com.itsaky.androidide.utils.isAtLeastT
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* @author Akash Yadav
*/
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class OnboardingActivityBehaviorTest {
/**
* Clear shared preferences after tests.
*/
@JvmField
@Rule
val clearPrefsRule = ClearPreferencesRule()
/**
* Clear databases after tests.
*/
@JvmField
@Rule
val clearDBsRule = ClearDatabaseRule()
/**
* Delete private data files after each test.
*/
@JvmField
@Rule
val clearFilesRule = ClearFilesRule()
@Test
fun A_testOnboarding_permissionScreen_storagePermissionBehavior() {
checkPermissionsBehavior(R.string.permission_desc_storage)
}
@Test
fun B_testOnboarding_permissionScreen_installPackagesPermissionBehavior() {
checkPermissionsBehavior(R.string.permission_desc_install_packages)
}
@Test
fun C_testOnboarding_toolsSetup_autoInstallBehavior() {
val device = launchAndroidIDE(fromLauncher = true, clearTasks = true)
device.grantAndroidIDEPermissions()
val autoInstallSwitch = device.findObject(By.checkable(true).clazz(Switch::class.java))
assertThat(autoInstallSwitch).isNotNull()
assertThat(autoInstallSwitch.isChecked).isTrue()
// verify basic switch behavior
autoInstallSwitch.clickAndWaitForWindowUpdate(device)
assertThat(autoInstallSwitch.isChecked).isFalse()
assertThat(device.getObjectWithText(R.string.action_install_git).isEnabled).isFalse()
assertThat(device.getObjectWithText(R.string.action_install_openssh).isEnabled).isFalse()
autoInstallSwitch.clickAndWaitForWindowUpdate(device)
assertThat(autoInstallSwitch.isChecked).isTrue()
// clicking on the 'done' button with 'Automatic installation' must navigate the user to
// terminal and start the installation
val doneButton = device.findObject(By.desc(stringRes(R.string.app_intro_done_button)))
assertThat(doneButton).isNotNull()
assertThat(doneButton.isClickable).isTrue()
if (isAtLeastT()) {
// grant notification permission first
device.uiAutomation.grantRuntimePermissionAsUser(
getApplicationContext<Context>().packageName,
Manifest.permission.POST_NOTIFICATIONS,
UserHandleCompat.getUserHandleForUid(Process.myUid())
)
}
doneButton.clickAndWaitForNewWindow()
// 'Installing bootstrap packages' dialog..
assertThat(device.findObjectWithText(R.string.bootstrap_installer_body)).isNotNull()
// wait for the packages to be installed
// this should take a minute at most
// we wait until the installation finishes and the keys GridLayout is visible
device.wait(Until.hasObject(By.clazz(GridLayout::class.java)), 60 * 1000L)
// verify that the terminal keys view is visible
assertThat(device.findObject(By.clazz(GridLayout::class.java))).isNotNull()
assertThat(device.findObject(By.clazz(ViewPager::class.java))).isNotNull()
}
@Test
fun C_testOnboarding_toolsSetup_manualInstallBehavior() {
val device = launchAndroidIDE(fromLauncher = true, clearTasks = true)
device.grantAndroidIDEPermissions()
val autoInstallSwitch = device.findObject(By.checkable(true).clazz(Switch::class.java))
assertThat(autoInstallSwitch).isNotNull()
assertThat(autoInstallSwitch.isChecked).isTrue()
// verify basic switch behavior
autoInstallSwitch.clickAndWaitForWindowUpdate(device)
assertThat(autoInstallSwitch.isChecked).isFalse()
assertThat(device.getObjectWithText(R.string.action_install_git).isEnabled).isFalse()
assertThat(device.getObjectWithText(R.string.action_install_openssh).isEnabled).isFalse()
// clicking on the 'done' button with 'Automatic installation' must navigate the user to
// terminal and start the installation
val doneButton = device.findObject(By.desc(stringRes(R.string.app_intro_done_button)))
assertThat(doneButton).isNotNull()
assertThat(doneButton.isClickable).isTrue()
if (isAtLeastT()) {
// grant notification permission first
device.uiAutomation.grantRuntimePermissionAsUser(
getApplicationContext<Context>().packageName,
Manifest.permission.POST_NOTIFICATIONS,
UserHandleCompat.getUserHandleForUid(Process.myUid())
)
}
doneButton.clickAndWaitForNewWindow()
// 'Installing bootstrap packages' dialog..
assertThat(device.findObjectWithText(R.string.bootstrap_installer_body)).isNotNull()
// wait for the packages to be installed
// this should take a minute at most
// we wait until the installation finishes and the keys GridLayout is visible
device.wait(Until.hasObject(By.clazz(GridLayout::class.java)), 60 * 1000L)
// verify that the terminal keys view is visible
assertThat(device.findObject(By.clazz(GridLayout::class.java))).isNotNull()
assertThat(device.findObject(By.clazz(ViewPager::class.java))).isNotNull()
}
private fun checkPermissionsBehavior(@StringRes permissionDesc: Int) {
val device = launchAndroidIDE(fromLauncher = true, clearTasks = true)
device.nextButton().clickAndWaitForWindowUpdate(device)
device.nextButton().clickAndWaitForWindowUpdate(device)
var grantButton = device.getObjectWithText(permissionDesc)
.parent!!
.parent!!
.findObject(By.clazz(Button::class.java))
// verify that clicking the 'Grant' button next to storage navigates requests the permissions
grantButton.clickAndWaitForNewWindow()
// check permission denied behavior
device.handleAndroidRPermissions(false)
// verify AndroidIDE is visible
assertThat(device.currentPackageName).isEqualTo(BuildInfo.PACKAGE_NAME)
grantButton = device.getObjectWithText(permissionDesc)
.parent!!
.parent!!
.findObject(By.clazz(Button::class.java))
// verify button state after permission was not granted
assertThat(grantButton).isNotNull()
assertThat(grantButton.text).isNotNull()
// navigate to the permission page again
grantButton.clickAndWaitForNewWindow()
// check permission granted behavior
device.handleAndroidRPermissions(true)
grantButton = device.getObjectWithText(permissionDesc)
.parent!!
.parent!!
.findObject(By.clazz(Button::class.java))
// verify AndroidIDE is visible
assertThat(device.currentPackageName).isEqualTo(BuildInfo.PACKAGE_NAME)
// verify button state after permission was granted
assertThat(grantButton).isNotNull()
assertThat(grantButton.text).isNull()
}
}
@@ -0,0 +1,164 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.activities
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.adevinta.android.barista.assertion.BaristaCheckedAssertions.assertChecked
import com.adevinta.android.barista.assertion.BaristaCheckedAssertions.assertUnchecked
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled
import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertEnabled
import com.adevinta.android.barista.assertion.BaristaRecyclerViewAssertions.assertRecyclerViewItemCount
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertNotDisplayed
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickBack
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
import com.adevinta.android.barista.rule.BaristaRule
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R
import com.itsaky.androidide.preferences.internal.statOptIn
import com.itsaky.androidide.testing.android.launchAndroidIDE
import com.itsaky.androidide.testing.android.stringRes
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* @author Akash Yadav
*/
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class OnboardingActivityStatesTest {
@JvmField
@Rule
val baristaActivityRule = BaristaRule.create(OnboardingActivity::class.java)
@Test
fun A_testOnboarding_welcomeScreen() {
baristaActivityRule.launchActivity()
assertDisplayed(stringRes(R.string.greeting_title))
assertDisplayed(stringRes(R.string.greeting_subtitle))
assertDisplayed(R.id.next)
assertNotDisplayed(R.id.back)
}
@Test
fun B_testOnboarding_statConsentScreen() {
baristaActivityRule.launchActivity()
clickOn(R.id.next)
assertDisplayed(R.id.next)
assertDisplayed(stringRes(R.string.title_androidide_statistics))
assertDisplayed(stringRes(R.string.idepref_stats_optIn_summary))
assertDisplayed(stringRes(R.string.msg_androidide_statistics))
assertDisplayed(R.id.stat_opt_in, stringRes(R.string.title_androidide_statistics))
assertThat(statOptIn).isTrue()
assertChecked(R.id.stat_opt_in)
// disable and navigate to next screen, then verify
clickOn(R.id.stat_opt_in)
clickOn(R.id.next)
assertThat(statOptIn).isFalse()
assertUnchecked(R.id.stat_opt_in)
// Navigate back (statistics)
clickBack()
// enable again and navigate to next screen, then verify
clickOn(R.id.stat_opt_in)
clickOn(R.id.next)
assertThat(statOptIn).isTrue()
assertChecked(R.id.stat_opt_in)
}
@Test
fun C_testOnboarding_permissionsScreen() {
baristaActivityRule.launchActivity()
clickOn(R.id.next)
clickOn(R.id.next)
assertDisplayed(stringRes(R.string.onboarding_title_permissions))
assertDisplayed(stringRes(R.string.permission_title_storage))
assertDisplayed(stringRes(R.string.permission_desc_storage))
assertDisplayed(stringRes(R.string.permission_title_install_packages))
assertDisplayed(stringRes(R.string.permission_desc_install_packages))
assertRecyclerViewItemCount(R.id.onboarding_items, 2)
clickOn(R.id.next)
assertDisplayed(stringRes(R.string.msg_grant_permissions))
}
@Test
fun F_testOnboarding_toolsSetup() {
baristaActivityRule.launchActivity()
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.grantAndroidIDEPermissions()
assertDisplayed(stringRes(R.string.title_install_tools))
assertDisplayed(stringRes(R.string.subtitle_install_tools))
assertDisplayed(R.id.auto_install_switch)
assertDisplayed(R.id.sdk_version_layout)
assertDisplayed(R.id.jdk_version_layout)
assertDisplayed(R.id.install_git)
assertDisplayed(R.id.install_openssh)
assertEnabled(R.id.install_git)
assertEnabled(R.id.install_openssh)
assertEnabled(R.id.sdk_version_layout)
assertEnabled(R.id.jdk_version_layout)
assertChecked(R.id.auto_install_switch)
assertChecked(R.id.install_git)
assertChecked(R.id.install_openssh)
// switch to manual install
clickOn(R.id.auto_install_switch)
// check that the auto-install related fields are disabled
assertUnchecked(R.id.auto_install_switch)
assertDisabled(R.id.install_git)
assertDisabled(R.id.install_openssh)
assertDisabled(R.id.sdk_version_layout)
assertDisabled(R.id.jdk_version_layout)
// switch back to auto-install
clickOn(R.id.auto_install_switch)
// check that the auto-instal related fields are enabled again
assertChecked(R.id.auto_install_switch)
assertEnabled(R.id.install_git)
assertEnabled(R.id.install_openssh)
assertEnabled(R.id.sdk_version_layout)
assertEnabled(R.id.jdk_version_layout)
}
}
@@ -0,0 +1,92 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.activities
import android.widget.Button
import android.widget.Switch
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.UiSelector
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R
import com.itsaky.androidide.testing.android.clickAndWaitForNewWindow
import com.itsaky.androidide.testing.android.clickAndWaitForWindowUpdate
import com.itsaky.androidide.testing.android.getObjectWithText
import com.itsaky.androidide.testing.android.hasObjectWithText
/**
* @author Akash Yadav
*/
fun UiDevice.grantAndroidIDEPermissions() {
nextButton().clickAndWaitForWindowUpdate(this)
if (hasObjectWithText(R.string.title_androidide_statistics)) {
// If we haven't opted-in for statistics yet...
nextButton().clickAndWaitForWindowUpdate(this)
}
if (hasObjectWithText(R.string.onboarding_title_permissions)) {
// If the permissions are not granted, grant them
for (desc in arrayOf(R.string.permission_desc_storage,
R.string.permission_desc_install_packages)) {
val grantButton = getObjectWithText(desc)
.parent!!
.parent!!
.findObject(By.clazz(Button::class.java))
if (grantButton.text != null) {
grantButton.clickAndWaitForNewWindow()
handleAndroidRPermissions(allowPermission = true)
}
}
nextButton().clickAndWaitForWindowUpdate(this)
}
}
fun UiDevice.nextButton(): UiObject2 {
return findObject(By.desc("NEXT"))!!
}
fun UiDevice.handleAndroidRPermissions(
allowPermission: Boolean = true,
) {
assertThat(currentPackageName).isEqualTo("com.android.settings")
assertThat(hasObjectWithText(R.string.app_name)).isTrue()
val storageSwitch = findObject(UiSelector().className(Switch::class.java))
assertThat(storageSwitch).isNotNull()
// verify initial state
assertThat(storageSwitch.isCheckable).isTrue()
assertThat(storageSwitch.isEnabled).isTrue()
assertThat(storageSwitch.isChecked).isFalse()
if (allowPermission) {
// enable permission
storageSwitch.click()
// verify changed state
assertThat(storageSwitch.isChecked).isTrue()
}
// navigate back without enabling the permission
pressBack()
}
@@ -1,182 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments
import android.view.View
import android.view.ViewGroup
import androidx.cardview.widget.CardView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.R
import com.itsaky.androidide.activities.MainActivity
import com.itsaky.androidide.models.MainScreenAction
import com.itsaky.androidide.templates.BooleanParameter
import com.itsaky.androidide.templates.EnumParameter
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.templates.StringParameter
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.IsInstanceOf.instanceOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class MainFragmentTest {
@Rule
@JvmField
var mActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testMainFragmentActionsAreShown() {
val recyclerView = onView(allOf(
withId(R.id.actions),
childAtPosition(
withId(R.id.coordinator),
3)))
recyclerView.check(matches(isDisplayed()))
for (action in MainScreenAction.all()) {
val button = onView(
allOf(withText(action.text),
withParent(allOf(withId(R.id.actions),
withParent(withId(R.id.coordinator)))),
isDisplayed()))
button.check(matches(isDisplayed()))
}
}
@Test
fun testTemplateWizardUi() {
onView(withText(R.string.get_started)).check(matches(isCompletelyDisplayed()))
val recyclerView = onView(allOf(
withId(R.id.actions),
childAtPosition(
withId(R.id.coordinator),
3)))
recyclerView.check(matches(isDisplayed()))
val action = MainScreenAction.all().find { it.id == MainScreenAction.ACTION_CREATE_PROJECT }
assertThat(action).isNotNull()
action!!
val createProject = onView(allOf(withText(action.text), isDisplayed()))
createProject.check(matches(isCompletelyDisplayed()))
createProject.perform(click())
val exitButton = onView(
allOf(withId(R.id.exit_button), isCompletelyDisplayed())
)
exitButton.check(matches(isCompletelyDisplayed()))
val templates = ITemplateProvider.getInstance().getTemplates()
for (template in templates) {
val templateTitle = onView(
allOf(withId(R.id.template_name), withText(template.templateName),
withParent(
withParent(instanceOf(CardView::class.java))),
isCompletelyDisplayed()))
templateTitle.check(matches(isCompletelyDisplayed()))
}
val template = templates.first()
val firstTemplateItem = onView(
allOf(withId(R.id.template_name), withText(template.templateName),
withParent(
withParent(instanceOf(CardView::class.java))),
isCompletelyDisplayed()))
firstTemplateItem.perform(click())
val previousButton = onView(
allOf(withId(R.id.previous), withText(R.string.previous), isCompletelyDisplayed()))
val createProjectFinishButton = onView(
allOf(withId(R.id.finish), withText(R.string.create_project),
isCompletelyDisplayed()))
previousButton.check(matches(isCompletelyDisplayed()))
createProjectFinishButton.check(matches(isCompletelyDisplayed()))
for (parameter in template.parameters) {
val selector = when (parameter) {
is StringParameter -> withText(parameter.value)
is BooleanParameter -> allOf(withText(parameter.name),
if (parameter.value) isChecked() else isNotChecked())
is EnumParameter -> withText(parameter.getDisplayName())
else -> throw IllegalStateException("Unknown parameter type: $parameter")
}
val widget = onView(selector)
widget.check(matches(isCompletelyDisplayed()))
}
previousButton.perform(click())
Thread.sleep(1000L)
exitButton.check(matches(isCompletelyDisplayed()))
exitButton.perform(click())
Thread.sleep(1000L)
onView(withText(R.string.get_started)).check(matches(isCompletelyDisplayed()))
}
private fun childAtPosition(
parentMatcher: Matcher<View>, position: Int): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("Child at position $position in parent ")
parentMatcher.describeTo(description)
}
public override fun matchesSafely(view: View): Boolean {
val parent = view.parent
return parent is ViewGroup && parentMatcher.matches(parent)
&& view == parent.getChildAt(position)
}
}
}
}
+1
View File
@@ -21,6 +21,7 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -42,12 +42,6 @@ import com.termux.shared.termux.TermuxConstants
class OnboardingActivity : AppIntro2() {
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finishAffinity()
}
}
companion object {
private const val KEY_ARCHCONFIG_WARNING_IS_SHOWN = "ide.archConfig.experimentalWarning.isShown"
@@ -63,8 +57,6 @@ class OnboardingActivity : AppIntro2() {
finish()
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
setSwipeLock(true)
setTransformer(AppIntroPageTransformerType.Fade)
setProgressIndicator()
@@ -27,6 +27,7 @@ import org.greenrobot.eventbus.EventBus;
public class PreferenceManager {
private final SharedPreferences prefs;
private boolean isReadOnly = false;
public PreferenceManager(Context ctx) {
this(ctx, null);
@@ -46,7 +47,9 @@ public class PreferenceManager {
}
public PreferenceManager remove(String key) {
prefs.edit().remove(key).apply();
final var edit = prefs.edit();
edit.remove(key);
applyChanges(edit);
dispatchRemoveEvent(key);
return this;
}
@@ -55,8 +58,27 @@ public class PreferenceManager {
EventBus.getDefault().post(new PreferenceRemoveEvent(key));
}
/**
* @return Whether this preference manager is read-only.
*/
public boolean isReadOnly() {
return isReadOnly;
}
/**
* Whether this preference manager is read-only.
*
* @param readOnly <code>true</code>, if the preference manager is read-only, <code>false</code>
* otherwise.
*/
public void setReadOnly(boolean readOnly) {
isReadOnly = readOnly;
}
public PreferenceManager putInt(String key, int val) {
prefs.edit().putInt(key, val).apply();
final var edit = prefs.edit();
edit.putInt(key, val);
applyChanges(edit);
dispatchChangeEvent(key, val);
return this;
}
@@ -70,7 +92,9 @@ public class PreferenceManager {
}
public void putFloat(String key, float val) {
prefs.edit().putFloat(key, val).apply();
final var edit = prefs.edit();
edit.putFloat(key, val);
applyChanges(edit);
dispatchChangeEvent(key, val);
}
@@ -91,7 +115,9 @@ public class PreferenceManager {
}
public PreferenceManager putString(String key, String value) {
prefs.edit().putString(key, value).apply();
final var edit = prefs.edit();
edit.putString(key, value);
applyChanges(edit);
dispatchChangeEvent(key, value);
return this;
}
@@ -105,7 +131,9 @@ public class PreferenceManager {
}
public void putBoolean(String key, boolean value) {
prefs.edit().putBoolean(key, value).apply();
final var edit = prefs.edit();
edit.putBoolean(key, value);
applyChanges(edit);
dispatchChangeEvent(key, value);
}
@@ -114,7 +142,9 @@ public class PreferenceManager {
}
public void putLong(String key, long value) {
prefs.edit().putLong(key, value).apply();
final var edit = prefs.edit();
edit.putLong(key, value);
applyChanges(edit);
dispatchChangeEvent(key, value);
}
@@ -122,4 +152,7 @@ public class PreferenceManager {
return prefs.getInt(key, def);
}
protected void applyChanges(SharedPreferences.Editor editor) {
editor.apply();
}
}
+1
View File
@@ -132,6 +132,7 @@ tests-androidx-espresso-contrib = { module = "androidx.test.espresso:espresso-co
tests-androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.2.0" }
tests-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.2.1" }
tests-mockk = { module = "io.mockk:mockk", version = "1.13.8" }
tests-barista = { module = "com.adevinta.android:barista", version = "4.2.0" }
# Tooling
tooling-builderModel = { module = "com.android.tools.build:builder-model", version.ref = "agp-tooling" }
@@ -28,7 +28,7 @@ var statConsentDialogShown: Boolean
}
var statOptIn: Boolean
get() = prefManager.getBoolean(STAT_OPT_IN, false)
get() = prefManager.getBoolean(STAT_OPT_IN, true)
set(value) {
prefManager.putBoolean(STAT_OPT_IN, value)
}
+3
View File
@@ -35,6 +35,9 @@ dependencies {
api(libs.tests.androidx.uiautomator)
api(libs.tests.junit)
api(libs.tests.google.truth)
api(libs.tests.barista) {
exclude("org.jetbrains.kotlin")
}
api(projects.buildInfo)
api(projects.common)
@@ -20,6 +20,18 @@ package com.itsaky.androidide.testing.android
import android.content.Context
import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
import com.itsaky.androidide.app.BaseApplication
/**
* Get the application context.
*/
fun <AppContext : Context> getApplicationContext(): AppContext =
ApplicationProvider.getApplicationContext()
/**
* Get the [BaseApplication] instance.
*/
fun getBaseApplication() = getApplicationContext<BaseApplication>()
/**
* Get the value of the string resource.
@@ -18,11 +18,11 @@
package com.itsaky.androidide.testing.android
import android.view.KeyCharacterMap
import android.view.KeyEvent
import androidx.annotation.StringRes
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.buildinfo.BuildInfo
private val keyCharMap by lazy {
@@ -45,7 +45,7 @@ fun UiDevice.hasObjectWithText(text: String) =
* @see UiDevice.hasObject
*/
fun UiDevice.hasObjectWithText(@StringRes text: Int) =
hasObject(By.text(stringRes(text)))
hasObjectWithText(stringRes(text))
/**
* @see UiDevice.findObject
@@ -57,7 +57,16 @@ fun UiDevice.findObjectWithText(text: String): UiObject2? =
* @see UiDevice.findObjectWithText
*/
fun UiDevice.findObjectWithText(@StringRes text: Int): UiObject2? =
findObject(By.text(stringRes(text)))
findObjectWithText(stringRes(text))
fun UiDevice.getObjectWithText(text: String): UiObject2 {
val obj = findObjectWithText(text)
assertThat(obj).isNotNull()
return obj!!
}
fun UiDevice.getObjectWithText(@StringRes text: Int): UiObject2 = getObjectWithText(stringRes(text))
/**
* Sends key events to this [UiDevice] to type the given [text].
@@ -67,7 +76,6 @@ fun UiDevice.sendKeyEvents(text: String) {
val keyEvents = keyCharMap.getEvents(text.toCharArray())
for (keyEvent in keyEvents) {
uiAutomation.injectInputEvent(keyEvent, true)
// pressKeyCode(keyEvent.keyCode, keyEvent.metaState)
waitForIdle()
}
}
@@ -17,6 +17,8 @@
package com.itsaky.androidide.testing.android
import androidx.annotation.StringRes
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
@@ -43,4 +45,37 @@ fun UiObject2?.clickAndWaitForNewWindow(timeout: Long = LAUNCH_TIMEOUT) =
run {
assertThat(this).isNotNull()
this!!.clickAndWait(Until.newWindow(), timeout)
}
}
/**
* @see UiDevice.hasObject
*/
fun UiObject2.hasObjectWithText(text: String) =
hasObject(By.text(text))
/**
* @see UiObject2.hasObject
*/
fun UiObject2.hasObjectWithText(@StringRes text: Int) =
hasObjectWithText(stringRes(text))
/**
* @see UiObject2.findObject
*/
fun UiObject2.findObjectWithText(text: String): UiObject2? =
findObject(By.text(text))
/**
* @see UiObject2.findObjectWithText
*/
fun UiObject2.findObjectWithText(@StringRes text: Int): UiObject2? =
findObjectWithText(stringRes(text))
fun UiObject2.getObjectWithText(text: String): UiObject2 {
val obj = findObjectWithText(text)
assertThat(obj).isNotNull()
return obj!!
}
fun UiObject2.getObjectWithText(@StringRes text: Int): UiObject2 = getObjectWithText(stringRes(text))