Add test for BugReportPresenter
This commit is contained in:
parent
e954dccad9
commit
31eb86ebe4
8 changed files with 366 additions and 10 deletions
|
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.core.net.toUri
|
||||
import io.element.android.features.rageshake.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.logs.VectorFileLogger
|
||||
import io.element.android.features.rageshake.reporter.BugReporter
|
||||
|
|
@ -73,7 +72,7 @@ class BugReportPresenter @Inject constructor(
|
|||
override fun present(): BugReportState {
|
||||
val screenshotUri = rememberSaveable {
|
||||
mutableStateOf(
|
||||
screenshotHolder.getFile()?.toUri()?.toString()
|
||||
screenshotHolder.getFileUri()
|
||||
)
|
||||
}
|
||||
val crashInfo: String by crashDataStore
|
||||
|
|
@ -150,6 +149,6 @@ class BugReportPresenter @Inject constructor(
|
|||
private fun CoroutineScope.resetAll() = launch {
|
||||
screenshotHolder.reset()
|
||||
crashDataStore.reset()
|
||||
VectorFileLogger.getFromTimber().reset()
|
||||
VectorFileLogger.getFromTimber()?.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ class VectorFileLogger(
|
|||
) : Timber.Tree() {
|
||||
|
||||
companion object {
|
||||
fun getFromTimber(): VectorFileLogger {
|
||||
return Timber.forest().filterIsInstance<VectorFileLogger>().first()
|
||||
fun getFromTimber(): VectorFileLogger? {
|
||||
return Timber.forest().filterIsInstance<VectorFileLogger>().firstOrNull()
|
||||
}
|
||||
|
||||
private const val SIZE_20MB = 20 * 1024 * 1024
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ package io.element.android.features.rageshake.reporter
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import io.element.android.features.rageshake.R
|
||||
import io.element.android.features.rageshake.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.logs.VectorFileLogger
|
||||
|
|
@ -153,7 +155,7 @@ class DefaultBugReporter @Inject constructor(
|
|||
val gzippedFiles = ArrayList<File>()
|
||||
|
||||
val vectorFileLogger = VectorFileLogger.getFromTimber()
|
||||
if (withDevicesLogs) {
|
||||
if (withDevicesLogs && vectorFileLogger != null) {
|
||||
val files = vectorFileLogger.getLogFiles()
|
||||
files.mapNotNullTo(gzippedFiles) { f ->
|
||||
if (!mIsCancelled) {
|
||||
|
|
@ -254,7 +256,10 @@ class DefaultBugReporter @Inject constructor(
|
|||
mBugReportFiles.addAll(gzippedFiles)
|
||||
|
||||
if (withScreenshot) {
|
||||
screenshotHolder.getFile()?.let { screenshotFile ->
|
||||
screenshotHolder.getFileUri()
|
||||
?.toUri()
|
||||
?.toFile()
|
||||
?.let { screenshotFile ->
|
||||
try {
|
||||
builder.addFormDataPart(
|
||||
"file",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.rageshake.screenshot
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.bitmap.writeBitmap
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
|
|
@ -38,7 +39,12 @@ class DefaultScreenshotHolder @Inject constructor(
|
|||
file.writeBitmap(data, Bitmap.CompressFormat.PNG, 85)
|
||||
}
|
||||
|
||||
override fun getFile() = file.takeIf { it.exists() && it.length() > 0 }
|
||||
override fun getFileUri(): String? {
|
||||
return file
|
||||
.takeIf { it.exists() && it.length() > 0 }
|
||||
?.toUri()
|
||||
?.toString()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
file.safeDelete()
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@
|
|||
package io.element.android.features.rageshake.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import java.io.File
|
||||
|
||||
interface ScreenshotHolder {
|
||||
fun writeBitmap(data: Bitmap)
|
||||
fun getFile(): File?
|
||||
fun getFileUri(): String?
|
||||
fun reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Add a link
Reference in a new issue