mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Model the fake voice recorder on a cancellable recording job (#7466)
* Model the fake voice recorder on a cancellable recording job FakeVoiceRecorder pushed a whole recording synchronously from a single suspending call, which made it both weaker and stricter than DefaultVoiceRecorder. Every level emission carried the same elapsed time and the emissions were coalesced by Molecule, so no intermediate recording state was observable and the level-limit test had to skip half the emissions to compensate. Each level now advances the test time source and suspends for its share of the recording duration, so the states arrive one at a time and that magic number is gone. Call verification moved to the constructor lambdas the rest of the repo uses, in place of hand-rolled counters and assertCalls. The counters discarded the argument, so nothing pinned that cancelling a recording stops it with cancelled = true; that is now asserted. Truth is no longer needed by the fixture module. A second startRecord() no longer raises an error where production only warns and returns, and stopping or deleting terminates an in-flight emission loop the way cancelling the real recording job does, so a stale recording state can no longer arrive after the recording has finished. * Keep each recording's level loop to its own recording The level loop checked a single isRecording flag, which cannot tell "this recording was cancelled" from "a later recording has started". After a cancel followed by a new start, the first loop resumed, saw the flag set again, and advanced the shared test time source, so the new recording reported the elapsed time of both. Each recording now carries an id and its loop stops as soon as that id is no longer the active one.
This commit is contained in:
+73
-27
@@ -47,6 +47,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
|||||||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -60,6 +61,7 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
@@ -67,8 +69,14 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
private val startRecordResult = lambdaRecorder<Unit> { }
|
||||||
|
private val stopRecordResult = lambdaRecorder<Boolean, Unit> { }
|
||||||
|
private val deleteRecordingResult = lambdaRecorder<Unit> { }
|
||||||
private val voiceRecorder = FakeVoiceRecorder(
|
private val voiceRecorder = FakeVoiceRecorder(
|
||||||
recordingDuration = RECORDING_DURATION
|
recordingDuration = RECORDING_DURATION,
|
||||||
|
startRecordResult = startRecordResult,
|
||||||
|
stopRecordResult = stopRecordResult,
|
||||||
|
deleteRecordingResult = deleteRecordingResult,
|
||||||
)
|
)
|
||||||
private val analyticsService = FakeAnalyticsService()
|
private val analyticsService = FakeAnalyticsService()
|
||||||
private val sendVoiceMessageResult =
|
private val sendVoiceMessageResult =
|
||||||
@@ -97,7 +105,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val RECORDING_DURATION = 1.seconds
|
private val RECORDING_DURATION = 1.seconds
|
||||||
|
private val FIRST_LEVEL_DURATION = 500.milliseconds
|
||||||
private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList())
|
private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList())
|
||||||
|
private val FIRST_RECORDING_STATE = VoiceMessageState.Recording(FIRST_LEVEL_DURATION, listOf(0.1f).toImmutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -106,7 +116,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 0)
|
startRecordResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(initialState)
|
testPauseAndDestroy(initialState)
|
||||||
}
|
}
|
||||||
@@ -118,9 +128,10 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
|
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(FIRST_RECORDING_STATE)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(started = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -133,14 +144,16 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val voiceRecorder = FakeVoiceRecorder(
|
val voiceRecorder = FakeVoiceRecorder(
|
||||||
levels = levels,
|
levels = levels,
|
||||||
recordingDuration = RECORDING_DURATION,
|
recordingDuration = RECORDING_DURATION,
|
||||||
|
startRecordResult = { },
|
||||||
|
stopRecordResult = { },
|
||||||
|
deleteRecordingResult = { },
|
||||||
)
|
)
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||||
voiceRecorder = voiceRecorder,
|
voiceRecorder = voiceRecorder,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
skipItems(numberOfLevels / 2 - 1)
|
val finalState = consumeItemsUntilTimeout().last()
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java)
|
assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java)
|
||||||
val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording
|
val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording
|
||||||
// The number of levels should be limited to 128 items
|
// The number of levels should be limited to 128 items
|
||||||
@@ -221,7 +234,8 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
@@ -235,7 +249,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(true))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +265,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -265,8 +283,10 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Nothing should happen
|
// Nothing should happen
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(FIRST_RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 0, deleted = 0)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isNeverCalled()
|
||||||
|
deleteRecordingResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -282,7 +302,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem().also {
|
val finalState = awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
||||||
}
|
}
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -299,7 +321,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem().also {
|
val finalState = awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(aPausedState())
|
assertThat(it.voiceMessageState).isEqualTo(aPausedState())
|
||||||
}
|
}
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -340,7 +364,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -360,7 +386,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -377,7 +405,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -455,7 +485,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -476,7 +508,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -499,7 +533,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
|
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true))
|
||||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||||
assertThat(analyticsService.trackedErrors).isEmpty()
|
assertThat(analyticsService.trackedErrors).isEmpty()
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -527,7 +563,9 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
deleteRecordingResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -577,7 +615,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||||
assertThat(analyticsService.trackedErrors).hasSize(1)
|
assertThat(analyticsService.trackedErrors).hasSize(1)
|
||||||
voiceRecorder.assertCalls(started = 0)
|
startRecordResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
testPauseAndDestroy(initialState)
|
testPauseAndDestroy(initialState)
|
||||||
}
|
}
|
||||||
@@ -586,8 +624,14 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `present - record error - security exceptions are tracked`() = runTest {
|
fun `present - record error - security exceptions are tracked`() = runTest {
|
||||||
val exception = SecurityException("")
|
val exception = SecurityException("")
|
||||||
voiceRecorder.givenThrowsSecurityException(exception)
|
val startRecordResult = lambdaRecorder<Unit> { throw exception }
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val voiceRecorder = FakeVoiceRecorder(
|
||||||
|
recordingDuration = RECORDING_DURATION,
|
||||||
|
startRecordResult = startRecordResult,
|
||||||
|
)
|
||||||
|
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||||
|
voiceRecorder = voiceRecorder,
|
||||||
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
@@ -596,7 +640,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
assertThat(analyticsService.trackedErrors).containsExactly(
|
assertThat(analyticsService.trackedErrors).containsExactly(
|
||||||
VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception)
|
VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception)
|
||||||
)
|
)
|
||||||
voiceRecorder.assertCalls(started = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(initialState)
|
testPauseAndDestroy(initialState)
|
||||||
}
|
}
|
||||||
@@ -616,7 +660,8 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
||||||
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
voiceRecorder.assertCalls(stopped = 1)
|
startRecordResult.assertions().isNeverCalled()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
|
||||||
permissionsPresenter.setPermissionGranted()
|
permissionsPresenter.setPermissionGranted()
|
||||||
|
|
||||||
@@ -625,7 +670,8 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = expectMostRecentItem()
|
val finalState = expectMostRecentItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(stopped = 1, started = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
stopRecordResult.assertions().isCalledOnce().with(value(false))
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -659,7 +705,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
|
|
||||||
val finalState = expectMostRecentItem()
|
val finalState = expectMostRecentItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(started = 1)
|
startRecordResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
}
|
}
|
||||||
@@ -696,7 +742,7 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||||||
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
assertThat(it.showPermissionRationaleDialog).isTrue()
|
assertThat(it.showPermissionRationaleDialog).isTrue()
|
||||||
}
|
}
|
||||||
voiceRecorder.assertCalls(started = 0)
|
startRecordResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
testPauseAndDestroy(finalState)
|
testPauseAndDestroy(finalState)
|
||||||
|
|||||||
@@ -19,6 +19,5 @@ dependencies {
|
|||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
|
|
||||||
implementation(libs.coroutines.test)
|
implementation(libs.coroutines.test)
|
||||||
implementation(libs.test.truth)
|
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-48
@@ -8,13 +8,13 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.voicerecorder.test
|
package io.element.android.libraries.voicerecorder.test
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
||||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.yield
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -23,51 +23,47 @@ import kotlin.time.TestTimeSource
|
|||||||
class FakeVoiceRecorder(
|
class FakeVoiceRecorder(
|
||||||
private val timeSource: TestTimeSource = TestTimeSource(),
|
private val timeSource: TestTimeSource = TestTimeSource(),
|
||||||
private val recordingDuration: Duration = 0.seconds,
|
private val recordingDuration: Duration = 0.seconds,
|
||||||
private val levels: List<Float> = listOf(0.1f, 0.2f)
|
private val levels: List<Float> = listOf(0.1f, 0.2f),
|
||||||
|
val waveform: List<Float> = A_WAVEFORM,
|
||||||
|
private val startRecordResult: () -> Unit = { lambdaError() },
|
||||||
|
private val stopRecordResult: (Boolean) -> Unit = { lambdaError() },
|
||||||
|
private val deleteRecordingResult: () -> Unit = { lambdaError() },
|
||||||
) : VoiceRecorder {
|
) : VoiceRecorder {
|
||||||
private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle)
|
private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle)
|
||||||
override val state: StateFlow<VoiceRecorderState> = _state
|
override val state: StateFlow<VoiceRecorderState> = _state
|
||||||
|
|
||||||
private var curRecording: File? = null
|
private val levelInterval = if (levels.isEmpty()) Duration.ZERO else recordingDuration / levels.size
|
||||||
|
|
||||||
private var securityException: SecurityException? = null
|
private var currentRecording: File? = null
|
||||||
|
private var lastRecordingId = 0
|
||||||
|
private var activeRecordingId = 0
|
||||||
|
|
||||||
private var startedCount = 0
|
|
||||||
private var stoppedCount = 0
|
|
||||||
private var deletedCount = 0
|
|
||||||
|
|
||||||
var waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f)
|
|
||||||
override suspend fun startRecord() {
|
override suspend fun startRecord() {
|
||||||
startedCount += 1
|
startRecordResult()
|
||||||
|
if (activeRecordingId != 0) return
|
||||||
|
val recordingId = ++lastRecordingId
|
||||||
|
activeRecordingId = recordingId
|
||||||
|
currentRecording = File("file.ogg")
|
||||||
val startedAt = timeSource.markNow()
|
val startedAt = timeSource.markNow()
|
||||||
securityException?.let { throw it }
|
|
||||||
|
|
||||||
if (curRecording != null) {
|
|
||||||
error("Previous recording was not cleared")
|
|
||||||
}
|
|
||||||
curRecording = File("file.ogg")
|
|
||||||
|
|
||||||
timeSource += recordingDuration
|
|
||||||
for (i in 1..levels.size) {
|
for (i in 1..levels.size) {
|
||||||
|
delay(levelInterval)
|
||||||
|
if (activeRecordingId != recordingId) return
|
||||||
|
timeSource += levelInterval
|
||||||
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i)))
|
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i)))
|
||||||
yield()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun stopRecord(
|
override suspend fun stopRecord(cancelled: Boolean) {
|
||||||
cancelled: Boolean
|
stopRecordResult(cancelled)
|
||||||
) {
|
activeRecordingId = 0
|
||||||
stoppedCount++
|
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
deleteRecording()
|
deleteRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.emit(
|
_state.emit(
|
||||||
when (curRecording) {
|
when (val file = currentRecording) {
|
||||||
null -> VoiceRecorderState.Idle
|
null -> VoiceRecorderState.Idle
|
||||||
else -> VoiceRecorderState.Finished(
|
else -> VoiceRecorderState.Finished(
|
||||||
file = curRecording!!,
|
file = file,
|
||||||
mimeType = MimeTypes.Ogg,
|
mimeType = MimeTypes.Ogg,
|
||||||
duration = recordingDuration,
|
duration = recordingDuration,
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
@@ -77,25 +73,11 @@ class FakeVoiceRecorder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteRecording() {
|
override suspend fun deleteRecording() {
|
||||||
deletedCount++
|
deleteRecordingResult()
|
||||||
curRecording = null
|
activeRecordingId = 0
|
||||||
|
currentRecording = null
|
||||||
_state.emit(
|
_state.emit(VoiceRecorderState.Idle)
|
||||||
VoiceRecorderState.Idle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertCalls(
|
|
||||||
started: Int = 0,
|
|
||||||
stopped: Int = 0,
|
|
||||||
deleted: Int = 0,
|
|
||||||
) {
|
|
||||||
assertThat(startedCount).isEqualTo(started)
|
|
||||||
assertThat(stoppedCount).isEqualTo(stopped)
|
|
||||||
assertThat(deletedCount).isEqualTo(deleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenThrowsSecurityException(exception: SecurityException) {
|
|
||||||
this.securityException = exception
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val A_WAVEFORM = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f)
|
||||||
|
|||||||
Reference in New Issue
Block a user