Merge pull request #244 from ouchadam/tech/gallery-tests

Tech/Image Gallery Tests
This commit is contained in:
Adam Brown 2022-11-03 12:44:51 +00:00 committed by GitHub
commit 78a4cbd140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 470 additions and 64 deletions

View File

@ -0,0 +1,30 @@
package app.dapk.st.core
import android.content.ContentResolver
import android.database.Cursor
import android.net.Uri
data class ContentResolverQuery(
val uri: Uri,
val projection: List<String>,
val selection: String,
val selectionArgs: List<String>,
val sortBy: String,
)
inline fun <T> ContentResolver.reduce(query: ContentResolverQuery, operation: (Cursor) -> T): List<T> {
return this.reduce(query, mutableListOf<T>()) { acc, cursor ->
acc.add(operation(cursor))
acc
}
}
inline fun <T> ContentResolver.reduce(query: ContentResolverQuery, initial: T, operation: (T, Cursor) -> T): T {
var accumulator: T = initial
this.query(query.uri, query.projection.toTypedArray(), query.selection, query.selectionArgs.toTypedArray(), query.sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
accumulator = operation(accumulator, cursor)
}
}
return accumulator
}

View File

@ -13,4 +13,20 @@ class FakeContentResolver {
fun givenFile(uri: Uri) = every { instance.openInputStream(uri) }.delegateReturn()
fun givenUriResult(uri: Uri) = every { instance.query(uri, null, null, null, null) }.delegateReturn()
fun givenQueryResult(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?,
) = every {
instance.query(
uri,
projection,
selection,
selectionArgs,
sortOrder
)
}.delegateReturn()
}

View File

@ -24,4 +24,56 @@ class FakeCursor {
every { instance.getColumnIndex(columnName) } returns columnId
every { instance.getString(columnId) } returns content
}
}
interface CreateCursorScope {
fun addRow(vararg item: Pair<String, Any?>)
}
fun createCursor(creator: CreateCursorScope.() -> Unit): Cursor {
val content = mutableListOf<Map<String, Any?>>()
val scope = object : CreateCursorScope {
override fun addRow(vararg item: Pair<String, Any?>) {
content.add(item.toMap())
}
}
creator(scope)
return StubCursor(content)
}
private class StubCursor(private val content: List<Map<String, Any?>>) : Cursor by mockk() {
private val columnNames = content.map { it.keys }.flatten().distinct()
private var currentRowIndex = -1
override fun getColumnIndexOrThrow(columnName: String): Int {
return getColumnIndex(columnName).takeIf { it != -1 } ?: throw IllegalArgumentException(columnName)
}
override fun getColumnIndex(columnName: String) = columnNames.indexOf(columnName)
override fun moveToNext() = (currentRowIndex + 1 < content.size).also {
currentRowIndex += 1
}
override fun moveToFirst() = content.isNotEmpty()
override fun getCount() = content.size
override fun getString(index: Int): String? = content[currentRowIndex][columnNames[index]] as? String
override fun getInt(index: Int): Int {
return content[currentRowIndex][columnNames[index]] as? Int ?: throw IllegalArgumentException("Int can't be null")
}
override fun getLong(index: Int): Long {
return content[currentRowIndex][columnNames[index]] as? Long ?: throw IllegalArgumentException("Long can't be null")
}
override fun getColumnCount() = columnNames.size
override fun close() {
// do nothing
}
}

View File

