Prepare for UI tests

parent 5798533e
......@@ -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(