Adapt HealthChat to upstream v26.10.0

Follow-ups needed after rebasing the HealthChat series from v26.07.0 onto
v26.10.0 that span several of the replayed commits:

- Rename the five PreviewParameterProvider subclasses in features/healthchat
  to the *PreviewParam suffix now enforced by Konsist.
- Rename MemberDirectoryEvents and HealthChatProfileSectionEvents to the
  *Event suffix used by every upstream sealed interface.
- Import FakeStartDMAction from features/startchat/test, where it lives now.
- ListItem lost its headlineContent parameter in favour of content.
- HomeView gained onAddAccountClick and onSwitchToAccount, and onRoomClick
  now carries an EventId; update HomeViewBackHandlingTest accordingly.
- Restore upstream's codecov-action pin (v7.1.1) that the CI squash carried
  back to v7.0.0.
- Gate the new pr-checks.yml on the upstream repository: this fork does not
  use the PR- label convention.
- Add the 26.10.0 section to HEALTHCHAT_CHANGES.md.
This commit is contained in:
Sedat Qaja
2026-10-19 09:05:35 +02:00
parent 9cb72c74aa
commit db839dc5c3
25 changed files with 91 additions and 73 deletions
+2
View File
@@ -10,6 +10,8 @@ permissions: {}
jobs: jobs:
pr-checks: pr-checks:
# Upstream-only: this fork does not use the PR- label convention.
if: github.repository == 'element-hq/element-x-android'
name: PR Checks name: PR Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
+1 -1
View File
@@ -152,7 +152,7 @@ jobs:
if: env.CODECOV_TOKEN != '' if: env.CODECOV_TOKEN != ''
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+12
View File
@@ -6,6 +6,18 @@ top section matching the released version — the heading must have the exact
form `## Changes in <version> (<date>)` and the whole section must stay under form `## Changes in <version> (<date>)` and the whole section must stay under
500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`. 500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`.
## Changes in 26.10.0 (2026-10-19)
🎉 Neue Funktionen
- Nachrichtensuche: Chats durchsuchen, auch in verschlüsselten Räumen.
- Umfragen mit Mehrfachauswahl.
🙌 Verbesserungen
- Basis aktualisiert auf Element X 26.10.0 mit zahlreichen Fehlerbehebungen.
- Einstellungen neu gegliedert in Konto- und App-Einstellungen.
## Changes in 26.08.0 (2026-08-06) ## Changes in 26.08.0 (2026-08-06)
🎉 Neue Funktionen 🎉 Neue Funktionen
@@ -10,17 +10,17 @@ package io.element.android.features.healthchat.api.directory
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface MemberDirectoryEvents { sealed interface MemberDirectoryEvent {
/** /**
* Fired by the view on first composition. The tab presenter runs from app start (it is * Fired by the view on first composition. The tab presenter runs from app start (it is
* composed by the Home presenter), so the first fetch is deferred until the tab is shown. * composed by the Home presenter), so the first fetch is deferred until the tab is shown.
*/ */
data object LoadIfNeeded : MemberDirectoryEvents data object LoadIfNeeded : MemberDirectoryEvent
/** Header refresh and "try again" — one load path. */ /** Header refresh and "try again" — one load path. */
data object Refresh : MemberDirectoryEvents data object Refresh : MemberDirectoryEvent
data class StartDm(val matrixUser: MatrixUser) : MemberDirectoryEvents data class StartDm(val matrixUser: MatrixUser) : MemberDirectoryEvent
data object ClearStartDmState : MemberDirectoryEvents data object ClearStartDmState : MemberDirectoryEvent
} }
@@ -18,7 +18,7 @@ data class MemberDirectoryState(
val searchQuery: TextFieldState, val searchQuery: TextFieldState,
val directory: AsyncData<HealthChatMemberDirectory>, val directory: AsyncData<HealthChatMemberDirectory>,
val startDmActionState: AsyncAction<RoomId>, val startDmActionState: AsyncAction<RoomId>,
val eventSink: (MemberDirectoryEvents) -> Unit, val eventSink: (MemberDirectoryEvent) -> Unit,
) { ) {
/** Unknown-while-loading counts as approved so no banner flashes during the first load. */ /** Unknown-while-loading counts as approved so no banner flashes during the first load. */
val isApproved: Boolean val isApproved: Boolean
@@ -25,7 +25,7 @@ fun aMemberDirectoryState(
searchQuery: TextFieldState = TextFieldState(), searchQuery: TextFieldState = TextFieldState(),
directory: AsyncData<HealthChatMemberDirectory> = AsyncData.Success(aMemberDirectory()), directory: AsyncData<HealthChatMemberDirectory> = AsyncData.Success(aMemberDirectory()),
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized, startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (MemberDirectoryEvents) -> Unit = {}, eventSink: (MemberDirectoryEvent) -> Unit = {},
) = MemberDirectoryState( ) = MemberDirectoryState(
searchQuery = searchQuery, searchQuery = searchQuery,
directory = directory, directory = directory,
@@ -21,7 +21,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.healthchat.api.HealthChatDirectoryService import io.element.android.features.healthchat.api.HealthChatDirectoryService
import io.element.android.features.healthchat.api.HealthChatMemberDirectory import io.element.android.features.healthchat.api.HealthChatMemberDirectory
import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents import io.element.android.features.healthchat.api.directory.MemberDirectoryEvent
import io.element.android.features.healthchat.api.directory.MemberDirectoryState import io.element.android.features.healthchat.api.directory.MemberDirectoryState
import io.element.android.features.startchat.api.StartDMAction import io.element.android.features.startchat.api.StartDMAction
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@@ -62,22 +62,22 @@ class MemberDirectoryPresenter(
) )
} }
fun handleEvents(event: MemberDirectoryEvents) { fun handleEvents(event: MemberDirectoryEvent) {
when (event) { when (event) {
MemberDirectoryEvents.LoadIfNeeded -> { MemberDirectoryEvent.LoadIfNeeded -> {
hasBeenRequested = true hasBeenRequested = true
} }
MemberDirectoryEvents.Refresh -> { MemberDirectoryEvent.Refresh -> {
loadTrigger++ loadTrigger++
} }
is MemberDirectoryEvents.StartDm -> coroutineScope.launch { is MemberDirectoryEvent.StartDm -> coroutineScope.launch {
startDMAction.execute( startDMAction.execute(
matrixUser = event.matrixUser, matrixUser = event.matrixUser,
createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming, createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming,
actionState = startDmActionState, actionState = startDmActionState,
) )
} }
MemberDirectoryEvents.ClearStartDmState -> { MemberDirectoryEvent.ClearStartDmState -> {
startDmActionState.value = AsyncAction.Uninitialized startDmActionState.value = AsyncAction.Uninitialized
} }
} }
@@ -15,7 +15,7 @@ import io.element.android.features.healthchat.api.directory.aMemberDirectory
import io.element.android.features.healthchat.api.directory.aMemberDirectoryState import io.element.android.features.healthchat.api.directory.aMemberDirectoryState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
open class MemberDirectoryStateProvider : PreviewParameterProvider<MemberDirectoryState> { open class MemberDirectoryStatePreviewParam : PreviewParameterProvider<MemberDirectoryState> {
override val values: Sequence<MemberDirectoryState> override val values: Sequence<MemberDirectoryState>
get() = sequenceOf( get() = sequenceOf(
aMemberDirectoryState(), aMemberDirectoryState(),
@@ -27,7 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.healthchat.api.HealthChatStrings import io.element.android.features.healthchat.api.HealthChatStrings
import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents import io.element.android.features.healthchat.api.directory.MemberDirectoryEvent
import io.element.android.features.healthchat.api.directory.MemberDirectoryState import io.element.android.features.healthchat.api.directory.MemberDirectoryState
import io.element.android.features.healthchat.api.toMatrixUser import io.element.android.features.healthchat.api.toMatrixUser
import io.element.android.features.healthchat.impl.directory.components.MemberCard import io.element.android.features.healthchat.impl.directory.components.MemberCard
@@ -63,7 +63,7 @@ fun MemberDirectoryView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
state.eventSink(MemberDirectoryEvents.LoadIfNeeded) state.eventSink(MemberDirectoryEvent.LoadIfNeeded)
} }
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
when (val directory = state.directory) { when (val directory = state.directory) {
@@ -84,7 +84,7 @@ fun MemberDirectoryView(
} }
is AsyncData.Failure -> MemberDirectoryErrorContent( is AsyncData.Failure -> MemberDirectoryErrorContent(
message = directory.error.healthChatUserMessage(), message = directory.error.healthChatUserMessage(),
onRetry = { state.eventSink(MemberDirectoryEvents.Refresh) }, onRetry = { state.eventSink(MemberDirectoryEvent.Refresh) },
) )
is AsyncData.Success -> MemberDirectoryContent( is AsyncData.Success -> MemberDirectoryContent(
state = state, state = state,
@@ -107,17 +107,17 @@ fun MemberDirectoryView(
onSuccess = onOpenDm, onSuccess = onOpenDm,
errorMessage = { stringResource(HealthChatStrings.healthchat_screen_member_directory_error_starting_chat) }, errorMessage = { stringResource(HealthChatStrings.healthchat_screen_member_directory_error_starting_chat) },
onRetry = null, onRetry = null,
onErrorDismiss = { state.eventSink(MemberDirectoryEvents.ClearStartDmState) }, onErrorDismiss = { state.eventSink(MemberDirectoryEvent.ClearStartDmState) },
confirmationDialog = { data -> confirmationDialog = { data ->
if (data is ConfirmingStartDmWithMatrixUser) { if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet( CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser, matrixUser = data.matrixUser,
isUserIdentityUnknown = data.isUserIdentityUnknown, isUserIdentityUnknown = data.isUserIdentityUnknown,
onSendInvite = { onSendInvite = {
state.eventSink(MemberDirectoryEvents.StartDm(data.matrixUser)) state.eventSink(MemberDirectoryEvent.StartDm(data.matrixUser))
}, },
onDismiss = { onDismiss = {
state.eventSink(MemberDirectoryEvents.ClearStartDmState) state.eventSink(MemberDirectoryEvent.ClearStartDmState)
}, },
) )
} }
@@ -154,7 +154,7 @@ private fun MemberDirectoryContent(
onMyProfileClick = onMyProfileClick, onMyProfileClick = onMyProfileClick,
onEditProfileClick = onEditProfileClick, onEditProfileClick = onEditProfileClick,
onInviteClick = if (state.isApproved) onInviteClick else null, onInviteClick = if (state.isApproved) onInviteClick else null,
onRefreshClick = { state.eventSink(MemberDirectoryEvents.Refresh) }, onRefreshClick = { state.eventSink(MemberDirectoryEvent.Refresh) },
) )
} }
if (!state.isApproved) { if (!state.isApproved) {
@@ -196,7 +196,7 @@ private fun MemberDirectoryContent(
MemberCard( MemberCard(
member = member, member = member,
onClick = onMemberClick?.let { callback -> { callback(member.userId) } }, onClick = onMemberClick?.let { callback -> { callback(member.userId) } },
onStartChatClick = { state.eventSink(MemberDirectoryEvents.StartDm(member.toMatrixUser())) }, onStartChatClick = { state.eventSink(MemberDirectoryEvent.StartDm(member.toMatrixUser())) },
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
) )
} }
@@ -219,7 +219,7 @@ private fun FullScreenLoading(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun MemberDirectoryViewPreview( internal fun MemberDirectoryViewPreview(
@PreviewParameter(MemberDirectoryStateProvider::class) state: MemberDirectoryState @PreviewParameter(MemberDirectoryStatePreviewParam::class) state: MemberDirectoryState
) = ElementPreview { ) = ElementPreview {
MemberDirectoryView( MemberDirectoryView(
state = state, state = state,
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.healthchat.api.HealthChatStrings import io.element.android.features.healthchat.api.HealthChatStrings
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
open class HealthChatInviteStateProvider : PreviewParameterProvider<HealthChatInviteState> { open class HealthChatInviteStatePreviewParam : PreviewParameterProvider<HealthChatInviteState> {
override val values: Sequence<HealthChatInviteState> override val values: Sequence<HealthChatInviteState>
get() = sequenceOf( get() = sequenceOf(
aHealthChatInviteState(), aHealthChatInviteState(),
@@ -136,7 +136,7 @@ private fun HealthChatFieldError.text(): String {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HealthChatInviteViewPreview( internal fun HealthChatInviteViewPreview(
@PreviewParameter(HealthChatInviteStateProvider::class) state: HealthChatInviteState @PreviewParameter(HealthChatInviteStatePreviewParam::class) state: HealthChatInviteState
) = ElementPreview { ) = ElementPreview {
HealthChatInviteView( HealthChatInviteView(
state = state, state = state,
@@ -24,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
open class EditHealthChatProfileStateProvider : PreviewParameterProvider<EditHealthChatProfileState> { open class EditHealthChatProfileStatePreviewParam : PreviewParameterProvider<EditHealthChatProfileState> {
override val values: Sequence<EditHealthChatProfileState> override val values: Sequence<EditHealthChatProfileState>
get() = sequenceOf( get() = sequenceOf(
anEditHealthChatProfileState(), anEditHealthChatProfileState(),
@@ -284,7 +284,7 @@ private fun ProfileFormField(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun EditHealthChatProfileViewPreview( internal fun EditHealthChatProfileViewPreview(
@PreviewParameter(EditHealthChatProfileStateProvider::class) state: EditHealthChatProfileState @PreviewParameter(EditHealthChatProfileStatePreviewParam::class) state: EditHealthChatProfileState
) = ElementPreview { ) = ElementPreview {
EditHealthChatProfileView( EditHealthChatProfileView(
state = state, state = state,
@@ -8,6 +8,6 @@
package io.element.android.features.healthchat.impl.profile.section package io.element.android.features.healthchat.impl.profile.section
sealed interface HealthChatProfileSectionEvents { sealed interface HealthChatProfileSectionEvent {
data object Retry : HealthChatProfileSectionEvents data object Retry : HealthChatProfileSectionEvent
} }
@@ -63,9 +63,9 @@ class HealthChatProfileSectionPresenter(
memberProfile memberProfile
} }
fun handleEvents(event: HealthChatProfileSectionEvents) { fun handleEvents(event: HealthChatProfileSectionEvent) {
when (event) { when (event) {
HealthChatProfileSectionEvents.Retry -> retryTrigger++ HealthChatProfileSectionEvent.Retry -> retryTrigger++
} }
} }
@@ -13,5 +13,5 @@ import io.element.android.libraries.architecture.AsyncData
data class HealthChatProfileSectionState( data class HealthChatProfileSectionState(
val profile: AsyncData<HealthChatMember>, val profile: AsyncData<HealthChatMember>,
val eventSink: (HealthChatProfileSectionEvents) -> Unit, val eventSink: (HealthChatProfileSectionEvent) -> Unit,
) )
@@ -14,7 +14,7 @@ import io.element.android.features.healthchat.api.HealthChatMember
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
open class HealthChatProfileSectionStateProvider : PreviewParameterProvider<HealthChatProfileSectionState> { open class HealthChatProfileSectionStatePreviewParam : PreviewParameterProvider<HealthChatProfileSectionState> {
override val values: Sequence<HealthChatProfileSectionState> override val values: Sequence<HealthChatProfileSectionState>
get() = sequenceOf( get() = sequenceOf(
aHealthChatProfileSectionState(profile = AsyncData.Success(aProfileSectionMember())), aHealthChatProfileSectionState(profile = AsyncData.Success(aProfileSectionMember())),
@@ -45,7 +45,7 @@ open class HealthChatProfileSectionStateProvider : PreviewParameterProvider<Heal
fun aHealthChatProfileSectionState( fun aHealthChatProfileSectionState(
profile: AsyncData<HealthChatMember> = AsyncData.Uninitialized, profile: AsyncData<HealthChatMember> = AsyncData.Uninitialized,
eventSink: (HealthChatProfileSectionEvents) -> Unit = {}, eventSink: (HealthChatProfileSectionEvent) -> Unit = {},
) = HealthChatProfileSectionState( ) = HealthChatProfileSectionState(
profile = profile, profile = profile,
eventSink = eventSink, eventSink = eventSink,
@@ -56,7 +56,7 @@ fun HealthChatProfileSectionView(
is HealthChatApiException.NotFound -> Unit is HealthChatApiException.NotFound -> Unit
is HealthChatApiException.NotApproved -> NotApprovedHint(modifier = modifier) is HealthChatApiException.NotApproved -> NotApprovedHint(modifier = modifier)
else -> LoadingErrorRow( else -> LoadingErrorRow(
onRetry = { state.eventSink(HealthChatProfileSectionEvents.Retry) }, onRetry = { state.eventSink(HealthChatProfileSectionEvent.Retry) },
modifier = modifier, modifier = modifier,
) )
} }
@@ -147,7 +147,7 @@ private fun ProfileFieldRow(
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
ListItem( ListItem(
headlineContent = { Text(value) }, content = { Text(value) },
supportingContent = { Text(stringResource(labelRes)) }, supportingContent = { Text(stringResource(labelRes)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(icon)), leadingContent = ListItemContent.Icon(IconSource.Vector(icon)),
onClick = onClick, onClick = onClick,
@@ -175,7 +175,7 @@ private fun LoadingErrorRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ListItem( ListItem(
headlineContent = { Text(stringResource(HealthChatStrings.healthchat_profile_error_loading)) }, content = { Text(stringResource(HealthChatStrings.healthchat_profile_error_loading)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Error())),
trailingContent = ListItemContent.Custom { trailingContent = ListItemContent.Custom {
TextButton( TextButton(
@@ -206,7 +206,7 @@ private fun String.withHttpScheme(): String {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HealthChatProfileSectionViewPreview( internal fun HealthChatProfileSectionViewPreview(
@PreviewParameter(HealthChatProfileSectionStateProvider::class) state: HealthChatProfileSectionState @PreviewParameter(HealthChatProfileSectionStatePreviewParam::class) state: HealthChatProfileSectionState
) = ElementPreview { ) = ElementPreview {
HealthChatProfileSectionView(state = state) HealthChatProfileSectionView(state = state)
} }
@@ -11,7 +11,7 @@ package io.element.android.features.healthchat.impl.status
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
open class HealthChatAccountStatusStateProvider : PreviewParameterProvider<HealthChatAccountStatusState> { open class HealthChatAccountStatusStatePreviewParam : PreviewParameterProvider<HealthChatAccountStatusState> {
override val values: Sequence<HealthChatAccountStatusState> override val values: Sequence<HealthChatAccountStatusState>
get() = sequenceOf( get() = sequenceOf(
HealthChatAccountStatusState(status = HealthChatAccountStatus.PendingApproval(serverMessage = null)), HealthChatAccountStatusState(status = HealthChatAccountStatus.PendingApproval(serverMessage = null)),
@@ -74,7 +74,7 @@ fun HealthChatAccountStatusView(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HealthChatAccountStatusViewPreview( internal fun HealthChatAccountStatusViewPreview(
@PreviewParameter(HealthChatAccountStatusStateProvider::class) state: HealthChatAccountStatusState @PreviewParameter(HealthChatAccountStatusStatePreviewParam::class) state: HealthChatAccountStatusState
) = ElementPreview { ) = ElementPreview {
HealthChatAccountStatusView( HealthChatAccountStatusView(
state = state, state = state,
@@ -15,11 +15,11 @@ import io.element.android.features.healthchat.api.HealthChatApiException
import io.element.android.features.healthchat.api.HealthChatDirectoryService import io.element.android.features.healthchat.api.HealthChatDirectoryService
import io.element.android.features.healthchat.api.HealthChatMember import io.element.android.features.healthchat.api.HealthChatMember
import io.element.android.features.healthchat.api.HealthChatMemberDirectory import io.element.android.features.healthchat.api.HealthChatMemberDirectory
import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents import io.element.android.features.healthchat.api.directory.MemberDirectoryEvent
import io.element.android.features.healthchat.test.FakeHealthChatDirectoryService import io.element.android.features.healthchat.test.FakeHealthChatDirectoryService
import io.element.android.features.healthchat.test.aHealthChatMember import io.element.android.features.healthchat.test.aHealthChatMember
import io.element.android.features.invitepeople.test.FakeStartDMAction
import io.element.android.features.startchat.api.StartDMAction import io.element.android.features.startchat.api.StartDMAction
import io.element.android.features.startchat.test.FakeStartDMAction
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -52,7 +52,7 @@ class MemberDirectoryPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.directory).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.directory).isEqualTo(AsyncData.Uninitialized)
searchLambda.assertions().isNeverCalled() searchLambda.assertions().isNeverCalled()
initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) initialState.eventSink(MemberDirectoryEvent.LoadIfNeeded)
val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last()
assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory()) assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory())
searchLambda.assertions().isCalledOnce() searchLambda.assertions().isCalledOnce()
@@ -72,7 +72,7 @@ class MemberDirectoryPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) initialState.eventSink(MemberDirectoryEvent.LoadIfNeeded)
val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last()
// Typing restarts the effect: intermediate values must be debounced away. // Typing restarts the effect: intermediate values must be debounced away.
loadedState.searchQuery.setTextAndPlaceCursorAtEnd("M") loadedState.searchQuery.setTextAndPlaceCursorAtEnd("M")
@@ -100,11 +100,11 @@ class MemberDirectoryPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) initialState.eventSink(MemberDirectoryEvent.LoadIfNeeded)
val failedState = consumeItemsUntilPredicate { it.directory is AsyncData.Failure }.last() val failedState = consumeItemsUntilPredicate { it.directory is AsyncData.Failure }.last()
// No stale fallback. // No stale fallback.
assertThat((failedState.directory as AsyncData.Failure).prevData).isNull() assertThat((failedState.directory as AsyncData.Failure).prevData).isNull()
failedState.eventSink(MemberDirectoryEvents.Refresh) failedState.eventSink(MemberDirectoryEvent.Refresh)
val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() val loadedState = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last()
assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory()) assertThat(loadedState.directory.dataOrNull()).isEqualTo(aDirectory())
} }
@@ -127,7 +127,7 @@ class MemberDirectoryPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MemberDirectoryEvents.LoadIfNeeded) initialState.eventSink(MemberDirectoryEvent.LoadIfNeeded)
val state = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last() val state = consumeItemsUntilPredicate { it.directory is AsyncData.Success }.last()
assertThat(state.isApproved).isFalse() assertThat(state.isApproved).isFalse()
assertThat(state.pendingMessage).isEqualTo("Ihr Konto wird derzeit geprüft.") assertThat(state.pendingMessage).isEqualTo("Ihr Konto wird derzeit geprüft.")
@@ -149,11 +149,11 @@ class MemberDirectoryPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(MemberDirectoryEvents.StartDm(MatrixUser(A_USER_ID))) initialState.eventSink(MemberDirectoryEvent.StartDm(MatrixUser(A_USER_ID)))
val state = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Success }.last() val state = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Success }.last()
assertThat((state.startDmActionState as AsyncAction.Success).data).isEqualTo(A_ROOM_ID) assertThat((state.startDmActionState as AsyncAction.Success).data).isEqualTo(A_ROOM_ID)
executeLambda.assertions().isCalledOnce() executeLambda.assertions().isCalledOnce()
state.eventSink(MemberDirectoryEvents.ClearStartDmState) state.eventSink(MemberDirectoryEvent.ClearStartDmState)
val clearedState = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Uninitialized }.last() val clearedState = consumeItemsUntilPredicate { it.startDmActionState is AsyncAction.Uninitialized }.last()
assertThat(clearedState.startDmActionState).isEqualTo(AsyncAction.Uninitialized) assertThat(clearedState.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
} }
@@ -18,7 +18,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.healthchat.api.HealthChatApiException import io.element.android.features.healthchat.api.HealthChatApiException
import io.element.android.features.healthchat.api.directory.MemberDirectoryEvents import io.element.android.features.healthchat.api.directory.MemberDirectoryEvent
import io.element.android.features.healthchat.api.directory.MemberDirectoryState import io.element.android.features.healthchat.api.directory.MemberDirectoryState
import io.element.android.features.healthchat.api.directory.aDirectoryMember import io.element.android.features.healthchat.api.directory.aDirectoryMember
import io.element.android.features.healthchat.api.directory.aMemberDirectory import io.element.android.features.healthchat.api.directory.aMemberDirectory
@@ -37,19 +37,19 @@ import org.junit.Test
class MemberDirectoryViewTest : RobolectricTest() { class MemberDirectoryViewTest : RobolectricTest() {
@Test @Test
fun `on composition LoadIfNeeded is emitted`() = runAndroidComposeUiTest<ComponentActivity> { fun `on composition LoadIfNeeded is emitted`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>() val eventsRecorder = EventsRecorder<MemberDirectoryEvent>()
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState( state = aMemberDirectoryState(
directory = AsyncData.Uninitialized, directory = AsyncData.Uninitialized,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
eventsRecorder.assertSingle(MemberDirectoryEvents.LoadIfNeeded) eventsRecorder.assertSingle(MemberDirectoryEvent.LoadIfNeeded)
} }
@Test @Test
fun `clicking retry on the error state emits Refresh`() = runAndroidComposeUiTest<ComponentActivity> { fun `clicking retry on the error state emits Refresh`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>() val eventsRecorder = EventsRecorder<MemberDirectoryEvent>()
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState( state = aMemberDirectoryState(
directory = AsyncData.Failure(HealthChatApiException.Http(500, "Fehler beim Laden der Benutzer")), directory = AsyncData.Failure(HealthChatApiException.Http(500, "Fehler beim Laden der Benutzer")),
@@ -61,8 +61,8 @@ class MemberDirectoryViewTest : RobolectricTest() {
clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
MemberDirectoryEvents.LoadIfNeeded, MemberDirectoryEvent.LoadIfNeeded,
MemberDirectoryEvents.Refresh, MemberDirectoryEvent.Refresh,
) )
) )
} }
@@ -70,7 +70,7 @@ class MemberDirectoryViewTest : RobolectricTest() {
@Test @Test
fun `clicking start chat emits StartDm with the member as MatrixUser`() = runAndroidComposeUiTest<ComponentActivity> { fun `clicking start chat emits StartDm with the member as MatrixUser`() = runAndroidComposeUiTest<ComponentActivity> {
val member = aDirectoryMember() val member = aDirectoryMember()
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>() val eventsRecorder = EventsRecorder<MemberDirectoryEvent>()
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState( state = aMemberDirectoryState(
directory = AsyncData.Success(aMemberDirectory(members = listOf(member))), directory = AsyncData.Success(aMemberDirectory(members = listOf(member))),
@@ -82,8 +82,8 @@ class MemberDirectoryViewTest : RobolectricTest() {
.performClick() .performClick()
eventsRecorder.assertList( eventsRecorder.assertList(
listOf( listOf(
MemberDirectoryEvents.LoadIfNeeded, MemberDirectoryEvent.LoadIfNeeded,
MemberDirectoryEvents.StartDm(member.toMatrixUser()), MemberDirectoryEvent.StartDm(member.toMatrixUser()),
) )
) )
} }
@@ -91,7 +91,7 @@ class MemberDirectoryViewTest : RobolectricTest() {
@Test @Test
fun `clicking a member card opens the profile`() = runAndroidComposeUiTest<ComponentActivity> { fun `clicking a member card opens the profile`() = runAndroidComposeUiTest<ComponentActivity> {
val member = aDirectoryMember() val member = aDirectoryMember()
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>(expectEvents = true) val eventsRecorder = EventsRecorder<MemberDirectoryEvent>(expectEvents = true)
ensureCalledOnceWithParam(member.userId) { callback -> ensureCalledOnceWithParam(member.userId) { callback ->
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState( state = aMemberDirectoryState(
@@ -106,7 +106,7 @@ class MemberDirectoryViewTest : RobolectricTest() {
@Test @Test
fun `an unapproved account shows the pending banner and no search field`() = runAndroidComposeUiTest<ComponentActivity> { fun `an unapproved account shows the pending banner and no search field`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>(expectEvents = true) val eventsRecorder = EventsRecorder<MemberDirectoryEvent>(expectEvents = true)
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState( state = aMemberDirectoryState(
directory = AsyncData.Success( directory = AsyncData.Success(
@@ -127,7 +127,7 @@ class MemberDirectoryViewTest : RobolectricTest() {
@Test @Test
fun `hidden affordances are not rendered when their callbacks are null`() = runAndroidComposeUiTest<ComponentActivity> { fun `hidden affordances are not rendered when their callbacks are null`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<MemberDirectoryEvents>(expectEvents = true) val eventsRecorder = EventsRecorder<MemberDirectoryEvent>(expectEvents = true)
setMemberDirectoryView( setMemberDirectoryView(
state = aMemberDirectoryState(eventSink = eventsRecorder), state = aMemberDirectoryState(eventSink = eventsRecorder),
onMyProfileClick = null, onMyProfileClick = null,
@@ -106,7 +106,7 @@ class HealthChatProfileSectionPresenterTest {
val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService) val presenter = createPresenter(isCurrentUser = false, directoryService = directoryService)
presenter.test { presenter.test {
val failedState = consumeItemsUntilPredicate { it.profile is AsyncData.Failure }.last() val failedState = consumeItemsUntilPredicate { it.profile is AsyncData.Failure }.last()
failedState.eventSink(HealthChatProfileSectionEvents.Retry) failedState.eventSink(HealthChatProfileSectionEvent.Retry)
val state = consumeItemsUntilPredicate { it.profile is AsyncData.Success }.last() val state = consumeItemsUntilPredicate { it.profile is AsyncData.Success }.last()
assertThat(state.profile).isEqualTo(AsyncData.Success(member)) assertThat(state.profile).isEqualTo(AsyncData.Success(member))
assertThat(callCount).isEqualTo(2) assertThat(callCount).isEqualTo(2)
@@ -30,7 +30,7 @@ import org.robolectric.Shadows.shadowOf
class HealthChatProfileSectionViewTest : RobolectricTest() { class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `a loaded profile renders its fields with values`() = runAndroidComposeUiTest<ComponentActivity> { fun `a loaded profile renders its fields with values`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -50,7 +50,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `empty fields are omitted`() = runAndroidComposeUiTest<ComponentActivity> { fun `empty fields are omitted`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -77,7 +77,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `clicking the email row opens a mailto intent`() = runAndroidComposeUiTest<ComponentActivity> { fun `clicking the email row opens a mailto intent`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -94,7 +94,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `clicking the phone row opens a tel intent`() = runAndroidComposeUiTest<ComponentActivity> { fun `clicking the phone row opens a tel intent`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -110,7 +110,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `a loading state renders nothing`() = runAndroidComposeUiTest<ComponentActivity> { fun `a loading state renders nothing`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -124,7 +124,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `a NotFound failure renders nothing`() = runAndroidComposeUiTest<ComponentActivity> { fun `a NotFound failure renders nothing`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>(expectEvents = false)
val activity = requireNotNull(activity) val activity = requireNotNull(activity)
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
@@ -139,7 +139,7 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
@Test @Test
fun `an error state shows a retry row which emits Retry`() = runAndroidComposeUiTest<ComponentActivity> { fun `an error state shows a retry row which emits Retry`() = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvents>() val eventsRecorder = EventsRecorder<HealthChatProfileSectionEvent>()
setContent { setContent {
HealthChatProfileSectionView( HealthChatProfileSectionView(
state = aHealthChatProfileSectionState( state = aHealthChatProfileSectionState(
@@ -149,6 +149,6 @@ class HealthChatProfileSectionViewTest : RobolectricTest() {
) )
} }
clickOn(CommonStrings.action_retry) clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(HealthChatProfileSectionEvents.Retry) eventsRecorder.assertSingle(HealthChatProfileSectionEvent.Retry)
} }
} }
@@ -28,8 +28,10 @@ class HomeViewBackHandlingTest : RobolectricTest() {
currentHomeNavigationBarItem = HomeNavigationBarItem.Members, currentHomeNavigationBarItem = HomeNavigationBarItem.Members,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onRoomClick = {}, onRoomClick = { _, _ -> },
onSettingsClick = {}, onSettingsClick = {},
onAddAccountClick = {},
onSwitchToAccount = {},
onSetUpRecoveryClick = {}, onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {}, onConfirmRecoveryKeyClick = {},
onStartChatClick = {}, onStartChatClick = {},
@@ -58,8 +60,10 @@ class HomeViewBackHandlingTest : RobolectricTest() {
currentHomeNavigationBarItem = HomeNavigationBarItem.Members, currentHomeNavigationBarItem = HomeNavigationBarItem.Members,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onRoomClick = {}, onRoomClick = { _, _ -> },
onSettingsClick = {}, onSettingsClick = {},
onAddAccountClick = {},
onSwitchToAccount = {},
onSetUpRecoveryClick = {}, onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {}, onConfirmRecoveryKeyClick = {},
onStartChatClick = {}, onStartChatClick = {},