Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/4674
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bugfix: Fix spaces search on Kiteworks accounts

All matching personal spaces have been included in the spaces search filter for multi-personal accounts.

https://github.com/owncloud/android/issues/4674
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,19 @@ class SpacesListFragment :
collectLatestLifecycleFlow(spacesListViewModel.spacesList) { uiState ->
var spacesToListFiltered: List<OCSpace>
if (uiState.searchFilter != "") {
spacesToListFiltered =
uiState.spaces.filter { it.name.lowercase().contains(uiState.searchFilter.lowercase()) && !it.isPersonal &&
val searchFilter = uiState.searchFilter.lowercase()
spacesToListFiltered = if (isMultiPersonal) {
uiState.spaces.filter { it.name.lowercase().contains(searchFilter) && shouldShowDisabledSpace(it) }
} else {
uiState.spaces.filter { it.name.lowercase().contains(searchFilter) && !it.isPersonal &&
shouldShowDisabledSpace(it) }
val personalSpace = uiState.spaces.find { it.isPersonal }
personalSpace?.let {
spacesToListFiltered = spacesToListFiltered.toMutableList().apply {
add(0, personalSpace)
}
if (!isMultiPersonal) {
val personalSpace = uiState.spaces.find { it.isPersonal }
personalSpace?.let {
spacesToListFiltered = spacesToListFiltered.toMutableList().apply {
add(0, personalSpace)
}
}
}
showOrHideEmptyView(spacesToListFiltered)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage: 0% on this branch — below the 85% gate. I searched the repo (no local checkout, used GitHub code search) and confirmed there is no SpacesListFragmentTest, no SpacesListViewModelTest, and no reference to searchFilter/isMultiPersonal anywhere in src/test or src/androidTest. The PR description only lists manual QA steps.

Root cause: the new isMultiPersonal branch (and the personal-space pinning it now skips) lives inline inside a Fragment lambda, and this codebase has no precedent for unit-testing Fragments directly (Robolectric isn't wired into owncloudApp; the existing pattern is plain JUnit4 + MockK against ViewModels, see owncloudApp/src/test/java/.../viewmodels/*ViewModelTest.kt).

Suggested fix — extract to a pure, testable function so this exact branch can be unit tested without Robolectric:

object SpacesFilter {
    fun filterSpaces(
        spaces: List<OCSpace>,
        searchFilter: String,
        isMultiPersonal: Boolean,
        shouldShowDisabledSpace: (OCSpace) -> Boolean,
    ): List<OCSpace> {
        if (searchFilter.isEmpty()) return spaces.filter { shouldShowDisabledSpace(it) }
        val lowerFilter = searchFilter.lowercase()
        var filtered = if (isMultiPersonal) {
            spaces.filter { it.name.lowercase().contains(lowerFilter) && shouldShowDisabledSpace(it) }
        } else {
            spaces.filter { it.name.lowercase().contains(lowerFilter) && !it.isPersonal && shouldShowDisabledSpace(it) }
        }
        if (!isMultiPersonal) {
            spaces.find { it.isPersonal }?.let { personalSpace ->
                filtered = filtered.toMutableList().apply { add(0, personalSpace) }
            }
        }
        return filtered
    }
}

SpacesListFragment would then call SpacesFilter.filterSpaces(uiState.spaces, uiState.searchFilter, isMultiPersonal) { shouldShowDisabledSpace(it) }. Proposed test (owncloudApp/src/test/java/com/owncloud/android/presentation/spaces/SpacesFilterTest.kt), covering multi-personal multi-match, single-personal pinning, no-match, and empty-filter cases:

class SpacesFilterTest {
    private fun space(name: String, isPersonal: Boolean, isDisabled: Boolean = false): OCSpace =
        mockk<OCSpace>(relaxed = true).also {
            every { it.name } returns name
            every { it.isPersonal } returns isPersonal
            every { it.isDisabled } returns isDisabled
        }
    private val showAll: (OCSpace) -> Boolean = { true }

    @Test fun `multi-personal with filter matches multiple personal spaces`() {
        val a = space("Personal Alpha", true); val b = space("Personal Beta", true)
        val project = space("Marketing Project", false)
        assertEquals(listOf(a, b), SpacesFilter.filterSpaces(listOf(a, b, project), "personal", true, showAll))
    }

    @Test fun `single-personal with filter still pins one personal space at top`() {
        val personal = space("Personal", true); val match = space("Engineering Docs", false); val noMatch = space("Random", false)
        assertEquals(listOf(personal, match), SpacesFilter.filterSpaces(listOf(personal, match, noMatch), "eng", false, showAll))
    }

    @Test fun `no matches returns empty list`() {
        val personal = space("Personal", true); val project = space("Marketing", false)
        assertEquals(emptyList<OCSpace>(), SpacesFilter.filterSpaces(listOf(personal, project), "zzz-no-match", false, showAll))
    }

    @Test fun `empty filter returns all spaces passing disabled-space check`() {
        val personal = space("Personal", true); val project = space("Marketing", false); val disabled = space("Archived", false, true)
        assertEquals(listOf(personal, project), SpacesFilter.filterSpaces(listOf(personal, project, disabled), "", true) { !it.isDisabled })
    }
}

This also resolves the MVVM nit below: filtering is business logic and reads better as a pure function than inline in the Fragment's flow collector.


Generated by Claude Code

Expand Down