Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
🤖 Update search logic to include blog name and status
This task is to update the "My domains" search logic to match on `blogName` and `status`.


Discussion hints:
nan

Pull request: Update search logic to include blog name and status
Fixes #19484

This PR includes additional "blog name" and "status" properties to filter my domains list by. In addition, I moved the filter logic from the UI layer to the ViewModel to make it more efficient. The previous implementation caused unnecessary invoking of filtering while scrolling the list. Composables should just render data to avoid performance issues caused by repeated calculations while recomposition.

To test:
- Make sure you enabled `domain_management` feature flag
- If your account doesn't have a list of domains you can use a mocked list via sandboxing.  (Details here - p1692765970283879-slack-C05NS0YV7HS) Unfortunately, you can only search by domain and status using the mocked domains.
- Go to Me -> Domains
- [ ] Make sure you can search locally for domains by blog name and domain status

## Regression Notes
1. Potential unintended areas of impact
All Domains screen

2. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual testing

3. What automated tests I added (or what prevented me from doing so)
N/A

PR submission checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:

- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] Talkback.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] Large and small screen sizes. (Tablet and smaller phones)
- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.domains.management.DomainManagementViewModelTest" --tests "org.wordpress.android.ui.domains.management.util.DomainLocalSearchEngineTest" --tests "org.wordpress.android.ui.domains.management.PopulatedListTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
index 8d39603ac8cf..5fec8c1a1a04 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementActivity.kt
@@ -22,10 +22,11 @@ class DomainManagementActivity : AppCompatActivity() {
                 val uiState by viewModel.uiStateFlow.collectAsState()
                 MyDomainsScreen(
                     uiState = uiState,
+                    onSearchQueryChanged = viewModel::onSearchQueryChanged,
                     onDomainTapped = viewModel::onDomainTapped,
                     onAddDomainTapped = viewModel::onAddDomainClicked,
                     onFindDomainTapped = viewModel::onAddDomainClicked,
-                    onBackTapped = viewModel::onBackTapped
+                    onBackTapped = viewModel::onBackTapped,
                 )
             }
         }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
index ba4617837131..9654e86fdb24 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/DomainManagementViewModel.kt
@@ -5,18 +5,22 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 import org.wordpress.android.R
 import org.wordpress.android.analytics.AnalyticsTracker.Stat
 import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain
 import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainStatus
 import org.wordpress.android.fluxc.network.rest.wpcom.site.StatusType
 import org.wordpress.android.modules.UI_THREAD
+import org.wordpress.android.ui.domains.management.util.DomainLocalSearchEngine
 import org.wordpress.android.ui.domains.usecases.AllDomains
 import org.wordpress.android.ui.domains.usecases.FetchAllDomainsUseCase
 import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
