mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 06:32:59 +00:00
ui tests: known issues applied by an outer JUnit rule (teardown crashes); finish the IME composing word before Enter; field text in typing failures; flake 15.4.1e
This commit is contained in:
1 parent
2146bd2ab4
commit
0a50e5d348
6 files changed
+44
-41
No files matched your search
+3
@@ -127,7 +127,10 @@ private class ImeEnterKey : ViewAction {
|
||||
check((info.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0 && (info.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
|
||||
"the soft keyboard's Enter is an editor action for the message field: inputType 0x${info.inputType.toString(16)}, imeOptions 0x${info.imeOptions.toString(16)}"
|
||||
}
|
||||
ic.beginBatchEdit()
|
||||
ic.finishComposingText()
|
||||
ic.commitText("\n", 1)
|
||||
ic.endBatchEdit()
|
||||
uiController.loopMainThreadUntilIdle()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ class ComposeKeyboardAndroidTest : UiTest() {
|
||||
val chat = alice.chatList.open("bob")
|
||||
typeInto(Ui.Chat.Message, "hello")
|
||||
pressImeEnter()
|
||||
poll(what = "a line break after `hello` in the message field") { editableText(Ui.Chat.Message) == "hello\n" }
|
||||
poll(what = "a line break after `hello` in the message field") { editableText(Ui.Chat.Message).let { check(it == "hello\n") { "the field holds `$it`" }; true } }
|
||||
check(chat.itemTexts().none { "hello" in it }) { "Enter sent the message: ${chat.itemTexts()}" }
|
||||
chat.send("there")
|
||||
bob.expectsMessage("alice> hello", CONNECT_TIMEOUT_MS)
|
||||
|
||||
+11
-13
@@ -28,8 +28,8 @@ import chat.simplex.common.helpers.APPLICATION_ID
|
||||
import java.io.File
|
||||
|
||||
abstract class UiTest {
|
||||
@get:Rule val rule: ComposeTestRule = createEmptyComposeRule()
|
||||
@get:Rule val knownIssue = KnownIssueRule()
|
||||
@get:Rule(order = 0) val knownIssue = KnownIssueRule()
|
||||
@get:Rule(order = 1) val rule: ComposeTestRule = createEmptyComposeRule()
|
||||
|
||||
fun uiTest(body: UiScope.() -> Unit) {
|
||||
val watchdog = Watchdog.start(HARD_TIMEOUT_MS)
|
||||
@@ -37,16 +37,14 @@ abstract class UiTest {
|
||||
UiTestApp.bootOnce()
|
||||
AppCrashes.install()
|
||||
AppCrashes.clear()
|
||||
withKnownIssue(knownIssue.issues) {
|
||||
val scope = UiScope(rule)
|
||||
try {
|
||||
scope.body()
|
||||
scope.checkEnd()
|
||||
} finally {
|
||||
scope.finish()
|
||||
}
|
||||
AppCrashes.failIfAny()
|
||||
val scope = UiScope(rule)
|
||||
try {
|
||||
scope.body()
|
||||
scope.checkEnd()
|
||||
} finally {
|
||||
scope.finish()
|
||||
}
|
||||
AppCrashes.failIfAny()
|
||||
} finally {
|
||||
watchdog.cancel()
|
||||
}
|
||||
@@ -188,7 +186,7 @@ class UiScope(val rule: ComposeTestRule) {
|
||||
rule.mainClock.advanceTimeBy(48, ignoreFrameDuration = true)
|
||||
try {
|
||||
rule.waitForIdle()
|
||||
} catch (e: RuntimeException) {
|
||||
} catch (e: Exception) {
|
||||
if (e.javaClass.simpleName != "ComposeNotIdleException") throw e
|
||||
val shot = if (screenshot) FailureShots.take("UI not idle while waiting for $what") else null
|
||||
throw AssertionError("the app's UI did not become idle while waiting for $what; in flight: ${UiIdle.inFlight().joinToString { it.what }}; screenshot: $shot", e)
|
||||
@@ -323,7 +321,7 @@ class UiScope(val rule: ComposeTestRule) {
|
||||
waitStable(key)
|
||||
settle()
|
||||
messageEditText(key).perform(CommitText(value))
|
||||
poll(what = "the message field to hold `$value`") { editableText(key) == value }
|
||||
poll(what = "the message field to hold `$value`") { editableText(key).let { check(it == value) { "the field holds `$it`" }; true } }
|
||||
}
|
||||
|
||||
fun enterText(key: UiKey, text: String) {
|
||||
|
||||
+14
-16
@@ -34,25 +34,23 @@ abstract class UiTest {
|
||||
try {
|
||||
AppCrashes.install()
|
||||
AppCrashes.clear()
|
||||
withKnownIssue(knownIssue.issues) {
|
||||
try {
|
||||
runInternalSkikoComposeUiTest(width = 1600, height = 900, coroutineDispatcher = StandardTestDispatcher()) {
|
||||
mainClock.autoAdvance = false
|
||||
UiScope(this, dir).apply {
|
||||
body()
|
||||
failOnPeerErrors(Peers.all())
|
||||
waitForAppWork()
|
||||
failOnStuckWork(UiTestApp.drain())
|
||||
AppCrashes.failIfAny()
|
||||
}
|
||||
try {
|
||||
runInternalSkikoComposeUiTest(width = 1600, height = 900, coroutineDispatcher = StandardTestDispatcher()) {
|
||||
mainClock.autoAdvance = false
|
||||
UiScope(this, dir).apply {
|
||||
body()
|
||||
failOnPeerErrors(Peers.all())
|
||||
waitForAppWork()
|
||||
failOnStuckWork(UiTestApp.drain())
|
||||
AppCrashes.failIfAny()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
throw if (e.suppressed.isEmpty()) e else AssertionError("${e.message}; suppressed:\n" + e.suppressed.joinToString("\n") { it.stackTraceToString().lines().take(25).joinToString("\n") }, e)
|
||||
}
|
||||
repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} }
|
||||
failOnStuckWork(UiTestApp.drain())
|
||||
AppCrashes.failIfAny()
|
||||
} catch (e: Throwable) {
|
||||
throw if (e.suppressed.isEmpty()) e else AssertionError("${e.message}; suppressed:\n" + e.suppressed.joinToString("\n") { it.stackTraceToString().lines().take(25).joinToString("\n") }, e)
|
||||
}
|
||||
repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} }
|
||||
failOnStuckWork(UiTestApp.drain())
|
||||
AppCrashes.failIfAny()
|
||||
passed = true
|
||||
} finally {
|
||||
repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} }
|
||||
|
||||
+11
-11
@@ -1,8 +1,9 @@
|
||||
package chat.simplex.app.ui.harness
|
||||
|
||||
import org.junit.AssumptionViolatedException
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@@ -10,17 +11,16 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@Repeatable
|
||||
annotation class KnownAppIssue(val finding: String, val error: String, val intermittent: Boolean = false)
|
||||
|
||||
class KnownIssueRule : TestWatcher() {
|
||||
var issues: List<KnownAppIssue> = emptyList()
|
||||
private set
|
||||
class KnownIssueRule : TestRule {
|
||||
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
|
||||
override fun evaluate() = withKnownIssue(knownIssues(description)) { base.evaluate() }
|
||||
}
|
||||
|
||||
override fun starting(description: Description) {
|
||||
issues = description.annotations.flatMap { a ->
|
||||
when {
|
||||
a is KnownAppIssue -> listOf(a)
|
||||
a.annotationClass.java.enclosingClass == KnownAppIssue::class.java -> (a.annotationClass.java.getMethod("value").invoke(a) as Array<*>).filterIsInstance<KnownAppIssue>()
|
||||
else -> emptyList()
|
||||
}
|
||||
private fun knownIssues(description: Description): List<KnownAppIssue> = description.annotations.flatMap { a ->
|
||||
when {
|
||||
a is KnownAppIssue -> listOf(a)
|
||||
a.annotationClass.java.enclosingClass == KnownAppIssue::class.java -> (a.annotationClass.java.getMethod("value").invoke(a) as Array<*>).filterIsInstance<KnownAppIssue>()
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1597,6 +1597,10 @@ Each entry gives the symptom, the cause (how it was confirmed), and the fix or m
|
||||
- Pausing the clock, as desktop does, breaks typing on Android instead (CI trial 6, 2026-10-23: 77 failures waiting for the message field to hold the typed text). The message field is an `AndroidView` `EditText`. Its update block reads `composeState`, so a text change re-runs it before the paused clock lets the field recompose. It then compares against `cs` captured at the last composition (`PlatformTextField.android.kt:69`, `:186-188`) and writes the old text back.
|
||||
- These three tests (`liveMessage_menuStart_alertConfirm_peerSeesStarted`, `liveMessage_boltButton_emptyStart`, `liveMessage_chatSwitch_sendsToOriginalChat`) type while the message is live, so they are skipped on Android with this reason (`assumeTrue`) and run on desktop. `liveMessage_received_showsLiveBadge` runs on both.
|
||||
|
||||
### 15.4.1e Open flake: 60 messages from a CLI peer stopped at 52 (Android CI, 1 of 2 runs)
|
||||
|
||||
- `UnreadScrollTest.unread_openChat_scrollsToFirstUnread` (GitHub emulator, trial 7, 2026-10-23): bob's CLI sent `m1`..`m60` to alice; after 60 s alice's row showed `m52` with unread 52, and nothing more arrived. In trial 5 the same wait took 574 ms. Logcat has no error in that window, and the bridge log has no per-message lines. So it is not known whether bob's CLI did not send the last eight messages or alice did not receive them. Next step: log bob's sent-message count when the wait times out.
|
||||
|
||||
### 15.4.2 Test strength limits (second review, 2026-10-14)
|
||||
|
||||
Under the minimal-app-code rule (item 31) some states are asserted through the model, not the UI. These tests pass if the model is right even when the UI fails to render the state:
|
||||
|
||||
Reference in new issue
Block a user