Improve controller behavior and update project requirements
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id("com.android.test")
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
}
|
||||
android {
|
||||
namespace = "org.qimiaoscreen.uitest"
|
||||
compileSdk = 37
|
||||
targetProjectPath = ":androidApp"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
buildTypes { getByName("debug") { isDebuggable = true } }
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation(project(":sharedCore"))
|
||||
implementation(project(":sharedUi"))
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.coroutines.android)
|
||||
implementation(libs.serialization.json)
|
||||
implementation("org.jetbrains.compose.ui:ui-test-junit4:1.12.1")
|
||||
implementation("androidx.test:runner:1.7.0")
|
||||
implementation("androidx.test.ext:junit:1.3.0")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:label="QMS UI tests" android:theme="@android:style/Theme.Material.Light.NoActionBar" android:allowBackup="false">
|
||||
<activity android:name=".FixtureActivity" android:exported="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package org.qimiaoscreen.uitest
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import android.os.Bundle
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.serialization.json.*
|
||||
import org.qimiaoscreen.core.*
|
||||
import org.qimiaoscreen.ui.QimiaoApp
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
/** Only in the self-instrumenting test APK. No AndroidBle, network or private data. */
|
||||
class FixtureActivity : ComponentActivity() {
|
||||
val platform = FixturePlatform()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
val controller = Controller(platform, scope)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent { QimiaoApp(controller, controller::scan, { ImageBitmap(64, 64) }) }
|
||||
}
|
||||
override fun onDestroy() { controller.disconnect(); scope.cancel(); super.onDestroy() }
|
||||
}
|
||||
class FixturePlatform : MobilePlatform {
|
||||
override val model = "模拟测试手机"
|
||||
private val saved = mutableMapOf<String, String>()
|
||||
val calls = mutableListOf<String>()
|
||||
var scanError = false
|
||||
var emptyLibrary = true
|
||||
override fun load(key: String) = saved[key]
|
||||
override fun save(key: String, value: String) { saved[key] = value }
|
||||
override fun scan(onDevice: (DiscoveredDevice) -> Unit, onError: (String) -> Unit) {
|
||||
if (scanError) onError("模拟扫描失败") else onDevice(DiscoveredDevice("fixture", "QMS-00000000", -40))
|
||||
}
|
||||
override fun stopScan() {}
|
||||
override suspend fun connect(handle: String): DeviceLink {
|
||||
check(handle == "fixture")
|
||||
return FixtureLink(this)
|
||||
}
|
||||
override fun crypto() = object : SecureChannel {
|
||||
override val hello = byteArrayOf(1)
|
||||
override fun finish(serverHello: ByteArray) {}
|
||||
override fun encrypt(payload: ByteArray) = payload
|
||||
override fun decrypt(record: ByteArray) = record
|
||||
}
|
||||
}
|
||||
private class FixtureLink(val platform: FixturePlatform) : DeviceLink {
|
||||
override val mtu = 247
|
||||
private val reassembler = Reassembler()
|
||||
private val messages = Channel<ByteArray>(Channel.UNLIMITED)
|
||||
private var sequence = 0L
|
||||
override suspend fun send(fragment: ByteArray) {
|
||||
val (kind, payload) = reassembler.accept(fragment) ?: return
|
||||
val response = if (kind == 1) byteArrayOf(2) else {
|
||||
val request = Protocol.json.parseToJsonElement(payload.decodeToString()).jsonObject
|
||||
val method = request.getValue("method").jsonPrimitive.content
|
||||
platform.calls += method
|
||||
val result = when (method) {
|
||||
"session.open" -> """{"device_id":"00000000-0000-4000-8000-000000000001","device_name":"模拟小屏幕","short_id":"00000000","protocol_major":1,"capabilities":["status","frame","library","library_progressive","settings","wifi","wifi_scan"]}"""
|
||||
"session.ping" -> """{"alive":true}"""
|
||||
"status.get" -> """{"state":{"animation_playback":{"active":false}}}"""
|
||||
"frame.get" -> """{"frame_revision":"fixture","data_base64":"fixture-image"}"""
|
||||
"settings.get" -> """{"revision":"fixture","values":{"brightness":40,"orientation":0,"prompt_delay_seconds":5}}"""
|
||||
"wifi.get" -> """{"revision":"fixture","saved":null,"active":{"connected":false}}"""
|
||||
"wifi.scan" -> """{"networks":[]}"""
|
||||
"library.summary" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","item_count":0}""" else """{"library_revision":"fixture-full","item_count":1}"""
|
||||
"library.list" -> if (platform.emptyLibrary) """{"library_revision":"fixture-empty","items":[],"next_cursor":null}""" else """{"library_revision":"fixture-full","items":[{"id":"fixture","type":"template","name":"测试模板","revision":"fixture-1","playable":true}],"next_cursor":null}"""
|
||||
"content.default.get" -> """{"name":"测试默认"}"""
|
||||
"library.preview" -> """{"data_base64":"fixture-preview"}"""
|
||||
"library.thumbnail" -> """{"data_base64":"fixture-image"}"""
|
||||
else -> error("Unexpected fixture RPC: $method")
|
||||
}
|
||||
buildJsonObject { put("id", request.getValue("id")); put("ok", true); put("result", Protocol.json.parseToJsonElement(result)) }.toString().encodeToByteArray()
|
||||
}
|
||||
Protocol.encode(if (kind == 1) 2 else 3, sequence++, response, mtu).forEach { messages.send(it) }
|
||||
}
|
||||
override suspend fun receive() = messages.receive()
|
||||
override fun close() { messages.close() }
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package org.qimiaoscreen.uitest
|
||||
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
|
||||
class IsolatedUiTest {
|
||||
@get:Rule val ui = createAndroidComposeRule<FixtureActivity>()
|
||||
private fun click(text: String) { ui.onNodeWithText(text).performClick() }
|
||||
private fun connect() {
|
||||
click("扫描设备")
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.devices.isNotEmpty() }
|
||||
ui.onNodeWithTag("scan_connect").performScrollTo().performClick()
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.status != null }
|
||||
}
|
||||
@Test fun connection() {
|
||||
ui.onNodeWithText("暂无已保存设备,扫描后选择即可连接").assertExists()
|
||||
ui.runOnIdle { ui.activity.platform.scanError = true }
|
||||
click("扫描设备")
|
||||
ui.onNodeWithText("模拟扫描失败").assertExists()
|
||||
click("知道了")
|
||||
ui.onNodeWithTag("controller_error").assertDoesNotExist()
|
||||
ui.runOnIdle { ui.activity.platform.scanError = false }
|
||||
connect()
|
||||
ui.onNodeWithTag("connected_scan_disconnect").performScrollTo().assertIsEnabled().performClick()
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.identity == null }
|
||||
}
|
||||
@Test fun status() {
|
||||
click("设备状态")
|
||||
ui.onNodeWithText("请先在连接页连接设备").assertExists()
|
||||
click("连接"); connect(); click("设备状态")
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.frame != null }
|
||||
ui.onNodeWithTag("frame_card").assertIsDisplayed()
|
||||
ui.onNodeWithTag("frame_refresh").performClick()
|
||||
ui.waitUntil(5_000) { !ui.activity.controller.state.value.busy }
|
||||
assertTrue(ui.activity.platform.calls.contains("frame.get"))
|
||||
}
|
||||
@Test fun content() {
|
||||
connect(); click("显示内容")
|
||||
ui.waitUntil(5_000) { !ui.activity.controller.state.value.busy }
|
||||
ui.onNodeWithText("可播放内容").performScrollTo().assertIsDisplayed()
|
||||
ui.onNodeWithText("播放").assertDoesNotExist()
|
||||
ui.runOnIdle { ui.activity.platform.emptyLibrary = false; ui.activity.controller.refresh() }
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.library.isNotEmpty() }
|
||||
ui.onNodeWithText("测试模板").performScrollTo().assertIsDisplayed()
|
||||
ui.onNodeWithText("播放").assertIsEnabled()
|
||||
ui.waitUntil(7_000) { ui.activity.controller.state.value.previews.isNotEmpty() }
|
||||
ui.waitUntil(7_000) { ui.activity.controller.state.value.thumbnails.isNotEmpty() }
|
||||
assertTrue(ui.activity.platform.calls.indexOf("library.preview") < ui.activity.platform.calls.indexOf("library.thumbnail"))
|
||||
assertFalse(ui.activity.platform.calls.contains("content.play"))
|
||||
}
|
||||
@Test fun settings() {
|
||||
connect(); click("设备设置")
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.settings != null }
|
||||
ui.onNodeWithText("WiFi设置 展开").performScrollTo().performClick()
|
||||
ui.onNodeWithText("保存WiFi设置").performScrollTo().assertIsNotEnabled()
|
||||
ui.onNodeWithText("WiFi名称").performScrollTo().performTextInput("fixture-network")
|
||||
ui.onNodeWithText("保存WiFi设置").performScrollTo().performClick()
|
||||
ui.onNodeWithText("立即生效WiFi设置").assertExists()
|
||||
click("取消")
|
||||
assertFalse(ui.activity.platform.calls.contains("wifi.set"))
|
||||
}
|
||||
@Test fun navigation() {
|
||||
for (page in listOf("设备状态", "显示内容", "设备设置", "连接")) click(page)
|
||||
click("⋮"); click("关于")
|
||||
ui.onNodeWithText("关于奇妙小屏幕").assertExists(); click("关闭")
|
||||
click("⋮"); click("软件设置")
|
||||
ui.onNodeWithText("当前手机昵称").performTextReplacement("模拟昵称")
|
||||
click("保存")
|
||||
ui.runOnIdle { assertEquals("模拟昵称", ui.activity.controller.state.value.clientName) }
|
||||
click("⋮"); click("软件设置")
|
||||
ui.onNodeWithText("内容缓存:0 KiB").assertExists()
|
||||
ui.onNodeWithTag("cache_clear_open").performClick()
|
||||
ui.onNodeWithTag("cache_clear_confirm").performClick()
|
||||
assertTrue(ui.activity.platform.calls.isEmpty())
|
||||
}
|
||||
@Test fun layout() {
|
||||
connect()
|
||||
for (page in listOf("设备状态", "显示内容")) {
|
||||
click(page)
|
||||
ui.waitUntil(5_000) { ui.activity.controller.state.value.frame != null }
|
||||
val card = ui.onNodeWithTag("frame_card").fetchSemanticsNode().boundsInRoot
|
||||
val image = ui.onNodeWithTag("frame_image").fetchSemanticsNode().boundsInRoot
|
||||
val title = ui.onNodeWithTag("frame_title").fetchSemanticsNode().boundsInRoot
|
||||
val refresh = ui.onNodeWithTag("frame_refresh").fetchSemanticsNode().boundsInRoot
|
||||
assertTrue(kotlin.math.abs(image.center.x - card.center.x) <= 3)
|
||||
assertTrue(kotlin.math.abs(title.center.x - card.center.x) <= 3)
|
||||
assertTrue(title.right <= refresh.left)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user