- 更新发布版本从 0.1.0-SNAPSHOT 到 0.4.0 - 更新 README.md 中的依赖版本引用 - 完善 TEST_REPORT.md 包括最新测试结果和新增测试用例 - 添加详细的 TEST_PLAN.md 文档 - 更新 sample-app 的测试配置和依赖 - 为各个 SDK 模块添加 ProGuard 规则文件 - 修复 ApiClient 中的 Gson 类型适配器问题 - 改进测试架构,解决会话删除和跨设备测试问题
218 行
9.0 KiB
Kotlin
218 行
9.0 KiB
Kotlin
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" })
|
|
}
|
|
}
|