From 764c3fa72e0ada718b332a8d3ad2000e54f19d57 Mon Sep 17 00:00:00 2001 From: William 'Billy' Brawner Date: Mon, 19 Aug 2019 19:23:24 -0700 Subject: [PATCH] Redo UI tests to cover basic markdown editing flows --- app/build.gradle | 27 +-- .../simplemarkdown/MainActivityTests.kt | 65 ------- .../wbrawner/simplemarkdown/MarkdownTests.kt | 182 ++++++++++++++++++ .../view/activity/AutosaveTest.kt | 157 --------------- .../simplemarkdown/model/MarkdownFile.kt | 13 -- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 199 insertions(+), 247 deletions(-) delete mode 100644 app/src/androidTest/java/com/wbrawner/simplemarkdown/MainActivityTests.kt create mode 100644 app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt delete mode 100644 app/src/androidTest/java/com/wbrawner/simplemarkdown/view/activity/AutosaveTest.kt delete mode 100644 app/src/main/java/com/wbrawner/simplemarkdown/model/MarkdownFile.kt diff --git a/app/build.gradle b/app/build.gradle index 1a535ed..4b75de6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { exclude 'META-INF/LICENSE' exclude 'META-INF/DEPENDENCIES' } - compileSdkVersion 28 + compileSdkVersion 29 buildToolsVersion '28.0.3' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -38,7 +38,7 @@ android { defaultConfig { applicationId "com.wbrawner.simplemarkdown" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 20 versionName "0.7.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -70,6 +70,7 @@ android { unitTests { includeAndroidResources = true } + execution 'ANDROIDX_TEST_ORCHESTRATOR' } } @@ -77,12 +78,15 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:4.2' implementation fileTree(include: ['*.jar'], dir: 'libs') - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' + def espresso_version = '3.2.0' + androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" + androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version" + def android_test = '1.2.0' + androidTestImplementation "androidx.test:runner:$android_test" + androidTestImplementation "androidx.test:rules:$android_test" + androidTestUtil "androidx.test:orchestrator:$android_test" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' @@ -93,7 +97,7 @@ dependencies { annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1' kapt 'com.google.dagger:dagger-android-processor:2.22.1' kapt 'com.google.dagger:dagger-compiler:2.22.1' - implementation 'com.google.firebase:firebase-core:17.0.1' + implementation 'com.google.firebase:firebase-core:17.1.0' implementation 'com.android.billingclient:billing:1.2' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation "androidx.core:core-ktx:1.0.2" @@ -121,7 +125,7 @@ tasks.withType(Test) { jacoco.includeNoLocationClasses = true } -task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) { reports { xml.enabled = true html.enabled = true @@ -135,6 +139,7 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { sourceDirectories = files([mainSrc]) classDirectories = files([javaDebugTree, kotlinDebugTree]) executionData = fileTree(dir: project.buildDir, includes: [ - 'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec' + 'jacoco/testDebugUnitTest.exec', + 'outputs/code-coverage/connected/*coverage.ec' ]) } diff --git a/app/src/androidTest/java/com/wbrawner/simplemarkdown/MainActivityTests.kt b/app/src/androidTest/java/com/wbrawner/simplemarkdown/MainActivityTests.kt deleted file mode 100644 index 3fbb88e..0000000 --- a/app/src/androidTest/java/com/wbrawner/simplemarkdown/MainActivityTests.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.wbrawner.simplemarkdown - -import android.content.pm.ActivityInfo -import androidx.test.InstrumentationRegistry -import androidx.test.InstrumentationRegistry.getInstrumentation -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.AndroidJUnit4 -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.UiScrollable -import androidx.test.uiautomator.UiSelector -import com.wbrawner.simplemarkdown.view.activity.MainActivity -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see [Testing documentation](http://d.android.com/tools/testing) - */ -@RunWith(AndroidJUnit4::class) -class MainActivityTests { - - @Rule - var mActivityRule = ActivityTestRule(MainActivity::class.java) - - @Before - fun setup() { - mActivityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - - @Test - @Throws(Exception::class) - fun openAppTest() { - val mDevice = UiDevice.getInstance(getInstrumentation()) - mDevice.pressHome() - // Bring up the default launcher by searching for a UI component - // that matches the content description for the launcher button. - val allAppsButton = mDevice - .findObject(UiSelector().description("Apps")) - - // Perform a click on the button to load the launcher. - allAppsButton.clickAndWaitForNewWindow() - // Context of the app under test. - val appContext = InstrumentationRegistry.getTargetContext() - - assertEquals("com.wbrawner.simplemarkdown", appContext.packageName) - val appView = UiScrollable(UiSelector().scrollable(true)) - val simpleMarkdownSelector = UiSelector().text("Simple Markdown") - appView.scrollIntoView(simpleMarkdownSelector) - mDevice.findObject(simpleMarkdownSelector).clickAndWaitForNewWindow() - } - - @Test - fun openFileWithoutFilesTest() { - openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext()) - onView(withText("Open")).perform(click()) - } -} diff --git a/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt b/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt new file mode 100644 index 0000000..9f7ec93 --- /dev/null +++ b/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt @@ -0,0 +1,182 @@ +package com.wbrawner.simplemarkdown + +import android.app.Activity.RESULT_OK +import android.app.Instrumentation +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.net.Uri +import android.view.KeyEvent +import androidx.core.content.FileProvider +import androidx.test.InstrumentationRegistry +import androidx.test.InstrumentationRegistry.getInstrumentation +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches +import androidx.test.espresso.web.sugar.Web.onWebView +import androidx.test.espresso.web.webdriver.DriverAtoms.findElement +import androidx.test.espresso.web.webdriver.DriverAtoms.getText +import androidx.test.espresso.web.webdriver.Locator +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import com.wbrawner.simplemarkdown.view.activity.MainActivity +import org.hamcrest.Matchers.containsString +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.io.File +import java.io.Reader + +class MarkdownTests { + + @get:Rule + var activityRule = IntentsTestRule(MainActivity::class.java, false, false) + + lateinit var file: File + + @Before + fun setup() { + file = File(getApplicationContext().filesDir.absolutePath + "/tmp", "temp.md") + file.parentFile?.mkdirs() + file.delete() + activityRule.launchActivity(null) + activityRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + @Test + @Throws(Exception::class) + fun openAppTest() { + val mDevice = UiDevice.getInstance(getInstrumentation()) + mDevice.pressHome() + // Bring up the default launcher by searching for a UI component + // that matches the content description for the launcher button. + val allAppsButton = mDevice + .findObject(UiSelector().description("Apps")) + + // Perform a click on the button to load the launcher. + allAppsButton.clickAndWaitForNewWindow() + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.wbrawner.simplemarkdown", appContext.packageName) + val appView = UiScrollable(UiSelector().scrollable(true)) + val simpleMarkdownSelector = UiSelector() + .text(getApplicationContext().getString(R.string.app_name_short)) + appView.scrollIntoView(simpleMarkdownSelector) + mDevice.findObject(simpleMarkdownSelector).clickAndWaitForNewWindow() + } + + @Test + fun editAndPreviewMarkdownTest() { + onView(withId(R.id.markdown_edit)).perform(typeText("# Header test")) + onView(withText(R.string.action_preview)).perform(click()) + onWebView(withId(R.id.markdown_view)).forceJavascriptEnabled() + .withElement(findElement(Locator.TAG_NAME, "h1")) + .check(webMatches(getText(), containsString("Header test"))) + } + + @Test + fun newMarkdownTest() { + onView(withId(R.id.markdown_edit)) + .perform(typeText("# UI Testing\n\nThe quick brown fox jumped over the lazy dog.")) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_new)).perform(click()) + onView(withText(R.string.action_discard)).perform(click()) + onView(withId(R.id.markdown_edit)).check(matches(withText(""))) + } + + @Test + fun saveMarkdownWithFileUriTest() { + val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." + onView(withId(R.id.markdown_edit)).perform(typeText(markdownText)) + val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { + data = Uri.fromFile(file) + }) + intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_save_as)).perform(click()) + Thread.sleep(500) + assertEquals(markdownText, file.inputStream().reader().use(Reader::readText)) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + } + + @Test + fun saveMarkdownWithContentUriTest() { + val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." + onView(withId(R.id.markdown_edit)).perform(typeText(markdownText)) + val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { + data = FileProvider.getUriForFile(getApplicationContext(), "com.wbrawner.simplemarkdown.fileprovider", file) + }) + intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_save_as)).perform(click()) + Thread.sleep(500) + assertEquals(markdownText, file.inputStream().reader().use(Reader::readText)) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + } + + @Test + fun loadMarkdownWithFileUriTest() { + val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." + file.outputStream().writer().use { it.write(markdownText) } + val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { + data = Uri.fromFile(file) + }) + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_open)).perform(click()) + Thread.sleep(500) + onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText))) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + } + + @Test + fun loadMarkdownWithContentUriTest() { + val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." + file.outputStream().writer().use { it.write(markdownText) } + val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { + data = FileProvider.getUriForFile(getApplicationContext(), "com.wbrawner.simplemarkdown.fileprovider", file) + }) + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_open)).perform(click()) + Thread.sleep(500) + onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText))) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + } + + + @Test + fun openEditAndSaveMarkdownTest() { + val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog." + file.outputStream().writer().use { it.write(markdownText) } + val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply { + data = Uri.fromFile(file) + }) + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_open)).perform(click()) + Thread.sleep(500) + onView(withId(R.id.markdown_edit)).check(matches(withText(markdownText))) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + val additionalText = "# More info\n\nThis is some additional text" + onView(withId(R.id.markdown_edit)).perform( + clearText(), + typeText(additionalText) + ) + openActionBarOverflowOrOptionsMenu(getApplicationContext()) + onView(withText(R.string.action_save)).perform(click()) + Thread.sleep(500) + onView(withText(getApplicationContext().getString(R.string.file_saved, "temp.md"))) + assertEquals(additionalText, file.inputStream().reader().use(Reader::readText)) + onView(withText("temp.md")).check(matches(withParent(withId(R.id.toolbar)))) + } +} diff --git a/app/src/androidTest/java/com/wbrawner/simplemarkdown/view/activity/AutosaveTest.kt b/app/src/androidTest/java/com/wbrawner/simplemarkdown/view/activity/AutosaveTest.kt deleted file mode 100644 index d95c144..0000000 --- a/app/src/androidTest/java/com/wbrawner/simplemarkdown/view/activity/AutosaveTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.wbrawner.simplemarkdown.view.activity - - -import android.Manifest -import android.view.View -import android.view.ViewGroup -import androidx.test.InstrumentationRegistry.getInstrumentation -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu -import androidx.test.espresso.action.ViewActions.* -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* -import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule -import androidx.test.rule.GrantPermissionRule -import androidx.test.runner.AndroidJUnit4 -import com.wbrawner.simplemarkdown.R -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.allOf -import org.hamcrest.TypeSafeMatcher -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -class AutosaveTest { - - @Rule - var mActivityTestRule = ActivityTestRule(MainActivity::class.java) - - @Rule - var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE) - - @Test - fun autosaveTest() { - val dummyFileName = "dummy-autosave.md" - val realFileName = "test-autosave.md" - val testText = "This should be automatically saved" - - // Create a dummy file that we'll later use to provoke the autosave - saveFile(dummyFileName) - - // Then create our actual file that we expect to be automatically saved. - saveFile(realFileName) - - val appCompatEditText3 = onView( - allOf(withId(R.id.markdown_edit), - childAtPosition( - withParent(withId(R.id.pager)), - 0), - isDisplayed())) - appCompatEditText3.perform(click()) - - val appCompatEditText4 = onView( - allOf(withId(R.id.markdown_edit), - childAtPosition( - withParent(withId(R.id.pager)), - 0), - isDisplayed())) - appCompatEditText4.perform(replaceText(testText), closeSoftKeyboard()) - - // Jump back to the dummy file. This should provoke the autosave - openFile(dummyFileName) - - val editText = onView( - allOf(withId( - R.id.markdown_edit), - childAtPosition( - withParent(withId(R.id.pager)), 0), - isDisplayed()) - ) - - // Assert that the text is empty - editText.check(matches(withText(""))) - - // Then re-open the actual file - openFile(realFileName) - - // And assert that we have our expected text (a newline is appended upon reading the file - // so we'll need to account for that here as well) - editText.check(matches(withText(testText + "\n"))) - } - - private fun saveFile(fileName: String) { - openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) - - // TODO: Rewrite this test -// val appCompatTextView = onView( -// allOf(withId(R.id.title), withText("Save"), -// childAtPosition( -// childAtPosition( -// withClassName(`is`("android.support.v7.view.menu.ListMenuItemView")), -// 0), -// 0), -// isDisplayed())) -// appCompatTextView.perform(click()) -// -// val appCompatEditText = onView( -// allOf(withId(R.id.file_name), -// childAtPosition( -// childAtPosition( -// withId(android.R.id.content), -// 0), -// 3), -// isDisplayed())) -// appCompatEditText.perform(replaceText(fileName)) -// -// appCompatEditText.perform(closeSoftKeyboard()) -// -// val appCompatButton = onView( -// allOf(withId(R.id.button_save), -// childAtPosition( -// childAtPosition( -// withId(android.R.id.content), -// 0), -// 4), -// isDisplayed())) -// appCompatButton.perform(click()) - } - - private fun openFile(fileName: String) { - openActionBarOverflowOrOptionsMenu(getInstrumentation().targetContext) - - val openMenuItem = onView( - allOf(withId(R.id.title), withText("Open"), - childAtPosition( - childAtPosition( - withClassName(`is`("android.support.v7.view.menu.ListMenuItemView")), - 0), - 0), - isDisplayed())) - openMenuItem.perform(click()) - - onView(withText(fileName)) - .perform(click()) - } - - private fun childAtPosition( - parentMatcher: Matcher, position: Int): Matcher { - - return object : TypeSafeMatcher() { - 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)) - } - } - } -} diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/model/MarkdownFile.kt b/app/src/main/java/com/wbrawner/simplemarkdown/model/MarkdownFile.kt deleted file mode 100644 index 547adac..0000000 --- a/app/src/main/java/com/wbrawner/simplemarkdown/model/MarkdownFile.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.wbrawner.simplemarkdown.model - -import java.io.InputStream -import java.io.OutputStream -import java.io.Reader - -/** - * This class serves as a wrapper to manage the manage the file input and output operations, as well - * as to keep track of the data itself in memory. - */ -class MarkdownFile(var name: String = "Untitled.md", var content: String = "") { - -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 073fc06..c5d8808 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,7 +64,7 @@ Save Changes Would you like to save your changes? Discard - Save as... + Save as… @string/pref_value_light @string/pref_value_dark