Benoit Marty
2 years ago
8 changed files with 366 additions and 10 deletions
@ -0,0 +1,247 @@
@@ -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 @@
@@ -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 @@
@@ -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