ShellAccessibilityUiTest.kt (8035B)
1 package org.harvestcircle.ui.shell 2 3 import androidx.compose.runtime.CompositionLocalProvider 4 import androidx.compose.runtime.getValue 5 import androidx.compose.runtime.mutableStateOf 6 import androidx.compose.runtime.remember 7 import androidx.compose.runtime.setValue 8 import androidx.compose.ui.Modifier 9 import androidx.compose.ui.input.key.Key 10 import androidx.compose.ui.platform.testTag 11 import androidx.compose.ui.test.ExperimentalTestApi 12 import androidx.compose.ui.test.assertCountEquals 13 import androidx.compose.ui.test.assertHeightIsAtLeast 14 import androidx.compose.ui.test.assertIsFocused 15 import androidx.compose.ui.test.onAllNodesWithTag 16 import androidx.compose.ui.test.onNodeWithTag 17 import androidx.compose.ui.test.performClick 18 import androidx.compose.ui.test.performKeyInput 19 import androidx.compose.ui.test.v2.runComposeUiTest 20 import androidx.compose.ui.unit.dp 21 import org.harvestcircle.application.ApplicationLifecycle 22 import org.harvestcircle.application.ApplicationSnapshot 23 import org.harvestcircle.application.BuildInfo 24 import org.harvestcircle.application.FoundationOverlay 25 import org.harvestcircle.application.HarvestCirclePresenterState 26 import org.harvestcircle.application.HarvestCircleShellState 27 import org.harvestcircle.application.OverlayState 28 import org.harvestcircle.application.SessionLifecycle 29 import org.harvestcircle.application.ShellFocusTarget 30 import org.harvestcircle.application.SnapshotRevision 31 import org.harvestcircle.application.StatusOverlayKey 32 import org.harvestcircle.designsystem.component.action.HarvestCircleLabeledButton 33 import org.harvestcircle.identities.ui.HarvestCirclePlatformActions 34 import org.harvestcircle.identities.ui.HarvestCircleUiActions 35 import kotlin.test.Test 36 37 @OptIn(ExperimentalTestApi::class) 38 class ShellAccessibilityUiTest { 39 @Test 40 fun sharedActionsMeetTheMinimumTargetAndRouteContentRestoresFocus() = 41 runComposeUiTest { 42 setHarvestCircleContent { 43 RouteFocusTarget("today", "Today main content") { 44 HarvestCircleLabeledButton( 45 "Action", 46 "Accessible action", 47 {}, 48 Modifier.testTag("accessible-action"), 49 ) 50 } 51 } 52 53 onNodeWithTag("accessible-action").assertHeightIsAtLeast(32.dp) 54 onNodeWithTag("route-focus-target").assertIsFocused() 55 } 56 57 @Test 58 fun keyboardHostDispatchesModifiedAndEscapeShortcuts() = 59 runComposeUiTest { 60 val shortcuts = mutableListOf<ShellShortcut>() 61 setHarvestCircleContent { 62 ShellKeyboardHost(onShortcut = shortcuts::add) { 63 RouteFocusTarget("today", "Today main content", content = {}) 64 } 65 } 66 67 onNodeWithTag("route-focus-target").performKeyInput { 68 keyDown(Key.CtrlLeft) 69 keyDown(Key.K) 70 keyUp(Key.K) 71 keyUp(Key.CtrlLeft) 72 keyDown(Key.Escape) 73 keyUp(Key.Escape) 74 } 75 kotlin.test.assertEquals( 76 listOf(ShellShortcut.OpenNostrReference, ShellShortcut.CloseOverlay), 77 shortcuts, 78 ) 79 } 80 81 @Test 82 fun keyboardHostSuppressesBackgroundShortcutsWhileModalIsOpen() = 83 runComposeUiTest { 84 val shortcuts = mutableListOf<ShellShortcut>() 85 setHarvestCircleContent { 86 ShellKeyboardHost( 87 modal = FoundationOverlay.Status(StatusOverlayKey.Sync), 88 onShortcut = shortcuts::add, 89 ) { 90 RouteFocusTarget("today", "Today main content", content = {}) 91 } 92 } 93 94 onNodeWithTag("route-focus-target").performKeyInput { 95 keyDown(Key.CtrlLeft) 96 keyDown(Key.K) 97 keyUp(Key.K) 98 keyUp(Key.CtrlLeft) 99 keyDown(Key.Escape) 100 keyUp(Key.Escape) 101 } 102 kotlin.test.assertEquals(listOf(ShellShortcut.CloseOverlay), shortcuts) 103 } 104 105 @Test 106 fun hcSc011ModalRemovesTheBackgroundSemanticsSubtree() = 107 runComposeUiTest { 108 setHarvestCircleContent { 109 HarvestCircleShell( 110 state = modalBootstrapState(), 111 identityActions = HarvestCircleUiActions(), 112 platformActions = HarvestCirclePlatformActions(), 113 dispatch = {}, 114 ) 115 } 116 117 onAllNodesWithTag("bootstrap-welcome").assertCountEquals(0) 118 onAllNodesWithTag("foundation-overlay").assertCountEquals(1) 119 } 120 121 @Test 122 fun routeTargetRestoresFocusAfterAClosedModal() = 123 runComposeUiTest { 124 var modalOpen by mutableStateOf(false) 125 var restoreTarget by mutableStateOf<ShellFocusTarget?>(null) 126 setHarvestCircleContent { 127 val registry = remember { ShellFocusRegistry() } 128 CompositionLocalProvider(LocalShellFocusRegistry provides registry) { 129 RouteFocusTarget("today", "Today main content") { 130 HarvestCircleLabeledButton( 131 label = "Open", 132 accessibilityLabel = "Open dialog", 133 modifier = 134 Modifier 135 .shellFocusTarget(ShellFocusTarget.TodayReference) 136 .testTag("restore-trigger"), 137 onClick = { 138 restoreTarget = null 139 modalOpen = true 140 }, 141 ) 142 if (modalOpen) { 143 FoundationOverlayHost( 144 org.harvestcircle.application.OverlayState( 145 org.harvestcircle.application.FoundationOverlay.Status( 146 org.harvestcircle.application.StatusOverlayKey.Sync, 147 ), 148 ), 149 org.harvestcircle.application.ShellStatusModel( 150 org.harvestcircle.application.SyncStatusLabel.NotYetObserved, 151 org.harvestcircle.application.SignerStatusLabel.SignedOut, 152 null, 153 ), 154 ) { 155 modalOpen = false 156 restoreTarget = ShellFocusTarget.TodayReference 157 } 158 } 159 } 160 ShellFocusRestorer(restoreTarget, ShellFocusTarget.RouteFallback) 161 } 162 } 163 164 onNodeWithTag("restore-trigger").performClick() 165 onNodeWithTag("overlay-close").assertIsFocused().performClick() 166 onNodeWithTag("restore-trigger").assertIsFocused() 167 } 168 } 169 170 private fun modalBootstrapState(): HarvestCircleShellState = 171 HarvestCircleShellState( 172 identity = 173 HarvestCirclePresenterState( 174 ApplicationSnapshot( 175 revision = SnapshotRevision(1UL), 176 lifecycle = ApplicationLifecycle.Ready, 177 lifecycleProblem = null, 178 configuredRelays = emptyList(), 179 identities = emptyList(), 180 selectedIdentityId = null, 181 session = SessionLifecycle.SignedOut, 182 sessionSubjectIdentityId = null, 183 sessionProblem = null, 184 activeIdentity = null, 185 recoverableProblem = null, 186 ), 187 ), 188 buildInfo = BuildInfo.unknown(), 189 overlays = OverlayState(current = FoundationOverlay.Status(StatusOverlayKey.Sync)), 190 )