[Chat][Feature]UI Test Infrasture with Send Button (#535)

This commit is contained in:
ShaunaSong 2022-10-26 09:26:01 -07:00 коммит произвёл GitHub
Родитель 2ece7bd9f8
Коммит 553ebe2577
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 375 добавлений и 31 удалений

Просмотреть файл

@ -8,7 +8,7 @@ buildscript {
ui_library_version_name = '1.1.0'
ui_library_version_code = getVersionCode()
kotlin_version = '1.7.10'
kotlin_version = '1.7.20'
jacoco_version = '0.8.7'
junit_version = '4.13.2'
@ -55,7 +55,7 @@ buildscript {
androidx_activity_compose_version = '1.5.1'
compose_version = '1.2.1'
kotlin_compiler_extension_version = '1.3.1'
kotlin_compiler_extension_version = '1.3.2'
}
repositories {

Просмотреть файл

@ -73,6 +73,7 @@ dependencies {
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.activity:activity-compose:$androidx_activity_compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
@ -81,6 +82,7 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling-data:$compose_version"
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
// Next two are added as an workaround for not being able to preview for latest compose version
@ -97,4 +99,5 @@ dependencies {
androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidx_espresso_core_version"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation "androidx.test:rules:$androidx_test_rules_version"
}

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat
import androidx.test.rule.GrantPermissionRule
import com.azure.android.communication.ui.chat.mocking.TestChatSDK
import com.azure.android.communication.ui.chat.mocking.TestContextProvider
import com.azure.android.communication.ui.chat.utilities.TestHelper
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.After
import org.junit.Rule
/**
* Basic functionality required for our UI tests: UI elements, permissions, dependency injection.
*/
internal open class BaseUiTest {
lateinit var chatSDK: TestChatSDK
@Rule
@JvmField
var grantPermissionRule: GrantPermissionRule
private val basePermissionList = arrayOf(
"android.permission.ACCESS_NETWORK_STATE"
)
init {
grantPermissionRule = GrantPermissionRule.grant(*basePermissionList)
}
// Can't be @Before due to requiring a specific test scheduler.
fun injectDependencies(scheduler: TestCoroutineScheduler) {
val coroutineContextProvider = TestContextProvider(UnconfinedTestDispatcher(scheduler))
chatSDK = TestChatSDK(coroutineContextProvider)
TestHelper.chatSDK = chatSDK
TestHelper.coroutineContextProvider = coroutineContextProvider
}
@After
fun teardown() {
TestHelper.chatSDK = null
TestHelper.coroutineContextProvider = null
}
}

Просмотреть файл

@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.azure.android.communication.ui.chat.presentation.ui.chat.UITestTags
import com.azure.android.communication.ui.chat.redux.state.ChatStatus
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
internal class BottomBarUITest : BaseUiTest() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testOnMessageSentInputViewIsCleared() = runTest {
injectDependencies(testScheduler)
// launch composite
chatSDK.setChatStatus(ChatStatus.INITIALIZED)
launchChatComposite()
// type message
val message = "hello"
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).performTextInput(message)
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).assert(hasText(message))
// send message
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_SEND_BUTTON).performClick()
// assert message is cleared after send
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).assert(hasText(""))
}
@Test
fun testOnMessageSentFailedInputViewIsNotCleared() = runTest {
injectDependencies(testScheduler)
// launch composite
chatSDK.setChatStatus(ChatStatus.INITIALIZATION)
launchChatComposite()
// type message
val message = "hello"
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).performTextInput(message)
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).assert(hasText(message))
// send message
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_SEND_BUTTON).performClick()
// assert message is cleared after send
composeTestRule.onNodeWithTag(UITestTags.MESSAGE_INPUT_BOX).assert(hasText(message))
}
}

Просмотреть файл

@ -1,23 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat
import androidx.test.platform.app.InstrumentationRegistry
import com.azure.android.communication.common.CommunicationTokenCredential
import com.azure.android.communication.common.CommunicationTokenRefreshOptions
import com.azure.android.communication.ui.chat.models.ChatCompositeJoinLocator
import com.azure.android.communication.ui.chat.models.ChatCompositeRemoteOptions
// Helper functions that access internal UI chat API.
// These must reside in `com.azure.android.communication.ui.chat`
internal fun launchChatComposite() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val chatComposite = ChatCompositeBuilder().build()
val communicationTokenRefreshOptions = CommunicationTokenRefreshOptions({ "token" }, true)
val communicationTokenCredential =
CommunicationTokenCredential(communicationTokenRefreshOptions)
val remoteOptions =
ChatCompositeRemoteOptions(
ChatCompositeJoinLocator(
"19:lSNju7o5X9EYJInIIxkJQw1TMnllGMytNCtvhYCxvpE1@thread.v2",
"https://acs-ui-dev.communication.azure.com/"
),
communicationTokenCredential,
"test"
)
chatComposite.launchTest(appContext, remoteOptions, null)
}

Просмотреть файл

@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat.mocking
import com.azure.android.communication.ui.chat.models.ChatEventModel
import com.azure.android.communication.ui.chat.models.MessageInfoModel
import com.azure.android.communication.ui.chat.models.MessagesPageModel
import com.azure.android.communication.ui.chat.redux.state.ChatStatus
import com.azure.android.communication.ui.chat.service.sdk.ChatSDK
import com.azure.android.communication.ui.chat.service.sdk.wrapper.CommunicationIdentifier
import com.azure.android.communication.ui.chat.service.sdk.wrapper.SendChatMessageResult
import com.azure.android.communication.ui.chat.utilities.CoroutineContextProvider
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.threeten.bp.OffsetDateTime
internal class TestChatSDK(
coroutineContextProvider: CoroutineContextProvider = CoroutineContextProvider(),
) : ChatSDK {
private val coroutineScope = CoroutineScope(coroutineContextProvider.Default)
private var chatEventSharedFlow = MutableSharedFlow<ChatEventModel>()
private var chatStatusStateFlow: MutableStateFlow<ChatStatus> =
MutableStateFlow(ChatStatus.NONE)
private val messagesSharedFlow: MutableSharedFlow<MessagesPageModel> = MutableSharedFlow()
private val chatEventModelSharedFlow: MutableSharedFlow<ChatEventModel> = MutableSharedFlow()
fun setChatStatus(status: ChatStatus) {
chatStatusStateFlow.value = status
}
override fun initialization() {
}
override fun destroy() {}
override fun requestPreviousPage() {
}
override fun requestChatParticipants() {
}
override fun startEventNotifications() {
}
override fun stopEventNotifications() {
}
override fun getChatStatusStateFlow(): StateFlow<ChatStatus> = chatStatusStateFlow
override fun getMessagesPageSharedFlow(): SharedFlow<MessagesPageModel> = messagesSharedFlow
override fun getChatEventSharedFlow(): SharedFlow<ChatEventModel> = chatEventSharedFlow
override fun sendMessage(messageInfoModel: MessageInfoModel): CompletableFuture<SendChatMessageResult> {
val future = CompletableFuture<SendChatMessageResult>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun deleteMessage(id: String): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun editMessage(id: String, content: String): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun sendTypingIndicator(): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun sendReadReceipt(id: String): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun removeParticipant(communicationIdentifier: CommunicationIdentifier): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
// coroutine to make sure requests are not blocking
coroutineScope.launch {
future.complete(null)
}
return future
}
override fun fetchMessages(from: OffsetDateTime?) {}
private fun onChatEventReceived(infoModel: ChatEventModel) {
coroutineScope.launch {
chatEventModelSharedFlow.emit(infoModel)
}
}
}
internal fun <T> completedFuture(f: () -> T): CompletableFuture<T> {
return CompletableFuture<T>().also { it.complete(f.invoke()) }
}
internal fun <T> completedFuture(res: T): CompletableFuture<T> {
return CompletableFuture<T>().also { it.complete(res) }
}
internal fun completedNullFuture(): CompletableFuture<Void> {
return CompletableFuture<Void>().also { it.complete(null) }
}
internal fun completedNullFuture(
coroutineScope: CoroutineScope,
f: suspend () -> Any,
): CompletableFuture<Void> {
val future = CompletableFuture<Void>()
coroutineScope.launch {
f.invoke()
future.complete(null)
}
return future
}

Просмотреть файл

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat.mocking
import com.azure.android.communication.ui.chat.utilities.CoroutineContextProvider
import kotlinx.coroutines.test.TestDispatcher
import kotlin.coroutines.CoroutineContext
internal class TestContextProvider(testCoroutineDispatcher: TestDispatcher) :
CoroutineContextProvider() {
override val Main: CoroutineContext = testCoroutineDispatcher
override val IO: CoroutineContext = testCoroutineDispatcher
override val Default: CoroutineContext = testCoroutineDispatcher
override val SingleThreaded: CoroutineContext = testCoroutineDispatcher
}

Просмотреть файл

@ -161,4 +161,18 @@ public class ChatComposite {
showCompositeUI(context);
}
}
void launchTest(final Context context,
final ChatCompositeRemoteOptions remoteOptions,
final ChatCompositeLocalOptions localOptions) {
chatContainer.start(context, remoteOptions, localOptions);
showTestCompositeUI(context);
}
private void showTestCompositeUI(final Context context) {
final Intent launchIntent = new Intent(context, ChatCompositeActivity.class);
launchIntent.putExtra(ChatCompositeActivity.KEY_INSTANCE_ID, instanceId);
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(launchIntent);
}
}

Просмотреть файл

@ -35,6 +35,7 @@ import com.azure.android.communication.ui.chat.service.sdk.ChatSDKWrapper
import com.azure.android.communication.ui.chat.service.sdk.ChatEventHandler
import com.azure.android.communication.ui.chat.service.sdk.ChatFetchNotificationHandler
import com.azure.android.communication.ui.chat.utilities.CoroutineContextProvider
import com.azure.android.communication.ui.chat.utilities.TestHelper
import com.jakewharton.threetenabp.AndroidThreeTen
internal class ChatContainer(
@ -91,7 +92,7 @@ internal class ChatContainer(
context: Context,
) =
ServiceLocator.getInstance(instanceId = instanceId).apply {
addTypedBuilder { CoroutineContextProvider() }
addTypedBuilder { TestHelper.coroutineContextProvider ?: CoroutineContextProvider() }
val messageRepository = MessageRepository.createListBackedRepository()
@ -108,7 +109,7 @@ internal class ChatContainer(
addTypedBuilder {
ChatService(
chatSDK = ChatSDKWrapper(
chatSDK = TestHelper.chatSDK ?: ChatSDKWrapper(
context = context,
chatConfig = configuration.chatConfig!!,
coroutineContextProvider = locate(),

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat.presentation.ui.chat
internal class UITestTags {
companion object {
const val MESSAGE_INPUT_BOX = "MESSAGE_INPUT_BOX"
const val MESSAGE_SEND_BUTTON = "MESSAGE_SEND_BUTTON"
}
}

Просмотреть файл

@ -12,7 +12,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.azure.android.communication.ui.chat.R
import com.azure.android.communication.ui.chat.models.MessageInfoModel
import com.azure.android.communication.ui.chat.redux.action.Action
import com.azure.android.communication.ui.chat.redux.action.ChatAction
@ -32,12 +34,15 @@ internal fun BottomBarView(
) {
MessageInputView(
contentDescription = "Message Input Field",
contentDescription = stringResource(R.string.azure_communication_ui_chat_message_input_view_content_description),
messageInputTextState = messageInputTextState,
postAction = postAction
)
SendMessageButtonView("Send Message Button", chatStatus = chatStatus) {
SendMessageButtonView(
contentDescription = stringResource(R.string.azure_communication_ui_chat_message_send_button_content_description),
chatStatus = chatStatus
) {
postAction(
ChatAction.SendMessage(
MessageInfoModel(

Просмотреть файл

@ -33,6 +33,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.testTag
import com.azure.android.communication.ui.chat.presentation.ui.chat.UITestTags
import com.azure.android.communication.ui.chat.redux.action.Action
import com.azure.android.communication.ui.chat.redux.action.ChatAction
@ -83,6 +85,7 @@ internal fun MessageInput(
.padding(6.dp)
.heightIn(52.dp, maxInputHeight)
.onFocusChanged { onTextFieldFocused(it.isFocused) }
.testTag(UITestTags.MESSAGE_INPUT_BOX)
.then(semantics),
value = textContent,

Просмотреть файл

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
@ -18,6 +19,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.azure.android.communication.ui.chat.R
import com.azure.android.communication.ui.chat.presentation.ui.chat.UITestTags
import com.azure.android.communication.ui.chat.redux.state.ChatStatus
@Composable
@ -36,7 +38,7 @@ internal fun SendMessageButtonView(
else
painterResource(id = R.drawable.azure_communication_ui_chat_ic_fluent_send_message_button_20_filled_disabled)
Box(
modifier = Modifier.clickable {
modifier = Modifier.testTag(UITestTags.MESSAGE_SEND_BUTTON).clickable {
if (chatStatus == ChatStatus.INITIALIZED) {
onClick()
}

Просмотреть файл

@ -49,7 +49,7 @@ internal class ChatServiceListener(
}
coroutineScope.launch {
chatService.getChatEventSharedFlow().collect {
chatService.getChatEventSharedFlow()?.collect {
handleInfoModel(it, dispatch)
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.android.communication.ui.chat.utilities
import com.azure.android.communication.ui.chat.service.sdk.ChatSDK
/**
* This singleton provides a shared global state that our in-process tests (i.e. instrumented on-device unit tests)
* may use to inject their own implementations of dependencies into the library.
*/
internal object TestHelper {
/**
* Allows injecting a custom [ChatSDK] implementation.
* E.g. a test may inject an in-memory implementation to avoid hitting real servers and simulate remote events.
*/
var chatSDK: ChatSDK? = null
/**
* Allows injecting custom set of dispatchers used by the store reducer and within the library.
* Tests will pass along their TestDispatcher in order to sequence events properly.
*/
var coroutineContextProvider: CoroutineContextProvider? = null
}

Просмотреть файл

@ -12,6 +12,8 @@
<string name="azure_communication_ui_chat_first_name_is_typing">%1$s is typing</string>
<string name="azure_communication_ui_chat_two_names_are_typing">%1$s and %2$s are typing"</string>
<string name="azure_communication_ui_chat_three_or_more_are_typing">%1$s, %2$s and %3$d %4$s are typing</string>
<string name="azure_communication_ui_chat_message_input_view_content_description">Message Input Field</string>
<string name="azure_communication_ui_chat_message_send_button_content_description">Send Message Button</string>
<string name="azure_communication_ui_chat_joined_chat">%1$s joined the chat</string>
<string name="azure_communication_ui_chat_left_chat">%1$s left the chat</string>
<string name="azure_communication_ui_chat_topic_updated">Topic Updated: %1$s</string>