From 2e4514377debe4b7eb6b5ef7934df5e8cffbf665 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Thu, 22 Aug 2024 19:09:39 -0600 Subject: [PATCH 1/2] Rewrite UI tests to use robots --- .../com/wbrawner/simplemarkdown/HelpTest.kt | 27 ++ .../wbrawner/simplemarkdown/MarkdownTests.kt | 286 +++++++----------- .../com/wbrawner/simplemarkdown/TestUtils.kt | 29 ++ .../simplemarkdown/robot/MainScreenRobot.kt | 78 +++++ .../robot/MarkdownInfoScreenRobot.kt | 7 + .../robot/NavigationDrawerRobot.kt | 16 + .../simplemarkdown/robot/TopAppBarRobot.kt | 24 ++ .../simplemarkdown/robot/WebViewRobot.kt | 24 ++ 8 files changed, 310 insertions(+), 181 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/HelpTest.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/TestUtils.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MainScreenRobot.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MarkdownInfoScreenRobot.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/NavigationDrawerRobot.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/TopAppBarRobot.kt create mode 100644 app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/WebViewRobot.kt diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/HelpTest.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/HelpTest.kt new file mode 100644 index 0000000..d84f82f --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/HelpTest.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/MarkdownTests.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/MarkdownTests.kt index a3876b7..6502c20 100644 --- a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/MarkdownTests.kt +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/MarkdownTests.kt @@ -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() } } diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/TestUtils.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/TestUtils.kt new file mode 100644 index 0000000..f3f59e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/TestUtils.kt @@ -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 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 +} diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MainScreenRobot.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MainScreenRobot.kt new file mode 100644 index 0000000..161ef63 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MainScreenRobot.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MarkdownInfoScreenRobot.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MarkdownInfoScreenRobot.kt new file mode 100644 index 0000000..7d56a53 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/MarkdownInfoScreenRobot.kt @@ -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() \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/NavigationDrawerRobot.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/NavigationDrawerRobot.kt new file mode 100644 index 0000000..1ad687d --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/NavigationDrawerRobot.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/TopAppBarRobot.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/TopAppBarRobot.kt new file mode 100644 index 0000000..0995c95 --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/TopAppBarRobot.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/WebViewRobot.kt b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/WebViewRobot.kt new file mode 100644 index 0000000..ee9d59c --- /dev/null +++ b/app/src/androidTest/kotlin/com/wbrawner/simplemarkdown/robot/WebViewRobot.kt @@ -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))) + } +} \ No newline at end of file -- 2.45.2 From 61e6da997d17471e54cd4981a4a9cf8ae5a58fbf Mon Sep 17 00:00:00 2001 From: William Brawner Date: Thu, 22 Aug 2024 19:10:05 -0600 Subject: [PATCH 2/2] Fix crash on markdown preview --- .../simplemarkdown/ui/MarkdownText.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownText.kt b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownText.kt index dbdbc61..ae2796d 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownText.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/ui/MarkdownText.kt @@ -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_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" \ No newline at end of file -- 2.45.2