Compare commits
6 commits
98688cbc3b
...
01978548c6
Author | SHA1 | Date | |
---|---|---|---|
01978548c6 | |||
b42b949bdb | |||
040777b99a | |||
307e7642b9 | |||
617035c424 | |||
9ce5e129a6 |
16 changed files with 369 additions and 204 deletions
|
@ -181,7 +181,7 @@ fladle {
|
|||
environmentVariables.put("clearPackageData", "true")
|
||||
testTimeout.set("7m")
|
||||
devices.add(
|
||||
mapOf("model" to "NexusLowRes", "version" to "29")
|
||||
mapOf("model" to "Pixel2.arm", "version" to "33")
|
||||
)
|
||||
projectId.set("simplemarkdown")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.wbrawner.simplemarkdown
|
||||
|
||||
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import com.wbrawner.simplemarkdown.robot.onMainScreen
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class HelpTest {
|
||||
@get:Rule
|
||||
val composeRule = createEmptyComposeRule()
|
||||
|
||||
@Test
|
||||
fun openHelpPageTest() {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
checkMarkdownEquals("")
|
||||
openDrawer()
|
||||
} onNavigationDrawer {
|
||||
openHelpPage()
|
||||
} onHelpScreen {
|
||||
checkTitleEquals("Help")
|
||||
verifyH1("Headings/Titles")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,27 +5,7 @@ import android.app.Instrumentation
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.hasAnyDescendant
|
||||
import androidx.compose.ui.test.hasAnySibling
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextReplacement
|
||||
import androidx.compose.ui.test.printToLog
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
|
@ -34,14 +14,9 @@ import androidx.test.espresso.intent.Intents.intending
|
|||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.rule.IntentsRule
|
||||
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 com.wbrawner.simplemarkdown.robot.onMainScreen
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
|
@ -79,13 +54,13 @@ class MarkdownTests {
|
|||
@Test
|
||||
fun editAndPreviewMarkdownTest() {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.typeMarkdown("# Header test")
|
||||
composeRule.checkMarkdownEquals("# Header test")
|
||||
composeRule.openPreview()
|
||||
onWebView(isAssignableFrom(WebView::class.java))
|
||||
.forceJavascriptEnabled()
|
||||
.withElement(findElement(Locator.TAG_NAME, "h1"))
|
||||
.check(webMatches(getText(), containsString("Header test")))
|
||||
onMainScreen(composeRule) {
|
||||
typeMarkdown("# Header test")
|
||||
checkMarkdownEquals("# Header test")
|
||||
openPreview()
|
||||
} onPreview {
|
||||
verifyH1("Header test")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -98,102 +73,113 @@ class MarkdownTests {
|
|||
})
|
||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickOpenMenuItem()
|
||||
composeRule.checkMarkdownEquals(markdownText)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickNewMenuItem()
|
||||
composeRule.verifyDialogIsNotShown()
|
||||
composeRule.checkMarkdownEquals("")
|
||||
onMainScreen(composeRule) {
|
||||
openMenu()
|
||||
clickOpenMenuItem()
|
||||
checkMarkdownEquals(markdownText)
|
||||
openMenu()
|
||||
clickNewMenuItem()
|
||||
verifyDialogIsNotShown()
|
||||
checkMarkdownEquals("")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun editThenNewMarkdownTest() {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||
composeRule.typeMarkdown(markdownText)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickNewMenuItem()
|
||||
composeRule.onNode(isDialog()).printToLog("TestDebugging")
|
||||
composeRule.verifyDialogIsShown("Would you like to save your changes?")
|
||||
composeRule.discardChanges()
|
||||
composeRule.checkMarkdownEquals("")
|
||||
onMainScreen(composeRule) {
|
||||
typeMarkdown(markdownText)
|
||||
openMenu()
|
||||
clickNewMenuItem()
|
||||
verifyDialogIsShown("Would you like to save your changes?")
|
||||
discardChanges()
|
||||
checkMarkdownEquals("")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveMarkdownWithFileUriTest() = runTest {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.checkTitleEquals("Untitled.md")
|
||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||
composeRule.typeMarkdown(markdownText)
|
||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||
data = Uri.fromFile(file)
|
||||
})
|
||||
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickSaveMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||
typeMarkdown(markdownText)
|
||||
val activityResult = Instrumentation.ActivityResult(RESULT_OK, Intent().apply {
|
||||
data = Uri.fromFile(file)
|
||||
})
|
||||
intending(hasAction(Intent.ACTION_CREATE_DOCUMENT)).respondWith(activityResult)
|
||||
openMenu()
|
||||
clickSaveMenuItem()
|
||||
awaitIdle()
|
||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||
checkTitleEquals("temp.md")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveMarkdownWithContentUriTest() = runTest {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.checkTitleEquals("Untitled.md")
|
||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||
composeRule.typeMarkdown(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)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickSaveMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
val markdownText = "# UI Testing\n\nThe quick brown fox jumped over the lazy dog."
|
||||
typeMarkdown(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)
|
||||
openMenu()
|
||||
clickSaveMenuItem()
|
||||
awaitIdle()
|
||||
assertEquals(markdownText, file.inputStream().reader().use(Reader::readText))
|
||||
checkTitleEquals("temp.md")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadMarkdownWithFileUriTest() = runTest {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.checkTitleEquals("Untitled.md")
|
||||
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)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickOpenMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
composeRule.checkMarkdownEquals(markdownText)
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
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)
|
||||
openMenu()
|
||||
clickOpenMenuItem()
|
||||
awaitIdle()
|
||||
checkMarkdownEquals(markdownText)
|
||||
checkTitleEquals("temp.md")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadMarkdownWithContentUriTest() = runTest {
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.checkTitleEquals("Untitled.md")
|
||||
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)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickOpenMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
composeRule.checkMarkdownEquals(markdownText)
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
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)
|
||||
openMenu()
|
||||
clickOpenMenuItem()
|
||||
awaitIdle()
|
||||
checkMarkdownEquals(markdownText)
|
||||
checkTitleEquals("temp.md")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -206,84 +192,22 @@ class MarkdownTests {
|
|||
})
|
||||
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(activityResult)
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
composeRule.checkTitleEquals("Untitled.md")
|
||||
composeRule.openMenu()
|
||||
composeRule.clickOpenMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
composeRule.verifyTextIsShown("Successfully loaded temp.md")
|
||||
composeRule.checkMarkdownEquals(markdownText)
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
val additionalText = "# More info\n\nThis is some additional text"
|
||||
composeRule.typeMarkdown(additionalText)
|
||||
composeRule.openMenu()
|
||||
composeRule.clickSaveMenuItem()
|
||||
composeRule.awaitIdle()
|
||||
composeRule.verifyTextIsShown("Successfully saved temp.md")
|
||||
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
|
||||
composeRule.checkTitleEquals("temp.md")
|
||||
}
|
||||
|
||||
private fun ComposeTestRule.checkTitleEquals(title: String) =
|
||||
onNode(hasAnySibling(hasContentDescription("Main Menu")).and(hasText(title)))
|
||||
.waitUntilIsDisplayed()
|
||||
|
||||
private fun ComposeTestRule.typeMarkdown(markdown: String) =
|
||||
onNode(hasSetTextAction()).performTextReplacement(markdown)
|
||||
|
||||
|
||||
private fun ComposeTestRule.checkMarkdownEquals(markdown: String) {
|
||||
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
|
||||
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
|
||||
onMainScreen(composeRule) {
|
||||
checkTitleEquals("Untitled.md")
|
||||
openMenu()
|
||||
clickOpenMenuItem()
|
||||
awaitIdle()
|
||||
verifyTextIsShown("Successfully loaded temp.md")
|
||||
checkMarkdownEquals(markdownText)
|
||||
checkTitleEquals("temp.md")
|
||||
val additionalText = "# More info\n\nThis is some additional text"
|
||||
typeMarkdown(additionalText)
|
||||
openMenu()
|
||||
clickSaveMenuItem()
|
||||
awaitIdle()
|
||||
verifyTextIsShown("Successfully saved temp.md")
|
||||
assertEquals(additionalText, file.inputStream().reader().use(Reader::readText))
|
||||
checkTitleEquals("temp.md")
|
||||
}
|
||||
onNode(hasSetTextAction()).waitUntil {
|
||||
assert(markdownMatcher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeTestRule.openPreview() = onNodeWithText("Preview").performClick()
|
||||
|
||||
private fun ComposeTestRule.openMenu() =
|
||||
onNodeWithContentDescription("Editor Actions").performClick()
|
||||
|
||||
private fun ComposeTestRule.clickOpenMenuItem() = onNodeWithText("Open").performClick()
|
||||
|
||||
private fun ComposeTestRule.clickNewMenuItem() = onNodeWithText("New").performClick()
|
||||
|
||||
private fun ComposeTestRule.clickSaveMenuItem() = onNodeWithText("Save").performClick()
|
||||
|
||||
private fun ComposeTestRule.verifyDialogIsShown(text: String) =
|
||||
onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed()
|
||||
|
||||
private fun ComposeTestRule.verifyDialogIsNotShown() =
|
||||
onNode(isDialog()).waitUntilIsNotDisplayed()
|
||||
|
||||
private fun ComposeTestRule.discardChanges() = onNodeWithText("No").performClick()
|
||||
|
||||
private fun ComposeTestRule.verifyTextIsShown(text: String) =
|
||||
onNodeWithText(text).waitUntilIsDisplayed()
|
||||
|
||||
private val ASSERTION_TIMEOUT = 5_000L
|
||||
|
||||
private fun SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> Unit) {
|
||||
val start = System.currentTimeMillis()
|
||||
lateinit var assertionError: AssertionError
|
||||
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
|
||||
try {
|
||||
assertion()
|
||||
return
|
||||
} catch (e: AssertionError) {
|
||||
assertionError = e
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
throw assertionError
|
||||
}
|
||||
|
||||
private fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
|
||||
assertIsDisplayed()
|
||||
}
|
||||
|
||||
private fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
|
||||
assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.wbrawner.simplemarkdown
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
|
||||
private const val ASSERTION_TIMEOUT = 5_000L
|
||||
|
||||
fun SemanticsNodeInteraction.waitUntilIsDisplayed() = waitUntil {
|
||||
assertIsDisplayed()
|
||||
}
|
||||
|
||||
fun SemanticsNodeInteraction.waitUntilIsNotDisplayed() = waitUntil {
|
||||
assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
fun <T> SemanticsNodeInteraction.waitUntil(assertion: SemanticsNodeInteraction.() -> T): T {
|
||||
val start = System.currentTimeMillis()
|
||||
lateinit var assertionError: AssertionError
|
||||
while (System.currentTimeMillis() - start < ASSERTION_TIMEOUT) {
|
||||
try {
|
||||
return assertion()
|
||||
} catch (e: AssertionError) {
|
||||
assertionError = e
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
throw assertionError
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.wbrawner.simplemarkdown.robot
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.hasAnyDescendant
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextReplacement
|
||||
import com.wbrawner.simplemarkdown.waitUntil
|
||||
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
|
||||
import com.wbrawner.simplemarkdown.waitUntilIsNotDisplayed
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
fun onMainScreen(composeRule: ComposeTestRule, block: MainScreenRobot.() -> Unit) =
|
||||
MainScreenRobot(composeRule).apply(block)
|
||||
|
||||
@Suppress("UnusedReceiverParameter") // Used to avoid import ambiguity for tests
|
||||
suspend fun CoroutineScope.onMainScreen(
|
||||
composeRule: ComposeTestRule,
|
||||
block: suspend MainScreenRobot.() -> Unit
|
||||
) {
|
||||
block.invoke(MainScreenRobot(composeRule))
|
||||
}
|
||||
|
||||
class MainScreenRobot(private val composeRule: ComposeTestRule) :
|
||||
TopAppBarRobot by ComposeTopAppBarRobot(composeRule) {
|
||||
|
||||
fun typeMarkdown(markdown: String) = composeRule.onNode(hasSetTextAction())
|
||||
.performTextReplacement(markdown)
|
||||
|
||||
fun checkMarkdownEquals(markdown: String) {
|
||||
val markdownMatcher = SemanticsMatcher("Markdown = [$markdown]") {
|
||||
it.config.getOrNull(SemanticsProperties.EditableText)?.text == markdown
|
||||
}
|
||||
composeRule.onNode(hasSetTextAction()).waitUntil {
|
||||
assert(markdownMatcher)
|
||||
}
|
||||
}
|
||||
|
||||
fun openPreview() = composeRule.onNodeWithText("Preview").performClick()
|
||||
|
||||
fun openMenu() = composeRule.onNodeWithContentDescription("Editor Actions").performClick()
|
||||
|
||||
fun clickOpenMenuItem() = composeRule.onNodeWithText("Open").performClick()
|
||||
|
||||
fun clickNewMenuItem() = composeRule.onNodeWithText("New").performClick()
|
||||
|
||||
fun clickSaveMenuItem() = composeRule.onNodeWithText("Save").performClick()
|
||||
|
||||
fun verifyDialogIsShown(text: String) =
|
||||
composeRule.onNode(isDialog().and(hasAnyDescendant(hasText(text)))).waitUntilIsDisplayed()
|
||||
|
||||
fun verifyDialogIsNotShown() = composeRule.onNode(isDialog()).waitUntilIsNotDisplayed()
|
||||
|
||||
fun discardChanges() = composeRule.onNodeWithText("No").performClick()
|
||||
|
||||
fun verifyTextIsShown(text: String) = composeRule.onNodeWithText(text).waitUntilIsDisplayed()
|
||||
|
||||
fun openDrawer() = composeRule.onNode(hasClickAction() and hasContentDescription("Main Menu"))
|
||||
.waitUntilIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
suspend fun awaitIdle() = composeRule.awaitIdle()
|
||||
|
||||
infix fun onPreview(block: WebViewRobot.() -> Unit) = EspressoWebViewRobot().apply(block)
|
||||
|
||||
infix fun onNavigationDrawer(block: NavigationDrawerRobot.() -> Unit): NavigationDrawerRobot =
|
||||
NavigationDrawerRobot(composeRule).apply(block = block)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.wbrawner.simplemarkdown.robot
|
||||
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
|
||||
class MarkdownInfoScreenRobot(private val composeTestRule: ComposeTestRule) :
|
||||
TopAppBarRobot by ComposeTopAppBarRobot(composeTestRule),
|
||||
WebViewRobot by EspressoWebViewRobot()
|
|
@ -0,0 +1,16 @@
|
|||
package com.wbrawner.simplemarkdown.robot
|
||||
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
|
||||
|
||||
class NavigationDrawerRobot(private val composeTestRule: ComposeTestRule) {
|
||||
fun openHelpPage() = composeTestRule.onNode(hasClickAction() and hasText("Help"))
|
||||
.waitUntilIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
infix fun onHelpScreen(block: MarkdownInfoScreenRobot.() -> Unit) =
|
||||
MarkdownInfoScreenRobot(composeTestRule).apply(block)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.wbrawner.simplemarkdown.robot
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.hasAnySibling
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import com.wbrawner.simplemarkdown.waitUntilIsDisplayed
|
||||
|
||||
interface TopAppBarRobot {
|
||||
fun checkTitleEquals(title: String): SemanticsNodeInteraction
|
||||
}
|
||||
|
||||
class ComposeTopAppBarRobot(private val composeTestRule: ComposeTestRule) : TopAppBarRobot {
|
||||
override fun checkTitleEquals(title: String) =
|
||||
composeTestRule.onNode(
|
||||
hasAnySibling(
|
||||
hasContentDescription("Main Menu") or hasContentDescription(
|
||||
"Back"
|
||||
)
|
||||
).and(hasText(title))
|
||||
)
|
||||
.waitUntilIsDisplayed()
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.wbrawner.simplemarkdown.robot
|
||||
|
||||
import android.webkit.WebView
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
|
||||
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 org.hamcrest.CoreMatchers.containsString
|
||||
|
||||
interface WebViewRobot {
|
||||
fun verifyH1(text: String)
|
||||
}
|
||||
|
||||
class EspressoWebViewRobot : WebViewRobot {
|
||||
private fun findWebView() = onWebView(isAssignableFrom(WebView::class.java))
|
||||
.forceJavascriptEnabled()
|
||||
|
||||
override fun verifyH1(text: String) {
|
||||
findWebView().withElement(findElement(Locator.TAG_NAME, "h1"))
|
||||
.check(webMatches(getText(), containsString(text)))
|
||||
}
|
||||
}
|
|
@ -38,7 +38,9 @@ class MarkdownApplication : Application() {
|
|||
}
|
||||
}
|
||||
}
|
||||
Timber.plant(ErrorReporterTree.create(this))
|
||||
coroutineScope.launch {
|
||||
Timber.plant(ErrorReporterTree.create(this@MarkdownApplication))
|
||||
}
|
||||
super.onCreate()
|
||||
ReviewHelper.init(this)
|
||||
fileHelper = AndroidFileHelper(this)
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import com.wbrawner.simplemarkdown.core.LocalOnlyException
|
||||
import com.wbrawner.simplemarkdown.utility.FileHelper
|
||||
import com.wbrawner.simplemarkdown.utility.Preference
|
||||
import com.wbrawner.simplemarkdown.utility.PreferenceHelper
|
||||
|
@ -109,7 +110,7 @@ class MarkdownViewModel(
|
|||
preferenceHelper[Preference.AUTOSAVE_URI] = actualLoadPath
|
||||
} ?: throw IllegalStateException("Opened file was null")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to open file at path: $actualLoadPath")
|
||||
Timber.e(LocalOnlyException(e), "Failed to open file at path: $actualLoadPath")
|
||||
_state.value = _state.value.copy(
|
||||
alert = AlertDialogModel(
|
||||
text = ParameterizedText(R.string.file_load_error),
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package com.wbrawner.simplemarkdown.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color.TRANSPARENT
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -15,7 +17,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.wbrawner.simplemarkdown.BuildConfig
|
||||
|
@ -91,22 +92,34 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
|
|||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||
FrameLayout(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
isNestedScrollingEnabled = false
|
||||
settings.javaScriptEnabled = true
|
||||
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||
addView(
|
||||
WebView(context).apply {
|
||||
tag = WEBVIEW_TAG
|
||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
setBackgroundColor(TRANSPARENT)
|
||||
isNestedScrollingEnabled = false
|
||||
settings.javaScriptEnabled = true
|
||||
loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
update = { webView ->
|
||||
webView.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||
update = { frameLayout ->
|
||||
frameLayout.findViewWithTag<WebView>(WEBVIEW_TAG)
|
||||
.loadDataWithBaseURL(null, style + html, "text/html", "UTF-8", null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private const val WEBVIEW_TAG = "com.wbrawner.simplemarkdown.MarkdownText#WebView"
|
||||
|
||||
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
|
@ -3,7 +3,6 @@ package com.wbrawner.simplemarkdown.utility
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -34,8 +33,10 @@ interface FileHelper {
|
|||
}
|
||||
|
||||
class AndroidFileHelper(private val context: Context) : FileHelper {
|
||||
override val defaultDirectory: File = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
|
||||
?: context.filesDir
|
||||
override val defaultDirectory: File by lazy {
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
|
||||
?: context.filesDir
|
||||
}
|
||||
|
||||
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val uri = source.toString().toUri()
|
||||
|
|
|
@ -19,10 +19,17 @@ interface PreferenceHelper {
|
|||
}
|
||||
|
||||
class AndroidPreferenceHelper(context: Context, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)): PreferenceHelper {
|
||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val states = Preference.entries.associateWith { MutableStateFlow(get(it)) }
|
||||
private val sharedPreferences by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
private val states by lazy {
|
||||
val allPrefs: Map<String, Any?> = sharedPreferences.all
|
||||
Preference.entries.associateWith { preference ->
|
||||
MutableStateFlow(allPrefs[preference.key] ?: preference.default)
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(preference: Preference): Any? = sharedPreferences.all[preference.key]?: preference.default
|
||||
override fun get(preference: Preference): Any? = states[preference]?.value
|
||||
|
||||
override fun set(preference: Preference, value: Any?) {
|
||||
sharedPreferences.edit {
|
||||
|
|
|
@ -54,6 +54,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.acra.core)
|
||||
implementation(libs.acra.http)
|
||||
runtimeOnly(libs.acra.limiter)
|
||||
|
|
|
@ -2,7 +2,8 @@ package com.wbrawner.simplemarkdown.core
|
|||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import org.acra.ACRA
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
|
@ -13,22 +14,32 @@ import timber.log.Timber
|
|||
class ErrorReporterTree private constructor(): Timber.Tree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (priority != Log.ERROR) return
|
||||
t?.sendSilentlyWithAcra()
|
||||
if (t !is LocalOnlyException) {
|
||||
t?.sendSilentlyWithAcra()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(application: Application): ErrorReporterTree {
|
||||
suspend fun create(application: Application): ErrorReporterTree {
|
||||
application.createErrorReporterTree()
|
||||
return ErrorReporterTree()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.createErrorReporterTree() {
|
||||
/**
|
||||
* An exception wrapper that prevents exceptions from being sent to an error reporter. Useful for
|
||||
* logging things like IOExceptions that are useful to see locally but not so helpful if reported
|
||||
*/
|
||||
class LocalOnlyException(override val message: String?, override val cause: Throwable): Exception(message, cause) {
|
||||
constructor(cause: Throwable): this(null, cause)
|
||||
}
|
||||
|
||||
private suspend fun Application.createErrorReporterTree() = withContext(Dispatchers.IO) {
|
||||
initAcra {
|
||||
reportFormat = StringFormat.JSON
|
||||
httpSender {
|
||||
uri = "${BuildConfig.ACRA_URL}/report" /*best guess, you may need to adjust this*/
|
||||
uri = "${BuildConfig.ACRA_URL}/report"
|
||||
basicAuthLogin = BuildConfig.ACRA_USER
|
||||
basicAuthPassword = BuildConfig.ACRA_PASS
|
||||
httpMethod = HttpSender.Method.POST
|
||||
|
|
Loading…
Reference in a new issue