Benoit Marty
2 years ago
8 changed files with 366 additions and 10 deletions
@ -0,0 +1,247 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class) |
||||||
|
|
||||||
|
package io.element.android.features.rageshake.bugreport |
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock |
||||||
|
import app.cash.molecule.moleculeFlow |
||||||
|
import app.cash.turbine.test |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.features.rageshake.crash.ui.A_CRASH_DATA |
||||||
|
import io.element.android.features.rageshake.crash.ui.FakeCrashDataStore |
||||||
|
import io.element.android.libraries.architecture.Async |
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
const val A_SHORT_DESCRIPTION = "bug!" |
||||||
|
const val A_LONG_DESCRIPTION = "I have seen a bug!" |
||||||
|
|
||||||
|
class BugReportPresenterTest { |
||||||
|
@Test |
||||||
|
fun `present - initial state`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
assertThat(initialState.hasCrashLogs).isFalse() |
||||||
|
assertThat(initialState.formState).isEqualTo(BugReportFormState.Default) |
||||||
|
assertThat(initialState.sending).isEqualTo(Async.Uninitialized) |
||||||
|
assertThat(initialState.screenshotUri).isNull() |
||||||
|
assertThat(initialState.sendingProgress).isEqualTo(0f) |
||||||
|
assertThat(initialState.submitEnabled).isFalse() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - set description`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION)) |
||||||
|
assertThat(awaitItem().submitEnabled).isFalse() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) |
||||||
|
assertThat(awaitItem().submitEnabled).isTrue() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - can contact`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetCanContact(true)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = true)) |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetCanContact(false)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = false)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send crash logs`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
// Since this is true by default, start by disabling |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(false)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = false)) |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendCrashLog(true)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendCrashLogs = true)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send logs`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
// Since this is true by default, start by disabling |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendLog(false)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = false)) |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendLog(true)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = true)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send screenshot`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(), |
||||||
|
FakeScreenshotHolder(), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(true)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = true)) |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(false)) |
||||||
|
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = false)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - reset all`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(), |
||||||
|
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), |
||||||
|
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
skipItems(1) |
||||||
|
val initialState = awaitItem() |
||||||
|
assertThat(initialState.hasCrashLogs).isTrue() |
||||||
|
assertThat(initialState.screenshotUri).isEqualTo(A_SCREENSHOT_URI) |
||||||
|
initialState.eventSink.invoke(BugReportEvents.ResetAll) |
||||||
|
val resetState = awaitItem() |
||||||
|
assertThat(resetState.hasCrashLogs).isFalse() |
||||||
|
// TODO Make it live assertThat(resetState.screenshotUri).isNull() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send success`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(mode = FakeBugReporterMode.Success), |
||||||
|
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), |
||||||
|
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SendBugReport) |
||||||
|
skipItems(1) |
||||||
|
val progressState = awaitItem() |
||||||
|
assertThat(progressState.sending).isEqualTo(Async.Loading(null)) |
||||||
|
assertThat(progressState.sendingProgress).isEqualTo(0f) |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(1f) |
||||||
|
skipItems(1) |
||||||
|
assertThat(awaitItem().sending).isEqualTo(Async.Success(Unit)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send failure`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(mode = FakeBugReporterMode.Failure), |
||||||
|
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), |
||||||
|
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SendBugReport) |
||||||
|
skipItems(1) |
||||||
|
val progressState = awaitItem() |
||||||
|
assertThat(progressState.sending).isEqualTo(Async.Loading(null)) |
||||||
|
assertThat(progressState.sendingProgress).isEqualTo(0f) |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) |
||||||
|
// Failure |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(0f) |
||||||
|
assertThat((awaitItem().sending as Async.Failure).error.message).isEqualTo(A_REASON) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - send cancel`() = runTest { |
||||||
|
val presenter = BugReportPresenter( |
||||||
|
FakeBugReporter(mode = FakeBugReporterMode.Cancel), |
||||||
|
FakeCrashDataStore(crashData = A_CRASH_DATA, appHasCrashed = true), |
||||||
|
FakeScreenshotHolder(screenshotUri = A_SCREENSHOT_URI), |
||||||
|
this, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(BugReportEvents.SendBugReport) |
||||||
|
skipItems(1) |
||||||
|
val progressState = awaitItem() |
||||||
|
assertThat(progressState.sending).isEqualTo(Async.Loading(null)) |
||||||
|
assertThat(progressState.sendingProgress).isEqualTo(0f) |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(0.5f) |
||||||
|
// Cancelled |
||||||
|
assertThat(awaitItem().sendingProgress).isEqualTo(0f) |
||||||
|
assertThat(awaitItem().sending).isEqualTo(Async.Uninitialized) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 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.bugreport |
||||||
|
|
||||||
|
import io.element.android.features.rageshake.reporter.BugReporter |
||||||
|
import io.element.android.features.rageshake.reporter.BugReporterListener |
||||||
|
import io.element.android.features.rageshake.reporter.ReportType |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.delay |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
|
||||||
|
const val A_REASON = "There has been a failure" |
||||||
|
|
||||||
|
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter { |
||||||
|
override fun sendBugReport( |
||||||
|
coroutineScope: CoroutineScope, |
||||||
|
reportType: ReportType, |
||||||
|
withDevicesLogs: Boolean, |
||||||
|
withCrashLogs: Boolean, |
||||||
|
withKeyRequestHistory: Boolean, |
||||||
|
withScreenshot: Boolean, |
||||||
|
theBugDescription: String, |
||||||
|
serverVersion: String, |
||||||
|
canContact: Boolean, |
||||||
|
customFields: Map<String, String>?, |
||||||
|
listener: BugReporterListener?, |
||||||
|
) { |
||||||
|
coroutineScope.launch { |
||||||
|
delay(100) |
||||||
|
listener?.onProgress(0) |
||||||
|
delay(100) |
||||||
|
listener?.onProgress(50) |
||||||
|
delay(100) |
||||||
|
when (mode) { |
||||||
|
FakeBugReporterMode.Success -> Unit |
||||||
|
FakeBugReporterMode.Failure -> { |
||||||
|
listener?.onUploadFailed(A_REASON) |
||||||
|
return@launch |
||||||
|
} |
||||||
|
FakeBugReporterMode.Cancel -> { |
||||||
|
listener?.onUploadCancelled() |
||||||
|
return@launch |
||||||
|
} |
||||||
|
} |
||||||
|
listener?.onProgress(100) |
||||||
|
delay(100) |
||||||
|
listener?.onUploadSucceed(null) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum class FakeBugReporterMode { |
||||||
|
Success, |
||||||
|
Failure, |
||||||
|
Cancel |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2023 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.bugreport |
||||||
|
|
||||||
|
import android.graphics.Bitmap |
||||||
|
import io.element.android.features.rageshake.screenshot.ScreenshotHolder |
||||||
|
|
||||||
|
const val A_SCREENSHOT_URI = "file://content/uri" |
||||||
|
|
||||||
|
class FakeScreenshotHolder(private val screenshotUri: String? = null) : ScreenshotHolder { |
||||||
|
override fun writeBitmap(data: Bitmap) = Unit |
||||||
|
|
||||||
|
override fun getFileUri() = screenshotUri |
||||||
|
|
||||||
|
override fun reset() = Unit |
||||||
|
} |
Loading…
Reference in new issue