Add tests for MarkdownViewModel#load

This commit is contained in:
William Brawner 2023-09-12 22:18:00 -06:00
parent ae5b13dfd0
commit c2cc9fbd1c
7 changed files with 115 additions and 187 deletions

View file

@ -150,6 +150,7 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.ui:ui-tooling")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
val coroutinesVersion = "1.7.1" val coroutinesVersion = "1.7.1"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
val lifecycleVersion = "2.2.0" val lifecycleVersion = "2.2.0"

View file

@ -1,181 +0,0 @@
package com.wbrawner.simplemarkdown
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
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 androidx.core.content.FileProvider
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.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.GrantPermissionRule
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 context = getInstrumentation().targetContext
context.packageManager
.getLaunchIntentForPackage(context.packageName)
.apply { context.startActivity(this) }
}
@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 openThenNewMarkdownTest() {
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())
openActionBarOverflowOrOptionsMenu(getApplicationContext())
onView(withText(R.string.action_new)).perform(click())
// The dialog to save or discard changes shouldn't be shown here since no edits were made
onView(withId(R.id.markdown_edit)).check(matches(withText("")))
}
@Test
fun editThenNewMarkdownTest() {
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(), "${BuildConfig.APPLICATION_ID}.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(), "${BuildConfig.APPLICATION_ID}.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

@ -33,7 +33,7 @@ class MarkdownViewModel(
val effects = _effects.asSharedFlow() val effects = _effects.asSharedFlow()
private val saveMutex = Mutex() private val saveMutex = Mutex()
fun updateMarkdown(markdown: String?) = viewModelScope.launch { suspend fun updateMarkdown(markdown: String?) {
this@MarkdownViewModel._markdown.emit(markdown ?: "") this@MarkdownViewModel._markdown.emit(markdown ?: "")
isDirty.set(true) isDirty.set(true)
} }
@ -150,7 +150,6 @@ class MarkdownViewModel(
} }
companion object { companion object {
fun factory(fileHelper: FileHelper, preferenceHelper: PreferenceHelper): ViewModelProvider.Factory = object : ViewModelProvider.Factory { fun factory(fileHelper: FileHelper, preferenceHelper: PreferenceHelper): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create( override fun <T : ViewModel> create(

View file

@ -37,6 +37,7 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -239,7 +240,7 @@ fun MainScreen(
mutableStateOf(TextFieldValue(annotatedMarkdown)) mutableStateOf(TextFieldValue(annotatedMarkdown))
} }
if (page == 0) { if (page == 0) {
BasicTextField( TextField(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(8.dp), .padding(8.dp),
@ -250,14 +251,18 @@ fun MainScreen(
} else { } else {
it it
} }
viewModel.updateMarkdown(it.text) coroutineScope.launch {
viewModel.updateMarkdown(it.text)
}
},
placeholder = {
Text("Markdown here…")
}, },
textStyle = TextStyle.Default.copy( textStyle = TextStyle.Default.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
), ),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
) )
} else { } else {
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown) MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)

View file

@ -0,0 +1,30 @@
package com.wbrawner.simplemarkdown
import com.wbrawner.simplemarkdown.utility.FileHelper
import java.io.File
import java.net.URI
class FakeFileHelper : FileHelper {
override val defaultDirectory: File
get() = File.createTempFile("sm", null)
.apply {
delete()
mkdir()
}
var file: Pair<String, String> = "Untitled.md" to "This is a test file"
var openedUris = ArrayDeque<URI>()
var savedData = ArrayDeque<SavedData>()
override suspend fun open(source: URI): Pair<String, String> {
openedUris.addLast(source)
return file
}
override suspend fun save(destination: URI, content: String): String {
savedData.addLast(SavedData(destination, content))
return file.first
}
}
data class SavedData(val uri: URI, val content: String)

View file

@ -0,0 +1,18 @@
package com.wbrawner.simplemarkdown
import com.wbrawner.simplemarkdown.utility.Preference
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
import kotlinx.coroutines.flow.StateFlow
class FakePreferenceHelper: PreferenceHelper {
val preferences = mutableMapOf<Preference, Any?>()
override fun get(preference: Preference): Any? = preferences[preference]
override fun set(preference: Preference, value: Any?) {
preferences[preference] = value
}
override fun <T> observe(preference: Preference): StateFlow<T> {
TODO("Not yet implemented")
}
}

View file

@ -0,0 +1,56 @@
package com.wbrawner.simplemarkdown
import com.wbrawner.simplemarkdown.utility.Preference
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.net.URI
class MarkdownViewModelTest {
private lateinit var fileHelper: FakeFileHelper
private lateinit var preferenceHelper: FakePreferenceHelper
private lateinit var viewModel: MarkdownViewModel
@Before
fun setup() {
fileHelper = FakeFileHelper()
preferenceHelper = FakePreferenceHelper()
viewModel = MarkdownViewModel(fileHelper, preferenceHelper)
}
@Test
fun testMarkdownUpdate() = runBlocking {
assertEquals("", viewModel.markdown.value)
viewModel.updateMarkdown("Updated content")
assertEquals("Updated content", viewModel.markdown.value)
}
@Test
fun testLoadWithNoPathAndNoAutosaveUri() = runBlocking {
viewModel.load(null)
assertTrue(fileHelper.openedUris.isEmpty())
}
@Test
fun testLoadWithNoPathAndAutosaveUri() = runBlocking {
val uri = URI.create("file:///home/user/Untitled.md")
preferenceHelper[Preference.AUTOSAVE_URI] = uri.toString()
viewModel.load(null)
assertEquals(uri, fileHelper.openedUris.firstOrNull())
val (fileName, contents) = fileHelper.file
assertEquals(fileName, viewModel.fileName.value)
assertEquals(contents, viewModel.markdown.value)
}
@Test
fun testLoadWithPath() = runBlocking {
val uri = URI.create("file:///home/user/Untitled.md")
viewModel.load(uri.toString())
assertEquals(uri, fileHelper.openedUris.firstOrNull())
val (fileName, contents) = fileHelper.file
assertEquals(fileName, viewModel.fileName.value)
assertEquals(contents, viewModel.markdown.value)
}
}