Skip to content

Commit 542106f

Browse files
committed
Add tests for StatsScreen
1 parent 592af70 commit 542106f

File tree

2 files changed

+236
-16
lines changed

2 files changed

+236
-16
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package com.pcapplusplus.toyvpn
2+
3+
import androidx.compose.material3.Text
4+
import androidx.compose.ui.test.assertIsDisplayed
5+
import androidx.compose.ui.test.assertIsEnabled
6+
import androidx.compose.ui.test.assertIsNotEnabled
7+
import androidx.compose.ui.test.assertTextEquals
8+
import androidx.compose.ui.test.junit4.createComposeRule
9+
import androidx.compose.ui.test.onNodeWithTag
10+
import androidx.compose.ui.test.onNodeWithText
11+
import androidx.compose.ui.test.onSibling
12+
import androidx.compose.ui.test.performClick
13+
import androidx.lifecycle.MutableLiveData
14+
import androidx.navigation.compose.NavHost
15+
import androidx.navigation.compose.composable
16+
import androidx.navigation.compose.rememberNavController
17+
import com.pcapplusplus.toyvpn.model.DomainData
18+
import com.pcapplusplus.toyvpn.model.VpnConnectionState
19+
import com.pcapplusplus.toyvpn.ui.theme.ToyVpnPcapPlusPlusTheme
20+
import io.mockk.every
21+
import io.mockk.mockk
22+
import io.mockk.verify
23+
import org.junit.Before
24+
import org.junit.Rule
25+
import org.junit.Test
26+
27+
class StatsScreenTest {
28+
@get:Rule
29+
val composeTestRule = createComposeRule()
30+
31+
private lateinit var mockViewModel: ToyVpnViewModel
32+
33+
private fun renderScreen(
34+
vpnConnectionState: VpnConnectionState = VpnConnectionState.CONNECTED,
35+
clientAddress: String? = null,
36+
totalPacketCount: Int = 0,
37+
ipv4PacketCount: Int = 0,
38+
ipv6PacketCount: Int = 0,
39+
tcpPacketCount: Int = 0,
40+
udpPacketCount: Int = 0,
41+
dnsPacketCount: Int = 0,
42+
tlsPacketCount: Int = 0,
43+
tcpConnectionCount: Int = 0,
44+
udpConnectionCount: Int = 0,
45+
topDnsDomains: List<DomainData> = listOf(),
46+
topTlsServerNames: List<DomainData> = listOf(),
47+
) {
48+
val vpnConnectionStateLiveData = MutableLiveData(vpnConnectionState)
49+
val clientAddressLiveData = MutableLiveData<String?>(clientAddress)
50+
val topDnsDomainsLiveData = MutableLiveData(topDnsDomains)
51+
val topTlsServerNamesLiveData = MutableLiveData(topTlsServerNames)
52+
val packetCountLiveData = MutableLiveData(totalPacketCount)
53+
val ipv4PacketCountLiveData = MutableLiveData(ipv4PacketCount)
54+
val ipv6PacketCountLiveData = MutableLiveData(ipv6PacketCount)
55+
val tcpPacketCountLiveData = MutableLiveData(tcpPacketCount)
56+
val udpPacketCountLiveData = MutableLiveData(udpPacketCount)
57+
val dnsPacketCountLiveData = MutableLiveData(dnsPacketCount)
58+
val tlsPacketCountLiveData = MutableLiveData(tlsPacketCount)
59+
val tcpConnectionsLiveData = MutableLiveData(tcpConnectionCount)
60+
val udpConnectionsLiveData = MutableLiveData(udpConnectionCount)
61+
62+
every { mockViewModel.vpnConnectionState } returns vpnConnectionStateLiveData
63+
every { mockViewModel.clientAddress } returns clientAddressLiveData
64+
every { mockViewModel.topDnsDomains } returns topDnsDomainsLiveData
65+
every { mockViewModel.topTlsServerNames } returns topTlsServerNamesLiveData
66+
every { mockViewModel.packetCount } returns packetCountLiveData
67+
every { mockViewModel.ipv4PacketCount } returns ipv4PacketCountLiveData
68+
every { mockViewModel.ipv6PacketCount } returns ipv6PacketCountLiveData
69+
every { mockViewModel.tcpPacketCount } returns tcpPacketCountLiveData
70+
every { mockViewModel.udpPacketCount } returns udpPacketCountLiveData
71+
every { mockViewModel.dnsPacketCount } returns dnsPacketCountLiveData
72+
every { mockViewModel.tlsPacketCount } returns tlsPacketCountLiveData
73+
every { mockViewModel.tcpConnectionCount } returns tcpConnectionsLiveData
74+
every { mockViewModel.udpConnectionCount } returns udpConnectionsLiveData
75+
76+
composeTestRule.setContent {
77+
ToyVpnPcapPlusPlusTheme {
78+
val navController = rememberNavController()
79+
80+
NavHost(
81+
navController = navController,
82+
startDestination = "stats_screen",
83+
) {
84+
composable("stats_screen") {
85+
StatsScreen(navController, mockViewModel)
86+
}
87+
composable("connect_screen") {
88+
Text("Connect Screen")
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
@Before
96+
fun setUp() {
97+
mockViewModel = mockk(relaxed = true)
98+
}
99+
100+
@Test
101+
fun testVpnConnected() {
102+
renderScreen()
103+
104+
composeTestRule.onNodeWithText("Disconnect").assertIsDisplayed().assertIsEnabled()
105+
}
106+
107+
@Test
108+
fun testVpnConnectedWithClientAddress() {
109+
renderScreen(clientAddress = "1.2.3.4")
110+
111+
composeTestRule.onNodeWithText("IP Address").assertIsDisplayed()
112+
composeTestRule.onNodeWithText("1.2.3.4").assertIsDisplayed()
113+
}
114+
115+
@Test
116+
fun testVpnConnectedWithPacketTraffic() {
117+
val totalPacketCount = 396
118+
val ipv4PacketCount = 11
119+
val ipv6PacketCount = 22
120+
val tcpPacketCount = 33
121+
val udpPacketCount = 44
122+
val dnsPacketCount = 55
123+
val tlsPacketCount = 66
124+
val tcpConnectionCount = 77
125+
val udpConnectionCount = 88
126+
127+
renderScreen(
128+
totalPacketCount = totalPacketCount,
129+
ipv4PacketCount = ipv4PacketCount,
130+
ipv6PacketCount = ipv6PacketCount,
131+
tcpPacketCount = tcpPacketCount,
132+
udpPacketCount = udpPacketCount,
133+
dnsPacketCount = dnsPacketCount,
134+
tlsPacketCount = tlsPacketCount,
135+
tcpConnectionCount = tcpConnectionCount,
136+
udpConnectionCount = udpConnectionCount,
137+
)
138+
139+
val expectedValues =
140+
listOf(
141+
Triple("IPv4", "IPv4", ipv4PacketCount),
142+
Triple("IPv6", "IPv6", ipv6PacketCount),
143+
Triple("TCP", "TCP", tcpPacketCount),
144+
Triple("UDP", "UDP", udpPacketCount),
145+
Triple("DNS", "DNS", dnsPacketCount),
146+
Triple("TLS", "TLS", tlsPacketCount),
147+
Triple("TCPConn", "TCP", tcpConnectionCount),
148+
Triple("UDPConn", "UDP", udpConnectionCount),
149+
)
150+
151+
composeTestRule.onNodeWithText("Total Packets").assertIsDisplayed().onSibling().assertTextEquals(totalPacketCount.toString())
152+
153+
expectedValues.forEach { (testTag, label, count) ->
154+
composeTestRule.onNodeWithTag("${testTag}_label").assertIsDisplayed().assertTextEquals(label)
155+
composeTestRule.onNodeWithTag("${testTag}_count").assertIsDisplayed().assertTextEquals(count.toString())
156+
composeTestRule.onNodeWithTag("${testTag}_progress").assertIsDisplayed()
157+
}
158+
}
159+
160+
@Test
161+
fun testVpnConnectedWithTopDnsDomainData() {
162+
val topDnsDomains =
163+
listOf(
164+
DomainData("google.com", 11),
165+
DomainData("example.com", 22),
166+
)
167+
168+
renderScreen(topDnsDomains = topDnsDomains)
169+
170+
topDnsDomains.forEach { (domain, count) ->
171+
composeTestRule.onNodeWithText("https://$domain")
172+
composeTestRule.onNodeWithText(count.toString())
173+
}
174+
}
175+
176+
@Test
177+
fun testVpnConnectedWithTopTlsServerNamesData() {
178+
val topTlsServerNames =
179+
listOf(
180+
DomainData("facebook.com", 33),
181+
DomainData("apple.com", 44),
182+
)
183+
184+
renderScreen(topTlsServerNames = topTlsServerNames)
185+
186+
topTlsServerNames.forEach { (domain, count) ->
187+
composeTestRule.onNodeWithText("https://$domain")
188+
composeTestRule.onNodeWithText(count.toString())
189+
}
190+
}
191+
192+
@Test
193+
fun testClickDisconnectButton() {
194+
renderScreen()
195+
196+
composeTestRule.onNodeWithText("Disconnect").performClick()
197+
198+
verify { mockViewModel.disconnectVpn() }
199+
}
200+
201+
@Test
202+
fun testVpnDisconnecting() {
203+
renderScreen(vpnConnectionState = VpnConnectionState.DISCONNECTING)
204+
205+
composeTestRule.onNodeWithText("Disconnecting...").assertIsDisplayed().assertIsNotEnabled()
206+
}
207+
208+
@Test
209+
fun testVpnDisconnected() {
210+
renderScreen(vpnConnectionState = VpnConnectionState.DISCONNECTED)
211+
212+
composeTestRule.onNodeWithText("Connect Screen").assertIsDisplayed()
213+
}
214+
}

app/src/main/java/com/pcapplusplus/toyvpn/StatsScreen.kt

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment
3737
import androidx.compose.ui.Modifier
3838
import androidx.compose.ui.graphics.Color
3939
import androidx.compose.ui.platform.LocalContext
40+
import androidx.compose.ui.platform.testTag
4041
import androidx.compose.ui.text.style.TextAlign
4142
import androidx.compose.ui.text.style.TextDecoration
4243
import androidx.compose.ui.text.style.TextOverflow
@@ -51,6 +52,7 @@ import com.pcapplusplus.toyvpn.model.VpnConnectionState
5152

5253
data class TrafficStat(
5354
val label: String,
55+
val testTag: String,
5456
val count: Int,
5557
val total: Int,
5658
)
@@ -69,8 +71,8 @@ fun StatsScreen(
6971
val udpPacketCount by viewModel.udpPacketCount.observeAsState(0)
7072
val dnsPacketCount by viewModel.dnsPacketCount.observeAsState(0)
7173
val tlsPacketCount by viewModel.tlsPacketCount.observeAsState(0)
72-
val tcpConnections by viewModel.tcpConnectionCount.observeAsState(0)
73-
val udpConnections by viewModel.udpConnectionCount.observeAsState(0)
74+
val tcpConnectionCount by viewModel.tcpConnectionCount.observeAsState(0)
75+
val udpConnectionCount by viewModel.udpConnectionCount.observeAsState(0)
7476
val topDnsDomains by viewModel.topDnsDomains.observeAsState()
7577
val topTlsServerNames by viewModel.topTlsServerNames.observeAsState()
7678

@@ -126,12 +128,12 @@ fun StatsScreen(
126128
title = "Packet Count by Protocol",
127129
stats =
128130
listOf(
129-
TrafficStat("IPv4", ipv4PacketCount, packetCount),
130-
TrafficStat("IPv6", ipv6PacketCount, packetCount),
131-
TrafficStat("TCP", tcpPacketCount, packetCount),
132-
TrafficStat("UDP", udpPacketCount, packetCount),
133-
TrafficStat("DNS", dnsPacketCount, packetCount),
134-
TrafficStat("TLS", tlsPacketCount, packetCount),
131+
TrafficStat("IPv4", "IPv4", ipv4PacketCount, packetCount),
132+
TrafficStat("IPv6", "IPv6", ipv6PacketCount, packetCount),
133+
TrafficStat("TCP", "TCP", tcpPacketCount, packetCount),
134+
TrafficStat("UDP", "UDP", udpPacketCount, packetCount),
135+
TrafficStat("DNS", "DNS", dnsPacketCount, packetCount),
136+
TrafficStat("TLS", "TLS", tlsPacketCount, packetCount),
135137
),
136138
)
137139

@@ -141,8 +143,8 @@ fun StatsScreen(
141143
title = "Connections",
142144
stats =
143145
listOf(
144-
TrafficStat("TCP", tcpConnections, tcpConnections + udpConnections),
145-
TrafficStat("UDP", udpConnections, tcpConnections + udpConnections),
146+
TrafficStat("TCP", "TCPConn", tcpConnectionCount, tcpConnectionCount + udpConnectionCount),
147+
TrafficStat("UDP", "UDPConn", udpConnectionCount, tcpConnectionCount + udpConnectionCount),
146148
),
147149
)
148150

@@ -158,13 +160,13 @@ fun StatsScreen(
158160

159161
Button(
160162
onClick = onDisconnectClicked,
161-
enabled = vpnConnectionState != VpnConnectionState.DISCONNECTED,
163+
enabled = vpnConnectionState !in listOf(VpnConnectionState.DISCONNECTED, VpnConnectionState.DISCONNECTING),
162164
modifier =
163165
Modifier
164166
.fillMaxWidth()
165167
.height(60.dp),
166168
) {
167-
if (vpnConnectionState == VpnConnectionState.DISCONNECTED) {
169+
if (vpnConnectionState == VpnConnectionState.DISCONNECTING) {
168170
Text("Disconnecting...")
169171
} else {
170172
Text("Disconnect")
@@ -229,14 +231,18 @@ fun TrafficStatRow(stat: TrafficStat) {
229231
Text(
230232
text = stat.label,
231233
style = MaterialTheme.typography.titleMedium.copy(fontSize = 20.sp),
232-
modifier = Modifier.wrapContentWidth(Alignment.Start),
234+
modifier = Modifier.wrapContentWidth(Alignment.Start).testTag("${stat.testTag}_label"),
233235
)
234236
Spacer(modifier = Modifier.width(8.dp))
235-
ProgressBar(modifier = Modifier.weight(1f), count = stat.count, total = stat.total)
237+
ProgressBar(
238+
modifier = Modifier.weight(1f).testTag("${stat.testTag}_progress"),
239+
count = stat.count,
240+
total = if (stat.total != 0) stat.total else 1,
241+
)
236242
Text(
237243
text = stat.count.toString(),
238244
style = MaterialTheme.typography.titleMedium.copy(fontSize = 20.sp),
239-
modifier = Modifier.align(Alignment.CenterVertically),
245+
modifier = Modifier.align(Alignment.CenterVertically).testTag("${stat.testTag}_count"),
240246
textAlign = TextAlign.End,
241247
)
242248
}
@@ -297,7 +303,7 @@ fun DomainRow(stat: DomainData) {
297303
) {
298304
Icon(
299305
imageVector = Icons.Default.Language,
300-
contentDescription = "DNS query",
306+
contentDescription = "Domain",
301307
modifier = Modifier.size(24.dp),
302308
tint = MaterialTheme.colorScheme.primary,
303309
)

0 commit comments

Comments
 (0)