Skip to content

Commit e0b9479

Browse files
committed
Add grid widget unit tests
1 parent 0847eb9 commit e0b9479

File tree

7 files changed

+390
-1
lines changed

7 files changed

+390
-1
lines changed

app/src/main/kotlin/io/homeassistant/companion/android/widgets/grid/GridGlanceAppWidget.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.homeassistant.companion.android.widgets.grid
22

33
import android.content.Context
4+
import androidx.annotation.VisibleForTesting
45
import androidx.compose.runtime.Composable
56
import androidx.compose.runtime.collectAsState
67
import androidx.compose.runtime.getValue
@@ -102,7 +103,8 @@ private fun GlanceModifier.gridWidgetBackground(): GlanceModifier = this
102103
.background(GlanceTheme.colors.widgetBackground)
103104

104105
@Composable
105-
private fun GridWidgetContent(state: GridWidgetState) {
106+
@VisibleForTesting
107+
internal fun GridWidgetContent(state: GridWidgetState) {
106108
when (state) {
107109
is LoadingGridState -> LoadingScreen()
108110
is GridStateWithData -> GridContentWithData(state)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.homeassistant.companion.android.widgets.grid
2+
3+
import androidx.compose.ui.unit.DpSize
4+
import androidx.compose.ui.unit.dp
5+
import androidx.glance.appwidget.testing.unit.isIndeterminateCircularProgressIndicator
6+
import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
7+
import androidx.glance.testing.unit.hasContentDescriptionEqualTo
8+
import androidx.glance.testing.unit.hasTestTag
9+
import androidx.glance.testing.unit.hasTextEqualTo
10+
import io.homeassistant.companion.android.common.R as commonR
11+
import org.junit.Test
12+
import org.junit.runner.RunWith
13+
import org.robolectric.RobolectricTestRunner
14+
import org.robolectric.RuntimeEnvironment
15+
16+
@RunWith(RobolectricTestRunner::class)
17+
class GridGlanceAppWidgetTest {
18+
private val context = RuntimeEnvironment.getApplication()
19+
20+
@Test
21+
fun `Given LoadingState when GridWidgetContent then it displays CircularProgressIndicator`() = runGlanceAppWidgetUnitTest {
22+
setContext(context)
23+
24+
provideComposable {
25+
GridWidgetContent(LoadingGridState)
26+
}
27+
28+
onNode(hasTestTag("LoadingScreen"))
29+
.assertExists()
30+
31+
onNode(isIndeterminateCircularProgressIndicator())
32+
.assertExists()
33+
}
34+
35+
@Test
36+
fun `Given GridStateWithData when GridWidgetContent then it displays items`() = runGlanceAppWidgetUnitTest {
37+
setContext(context)
38+
39+
val items = listOf(
40+
GridButtonData("1", "Light", "mdi:lightbulb", "On", true),
41+
GridButtonData("2", "Switch", "mdi:toggle-switch", "Off", false),
42+
)
43+
val state = GridStateWithData(label = "My Grid", items = items)
44+
45+
provideComposable {
46+
GridWidgetContent(state)
47+
}
48+
49+
onNode(hasTextEqualTo("Light")).assertExists()
50+
onNode(hasTextEqualTo("On")).assertExists()
51+
52+
onNode(hasTextEqualTo("Switch")).assertExists()
53+
onNode(hasTextEqualTo("Off")).assertExists()
54+
}
55+
56+
@Test
57+
fun `Given GridStateWithData with label when GridWidgetContent then it displays title bar if size permits`() = runGlanceAppWidgetUnitTest {
58+
setContext(context)
59+
setAppWidgetSize(DpSize(400.dp, 400.dp))
60+
61+
val state = GridStateWithData(label = "My Grid", items = emptyList())
62+
63+
provideComposable {
64+
GridWidgetContent(state)
65+
}
66+
67+
onNode(hasContentDescriptionEqualTo(context.getString(commonR.string.refresh)))
68+
.assertExists()
69+
}
70+
71+
@Test
72+
fun `Given GridStateWithData in small width when GridWidgetContent then it does not display title bar`() = runGlanceAppWidgetUnitTest {
73+
setContext(context)
74+
setAppWidgetSize(DpSize(100.dp, 400.dp)) // Small width
75+
76+
val state = GridStateWithData(label = "My Grid", items = emptyList())
77+
78+
provideComposable {
79+
GridWidgetContent(state)
80+
}
81+
82+
onNode(hasContentDescriptionEqualTo(context.getString(commonR.string.refresh)))
83+
.assertDoesNotExist()
84+
}
85+
86+
@Test
87+
fun `Given GridStateWithData in normal width but small height when GridWidgetContent then it does not display title bar`() = runGlanceAppWidgetUnitTest {
88+
setContext(context)
89+
// Normal width (>180dp), Small height (<180dp)
90+
setAppWidgetSize(DpSize(200.dp, 100.dp))
91+
92+
val state = GridStateWithData(label = "My Grid", items = emptyList())
93+
94+
provideComposable {
95+
GridWidgetContent(state)
96+
}
97+
98+
onNode(hasContentDescriptionEqualTo(context.getString(commonR.string.refresh)))
99+
.assertDoesNotExist()
100+
}
101+
102+
@Test
103+
fun `Given GridStateWithData in compact mode then items do not show labels`() = runGlanceAppWidgetUnitTest {
104+
setContext(context)
105+
setAppWidgetSize(DpSize(100.dp, 400.dp)) // Small width -> Compact mode
106+
107+
val items = listOf(
108+
GridButtonData("1", "Light", "mdi:lightbulb", "On", true),
109+
)
110+
val state = GridStateWithData(label = "My Grid", items = items)
111+
112+
provideComposable {
113+
GridWidgetContent(state)
114+
}
115+
116+
// In compact mode, text is not shown
117+
onNode(hasTextEqualTo("Light")).assertDoesNotExist()
118+
onNode(hasTextEqualTo("On")).assertDoesNotExist()
119+
}
120+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.homeassistant.companion.android.widgets.grid
2+
3+
import androidx.glance.GlanceId
4+
import androidx.glance.action.actionParametersOf
5+
import androidx.glance.appwidget.GlanceAppWidgetManager
6+
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
7+
import io.homeassistant.companion.android.common.data.servers.ServerManager
8+
import io.homeassistant.companion.android.database.widget.GridWidgetDao
9+
import io.homeassistant.companion.android.database.widget.GridWidgetEntity
10+
import io.mockk.coEvery
11+
import io.mockk.coVerify
12+
import io.mockk.every
13+
import io.mockk.mockk
14+
import io.mockk.spyk
15+
import kotlinx.coroutines.test.runTest
16+
import org.junit.jupiter.api.Assertions.assertEquals
17+
import org.junit.jupiter.api.Assertions.assertThrows
18+
import org.junit.jupiter.api.Test
19+
20+
class GridWidgetPressActionTest {
21+
22+
private val entryPoints = object : PressEntityAction.PressEntityActionEntryPoint {
23+
val dao = mockk<GridWidgetDao>()
24+
val serverManager = mockk<ServerManager>()
25+
26+
override fun serverManager(): ServerManager = serverManager
27+
override fun gridWidgetDao(): GridWidgetDao = dao
28+
}
29+
30+
private data class FakeGlanceId(val id: Int) : GlanceId
31+
32+
@Test
33+
fun `Given a widgetID when not present in DAO and invoking onAction then do nothing`() = runTest {
34+
val action = spyk<PressEntityAction>()
35+
val glanceManager = mockk<GlanceAppWidgetManager>()
36+
val parameters = actionParametersOf(ENTITY_ID_KEY to "switch.test")
37+
val widgetId = 1
38+
39+
every { action.getEntryPoints(any()) } returns entryPoints
40+
every { action.getGlanceManager(any()) } returns glanceManager
41+
every { glanceManager.getAppWidgetId(any()) } returns widgetId
42+
43+
coEvery { entryPoints.gridWidgetDao().get(widgetId) } returns null
44+
45+
action.onAction(mockk(), FakeGlanceId(widgetId), parameters)
46+
47+
coVerify(exactly = 0) { entryPoints.serverManager().integrationRepository(any()) }
48+
}
49+
50+
@Test
51+
fun `Given a widgetID when present in DAO and invoking onAction then call integration repository`() {
52+
val action = spyk<PressEntityAction>()
53+
val glanceManager = mockk<GlanceAppWidgetManager>()
54+
val parameters = actionParametersOf(ENTITY_ID_KEY to "switch.test")
55+
val widgetId = 1
56+
val serverId = 123
57+
val entity = GridWidgetEntity(widgetId, serverId, "Label", emptyList())
58+
val integrationRepository = mockk<IntegrationRepository>(relaxed = true)
59+
60+
every { action.getEntryPoints(any()) } returns entryPoints
61+
every { action.getGlanceManager(any()) } returns glanceManager
62+
every { glanceManager.getAppWidgetId(any()) } returns widgetId
63+
64+
coEvery { entryPoints.gridWidgetDao().get(widgetId) } returns entity
65+
coEvery { entryPoints.serverManager().integrationRepository(serverId) } returns integrationRepository
66+
67+
// GridGlanceAppWidget.update() will throw IllegalArgumentException when called, so use it to verify onAction
68+
val exception = assertThrows(IllegalArgumentException::class.java) {
69+
runTest {
70+
action.onAction(mockk(), FakeGlanceId(widgetId), parameters)
71+
}
72+
}
73+
assertEquals("Invalid Glance ID", exception.message)
74+
75+
coVerify { integrationRepository.callAction(any(), any(), any()) }
76+
}
77+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.homeassistant.companion.android.widgets.grid
2+
3+
import org.junit.jupiter.api.Assertions.assertEquals
4+
import org.junit.jupiter.api.Assertions.assertTrue
5+
import org.junit.jupiter.api.Test
6+
7+
class GridWidgetStateTest {
8+
9+
@Test
10+
fun `Given GridStateWithData with items when created then properties are correct`() {
11+
val item1 = GridButtonData("1", "Label", "mdi:icon", "on", true)
12+
val state = GridStateWithData(
13+
label = "My Grid",
14+
items = listOf(item1),
15+
)
16+
17+
assertEquals("My Grid", state.label)
18+
assertEquals(1, state.items.size)
19+
assertEquals(item1, state.items[0])
20+
}
21+
22+
@Test
23+
fun `Given GridStateWithData default values when created then defaults are correct`() {
24+
val state = GridStateWithData()
25+
26+
assertEquals(null, state.label)
27+
assertTrue(state.items.isEmpty())
28+
}
29+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.homeassistant.companion.android.widgets.grid
2+
3+
import android.content.Context
4+
import app.cash.turbine.test
5+
import io.homeassistant.companion.android.common.data.integration.Entity
6+
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
7+
import io.homeassistant.companion.android.common.data.servers.ServerManager
8+
import io.homeassistant.companion.android.database.widget.GridWidgetDao
9+
import io.homeassistant.companion.android.database.widget.GridWidgetEntity
10+
import io.mockk.coEvery
11+
import io.mockk.mockk
12+
import java.time.LocalDateTime
13+
import kotlinx.coroutines.flow.emptyFlow
14+
import kotlinx.coroutines.flow.flowOf
15+
import kotlinx.coroutines.test.runTest
16+
import org.junit.jupiter.api.Assertions.assertEquals
17+
import org.junit.jupiter.api.Assertions.assertTrue
18+
import org.junit.jupiter.api.Test
19+
20+
class GridWidgetStateUpdaterTest {
21+
22+
private val dao = mockk<GridWidgetDao>()
23+
private val serverManager = mockk<ServerManager>()
24+
private val integrationRepository = mockk<IntegrationRepository>()
25+
private val context = mockk<Context>(relaxed = true)
26+
private val updater = GridWidgetStateUpdater(dao, serverManager, context)
27+
28+
@Test
29+
fun `Given widget in DAO when subscribing to stateFlow then emits initial state`() = runTest {
30+
val widgetId = 42
31+
val serverId = 1
32+
val item1 = GridWidgetEntity.Item(1, "switch.test", "Switch", "mdi:switch")
33+
val entity = GridWidgetEntity(widgetId, serverId, "Label", listOf(item1))
34+
35+
coEvery { dao.observe(widgetId) } returns flowOf(entity)
36+
coEvery { serverManager.integrationRepository(serverId) } returns integrationRepository
37+
coEvery { integrationRepository.getEntity("switch.test") } returns null
38+
coEvery { integrationRepository.getEntityUpdates(listOf("switch.test")) } returns emptyFlow()
39+
40+
updater.stateFlow(widgetId).test {
41+
val state = awaitItem() as GridStateWithData
42+
assertEquals("Label", state.label)
43+
assertEquals(1, state.items.size)
44+
assertEquals("switch.test", state.items[0].id)
45+
assertEquals("Switch", state.items[0].label)
46+
awaitComplete()
47+
}
48+
}
49+
50+
@Test
51+
fun `Given widget in DAO with entity updates when subscribing to stateFlow then emits updated state`() = runTest {
52+
val widgetId = 42
53+
val serverId = 1
54+
val item1 = GridWidgetEntity.Item(1, "switch.test", "Switch", "mdi:switch")
55+
val entity = GridWidgetEntity(widgetId, serverId, "Label", listOf(item1))
56+
57+
coEvery { dao.observe(widgetId) } returns flowOf(entity)
58+
coEvery { serverManager.integrationRepository(serverId) } returns integrationRepository
59+
coEvery { integrationRepository.getEntity("switch.test") } returns null
60+
61+
val updatedEntity = Entity(
62+
entityId = "switch.test",
63+
state = "on",
64+
attributes = mapOf<String, Any>(),
65+
lastChanged = LocalDateTime.now(),
66+
lastUpdated = LocalDateTime.now(),
67+
)
68+
69+
coEvery { integrationRepository.getEntityUpdates(listOf("switch.test")) } returns flowOf(updatedEntity)
70+
71+
updater.stateFlow(widgetId).test {
72+
awaitItem() // Ignore initial
73+
74+
val updatedState = awaitItem() as GridStateWithData
75+
assertEquals("switch.test", updatedState.items[0].id)
76+
assertEquals("Switch", updatedState.items[0].label)
77+
assertTrue(updatedState.items[0].isActive)
78+
awaitComplete()
79+
}
80+
}
81+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.homeassistant.companion.android.widgets.grid
2+
3+
import android.content.Context
4+
import io.homeassistant.companion.android.database.widget.GridWidgetDao
5+
import io.homeassistant.companion.android.database.widget.GridWidgetEntity
6+
import io.homeassistant.companion.android.widgets.EntitiesPerServer
7+
import io.mockk.coEvery
8+
import io.mockk.mockk
9+
import kotlinx.coroutines.test.runTest
10+
import org.junit.jupiter.api.Assertions.assertEquals
11+
import org.junit.jupiter.api.Test
12+
13+
class GridWidgetTest {
14+
private val dao: GridWidgetDao = mockk()
15+
16+
private val receiver = GridWidget().apply {
17+
dao = this@GridWidgetTest.dao
18+
}
19+
20+
@Test
21+
fun `Given no entry in DAO when invoking getWidgetEntitiesByServer then returns empty`() = runTest {
22+
val context = mockk<Context>()
23+
24+
coEvery { dao.getAll() } returns emptyList()
25+
26+
assertEquals(emptyMap<Int, EntitiesPerServer>(), receiver.getWidgetEntitiesByServer(context))
27+
}
28+
29+
@Test
30+
fun `Given entries in DAO when invoking getWidgetEntitiesByServer then returns properly mapped items`() = runTest {
31+
val context = mockk<Context>()
32+
33+
coEvery { dao.getAll() } returns listOf(
34+
GridWidgetEntity(
35+
id = 1,
36+
serverId = 42,
37+
label = null,
38+
items = listOf(GridWidgetEntity.Item(1, "entity1", "Label", "Icon")),
39+
),
40+
GridWidgetEntity(
41+
id = 2,
42+
serverId = 43,
43+
label = null,
44+
items = listOf(GridWidgetEntity.Item(2, "entity2", "Label", "Icon")),
45+
),
46+
)
47+
assertEquals(
48+
mapOf(
49+
1 to EntitiesPerServer(serverId = 42, listOf("entity1")),
50+
2 to EntitiesPerServer(serverId = 43, listOf("entity2")),
51+
),
52+
receiver.getWidgetEntitiesByServer(context),
53+
)
54+
}
55+
}

0 commit comments

Comments
 (0)