commit b54b08da083d280b0c2797f1b72133719bb75714 parent f0632a1b7479e8cc243af0d96697c83658cce12c Author: triesap <tyson@radroots.org> Date: Fri, 14 Aug 2026 17:44:59 +0000 ui: resolve dashboard window chrome geometry - Replace the sidebar magic offset with root-coordinate clearances - Relocate fully occluded sidebar controls into safe top-bar content - Hide redundant branding when native chrome consumes its layout slot - Refresh macOS goldens and verify responsive interaction bounds Diffstat:
11 files changed, 272 insertions(+), 41 deletions(-)
diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleAppFrame.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleAppFrame.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -44,6 +45,9 @@ public data class HarvestCircleFrameGeometry( public val sidebarWidth: Dp, public val showSecondaryPane: Boolean, public val showUtilityPane: Boolean, + public val sidebarChromeClearance: HarvestCircleWindowChromeClearance, + public val topBarChromeClearance: HarvestCircleWindowChromeClearance, + public val sidebarTopBandFullyExcluded: Boolean, ) public class HarvestCirclePaneSlot( @@ -67,6 +71,7 @@ public fun resolveHarvestCircleFrameGeometry( metrics: HarvestCircleFrameMetrics, secondaryWidth: HarvestCirclePaneWidth? = null, utilityWidth: HarvestCirclePaneWidth? = null, + windowChromeExclusion: HarvestCircleWindowChromeExclusion = HarvestCircleWindowChromeExclusion.None, ): HarvestCircleFrameGeometry { val layoutClass = when { @@ -99,22 +104,48 @@ public fun resolveHarvestCircleFrameGeometry( val secondaryRequested = secondaryWidth != null val utilityRequested = utilityWidth != null - return when (layoutClass) { - HarvestCircleFrameLayoutClass.Compact -> - HarvestCircleFrameGeometry(layoutClass, sidebarWidth, false, false) - - HarvestCircleFrameLayoutClass.Medium -> { - val showSecondary = secondaryRequested && fits(true, false) - val showUtility = !showSecondary && utilityRequested && fits(false, true) - HarvestCircleFrameGeometry(layoutClass, sidebarWidth, showSecondary, showUtility) + val (showSecondary, showUtility) = + when (layoutClass) { + HarvestCircleFrameLayoutClass.Compact -> false to false + HarvestCircleFrameLayoutClass.Medium -> { + val secondary = secondaryRequested && fits(true, false) + secondary to (!secondary && utilityRequested && fits(false, true)) + } + HarvestCircleFrameLayoutClass.Expanded -> { + val secondary = secondaryRequested && fits(true, false) + secondary to (utilityRequested && fits(secondary, true)) + } } + val sidebarRegionWidth = minOf(sidebarWidth, width) + val topBarRegionLeft = minOf(width, sidebarWidth + metrics.structuralDividerWidth) + val sidebarChromeClearance = + resolveHarvestCircleWindowChromeClearance( + exclusion = windowChromeExclusion, + windowWidth = width, + regionLeft = 0.dp, + regionWidth = sidebarRegionWidth, + minimumTopBandHeight = metrics.topBarHeight, + ) + val topBarChromeClearance = + resolveHarvestCircleWindowChromeClearance( + exclusion = windowChromeExclusion, + windowWidth = width, + regionLeft = topBarRegionLeft, + regionWidth = width - topBarRegionLeft, + minimumTopBandHeight = metrics.topBarHeight, + ) - HarvestCircleFrameLayoutClass.Expanded -> { - val showSecondary = secondaryRequested && fits(true, false) - val showUtility = utilityRequested && fits(showSecondary, true) - HarvestCircleFrameGeometry(layoutClass, sidebarWidth, showSecondary, showUtility) - } - } + return HarvestCircleFrameGeometry( + layoutClass = layoutClass, + sidebarWidth = sidebarWidth, + showSecondaryPane = showSecondary, + showUtilityPane = showUtility, + sidebarChromeClearance = sidebarChromeClearance, + topBarChromeClearance = topBarChromeClearance, + sidebarTopBandFullyExcluded = + sidebarRegionWidth > 0.dp && + sidebarChromeClearance.left + sidebarChromeClearance.right >= sidebarRegionWidth, + ) } @Composable @@ -149,6 +180,7 @@ public fun HarvestCircleAppFrame( metrics = frameMetrics, secondaryWidth = secondaryPane?.width, utilityWidth = utilityPane?.width, + windowChromeExclusion = HarvestCircleWindowChrome.exclusion, ) Row(Modifier.fillMaxSize()) { @@ -173,10 +205,20 @@ public fun HarvestCircleAppFrame( modifier = Modifier .fillMaxWidth() - .height(frameMetrics.topBarHeight) + .height(geometry.topBarChromeClearance.topBandHeight) + .background(shellColors.applicationFrame) .testTag("harvestcircle-top-bar"), ) { - topBar(geometry) + Box( + Modifier + .fillMaxSize() + .absolutePadding( + left = geometry.topBarChromeClearance.left, + right = geometry.topBarChromeClearance.right, + ).testTag("harvestcircle-top-bar-chrome-content"), + ) { + topBar(geometry) + } } HarvestCircleStructuralDivider(vertical = false) Row( diff --git a/app/design_system/src/commonTest/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleAppFrameTest.kt b/app/design_system/src/commonTest/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleAppFrameTest.kt @@ -40,4 +40,41 @@ class HarvestCircleAppFrameTest { assertTrue(geometry.showSecondaryPane) assertTrue(geometry.showUtilityPane) } + + @Test + fun expandedSidebarAbsorbsTheMacOsChromeExclusion() { + val geometry = + resolveHarvestCircleFrameGeometry( + width = 1280.dp, + sidebarCollapsed = false, + metrics = HarvestCircleDefaultFrameMetrics, + windowChromeExclusion = macOsExclusion(), + ) + + assertEquals(112.dp, geometry.sidebarChromeClearance.left) + assertEquals(0.dp, geometry.topBarChromeClearance.left) + assertFalse(geometry.sidebarTopBandFullyExcluded) + } + + @Test + fun collapsedSidebarMovesResidualMacOsClearanceIntoTheTopBar() { + val geometry = + resolveHarvestCircleFrameGeometry( + width = 1280.dp, + sidebarCollapsed = true, + metrics = HarvestCircleDefaultFrameMetrics, + windowChromeExclusion = macOsExclusion(), + ) + + assertEquals(72.dp, geometry.sidebarChromeClearance.left) + assertEquals(39.dp, geometry.topBarChromeClearance.left) + assertTrue(geometry.sidebarTopBandFullyExcluded) + } + + private fun macOsExclusion(): HarvestCircleWindowChromeExclusion = + HarvestCircleWindowChromeExclusion( + edge = HarvestCircleWindowChromeEdge.Left, + width = 112.dp, + height = 40.dp, + ) } diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt @@ -2,11 +2,13 @@ package org.harvestcircle.application import androidx.compose.foundation.clickable import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed @@ -23,6 +25,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEnvironment +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeExclusion import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -38,7 +42,7 @@ class HarvestCircleApplicationTest { var runtime: ApplicationRuntime? = null var applicationJob: Job? = null - setContent { + setApplicationContent { HarvestCircleApplication( closeRequested = closeRequested, onExitApproved = { approvedExits += 1 }, @@ -77,7 +81,7 @@ class HarvestCircleApplicationTest { @Test fun welcomeEntersOneReadOnlyDashboardSession() = runComposeUiTest { - setContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } + setApplicationContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } onNodeWithText("Coordinate local food with clear, signed terms.").assertIsDisplayed() onAllNodesWithText("Connect a remote signer").assertCountEquals(0) onNodeWithTag("bootstrap-read-only").performClick() @@ -90,7 +94,7 @@ class HarvestCircleApplicationTest { @Test fun welcomeRoutesToLocalCreateAndImportEntry() = runComposeUiTest { - setContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } + setApplicationContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } onNodeWithTag("bootstrap-create").performClick() waitUntil { onAllNodesWithTag("generate-key").fetchSemanticsNodes().isNotEmpty() } onNodeWithTag("generate-key").assertIsDisplayed() @@ -100,7 +104,7 @@ class HarvestCircleApplicationTest { @Test fun welcomeRoutesDirectlyToMaskedImportEntry() = runComposeUiTest { - setContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } + setApplicationContent { HarvestCircleApplication(presenterFactory = ::testPresenter) } onNodeWithTag("bootstrap-import").performClick() waitUntil { onAllNodesWithTag("import-nsec-input").fetchSemanticsNodes().isNotEmpty() } onNodeWithTag("import-nsec-input").assertIsDisplayed() @@ -114,7 +118,7 @@ class HarvestCircleApplicationTest { var runtime: ApplicationRuntime? = null var applicationJob: Job? = null - setContent { + setApplicationContent { if (showApplication) { HarvestCircleApplication { scope -> applicationJob = scope.coroutineContext[Job] @@ -174,7 +178,7 @@ class HarvestCircleApplicationTest { fun failedNativeShutdownRequiresAnExplicitForceExitChoice() = runComposeUiTest { var approvedExits = 0 - setContent { + setApplicationContent { HarvestCircleApplication( closeRequested = true, onExitApproved = { approvedExits += 1 }, @@ -199,7 +203,7 @@ class HarvestCircleApplicationTest { fun shutdownTimeoutRequiresAnExplicitForceExitChoice() = runComposeUiTest { var approvedExits = 0 - setContent { + setApplicationContent { HarvestCircleApplication( closeRequested = true, onExitApproved = { approvedExits += 1 }, @@ -224,7 +228,7 @@ class HarvestCircleApplicationTest { @Test fun applicationRendersSafeStartupFailureWithoutLeakingInternalMessage() = runComposeUiTest { - setContent { + setApplicationContent { HarvestCircleApplication { error("sensitive internal startup detail") } @@ -236,6 +240,13 @@ class HarvestCircleApplicationTest { } } +@OptIn(ExperimentalTestApi::class) +private fun ComposeUiTest.setApplicationContent(content: @Composable () -> Unit) { + setContent { + HarvestCircleWindowChromeEnvironment(HarvestCircleWindowChromeExclusion.None, content) + } +} + private fun testPresenter(scope: kotlinx.coroutines.CoroutineScope): HarvestCirclePresenter = HarvestCirclePresenter( runtime = ApplicationRuntime(), 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 @@ -67,6 +67,9 @@ fun GlobalTopBar( model: GlobalTopBarModel, onIntent: (GlobalTopBarIntent) -> Unit, compact: Boolean = false, + showSidebarToggle: Boolean = false, + sidebarCollapsed: Boolean = false, + onToggleSidebar: () -> Unit = {}, ) { val colors = HarvestCircleShellPalette Row( @@ -79,6 +82,14 @@ fun GlobalTopBar( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(HarvestCircleShellMetrics.topBarControlGap), ) { + if (showSidebarToggle) { + HarvestCircleShellIconButton( + onClick = onToggleSidebar, + icon = if (sidebarCollapsed) HarvestCircleIcons.ChevronRight else HarvestCircleIcons.ChevronLeft, + label = if (sidebarCollapsed) "Expand sidebar" else "Collapse sidebar", + modifier = Modifier.testTag("top-bar-sidebar-toggle"), + ) + } HarvestCircleShellTab( "Today", model.selectedScreen == ScreenKey.PersonalToday, 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 @@ -228,6 +228,9 @@ private fun DashboardRoot( ), onIntent = { intent -> dispatchTopBar(intent, platformActions, dispatch) }, compact = geometry.layoutClass != HarvestCircleFrameLayoutClass.Expanded, + showSidebarToggle = geometry.sidebarTopBandFullyExcluded, + sidebarCollapsed = geometry.sidebarWidth == HarvestCircleDesignTokens.shell.frame.collapsedSidebarWidth, + onToggleSidebar = { sidebarCollapsed = !sidebarCollapsed }, ) }, sidebar = { geometry -> @@ -237,6 +240,8 @@ private fun DashboardRoot( compact = geometry.sidebarWidth == HarvestCircleDesignTokens.shell.frame.collapsedSidebarWidth, onToggleCollapsed = { sidebarCollapsed = !sidebarCollapsed }, sessionLabel = todayContext(state), + chromeClearance = geometry.sidebarChromeClearance, + topBandFullyExcluded = geometry.sidebarTopBandFullyExcluded, ) }, mainHeader = { 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -23,6 +24,7 @@ import org.harvestcircle.application.addFarmWorkspaceAction import org.harvestcircle.application.shellNavigationItems import org.harvestcircle.application.shellSettingsItem import org.harvestcircle.designsystem.icon.HarvestCircleIcons +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeClearance import org.harvestcircle.designsystem.primitive.HarvestCircleIcon import org.harvestcircle.designsystem.shell.HarvestCircleShellButton import org.harvestcircle.designsystem.shell.HarvestCircleShellIconButton @@ -41,8 +43,18 @@ fun WorkspaceSidebar( compact: Boolean = false, onToggleCollapsed: () -> Unit = {}, sessionLabel: String = "Read-only", + chromeClearance: HarvestCircleWindowChromeClearance = + HarvestCircleWindowChromeClearance( + topBandHeight = HarvestCircleShellMetrics.sidebarHeaderHeight, + left = 0.dp, + right = 0.dp, + ), + topBandFullyExcluded: Boolean = false, ) { val colors = HarvestCircleShellPalette + val chromeConsumesTitle = + chromeClearance.left > HarvestCircleShellMetrics.sidebarHorizontalInset || + chromeClearance.right > HarvestCircleShellMetrics.sidebarHorizontalInset Column( Modifier .fillMaxHeight() @@ -53,22 +65,28 @@ fun WorkspaceSidebar( Row( Modifier .fillMaxWidth() - .height(HarvestCircleShellMetrics.sidebarHeaderHeight) - .padding(start = if (compact) HarvestCircleShellMetrics.sidebarHorizontalInset else 74.dp, end = 10.dp), + .height(chromeClearance.topBandHeight) + .absolutePadding( + left = maxOf(HarvestCircleShellMetrics.sidebarHorizontalInset, chromeClearance.left), + right = maxOf(10.dp, chromeClearance.right), + ).testTag("workspace-sidebar-chrome-content"), verticalAlignment = Alignment.CenterVertically, ) { - if (!compact) { + if (!compact && !chromeConsumesTitle) { HarvestCircleShellText("HarvestCircle", Modifier.weight(1f), HarvestCircleShellTextRole.PaneTitle) } else { Spacer(Modifier.weight(1f)) } - HarvestCircleShellIconButton( - onClick = onToggleCollapsed, - icon = HarvestCircleIcons.ChevronLeft, - label = if (compact) "Expand sidebar" else "Collapse sidebar", - controlSize = HarvestCircleShellMetrics.sidebarHeaderIconTarget, - iconSize = HarvestCircleShellMetrics.sidebarHeaderIconSize, - ) + if (!topBandFullyExcluded) { + HarvestCircleShellIconButton( + onClick = onToggleCollapsed, + icon = HarvestCircleIcons.ChevronLeft, + label = if (compact) "Expand sidebar" else "Collapse sidebar", + modifier = Modifier.testTag("workspace-sidebar-toggle"), + controlSize = HarvestCircleShellMetrics.sidebarHeaderIconTarget, + iconSize = HarvestCircleShellMetrics.sidebarHeaderIconSize, + ) + } } Box(Modifier.fillMaxWidth().padding(horizontal = HarvestCircleShellMetrics.sidebarHorizontalInset)) { diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/DashboardScaffoldTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/DashboardScaffoldTest.kt @@ -11,9 +11,17 @@ import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runComposeUiTest import androidx.compose.ui.unit.dp +import org.harvestcircle.application.SignerStatusLabel +import org.harvestcircle.application.SyncStatusLabel +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEdge +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeExclusion +import org.harvestcircle.product.ScreenKey import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) class DashboardScaffoldTest { @@ -48,6 +56,64 @@ class DashboardScaffoldTest { onAllNodesWithTag("dashboard-inspector-beside").assertCountEquals(0) onAllNodesWithTag("dashboard-inspector-overlay").assertCountEquals(1) } + + @Test + fun collapsedMacOsSidebarRelocatesItsControlIntoSafeTopBarContent() = + runComposeUiTest { + var expandRequests = 0 + setHarvestCircleContent( + windowChromeExclusion = + HarvestCircleWindowChromeExclusion( + edge = HarvestCircleWindowChromeEdge.Left, + width = 112.dp, + height = 40.dp, + ), + ) { + Box(Modifier.requiredSize(1280.dp, 720.dp)) { + DashboardScaffold( + inspectorVisible = false, + sidebarCollapsed = true, + topBar = { geometry -> + GlobalTopBar( + model = + GlobalTopBarModel( + canGoBack = false, + canGoForward = false, + syncStatus = SyncStatusLabel.Available, + signerStatus = SignerStatusLabel.ReadOnly, + ), + onIntent = {}, + compact = true, + showSidebarToggle = geometry.sidebarTopBandFullyExcluded, + sidebarCollapsed = true, + onToggleSidebar = { expandRequests += 1 }, + ) + }, + sidebar = { geometry -> + WorkspaceSidebar( + selected = ScreenKey.PersonalToday, + onScreen = {}, + compact = true, + chromeClearance = geometry.sidebarChromeClearance, + topBandFullyExcluded = geometry.sidebarTopBandFullyExcluded, + ) + }, + mainHeader = {}, + mainBody = {}, + ) + } + } + + val frame = onNodeWithTag("harvestcircle-frame").fetchSemanticsNode().boundsInRoot + val safeTopBar = onNodeWithTag("harvestcircle-top-bar-chrome-content").fetchSemanticsNode().boundsInRoot + val relocatedToggle = onNodeWithTag("top-bar-sidebar-toggle").fetchSemanticsNode().boundsInRoot + + assertEquals(frame.left + 112f, safeTopBar.left) + assertTrue(relocatedToggle.left >= frame.left + 112f) + onAllNodesWithTag("workspace-sidebar-toggle").assertCountEquals(0) + onNodeWithTag("top-bar-sidebar-toggle").performClick() + assertEquals(1, expandRequests) + } } @androidx.compose.runtime.Composable diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleMacGoldenTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleMacGoldenTest.kt @@ -22,6 +22,9 @@ import org.harvestcircle.application.HarvestCircleShellState import org.harvestcircle.application.SessionLifecycle import org.harvestcircle.application.ShellSessionState import org.harvestcircle.application.SnapshotRevision +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEdge +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeEnvironment +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeExclusion import org.harvestcircle.identities.ui.HarvestCirclePlatformActions import org.harvestcircle.identities.ui.HarvestCircleUiActions import java.awt.image.BufferedImage @@ -70,13 +73,15 @@ private fun captureLiveTodayShell(theme: ThemePreference): BufferedImage { lateinit var captured: ImageBitmap runComposeUiTest { setContent { - Box(Modifier.size(width = 1280.dp, height = 800.dp).testTag("golden-surface")) { - HarvestCircleShell( - state = liveTodayState(theme), - identityActions = HarvestCircleUiActions(), - platformActions = HarvestCirclePlatformActions(), - dispatch = {}, - ) + HarvestCircleWindowChromeEnvironment(macOsWindowChromeExclusion()) { + Box(Modifier.size(width = 1280.dp, height = 800.dp).testTag("golden-surface")) { + HarvestCircleShell( + state = liveTodayState(theme), + identityActions = HarvestCircleUiActions(), + platformActions = HarvestCirclePlatformActions(), + dispatch = {}, + ) + } } } waitForIdle() @@ -108,6 +113,13 @@ private fun liveTodayState(theme: ThemePreference): HarvestCircleShellState = appearance = AppearanceState(theme = theme), ) +private fun macOsWindowChromeExclusion(): HarvestCircleWindowChromeExclusion = + HarvestCircleWindowChromeExclusion( + edge = HarvestCircleWindowChromeEdge.Left, + width = 112.dp, + height = 40.dp, + ) + private fun ImageBitmap.toBufferedImage(): BufferedImage { val pixels = toPixelMap() return BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB).also { image -> 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 @@ -1,14 +1,21 @@ package org.harvestcircle.ui.shell +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.unit.dp +import org.harvestcircle.designsystem.layout.HarvestCircleWindowChromeClearance import org.harvestcircle.product.ScreenKey import kotlin.test.Test import kotlin.test.assertEquals @@ -37,4 +44,26 @@ class WorkspaceSidebarTest { ) assertEquals(listOf(ScreenKey.Network, ScreenKey.Settings), selected) } + + @Test + fun hostChromeHidesRedundantBrandingAndKeepsTheCollapseControlSafe() = + runComposeUiTest { + setHarvestCircleContent { + Box(Modifier.requiredSize(232.dp, 720.dp)) { + WorkspaceSidebar( + selected = ScreenKey.PersonalToday, + onScreen = {}, + chromeClearance = + HarvestCircleWindowChromeClearance( + topBandHeight = 48.dp, + left = 112.dp, + right = 0.dp, + ), + ) + } + } + + onAllNodesWithText("HarvestCircle").assertCountEquals(0) + assertTrue(onNodeWithTag("workspace-sidebar-toggle").fetchSemanticsNode().boundsInRoot.left >= 112f) + } } diff --git a/app/shared/src/desktopTest/resources/goldens/macos-aarch64/design-surface-dark.png b/app/shared/src/desktopTest/resources/goldens/macos-aarch64/design-surface-dark.png Binary files differ. diff --git a/app/shared/src/desktopTest/resources/goldens/macos-aarch64/design-surface-light.png b/app/shared/src/desktopTest/resources/goldens/macos-aarch64/design-surface-light.png Binary files differ.