Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Communication: Fix Tagging other Users does not work #71

Merged
merged 8 commits into from
Nov 14, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.ktor.http.HttpStatusCode
/**
* Allows the user to register
*/
internal interface RegisterService {
interface RegisterService {

/**
* Registers a new user. This is only possible if the password is long enough and there is no other user with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,12 +417,9 @@ internal open class ConversationViewModel(
) { authToken, serverUrl ->
retryOnInternet(networkStatusProvider.currentNetworkStatus) {
conversationService
.searchForPotentialCommunicationParticipants(
.searchForCourseMembers(
courseId = metisContext.courseId,
query = query,
includeStudents = true,
includeTutors = true,
includeInstructors = true,
authToken = authToken,
serverUrl = serverUrl
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.MarkdownText
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R


const val TEST_TAG_MARKDOWN_TEXTFIELD = "TEST_TAG_MARKDOWN_TEXTFIELD"

/**
* @param sendButton composable centered vertically right to the text field.
*/
Expand Down Expand Up @@ -94,7 +98,8 @@ internal fun MarkdownTextField(
onFocusLost()
hadFocus = false
}
},
}
.testTag(TEST_TAG_MARKDOWN_TEXTFIELD),
value = textFieldValue,
onValueChange = onTextChanged,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import kotlin.time.Duration.Companion.seconds
internal const val TEST_TAG_CAN_CREATE_REPLY = "TEST_TAG_CAN_CREATE_REPLY"
internal const val TEST_TAG_REPLY_TEXT_FIELD = "TEST_TAG_REPLY_TEXT_FIELD"
internal const val TEST_TAG_REPLY_SEND_BUTTON = "TEST_TAG_REPLY_SEND_BUTTON"
internal const val TEST_TAG_UNFOCUSED_TEXT_FIELD = "TEST_TAG_UNFOCUSED_TEXT_FIED"

private const val DisabledContentAlpha = 0.75f

Expand Down Expand Up @@ -443,7 +444,8 @@ private fun UnfocusedPreviewReplyTextField(onRequestShowTextField: () -> Unit, t
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onRequestShowTextField)
.padding(horizontal = 16.dp),
.padding(horizontal = 16.dp)
.testTag(TEST_TAG_UNFOCUSED_TEXT_FIELD),
verticalAlignment = Alignment.CenterVertically
) {
Text(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation

import de.tum.informatics.www1.artemis.native_app.core.common.test.EndToEndTest
import de.tum.informatics.www1.artemis.native_app.core.common.test.DefaultTestTimeoutMillis
import de.tum.informatics.www1.artemis.native_app.core.test.test_setup.DefaultTimeoutMillis
import de.tum.informatics.www1.artemis.native_app.core.common.test.testServerUrl
import de.tum.informatics.www1.artemis.native_app.core.model.account.User
import de.tum.informatics.www1.artemis.native_app.feature.login.service.network.RegisterService
import de.tum.informatics.www1.artemis.native_app.feature.metistest.ConversationBaseTest
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.experimental.categories.Category
import org.junit.runner.RunWith
import org.koin.test.get
import org.robolectric.RobolectricTestRunner
import org.robolectric.util.Logger
import java.util.Locale
import kotlin.test.assertTrue
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds


@Category(EndToEndTest::class)
@RunWith(RobolectricTestRunner::class)
class ConversationAutoCompletionE2eTest : ConversationBaseTest() {

private val registerService: RegisterService get() = get()

@Test(timeout = DefaultTestTimeoutMillis)
fun `auto-completion suggests users`() {
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
val usernames = (0 until 3).map {
"user$it"
}

runTest(timeout = DefaultTimeoutMillis.milliseconds * 4) {
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
usernames.map { username ->
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
registerService.register(
account = User(
firstName = "firstName",
lastName = "lastName",
username = username,
email = "email@domain.com",
password = "password",
langKey = Locale.getDefault().toLanguageTag()
),
serverUrl = testServerUrl,
).orThrow("Could not create user with username $username")
}

val typedText = "@user"
val autoCompleteSuggestions = conversationService.searchForCourseMembers(
courseId = course.id!!,
query = typedText,
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
authToken = accessToken,
serverUrl = testServerUrl
).orThrow("Could not get auto-complete suggestions")

Logger.info("Auto-complete suggestions: $autoCompleteSuggestions")

assertEquals(usernames.size, autoCompleteSuggestions.size)

usernames.forEach { username ->
assertTrue(
autoCompleteSuggestions.any { it.username == username },
"Auto-complete suggestions do not contain user $username"
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.reply

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.text.input.TextFieldValue
import androidx.test.ext.junit.runners.AndroidJUnit4
import de.tum.informatics.www1.artemis.native_app.core.data.DataState
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ReplyTextFieldUiTest {

@get:Rule
val composeTestRule = createComposeRule()

private val autoCompleteHints = listOf(
AutoCompleteCategory(
R.string.markdown_textfield_autocomplete_category_users, listOf(
AutoCompleteHint("User1", "<User1>", "1"),
AutoCompleteHint("User2", "<User2>", "2"),
AutoCompleteHint("User3", "<User3>", "3"),
))
)

private val mockHintProvider = object : ReplyAutoCompleteHintProvider {
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
override val legalTagChars: List<Char> = listOf('@')
override fun produceAutoCompleteHints(tagChar: Char, query: String): Flow<DataState<List<AutoCompleteCategory>>> {
return flowOf(DataState.Success(autoCompleteHints))
}
}

@Before
fun setUp() {
composeTestRule.setContent {
CompositionLocalProvider(LocalReplyAutoCompleteHintProvider provides mockHintProvider) {
val text = remember { mutableStateOf(TextFieldValue()) }

ReplyTextField(
modifier = Modifier.fillMaxSize(),
replyMode = ReplyMode.NewMessage(
text,
onUpdateTextUpstream = { text.value = it }
) {
CompletableDeferred()
},
updateFailureState = {},
title = "TestChat"
)
}
}

// Click the unfocused textField to focus and expand the textField
composeTestRule.onNodeWithTag(TEST_TAG_UNFOCUSED_TEXT_FIELD).performClick()
}

@Test
fun `test GIVEN an empty reply textField WHEN doing nothing THEN the autoCompletion dialog is hidden`() {
composeTestRule.assertAllAutoCompletionHintsHidden()
}

@Test
fun `test GIVEN an empty reply textField WHEN entering the tag character @ THEN a list of autoCompletionHints for users shows`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@")
composeTestRule.assertAllAutoCompletionHintsShown()
}

@Test
fun `test GIVEN the autoCompletion dialog WHEN clicking an entry THEN the replacement is inserted into the textField and the dialog is hidden`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@")

composeTestRule.onNodeWithText("User1").performClick()

composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).assertTextEquals("<User1>")
composeTestRule.assertAllAutoCompletionHintsHidden()
}

@Test
fun `test GIVEN the textField WHEN entering a non-tag character THEN the autoCompletion dialog is hidden`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("a")
composeTestRule.assertAllAutoCompletionHintsHidden()
}

@Test
fun `test GIVEN the autoCompletion dialog WHEN removing the tag character @ THEN the autoCompletion dialog is hidden`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@")
composeTestRule.assertAllAutoCompletionHintsShown()

composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextClearance()
composeTestRule.assertAllAutoCompletionHintsHidden()
}

@Test
fun `test GIVEN the autoCompletion has been performed WHEN entering the tag character again THEN the autoCompletion dialog shows again`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@")
composeTestRule.onNodeWithText("User1").performClick()
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).assertTextEquals("<User1>")
composeTestRule.assertAllAutoCompletionHintsHidden()

composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@")
composeTestRule.assertAllAutoCompletionHintsShown()
}

@Test
fun `test GIVEN the textField WHEN entering a first and surname separated by a single whitespace THEN the dialog shows`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@Olaf Scholz")
FelberMartin marked this conversation as resolved.
Show resolved Hide resolved
composeTestRule.assertAllAutoCompletionHintsShown()
}

