diff --git a/build.gradle.kts b/build.gradle.kts index 7816c1e66..0c745209f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.ksp) apply false alias(libs.plugins.ktlint.plugin) apply false alias(libs.plugins.shadow) apply false + id("io.qameta.allure") version "2.11.2" } apply { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 783e87003..f03a1dfaa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ shadow = "8.1.1" sqliteJdbc = "3.45.1.0" uiautomator = "2.3.0" documentfile = "1.0.1" +espressoCore = "3.7.0" [libraries] annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } @@ -75,6 +76,7 @@ rules = { group = "androidx.test", name = "rules", version.ref = "rules" } sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" } uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } [bundles] androidTest = [ diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index 960333f35..cd1ac0b16 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -22,12 +22,22 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.ksp) alias(libs.plugins.ktlint.plugin) + id("io.qameta.allure") version "2.11.2" } tasks.compileLint { dependsOn("updateTranslators") } +allure { + adapter { + frameworks { + junit4 { } + } + autoconfigureListeners.set(true) + } +} + /* Added on top of kotlinOptions to work around this issue: https://youtrack.jetbrains.com/issue/KTIJ-24311/task-current-target-is-17-and-kaptGenerateStubsProductionDebugKotlin-task-current-target-is-1.8-jvm-target-compatibility-should#focus=Comments-27-6798448.0-0 @@ -49,7 +59,7 @@ android { minSdk = 28 targetSdk = 36 applicationId = "org.isoron.uhabits" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "io.qameta.allure.android.runners.AllureAndroidJUnitRunner" } signingConfigs { @@ -89,6 +99,7 @@ android { } dependencies { + implementation(libs.androidx.espresso.core) compileOnly(libs.jsr250.api) coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.appIntro) @@ -114,4 +125,6 @@ dependencies { androidTestImplementation(libs.bundles.androidTest) testImplementation(libs.bundles.test) + androidTestImplementation("io.qameta.allure:allure-kotlin-junit4:2.4.0") + androidTestImplementation("io.qameta.allure:allure-kotlin-android:2.4.0") } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsSmokeTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsSmokeTest.kt new file mode 100644 index 000000000..2435c0c63 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitsSmokeTest.kt @@ -0,0 +1,54 @@ +package org.isoron.uhabits + +import io.qameta.allure.Feature +import io.qameta.allure.junit4.DisplayName +import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp +import org.isoron.uhabits.pages.MainPage +import org.isoron.uhabits.pages.NewHabitPage +import org.junit.Test + +@Feature("Habits smoke test") +//@RunWith(AllureAndroidJUnit4::class) +//@RunWith(AndroidJUnit4::class) +class HabitsSmokeTest : BaseUserInterfaceTest() { + + @Test + @DisplayName("Create new habit") + fun createNewHabit() { + launchApp() + MainPage.openNewHabitPage() + NewHabitPage.fillDefaultHabit("Test Name", "Test Question") + MainPage.checkHabitPresentOnScroller("Test Name") + } + + @Test + @DisplayName("Delete habit") + fun deleteHabit() { + launchApp() + with(MainPage) { + checkHabitPresentOnScroller("Wake up early") + deleteHabit("Wake up early") + checkHabitIsNotPresentOnScroller("Wake up early") + } + } + + @Test + @DisplayName("Hide completed habits") + fun hideCompleted() { + launchApp() + with(MainPage) { + hideCompleted() + checkHabitIsNotPresentOnScroller("Track time") + } + } + + @Test + @DisplayName("Hide archived habits") + fun hideArchived() { + launchApp() + with(MainPage) { + makeArchived("Track time") + checkHabitIsNotPresentOnScroller("Track time") + } + } +} \ No newline at end of file diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/MainPage.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/MainPage.kt new file mode 100644 index 000000000..2a43f74e4 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/MainPage.kt @@ -0,0 +1,122 @@ +package org.isoron.uhabits.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import io.qameta.allure.Step +import org.hamcrest.CoreMatchers +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.endsWith +import org.isoron.uhabits.BaseUserInterfaceTest.Companion.device +import org.isoron.uhabits.R +import org.isoron.uhabits.acceptance.steps.CommonSteps.scrollToText + +object MainPage { + + private val addHabitButton by lazy { onView(withId(R.id.actionCreateHabit)) } + private val sortMenuButton by lazy { onView(withId(R.id.action_filter)) } + private val moreMenuButton by lazy { + onView( + CoreMatchers.allOf( + ViewMatchers.withContentDescription("More options"), + ViewMatchers.withParent( + ViewMatchers.withParent( + ViewMatchers.withClassName( + endsWith("Toolbar") + ) + ) + ) + ) + ) + } + + // More menu window + private val deleteMenuButton by lazy { onView(withText(R.string.delete)) } + private val yesDeleteMenuButton by lazy { onView(withText("Yes")) } + + // Sort window + private val hideCompleted by lazy { onView(withText(R.string.hide_completed)) } + private val hideArchived by lazy { onView(withText(R.string.hide_archived)) } + private val archive by lazy { onView(withText(R.string.archive)) } + private val sort by lazy { onView(withText("Sort")) } + + private val habitsScrollerRow = + Getter { habitName -> + onView( + allOf( + ViewMatchers.hasDescendant(withText(habitName)), + ViewMatchers.withClassName(endsWith("HabitCardView")) + ) + ) + } + + fun interface Getter { + operator fun get(value: V): R + } + + private val habitTypeWindow by lazy { onView(withText(R.string.yes_or_no_example)) } + private val habitTypeYesOnNo by lazy { onView(withText("Yes or No")) } + + @Step("Open New Habit Page") + fun openNewHabitPage() { + addHabitButton.perform(ViewActions.click()) + habitTypeWindow.check(matches(isDisplayed())) + habitTypeYesOnNo.perform(ViewActions.click()) + } + + @Step("Hide completed") + fun hideCompleted() { + sortMenuButton.perform(ViewActions.click()) + hideCompleted.perform(ViewActions.click()) + } + + @Step("Hide archived") + fun hideArchived() { + sortMenuButton.perform(ViewActions.click()) + hideArchived.perform(ViewActions.click()) + } + + @Step("Change sort on {sortText}") + fun changeSort(sortText: String) { + sortMenuButton.perform(ViewActions.click()) + sort.perform(ViewActions.click()) + onView(withText(sortText)).perform(ViewActions.click()) + } + + @Step("Delete habit with name {habitName}") + fun deleteHabit(habitName: String) { + scrollToText(habitName) + onView(withText(habitName)).perform(ViewActions.longClick()) + device.waitForIdle() + moreMenuButton.perform(ViewActions.click()) + deleteMenuButton.perform(ViewActions.click()) + yesDeleteMenuButton.perform(ViewActions.click()) + } + + @Step("Check habit with name {habitName} present on scroller") + fun checkHabitPresentOnScroller(habitName: String) { + scrollToText(habitName) + habitsScrollerRow[habitName].check(matches(isDisplayed())) + } + + @Step("Check habit with name {habitName} is not present on scroller") + fun checkHabitIsNotPresentOnScroller(habitName: String) { + onView(withText(habitName)).check(doesNotExist()) + } + + @Step("Check habit with name {habitName} is not present on scroller") + fun makeArchived(habitName: String) { + scrollToText(habitName) + onView(withText(habitName)).perform(ViewActions.longClick()) + device.waitForIdle() + moreMenuButton.perform(ViewActions.click()) + archive.perform(ViewActions.click()) + } + +} \ No newline at end of file diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/NewHabitPage.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/NewHabitPage.kt new file mode 100644 index 000000000..14edf6615 --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/pages/NewHabitPage.kt @@ -0,0 +1,41 @@ +package org.isoron.uhabits.pages + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers.withId +import org.isoron.uhabits.R + +object NewHabitPage { + + private val nameInput by lazy { onView(withId(R.id.nameInput)) } + private val questionInput by lazy { onView(withId(R.id.questionInput)) } + private val frequencyPicker by lazy { onView(withId(R.id.boolean_frequency_picker)) } + private val notesInput by lazy { onView(withId(R.id.notesInput)) } + private val colorButton by lazy { onView(withId(R.id.colorButton)) } + + private val save by lazy { onView(withId(R.id.buttonSave)) } + + fun fillDefaultHabit(name: String, question: String, note: String? = null) { + nameInput.perform( + ViewActions.clearText(), + ViewActions.typeText(name), + ViewActions.closeSoftKeyboard() + ) + + questionInput.perform( + ViewActions.clearText(), + ViewActions.typeText(question), + ViewActions.closeSoftKeyboard() + ) + + note?.let { + notesInput.perform( + ViewActions.clearText(), + ViewActions.typeText(it), + ViewActions.closeSoftKeyboard() + ) + } + + save.perform(ViewActions.click()) + } +} \ No newline at end of file