diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76cf219..ccb9a1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,6 +150,7 @@ dependencies { implementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-extended") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") val coroutinesVersion = "1.7.1" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") val lifecycleVersion = "2.2.0" diff --git a/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt b/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt deleted file mode 100644 index bb847ef..0000000 --- a/app/src/androidTest/java/com/wbrawner/simplemarkdown/MarkdownTests.kt +++ /dev/null @@ -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().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().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/main/java/com/wbrawner/simplemarkdown/MarkdownViewModel.kt b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownViewModel.kt index ba3483c..ad73a46 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownViewModel.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownViewModel.kt @@ -33,7 +33,7 @@ class MarkdownViewModel( val effects = _effects.asSharedFlow() private val saveMutex = Mutex() - fun updateMarkdown(markdown: String?) = viewModelScope.launch { + suspend fun updateMarkdown(markdown: String?) { this@MarkdownViewModel._markdown.emit(markdown ?: "") isDirty.set(true) } @@ -150,7 +150,6 @@ class MarkdownViewModel( } companion object { - fun factory(fileHelper: FileHelper, preferenceHelper: PreferenceHelper): ViewModelProvider.Factory = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MainScreen.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MainScreen.kt index b74f62b..cacbc45 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MainScreen.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MainScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable @@ -239,7 +240,7 @@ fun MainScreen( mutableStateOf(TextFieldValue(annotatedMarkdown)) } if (page == 0) { - BasicTextField( + TextField( modifier = Modifier .fillMaxSize() .padding(8.dp), @@ -250,14 +251,18 @@ fun MainScreen( } else { it } - viewModel.updateMarkdown(it.text) + coroutineScope.launch { + viewModel.updateMarkdown(it.text) + } + }, + placeholder = { + Text("Markdown hereā€¦") }, textStyle = TextStyle.Default.copy( fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onSurface ), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences) ) } else { MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown) diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/FakeFileHelper.kt b/app/src/test/java/com/wbrawner/simplemarkdown/FakeFileHelper.kt new file mode 100644 index 0000000..3d44de2 --- /dev/null +++ b/app/src/test/java/com/wbrawner/simplemarkdown/FakeFileHelper.kt @@ -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 = "Untitled.md" to "This is a test file" + var openedUris = ArrayDeque() + var savedData = ArrayDeque() + + override suspend fun open(source: URI): Pair { + 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) \ No newline at end of file diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/FakePreferenceHelper.kt b/app/src/test/java/com/wbrawner/simplemarkdown/FakePreferenceHelper.kt new file mode 100644 index 0000000..655e3e4 --- /dev/null +++ b/app/src/test/java/com/wbrawner/simplemarkdown/FakePreferenceHelper.kt @@ -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() + override fun get(preference: Preference): Any? = preferences[preference] + + override fun set(preference: Preference, value: Any?) { + preferences[preference] = value + } + + override fun observe(preference: Preference): StateFlow { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/test/java/com/wbrawner/simplemarkdown/MarkdownViewModelTest.kt b/app/src/test/java/com/wbrawner/simplemarkdown/MarkdownViewModelTest.kt new file mode 100644 index 0000000..0d99a38 --- /dev/null +++ b/app/src/test/java/com/wbrawner/simplemarkdown/MarkdownViewModelTest.kt @@ -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) + } +} \ No newline at end of file