Merge pull request #2350 from element-hq/feature/bma/improveSendProblemUx

Improve send problem ux
This commit is contained in:
Benoit Marty
2024-02-06 16:11:16 +01:00
committed by GitHub
13 changed files with 107 additions and 55 deletions

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.rageshake.impl.bugreport
sealed class BugReportFormError : Exception() {
data object DescriptionTooShort : BugReportFormError()
}

View File

@@ -91,7 +91,13 @@ class BugReportPresenter @Inject constructor(
fun handleEvents(event: BugReportEvents) {
when (event) {
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener)
BugReportEvents.SendBugReport -> {
if (formState.value.description.length < 10) {
sendingAction.value = AsyncAction.Failure(BugReportFormError.DescriptionTooShort)
} else {
appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener)
}
}
BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
is BugReportEvents.SetDescription -> updateFormState(formState) {
copy(description = event.description)

View File

@@ -28,8 +28,9 @@ data class BugReportState(
val sending: AsyncAction<Unit>,
val eventSink: (BugReportEvents) -> Unit
) {
val submitEnabled =
formState.description.length > 10 && sending !is AsyncAction.Loading
val submitEnabled = sending !is AsyncAction.Loading
val isDescriptionInError = sending is AsyncAction.Failure &&
sending.error is BugReportFormError.DescriptionTooShort
}
@Parcelize

View File

@@ -33,6 +33,7 @@ open class BugReportStateProvider : PreviewParameterProvider<BugReportState> {
),
aBugReportState().copy(sending = AsyncAction.Loading),
aBugReportState().copy(sending = AsyncAction.Success(Unit)),
aBugReportState().copy(sending = AsyncAction.Failure(BugReportFormError.DescriptionTooShort)),
)
}

View File

@@ -96,7 +96,7 @@ fun BugReportView(
imeAction = ImeAction.Next
),
minLines = 3,
// TODO Error text too short
isError = state.isDescriptionInError,
)
}
Spacer(modifier = Modifier.height(16.dp))
@@ -167,6 +167,12 @@ fun BugReportView(
eventSink(BugReportEvents.ResetAll)
onDone()
},
errorMessage = { error ->
when (error) {
BugReportFormError.DescriptionTooShort -> stringResource(id = R.string.screen_bug_report_error_description_too_short)
else -> error.message ?: error.toString()
}
},
onErrorDismiss = { eventSink(BugReportEvents.ClearError) },
)
}

View File

@@ -7,6 +7,7 @@
<string name="screen_bug_report_editor_description">"Please describe the problem. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can."</string>
<string name="screen_bug_report_editor_placeholder">"Describe the problem…"</string>
<string name="screen_bug_report_editor_supporting">"If possible, please write the description in English."</string>
<string name="screen_bug_report_error_description_too_short">"The description is too short, please provide more details about what happened. Thanks!"</string>
<string name="screen_bug_report_include_crash_logs">"Send crash logs"</string>
<string name="screen_bug_report_include_logs">"Allow logs"</string>
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>

View File

@@ -20,6 +20,9 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI
@@ -27,6 +30,7 @@ import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolde
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -40,12 +44,7 @@ class BugReportPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -55,24 +54,19 @@ class BugReportPresenterTest {
assertThat(initialState.sending).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.screenshotUri).isNull()
assertThat(initialState.sendingProgress).isEqualTo(0f)
assertThat(initialState.submitEnabled).isFalse()
assertThat(initialState.submitEnabled).isTrue()
}
}
@Test
fun `present - set description`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION))
assertThat(awaitItem().submitEnabled).isFalse()
assertThat(awaitItem().submitEnabled).isTrue()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION))
assertThat(awaitItem().submitEnabled).isTrue()
}
@@ -80,12 +74,7 @@ class BugReportPresenterTest {
@Test
fun `present - can contact`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -99,12 +88,7 @@ class BugReportPresenterTest {
@Test
fun `present - send logs`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -119,12 +103,7 @@ class BugReportPresenterTest {
@Test
fun `present - send screenshot`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(),
FakeScreenshotHolder(),
this,
)
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -138,11 +117,9 @@ class BugReportPresenterTest {
@Test
fun `present - reset all`() = runTest {
val presenter = BugReportPresenter(
FakeBugReporter(),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
val presenter = createPresenter(
crashDataStore = FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
screenshotHolder = FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -160,16 +137,17 @@ class BugReportPresenterTest {
@Test
fun `present - send success`() = runTest {
val presenter = BugReportPresenter(
val presenter = createPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Success),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION))
skipItems(1)
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
@@ -185,16 +163,17 @@ class BugReportPresenterTest {
@Test
fun `present - send failure`() = runTest {
val presenter = BugReportPresenter(
val presenter = createPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Failure),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION))
skipItems(1)
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
@@ -212,18 +191,38 @@ class BugReportPresenterTest {
}
}
@Test
fun `present - send failure description too short`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION))
skipItems(1)
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
val errorState = awaitItem()
assertThat(errorState.sending).isEqualTo(AsyncAction.Failure(BugReportFormError.DescriptionTooShort))
// Reset failure
initialState.eventSink.invoke(BugReportEvents.ClearError)
val lastItem = awaitItem()
assertThat(lastItem.sending).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
@Test
fun `present - send cancel`() = runTest {
val presenter = BugReportPresenter(
val presenter = createPresenter(
FakeBugReporter(mode = FakeBugReporterMode.Cancel),
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true),
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI),
this,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION))
skipItems(1)
initialState.eventSink.invoke(BugReportEvents.SendBugReport)
skipItems(1)
val progressState = awaitItem()
@@ -235,4 +234,15 @@ class BugReportPresenterTest {
assertThat(awaitItem().sending).isEqualTo(AsyncAction.Uninitialized)
}
}
private fun TestScope.createPresenter(
bugReporter: BugReporter = FakeBugReporter(),
crashDataStore: CrashDataStore = FakeCrashDataStore(),
screenshotHolder: ScreenshotHolder = FakeScreenshotHolder(),
) = BugReportPresenter(
bugReporter,
crashDataStore,
screenshotHolder,
this,
)
}