@@ -32,6 +36,7 @@ class DomainManagementViewModel @Inject constructor(
     @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
     private val analyticsTracker: AnalyticsTrackerWrapper,
     private val fetchAllDomainsUseCase: FetchAllDomainsUseCase,
+    private val domainLocalSearchEngine: DomainLocalSearchEngine,
 ) : ScopedViewModel(mainDispatcher) {
     private val _actionEvents = MutableSharedFlow<ActionEvent>()
     val actionEvents: Flow<ActionEvent> = _actionEvents
@@ -39,19 +44,45 @@ class DomainManagementViewModel @Inject constructor(
     private val _uiStateFlow: MutableStateFlow<UiState> = MutableStateFlow(UiState.PopulatedList.Initial)
     val uiStateFlow = _uiStateFlow.asStateFlow()
 
+    private val searchQuery = MutableStateFlow("")
+
     init {
         analyticsTracker.track(Stat.DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_SHOWN)
         launch {
-            fetchAllDomainsUseCase.execute().let {
-                _uiStateFlow.value = when (it) {
-                    AllDomains.Empty -> UiState.Empty
-                    AllDomains.Error -> UiState.Error
-                    is AllDomains.Success -> UiState.PopulatedList.Loaded(it.domains)
-                }
+            fetchAllDomains()
+        }
+        launch {
+            searchQuery
+                .onEach(::filterDomains)
+                .launchIn(viewModelScope)
+        }
+    }
+
+    private suspend fun fetchAllDomains() {
+        fetchAllDomainsUseCase.execute().let {
+            _uiStateFlow.value = when (it) {
+                AllDomains.Empty -> UiState.Empty
+                AllDomains.Error -> UiState.Error
+                is AllDomains.Success -> UiState.PopulatedList.Loaded.Complete(it.domains)
             }
         }
     }
 
+    private fun filterDomains(query: String) {
+        val domains = when (val state = _uiStateFlow.value) {
+            is UiState.PopulatedList.Loaded -> state.allDomains
+            else -> return
+        }
+        _uiStateFlow.value = if (query.isBlank()) {
+            UiState.PopulatedList.Loaded.Complete(allDomains = domains)
+        } else {
+            UiState.PopulatedList.Loaded.Filtered(
+                filtered = domainLocalSearchEngine.filter(domains, query),
+                allDomains = domains
+            )
+        }
+    }
+
     fun onDomainTapped(detailUrl: String) {
         analyticsTracker.track(Stat.DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED)
         launch {
@@ -72,6 +103,12 @@ class DomainManagementViewModel @Inject constructor(
         }
     }
 
+    fun onSearchQueryChanged(query: String) {
+        launch {
+            searchQuery.emit(query.trim())
+        }
+    }
+
     sealed class ActionEvent {
         data class DomainTapped(val detailUrl: String): ActionEvent()
         object AddDomainTapped: ActionEvent()
@@ -81,12 +118,15 @@ class DomainManagementViewModel @Inject constructor(
     sealed class UiState {
         sealed class PopulatedList: UiState() {
             object Initial: PopulatedList()
-            data class Loaded(val domains: List<AllDomainsDomain>): PopulatedList()
-
-            fun filter(query: String): PopulatedList = if (this is Loaded) {
-                copy(domains = domains.filter { domain -> domain.matches(query) })
-            } else {
-                this
+            sealed class Loaded : PopulatedList() {
+                abstract val allDomains: List<AllDomainsDomain>
+                data class Complete(
+                    override val allDomains: List<AllDomainsDomain>
+                ): Loaded()
+                data class Filtered(
+                    override val allDomains: List<AllDomainsDomain>,
+                    val filtered: List<AllDomainsDomain>
+                ) : Loaded()
             }
         }
         object Empty: UiState()
@@ -169,7 +209,3 @@ val DomainStatus.isBold
         null -> true
         else -> false
     }
-
-private fun AllDomainsDomain.matches(query: String) =
-    domain?.contains(query, true) ?: false
-            || siteSlug?.contains(query, true) ?: false
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
index 7a76f20c68c2..1a03e5d279d6 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/MyDomainsScreen.kt
@@ -49,6 +49,7 @@ import org.wordpress.android.ui.domains.management.composable.DomainsSearchTextF
 @Composable
 fun MyDomainsScreen(
     uiState: UiState,
+    onSearchQueryChanged: (String) -> Unit,
     onDomainTapped: (detailUrl: String) -> Unit,
     onAddDomainTapped: () -> Unit,
     onFindDomainTapped: () -> Unit,
@@ -87,12 +88,15 @@ fun MyDomainsScreen(
             MyDomainsSearchInput(
                 elevation.value,
                 queryString = queryString,
-                onQueryStringChanged = { queryString = it },
+                onQueryStringChanged = {
+                    queryString = it
+                    onSearchQueryChanged(it)
+                },
                 enabled = uiState is PopulatedList.Loaded,
             )
             when (uiState) {
                 is PopulatedList -> MyDomainsList(
-                    listUiState = uiState.filter(queryString),
+                    listUiState = uiState,
                     listState = listState,
                     onDomainTapped,
                 )
@@ -211,8 +215,14 @@ fun MyDomainsList(
                     }
                 }
 
-            is PopulatedList.Loaded -> {
-                items(items = listUiState.domains) {
+            is PopulatedList.Loaded.Complete -> {
+                items(items = listUiState.allDomains) {
+                    DomainListCard(uiState = DomainCardUiState.fromDomain(domain = it), onDomainTapped)
+                }
+            }
+
+            is PopulatedList.Loaded.Filtered -> {
+                items(items = listUiState.filtered) {
                     DomainListCard(uiState = DomainCardUiState.fromDomain(domain = it), onDomainTapped)
                 }
             }
@@ -227,6 +237,7 @@ fun PreviewMyDomainsScreen() {
     M3Theme {
         MyDomainsScreen(
             uiState = PopulatedList.Initial,
+            onSearchQueryChanged = {},
             onAddDomainTapped = {},
             onDomainTapped = {},
             onFindDomainTapped = {},
@@ -242,6 +253,7 @@ fun PreviewMyDomainsScreenError() {
     M3Theme {
         MyDomainsScreen(
             uiState = Error,
+            onSearchQueryChanged = {},
             onAddDomainTapped = {},
             onDomainTapped = {},
             onFindDomainTapped = {},
@@ -257,6 +269,7 @@ fun PreviewMyDomainsScreenEmpty() {
     M3Theme {
         MyDomainsScreen(
             uiState = Empty,
+            onSearchQueryChanged = {},
             onAddDomainTapped = {},
             onDomainTapped = {},
             onFindDomainTapped = {},
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/NewDomainSearchActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/NewDomainSearchActivity.kt
index ceab64625fb9..07d39004e059 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/NewDomainSearchActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/newdomainsearch/NewDomainSearchActivity.kt
@@ -32,7 +32,7 @@ class NewDomainSearchActivity : AppCompatActivity() {
                     onSearchQueryChanged = viewModel::onSearchQueryChanged,
                     onTransferDomainClicked = viewModel::onTransferDomainClicked,
                     onDomainTapped = viewModel::onDomainTapped,
-                    onBackPressed = viewModel::onBackPressed
+                    onBackPressed = viewModel::onBackPressed,
                 )
             }
         }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngine.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngine.kt
new file mode 100644
index 000000000000..3337dbb0d4c2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngine.kt
@@ -0,0 +1,15 @@
+package org.wordpress.android.ui.domains.management.util
+
+import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain
+import javax.inject.Inject
+
+class DomainLocalSearchEngine @Inject constructor() {
+    fun filter(domains: List<AllDomainsDomain>, query: String): List<AllDomainsDomain> =
+        domains.filter { it.matches(query) }
+
+    private fun AllDomainsDomain.matches(query: String) =
+        domain?.contains(query, true) ?: false
+                || siteSlug?.contains(query, true) ?: false
+                || blogName?.contains(query, true) ?: false
+                || domainStatus?.status?.contains(query, true) ?: false
+}


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
index 716480b54830..f7014f808bbe 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/DomainManagementViewModelTest.kt
@@ -10,11 +10,15 @@ import org.junit.Test
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoInteractions
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED
 import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_SHOWN
+import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain
 import org.wordpress.android.ui.domains.management.DomainManagementViewModel.ActionEvent
+import org.wordpress.android.ui.domains.management.DomainManagementViewModel.UiState
+import org.wordpress.android.ui.domains.management.util.DomainLocalSearchEngine
 import org.wordpress.android.ui.domains.usecases.AllDomains
 import org.wordpress.android.ui.domains.usecases.FetchAllDomainsUseCase
 import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
@@ -27,22 +31,25 @@ class DomainManagementViewModelTest : BaseUnitTest() {
     @Mock
     lateinit var useCase: FetchAllDomainsUseCase
 
+    @Mock
+    lateinit var domainLocalSearchEngine: DomainLocalSearchEngine
+
     private lateinit var viewModel: DomainManagementViewModel
 
     @Before
-    fun setUp() = test {
+    fun setUp() {
         MockitoAnnotations.openMocks(this)
-        whenever(useCase.execute()).thenReturn(AllDomains.Empty)
-        viewModel = DomainManagementViewModel(testDispatcher(), analyticsTracker, useCase)
     }
 
     @Test
-    fun `WHEN ViewModel initialized THEN track DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_SHOWN event`() {
+    fun `WHEN ViewModel initialized THEN track DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_SHOWN event`() = test {
+        initializeViewModel()
         verify(analyticsTracker).track(DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_SHOWN)
     }
 
     @Test
-    fun `WHEN a domain is tapped THEN track DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED event`() {
+    fun `WHEN a domain is tapped THEN track DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED event`() = test {
+        initializeViewModel()
         viewModel.onDomainTapped(testDomain)
         verify(analyticsTracker).track(DOMAIN_MANAGEMENT_MY_DOMAINS_SCREEN_DOMAIN_TAPPED)
     }
@@ -62,15 +69,84 @@ class DomainManagementViewModelTest : BaseUnitTest() {
             assertThat(events.last()).isEqualTo(ActionEvent.NavigateBackTapped)
         }
 
-    private fun testWithActionEvents(block: suspend TestScope.(events: List<ActionEvent>) -> Unit) =
-        test {
-            val actionEvents = mutableListOf<ActionEvent>()
-            val job = launch { viewModel.actionEvents.toList(actionEvents) }
+    @Test
+    fun `GIVEN empty domains result WHEN initialized THEN send empty UI state`() = testWithState { state ->
+        assertThat(state.last()).isEqualTo(UiState.Empty)
+    }
+
+    @Test
+    fun `GIVEN error domains result WHEN initialized THEN send error UI state`() =
+        testWithState(initialAllDomainsFetchResult = AllDomains.Error) { state ->
+            assertThat(state.last()).isEqualTo(UiState.Error)
+        }
+
+    @Test
+    fun `GIVEN successful domains result WHEN initialized THEN send populated list loaded complete UI state`() {
+        val domains = listOf(AllDomainsDomain(), AllDomainsDomain())
+        testWithState(initialAllDomainsFetchResult = AllDomains.Success(domains)) { state ->
+            assertThat(state.last()).isEqualTo(UiState.PopulatedList.Loaded.Complete(domains))
+        }
+    }
+
+    @Suppress("MaxLineLength")
+    @Test
+    fun `GIVEN successful domains result and query is not blank WHEN search query changed THEN return populated loaded filtered UI state`() {
+        val domains = listOf(AllDomainsDomain(), AllDomainsDomain())
+        val filteredDomains = listOf(AllDomainsDomain())
+        whenever(domainLocalSearchEngine.filter(domains, query = "query")).thenReturn(filteredDomains)
+
+        testWithState(initialAllDomainsFetchResult = AllDomains.Success(domains)) { state ->
+            viewModel.onSearchQueryChanged("query")
+            advanceUntilIdle()
+            assertThat(state.last()).isEqualTo(
+                UiState.PopulatedList.Loaded.Filtered(allDomains = domains, filtered = filteredDomains)
+            )
+        }
+    }
 
-            block(actionEvents)
+    @Suppress("MaxLineLength")
+    @Test
+    fun `GIVEN successful domains result and blank query WHEN search query changed THEN return populated loaded complete UI state`() {
+        val domains = listOf(AllDomainsDomain(), AllDomainsDomain())
 
-            job.cancel()
+        testWithState(initialAllDomainsFetchResult = AllDomains.Success(domains)) { state ->
+            viewModel.onSearchQueryChanged(" ")
+            advanceUntilIdle()
+            assertThat(state.last()).isEqualTo(UiState.PopulatedList.Loaded.Complete(allDomains = domains))
+            verifyNoInteractions(domainLocalSearchEngine)
         }
+    }
+
+    private fun testWithActionEvents(
+        initialAllDomainsFetchResult: AllDomains = AllDomains.Empty,
+        block: suspend TestScope.(events: List<ActionEvent>) -> Unit
+    ) = test {
+        initializeViewModel(initialAllDomainsFetchResult)
+        val actionEvents = mutableListOf<ActionEvent>()
+        val job = launch { viewModel.actionEvents.toList(actionEvents) }
+
+        block(actionEvents)
+
+        job.cancel()
+    }
+
+    private fun testWithState(
+        initialAllDomainsFetchResult: AllDomains = AllDomains.Empty,
+        block: suspend TestScope.(states: List<UiState>) -> Unit
+    ) = test {
+        initializeViewModel(initialAllDomainsFetchResult)
+        val states = mutableListOf<UiState>()
+        val job = launch { viewModel.uiStateFlow.toList(states) }
+
+        block(states)
+
+        job.cancel()
+    }
+
+    private suspend fun initializeViewModel(initialAllDomainsFetchResult: AllDomains = AllDomains.Empty) {
+        whenever(useCase.execute()).thenReturn(initialAllDomainsFetchResult)
+        viewModel = DomainManagementViewModel(testDispatcher(), analyticsTracker, useCase, domainLocalSearchEngine)
+    }
 
     companion object {
         private const val testDomain = "domain"
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/PopulatedListTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/PopulatedListTest.kt
deleted file mode 100644
index e03a3140e9f2..000000000000
--- a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/PopulatedListTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package org.wordpress.android.ui.domains.management
-
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-import org.wordpress.android.BaseUnitTest
-import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain
-import org.wordpress.android.ui.domains.management.DomainManagementViewModel.UiState.PopulatedList
-
-@ExperimentalCoroutinesApi
-class PopulatedListTest : BaseUnitTest() {
-    private val fakeDomainFoo = AllDomainsDomain(domain = "foo.com", siteSlug = "Foo Warehouse")
-    private val fakeDomainBar = AllDomainsDomain(domain = "bar.com", siteSlug = "Chocolate Bar")
-    private val fakeDomainBah = AllDomainsDomain(domain = "bah.com", siteSlug = "Black sheep")
-
-    private val uiState = PopulatedList.Loaded(
-        domains = listOf(fakeDomainFoo, fakeDomainBar, fakeDomainBah)
-    )
-
-    // PopulatedList
-
-    @Test
-    fun `PopulatedList filter matches correctly by domain`() {
-        // Given
-        val query = "foo"
-
-        // When
-        val result = uiState.filter(query)
-
-        // Then
-        assertThat(result).isEqualTo(PopulatedList.Loaded(listOf(fakeDomainFoo)))
-    }
-
-    @Test
-    fun `PopulatedList filter matches correctly by site slug`() {
-        // Given
-        val query = "chocolate"
-
-        // When
-        val result = uiState.filter(query)
-
-        // Then
-        assertThat(result).isEqualTo(PopulatedList.Loaded(listOf(fakeDomainBar)))
-    }
-
-    @Test
-    fun `PopulatedList filter matches all for the empty string`() {
-        // Given
-        val query = ""
-
-        // When
-        val result = uiState.filter(query)
-
-        // Then
-        assertThat(result).isEqualTo(uiState)
-    }
-}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngineTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngineTest.kt
new file mode 100644
index 000000000000..93317d5ca245
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/domains/management/util/DomainLocalSearchEngineTest.kt
@@ -0,0 +1,90 @@
+package org.wordpress.android.ui.domains.management.util
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain
+import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainStatus
+
+class DomainLocalSearchEngineTest {
+    private val fakeDomainFoo = AllDomainsDomain(
+        domain = "foo.com",
+        siteSlug = "Foo Warehouse",
+        blogName = "Awesome blog",
+        domainStatus = DomainStatus(status = "Active")
+    )
+    private val fakeDomainBar = AllDomainsDomain(
+        domain = "bar.com",
+        siteSlug = "Chocolate Bar",
+        blogName = "Sweet blog",
+        domainStatus = DomainStatus(status = "Activating")
+    )
+    private val fakeDomainBah = AllDomainsDomain(
+        domain = "bah.com",
+        siteSlug = "Black sheep",
+        blogName = "Unique blog",
+        domainStatus = DomainStatus(status = "Expired")
+    )
+    private val allDomains = listOf(fakeDomainFoo, fakeDomainBar, fakeDomainBah)
+
+    private val engine = DomainLocalSearchEngine()
+
+    @Test
+    fun `PopulatedList filter matches correctly by domain`() {
+        // Given
+        val query = "foo"
+
+        // When
+        val result = engine.filter(allDomains, query)
+
+        // Then
+        assertThat(result).isEqualTo(listOf(fakeDomainFoo))
+    }
+
+    @Test
+    fun `PopulatedList filter matches correctly by site slug`() {
+        // Given
+        val query = "chocolate"
+
+        // When
+        val result = engine.filter(allDomains, query)
+
+        // Then
+        assertThat(result).isEqualTo(listOf(fakeDomainBar))
+    }
+
+    @Test
+    fun `PopulatedList filter matches correctly by domain status`() {
+        // Given
+        val query = "active"
+
+        // When
+        val result = engine.filter(allDomains, query)
+
+        // Then
+        assertThat(result).isEqualTo(listOf(fakeDomainFoo))
+    }
+
+    @Test
+    fun `PopulatedList filter matches correctly by blog name`() {
+        // Given
+        val query = "unique"
+
+        // When
+        val result = engine.filter(allDomains, query)
+
+        // Then
+        assertThat(result).isEqualTo(listOf(fakeDomainBah))
+    }
+
+    @Test
+    fun `PopulatedList filter matches all for the empty string`() {
+        // Given
+        val query = ""
+
+        // When
+        val result = engine.filter(allDomains, query)
+
+        // Then
+        assertThat(result).isEqualTo(allDomains)
+    }
+}