Skip to content

Commit

Permalink
Merge pull request #48 from spine-examples/ui-testing
Browse files Browse the repository at this point in the history
Implement UI testing for Login page
  • Loading branch information
MykytaPimonovTD authored Oct 21, 2024
2 parents f895562 + 34e5083 commit b91ba67
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build-under-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ jobs:
env:
JAVA_HOME: ${{ env.JAVA_HOME_11_X64 }}

- name: Start Xvfb
run: Xvfb :99 &

- name: Build client with JDK 17
run: |
cd desktop
./gradlew build --stacktrace
env:
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
DISPLAY: ":99"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ There are several auxiliary modules available for testing:

- `testutil-sessions` allows authentication to the app without using the GitHub REST API.
- `testutil-mentions` allows retrieving new user mentions without using the GitHub REST API.
- `testutil-client` is a utility code for client-side testing.

For a detailed analysis of the processes within domain contexts,
refer to the [#EventStorming documentation](./EventStorming.md).
Expand Down
9 changes: 1 addition & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@ subprojects {
/**
* The set of names of modules that required for building the `desktop` standalone project.
*/
val modulesRequiredForDesktop = setOf(
"clock",
"github",
"sessions",
"mentions",
"server",
"client"
)
val modulesRequiredForDesktop = project.subprojects.map { it.name }.toSet()

/**
* Publishes modules required for building the `desktop` standalone project
Expand Down
1 change: 1 addition & 0 deletions client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation(Grpc.inprocess)
implementation(KotlinX.Coroutines.core)

testImplementation(project(":testutil-client"))
testImplementation(project(":testutil-mentions"))
testImplementation(project(":testutil-sessions"))
testImplementation(project(":clock"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import io.spine.examples.pingh.github.of
import io.spine.examples.pingh.mentions.MentionId
import io.spine.examples.pingh.mentions.MentionStatus
import io.spine.examples.pingh.mentions.MentionView
import io.spine.examples.pingh.testing.client.IntegrationTest
import io.spine.protobuf.Durations2.hours
import io.spine.protobuf.Durations2.milliseconds
import java.lang.Thread.sleep
Expand Down
7 changes: 7 additions & 0 deletions desktop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import io.spine.internal.dependency.Pingh
import io.spine.internal.gradle.AppVersion
import io.spine.internal.gradle.allowBackgroundExecution
import io.spine.internal.gradle.extractSemanticVersion
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
Expand Down Expand Up @@ -90,11 +91,17 @@ dependencies {
implementation(Compose.Runtime.lib)
implementation(compose.desktop.currentOs)
implementation(Material3.Desktop.lib)
implementation(Guava.lib)
implementation(Coil.lib)
implementation(Coil.networkKtor)
implementation(Coil.compose)
implementation(Ktor.Client.android)
implementation(Pingh.client)

testImplementation(Pingh.testutilClient)
testImplementation(kotlin("test"))
@OptIn(ExperimentalComposeLibrary::class)
testImplementation(compose.uiTest)
}

compose.desktop {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ package io.spine.internal.dependency

// https://github.com/spine-examples/Pingh
public object Pingh {
private const val version = "1.0.0-SNAPSHOT.8"
public const val client: String = "io.spine.examples.pingh:client:$version"
private const val version = "1.0.0-SNAPSHOT.9"
private const val group = "io.spine.examples.pingh"

public const val client: String = "$group:client:$version"
public const val testutilClient: String = "$group:testutil-client:$version"
}
11 changes: 8 additions & 3 deletions desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
Expand Down Expand Up @@ -236,7 +237,8 @@ private fun UsernameInput(
},
modifier = Modifier
.width(180.dp)
.height(52.dp),
.height(52.dp)
.testTag("username-input"),
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSecondary
),
Expand Down Expand Up @@ -371,7 +373,8 @@ private fun LoginButton(
onClick = onClick,
modifier = Modifier
.width(180.dp)
.height(40.dp),
.height(40.dp)
.testTag("login-button"),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
Expand Down Expand Up @@ -618,7 +621,8 @@ private fun SubmitButton(
) {
Button(
onClick = onClick,
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize()
.testTag("submit-button"),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
Expand Down Expand Up @@ -657,6 +661,7 @@ private fun NoResponseErrorMessage(flow: VerifyLogin) {
modifier = Modifier
.width(180.dp)
.offset(y = 40.dp)
.testTag("no-response-message")
)
}

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/main/resources/config/server.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
#

server.address=localhost
server.port=50051
server.port=4242
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.runComposeUiTest
import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact
import io.spine.examples.pingh.desktop.given.delay
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds
import org.junit.jupiter.api.DisplayName

@DisplayName("Login page should")
@OptIn(ExperimentalTestApi::class)
internal class LoginPageUiTest : UiTest() {

private val username = "MykytaPimonovTD"

private val SemanticsNodeInteractionsProvider.loginButton
get() = onNodeWithTag("login-button")

private val SemanticsNodeInteractionsProvider.usernameInput
get() = onNodeWithTag("username-input")

private val SemanticsNodeInteractionsProvider.submitButton
get() = onNodeWithTag("submit-button")

private val SemanticsNodeInteractionsProvider.noResponseMessage
get() = onNodeWithTag("no-response-message")

@Test
internal fun `have login button enabled only when a valid username is entered`() =
runComposeUiTest {
runApp()
loginButton.assertIsNotEnabled()
usernameInput.performTextInput("()+$")
awaitFact { loginButton.assertIsNotEnabled() }
usernameInput.performTextClearance()
usernameInput.performTextInput(username)
awaitFact { loginButton.assertIsEnabled() }
}

@Test
internal fun `have submit button disabled after it is clicked, if no code has been entered`() =
runComposeUiTest {
runApp()
toVerificationPage()
submitButton.assertIsEnabled()
noResponseMessage.assertDoesNotExist()
submitButton.performClick()
awaitFact {
submitButton.assertIsNotEnabled()
noResponseMessage.assertExists()
}
}

@Test
internal fun `have submit button become available again 5 seconds after unsuccessful click`() =
runComposeUiTest {
runApp()
toVerificationPage()
submitButton.performClick()
delay(5.seconds)
awaitFact {
submitButton.assertIsEnabled()
noResponseMessage.assertDoesNotExist()
}
}

private fun ComposeUiTest.toVerificationPage() {
usernameInput.performTextInput(username)
awaitFact { loginButton.assertIsEnabled() }
loginButton.performClick()
awaitFact {
loginButton.assertDoesNotExist()
submitButton.assertExists()
}
}
}
61 changes: 61 additions & 0 deletions desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.runtime.remember
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import io.spine.examples.pingh.testing.client.IntegrationTest
import org.junit.jupiter.api.AfterEach

/**
* Abstract base for UI tests that require a server to run and
* a client application connected to it.
*/
internal abstract class UiTest : IntegrationTest() {

private var state: AppState? = null

@AfterEach
internal fun shutdownChannel() {
state?.app?.close()
}

/**
* Launches the Pingh application for testing and sets application state for this test case.
*/
@OptIn(ExperimentalTestApi::class)
protected fun ComposeUiTest.runApp() {
setContent {
Theme {
val settings = retrieveSystemSettings()
state = remember { AppState(settings) }
Window(state!!.window, state!!.app)
}
}
}
}
Loading

0 comments on commit b91ba67

Please sign in to comment.