Fix MessageRendererTest.kt

Tests were failing because coroutines-test was not used and objects were not properly mocked.
Additionally another test has been added
Affected: MessageRendererTest.kt
This commit is contained in:
Davide Farella 2020-09-24 11:30:19 +02:00 committed by Davide Giuseppe Farella
parent 228af2d845
commit 5c3f788eaa
3 changed files with 103 additions and 33 deletions

View File

@ -27,13 +27,13 @@ import ch.protonmail.android.di.AttachmentsDirectory
import ch.protonmail.android.jobs.helper.EmbeddedImage
import ch.protonmail.android.utils.extensions.forEachAsync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.channels.toList
import kotlinx.coroutines.delay
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.plus
import me.proton.core.util.kotlin.DispatcherProvider
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
@ -42,6 +42,7 @@ import java.io.File
import javax.inject.Inject
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.time.milliseconds
/**
* A class that will inline the images in the message's body.
@ -54,11 +55,12 @@ import kotlin.math.sqrt
* @author Davide Farella
*/
internal class MessageRenderer(
private val dispatchers: DispatcherProvider,
private val directory: File,
private val documentParser: DocumentParser,
private val bitmapImageDecoder: ImageDecoder,
scope: CoroutineScope
) : CoroutineScope by ( scope + Default ) {
) : CoroutineScope by scope + dispatchers.Comp {
/** The [String] html of the message body */
var messageBody: String? = null
@ -79,7 +81,7 @@ internal class MessageRenderer(
imageCompressor.send(embeddedImages)
// Workaround that ignore values for the next half second, since ViewModel is emitting
// too many times
delay(500)
delay(DebounceDelay)
}
}
@ -209,6 +211,7 @@ internal class MessageRenderer(
* @param imageDecoder [ImageDecoder]
*/
class Factory @Inject constructor(
private val dispatchers: DispatcherProvider,
@AttachmentsDirectory private val attachmentsDirectory: File,
private val documentParser: DocumentParser = DefaultDocumentParser(),
private val imageDecoder: ImageDecoder = DefaultImageDecoder()
@ -218,7 +221,11 @@ internal class MessageRenderer(
/** @return new instance of [MessageRenderer] with the given [messageBody] */
fun create(scope: CoroutineScope, messageId: String) =
MessageRenderer(messageDirectory(messageId), documentParser, imageDecoder, scope)
MessageRenderer(dispatchers, messageDirectory(messageId), documentParser, imageDecoder, scope)
}
companion object {
val DebounceDelay = 500.milliseconds
}
}

View File

@ -16,50 +16,113 @@
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
package ch.protonmail.android.activities.messageDetails
import android.util.Base64
import ch.protonmail.android.jobs.helper.EmbeddedImage
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.plus
import me.proton.core.test.kotlin.CoroutinesTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
/**
* Test class for [MessageRenderer]
* @author Davide Farella
*/
internal class MessageRendererTest {
internal class MessageRendererTest : CoroutinesTest {
private val mockImageDecoder: ImageDecoder = mockk()
private val mockDocumentParser: DocumentParser = mockk()
@get:Rule
val folder: TemporaryFolder = TemporaryFolder()
.also { it.create() }
private val mockImageDecoder: ImageDecoder = mockk(relaxed = true)
private val mockDocumentParser: DocumentParser = mockk(relaxed = true)
private val mockEmbeddedImages = (1..10).map {
mockk<EmbeddedImage>(relaxed = true) { every { localFileName } returns "" }
}
private val CoroutineScope.newRenderer get() =
MessageRenderer(mockk(), mockDocumentParser, mockImageDecoder, this)
.apply { messageBody = "" }
@Test // TODO, replace with `runBlockingTest` and `advanceTimeBy` when this is resolved: java.lang.IllegalStateException: This job has not completed yet
fun `renderedBody emits just once for every images sent`() {
val count = 2
val consumer: (String) -> Unit = mockk(relaxed = true)
with(TestCoroutineScope().newRenderer) {
launch { renderedBody.consumeEach(consumer) }
repeat(count) { runBlocking {
images.send(mockEmbeddedImages)
delay(550)
} }
renderedBody.close()
mockk<EmbeddedImage>(relaxed = true) {
every { localFileName } returns "$it"
every { contentId } returns "id"
every { encoding } returns ""
}
verify(exactly = count) { consumer(any()) }
}
@Before
fun before() {
for (image in mockEmbeddedImages) {
val file = folder.newFile(image.localFileName)
file.writeText("_")
}
}
private fun CoroutineScope.Renderer() =
MessageRenderer(dispatchers, folder.root, mockDocumentParser, mockImageDecoder, this)
.apply { messageBody = "" }
@Test
fun `renderedBody doesn't emit for images sent with too short delay`() = coroutinesTest {
mockkStatic(Base64::class) {
every { Base64.encodeToString(any(), any()) } returns "string"
val scope = this + Job()
val renderer = scope.Renderer()
val count = 2
val consumer: (String) -> Unit = mockk(relaxed = true)
with(renderer) {
launch(Unconfined) {
renderedBody.consumeEach(consumer)
}
repeat(count) {
images.offer(mockEmbeddedImages)
}
advanceUntilIdle()
renderedBody.close()
scope.cancel()
}
verify(exactly = 1) { consumer(any()) }
}
}
@Test
fun `renderedBody emits for every image sent with right delay`() = coroutinesTest {
mockkStatic(Base64::class) {
every { Base64.encodeToString(any(), any()) } returns "string"
val scope = this + Job()
val renderer = scope.Renderer()
val count = 2
val consumer: (String) -> Unit = mockk(relaxed = true)
with(renderer) {
launch(Unconfined) {
renderedBody.consumeEach(consumer)
}
repeat(count) {
images.offer(mockEmbeddedImages)
advanceTimeBy(MessageRenderer.DebounceDelay.toLongMilliseconds())
}
advanceUntilIdle()
renderedBody.close()
scope.cancel()
}
verify(exactly = count) { consumer(any()) }
}
}
}

View File

@ -24,8 +24,8 @@ import studio.forface.easygradle.dsl.android.*
fun initVersions() {
// region Kotlin
`kotlin version` = "1.3.70" // Released: Mar 03, 2020
`coroutines version` = "1.3.4" // Released: Mar 06, 2020
`kotlin version` = "1.3.72" // Released: Apr 14, 2020
`coroutines version` = "1.3.9" // Released: Aug 14, 2020
`serialization version` = "0.20.0" // Released: Mar 04, 2020
// endregion
@ -69,7 +69,7 @@ const val `Proton-work-manager version` = "0.1.1" // Released: Sep
// Test
const val `Proton-android-test version` = "0.1" // Released: May 30, 2020
const val `Proton-android-instr-test version` = "0.1.2" // Released: Sep 14, 2020
const val `Proton-kotlin-test version` = "0.1" // Released: Jun 10, 2020
const val `Proton-kotlin-test version` = "0.1.1" // Released: Sep 16, 2020
const val `Proton-domain version` = "0.1" // Released: Jul 03, 2020