@Test
fun `test GIVEN the textField WHEN entering a second whitespace THEN the dialog is hidden`() {
composeTestRule.onNodeWithTag(TEST_TAG_MARKDOWN_TEXTFIELD).performTextInput("@Olaf Scholz ")
composeTestRule.assertAllAutoCompletionHintsHidden()
}



private fun ComposeContentTestRule.assertAllAutoCompletionHintsHidden() {
onNodeWithText("User1").assertDoesNotExist()
onNodeWithText("User2").assertDoesNotExist()
onNodeWithText("User3").assertDoesNotExist()
}

private fun ComposeContentTestRule.assertAllAutoCompletionHintsShown() {
onNodeWithText("User1").assertExists()
onNodeWithText("User2").assertExists()
onNodeWithText("User3").assertExists()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ open class ConversationServiceStub(
serverUrl: String
): NetworkResponse<List<User>> = NetworkResponse.Response(emptyList())

override suspend fun searchForCourseMembers(
courseId: Long,
query: String,
authToken: String,
serverUrl: String
): NetworkResponse<List<User>> = NetworkResponse.Response(emptyList())

override suspend fun createOneToOneConversation(
courseId: Long,
partner: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ interface ConversationService {
serverUrl: String
): NetworkResponse<List<User>>

suspend fun searchForCourseMembers(
courseId: Long,
query: String,
authToken: String,
serverUrl: String
): NetworkResponse<List<User>>

suspend fun createOneToOneConversation(
courseId: Long,
partner: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ class ConversationServiceImpl(private val ktorProvider: KtorProvider) : Conversa
}
}

override suspend fun searchForCourseMembers(
courseId: Long,
query: String,
authToken: String,
serverUrl: String
): NetworkResponse<List<User>> {
return performNetworkCall {
ktorProvider.ktorClient.get(serverUrl) {
url {
appendPathSegments("api", "courses", courseId.toString(), "members", "search")

parameter("loginOrName", query)
}

cookieAuth(authToken)
}.body()
}
}

override suspend fun createGroupChat(
courseId: Long,
groupMembers: List<String>,
Expand Down
Loading