Improve Android connection and preview UI testing

This commit is contained in:
2026-09-27 20:52:24 +08:00
parent f80c4d9938
commit 55374356ed
32 changed files with 1227 additions and 31 deletions
@@ -7,6 +7,13 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -21,6 +28,7 @@ private fun JsonObject.text(key: String, fallback: String = "—") = get(key)?.j
data class AppInfo(val version: String, val build: String, val date: String)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> ImageBitmap?,
appInfo: AppInfo = AppInfo("—", "—", "—")) {
@@ -34,12 +42,16 @@ fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> Im
icon = { Text(listOf("◉", "▣", "▦", "⚙")[index]) }, label = { Text(name) })
}
} }) { padding ->
Column(Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(Modifier.fillMaxSize().semantics { testTagsAsResourceId = true }.padding(padding).padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
val identity = state.identity
Text(if (identity == null) "当前连接设备:未连接${if (state.phase == "正在连接") " · 连接中" else ""}"
else "当前连接设备:${identity.text("device_name")}(${identity.text("short_id")}) · ${wifiSummary(state)}",
Modifier.weight(1f), style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis)
val summary = if (identity == null) "当前连接设备:未连接${if (state.phase == "正在连接") " · 连接中" else ""}"
else "当前连接设备:${identity.text("device_name")}(${identity.text("short_id")}) · ${wifiSummary(state)}"
key(summary) { Text(summary,
Modifier.weight(1f).testTag("connection_summary").clipToBounds()
.basicMarquee(iterations = Int.MAX_VALUE, repeatDelayMillis = 1_000,
initialDelayMillis = 1_000, velocity = 30.dp),
style = MaterialTheme.typography.bodySmall, maxLines = 1, softWrap = false, overflow = TextOverflow.Clip) }
Box {
TextButton({ menu = true }) { Text("⋮") }
DropdownMenu(menu, { menu = false }) {
@@ -50,7 +62,7 @@ fun QimiaoApp(controller: Controller, onScan: () -> Unit, decode: (String) -> Im
}
if (state.busy) LinearProgressIndicator(Modifier.fillMaxWidth())
state.error?.let { error -> Card { Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
Text(error, Modifier.weight(1f)); TextButton(controller::dismissError) { Text("知道了") }
Text(error, Modifier.weight(1f).testTag("controller_error")); TextButton(controller::dismissError) { Text("知道了") }
} } }
when (state.page) {
0 -> ConnectionPage(state, controller, onScan)
@@ -79,7 +91,7 @@ private fun ConnectionPage(state: ControllerState, c: Controller, scan: () -> Un
var selected by remember { mutableStateOf<RememberedDevice?>(null) }
var renaming by remember { mutableStateOf(false) }
var name by remember { mutableStateOf("") }
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
LazyColumn(Modifier.fillMaxWidth().testTag("connection_list"), verticalArrangement = Arrangement.spacedBy(10.dp)) {
item { Text("已保存的设备", style = MaterialTheme.typography.titleMedium) }
if (state.remembered.isEmpty()) item { Text("暂无已保存设备,扫描后选择即可连接") }
items(state.remembered, key = { it.id }) { device ->
@@ -96,12 +108,16 @@ private fun ConnectionPage(state: ControllerState, c: Controller, scan: () -> Un
} }
item { Text("扫描到的设备", style = MaterialTheme.typography.titleMedium) }
items(state.devices, key = { it.handle }) { device -> Card(Modifier.fillMaxWidth()) {
val connected = state.identity != null && state.connectedDevice?.handle == device.handle
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text(device.name)
Text("信号 ${device.signal} dBm${if (c.rememberedFor(device) != null) " · 已记忆" else ""}", style = MaterialTheme.typography.bodySmall)
Text(if (connected) state.identity!!.text("device_name") else device.name)
Text(if (connected) "已连接 · 蓝牙信号:${state.signal?.let { "$it dBm" } ?: "暂不可用"}"
else "扫描信号 ${device.signal} dBm${if (c.rememberedFor(device) != null) " · 已记忆" else ""}", style = MaterialTheme.typography.bodySmall)
}
Button({ c.connect(device, c.rememberedFor(device)?.id) }, enabled = state.phase != "正在连接") { Text("连接") }
Button({ if (connected) c.disconnect() else c.connect(device, c.rememberedFor(device)?.id) },
Modifier.testTag(if (connected) "connected_scan_disconnect" else "scan_connect"),
enabled = state.phase != "正在连接") { Text(if (connected) "断开" else "连接") }
}
} }
if (state.identity != null) item { Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -130,13 +146,17 @@ private fun Frame(encoded: String?, checkedAt: Long?, decode: (String) -> ImageB
val bitmap = remember(encoded) { encoded?.let(decode) }
var now by remember { mutableLongStateOf(Clock.System.now().toEpochMilliseconds()) }
LaunchedEffect(checkedAt) { while (true) { now = Clock.System.now().toEpochMilliseconds(); delay(1_000) } }
Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp)) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("设备画面", Modifier.weight(1f), style = MaterialTheme.typography.titleMedium)
TextButton(refresh, enabled = enabled) { Text("手动刷新") }
val density = LocalDensity.current
var refreshWidth by remember { mutableStateOf(96.dp) }
Card(Modifier.fillMaxWidth().testTag("frame_card")) { Column(Modifier.fillMaxWidth().padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Box(Modifier.fillMaxWidth()) {
Text("设备画面", Modifier.fillMaxWidth().padding(horizontal = refreshWidth).align(Alignment.Center).testTag("frame_title"),
style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
TextButton(refresh, Modifier.align(Alignment.CenterEnd).testTag("frame_refresh")
.onSizeChanged { refreshWidth = with(density) { it.width.toDp() } }, enabled = enabled) { Text("手动刷新") }
}
if (bitmap != null) Image(bitmap, "设备当前输出帧", Modifier.size(160.dp)) else Text("等待画面")
if (checkedAt != null && now - checkedAt > 6_000) Text("画面可能已过期,请刷新", style = MaterialTheme.typography.bodySmall)
if (bitmap != null) Image(bitmap, "设备当前输出帧", Modifier.size(160.dp).testTag("frame_image")) else Text("等待画面", textAlign = TextAlign.Center)
if (checkedAt != null && now - checkedAt > 6_000) Text("画面可能已过期,请刷新", style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Center)
} }
}