diff --git a/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt b/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt index 58d811e2..6054755f 100644 --- a/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt +++ b/desktop/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pingh.kt @@ -28,7 +28,7 @@ package io.spine.internal.dependency // https://github.com/spine-examples/Pingh public object Pingh { - private const val version = "1.0.0-SNAPSHOT.15" + private const val version = "1.0.0-SNAPSHOT.16" private const val group = "io.spine.examples.pingh" public const val client: String = "$group:client:$version" diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt index 21cac06f..bdf21e76 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Login.kt @@ -68,6 +68,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag @@ -139,6 +142,10 @@ private fun UsernameEnteringPage( var username by remember { mutableStateOf("") } var wasChanged by remember { mutableStateOf(false) } val isError = remember { mutableStateOf(false) } + val requestUserCode = { + val name = Username::class.of(username) + flow.requestUserCode(name) + } Column( modifier = Modifier .fillMaxSize() @@ -154,15 +161,18 @@ private fun UsernameEnteringPage( username = value wasChanged = true }, + onEnterPressed = { + if (wasChanged && !isError.value) { + requestUserCode() + } + }, isError = isError ) - Spacer(Modifier.height(10.dp)) + Spacer(Modifier.height(15.dp)) LoginButton( - enabled = wasChanged && !isError.value - ) { - val name = Username::class.of(username) - flow.requestUserCode(name) - } + enabled = wasChanged && !isError.value, + onClick = requestUserCode + ) } } @@ -184,7 +194,7 @@ private fun ApplicationInfo() { Icon( painter = Icons.pingh, contentDescription = null, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(50.dp), tint = MaterialTheme.colorScheme.onSecondary ) Spacer(Modifier.width(10.dp)) @@ -196,11 +206,11 @@ private fun ApplicationInfo() { style = MaterialTheme.typography.displayLarge ) } - Spacer(Modifier.height(10.dp)) + Spacer(Modifier.height(20.dp)) Text( text = "Pingh is a GitHub app that looks up mentions on behalf of the user. " + "It requires authentication via GitHub.", - modifier = Modifier.width(180.dp), + modifier = Modifier.width(240.dp), color = MaterialTheme.colorScheme.onSecondaryContainer, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium @@ -213,12 +223,14 @@ private fun ApplicationInfo() { * * @param value The current value of the input field. * @param onChange Called when input value is changed. + * @param onEnterPressed Called when this input is focused and the "Enter" key is pressed. * @param isError Indicates if the input's current value is in error. */ @Composable private fun UsernameInput( value: String, onChange: (String) -> Unit, + onEnterPressed: () -> Unit, isError: MutableState ) { val interactionSource = remember { MutableInteractionSource() } @@ -236,8 +248,16 @@ private fun UsernameInput( isError.value = !isValidUsername(changedValue) }, modifier = Modifier - .width(180.dp) - .height(52.dp) + .width(240.dp) + .height(57.dp) + .onKeyEvent { event -> + if (event.key == Key.Enter) { + onEnterPressed() + true + } else { + false + } + } .testTag("username-input"), textStyle = MaterialTheme.typography.bodyLarge.copy( color = MaterialTheme.colorScheme.onSecondary @@ -274,8 +294,8 @@ private fun InputContainer( ) { Row( modifier = Modifier - .width(180.dp) - .height(40.dp) + .width(240.dp) + .height(45.dp) .border(border = border, shape = MaterialTheme.shapes.medium) .background( color = MaterialTheme.colorScheme.secondary, @@ -305,9 +325,9 @@ private fun InputContainer( private fun Label(color: Color) { Box( modifier = Modifier - .width(90.dp) - .height(10.dp) - .absoluteOffset(x = 10.dp, y = (-5).dp) + .width(110.dp) + .height(12.dp) + .absoluteOffset(x = 12.dp, y = (-6).dp) ) { Text( text = "GitHub username", @@ -350,7 +370,7 @@ private fun ErrorMessage(isShown: Boolean) { modifier = Modifier .width(155.dp) .height(30.dp) - .absoluteOffset(x = 15.dp, y = 44.dp), + .absoluteOffset(x = 15.dp, y = 49.dp), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall ) @@ -372,10 +392,11 @@ private fun LoginButton( Button( onClick = onClick, modifier = Modifier - .width(180.dp) - .height(40.dp) + .width(240.dp) + .height(45.dp) .testTag("login-button"), enabled = enabled, + shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary @@ -420,12 +441,12 @@ private fun VerificationPage( horizontalAlignment = Alignment.CenterHorizontally ) { VerificationTitle() - Spacer(Modifier.height(15.dp)) + Spacer(Modifier.height(25.dp)) UserCodeField( userCode = userCode, isExpired = isUserCodeExpired ) - Spacer(Modifier.height(10.dp)) + Spacer(Modifier.height(25.dp)) if (isUserCodeExpired) { Spacer(Modifier.height(5.dp)) CodeExpiredErrorMessage(flow) @@ -434,7 +455,7 @@ private fun VerificationPage( verificationUrl = verificationUrl, expiresIn = expiresIn ) - Spacer(Modifier.height(20.dp)) + Spacer(Modifier.height(25.dp)) SubmitButton( flow = flow, toMentionsPage = toMentionsPage @@ -450,7 +471,7 @@ private fun VerificationPage( private fun VerificationTitle() { Text( text = "Verify your login", - fontSize = 18.sp, + fontSize = 20.sp, style = MaterialTheme.typography.displayLarge ) } @@ -471,15 +492,32 @@ private fun UserCodeField( } else { MaterialTheme.colorScheme.onSecondary } - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center + Row( + modifier = Modifier + .width(260.dp) + .height(46.dp) + .run { + if (isExpired) { + this + } else { + border( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground, + shape = MaterialTheme.shapes.medium + ) + } + } + .padding(horizontal = 15.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { SelectionContainer { Text( text = userCode.value, + modifier = Modifier.width(200.dp), color = color, - fontSize = 28.sp, + fontSize = 30.sp, + textAlign = if (isExpired) TextAlign.Center else TextAlign.Start, letterSpacing = 3.sp, style = MaterialTheme.typography.displayLarge ) @@ -500,20 +538,16 @@ private fun CopyToClipboardIcon( userCode: UserCode ) { val clipboardManager = LocalClipboardManager.current - Box( - modifier = Modifier.offset(x = 103.dp) - ) { - IconButton( - icon = Icons.copy, - onClick = { - clipboardManager.setText(AnnotatedString(userCode.value)) - }, - modifier = Modifier.size(30.dp), - colors = IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) + IconButton( + icon = Icons.copy, + onClick = { + clipboardManager.setText(AnnotatedString(userCode.value)) + }, + modifier = Modifier.size(30.dp), + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.onSecondaryContainer ) - } + ) } /** @@ -543,17 +577,16 @@ private fun VerificationText( expiresIn: Duration ) { Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.width(260.dp) ) { Text( text = "Enter this code at", color = MaterialTheme.colorScheme.onSecondaryContainer, style = MaterialTheme.typography.bodyLarge ) - Spacer(Modifier.height(3.dp)) + Spacer(Modifier.height(5.dp)) VerificationUrlButton(verificationUrl) - Spacer(Modifier.height(3.dp)) + Spacer(Modifier.height(12.dp)) Text( text = "The code is valid for ${toMinutes(expiresIn)} minutes.", color = MaterialTheme.colorScheme.onSecondaryContainer, @@ -615,8 +648,8 @@ private fun SubmitButton( } Box( modifier = Modifier - .width(210.dp) - .height(32.dp), + .width(260.dp) + .height(46.dp), contentAlignment = Alignment.TopCenter ) { Button( @@ -624,6 +657,7 @@ private fun SubmitButton( modifier = Modifier.fillMaxSize() .testTag("submit-button"), enabled = enabled, + shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary @@ -631,7 +665,7 @@ private fun SubmitButton( ) { Text( text = "I have entered the code", - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displaySmall ) } if (!enabled) { @@ -659,8 +693,8 @@ private fun NoResponseErrorMessage(flow: VerifyLogin) { clickablePartOfText = "start over", onClick = flow::requestNewUserCode, modifier = Modifier - .width(180.dp) - .offset(y = 40.dp) + .width(240.dp) + .offset(y = 50.dp) .testTag("no-response-message") ) } @@ -737,11 +771,11 @@ private fun FailedPage(flow: LoginFailed) { ) { Text( text = flow.errorMessage.value, - modifier = Modifier.width(210.dp), + modifier = Modifier.width(240.dp), textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyLarge ) - Spacer(Modifier.height(20.dp)) + Spacer(Modifier.height(25.dp)) RestartButton(flow) } } @@ -756,8 +790,9 @@ private fun RestartButton(flow: LoginFailed) { Button( onClick = flow::restartLogin, modifier = Modifier - .width(210.dp) - .height(32.dp), + .width(240.dp) + .height(40.dp), + shape = MaterialTheme.shapes.medium, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt index 519ae843..1d9177c3 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Mentions.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import io.spine.examples.pingh.client.MentionsFlow import io.spine.examples.pingh.client.howMuchTimeHasPassed import io.spine.examples.pingh.client.sorted @@ -105,33 +106,36 @@ private fun ToolBar( toSettingsPage: () -> Unit ) { val contentColor = MaterialTheme.colorScheme.onSecondary + val borderColor = MaterialTheme.colorScheme.onBackground Row( modifier = Modifier .fillMaxWidth() - .height(48.dp) + .height(72.dp) .background(MaterialTheme.colorScheme.secondary) .drawBehind { drawLine( - color = contentColor, + color = borderColor, start = Offset(0f, size.height), end = Offset(size.width, size.height), strokeWidth = 1.dp.toPx() ) } - .padding(horizontal = 5.dp, vertical = 4.dp), + .padding(start = 27.dp), verticalAlignment = Alignment.CenterVertically ) { IconButton( icon = Icons.pingh, onClick = toSettingsPage, - modifier = Modifier.size(40.dp).testTag("settings-button"), + modifier = Modifier.size(56.dp).testTag("settings-button"), colors = IconButtonDefaults.iconButtonColors( contentColor = contentColor ) ) + Spacer(Modifier.width(7.dp)) Text( text = "Recent mentions", - modifier = Modifier.width(140.dp), + modifier = Modifier.width(250.dp), + fontSize = 20.sp, color = contentColor, style = MaterialTheme.typography.displayLarge ) @@ -140,7 +144,7 @@ private fun ToolBar( onClick = { flow.updateMentions() }, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(50.dp), colors = IconButtonDefaults.iconButtonColors( contentColor = contentColor ) @@ -165,17 +169,17 @@ private fun MentionCards( Column( Modifier .fillMaxSize() - .padding(horizontal = 5.dp) + .padding(horizontal = 10.dp) .verticalScroll(scrollState) .background(MaterialTheme.colorScheme.background) .testTag("mention-cards"), ) { mentions.sorted() .forEach { mention -> - Spacer(Modifier.height(5.dp)) + Spacer(Modifier.height(10.dp)) MentionCard(flow, mention) } - Spacer(Modifier.height(5.dp)) + Spacer(Modifier.height(10.dp)) } } @@ -216,7 +220,7 @@ private fun MentionCard( modifier = Modifier .fillMaxWidth() .testTag("mention-card-${mention.id}") - .height(50.dp), + .height(60.dp), interactionSource = interactionSource, colors = CardDefaults.elevatedCardColors( containerColor = containerColor, @@ -225,17 +229,17 @@ private fun MentionCard( ) { Row( modifier = Modifier - .padding(vertical = 3.dp, horizontal = 10.dp), + .padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically ) { Avatar( url = mention.whoMentioned.avatarUrl, - modifier = Modifier.size(40.dp) + modifier = Modifier.size(50.dp) ) - Spacer(Modifier.width(5.dp)) + Spacer(Modifier.width(10.dp)) MentionCardText(mention, isHovered) - Spacer(Modifier.width(5.dp)) - SnoozeButton(flow, mention) + Spacer(Modifier.width(10.dp)) + SnoozeButton(flow, mention, isHovered.value) } } } @@ -255,10 +259,15 @@ private fun MentionCardText( val time = mention.whenMentioned.run { if (isHovered.value) toDatetime() else howMuchTimeHasPassed() } + val textWidth = if (isHovered.value || mention.status == MentionStatus.SNOOZED) { + 240.dp + } else { + 300.dp + } Column( modifier = Modifier .fillMaxHeight() - .width(120.dp), + .width(textWidth), verticalArrangement = Arrangement.Center ) { Text( @@ -267,7 +276,7 @@ private fun MentionCardText( maxLines = 1, style = MaterialTheme.typography.bodyLarge ) - Spacer(Modifier.height(2.dp)) + Spacer(Modifier.height(4.dp)) Text( text = "$time, by ${mention.whoMentioned.username.value}", overflow = TextOverflow.Ellipsis, @@ -285,33 +294,33 @@ private fun MentionCardText( * * @param flow The flow for managing the lifecycle of mentions. * @param mention The mention whose information is displayed. + * @param isParentHovered Whether the parent mention card is being hovered. */ @Composable private fun SnoozeButton( flow: MentionsFlow, - mention: MentionView + mention: MentionView, + isParentHovered: Boolean ) { - when (mention.status) { - MentionStatus.UNREAD -> + when { + isParentHovered && mention.status == MentionStatus.UNREAD -> IconButton( icon = Icons.snooze, onClick = { flow.snooze(mention.id) }, - modifier = Modifier.size(40.dp).testTag("snooze-button"), + modifier = Modifier.size(50.dp).testTag("snooze-button"), colors = IconButtonDefaults.iconButtonColors( contentColor = MaterialTheme.colorScheme.onSecondary ) ) - MentionStatus.SNOOZED -> + mention.status == MentionStatus.SNOOZED -> Text( text = "Snoozed", - modifier = Modifier.size(40.dp) + modifier = Modifier.size(50.dp) .wrapContentSize(Alignment.Center), style = MaterialTheme.typography.bodySmall ) - - else -> {} } } diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt index 01ff2859..104b6b7a 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Settings.kt @@ -62,6 +62,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -118,14 +119,14 @@ private fun SettingsHeader( Row( modifier = Modifier .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 5.dp, vertical = 4.dp), + .height(60.dp) + .padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically ) { IconButton( icon = Icons.back, onClick = toMentionsPage, - modifier = Modifier.size(30.dp), + modifier = Modifier.size(35.dp), colors = IconButtonDefaults.iconButtonColors( contentColor = MaterialTheme.colorScheme.onSecondary ) @@ -153,7 +154,7 @@ private fun SettingsBox( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) - .padding(5.dp), + .padding(10.dp), ) { Card( modifier = Modifier.fillMaxSize(), @@ -165,8 +166,8 @@ private fun SettingsBox( Column( modifier = Modifier .fillMaxSize() - .padding(vertical = 5.dp), - verticalArrangement = Arrangement.spacedBy(15.dp), + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(25.dp), content = content ) } @@ -189,7 +190,7 @@ private fun Profile( modifier = Modifier .fillMaxWidth() .height(60.dp) - .padding(horizontal = 10.dp, vertical = 4.dp), + .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Avatar( @@ -246,7 +247,7 @@ private fun LogOutButton( ) { OutlinedButton( onClick = onClick, - modifier = Modifier.height(20.dp).testTag("logout-button"), + modifier = Modifier.height(22.dp).testTag("logout-button"), colors = ButtonDefaults.outlinedButtonColors( containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary @@ -272,7 +273,7 @@ private fun SnoozeTimeOption(state: SettingsState) { Option( title = "Snooze time", description = "Time after which the notification is repeated.", - titleWight = 68.dp + titleWight = 150.dp ) { SnoozeTimeSegmentedButtonRow(state) } @@ -293,7 +294,7 @@ private fun DndOption( Option( title = "Do not disturb", description = "Turn off notifications for new mentions or snooze expirations.", - titleWight = 174.dp + titleWight = 324.dp ) { Switch( checked = enabledDndMode, @@ -333,9 +334,7 @@ private fun Option( control: @Composable () -> Unit ) { Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp) + Modifier.fillMaxWidth() ) { Row( modifier = Modifier.fillMaxWidth(), @@ -344,14 +343,14 @@ private fun Option( Text( text = title, modifier = Modifier.width(titleWight), - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyLarge ) control() } - Spacer(Modifier.height(5.dp)) + Spacer(Modifier.height(10.dp)) Text( text = description, - modifier = Modifier.width(170.dp), + modifier = Modifier.width(360.dp), color = MaterialTheme.colorScheme.secondaryContainer, style = MaterialTheme.typography.bodySmall ) @@ -423,7 +422,7 @@ private fun SegmentedButton( selected = selected, onClick = onClick, modifier = Modifier - .width(48.dp) + .width(70.dp) .height(20.dp) .semantics { role = Role.RadioButton }, shape = shape, diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Theme.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Theme.kt index 14f88237..2b3126c9 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Theme.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Theme.kt @@ -104,26 +104,26 @@ private val sanFrancisco = FontFamily( private val typography = Typography( displayLarge = TextStyle( fontFamily = sanFrancisco, - fontSize = 16.sp + fontSize = 18.sp ), displayMedium = TextStyle( fontFamily = sanFrancisco, - fontSize = 14.sp + fontSize = 16.sp ), displaySmall = TextStyle( fontFamily = sanFrancisco, - fontSize = 12.sp + fontSize = 14.sp ), bodyLarge = TextStyle( fontFamily = sanFrancisco, - fontSize = 12.sp + fontSize = 14.sp ), bodyMedium = TextStyle( fontFamily = sanFrancisco, - fontSize = 10.sp + fontSize = 12.sp ), bodySmall = TextStyle( fontFamily = sanFrancisco, - fontSize = 8.sp + fontSize = 10.sp ) ) diff --git a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt index 682f4234..5ab00ed2 100644 --- a/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt +++ b/desktop/src/main/kotlin/io/spine/examples/pingh/desktop/Window.kt @@ -26,16 +26,21 @@ package io.spine.examples.pingh.desktop +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.unit.dp @@ -88,8 +93,8 @@ private fun PlatformWindow( content: @Composable FrameWindowScope.() -> Unit ) { val windowState = rememberWindowState( - width = 240.dp, - height = 426.dp, + width = 460.dp, + height = 740.dp, position = WindowPosition(1200.dp, 30.dp) ) ComposeWindow( @@ -111,11 +116,26 @@ private fun PlatformWindow( @Composable private fun WindowContent(app: PinghApplication) { Box( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.small) + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter ) { - CurrentPage(app) + Box( + modifier = Modifier + .width(420.dp) + .height(700.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onBackground, + shape = MaterialTheme.shapes.small + ) + .shadow( + elevation = 10.dp, + shape = MaterialTheme.shapes.small + ) + .clip(MaterialTheme.shapes.small) + ) { + CurrentPage(app) + } } } diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt index 355c4f67..e782f423 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/MentionsPageUiTest.kt @@ -42,6 +42,7 @@ import io.kotest.matchers.floats.shouldBeLessThan import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual import io.kotest.matchers.shouldBe import io.spine.examples.pingh.desktop.given.DelayedFactAssertion.Companion.awaitFact +import io.spine.examples.pingh.desktop.given.performHover import io.spine.examples.pingh.desktop.given.testTag import kotlin.test.Test import org.junit.jupiter.api.DisplayName @@ -70,8 +71,13 @@ internal class MentionsPageUiTest : UiTest() { logIn() awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val tag = mentionCards().random().testTag + onNodeWithTag(tag).performHover() + awaitFact { onSnoozeButtonWithParentTag(tag).assertExists() } onSnoozeButtonWithParentTag(tag).performClick() - awaitFact { onSnoozeButtonWithParentTag(tag).assertDoesNotExist() } + awaitFact { + onNodeWithTag(tag).performHover() + onSnoozeButtonWithParentTag(tag).assertDoesNotExist() + } } @Test @@ -82,7 +88,10 @@ internal class MentionsPageUiTest : UiTest() { awaitFact { mentionCards().size shouldBeGreaterThanOrEqual 1 } val tag = mentionCards().random().testTag onNodeWithTag(tag).performClick() - awaitFact { onSnoozeButtonWithParentTag(tag).assertDoesNotExist() } + awaitFact { + onNodeWithTag(tag).performHover() + onSnoozeButtonWithParentTag(tag).assertDoesNotExist() + } } @Test @@ -94,8 +103,10 @@ internal class MentionsPageUiTest : UiTest() { val mentionsCards = mentionCards().sortedBy { it.positionInRoot.y } val readMentionTag = mentionsCards[0].testTag val snoozedMentionTag = mentionsCards[1].testTag - onNodeWithTag(readMentionTag).performClick() + onNodeWithTag(snoozedMentionTag).performHover() + awaitFact { onSnoozeButtonWithParentTag(snoozedMentionTag).assertExists() } onSnoozeButtonWithParentTag(snoozedMentionTag).performClick() + onNodeWithTag(readMentionTag).performClick() awaitFact { val mentions = mentionCards() val readMention = mentions.first { it.testTag == readMentionTag } diff --git a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt index 13effe40..e324bc7b 100644 --- a/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt +++ b/desktop/src/test/kotlin/io/spine/examples/pingh/desktop/given/UiTestEnv.kt @@ -26,8 +26,12 @@ package io.spine.examples.pingh.desktop.given +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.performMouseInput import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly import java.util.concurrent.TimeUnit import kotlin.time.Duration @@ -46,3 +50,13 @@ internal val SemanticsNode.testTag: String get() = config.getOrElse(SemanticsProperties.TestTag) { throw IllegalStateException("This node does not have a `TestTag` specified.") } + +/** + * Hovers the mouse pointer over the center of the element to trigger a hover event. + */ +@OptIn(ExperimentalTestApi::class) +internal fun SemanticsNodeInteraction.performHover() { + val size = this.fetchSemanticsNode().size + val middle = Offset(size.width / 2f, size.height / 2f) + this.performMouseInput { this.moveTo(middle) } +} diff --git a/version.gradle.kts b/version.gradle.kts index b1aa5587..ec80871d 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of the `Pingh` to publish. */ -val pinghVersion: String by extra("1.0.0-SNAPSHOT.15") +val pinghVersion: String by extra("1.0.0-SNAPSHOT.16")