From cea262c38e4e82ab1b27ea156333ce6a7591b6d8 Mon Sep 17 00:00:00 2001 From: todayama_r <13657682+Corvus400@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:10:32 +0900 Subject: [PATCH 1/2] :sparkles: I added a UI to display a message in the event of cancellation. --- .../sessions/TimetableItemDetailScreen.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt index 24995477f..5c32a9c5e 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt @@ -5,15 +5,24 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -25,9 +34,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import conference_app_2024.feature.sessions.generated.resources.image import io.github.droidkaigi.confsched.compose.EventFlow import io.github.droidkaigi.confsched.compose.rememberEventFlow import io.github.droidkaigi.confsched.designsystem.component.LoadingText @@ -42,6 +54,7 @@ import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSnackba import io.github.droidkaigi.confsched.model.Lang import io.github.droidkaigi.confsched.model.TimetableItem import io.github.droidkaigi.confsched.model.TimetableItem.Session +import io.github.droidkaigi.confsched.model.TimetableItem.Special import io.github.droidkaigi.confsched.model.TimetableItemId import io.github.droidkaigi.confsched.model.fake import io.github.droidkaigi.confsched.sessions.TimetableItemDetailScreenUiState.Loaded @@ -52,11 +65,14 @@ import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailHead import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailSummaryCard import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailTopAppBar import io.github.droidkaigi.confsched.sessions.navigation.TimetableItemDetailDestination +import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview const val timetableItemDetailScreenRouteItemIdParameterName = "timetableItemId" const val TimetableItemDetailBookmarkIconTestTag = "TimetableItemDetailBookmarkIconTestTag" const val TimetableItemDetailScreenLazyColumnTestTag = "TimetableItemDetailScreenLazyColumnTestTag" +const val TimetableItemDetailMessageRowTestTag = "TimetableItemDetailMessageRowTestTag" +const val TimetableItemDetailMessageRowTextTestTag = "TimetableItemDetailMessageRowTextTestTag" fun NavGraphBuilder.sessionScreens( onNavigationIconClick: () -> Unit, @@ -233,6 +249,41 @@ private fun TimetableItemDetailScreen( ) } + when (uiState.timetableItem) { + is Session -> uiState.timetableItem.message + is Special -> uiState.timetableItem.message + }?.let { + item { + Row( + modifier = Modifier + .padding( + start = 8.dp, + top = 24.dp, + end = 8.dp, + bottom = 4.dp, + ) + .height(IntrinsicSize.Min) + .testTag(TimetableItemDetailMessageRowTestTag), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + modifier = Modifier.fillMaxHeight(), + imageVector = Icons.Filled.Info, + contentDescription = stringResource(SessionsRes.string.image), + tint = MaterialTheme.colorScheme.error, + ) + Text( + modifier = Modifier.testTag( + TimetableItemDetailMessageRowTextTestTag, + ), + text = it.currentLangTitle, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.error, + ) + } + } + } + item { TimetableItemDetailSummaryCard( timetableItem = uiState.timetableItem, From 30a945fbbd09960888a1e785e5fba95285ff5e21 Mon Sep 17 00:00:00 2001 From: todayama_r <13657682+Corvus400@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:11:32 +0900 Subject: [PATCH 2/2] :recycle: We have added test code to test the UI. --- .../data/sessions/FakeSessionsApiClient.kt | 23 +++++++++++++++++-- .../confsched/testing/robot/MiniRobots.kt | 3 +++ .../robot/TimetableItemDetailScreenRobot.kt | 23 +++++++++++++++++++ .../sessions/TimetableItemDetailScreenTest.kt | 16 +++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/sessions/FakeSessionsApiClient.kt b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/sessions/FakeSessionsApiClient.kt index 0ff982ede..9cab2803a 100644 --- a/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/sessions/FakeSessionsApiClient.kt +++ b/core/data/src/commonMain/kotlin/io/github/droidkaigi/confsched/data/sessions/FakeSessionsApiClient.kt @@ -6,6 +6,7 @@ import io.github.droidkaigi.confsched.data.sessions.response.CategoryResponse import io.github.droidkaigi.confsched.data.sessions.response.LocaledResponse import io.github.droidkaigi.confsched.data.sessions.response.RoomResponse import io.github.droidkaigi.confsched.data.sessions.response.SessionAssetResponse +import io.github.droidkaigi.confsched.data.sessions.response.SessionMessageResponse import io.github.droidkaigi.confsched.data.sessions.response.SessionResponse import io.github.droidkaigi.confsched.data.sessions.response.SessionsAllResponse import io.github.droidkaigi.confsched.data.sessions.response.SpeakerResponse @@ -48,6 +49,12 @@ public class FakeSessionsApiClient : SessionsApiClient { } } + public data object OperationalMessageExists : Status() { + override suspend fun sessionsAllResponse(): SessionsAllResponse { + return SessionsAllResponse.messageExistsFake() + } + } + public data object Error : Status() { override suspend fun sessionsAllResponse(): SessionsAllResponse { throw IOException("Fake IO Exception") @@ -126,6 +133,17 @@ public fun SessionsAllResponse.Companion.onlyVideoAssetAvailableFake(): Sessions ), ) +public fun SessionsAllResponse.Companion.messageExistsFake(): SessionsAllResponse = SessionsAllResponse.fake( + sessions = SessionResponse.fakes( + message = SessionMessageResponse.fake(), + ), +) + +public fun SessionMessageResponse.Companion.fake(): SessionMessageResponse = SessionMessageResponse( + ja = "このセッションは中止になりました", + en = "This session has been canceled.", +) + private fun RoomResponse.Companion.fakes(): List = listOf( RoomResponse(name = LocaledResponse(ja = "Hedgehog ja", en = "Hedgehog"), id = 1, sort = 1), RoomResponse( @@ -180,6 +198,7 @@ private fun SessionResponse.Companion.fakes( rooms: List = RoomResponse.fakes(), categories: List = CategoryResponse.fakes(), asset: SessionAssetResponse = SessionAssetResponse.fake(), + message: SessionMessageResponse? = null, ): List { val sessions = mutableListOf() @@ -204,7 +223,7 @@ private fun SessionResponse.Companion.fakes( sessionCategoryItemId = 3, interpretationTarget = false, asset = asset, - message = null, + message = message, sessionType = "WELCOME_TALK", levels = listOf("UNSPECIFIED"), ), @@ -266,7 +285,7 @@ private fun SessionResponse.Companion.fakes( roomId = room.id, sessionCategoryItemId = sessionCategoryItemId, sessionType = "NORMAL", - message = null, + message = message, isPlenumSession = false, targetAudience = "For App developer アプリ開発者向け", interpretationTarget = false, diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/MiniRobots.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/MiniRobots.kt index 3e999aff2..9701e196e 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/MiniRobots.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/MiniRobots.kt @@ -43,6 +43,7 @@ import io.github.droidkaigi.confsched.testing.robot.TimetableItemCardRobot.Langu import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.Error import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.Operational import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.OperationalBothAssetAvailable +import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.OperationalMessageExists import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.OperationalOnlySlideAssetAvailable import io.github.droidkaigi.confsched.testing.robot.TimetableServerRobot.ServerStatus.OperationalOnlyVideoAssetAvailable import io.github.droidkaigi.confsched.testing.rules.RobotTestRule @@ -333,6 +334,7 @@ interface TimetableServerRobot { OperationalBothAssetAvailable, OperationalOnlySlideAssetAvailable, OperationalOnlyVideoAssetAvailable, + OperationalMessageExists, Error, } @@ -349,6 +351,7 @@ class DefaultTimetableServerRobot @Inject constructor(sessionsApiClient: Session OperationalBothAssetAvailable -> Status.OperationalBothAssetAvailable OperationalOnlySlideAssetAvailable -> Status.OperationalOnlySlideAssetAvailable OperationalOnlyVideoAssetAvailable -> Status.OperationalOnlyVideoAssetAvailable + OperationalMessageExists -> Status.OperationalMessageExists Error -> Status.Error }, ) diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableItemDetailScreenRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableItemDetailScreenRobot.kt index c811565cd..aee74ce7b 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableItemDetailScreenRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableItemDetailScreenRobot.kt @@ -22,6 +22,8 @@ import com.github.takahirom.roborazzi.captureRoboImage import io.github.droidkaigi.confsched.data.sessions.FakeSessionsApiClient import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme import io.github.droidkaigi.confsched.sessions.TimetableItemDetailBookmarkIconTestTag +import io.github.droidkaigi.confsched.sessions.TimetableItemDetailMessageRowTestTag +import io.github.droidkaigi.confsched.sessions.TimetableItemDetailMessageRowTextTestTag import io.github.droidkaigi.confsched.sessions.TimetableItemDetailScreen import io.github.droidkaigi.confsched.sessions.TimetableItemDetailScreenLazyColumnTestTag import io.github.droidkaigi.confsched.sessions.component.DescriptionMoreButtonTestTag @@ -118,6 +120,18 @@ class TimetableItemDetailScreenRobot @Inject constructor( .performScrollToNode(hasTestTag(TimetableItemDetailContentArchiveSectionTestTag)) } + fun scrollToMessageRow() { + composeTestRule + .onNode(hasTestTag(TimetableItemDetailScreenLazyColumnTestTag)) + .performScrollToNode(hasTestTag(TimetableItemDetailMessageRowTestTag)) + + // FIXME Without this, you won't be able to scroll to the exact middle of the message section. + composeTestRule.onRoot().performTouchInput { + swipeUp(startY = centerY, endY = centerY - 175) + } + waitUntilIdle() + } + fun checkScreenCapture() { composeTestRule .onNode(isRoot()) @@ -248,6 +262,15 @@ class TimetableItemDetailScreenRobot @Inject constructor( .assertDoesNotExist() } + fun checkMessageDisplayed() { + composeTestRule + .onAllNodes(hasTestTag(TimetableItemDetailMessageRowTextTestTag)) + .onFirst() + .assertExists() + .assertIsDisplayed() + .assertTextEquals("This session has been canceled.") + } + companion object { val defaultSessionId = FakeSessionsApiClient.defaultSession.id } diff --git a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreenTest.kt b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreenTest.kt index a4dc566c3..5dfe28cbc 100644 --- a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreenTest.kt +++ b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreenTest.kt @@ -214,6 +214,22 @@ class TimetableItemDetailScreenTest(private val testCase: DescribedBehavior