@ -39,6 +39,8 @@ class ReducerTestScope<S, E>(
private val expectTestScope: ExpectTestScope
) : ExpectTestScope by expectTestScope, Reducer<S> {
private var invalidateCapturedState: Boolean = false
private val actionSideEffects = mutableMapOf<Action, () -> S>()
private var manualState: S? = null
private var capturedResult: S? = null
@ -47,6 +49,10 @@ class ReducerTestScope<S, E>(
override val coroutineScope = CoroutineScope(UnconfinedTestDispatcher())
override fun dispatch(action: Action) {
actionCaptures.add(action)
if (actionSideEffects.containsKey(action)) {
setState(actionSideEffects.getValue(action).invoke(), invalidateCapturedState = true)
}
}
override fun getState() = manualState ?: reducerFactory.initialState()
@ -54,15 +60,20 @@ class ReducerTestScope<S, E>(
private val reducer: Reducer<S> = reducerFactory.create(reducerScope)
override fun reduce(action: Action) = reducer.reduce(action).also {
capturedResult = it
capturedResult = if (invalidateCapturedState) manualState else it
}
fun setState(state: S) {
fun actionSideEffect(action: Action, handler: () -> S) {
actionSideEffects[action] = handler
}
fun setState(state: S, invalidateCapturedState: Boolean = false) {
manualState = state
this.invalidateCapturedState = invalidateCapturedState
}
fun setState(block: (S) -> S) {
manualState = block(reducerScope.getState())
setState(block(reducerScope.getState()))
}
fun assertInitialState(expected: S) {

View File

@ -1,37 +1,39 @@
package app.dapk.st.messenger.gallery
import android.content.ContentResolver
import android.content.ContentUris
import android.net.Uri
import android.provider.MediaStore.Images
import app.dapk.st.core.ContentResolverQuery
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.reduce
import app.dapk.st.core.withIoContext
class FetchMediaFoldersUseCase(
private val contentResolver: ContentResolver,
private val uriAvoidance: MediaUriAvoidance,
private val dispatchers: CoroutineDispatchers,
) {
suspend fun fetchFolders(): List<Folder> {
return dispatchers.withIoContext {
val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED)
val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?"
val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
val query = ContentResolverQuery(
uriAvoidance.externalContentUri,
listOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED),
"${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?",
listOf("%image/svg%"),
"${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
)
val folders = mutableMapOf<String, Folder>()
val contentUri = Images.Media.EXTERNAL_CONTENT_URI
contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
val thumbnail = ContentUris.withAppendedId(contentUri, rowId)
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]))
val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: ""
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]))
val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
contentResolver.reduce(query, mutableMapOf<String, Folder>()) { acc, cursor ->
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media._ID))
val thumbnail = uriAvoidance.uriAppender(query.uri, rowId)
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_ID))
val title = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.BUCKET_DISPLAY_NAME)) ?: ""
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED))
val folder = acc.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
folder.incrementItemCount()
}
}
folders.values.toList()
acc
}.values.toList()
}
}

View File

