diff --git a/changes/unreleased/groupware-contacts-scene-test-flake.md b/changes/unreleased/groupware-contacts-scene-test-flake.md new file mode 100644 index 000000000..fcd09a676 --- /dev/null +++ b/changes/unreleased/groupware-contacts-scene-test-flake.md @@ -0,0 +1,7 @@ +category: internal +issue: none +pull: none +platforms: desktop +user-facing: no + +The desktop contacts mutation scene tests now wait for the rendered contact list and the reconciled end state instead of a fixed number of frames, so a slow machine no longer fails them. diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt index 9227538ea..429b959e6 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt @@ -3,6 +3,7 @@ package dev.obiente.nextcloudnative.app import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.text.AnnotatedString import java.lang.reflect.Proxy @@ -24,10 +25,15 @@ class GroupwareContactsMutationSceneTest { NativeGroupwareContactsScreen(fixture.services, fixture.session, "alice", {}, onMutationInProgressChanged = { blocked = it }) }) { + // The list appears only after the recovery read and the DAV discovery and listing requests. + settleUntil(SCENE_TIMEOUT_MILLIS) { has("Synthetic contact") } click("Synthetic contact") click("Delete") click("Delete") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.deletes > 0 && fixture.recovery == null && !blocked && !has("Synthetic contact") + } + settle() assertEquals(1, fixture.deletes) assertTrue(fixture.verifications >= 1, "Successful deletion must verify authoritative absence") assertNull(fixture.recovery) @@ -52,13 +58,19 @@ class GroupwareContactsMutationSceneTest { navigationEnabled = !blocked.value, content = workspace, ) }) { + // Creation is enabled only after the recovery read and the address book discovery complete. + settleUntil(SCENE_TIMEOUT_MILLIS) { enabled("Create contact") } + assertTrue(enabled("Create contact"), "Contacts did not finish loading a writable address book") click("Create contact") val name = generateSequence(requireNotNull(node("Name"))) { it.parent } .first { it.config.getOrNull(SemanticsActions.SetText)?.action != null } assertTrue(name.config[SemanticsActions.SetText].action!!.invoke(AnnotatedString("Synthetic contact"))) settle() click("Save") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.creates > 0 && fixture.recovery == null && !blocked.value && has("Synthetic contact") + } + settle() assertEquals(1, fixture.creates) assertNull(fixture.recovery) assertFalse(blocked.value) @@ -66,7 +78,10 @@ class GroupwareContactsMutationSceneTest { click("Synthetic contact") click("Delete") click("Delete") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.deletes > 0 && fixture.recovery == null && !blocked.value && !has("Synthetic contact") + } + settle() assertEquals(1, fixture.deletes) assertTrue(fixture.verifications >= 2) assertNull(fixture.recovery) @@ -76,6 +91,9 @@ class GroupwareContactsMutationSceneTest { } } + private fun NativeSceneTestDriver.enabled(label: String): Boolean = + node(label)?.let { SemanticsProperties.Disabled !in it.config } == true + private class Fixture { val session = NextcloudSession("https://contacts-scene.invalid", "alice", "synthetic") val home = "/remote.php/dav/addressbooks/users/alice/" @@ -130,4 +148,9 @@ class GroupwareContactsMutationSceneTest { private fun item(href: String, props: String) = "$href$propsHTTP/1.1 200 OK" private fun response(status: Int, body: String = "") = NextcloudApiResponse(status, body.encodeToByteArray(), "application/xml", "etag") } + + private companion object { + /** Bounds each wait for the delayed synthetic store and transport, which takes several frames. */ + const val SCENE_TIMEOUT_MILLIS = 5_000L + } }