diff --git a/.github/workflows/build-under-ubuntu.yml b/.github/workflows/build-under-ubuntu.yml index e5f3dc72..d506b603 100644 --- a/.github/workflows/build-under-ubuntu.yml +++ b/.github/workflows/build-under-ubuntu.yml @@ -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" diff --git a/README.md b/README.md index 795afc33..781ffc88 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/build.gradle.kts b/build.gradle.kts index 581b817d..fc608de5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 09f6d644..2307717c 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -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")) diff --git a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/PersonalInteractionIgTest.kt b/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/PersonalInteractionIgTest.kt index d380428f..3d1f3e04 100644 --- a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/PersonalInteractionIgTest.kt +++ b/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/PersonalInteractionIgTest.kt @@ -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 diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 3a78d120..3cff696a 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -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 { @@ -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 { 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 f695bd9c..be772b90 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,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" } diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt index 146d4d5a..21cac06f 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt @@ -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 @@ -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 ), @@ -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, @@ -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, @@ -657,6 +661,7 @@ private fun NoResponseErrorMessage(flow: VerifyLogin) { modifier = Modifier .width(180.dp) .offset(y = 40.dp) + .testTag("no-response-message") ) } diff --git a/desktop/src/main/resources/config/server.properties b/desktop/src/main/resources/config/server.properties index 48bae8b9..06bc1927 100644 --- a/desktop/src/main/resources/config/server.properties +++ b/desktop/src/main/resources/config/server.properties @@ -25,4 +25,4 @@ # server.address=localhost -server.port=50051 +server.port=4242 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 new file mode 100644 index 00000000..d0d02c4e --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/LoginPageUiTest.kt @@ -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() + } + } +} 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 new file mode 100644 index 00000000..62f1004a --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/UiTest.kt @@ -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) + } + } + } +} diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/DelayedFactAssertion.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/DelayedFactAssertion.kt new file mode 100644 index 00000000..db18448c --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/DelayedFactAssertion.kt @@ -0,0 +1,89 @@ +/* + * 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 java.lang.AssertionError +import java.util.concurrent.CompletableFuture +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +/** + * Checks whether the specified assertion has been successful within the given time period. + * + * Compose uses coroutines to update UI elements, which means that updates + * may not occur immediately after a state change. Therefore, periodic checking over + * a specified duration is essential. + * + * @param assertion The assertion that must be checked. + */ +internal class DelayedFactAssertion private constructor(private val assertion: () -> Unit) { + internal companion object { + /** + * The waiting period before performing the next check after an unsuccessful attempt. + */ + private val intervalBetweenChecks = 100.milliseconds + + /** + * Waits for the delayed fact to occur by repeatedly performing the `assertion`. + * + * Throws exception if no attempt was successful in the given `duration`. + * + * @param duration The duration within which a successful check must occur. + * @param assertion The assertion that must be checked. + */ + internal fun awaitFact(duration: Duration = 5.seconds, assertion: () -> Unit) { + DelayedFactAssertion(assertion).awaitFact(duration) + } + } + + /** + * The recent error that occurred during checking before the allocated time expired. + */ + private var error: AssertionError? = null + + /** + * Performs periodic `assertion` checks. + * + * Terminates if a check succeeds. If no successful check occurs before + * the check `duration` ends, throws an exception. + */ + private fun awaitFact(duration: Duration) { + val timer = CompletableFuture.supplyAsync { delay(duration) } + while (!timer.isDone) { + try { + assertion() + return + } catch (e: AssertionError) { + error = e + } finally { + delay(intervalBetweenChecks) + } + } + throw error ?: AssertionError("No checks have been made.") + } +} 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 new file mode 100644 index 00000000..c7b87d77 --- /dev/null +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt @@ -0,0 +1,38 @@ +/* + * 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 com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +/** + * Causes the currently executing thread to sleep for the specified duration. + */ +internal fun delay(duration: Duration) { + sleepUninterruptibly(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1696cbc6..39386d44 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,6 +32,7 @@ include( "mentions", "testutil-mentions", "testutil-sessions", + "testutil-client", "server", "client" ) diff --git a/testutil-client/build.gradle.kts b/testutil-client/build.gradle.kts new file mode 100644 index 00000000..ebe1cf2d --- /dev/null +++ b/testutil-client/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +import io.spine.internal.dependency.JUnit +import io.spine.internal.dependency.Spine + +dependencies { + implementation(project(":client")) + implementation(project(":testutil-mentions")) + implementation(project(":testutil-sessions")) + implementation(Spine.server) + implementation(JUnit.api) + implementation(Spine.GCloud.datastore) + implementation(Spine.GCloud.testutil) +} diff --git a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/IntegrationTest.kt b/testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/IntegrationTest.kt similarity index 96% rename from client/src/test/kotlin/io/spine/examples/pingh/client/e2e/IntegrationTest.kt rename to testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/IntegrationTest.kt index 22a46070..29e1cbd2 100644 --- a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/IntegrationTest.kt +++ b/testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/IntegrationTest.kt @@ -24,12 +24,11 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.examples.pingh.client.e2e +package io.spine.examples.pingh.testing.client import io.spine.environment.Tests import io.spine.examples.pingh.client.VerifyLogin import io.spine.examples.pingh.client.PinghApplication -import io.spine.examples.pingh.client.e2e.given.MemoizingNotificationSender import io.spine.examples.pingh.mentions.newMentionsContext import io.spine.examples.pingh.server.datastore.DatastoreStorageFactory import io.spine.examples.pingh.sessions.newSessionsContext @@ -48,7 +47,8 @@ import org.junit.jupiter.api.BeforeEach * * Also provides a [PinghApplication] for interacting with the `Server`. */ -internal abstract class IntegrationTest { +@Suppress("UnnecessaryAbstractClass" /* Avoids creating instances; only for inheritance. */) +public abstract class IntegrationTest { internal companion object { diff --git a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/given/MemoizingNotificationSender.kt b/testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/MemoizingNotificationSender.kt similarity index 97% rename from client/src/test/kotlin/io/spine/examples/pingh/client/e2e/given/MemoizingNotificationSender.kt rename to testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/MemoizingNotificationSender.kt index b5fe40ed..ae336abd 100644 --- a/client/src/test/kotlin/io/spine/examples/pingh/client/e2e/given/MemoizingNotificationSender.kt +++ b/testutil-client/src/main/kotlin/io/spine/examples/pingh/testing/client/MemoizingNotificationSender.kt @@ -24,7 +24,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.examples.pingh.client.e2e.given +package io.spine.examples.pingh.testing.client import io.spine.examples.pingh.client.NotificationSender diff --git a/version.gradle.kts b/version.gradle.kts index 854c5f4f..08686f70 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.8") +val pinghVersion: String by extra("1.0.0-SNAPSHOT.9")