Redo UI tests to cover basic markdown editing flows

This commit is contained in:
William 'Billy' Brawner 2019-08-19 19:23:24 -07:00 committed by William Brawner
parent 112b776080
commit 764c3fa72e
6 changed files with 199 additions and 247 deletions

View file

@ -29,7 +29,7 @@ android {
exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE'
exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/DEPENDENCIES'
} }
compileSdkVersion 28 compileSdkVersion 29
buildToolsVersion '28.0.3' buildToolsVersion '28.0.3'
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -38,7 +38,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.wbrawner.simplemarkdown" applicationId "com.wbrawner.simplemarkdown"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 29
versionCode 20 versionCode 20
versionName "0.7.0" versionName "0.7.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -70,6 +70,7 @@ android {
unitTests { unitTests {
includeAndroidResources = true includeAndroidResources = true
} }
execution 'ANDROIDX_TEST_ORCHESTRATOR'
} }
} }
@ -77,12 +78,15 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:4.2' testImplementation 'org.robolectric:robolectric:4.2'
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { def espresso_version = '3.2.0'
exclude group: 'com.android.support', module: 'support-annotations' androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
}) androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version"
androidTestImplementation 'androidx.test:runner:1.2.0' def android_test = '1.2.0'
androidTestImplementation 'androidx.test:rules: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' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
@ -93,7 +97,7 @@ dependencies {
annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1' annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1'
kapt 'com.google.dagger:dagger-android-processor:2.22.1' kapt 'com.google.dagger:dagger-android-processor:2.22.1'
kapt 'com.google.dagger:dagger-compiler: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.android.billingclient:billing:1.2'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation "androidx.core:core-ktx:1.0.2" implementation "androidx.core:core-ktx:1.0.2"
@ -121,7 +125,7 @@ tasks.withType(Test) {
jacoco.includeNoLocationClasses = true jacoco.includeNoLocationClasses = true
} }
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
reports { reports {
xml.enabled = true xml.enabled = true
html.enabled = true html.enabled = true
@ -135,6 +139,7 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
sourceDirectories = files([mainSrc]) sourceDirectories = files([mainSrc])
classDirectories = files([javaDebugTree, kotlinDebugTree]) classDirectories = files([javaDebugTree, kotlinDebugTree])
executionData = fileTree(dir: project.buildDir, includes: [ executionData = fileTree(dir: project.buildDir, includes: [
'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec' 'jacoco/testDebugUnitTest.exec',
'outputs/code-coverage/connected/*coverage.ec'
]) ])
} }

View file

@ -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())
}
}

View file

@ -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<Context>().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<Context>().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<Context>().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))))
}
}

View file

@ -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<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))
}
}
}
}

View file

@ -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 = "") {
}

View file

@ -64,7 +64,7 @@
<string name="save_changes">Save Changes</string> <string name="save_changes">Save Changes</string>
<string name="prompt_save_changes">Would you like to save your changes?</string> <string name="prompt_save_changes">Would you like to save your changes?</string>
<string name="action_discard">Discard</string> <string name="action_discard">Discard</string>
<string name="action_save_as">Save as...</string> <string name="action_save_as">Save as</string>
<string-array name="pref_entries_dark_mode"> <string-array name="pref_entries_dark_mode">
<item>@string/pref_value_light</item> <item>@string/pref_value_light</item>
<item>@string/pref_value_dark</item> <item>@string/pref_value_dark</item>