Skip to content

Commit 863c2a4

Browse files
committed
test(RevenueCatUI): tab state read and write end-to-end
1 parent 7be5099 commit 863c2a4

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

  • ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/components/tabs
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.revenuecat.purchases.ui.revenuecatui.components.tabs
2+
3+
import androidx.compose.ui.graphics.Color
4+
import androidx.compose.ui.graphics.toArgb
5+
import androidx.compose.ui.test.assertHasClickAction
6+
import androidx.compose.ui.test.junit4.createComposeRule
7+
import androidx.compose.ui.test.onNodeWithText
8+
import androidx.compose.ui.test.performClick
9+
import androidx.test.ext.junit.runners.AndroidJUnit4
10+
import com.revenuecat.purchases.InternalRevenueCatAPI
11+
import com.revenuecat.purchases.Offering
12+
import com.revenuecat.purchases.paywalls.components.PartialTextComponent
13+
import com.revenuecat.purchases.paywalls.components.StackComponent
14+
import com.revenuecat.purchases.paywalls.components.TabControlButtonComponent
15+
import com.revenuecat.purchases.paywalls.components.TabControlComponent
16+
import com.revenuecat.purchases.paywalls.components.TabsComponent
17+
import com.revenuecat.purchases.paywalls.components.TextComponent
18+
import com.revenuecat.purchases.paywalls.components.common.Background
19+
import com.revenuecat.purchases.paywalls.components.common.ComponentOverride
20+
import com.revenuecat.purchases.paywalls.components.common.ComponentsConfig
21+
import com.revenuecat.purchases.paywalls.components.common.LocaleId
22+
import com.revenuecat.purchases.paywalls.components.common.LocalizationData
23+
import com.revenuecat.purchases.paywalls.components.common.LocalizationKey
24+
import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsConfig
25+
import com.revenuecat.purchases.paywalls.components.common.PaywallComponentsData
26+
import com.revenuecat.purchases.paywalls.components.common.StateDeclaration
27+
import com.revenuecat.purchases.paywalls.components.common.StateUpdate
28+
import com.revenuecat.purchases.paywalls.components.common.StateUpdateValue
29+
import com.revenuecat.purchases.paywalls.components.properties.ColorInfo
30+
import com.revenuecat.purchases.paywalls.components.properties.ColorScheme
31+
import com.revenuecat.purchases.ui.revenuecatui.components.style.TabsComponentStyle
32+
import com.revenuecat.purchases.ui.revenuecatui.components.style.TextComponentStyle
33+
import com.revenuecat.purchases.ui.revenuecatui.components.text.TextComponentView
34+
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
35+
import com.revenuecat.purchases.ui.revenuecatui.extensions.toComponentsPaywallState
36+
import com.revenuecat.purchases.ui.revenuecatui.extensions.validatePaywallComponentsDataOrNull
37+
import com.revenuecat.purchases.ui.revenuecatui.helpers.FakePaywallState
38+
import com.revenuecat.purchases.ui.revenuecatui.helpers.StyleFactory
39+
import com.revenuecat.purchases.ui.revenuecatui.helpers.UiConfig
40+
import com.revenuecat.purchases.ui.revenuecatui.helpers.getOrThrow
41+
import com.revenuecat.purchases.ui.revenuecatui.helpers.nonEmptyMapOf
42+
import org.assertj.core.api.Assertions.assertThat
43+
import org.junit.Rule
44+
import org.junit.Test
45+
import org.junit.runner.RunWith
46+
import kotlinx.serialization.json.JsonPrimitive
47+
import java.net.URL
48+
49+
@OptIn(InternalRevenueCatAPI::class)
50+
@RunWith(AndroidJUnit4::class)
51+
class TabStateTests {
52+
53+
@get:Rule
54+
val composeTestRule = createComposeRule()
55+
56+
private val localeId = LocaleId("en_US")
57+
private val textColor = ColorScheme(light = ColorInfo.Hex(Color.Black.toArgb()))
58+
private val assetBaseURL = URL("https://assets.pawwalls.com")
59+
private val stateKey = "selectedTab"
60+
61+
/**
62+
* A component reacting via a `state_condition` override recomposes when the store value changes.
63+
* Proves the read path: the store snapshot is threaded into the resolver and a write triggers re-resolution.
64+
*/
65+
@Test
66+
fun `Component re-resolves its state_condition override when the store value changes`(): Unit =
67+
with(composeTestRule) {
68+
val defaultTextKey = LocalizationKey("default_text")
69+
val annualTextKey = LocalizationKey("annual_text")
70+
val localizations = nonEmptyMapOf(
71+
localeId to nonEmptyMapOf(
72+
defaultTextKey to LocalizationData.Text("Default copy"),
73+
annualTextKey to LocalizationData.Text("Annual copy"),
74+
),
75+
)
76+
val component = TextComponent(
77+
text = defaultTextKey,
78+
color = textColor,
79+
overrides = listOf(
80+
ComponentOverride(
81+
conditions = listOf(
82+
ComponentOverride.Condition.State(
83+
operator = ComponentOverride.EqualityOperator.EQUALS,
84+
name = stateKey,
85+
value = JsonPrimitive("annual"),
86+
),
87+
),
88+
properties = PartialTextComponent(text = annualTextKey),
89+
),
90+
),
91+
)
92+
val state = FakePaywallState(
93+
localizations = localizations,
94+
defaultLocaleIdentifier = localeId,
95+
components = listOf(component),
96+
packages = listOf(TestData.Packages.monthly),
97+
)
98+
// Seed the declaration the override reads (default selection is "monthly").
99+
state.stateStore.registerDeclarations(
100+
mapOf(stateKey to StateDeclaration(type = StateDeclaration.ValueType.STRING, defaultValue = JsonPrimitive("monthly"))),
101+
)
102+
val styleFactory = StyleFactory(localizations = localizations)
103+
val style = styleFactory.create(component).getOrThrow().componentStyle as TextComponentStyle
104+
105+
setContent { TextComponentView(style = style, state = state) }
106+
107+
// Default selection: override does not apply.
108+
onNodeWithText("Default copy").assertExists()
109+
onNodeWithText("Annual copy").assertDoesNotExist()
110+
111+
// Simulate another component publishing the "annual" tab selection.
112+
state.stateStore.applyUpdates(
113+
listOf(StateUpdate.Set(stateKey, StateUpdateValue.Literal(JsonPrimitive("annual")))),
114+
)
115+
waitForIdle()
116+
117+
// The override now applies and the component recomposed.
118+
onNodeWithText("Annual copy").assertExists()
119+
onNodeWithText("Default copy").assertDoesNotExist()
120+
}
121+
122+
/**
123+
* Selecting a tab publishes the selected tab id into the store via the component's `state_updates`.
124+
* Proves the write path: seed on first appearance plus republish on selection change.
125+
*/
126+
@Test
127+
fun `Selecting a tab publishes its id into the state store`(): Unit = with(composeTestRule) {
128+
val tab0LabelKey = LocalizationKey("tab0_label")
129+
val tab1LabelKey = LocalizationKey("tab1_label")
130+
val localizations = nonEmptyMapOf(
131+
localeId to nonEmptyMapOf(
132+
tab0LabelKey to LocalizationData.Text("Monthly"),
133+
tab1LabelKey to LocalizationData.Text("Annual"),
134+
),
135+
)
136+
val tabControlButtons = listOf(tab0LabelKey to "monthly", tab1LabelKey to "annual")
137+
.mapIndexed { index, (labelKey, _) ->
138+
TabControlButtonComponent(
139+
tabIndex = index,
140+
tabId = listOf("monthly", "annual")[index],
141+
stack = StackComponent(components = listOf(TextComponent(text = labelKey, color = textColor))),
142+
)
143+
}
144+
val tabsComponent = TabsComponent(
145+
tabs = listOf("monthly", "annual").map { id ->
146+
TabsComponent.Tab(id = id, stack = StackComponent(components = listOf(TabControlComponent)))
147+
},
148+
control = TabsComponent.TabControl.Buttons(stack = StackComponent(components = tabControlButtons)),
149+
stateUpdates = listOf(StateUpdate.Set(stateKey, StateUpdateValue.PayloadReference)),
150+
)
151+
val data = PaywallComponentsData(
152+
id = "paywall_id",
153+
templateName = "template",
154+
assetBaseURL = assetBaseURL,
155+
componentsConfig = ComponentsConfig(
156+
base = PaywallComponentsConfig(
157+
stack = StackComponent(components = listOf(tabsComponent)),
158+
background = Background.Color(ColorScheme(light = ColorInfo.Hex(Color.White.toArgb()))),
159+
stickyFooter = null,
160+
),
161+
),
162+
componentsLocalizations = localizations,
163+
defaultLocaleIdentifier = localeId,
164+
stateDeclarations = mapOf(
165+
stateKey to StateDeclaration(type = StateDeclaration.ValueType.STRING, defaultValue = JsonPrimitive("monthly")),
166+
),
167+
)
168+
val offering = Offering(
169+
identifier = "offering-id",
170+
serverDescription = "description",
171+
metadata = emptyMap(),
172+
availablePackages = listOf(TestData.Packages.monthly),
173+
paywallComponents = Offering.PaywallComponents(UiConfig(), data),
174+
)
175+
val validated = offering.validatePaywallComponentsDataOrNull()?.getOrThrow()!!
176+
val state = offering.toComponentsPaywallState(validated)
177+
val styleFactory = StyleFactory(localizations = localizations, offering = offering)
178+
val style = styleFactory.create(tabsComponent).getOrThrow().componentStyle as TabsComponentStyle
179+
180+
setContent { TabsComponentView(style = style, state = state, clickHandler = { }) }
181+
waitForIdle()
182+
183+
// Seeded on first appearance with the default tab id.
184+
assertThat(state.stateStore.values[stateKey]).isEqualTo(JsonPrimitive("monthly"))
185+
186+
onNodeWithText("Annual").assertHasClickAction().performClick()
187+
waitForIdle()
188+
189+
// Selection republished the new tab id.
190+
assertThat(state.stateStore.values[stateKey]).isEqualTo(JsonPrimitive("annual"))
191+
}
192+
}

0 commit comments

Comments
 (0)