app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 276aed5f681c0a192840b52ef7a95886eeaf349a
parent 184afe8c00c3d7f33873e9500e948cfd1333667e
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 22:51:48 +0000

accessibility: close shell interaction gaps

- Apply the locked shell, deferred-action, and reference-dialog copy.
- Expose selected state for navigation, tabs, and appearance choices.
- Label modal titles and restore route focus after dialog dismissal.
- Prove shortcuts and large-text Network and Settings reachability.

Diffstat:
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt | 4++--
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt | 8+++++---
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHost.kt | 29++++++++++++++++++++---------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreen.kt | 32++++++++++++++++++--------------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt | 2+-
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt | 6+++++-
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt | 1+
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellAccessibility.kt | 13+++++++++++++
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellControls.kt | 1+
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt | 2+-
Mapp/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/ShellControlsTest.kt | 2+-
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt | 26++++++++++++++++++++++++++
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHostTest.kt | 6++++++
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreenTest.kt | 32++++++++++++++++++++++++++++++++
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt | 1+
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt | 3++-
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/ShellAccessibilityUiTest.kt | 33+++++++++++++++++++++++++++++++++
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt | 3+++
18 files changed, 171 insertions(+), 33 deletions(-)

diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt @@ -68,7 +68,7 @@ val addFarmWorkspaceAction = ShellWorkspaceAction( label = "Add a farm workspace", enabled = false, - unavailableExplanation = "Available after collective contracts are implemented.", + unavailableExplanation = "Not available in this build.", ) sealed interface ShellRoot { @@ -170,7 +170,7 @@ private fun navigationItem(screenKey: ScreenKey): ShellNavigationItem { screenKey = screenKey, label = screenKey.label(), enabled = enabled, - unavailableExplanation = if (enabled) null else "Available after collective contracts are implemented.", + unavailableExplanation = if (enabled) null else "Not available in this build.", route = route.takeIf { enabled }, ) } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreen.kt @@ -94,12 +94,14 @@ fun FoundationNetworkScreen( tabRail = { available, current -> Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { available.forEach { tab -> - ShellAction( + ShellTab( label = tab.label, description = "Show ${tab.label}", - tag = "network-tab-${tab.key.value}", + selected = tab.key == current, + onClick = { selected = tab.key }, + modifier = Modifier.testTag("network-tab-${tab.key.value}"), enabled = tab.key != current, - ) { selected = tab.key } + ) } } }, diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHost.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHost.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -44,8 +46,10 @@ fun FoundationOverlayHost( ShellSurface( Modifier .focusGroup() - .semantics { contentDescription = "Dialog" } - .testTag("foundation-overlay") + .semantics { + contentDescription = "Dialog: ${overlay.title()}" + paneTitle = overlay.title() + }.testTag("foundation-overlay") .padding(24.dp), ) { when (overlay) { @@ -70,7 +74,7 @@ private fun ConfirmOverlay( ) { val requester = remember { FocusRequester() } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - ShellText(overlay.title, textRole = ShellTextRole.SectionTitle) + ShellText(overlay.title, Modifier.semantics { heading() }, ShellTextRole.SectionTitle) ShellText(overlay.explanation) Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { ShellButton( @@ -95,7 +99,7 @@ private fun StatusOverlay( ) { val requester = remember { FocusRequester() } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - ShellText(title, textRole = ShellTextRole.SectionTitle) + ShellText(title, Modifier.semantics { heading() }, ShellTextRole.SectionTitle) ShellText(status, Modifier.testTag("overlay-status")) ShellButton( "Close", @@ -115,20 +119,20 @@ private fun ReferenceOverlay( ) { val requester = remember { FocusRequester() } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - ShellText("Open a Nostr reference", textRole = ShellTextRole.SectionTitle) + ShellText("Open a Nostr reference", Modifier.semantics { heading() }, ShellTextRole.SectionTitle) ShellTextField( value = overlay.input, onValueChange = { onIntent(OverlayIntent.EditReference(it)) }, - label = "Nostr reference", - placeholder = "npub1…, note1…, or nevent1…", + label = "Nostr link, event ID, or address", + placeholder = "nostr:…", modifier = Modifier.focusRequester(requester).testTag("nostr-reference-input"), enabled = !busy, ) overlay.result?.let { ShellText(it.message, Modifier.testTag("nostr-reference-result")) } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { ShellButton( - "Open reference", - "Open Nostr reference", + "Open a Nostr reference", + "Open a Nostr reference", { onIntent(OverlayIntent.SubmitReference) }, Modifier.testTag("nostr-reference-submit"), enabled = !busy, @@ -139,3 +143,10 @@ private fun ReferenceOverlay( } LaunchedEffect(Unit) { requester.requestFocus() } } + +private fun FoundationOverlay.title(): String = + when (this) { + is FoundationOverlay.ConfirmAction -> title + is FoundationOverlay.Status -> if (key == StatusOverlayKey.Signer) "Signer status" else "Sync status" + is FoundationOverlay.OpenNostrReference -> "Open a Nostr reference" + } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreen.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreen.kt @@ -48,16 +48,18 @@ fun FoundationSettingsScreen( tabRail = { available, current -> Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { available.forEach { tab -> - ShellAction( - tab.label, - "Show ${tab.label} settings", - "settings-${tab.key.value}", + ShellTab( + label = tab.label, + description = "Show ${tab.label} settings", + selected = tab.key == current, + onClick = { + actions.selectSection( + if (tab.key.value == "appearance") SettingsSection.Appearance else SettingsSection.Project, + ) + }, + modifier = Modifier.testTag("settings-${tab.key.value}"), enabled = tab.key != current, - ) { - actions.selectSection( - if (tab.key.value == "appearance") SettingsSection.Appearance else SettingsSection.Project, - ) - } + ) } } }, @@ -114,12 +116,14 @@ private fun <T : Enum<T>> OptionRow( Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { values.forEach { value -> val label = value.label() - ShellAction( - label, - "Select $label", - "$tagPrefix-${value.name.lowercase()}", + ShellTab( + label = label, + description = "Select $label", + selected = value == selected, + onClick = { select(value) }, + modifier = Modifier.testTag("$tagPrefix-${value.name.lowercase()}"), enabled = value != selected, - ) { select(value) } + ) } } } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt @@ -44,7 +44,7 @@ fun GlobalTopBar( ShellAction("Forward", "Go forward", "top-bar-forward", model.canGoForward) { onIntent(GlobalTopBarIntent.Forward) } - ShellAction("Open reference", "Open Nostr reference", "top-bar-open-reference") { + ShellAction("Open a Nostr reference", "Open a Nostr reference", "top-bar-open-reference") { onIntent(GlobalTopBarIntent.OpenNostrReference) } ShellAction(model.syncStatus.text, "Sync status", "top-bar-sync") { onIntent(GlobalTopBarIntent.ShowSyncStatus) } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt @@ -160,7 +160,11 @@ private fun DashboardRoot( sidebar = { WorkspaceSidebar(route.screenKey) { dispatch(HarvestCircleShellIntent.Navigate(it)) } }, mainHeader = { MainPanelHeader(MainPanelHeaderModel(title = route.title())) }, mainBody = { - RouteFocusTarget(route.toString(), "${route.title()} main content") { + RouteFocusTarget( + route.toString(), + "${route.title()} main content", + restoreFocus = state.overlays.current == null, + ) { when (route) { AppRoute.PersonalToday -> FoundationTodayScreen( diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt @@ -55,6 +55,7 @@ fun MainPanelHeader( selected = tab.key == model.selectedTab, onClick = { onTabSelected(tab.key) }, modifier = Modifier.testTag("main-tab-${tab.key.value}"), + enabled = tab.key != model.selectedTab, ) } Row(Modifier.testTag("main-secondary-action")) { secondaryAction() } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellAccessibility.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellAccessibility.kt @@ -6,7 +6,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -71,9 +74,11 @@ fun ShellKeyboardHost( fun RouteFocusTarget( routeKey: String, label: String, + restoreFocus: Boolean = true, content: @Composable () -> Unit, ) { val requester = remember(routeKey) { FocusRequester() } + var modalWasOpen by remember(routeKey) { mutableStateOf(false) } Box( Modifier .fillMaxSize() @@ -85,6 +90,14 @@ fun RouteFocusTarget( content() } LaunchedEffect(routeKey) { requester.requestFocus() } + LaunchedEffect(restoreFocus) { + if (!restoreFocus) { + modalWasOpen = true + } else if (modalWasOpen) { + requester.requestFocus() + modalWasOpen = false + } + } } private fun KeyEvent.toShellShortcut(): ShellShortcut? { diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellControls.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/ShellControls.kt @@ -56,6 +56,7 @@ fun shellControlVisualState( hovered: Boolean, ): ShellControlVisualState = when { + !enabled && selected -> ShellControlVisualState.Selected !enabled -> ShellControlVisualState.Disabled focused -> ShellControlVisualState.Focused pressed -> ShellControlVisualState.Pressed diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt @@ -50,7 +50,7 @@ private fun SidebarItem( selected = selected, onClick = { onScreen(item.screenKey) }, modifier = Modifier.fillMaxWidth().testTag("sidebar-${item.screenKey.name}"), - enabled = item.enabled, + enabled = item.enabled && !selected, ) } diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/ShellControlsTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/ShellControlsTest.kt @@ -6,7 +6,7 @@ import kotlin.test.assertEquals class ShellControlsTest { @Test fun visualStateUsesAccessibilityAndInteractionPriority() { - assertEquals(ShellControlVisualState.Disabled, shellControlVisualState(false, true, true, true, true)) + assertEquals(ShellControlVisualState.Selected, shellControlVisualState(false, true, true, true, true)) assertEquals(ShellControlVisualState.Focused, shellControlVisualState(true, true, true, true, true)) assertEquals(ShellControlVisualState.Pressed, shellControlVisualState(true, true, false, true, true)) assertEquals(ShellControlVisualState.Selected, shellControlVisualState(true, true, false, false, true)) diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationNetworkScreenTest.kt @@ -1,14 +1,24 @@ package org.harvestcircle.ui.shell +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.unit.dp import org.harvestcircle.application.ApplicationLifecycle import org.harvestcircle.application.RelayDestination +import org.harvestcircle.design.AppearanceState +import org.harvestcircle.design.TextSizePreference import kotlin.test.Test @OptIn(ExperimentalTestApi::class) @@ -18,6 +28,7 @@ class FoundationNetworkScreenTest { runComposeUiTest { setContent { FoundationNetworkScreen(model()) } onNodeWithTag("bounded-detail-network").assertExists() + onNodeWithTag("network-tab-overview").assertIsSelected().assertIsNotEnabled() onNodeWithText("Signer").assertExists() onNodeWithText("Local identity active").assertExists() onNodeWithText("No managed HarvestCircle service is configured.").assertExists() @@ -62,6 +73,21 @@ class FoundationNetworkScreenTest { onNodeWithText("Not yet observed").assertExists() onNodeWithText("No public relay endpoints are configured.").assertExists() } + + @Test + fun veryLargeRuntimeDetailsRemainReachableInTheBoundedPane() = + runComposeUiTest { + setContent { + Box(Modifier.size(640.dp, 360.dp)) { + HarvestCircleTheme(AppearanceState(textSize = TextSizePreference.VeryLarge)) { + FoundationNetworkScreen(model()) + } + } + } + + onNodeWithTag("network-tab-runtime").performClick() + onNodeWithTag("network-runtime-problem").performScrollTo().assertIsDisplayed() + } } private fun model( diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHostTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHostTest.kt @@ -7,7 +7,9 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -53,6 +55,10 @@ class FoundationOverlayHostTest { } } onAllNodesWithTag("foundation-overlay").assertCountEquals(1) + onNode(isDialog()).assertExists() + onAllNodesWithText("Open a Nostr reference").assertCountEquals(2) + onNodeWithText("Nostr link, event ID, or address").assertExists() + onNodeWithText("nostr:…").assertExists() onAllNodesWithTag("global-status-banner").assertCountEquals(1) onNodeWithTag("nostr-reference-input").assertIsFocused().performTextInput("note1qqqqqq") onNodeWithTag("nostr-reference-submit").performClick() diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreenTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationSettingsScreenTest.kt @@ -1,13 +1,21 @@ package org.harvestcircle.ui.shell +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.unit.dp import org.harvestcircle.application.BuildInfo import org.harvestcircle.design.AppearanceState import org.harvestcircle.design.MotionPreference @@ -38,6 +46,10 @@ class FoundationSettingsScreenTest { } onNodeWithTag("bounded-detail-settings").assertExists() + onNodeWithTag("settings-appearance").assertIsSelected().assertIsNotEnabled() + onNodeWithTag("theme-system").assertIsSelected().assertIsNotEnabled() + onNodeWithTag("text-size-default").assertIsSelected().assertIsNotEnabled() + onNodeWithTag("motion-standard").assertIsSelected().assertIsNotEnabled() onNodeWithTag("theme-dark").performClick() onNodeWithTag("text-size-verylarge").performClick() onNodeWithTag("motion-reduced").performClick() @@ -78,4 +90,24 @@ class FoundationSettingsScreenTest { onAllNodesWithText("Locality").assertCountEquals(0) onAllNodesWithText("Delete data").assertCountEquals(0) } + + @Test + fun veryLargeProjectFactsRemainReachableInTheBoundedPane() = + runComposeUiTest { + setContent { + Box(Modifier.size(640.dp, 360.dp)) { + HarvestCircleTheme(AppearanceState(textSize = TextSizePreference.VeryLarge)) { + FoundationSettingsScreen( + SettingsSection.Project, + AppearanceState(textSize = TextSizePreference.VeryLarge), + BuildInfo.unknown(), + FoundationSettingsActions({}, {}, {}, {}), + HarvestCirclePlatformActions(), + ) + } + } + } + + onNodeWithTag("project-storage-schema").performScrollTo().assertIsDisplayed() + } } diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt @@ -30,6 +30,7 @@ class GlobalTopBarTest { } onNodeWithText("Limited connection").performClick() onNodeWithText("Read-only").performClick() + onNodeWithText("Open a Nostr reference").assertExists() onNodeWithTag("top-bar-open-reference").performClick() onNodeWithTag("top-bar-forward").assertIsNotEnabled() assertEquals( diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt @@ -3,6 +3,7 @@ package org.harvestcircle.ui.shell import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag @@ -34,7 +35,7 @@ class MainPanelHeaderTest { } onNodeWithText("Network").assertIsDisplayed() onNodeWithTag("main-breadcrumb").assertIsDisplayed() - onNodeWithTag("main-tab-overview").assertIsSelected() + onNodeWithTag("main-tab-overview").assertIsSelected().assertIsNotEnabled() onNodeWithTag("main-tab-identity").performClick() onAllNodesWithTag("main-primary-action").assertCountEquals(1) assertEquals(listOf(identity.key), selections) diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/ShellAccessibilityUiTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/ShellAccessibilityUiTest.kt @@ -1,10 +1,14 @@ package org.harvestcircle.ui.shell +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.input.key.Key import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performKeyInput import androidx.compose.ui.test.v2.runComposeUiTest import androidx.compose.ui.unit.dp @@ -48,4 +52,33 @@ class ShellAccessibilityUiTest { shortcuts, ) } + + @Test + fun routeTargetRestoresFocusAfterAClosedModal() = + runComposeUiTest { + var modalOpen by mutableStateOf(false) + setContent { + RouteFocusTarget("today", "Today main content", restoreFocus = !modalOpen) { + ShellAction("Open", "Open dialog", "restore-trigger") { modalOpen = true } + if (modalOpen) { + FoundationOverlayHost( + org.harvestcircle.application.OverlayState( + org.harvestcircle.application.FoundationOverlay.Status( + org.harvestcircle.application.StatusOverlayKey.Sync, + ), + ), + org.harvestcircle.application.ShellStatusModel( + org.harvestcircle.application.SyncStatusLabel.NotYetObserved, + org.harvestcircle.application.SignerStatusLabel.SignedOut, + null, + ), + ) { modalOpen = false } + } + } + } + + onNodeWithTag("restore-trigger").performClick() + onNodeWithTag("overlay-close").assertIsFocused().performClick() + onNodeWithTag("route-focus-target").assertIsFocused() + } } diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt @@ -3,6 +3,7 @@ package org.harvestcircle.ui.shell import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -20,9 +21,11 @@ class WorkspaceSidebarTest { val selected = mutableListOf<ScreenKey>() setContent { WorkspaceSidebar(ScreenKey.PersonalToday, selected::add) } onNodeWithTag("sidebar-PersonalToday").assertIsSelected() + onNodeWithTag("sidebar-PersonalToday").assertIsNotEnabled() onNodeWithText("Explore").assertIsNotEnabled() onNodeWithText("Activity").assertIsNotEnabled() onNodeWithText("Add a farm workspace").assertIsNotEnabled() + onNodeWithContentDescription("Explore. Not available in this build.").assertExists() onNodeWithText("Explore").performClick() onNodeWithText("Network").performClick() onNodeWithText("Settings").performClick()