From eae6cb7ca68031dd9b1fcb4d1bd4329d0daf0d90 Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 11:32:43 +0300 Subject: [PATCH 1/7] Create `MemoizingUriHandler`. --- .../io/spine/examples/pingh/desktop/Window.kt | 25 ++++++--- .../io/spine/examples/pingh/desktop/UiTest.kt | 13 ++++- .../desktop/given/MemoizingUriHandler.kt | 56 +++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/MemoizingUriHandler.kt diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt index 0690faab..2f88c42a 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt @@ -30,11 +30,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window as ComposeWindow @@ -47,21 +50,29 @@ import io.spine.examples.pingh.client.PinghApplication * * @param state The state of the application window. * @param app Manages the logic for the Pingh app. + * @param uriHandler The `CompositionLocal` that provides functionality for handling URL, + * such as opening a URI. * @see [PlatformWindow] */ @Composable -internal fun Window(state: WindowState, app: PinghApplication) { +internal fun Window( + state: WindowState, + app: PinghApplication, + uriHandler: UriHandler = LocalUriHandler.current +) { PlatformWindow( title = state.title, isShown = state.isShown, onClose = state::hide ) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.small) - ) { - CurrentPage(app) + CompositionLocalProvider(LocalUriHandler provides uriHandler) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.small) + ) { + CurrentPage(app) + } } } } diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt index 62f1004a..60efe80f 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt @@ -29,6 +29,7 @@ package io.spine.examples.pingh.desktop import androidx.compose.runtime.remember import androidx.compose.ui.test.ComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi +import io.spine.examples.pingh.desktop.given.MemoizingUriHandler import io.spine.examples.pingh.testing.client.IntegrationTest import org.junit.jupiter.api.AfterEach @@ -39,10 +40,12 @@ import org.junit.jupiter.api.AfterEach internal abstract class UiTest : IntegrationTest() { private var state: AppState? = null + private val uriHandler = MemoizingUriHandler() @AfterEach internal fun shutdownChannel() { state?.app?.close() + uriHandler.reset() } /** @@ -54,8 +57,16 @@ internal abstract class UiTest : IntegrationTest() { Theme { val settings = retrieveSystemSettings() state = remember { AppState(settings) } - Window(state!!.window, state!!.app) + Window(state!!.window, state!!.app, uriHandler) } } } + + /** + * Returns the number of opened URLs. + * + * To correctly count opened links, the test application + * must be executed using [runApp()][ComposeUiTest.runApp] method. + */ + protected fun openedUrlCount(): Int = uriHandler.urlCount } diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/MemoizingUriHandler.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/MemoizingUriHandler.kt new file mode 100644 index 00000000..1278a519 --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/MemoizingUriHandler.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.pingh.desktop.given + +import androidx.compose.ui.platform.UriHandler + +/** + * Stores the count of URLs that should be opened. + * + * Does not actually open any URLs. Intended for testing purposes only. + */ +internal class MemoizingUriHandler : UriHandler { + /** + * The number of URL that should be opened. + */ + internal var urlCount = 0 + private set + + /** + * Adds a URL to the [total count][urlCount] of URLs to be opened. + */ + override fun openUri(uri: String) { + urlCount++ + } + + /** + * Resets the count of opened URLs to zero. + */ + internal fun reset() { + urlCount = 0 + } +} From 9af335c83c5cd12cfa5564f5b526c9a76751f3e1 Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 11:35:21 +0300 Subject: [PATCH 2/7] Create `logIn` method in `UiTest` class. --- .../examples/pingh/desktop/LoginPageUiTest.kt | 2 -- .../io/spine/examples/pingh/desktop/UiTest.kt | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/LoginPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/LoginPageUiTest.kt index d0d02c4e..d6e9eaee 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/LoginPageUiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/LoginPageUiTest.kt @@ -46,8 +46,6 @@ import org.junit.jupiter.api.DisplayName @OptIn(ExperimentalTestApi::class) internal class LoginPageUiTest : UiTest() { - private val username = "MykytaPimonovTD" - private val SemanticsNodeInteractionsProvider.loginButton get() = onNodeWithTag("login-button") diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt index 60efe80f..4cf9cc4a 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt @@ -29,6 +29,11 @@ package io.spine.examples.pingh.desktop import androidx.compose.runtime.remember import androidx.compose.ui.test.ComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact import io.spine.examples.pingh.desktop.given.MemoizingUriHandler import io.spine.examples.pingh.testing.client.IntegrationTest import org.junit.jupiter.api.AfterEach @@ -39,6 +44,7 @@ import org.junit.jupiter.api.AfterEach */ internal abstract class UiTest : IntegrationTest() { + protected val username = "MykytaPimonovTD" private var state: AppState? = null private val uriHandler = MemoizingUriHandler() @@ -69,4 +75,19 @@ internal abstract class UiTest : IntegrationTest() { * must be executed using [runApp()][ComposeUiTest.runApp] method. */ protected fun openedUrlCount(): Int = uriHandler.urlCount + + /** + * Logs in the user with the specified username and + * navigates to the home page with mentions. + */ + @OptIn(ExperimentalTestApi::class) + protected fun ComposeUiTest.logIn() { + onNodeWithTag("username-input").performTextInput(username) + awaitFact { onNodeWithTag("login-button").assertIsEnabled() } + onNodeWithTag("login-button").performClick() + awaitFact { onNodeWithTag("submit-button").assertExists() } + enterUserCode() + onNodeWithTag("submit-button").performClick() + awaitFact { onNodeWithTag("submit-button").assertDoesNotExist() } + } } From 75964effacc4ee0dfeac0fd0b559b5c5a815db83 Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 12:27:03 +0300 Subject: [PATCH 3/7] Test Mentions page. --- .../spine/examples/pingh/desktop/Mentions.kt | 7 +- .../pingh/desktop/MentionsPageUiTest.kt | 123 ++++++++++++++++++ .../examples/pingh/desktop/given/UiTestEnv.kt | 10 ++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt index 82b2686d..37181853 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.spine.examples.pingh.client.MentionsFlow @@ -166,7 +167,8 @@ private fun MentionCards( .fillMaxSize() .padding(horizontal = 5.dp) .verticalScroll(scrollState) - .background(MaterialTheme.colorScheme.background), + .background(MaterialTheme.colorScheme.background) + .testTag("mention-cards"), ) { mentions.sorted() .forEach { mention -> @@ -213,6 +215,7 @@ private fun MentionCard( onClick = onClick, modifier = Modifier .fillMaxWidth() + .testTag("mention-card-${mention.id}") .height(50.dp), interactionSource = interactionSource, colors = CardDefaults.elevatedCardColors( @@ -295,7 +298,7 @@ private fun SnoozeButton( onClick = { flow.snooze(mention.id) }, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(40.dp).testTag("snooze-button"), colors = IconButtonDefaults.iconButtonColors( contentColor = MaterialTheme.colorScheme.onSecondary ) diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt new file mode 100644 index 00000000..e2499a2c --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.pingh.desktop + +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import io.kotest.matchers.floats.shouldBeLessThan +import io.kotest.matchers.shouldBe +import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact +import io.spine.examples.pingh.desktop.given.testTag +import kotlin.test.Test +import org.junit.jupiter.api.DisplayName + +@DisplayName("Mentions page should") +@OptIn(ExperimentalTestApi::class) +internal class MentionsPageUiTest : UiTest() { + + private val isSnoozeButton = hasTestTag("snooze-button") + + @Test + internal fun `allow users to open a mentions URL even after it has been read`() = + runComposeUiTest { + runApp() + logIn() + val testTag = mentionCards().random().testTag + onNodeWithTag(testTag).performClick() + awaitFact { openedUrlCount() shouldBe 1 } + onNodeWithTag(testTag).performClick() + awaitFact { openedUrlCount() shouldBe 2 } + } + + @Test + internal fun `have snooze button disabled after it is clicked`() = + runComposeUiTest { + runApp() + logIn() + val testTag = mentionCards().random().testTag + val snoozeButton = onNodeWithTag(testTag) + .onChildren() + .filterToOne(isSnoozeButton) + snoozeButton.performClick() + awaitFact { snoozeButton.assertDoesNotExist() } + } + + @Test + internal fun `have snooze button disabled if mention has been read`() = + runComposeUiTest { + runApp() + logIn() + val testTag = mentionCards().random().testTag + val snoozeButton = onNodeWithTag(testTag) + .onChildren() + .filterToOne(isSnoozeButton) + onNodeWithTag(testTag).performClick() + awaitFact { snoozeButton.assertDoesNotExist() } + } + + @Test + internal fun `have mentions sorted after their states have been changed`() = + runComposeUiTest { + runApp() + logIn() + val mentionsCards = mentionCards().sortedBy { it.positionInRoot.y } + val readMentionsTag = mentionsCards[0].testTag + val snoozedMentionsTag = mentionsCards[1].testTag + onNodeWithTag(readMentionsTag).performClick() + onNodeWithTag(snoozedMentionsTag) + .onChildren() + .filterToOne(isSnoozeButton) + .performClick() + awaitFact { + val mentions = mentionCards() + val readMention = mentions.first { it.testTag == readMentionsTag } + val snoozedMention = mentions.first { it.testTag == snoozedMentionsTag } + snoozedMention.positionInRoot.y shouldBeLessThan readMention.positionInRoot.y + mentions.forEach { mention -> + if (mention != readMention && mention != snoozedMention) { + mention.positionInRoot.y shouldBeLessThan readMention.positionInRoot.y + mention.positionInRoot.y shouldBeLessThan snoozedMention.positionInRoot.y + } + } + } + } + + private fun SemanticsNodeInteractionsProvider.mentionCards(): List = + onNodeWithTag("mention-cards") + .onChildren() + .filter(hasClickAction()) + .fetchSemanticsNodes() +} diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt index c7b87d77..13effe40 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt @@ -26,6 +26,8 @@ package io.spine.examples.pingh.desktop.given +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly import java.util.concurrent.TimeUnit import kotlin.time.Duration @@ -36,3 +38,11 @@ import kotlin.time.Duration internal fun delay(duration: Duration) { sleepUninterruptibly(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS) } + +/** + * Returns test tag attached to this semantics node. + */ +internal val SemanticsNode.testTag: String + get() = config.getOrElse(SemanticsProperties.TestTag) { + throw IllegalStateException("This node does not have a `TestTag` specified.") + } From a045eab98cd2969c59bb4b12d8d34c9bc67a90a0 Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 13:01:35 +0300 Subject: [PATCH 4/7] Test Settings page. --- .../spine/examples/pingh/desktop/Mentions.kt | 2 +- .../spine/examples/pingh/desktop/Settings.kt | 6 +- .../pingh/desktop/MentionsPageUiTest.kt | 5 ++ .../pingh/desktop/SettingsPageUiTest.kt | 67 +++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 desktop/src/test/kotlin/io/spine/examples/pingh/desktop/SettingsPageUiTest.kt diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt index 37181853..519ae843 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt @@ -124,7 +124,7 @@ private fun ToolBar( IconButton( icon = Icons.pingh, onClick = toSettingsPage, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(40.dp).testTag("settings-button"), colors = IconButtonDefaults.iconButtonColors( contentColor = contentColor ) diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt index dfd88b5a..01ff2859 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics @@ -245,7 +246,7 @@ private fun LogOutButton( ) { OutlinedButton( onClick = onClick, - modifier = Modifier.height(20.dp), + modifier = Modifier.height(20.dp).testTag("logout-button"), colors = ButtonDefaults.outlinedButtonColors( containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary @@ -302,7 +303,8 @@ private fun DndOption( modifier = Modifier .scale(switchScale) .width(36.dp) - .height(20.dp), + .height(20.dp) + .testTag("dnd-option"), colors = SwitchDefaults.colors( checkedThumbColor = MaterialTheme.colorScheme.secondary, checkedTrackColor = MaterialTheme.colorScheme.primary, diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt index e2499a2c..cd28e0fa 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import io.kotest.matchers.floats.shouldBeLessThan +import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual import io.kotest.matchers.shouldBe import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact import io.spine.examples.pingh.desktop.given.testTag @@ -55,6 +56,7 @@ internal class MentionsPageUiTest : UiTest() { runComposeUiTest { runApp() logIn() + awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val testTag = mentionCards().random().testTag onNodeWithTag(testTag).performClick() awaitFact { openedUrlCount() shouldBe 1 } @@ -67,6 +69,7 @@ internal class MentionsPageUiTest : UiTest() { runComposeUiTest { runApp() logIn() + awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val testTag = mentionCards().random().testTag val snoozeButton = onNodeWithTag(testTag) .onChildren() @@ -80,6 +83,7 @@ internal class MentionsPageUiTest : UiTest() { runComposeUiTest { runApp() logIn() + awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val testTag = mentionCards().random().testTag val snoozeButton = onNodeWithTag(testTag) .onChildren() @@ -93,6 +97,7 @@ internal class MentionsPageUiTest : UiTest() { runComposeUiTest { runApp() logIn() + awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 2 } val mentionsCards = mentionCards().sortedBy { it.positionInRoot.y } val readMentionsTag = mentionsCards[0].testTag val snoozedMentionsTag = mentionsCards[1].testTag diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/SettingsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/SettingsPageUiTest.kt new file mode 100644 index 00000000..92da5830 --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/SettingsPageUiTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.pingh.desktop + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact +import kotlin.test.Test +import org.junit.jupiter.api.DisplayName + +@DisplayName("Settings page should") +@OptIn(ExperimentalTestApi::class) +internal class SettingsPageUiTest : UiTest() { + + private val SemanticsNodeInteractionsProvider.settingsButton + get() = onNodeWithTag("settings-button") + + private val SemanticsNodeInteractionsProvider.logoutButton + get() = onNodeWithTag("logout-button") + + private val SemanticsNodeInteractionsProvider.dndOption + get() = onNodeWithTag("dnd-option") + + @Test + internal fun `have settings retained after the user logs in again`() = + runComposeUiTest { + runApp() + logIn() + settingsButton.performClick() + awaitFact { dndOption.assertExists() } + dndOption.performClick() + awaitFact { dndOption.assertIsOn() } + logoutButton.performClick() + awaitFact { logoutButton.assertDoesNotExist() } + logIn() + settingsButton.performClick() + awaitFact { dndOption.assertIsOn() } + } +} From f0df3c9c98119a4e5907e6272e9214a76dad346a Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 13:51:22 +0300 Subject: [PATCH 5/7] Clean up. --- .../pingh/desktop/MentionsPageUiTest.kt | 28 +++++++++---------- .../io/spine/examples/pingh/desktop/UiTest.kt | 8 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt index cd28e0fa..ea3a173a 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt @@ -57,10 +57,10 @@ internal class MentionsPageUiTest : UiTest() { runApp() logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } - val testTag = mentionCards().random().testTag - onNodeWithTag(testTag).performClick() + val tag = mentionCards().random().testTag + onNodeWithTag(tag).performClick() awaitFact { openedUrlCount() shouldBe 1 } - onNodeWithTag(testTag).performClick() + onNodeWithTag(tag).performClick() awaitFact { openedUrlCount() shouldBe 2 } } @@ -70,8 +70,8 @@ internal class MentionsPageUiTest : UiTest() { runApp() logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } - val testTag = mentionCards().random().testTag - val snoozeButton = onNodeWithTag(testTag) + val tag = mentionCards().random().testTag + val snoozeButton = onNodeWithTag(tag) .onChildren() .filterToOne(isSnoozeButton) snoozeButton.performClick() @@ -84,11 +84,11 @@ internal class MentionsPageUiTest : UiTest() { runApp() logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } - val testTag = mentionCards().random().testTag - val snoozeButton = onNodeWithTag(testTag) + val tag = mentionCards().random().testTag + val snoozeButton = onNodeWithTag(tag) .onChildren() .filterToOne(isSnoozeButton) - onNodeWithTag(testTag).performClick() + onNodeWithTag(tag).performClick() awaitFact { snoozeButton.assertDoesNotExist() } } @@ -99,17 +99,17 @@ internal class MentionsPageUiTest : UiTest() { logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 2 } val mentionsCards = mentionCards().sortedBy { it.positionInRoot.y } - val readMentionsTag = mentionsCards[0].testTag - val snoozedMentionsTag = mentionsCards[1].testTag - onNodeWithTag(readMentionsTag).performClick() - onNodeWithTag(snoozedMentionsTag) + val readMentionTag = mentionsCards[0].testTag + val snoozedMentionTag = mentionsCards[1].testTag + onNodeWithTag(readMentionTag).performClick() + onNodeWithTag(snoozedMentionTag) .onChildren() .filterToOne(isSnoozeButton) .performClick() awaitFact { val mentions = mentionCards() - val readMention = mentions.first { it.testTag == readMentionsTag } - val snoozedMention = mentions.first { it.testTag == snoozedMentionsTag } + val readMention = mentions.first { it.testTag == readMentionTag } + val snoozedMention = mentions.first { it.testTag == snoozedMentionTag } snoozedMention.positionInRoot.y shouldBeLessThan readMention.positionInRoot.y mentions.forEach { mention -> if (mention != readMention && mention != snoozedMention) { diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt index 4cf9cc4a..662faea6 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt @@ -71,14 +71,14 @@ internal abstract class UiTest : IntegrationTest() { /** * Returns the number of opened URLs. * - * To correctly count opened links, the test application - * must be executed using [runApp()][ComposeUiTest.runApp] method. + * To correctly count opened URLs, the test application + * must be run using [runApp()][ComposeUiTest.runApp] method. */ protected fun openedUrlCount(): Int = uriHandler.urlCount /** - * Logs in the user with the specified username and - * navigates to the home page with mentions. + * Logs the user in the application with the specified username and + * navigates to the Mentions page. */ @OptIn(ExperimentalTestApi::class) protected fun ComposeUiTest.logIn() { From 30ef641668d40ac5812165dc30f723cd4bd9e97d Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 13:52:09 +0300 Subject: [PATCH 6/7] Update versions. --- .../src/main/kotlin/io/spine/internal/dependency/Pingh.kt | 2 +- version.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt b/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt index b159bfc7..f46be3a7 100644 --- a/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt +++ b/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt @@ -28,7 +28,7 @@ package io.spine.internal.dependency // https://github.com/spine-examples/Pingh public object Pingh { - private const val version = "1.0.0-SNAPSHOT.10" + private const val version = "1.0.0-SNAPSHOT.11" private const val group = "io.spine.examples.pingh" public const val client: String = "$group:client:$version" diff --git a/version.gradle.kts b/version.gradle.kts index 323fd74d..9d292934 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of the `Pingh` to publish. */ -val pinghVersion: String by extra("1.0.0-SNAPSHOT.10") +val pinghVersion: String by extra("1.0.0-SNAPSHOT.11") From ba363989fc2437a3d0c9dbdccb7498f8df83e6ad Mon Sep 17 00:00:00 2001 From: MykytaPimonovTD Date: Wed, 23 Oct 2024 14:27:22 +0300 Subject: [PATCH 7/7] Improve `MentionsPageUiTest`. --- .../pingh/desktop/MentionsPageUiTest.kt | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt index ea3a173a..355c4f67 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt @@ -28,6 +28,7 @@ package io.spine.examples.pingh.desktop import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.filter import androidx.compose.ui.test.filterToOne @@ -49,8 +50,6 @@ import org.junit.jupiter.api.DisplayName @OptIn(ExperimentalTestApi::class) internal class MentionsPageUiTest : UiTest() { - private val isSnoozeButton = hasTestTag("snooze-button") - @Test internal fun `allow users to open a mentions URL even after it has been read`() = runComposeUiTest { @@ -71,11 +70,8 @@ internal class MentionsPageUiTest : UiTest() { logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val tag = mentionCards().random().testTag - val snoozeButton = onNodeWithTag(tag) - .onChildren() - .filterToOne(isSnoozeButton) - snoozeButton.performClick() - awaitFact { snoozeButton.assertDoesNotExist() } + onSnoozeButtonWithParentTag(tag).performClick() + awaitFact { onSnoozeButtonWithParentTag(tag).assertDoesNotExist() } } @Test @@ -85,11 +81,8 @@ internal class MentionsPageUiTest : UiTest() { logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val tag = mentionCards().random().testTag - val snoozeButton = onNodeWithTag(tag) - .onChildren() - .filterToOne(isSnoozeButton) onNodeWithTag(tag).performClick() - awaitFact { snoozeButton.assertDoesNotExist() } + awaitFact { onSnoozeButtonWithParentTag(tag).assertDoesNotExist() } } @Test @@ -102,10 +95,7 @@ internal class MentionsPageUiTest : UiTest() { val readMentionTag = mentionsCards[0].testTag val snoozedMentionTag = mentionsCards[1].testTag onNodeWithTag(readMentionTag).performClick() - onNodeWithTag(snoozedMentionTag) - .onChildren() - .filterToOne(isSnoozeButton) - .performClick() + onSnoozeButtonWithParentTag(snoozedMentionTag).performClick() awaitFact { val mentions = mentionCards() val readMention = mentions.first { it.testTag == readMentionTag } @@ -125,4 +115,10 @@ internal class MentionsPageUiTest : UiTest() { .onChildren() .filter(hasClickAction()) .fetchSemanticsNodes() + + private fun SemanticsNodeInteractionsProvider.onSnoozeButtonWithParentTag(tag: String): + SemanticsNodeInteraction = + onNodeWithTag(tag) + .onChildren() + .filterToOne(hasTestTag("snooze-button")) }