Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Issue #10335: Add TargetTab helper for observing specific tabs.
Browse files Browse the repository at this point in the history
  • Loading branch information
pocmo committed Jun 2, 2021
1 parent 4960751 commit f8341e3
Show file tree
Hide file tree
Showing 4 changed files with 417 additions and 0 deletions.
16 changes: 16 additions & 0 deletions components/browser/state/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ android {
defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
Expand All @@ -21,6 +23,15 @@ android {
}
}

buildFeatures {
compose true
}

composeOptions {
kotlinCompilerVersion = Versions.kotlin
kotlinCompilerExtensionVersion = Versions.compose_version
}

packagingOptions {
exclude 'META-INF/proguard/androidx-annotations.pro'
}
Expand All @@ -38,6 +49,7 @@ dependencies {
api project(':concept-fetch')

implementation Dependencies.androidx_browser
implementation Dependencies.androidx_compose_ui
implementation Dependencies.kotlin_coroutines
implementation Dependencies.kotlin_stdlib

Expand All @@ -47,6 +59,10 @@ dependencies {
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_robolectric

androidTestImplementation Dependencies.androidx_test_junit
androidTestImplementation Dependencies.androidx_compose_ui_test_manifest
androidTestImplementation Dependencies.androidx_compose_ui_test
}

apply from: '../../../publish.gradle'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.state.helper

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* On-device tests for [TargetTab].
*/
@RunWith(AndroidJUnit4::class)
class OnDeviceTargetTabTest {
@get:Rule
val rule = createComposeRule()

@Test
fun observingSelectedTab() {
val store = BrowserStore()

val target = TargetTab.Selected
var observedTabId: String? = null

rule.setContent {
val state = target.observeAsStateFrom(
store = store,
observe = { tab -> tab?.id }
)
observedTabId = state.value?.id
}

assertNull(observedTabId)

store.dispatchBlockingOnIdle(
TabListAction.AddTabAction(createTab("https://www.mozilla.org", id = "mozilla"))
)

rule.runOnIdle {
assertEquals("mozilla", observedTabId)
}

store.dispatchBlockingOnIdle(
TabListAction.AddTabAction(createTab("https://example.org", id = "example"))
)

rule.runOnIdle {
assertEquals("mozilla", observedTabId)
}

store.dispatchBlockingOnIdle(
TabListAction.SelectTabAction("example")
)

rule.runOnIdle {
assertEquals("example", observedTabId)
}

store.dispatchBlockingOnIdle(
TabListAction.RemoveTabAction("example")
)

rule.runOnIdle {
assertEquals("mozilla", observedTabId)
}

store.dispatchBlockingOnIdle(TabListAction.RemoveAllTabsAction)

rule.runOnIdle {
assertNull(observedTabId)
}
}

@Test
fun observingPinnedTab() {
val store = BrowserStore(
initialState = BrowserState(
tabs = listOf(
createTab("https://www.mozilla.org", id = "mozilla"),
createTab("https://www.example.org", id = "example")
),
selectedTabId = "mozilla"
)
)

val target = TargetTab.Pinned("mozilla")
var observedTabId: String? = null

rule.setContent {
val state = target.observeAsStateFrom(
store = store,
observe = { tab -> tab?.id }
)
observedTabId = state.value?.id
}

assertEquals("mozilla", observedTabId)

store.dispatchBlockingOnIdle(TabListAction.SelectTabAction("example"))

rule.runOnIdle {
assertEquals("mozilla", observedTabId)
}

store.dispatchBlockingOnIdle(TabListAction.RemoveTabAction("mozilla"))

rule.runOnIdle {
assertNull(observedTabId)
}
}

@Test
fun observingCustomTab() {
val store = BrowserStore(
initialState = BrowserState(
tabs = listOf(
createTab("https://www.mozilla.org", id = "mozilla"),
createTab("https://www.example.org", id = "example")
),
customTabs = listOf(
createCustomTab("https://www.reddit.com/r/firefox/", id = "reddit")
),
selectedTabId = "mozilla"
)
)

val target = TargetTab.Custom("reddit")

var observedTabId: String? = null

rule.setContent {
val state = target.observeAsStateFrom(
store = store,
observe = { tab -> tab?.id }
)
observedTabId = state.value?.id
}

assertEquals("reddit", observedTabId)

store.dispatchBlockingOnIdle(TabListAction.SelectTabAction("example"))

rule.runOnIdle {
assertEquals("reddit", observedTabId)
}

store.dispatchBlockingOnIdle(TabListAction.RemoveTabAction("mozilla"))

rule.runOnIdle {
assertEquals("reddit", observedTabId)
}

store.dispatchBlockingOnIdle(CustomTabListAction.RemoveCustomTabAction("reddit"))

rule.runOnIdle {
assertNull(observedTabId)
}
}

private fun BrowserStore.dispatchBlockingOnIdle(action: BrowserAction) {
rule.runOnIdle {
val job = dispatch(action)
runBlocking { job.join() }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.state.helper

import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.ext.observeAsState

/**
* Helper for allowing a component consumer to specify which tab a component should target (e.g.
* the selected tab, a specific pinned tab or a custom tab). Additional helper methods make it
* easier to lookup the current state of the tab or observe changes.
*/
sealed class TargetTab {
/**
* Looks up this target in the given [BrowserStore] and returns the matching [SessionState] if
* available. Otherwise returns `null`.
*/
fun lookupIn(store: BrowserStore): SessionState? = lookupIn(store.state)

/**
* Looks up this target in the given [BrowserState] and returns the matching [SessionState] if
* available. Otherwise returns `null`.
*/
abstract fun lookupIn(state: BrowserState): SessionState?

/**
* Observes this target and represents the mapped state (using [map]) via [State].
*
* Everytime the [Store] state changes and the result of the [observe] function changes for this
* state, the returned [State] will be updated causing recomposition of every [State.value] usage.
*
* The [Store] observer will automatically be removed when this composable disposes or the current
* [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
*/
@Composable
fun <R> observeAsStateFrom(
store: BrowserStore,
observe: (SessionState?) -> R
): State<SessionState?> {
return store.observeAsState(
map = { state -> lookupIn(state) },
observe = { state -> observe(lookupIn(state)) }
)
}

/**
* Targets the selected tab.
*/
object Selected : TargetTab() {
override fun lookupIn(state: BrowserState): SessionState? {
return state.selectedTab
}
}

/**
* Targets a specific tab by its [tabId].
*/
class Pinned(val tabId: String) : TargetTab() {
override fun lookupIn(state: BrowserState): SessionState? {
return state.findTab(tabId)
}
}

/**
* Targets a specific custom tab by its [customTabId].
*/
class Custom(val customTabId: String) : TargetTab() {
override fun lookupIn(state: BrowserState): SessionState? {
return state.findCustomTab(customTabId)
}
}
}
Loading

0 comments on commit f8341e3

Please sign in to comment.