Skip to content
Snippets Groups Projects
Verified Commit 817b6ac9 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Prepare for UI tests

parent 5798533e
Branches
Tags
No related merge requests found
Showing
with 562 additions and 42 deletions
......@@ -35,7 +35,7 @@ build:
test:
stage: "test"
script:
- "./gradlew check"
- "./gradlew check -x connectedCheck"
artifacts:
reports:
junit: "*/build/test-results/*/TEST-*.xml"
......
......@@ -61,6 +61,7 @@ android {
testInstrumentationRunnerArguments = mapOf(
"disableAnalytics" to "true"
)
testInstrumentationRunner = "de.kuschku.quasseldroid.util.TestRunner"
}
buildTypes {
......@@ -188,4 +189,11 @@ dependencies {
testImplementation("org.robolectric", "robolectric", "4.2") {
exclude(group = "org.threeten", module = "threetenbp")
}
androidTestImplementation("junit", "junit", "4.12")
androidTestImplementation("androidx.test.espresso", "espresso-core", "3.1.0")
androidTestImplementation("androidx.test.espresso", "espresso-contrib", "3.1.0")
androidTestImplementation("androidx.test.ext", "junit", "1.1.0")
androidTestImplementation("androidx.test", "runner", "1.1.0")
androidTestImplementation("androidx.test", "rules", "1.1.0")
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import de.kuschku.quasseldroid.ui.chat.ChatActivity
import de.kuschku.quasseldroid.ui.setup.accounts.selection.AccountSelectionActivity
import de.kuschku.quasseldroid.util.conditionwatcher.ConditionWatcher.waitForCondition
import de.kuschku.quasseldroid.util.conditionwatcher.Instruction
import de.kuschku.quasseldroid.util.matcher.IsFullyRenderedMatcher.Companion.isFullyRendered
import de.kuschku.quasseldroid.util.matches
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.Thread.sleep
@RunWith(AndroidJUnit4::class)
@LargeTest
class AccountBehaviorTest {
@get:Rule
val activityActivityTestRule = ActivityTestRule(ChatActivity::class.java)
data class TestData(
val host: String,
val port: Int,
val requireSsl: Boolean,
val user: String,
val pass: String
)
@Test
fun actuallyTestThisThing() {
val testData = TestData(
host = InstrumentationRegistry.getArguments().getString("host", "localhost"),
port = InstrumentationRegistry.getArguments().getInt("port", 4242),
requireSsl = InstrumentationRegistry.getArguments().getBoolean("requireSsl"),
user = InstrumentationRegistry.getArguments().getString("user", "test"),
pass = InstrumentationRegistry.getArguments().getString("pass", "test")
)
onView(withId(R.id.host))
.perform(typeText(testData.host))
onView(withId(R.id.port))
.perform(
clearText(),
typeText(testData.port.toString())
)
if (testData.requireSsl) {
onView(withId(R.id.require_ssl))
.perform(click())
}
waitForCondition(1_000, Instruction("next button should be visible") {
activity?.findViewById<FloatingActionButton>(R.id.next_button).matches(
isFullyRendered()
)
})
onView(withId(R.id.next_button))
.perform(click())
waitForCondition(1_000, Instruction("user input should be visible") {
activity?.findViewById<TextInputEditText>(R.id.user).matches(
isFullyRendered()
)
})
onView(withId(R.id.user))
.perform(
clearText(),
typeText(testData.user)
)
onView(withId(R.id.pass))
.perform(
clearText(),
typeText(testData.pass)
)
waitForCondition(1_000, Instruction("next button should be visible") {
activity?.findViewById<FloatingActionButton>(R.id.next_button).matches(
isFullyRendered()
)
})
onView(withId(R.id.next_button))
.perform(click())
waitForCondition(1_000, Instruction("name input should be visible") {
activity?.findViewById<TextInputEditText>(R.id.name).matches(
isFullyRendered()
)
})
onView(withId(R.id.name))
.perform(
clearText(),
typeText("Remote Test")
)
waitForCondition(1_000, Instruction("next button should be visible") {
activity?.findViewById<FloatingActionButton>(R.id.next_button).matches(
isFullyRendered()
)
})
onView(withId(R.id.next_button))
.perform(click())
waitForCondition(1_000, Instruction("selection activity should be shown") {
activity is AccountSelectionActivity
})
sleep(500)
onView(withId(R.id.account_list))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
waitForCondition(1_000, Instruction("next button should be visible") {
activity?.findViewById<FloatingActionButton>(R.id.next_button).matches(
isFullyRendered()
)
})
onView(withId(R.id.next_button))
.perform(click())
waitForCondition(1_000, Instruction("chat activity should be shown") {
activity is ChatActivity
})
waitForCondition(10_000, Instruction("connection display should not be shown") {
activity?.findViewById<View>(R.id.connection_status).matches(
withEffectiveVisibility(ViewMatchers.Visibility.GONE)
)
})
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid
import android.app.Activity
import android.app.Application
import android.os.Bundle
class ActivityLifecycleHandler : Application.ActivityLifecycleCallbacks {
var currentActivity: Activity? = null
private set
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) = Unit
override fun onActivityStarted(activity: Activity?) = Unit
override fun onActivityResumed(activity: Activity?) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity?) {
currentActivity = null
}
override fun onActivityStopped(activity: Activity?) = Unit
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) = Unit
override fun onActivityDestroyed(activity: Activity?) = Unit
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid
import de.kuschku.quasseldroid.app.QuasseldroidBaseDelegate
class QuasseldroidAndroidTest : Quasseldroid() {
val activityLifecycleHandler = ActivityLifecycleHandler()
override val delegate = QuasseldroidBaseDelegate(this)
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(activityLifecycleHandler)
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import de.kuschku.quasseldroid.Quasseldroid
import de.kuschku.quasseldroid.QuasseldroidAndroidTest
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?,
context: Context?): Application {
return super.newApplication(
cl,
if (className == Quasseldroid::class.java.canonicalName) QuasseldroidAndroidTest::class.java.canonicalName
else className,
context
)
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util
import org.hamcrest.Matcher
fun <T> T?.matches(vararg matchers: Matcher<T>): Boolean = this != null && matchers.all {
it.matches(this)
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util.conditionwatcher;
/**
* Created by F1sherKK on 08/10/15.
*/
public class ConditionWatcher {
public static final int CONDITION_NOT_MET = 0;
public static final int CONDITION_MET = 1;
public static final int TIMEOUT = 2;
public static final int DEFAULT_TIMEOUT_LIMIT = 1000 * 60;
public static final int DEFAULT_INTERVAL = 250;
private static ConditionWatcher conditionWatcher;
private int timeoutLimit = DEFAULT_TIMEOUT_LIMIT;
private int watchInterval = DEFAULT_INTERVAL;
private ConditionWatcher() {
super();
}
public static ConditionWatcher getInstance() {
if (conditionWatcher == null) {
conditionWatcher = new ConditionWatcher();
}
return conditionWatcher;
}
public static void waitForCondition(int timeoutLimit, Instruction instruction) throws Exception {
setTimeoutLimit(timeoutLimit);
waitForCondition(instruction);
}
public static void waitForCondition(Instruction instruction) throws Exception {
int status = CONDITION_NOT_MET;
int elapsedTime = 0;
do {
if (instruction.checkCondition()) {
status = CONDITION_MET;
} else {
elapsedTime += getInstance().watchInterval;
Thread.sleep(getInstance().watchInterval);
}
if (elapsedTime >= getInstance().timeoutLimit) {
status = TIMEOUT;
break;
}
} while (status != CONDITION_MET);
if (status == TIMEOUT)
throw new Exception(instruction.getDescription() + " - took more than " + getInstance().timeoutLimit / 1000 + " seconds. Test stopped.");
}
public static void setWatchInterval(int watchInterval) {
getInstance().watchInterval = watchInterval;
}
public static void setTimeoutLimit(int ms) {
getInstance().timeoutLimit = ms;
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util.conditionwatcher
import androidx.test.platform.app.InstrumentationRegistry
import de.kuschku.quasseldroid.QuasseldroidAndroidTest
class Instruction(
val description: String,
private val condition: InstructionContext.() -> Boolean
) {
fun checkCondition(): Boolean = condition.invoke(InstructionContext)
object InstructionContext {
val application
get() = (InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as QuasseldroidAndroidTest)
val activity
get() = application.activityLifecycleHandler.currentActivity
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.util.matcher
import android.view.View
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.remote.annotation.RemoteMsgConstructor
import de.kuschku.quasseldroid.util.matches
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
class IsFullyRenderedMatcher @RemoteMsgConstructor internal constructor(
areaPercentage: Int
) : TypeSafeMatcher<View>() {
private val isDisplayingAtLeastMatcher = ViewMatchers.isDisplayingAtLeast(
areaPercentage)
private val withEffectiveVisibilityMatcher = ViewMatchers.withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE)
override fun describeTo(description: Description) {
description.appendText("view is fully rendered")
}
public override fun matchesSafely(view: View) = view.matches(
isDisplayingAtLeastMatcher,
withEffectiveVisibilityMatcher
)
companion object {
fun isFullyRendered(areaPercentage: Int = 100) = IsFullyRenderedMatcher(
areaPercentage)
}
}
......@@ -34,6 +34,7 @@ open class Quasseldroid : DaggerApplication() {
super.onCreate()
if (delegate.shouldInit()) {
delegate.onInit()
delegate.onPreInit()
applicationInjector().inject(this)
delegate.onPostInit()
}
......@@ -41,6 +42,6 @@ open class Quasseldroid : DaggerApplication() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleHelper.setLocale(base))
delegate.onInstallMultidex()
delegate.onAttachBaseContext()
}
}
......@@ -21,7 +21,8 @@ package de.kuschku.quasseldroid.app
interface AppDelegate {
fun shouldInit(): Boolean
fun onInstallMultidex()
fun onAttachBaseContext()
fun onPreInit()
fun onInit()
fun onPostInit()
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2019 Janne Koschinski
* Copyright (c) 2019 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.quasseldroid.app
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import de.kuschku.quasseldroid.BuildConfig
import de.kuschku.quasseldroid.Quasseldroid
import de.kuschku.quasseldroid.util.backport.AndroidThreeTenBackport
import de.kuschku.quasseldroid.util.compatibility.AndroidCompatibilityUtils
import de.kuschku.quasseldroid.util.compatibility.AndroidLoggingHandler
import de.kuschku.quasseldroid.util.compatibility.AndroidStreamChannelFactory
open class QuasseldroidBaseDelegate(private val app: Quasseldroid) : AppDelegate {
override fun shouldInit() = true
override fun onPreInit() = Unit
override fun onInit() {
// Init compatibility utils
AndroidCompatibilityUtils.inject()
AndroidLoggingHandler.inject()
AndroidStreamChannelFactory.inject()
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AndroidThreeTenBackport.init(app)
}
override fun onPostInit() = Unit
override fun onAttachBaseContext() {
if (BuildConfig.DEBUG) {
MultiDex.install(app)
}
}
}
......@@ -21,8 +21,6 @@ package de.kuschku.quasseldroid.app
import android.os.Build
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import com.squareup.leakcanary.LeakCanary
import de.kuschku.malheur.CrashHandler
import de.kuschku.quasseldroid.BuildConfig
......@@ -34,45 +32,26 @@ import de.kuschku.quasseldroid.persistence.models.Account
import de.kuschku.quasseldroid.settings.AppearanceSettings
import de.kuschku.quasseldroid.settings.SettingsMigration
import de.kuschku.quasseldroid.settings.SettingsMigrationManager
import de.kuschku.quasseldroid.util.backport.AndroidThreeTenBackport
import de.kuschku.quasseldroid.util.compatibility.AndroidCompatibilityUtils
import de.kuschku.quasseldroid.util.compatibility.AndroidLoggingHandler
import de.kuschku.quasseldroid.util.compatibility.AndroidStreamChannelFactory
class QuasseldroidReleaseDelegate(private val app: Quasseldroid) :
AppDelegate {
class QuasseldroidReleaseDelegate(private val app: Quasseldroid) : QuasseldroidBaseDelegate(app) {
override fun shouldInit() = !LeakCanary.isInAnalyzerProcess(app)
override fun onInit() {
override fun onPreInit() {
LeakCanary.install(app)
// Normal app init code...
CrashHandler.init<BuildConfig>(application = app)
// Init compatibility utils
AndroidCompatibilityUtils.inject()
AndroidLoggingHandler.inject()
AndroidStreamChannelFactory.inject()
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AndroidThreeTenBackport.init(app)
}
override fun onPostInit() {
// Migrate preferences
SettingsMigrationManager(
listOf(
SettingsMigration.migrationOf(0,
1) { prefs, edit ->
SettingsMigration.migrationOf(0, 1) { prefs, edit ->
// Migrating database
val database = LegacyAccountDatabase.Creator.init(
app)
val database = LegacyAccountDatabase.Creator.init(app)
val accounts = database.accounts().all()
database.close()
val accountDatabase = AccountDatabase.Creator.init(
app)
val accountDatabase = AccountDatabase.Creator.init(app)
accountDatabase.accounts().create(*accounts.map {
Account(
id = it.id,
......@@ -205,10 +184,4 @@ class QuasseldroidReleaseDelegate(private val app: Quasseldroid) :
)
}
}
override fun onInstallMultidex() {
if (BuildConfig.DEBUG) {
MultiDex.install(app)
}
}
}
......@@ -194,6 +194,8 @@ class AccountAdapter(
}
sealed class AccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var data: Account? = null
class Item(itemView: View, actionListener: ItemListener, clickListener: ItemListener)
: AccountViewHolder(itemView) {
@BindView(R.id.account_name)
......@@ -208,20 +210,18 @@ class AccountAdapter(
@BindView(R.id.account_edit)
lateinit var accountEdit: AppCompatImageButton
private var id = -1L
init {
ButterKnife.bind(this, itemView)
accountEdit.setOnClickListener {
actionListener.onAction(id, adapterPosition)
actionListener.onAction(data?.id ?: -1L, adapterPosition)
}
itemView.setOnClickListener {
clickListener.onAction(id, adapterPosition)
clickListener.onAction(data?.id ?: -1L, adapterPosition)
}
}
fun bind(account: Account, selected: Boolean) {
id = account.id
data = account
accountName.text = account.name
accountDescription.text = itemView.context.resources.getString(
R.string.label_user_on_host, account.user, account.host, account.port
......@@ -230,7 +230,7 @@ class AccountAdapter(
}
fun clear() {
id = -1L
data = null
accountName.text = ""
accountDescription.text = ""
accountSelect.isChecked = false
......
......@@ -21,7 +21,8 @@ package de.kuschku.quasseldroid.app
class QuasseldroidTestDelegate : AppDelegate {
override fun shouldInit() = true
override fun onInstallMultidex() = Unit
override fun onAttachBaseContext() = Unit
override fun onPreInit() = Unit
override fun onInit() = Unit
override fun onPostInit() = Unit
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment