package com.xuqm.sdk.sample import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.im.ImSDK import com.xuqm.sdk.im.model.ImConnectionState import com.xuqm.sdk.sample.config.SampleEnvironmentConfig import com.xuqm.sdk.sample.data.api.DEMO_APP_ID import com.xuqm.sdk.sample.data.api.DemoApiFactory import com.xuqm.sdk.sample.data.api.LoginRequest import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import org.junit.After import org.junit.Before import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith // ───────────────────────────────────────────────────────────────────────────── // TC-03 / TC-04 双设备协同自动化测试 // // 用法: // 发送方 (emulator-5554): // adb -s emulator-5554 shell am instrument -w -r // -e class com.xuqm.sdk.sample.CrossDeviceSenderTest // com.xuqm.demo.test/androidx.test.runner.AndroidJUnitRunner // // 接收方 (emulator-5556) — 与发送方并行或在发送方完成后执行: // adb -s emulator-5556 shell am instrument -w -r // -e class com.xuqm.sdk.sample.CrossDeviceReceiverTest // com.xuqm.demo.test/androidx.test.runner.AndroidJUnitRunner // ───────────────────────────────────────────────────────────────────────────── private const val CROSS_MSG_PREFIX = "CROSS_DEVICE_AUTO" private const val GROUP_NAME_PREFIX = "TC04_AUTO_GROUP" private const val BASE_URL = "https://dev.xuqinmin.com/" private const val CONNECT_TIMEOUT_MS = 20_000L private const val POLL_INTERVAL_MS = 2_000L private const val POLL_TOTAL_MS = 40_000L private fun initSdkOnce(ctx: Context) { XuqmSDK.initialize(ctx, DEMO_APP_ID) XuqmSDK.logout() Thread.sleep(1_500) XuqmSDK.logout() SampleEnvironmentConfig.useExternal() Thread.sleep(500) } private suspend fun loginAndConnect(userId: String, ctx: Context): String { val api = DemoApiFactory.create(BASE_URL) { null } val res = api.login(LoginRequest(DEMO_APP_ID, userId, "123456")) val token = requireNotNull(res.data?.imToken) { "Login failed for $userId: ${res.message}" } XuqmSDK.login(userId, token) withTimeoutOrNull(CONNECT_TIMEOUT_MS) { ImSDK.connectionState.first { it is ImConnectionState.Connected } } ?: error("WebSocket 未在 ${CONNECT_TIMEOUT_MS}ms 内连接") return token } // ───────────────────────────────────────────────────────────────────────────── // 发送方: user_a on emulator-5554 // ───────────────────────────────────────────────────────────────────────────── @RunWith(AndroidJUnit4::class) class CrossDeviceSenderTest { companion object { lateinit var appCtx: Context @BeforeClass @JvmStatic fun init() { appCtx = InstrumentationRegistry.getInstrumentation().targetContext initSdkOnce(appCtx) } } @Before fun setUp() { runBlocking { loginAndConnect("user_a", appCtx) } } @After fun tearDown() { XuqmSDK.logout(); Thread.sleep(500) } /** * TC-03 发送方: user_a 向 user_b 发送带有唯一标识的单聊消息 */ @Test fun tc03_sender_sendSingleChatMessage() = runBlocking { val tag = "${CROSS_MSG_PREFIX}_SINGLE_${System.currentTimeMillis()}" val msg = ImSDK.sendTextMessage("user_b", "SINGLE", tag) assertTrue("消息发送不应失败", msg.status != "FAILED") delay(1_500) // 验证消息出现在 user_a 侧的历史中 val history = ImSDK.fetchHistory("user_b") val found = history.any { it.content.contains(CROSS_MSG_PREFIX) } assertTrue("发送的消息应出现在 user_a 的单聊历史中", found) } /** * TC-04 发送方: user_a 创建包含 user_b 的群组,并发送群消息 */ @Test fun tc04_sender_createGroupAndSendMessage() = runBlocking { val groupName = "${GROUP_NAME_PREFIX}_${System.currentTimeMillis() / 60_000}" // 分钟级唯一 val group = ImSDK.createGroup(groupName, listOf("user_b")) assertNotNull("群组创建应成功", group) val groupId = group!!.id delay(1_000) // 订阅群组并发送消息 ImSDK.subscribeGroup(groupId) delay(500) val tag = "${CROSS_MSG_PREFIX}_GROUP_${System.currentTimeMillis()}" val msg = ImSDK.sendTextMessage(groupId, "GROUP", tag) assertTrue("群消息发送不应失败", msg.status != "FAILED") delay(1_500) // 验证群历史 val history = ImSDK.fetchGroupHistory(groupId) val found = history.any { it.content.contains(CROSS_MSG_PREFIX) } assertTrue("发送的群消息应出现在历史中", found) } } // ───────────────────────────────────────────────────────────────────────────── // 接收方: user_b on emulator-5556 // ───────────────────────────────────────────────────────────────────────────── @RunWith(AndroidJUnit4::class) class CrossDeviceReceiverTest { companion object { lateinit var appCtx: Context @BeforeClass @JvmStatic fun init() { appCtx = InstrumentationRegistry.getInstrumentation().targetContext initSdkOnce(appCtx) } } @Before fun setUp() { runBlocking { loginAndConnect("user_b", appCtx) } } @After fun tearDown() { XuqmSDK.logout(); Thread.sleep(500) } /** * TC-03 接收方: user_b 通过轮询 fetchHistory 验证收到 user_a 发送的单聊消息 * 最长等待 POLL_TOTAL_MS,每 POLL_INTERVAL_MS 检查一次 */ @Test fun tc03_receiver_verifySingleChatMessage() = runBlocking { val cutoff = System.currentTimeMillis() - 60_000L // 最近 60 秒内 var found = false val deadline = System.currentTimeMillis() + POLL_TOTAL_MS while (System.currentTimeMillis() < deadline && !found) { val history = ImSDK.fetchHistory("user_a") found = history.any { it.content.contains(CROSS_MSG_PREFIX) && it.createdAt >= cutoff } if (!found) delay(POLL_INTERVAL_MS) } assertTrue( "user_b 应在 ${POLL_TOTAL_MS / 1000}s 内通过 fetchHistory 收到 user_a 的跨设备消息", found, ) // 标记已读 ImSDK.markRead("user_a", "SINGLE") delay(500) val convs = ImSDK.listConversations() val conv = convs.find { it.targetId == "user_a" && it.chatType == "SINGLE" } assertTrue("标记已读后 unreadCount 应为 0", conv?.unreadCount == 0) } /** * TC-04 接收方: user_b 查找 user_a 创建的最新群组,验证可读取群消息 * 不施加时间截止(群名含分钟级时间戳,最新群只含本轮测试消息) */ @Test fun tc04_receiver_verifyGroupMessage() = runBlocking { var found = false var groupFound = false val deadline = System.currentTimeMillis() + POLL_TOTAL_MS while (System.currentTimeMillis() < deadline && !found) { val groups = ImSDK.listGroups() val targetGroup = groups.filter { it.name.startsWith(GROUP_NAME_PREFIX) } .maxByOrNull { it.createdAt } if (targetGroup != null) { groupFound = true ImSDK.subscribeGroup(targetGroup.id) delay(800) val history = ImSDK.fetchGroupHistory(targetGroup.id) found = history.any { it.content.contains(CROSS_MSG_PREFIX) } } if (!found) delay(POLL_INTERVAL_MS) } assertTrue("user_b 应能看到 user_a 创建的 $GROUP_NAME_PREFIX 群组", groupFound) assertTrue( "user_b 应在 ${POLL_TOTAL_MS / 1000}s 内通过 fetchGroupHistory 收到 user_a 的群消息", found, ) // 验证群出现在会话列表中 val convs = ImSDK.listConversations() assertTrue("群聊会话应出现在会话列表中", convs.any { it.chatType == "GROUP" }) } }