@ -1,15 +1,20 @@
package app.dapk.st.messenger.gallery
import android.content.ContentResolver
import android.content.ContentUris
import android.net.Uri
import android.provider.MediaStore
import app.dapk.st.core.ContentResolverQuery
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.reduce
import app.dapk.st.core.withIoContext
class FetchMediaUseCase(private val contentResolver: ContentResolver, private val dispatchers: CoroutineDispatchers) {
class FetchMediaUseCase(
private val contentResolver: ContentResolver,
private val uriAvoidance: MediaUriAvoidance,
private val dispatchers: CoroutineDispatchers
) {
private val projection = arrayOf(
private val projection = listOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.DATE_MODIFIED,
@ -22,34 +27,33 @@ class FetchMediaUseCase(private val contentResolver: ContentResolver, private va
private val selection = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?"
suspend fun getMediaInBucket(bucketId: String): List<Media> {
return dispatchers.withIoContext {
val media = mutableListOf<Media>()
val selectionArgs = arrayOf(bucketId, "%image/svg%")
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
val uri = ContentUris.withAppendedId(contentUri, rowId)
val query = ContentResolverQuery(
uri = uriAvoidance.externalContentUri,
projection = projection,
selection = selection,
selectionArgs = listOf(bucketId, "%image/svg%"),
sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC",
)
contentResolver.reduce(query) { cursor ->
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
val uri = uriAvoidance.uriAppender(query.uri, rowId)
val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
media.add(Media(rowId, uri, mimetype, width, height, size, date))
Media(rowId, uri, mimetype, width, height, size, date)
}
}
media
}
}
private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT
private fun getHeightColumn(orientation: Int) =
if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
}
data class Media(

View File

@ -1,7 +1,12 @@
package app.dapk.st.messenger.gallery
import android.content.ContentResolver
import app.dapk.st.core.*
import android.content.ContentUris
import android.provider.MediaStore
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.JobBag
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.messenger.gallery.state.ImageGalleryState
import app.dapk.st.messenger.gallery.state.imageGalleryReducer
@ -11,10 +16,14 @@ class ImageGalleryModule(
) : ProvidableModule {
fun imageGalleryState(roomName: String): ImageGalleryState = createStateViewModel {
val uriAvoidance = MediaUriAvoidance(
uriAppender = { uri, rowId -> ContentUris.withAppendedId(uri, rowId) },
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
)
imageGalleryReducer(
roomName = roomName,
FetchMediaFoldersUseCase(contentResolver, dispatchers),
FetchMediaUseCase(contentResolver, dispatchers),
FetchMediaFoldersUseCase(contentResolver, uriAvoidance, dispatchers),
FetchMediaUseCase(contentResolver, uriAvoidance, dispatchers),
JobBag(),
)
}

View File

@ -0,0 +1,8 @@
package app.dapk.st.messenger.gallery
import android.net.Uri
class MediaUriAvoidance(
val uriAppender: (Uri, Long) -> Uri,
val externalContentUri: Uri,
)

View File

@ -46,7 +46,6 @@ fun imageGalleryReducer(
parent = ImageGalleryPage.Routes.folders,
state = ImageGalleryPage.Files(Lce.Loading(), action.folder)
)
dispatch(PageAction.GoTo(page))
jobBag.replace(ImageGalleryPage.Files::class, coroutineScope.launch {
@ -58,7 +57,7 @@ fun imageGalleryReducer(
},
sideEffect(PageStateChange.ChangePage::class) { action, _ ->
jobBag.cancel(action.previous::class)
jobBag.cancel(action.previous.state::class)
},
)
}

View File

@ -0,0 +1,91 @@
package app.dapk.st.messenger.gallery
import android.net.Uri
import android.provider.MediaStore
import fake.CreateCursorScope
import fake.FakeContentResolver
import fake.FakeUri
import fake.createCursor
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private val A_EXTERNAL_CONTENT_URI = FakeUri()
private const val A_BUCKET_ID = "a-bucket-id"
private const val A_SECOND_BUCKET_ID = "another-bucket"
private const val A_DISPLAY_NAME = "a-bucket-name"
private const val A_DATE_MODIFIED = 5000L
class FetchMediaFoldersUseCaseTest {
private val fakeContentResolver = FakeContentResolver()
private val fakeUriAppender = FakeUriAppender()
private val uriAvoidance = MediaUriAvoidance(
uriAppender = fakeUriAppender,
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
)
private val useCase = FetchMediaFoldersUseCase(fakeContentResolver.instance, uriAvoidance, aCoroutineDispatchers())
@Test
fun `given cursor content, when get folder, then reads unique folders`() = runTest {
fakeContentResolver.givenFolderQuery().returns(createCursor {
addFolderRow(rowId = 1, A_BUCKET_ID)
addFolderRow(rowId = 2, A_BUCKET_ID)
addFolderRow(rowId = 3, A_SECOND_BUCKET_ID)
})
val result = useCase.fetchFolders()
result shouldBeEqualTo listOf(
Folder(
bucketId = A_BUCKET_ID,
title = A_DISPLAY_NAME,
thumbnail = fakeUriAppender.get(rowId = 1),
),
Folder(
bucketId = A_SECOND_BUCKET_ID,
title = A_DISPLAY_NAME,
thumbnail = fakeUriAppender.get(rowId = 3),
),
)
result[0].itemCount shouldBeEqualTo 2
result[1].itemCount shouldBeEqualTo 1
}
private fun CreateCursorScope.addFolderRow(rowId: Long, bucketId: String) {
addRow(
MediaStore.Images.Media._ID to rowId,
MediaStore.Images.Media.BUCKET_ID to bucketId,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME to A_DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
)
}
}
private fun FakeContentResolver.givenFolderQuery() = this.givenQueryResult(
A_EXTERNAL_CONTENT_URI.instance,
arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED
),
"${isNotPending()} AND ${MediaStore.Images.Media.BUCKET_ID} AND ${MediaStore.Images.Media.MIME_TYPE} NOT LIKE ?",
arrayOf("%image/svg%"),
"${MediaStore.Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${MediaStore.Images.Media.DATE_MODIFIED} DESC",
)
class FakeUriAppender : (Uri, Long) -> Uri {
private val uris = mutableMapOf<Long, FakeUri>()
override fun invoke(uri: Uri, rowId: Long): Uri {
val fakeUri = FakeUri()
uris[rowId] = fakeUri
return fakeUri.instance
}
fun get(rowId: Long) = uris[rowId]!!.instance
}

View File

@ -0,0 +1,107 @@
package app.dapk.st.messenger.gallery
import android.provider.MediaStore
import fake.FakeContentResolver
import fake.FakeUri
import fake.createCursor
import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private val A_EXTERNAL_CONTENT_URI = FakeUri()
private val ROW_URI = FakeUri()
private const val A_BUCKET_ID = "a-bucket-id"
private const val A_ROW_ID = 20L
private const val A_MIME_TYPE = "image/png"
private const val A_DATE_MODIFIED = 5000L
private const val A_SIZE = 1000L
private const val A_NORMAL_ORIENTATION = 0
private const val AN_INVERTED_ORIENTATION = 90
private const val A_WIDTH = 250
private const val A_HEIGHT = 750
class FetchMediaUseCaseTest {
private val fakeContentResolver = FakeContentResolver()
private val uriAvoidance = MediaUriAvoidance(
uriAppender = { _, _ -> ROW_URI.instance },
externalContentUri = A_EXTERNAL_CONTENT_URI.instance,
)
private val useCase = FetchMediaUseCase(fakeContentResolver.instance, uriAvoidance, aCoroutineDispatchers())
@Test
fun `given cursor content, when get media for bucket, then reads media`() = runTest {
fakeContentResolver.givenMediaQuery().returns(createCursor {
addRow(
MediaStore.Images.Media._ID to A_ROW_ID,
MediaStore.Images.Media.MIME_TYPE to A_MIME_TYPE,
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
MediaStore.Images.Media.ORIENTATION to A_NORMAL_ORIENTATION,
MediaStore.Images.Media.WIDTH to A_WIDTH,
MediaStore.Images.Media.HEIGHT to A_HEIGHT,
MediaStore.Images.Media.SIZE to A_SIZE,
)
})
val result = useCase.getMediaInBucket(A_BUCKET_ID)
result shouldBeEqualTo listOf(
Media(
id = A_ROW_ID,
uri = ROW_URI.instance,
mimeType = A_MIME_TYPE,
width = A_WIDTH,
height = A_HEIGHT,
size = A_SIZE,
dateModifiedEpochMillis = A_DATE_MODIFIED
)
)
}
@Test
fun `given cursor content with 90 degree orientation, when get media for bucket, then reads media with inverted width and height`() = runTest {
fakeContentResolver.givenMediaQuery().returns(createCursor {
addRow(
MediaStore.Images.Media._ID to A_ROW_ID,
MediaStore.Images.Media.MIME_TYPE to A_MIME_TYPE,
MediaStore.Images.Media.DATE_MODIFIED to A_DATE_MODIFIED,
MediaStore.Images.Media.ORIENTATION to AN_INVERTED_ORIENTATION,
MediaStore.Images.Media.WIDTH to A_WIDTH,
MediaStore.Images.Media.HEIGHT to A_HEIGHT,
MediaStore.Images.Media.SIZE to A_SIZE,
)
})
val result = useCase.getMediaInBucket(A_BUCKET_ID)
result shouldBeEqualTo listOf(
Media(
id = A_ROW_ID,
uri = ROW_URI.instance,
mimeType = A_MIME_TYPE,
width = A_HEIGHT,
height = A_WIDTH,
size = A_SIZE,
dateModifiedEpochMillis = A_DATE_MODIFIED
)
)
}
}
private fun FakeContentResolver.givenMediaQuery() = this.givenQueryResult(
A_EXTERNAL_CONTENT_URI.instance,
arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.ORIENTATION,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.SIZE
),
MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?",
arrayOf(A_BUCKET_ID, "%image/svg%"),
MediaStore.Images.Media.DATE_MODIFIED + " DESC",
)

View File

@ -1,54 +1,131 @@
package app.dapk.st.messenger.gallery.state
import android.net.Uri
import app.dapk.st.core.Lce
import app.dapk.st.core.page.PageAction
import app.dapk.st.core.page.PageContainer
import app.dapk.st.core.page.PageStateChange
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.messenger.gallery.FetchMediaFoldersUseCase
import app.dapk.st.messenger.gallery.FetchMediaUseCase
import app.dapk.st.core.page.PageContainer
import app.dapk.st.messenger.gallery.Folder
import app.dapk.st.messenger.gallery.Media
import app.dapk.state.Combined2
import fake.FakeJobBag
import fake.FakeUri
import io.mockk.coEvery
import io.mockk.mockk
import org.junit.Test
import test.assertOnlyDispatches
import test.delegateReturn
import test.expect
import test.testReducer
private const val A_ROOM_NAME = "a room name"
private val A_FOLDER = Folder(
bucketId = "a-bucket-id",
title = "a title",
thumbnail = FakeUri().instance,
)
private val A_MEDIA_RESULT = listOf(aMedia())
private val A_FOLDERS_RESULT = listOf(aFolder())
private val AN_INITIAL_FILES_PAGE = SpiderPage(
route = ImageGalleryPage.Routes.files,
label = "Send to $A_ROOM_NAME",
parent = ImageGalleryPage.Routes.folders,
state = ImageGalleryPage.Files(Lce.Loading(), A_FOLDER)
)
private val AN_INITIAL_FOLDERS_PAGE = SpiderPage(
route = ImageGalleryPage.Routes.folders,
label = "Send to $A_ROOM_NAME",
parent = null,
state = ImageGalleryPage.Folders(Lce.Loading())
)
class ImageGalleryReducerTest {
private val fakeJobBag = FakeJobBag()
private val fakeFetchMediaFoldersUseCase = FakeFetchMediaFoldersUseCase()
private val fakeFetchMediaUseCase = FakeFetchMediaUseCase()
private val runReducerTest = testReducer { _: (Unit) -> Unit ->
imageGalleryReducer(
A_ROOM_NAME,
FakeFetchMediaFoldersUseCase().instance,
FakeFetchMediaUseCase().instance,
fakeFetchMediaFoldersUseCase.instance,
fakeFetchMediaUseCase.instance,
fakeJobBag.instance,
)
}
@Test
fun `initial state is folders page`() = runReducerTest {
assertInitialState(
Combined2(
state1 = PageContainer(
SpiderPage(
route = ImageGalleryPage.Routes.folders,
label = "Send to $A_ROOM_NAME",
parent = null,
state = ImageGalleryPage.Folders(Lce.Loading())
)
),
state2 = Unit
)
assertInitialState(pageState(AN_INITIAL_FOLDERS_PAGE))
}
@Test
fun `when Visible, then updates Folders content`() = runReducerTest {
fakeJobBag.instance.expect { it.replace(ImageGalleryPage.Folders::class, any()) }
fakeFetchMediaFoldersUseCase.givenFolders().returns(A_FOLDERS_RESULT)
reduce(ImageGalleryActions.Visible)
assertOnlyDispatches(
PageStateChange.UpdatePage(AN_INITIAL_FOLDERS_PAGE.state.copy(content = Lce.Content(A_FOLDERS_RESULT)))
)
}
@Test
fun `when SelectFolder, then goes to Folder page and fetches content`() = runReducerTest {
fakeJobBag.instance.expect { it.replace(ImageGalleryPage.Files::class, any()) }
fakeFetchMediaUseCase.givenMedia(A_FOLDER.bucketId).returns(A_MEDIA_RESULT)
val goToFolderPage = PageAction.GoTo(AN_INITIAL_FILES_PAGE)
actionSideEffect(goToFolderPage) { pageState(goToFolderPage.page) }
reduce(ImageGalleryActions.SelectFolder(A_FOLDER))
assertOnlyDispatches(
goToFolderPage,
PageStateChange.UpdatePage(goToFolderPage.page.state.copy(content = Lce.Content(A_MEDIA_RESULT)))
)
}
@Test
fun `when ChangePage, then cancels previous page jobs`() = runReducerTest {
fakeJobBag.instance.expect { it.cancel(ImageGalleryPage.Folders::class) }
reduce(PageStateChange.ChangePage(previous = AN_INITIAL_FOLDERS_PAGE, newPage = AN_INITIAL_FILES_PAGE))
assertOnlyStateChange(pageState(AN_INITIAL_FILES_PAGE))
}
}
private fun <P> pageState(page: SpiderPage<out P>) = Combined2(PageContainer(page), Unit)
class FakeFetchMediaFoldersUseCase {
val instance = mockk<FetchMediaFoldersUseCase>()
fun givenFolders() = coEvery { instance.fetchFolders() }.delegateReturn()
}
class FakeFetchMediaUseCase {
val instance = mockk<FetchMediaUseCase>()
fun givenMedia(bucketId: String) = coEvery { instance.getMediaInBucket(bucketId) }.delegateReturn()
}
fun aMedia(
id: Long = 1L,
uri: Uri = FakeUri().instance,
mimeType: String = "image/png",
width: Int = 100,
height: Int = 250,
size: Long = 1000L,
dateModifiedEpochMillis: Long = 5000L,
) = Media(id, uri, mimeType, width, height, size, dateModifiedEpochMillis)
fun aFolder(
bucketId: String = "a-bucket-id",
title: String = "a title",
thumbnail: Uri = FakeUri().instance,
) = Folder(bucketId, title, thumbnail)