diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
index 3b4b0b4..af6a4f9 100644
--- a/.idea/androidTestResultsUserPreferences.xml
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -3,6 +3,19 @@
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index df1c514..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index ed76bea..44ca2d9 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -5,6 +5,10 @@
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index aad08f6..284e372 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -56,6 +56,11 @@
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 712301c..0ac79a1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -30,7 +30,7 @@ android {
targetSdk = libs.versions.maxSdk.get().toInt()
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
- testInstrumentationRunner = "com.wbrawner.pihelper.HiltTestRunner"
+ testInstrumentationRunner = "com.wbrawner.pihelper.util.HiltTestRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
signingConfig = signingConfigs["debug"]
}
diff --git a/app/src/androidTest/assets/json/status_disabled.json b/app/src/androidTest/assets/json/status_disabled.json
new file mode 100644
index 0000000..5b39d20
--- /dev/null
+++ b/app/src/androidTest/assets/json/status_disabled.json
@@ -0,0 +1,3 @@
+{
+ "status": "disabled"
+}
\ No newline at end of file
diff --git a/app/src/androidTest/assets/json/status_enabled.json b/app/src/androidTest/assets/json/status_enabled.json
new file mode 100644
index 0000000..38ac44a
--- /dev/null
+++ b/app/src/androidTest/assets/json/status_enabled.json
@@ -0,0 +1,3 @@
+{
+ "status": "enabled"
+}
\ No newline at end of file
diff --git a/app/src/androidTest/assets/json/summary_disabled.json b/app/src/androidTest/assets/json/summary_disabled.json
new file mode 100644
index 0000000..3fe67fe
--- /dev/null
+++ b/app/src/androidTest/assets/json/summary_disabled.json
@@ -0,0 +1,38 @@
+{
+ "domains_being_blocked": 166010,
+ "dns_queries_today": 71567,
+ "ads_blocked_today": 37254,
+ "ads_percentage_today": 52.054718,
+ "unique_domains": 17559,
+ "queries_forwarded": 29352,
+ "queries_cached": 4864,
+ "clients_ever_seen": 6,
+ "unique_clients": 3,
+ "dns_queries_all_types": 71567,
+ "reply_UNKNOWN": 174,
+ "reply_NODATA": 7023,
+ "reply_NXDOMAIN": 2006,
+ "reply_CNAME": 13863,
+ "reply_IP": 47973,
+ "reply_DOMAIN": 48,
+ "reply_RRNAME": 0,
+ "reply_SERVFAIL": 0,
+ "reply_REFUSED": 0,
+ "reply_NOTIMP": 0,
+ "reply_OTHER": 0,
+ "reply_DNSSEC": 0,
+ "reply_NONE": 0,
+ "reply_BLOB": 480,
+ "dns_queries_all_replies": 71567,
+ "privacy_level": 0,
+ "status": "disabled",
+ "gravity_last_updated": {
+ "file_exists": true,
+ "absolute": 1671361515,
+ "relative": {
+ "days": 3,
+ "hours": 5,
+ "minutes": 23
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/assets/json/status_success.json b/app/src/androidTest/assets/json/summary_enabled.json
similarity index 99%
rename from app/src/androidTest/assets/json/status_success.json
rename to app/src/androidTest/assets/json/summary_enabled.json
index 6a2c9a1..1789624 100644
--- a/app/src/androidTest/assets/json/status_success.json
+++ b/app/src/androidTest/assets/json/summary_enabled.json
@@ -35,4 +35,4 @@
"minutes": 23
}
}
-}%
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/src/androidTest/assets/json/summary_failure.json b/app/src/androidTest/assets/json/summary_failure.json
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/app/src/androidTest/assets/json/summary_failure.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/FakeAPIService.kt b/app/src/androidTest/java/com/wbrawner/pihelper/FakeAPIService.kt
deleted file mode 100644
index aeede78..0000000
--- a/app/src/androidTest/java/com/wbrawner/pihelper/FakeAPIService.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.wbrawner.pihelper
-
-import com.wbrawner.pihelper.shared.PiholeAPIService
-import com.wbrawner.pihelper.shared.create
-import okhttp3.mockwebserver.MockWebServer
-
-class FakeAPIService(
- private val apiService: PiholeAPIService = PiholeAPIService.create()
-) : PiholeAPIService by apiService {
- val server = MockWebServer().apply {
- start()
- }
-
- val hostName: String = server.hostName
- val port = server.port
-}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/MainTests.kt b/app/src/androidTest/java/com/wbrawner/pihelper/MainTests.kt
new file mode 100644
index 0000000..98c27bb
--- /dev/null
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/MainTests.kt
@@ -0,0 +1,91 @@
+package com.wbrawner.pihelper
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.State
+import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.util.FakeAPIService
+import com.wbrawner.pihelper.util.onMainScreen
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@UninstallModules(PiHelperModule::class)
+@HiltAndroidTest
+@OptIn(ExperimentalAnimationApi::class)
+class MainTests {
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+
+ @get:Rule(order = 0)
+ val hiltTestRule = HiltAndroidRule(this)
+
+ @BindValue
+ @JvmField
+ val apiService: FakeAPIService = FakeAPIService()
+
+ @Test
+ fun testDisable() {
+ onMainScreen(composeTestRule) {
+ apiService.statusEnabled(context)
+ apiService.disableSuccess(context)
+ apiService.statusDisabled(context)
+ apiService.disabledPermanently()
+ verifyStatus("Enabled")
+ val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ assertTrue(request?.requestUrl?.queryParameterNames?.contains("auth") == true)
+ clickDisablePermanentlyButton()
+ verifyStatus("Disabled")
+ }
+ }
+
+ @Test
+ fun testEnable() {
+ onMainScreen(composeTestRule) {
+ apiService.statusDisabled(context)
+ apiService.disabledPermanently()
+ apiService.enableSuccess(context)
+ apiService.statusEnabled(context)
+ verifyStatus("Disabled")
+ val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ assertTrue(request?.requestUrl?.queryParameterNames?.contains("auth") == true)
+ clickEnableButton()
+ verifyStatus("Enabled")
+ }
+ }
+
+ @Module
+ @InstallIn(SingletonComponent::class)
+ inner class TestModule {
+
+ @Provides
+ @Singleton
+ fun providesPiholeAPIService(): PiholeAPIService = apiService
+
+ @Provides
+ @Singleton
+ fun providesInitialState(apiService: FakeAPIService): State = State(
+ apiKey = "key",
+ host = "${apiService.hostName}:${apiService.port}"
+ )
+
+ @Provides
+ @Singleton
+ fun providesStore(
+ apiService: PiholeAPIService,
+ initialState: State
+ ): Store = Store(apiService, initialState = initialState)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/SetupTests.kt b/app/src/androidTest/java/com/wbrawner/pihelper/SetupTests.kt
new file mode 100644
index 0000000..e24bc50
--- /dev/null
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/SetupTests.kt
@@ -0,0 +1,153 @@
+package com.wbrawner.pihelper
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.Store
+import com.wbrawner.pihelper.util.FakeAPIService
+import com.wbrawner.pihelper.util.onAddScreen
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@UninstallModules(PiHelperModule::class)
+@HiltAndroidTest
+@OptIn(ExperimentalAnimationApi::class)
+class SetupTests {
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+
+ @get:Rule(order = 0)
+ val hiltTestRule = HiltAndroidRule(this)
+
+ @BindValue
+ @JvmField
+ val apiService: FakeAPIService = FakeAPIService()
+
+ @Test
+ fun testSuccessfulConnectionWithPassword() {
+ onAddScreen(composeTestRule) {
+ apiService.testConnectionSuccess()
+ clearHost()
+ inputHost("${apiService.hostName}:${apiService.port}")
+ clickConnect()
+ apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ } onAuthScreen {
+ apiService.authenticationSuccess(context)
+ verifyConnectionSuccessMessage()
+ inputPassword("password")
+ clickAuthenticateWithPassword()
+ val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
+ assertEquals(
+ "113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
+ request?.requestUrl?.queryParameter("auth")
+ )
+ }
+ }
+
+ @Test
+ fun testSuccessfulConnectionWithAPIKey() {
+ onAddScreen(composeTestRule) {
+ apiService.testConnectionSuccess()
+ clearHost()
+ inputHost("${apiService.hostName}:${apiService.port}")
+ clickConnect()
+ apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ } onAuthScreen {
+ apiService.authenticationSuccess(context)
+ verifyConnectionSuccessMessage()
+ inputAPIKey("113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3")
+ clickAuthenticateWithAPIKey()
+ val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
+ assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
+ assertEquals(
+ "113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
+ request?.requestUrl?.queryParameter("auth")
+ )
+ }
+ }
+
+ @Test
+ fun testFailedConnection() {
+ onAddScreen(composeTestRule) {
+ clearHost()
+ inputHost("localhost")
+ clickConnect()
+ verifyErrorMessageIsDisplayed("Failed to connect")
+ }
+ }
+
+ @Test
+ fun testInvalidHost() {
+ onAddScreen(composeTestRule) {
+ apiService.testConnectionFailure()
+ clearHost()
+ inputHost("${apiService.hostName}:${apiService.port}")
+ clickConnect()
+ verifyErrorMessageIsDisplayed("Host does not appear to be a valid Pi-hole")
+ }
+ }
+
+ @Test
+ fun testFailedAuthenticationWithAPIKey() {
+ onAddScreen(composeTestRule) {
+ apiService.testConnectionSuccess()
+ clearHost()
+ inputHost("${apiService.hostName}:${apiService.port}")
+ clickConnect()
+ } onAuthScreen {
+ apiService.authenticationFailure(context)
+ verifyConnectionSuccessMessage()
+ inputAPIKey("113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3")
+ clickAuthenticateWithAPIKey()
+ verifyErrorMessageIsDisplayed("Invalid credentials")
+ }
+ }
+
+ @Test
+ fun testFailedAuthenticationWithPassword() {
+ onAddScreen(composeTestRule) {
+ apiService.testConnectionSuccess()
+ clearHost()
+ inputHost("${apiService.hostName}:${apiService.port}")
+ clickConnect()
+ } onAuthScreen {
+ verifyConnectionSuccessMessage()
+ apiService.authenticationFailure(context)
+ inputPassword("password")
+ clickAuthenticateWithPassword()
+ verifyErrorMessageIsDisplayed("Invalid credentials")
+ }
+ }
+
+ @Module
+ @InstallIn(SingletonComponent::class)
+ abstract class TestModule {
+
+ @Binds
+ @Singleton
+ abstract fun bindsPiholeAPIService(apiService: FakeAPIService): PiholeAPIService
+
+ companion object {
+ @Provides
+ @Singleton
+ fun providesStore(
+ apiService: PiholeAPIService
+ ): Store = Store(apiService)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/StartupTests.kt b/app/src/androidTest/java/com/wbrawner/pihelper/StartupTests.kt
deleted file mode 100644
index 8300226..0000000
--- a/app/src/androidTest/java/com/wbrawner/pihelper/StartupTests.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.wbrawner.pihelper
-
-import android.content.Context
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import dagger.hilt.android.testing.BindValue
-import dagger.hilt.android.testing.HiltAndroidRule
-import dagger.hilt.android.testing.HiltAndroidTest
-import dagger.hilt.android.testing.UninstallModules
-import okhttp3.mockwebserver.MockResponse
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Rule
-import org.junit.Test
-import java.util.concurrent.TimeUnit
-
-@UninstallModules(PiHelperModule::class)
-@HiltAndroidTest
-@OptIn(ExperimentalAnimationApi::class)
-class StartupTests {
-
- @get:Rule(order = 1)
- val composeTestRule = createAndroidComposeRule()
-
- @get:Rule(order = 0)
- val hiltTestRule = HiltAndroidRule(this)
-
- @BindValue
- @JvmField
- val apiService: FakeAPIService = FakeAPIService()
-
- @Test
- fun testManualConnectionWithPassword() {
- onAddScreen(composeTestRule) {
- apiService.server.enqueue(
- MockResponse()
- .setHeader("Content-Type", "application/json")
- .setBody(context.readAsset("json/version_success.json"))
- )
- clearHost()
- inputHost("${apiService.hostName}:${apiService.port}")
- clickConnect()
- val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
- assertTrue(request?.requestUrl?.queryParameterNames?.contains("version") ?: false)
- } onAuthScreen {
- verifyConnectionSuccessMessage()
- apiService.server.enqueue(
- MockResponse()
- .setHeader("Content-Type", "application/json")
- .setBody(context.readAsset("json/top_items_success.json"))
- )
- inputPassword("password")
- clickAuthenticateWithPassword()
- val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
- assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
- assertEquals(
- "113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
- request?.requestUrl?.queryParameter("auth")
- )
- }
- }
-
- @Test
- fun testManualConnectionWithAPIKey() {
- onAddScreen(composeTestRule) {
- val body = context.readAsset("json/version_success.json")
- apiService.server.enqueue(
- MockResponse()
- .setHeader("Content-Type", "application/json")
- .setBody(body)
- )
- clearHost()
- inputHost("${apiService.hostName}:${apiService.port}")
- clickConnect()
- val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
- assertTrue(request?.requestUrl?.queryParameterNames?.contains("version") ?: false)
- } onAuthScreen {
- verifyConnectionSuccessMessage()
- apiService.server.enqueue(
- MockResponse()
- .setHeader("Content-Type", "application/json")
- .setBody(context.readAsset("json/top_items_success.json"))
- )
- inputAPIKey("113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3")
- clickAuthenticateWithAPIKey()
- val request = apiService.server.takeRequest(1, TimeUnit.SECONDS)
- assertTrue(request?.requestUrl?.queryParameterNames?.contains("topItems") ?: false)
- assertEquals(
- "113459eb7bb31bddee85ade5230d6ad5d8b2fb52879e00a84ff6ae1067a210d3",
- request?.requestUrl?.queryParameter("auth")
- )
- }
- }
-}
-
-fun Context.readAsset(path: String) = assets.open(path).bufferedReader().use { it.readText() }
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/TestModule.kt b/app/src/androidTest/java/com/wbrawner/pihelper/TestModule.kt
deleted file mode 100644
index 0ab716e..0000000
--- a/app/src/androidTest/java/com/wbrawner/pihelper/TestModule.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.wbrawner.pihelper
-
-import com.wbrawner.pihelper.shared.PiholeAPIService
-import com.wbrawner.pihelper.shared.Store
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.components.SingletonComponent
-import dagger.hilt.testing.TestInstallIn
-import javax.inject.Singleton
-
-@Module
-@TestInstallIn(
- components = [SingletonComponent::class],
- replaces = [PiHelperModule::class]
-)
-abstract class TestModule {
-
- @Binds
- @Singleton
- abstract fun bindsPiholeAPIService(apiService: FakeAPIService): PiholeAPIService
-
- companion object {
- @Provides
- @Singleton
- fun providesStore(
- apiService: PiholeAPIService,
- ): Store = Store(apiService)
- }
-}
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/AddScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
similarity index 71%
rename from app/src/androidTest/java/com/wbrawner/pihelper/AddScreenRobot.kt
rename to app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
index 4f6d355..9a3d2c7 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/AddScreenRobot.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/AddScreenRobot.kt
@@ -1,9 +1,12 @@
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.util
import android.content.Context
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
+import com.wbrawner.pihelper.ADD_SCREEN_TAG
+import com.wbrawner.pihelper.CONNECT_BUTTON_TAG
+import com.wbrawner.pihelper.HOST_TAG
fun onAddScreen(testRule: ComposeTestRule, actions: AddScreenRobot.() -> Unit) =
AddScreenRobot(testRule).apply { actions() }
@@ -31,4 +34,11 @@ class AddScreenRobot(private val testRule: ComposeTestRule) {
fun clickConnect() = testRule.onNode(hasTestTag(CONNECT_BUTTON_TAG)).performClick()
+ fun verifyErrorMessageIsDisplayed(message: String) {
+ testRule.waitUntil(2_000) {
+ testRule
+ .onAllNodesWithText(message, substring = true)
+ .fetchSemanticsNodes().size == 1
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/AuthScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
similarity index 72%
rename from app/src/androidTest/java/com/wbrawner/pihelper/AuthScreenRobot.kt
rename to app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
index 401ff8e..e052509 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/AuthScreenRobot.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/AuthScreenRobot.kt
@@ -1,12 +1,10 @@
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.util
import android.content.Context
-import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.onAllNodesWithTag
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
import androidx.test.platform.app.InstrumentationRegistry
+import com.wbrawner.pihelper.*
class AuthScreenRobot(private val testRule: ComposeTestRule) {
val context: Context = InstrumentationRegistry.getInstrumentation().context
@@ -26,11 +24,21 @@ class AuthScreenRobot(private val testRule: ComposeTestRule) {
testRule.onNode(hasTestTag(PASSWORD_INPUT_TAG)).performTextInput(password)
fun clickAuthenticateWithPassword() = testRule.onNode(hasTestTag(PASSWORD_BUTTON_TAG))
+ .performScrollTo()
.performClick()
fun inputAPIKey(key: String) =
testRule.onNode(hasTestTag(API_KEY_INPUT_TAG)).performTextInput(key)
fun clickAuthenticateWithAPIKey() = testRule.onNode(hasTestTag(API_KEY_BUTTON_TAG))
+ .performScrollTo()
.performClick()
+
+ fun verifyErrorMessageIsDisplayed(message: String) {
+ testRule.waitUntil(2_000) {
+ testRule
+ .onAllNodesWithText(message, substring = true)
+ .fetchSemanticsNodes().size == 1
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/FakeAPIService.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/FakeAPIService.kt
new file mode 100644
index 0000000..6312cc8
--- /dev/null
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/FakeAPIService.kt
@@ -0,0 +1,100 @@
+package com.wbrawner.pihelper.util
+
+import android.content.Context
+import com.wbrawner.pihelper.shared.PiholeAPIService
+import com.wbrawner.pihelper.shared.create
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+
+class FakeAPIService : PiholeAPIService by PiholeAPIService.create() {
+ val server = MockWebServer().apply {
+ start()
+ }
+ val hostName: String = server.hostName
+ val port = server.port
+
+ fun testConnectionSuccess() {
+ server.enqueue(
+ MockResponse().setHeader(
+ "X-Pi-Hole",
+ "The Pi-hole Web interface is working!"
+ )
+ )
+ }
+
+ fun testConnectionFailure() {
+ server.enqueue(MockResponse().setResponseCode(204))
+ }
+
+ fun authenticationSuccess(context: Context) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/top_items_success.json")
+ )
+ )
+ }
+
+ fun authenticationFailure(context: Context) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/top_items_failure.json")
+ )
+ )
+ }
+
+ fun statusEnabled(context: Context) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/summary_enabled.json")
+ )
+ )
+ }
+
+ fun statusDisabled(context: Context, duration: Long? = null) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/summary_disabled.json")
+ )
+ )
+ }
+
+ fun enableSuccess(context: Context) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/status_enabled.json")
+ )
+ )
+ }
+
+ fun disableSuccess(context: Context) {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ context.readAsset("json/status_disabled.json")
+ )
+ )
+ }
+
+ fun disabledPermanently() {
+ server.enqueue(
+ MockResponse()
+ .setHeader("Content-Type", "text/html")
+ .setBody("Definitely not a number
")
+ )
+ }
+}
+
+private fun Context.readAsset(path: String) = assets.open(path)
+ .bufferedReader()
+ .use { it.readText() }
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/HiltTestRunner.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/HiltTestRunner.kt
similarity index 92%
rename from app/src/androidTest/java/com/wbrawner/pihelper/HiltTestRunner.kt
rename to app/src/androidTest/java/com/wbrawner/pihelper/util/HiltTestRunner.kt
index 3276f0b..6c4959a 100644
--- a/app/src/androidTest/java/com/wbrawner/pihelper/HiltTestRunner.kt
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/HiltTestRunner.kt
@@ -1,4 +1,4 @@
-package com.wbrawner.pihelper
+package com.wbrawner.pihelper.util
import android.app.Application
import android.content.Context
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt
new file mode 100644
index 0000000..0c0e392
--- /dev/null
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/MainScreenRobot.kt
@@ -0,0 +1,50 @@
+package com.wbrawner.pihelper.util
+
+import android.content.Context
+import androidx.compose.ui.test.*
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.test.platform.app.InstrumentationRegistry
+import com.wbrawner.pihelper.DISABLE_PERMANENT_BUTTON_TAG
+import com.wbrawner.pihelper.ENABLE_BUTTON_TAG
+import com.wbrawner.pihelper.MAIN_SCREEN_TAG
+import com.wbrawner.pihelper.STATUS_TEXT_TAG
+
+fun onMainScreen(testRule: ComposeTestRule, actions: MainScreenRobot.() -> Unit) =
+ MainScreenRobot(testRule).apply { actions() }
+
+class MainScreenRobot(private val testRule: ComposeTestRule) {
+ val context: Context = InstrumentationRegistry.getInstrumentation().context
+
+ init {
+ testRule.waitUntil {
+ testRule
+ .onAllNodesWithTag(MAIN_SCREEN_TAG)
+ .fetchSemanticsNodes().size == 1
+ }
+ }
+
+ infix fun onSettingsScreen(actions: AuthScreenRobot.() -> Unit) = AuthScreenRobot(testRule)
+ .run {
+ actions()
+ }
+
+ fun verifyStatus(status: String) {
+ testRule.waitUntil {
+ testRule.onAllNodes(hasTestTag(STATUS_TEXT_TAG).and(hasText(status)))
+ .fetchSemanticsNodes().size == 1
+ }
+ }
+
+ fun clickEnableButton() = testRule.onNode(hasTestTag(ENABLE_BUTTON_TAG)).performClick()
+
+ fun clickDisablePermanentlyButton() = testRule.onNode(hasTestTag(DISABLE_PERMANENT_BUTTON_TAG))
+ .performClick()
+
+ fun verifyErrorMessageIsDisplayed(message: String) {
+ testRule.waitUntil(2_000) {
+ testRule
+ .onAllNodesWithText(message, substring = true)
+ .fetchSemanticsNodes().size == 1
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/util/TestModule.kt b/app/src/androidTest/java/com/wbrawner/pihelper/util/TestModule.kt
new file mode 100644
index 0000000..71a5a07
--- /dev/null
+++ b/app/src/androidTest/java/com/wbrawner/pihelper/util/TestModule.kt
@@ -0,0 +1,2 @@
+package com.wbrawner.pihelper.util
+
diff --git a/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
index db3f553..57a5183 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AddScreen.kt
@@ -11,18 +11,19 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.wbrawner.pihelper.shared.Action
+import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Store
import com.wbrawner.pihelper.ui.DayNightPreview
import com.wbrawner.pihelper.ui.PihelperTheme
@@ -40,6 +41,7 @@ const val SCAN_BUTTON_TAG = "scanButton"
@Composable
fun AddScreen(store: Store) {
+ val effect by store.effects.collectAsState(initial = Effect.Empty)
val context = LocalContext.current
AddScreen(
scanNetwork = scan@{
@@ -76,18 +78,21 @@ fun AddScreen(store: Store) {
connectToPihole = {
store.dispatch(Action.Connect(it))
},
- store.state.value.loading
+ store.state.value.loading,
+ error = effect as? Effect.Error
)
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun AddScreen(
scanNetwork: () -> Unit,
connectToPihole: (String) -> Unit,
- loading: Boolean = false
+ loading: Boolean = false,
+ error: Effect.Error? = null
) {
val (host: String, setHost: (String) -> Unit) = remember { mutableStateOf("pi.hole") }
+ val keyboardController = LocalSoftwareKeyboardController.current
Column(
modifier = Modifier
.testTag(ADD_SCREEN_TAG)
@@ -124,8 +129,18 @@ fun AddScreen(
PrimaryButton(
modifier = Modifier.testTag(CONNECT_BUTTON_TAG),
text = "Connect to Pi-hole",
- onClick = { connectToPihole(host) }
+ onClick = {
+ keyboardController?.hide()
+ connectToPihole(host)
+ }
)
+ error?.let {
+ Text(
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Center,
+ text = "Connection failed: ${it.message}"
+ )
+ }
}
}
@@ -162,7 +177,7 @@ fun OrDivider() {
@DayNightPreview
fun AddScreen_Preview() {
PihelperTheme {
- AddScreen({}, {})
+ AddScreen({}, {}, error = Effect.Error("Something bad happened"))
}
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
index dd4bf89..e5127d6 100644
--- a/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/AuthScreen.kt
@@ -5,11 +5,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -21,6 +20,7 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.wbrawner.pihelper.shared.Action
import com.wbrawner.pihelper.shared.AuthenticationString
+import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Store
const val AUTH_SCREEN_TAG = "authScreen"
@@ -39,12 +40,20 @@ const val API_KEY_BUTTON_TAG = "apiKeyButton"
@Composable
fun AuthScreen(store: Store) {
- AuthScreen(dispatch = store::dispatch)
+ val effect by store.effects.collectAsState(initial = Effect.Empty)
+ AuthScreen(
+ dispatch = store::dispatch,
+ effect as? Effect.Error
+ )
}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
-fun AuthScreen(dispatch: (Action) -> Unit) {
+fun AuthScreen(
+ dispatch: (Action) -> Unit,
+ error: Effect.Error? = null
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
val (password: String, setPassword: (String) -> Unit) = remember { mutableStateOf("") }
val (apiKey: String, setApiKey: (String) -> Unit) = remember { mutableStateOf("") }
val scrollState = rememberScrollState()
@@ -85,6 +94,7 @@ fun AuthScreen(dispatch: (Action) -> Unit) {
modifier = Modifier.testTag(PASSWORD_BUTTON_TAG),
text = "Authenticate with Password"
) {
+ keyboardController?.hide()
dispatch(Action.Authenticate(AuthenticationString.Password(password)))
}
OrDivider()
@@ -103,8 +113,16 @@ fun AuthScreen(dispatch: (Action) -> Unit) {
modifier = Modifier.testTag(API_KEY_BUTTON_TAG),
text = "Authenticate with API Key"
) {
+ keyboardController?.hide()
dispatch(Action.Authenticate(AuthenticationString.Token(apiKey)))
}
+ error?.let {
+ Text(
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Center,
+ text = "Authentication failed: ${it.message}"
+ )
+ }
}
}
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
index c1b4814..0190059 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt
@@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.View
import android.view.WindowInsetsController
import android.view.animation.AnticipateInterpolator
-import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
@@ -21,7 +20,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.animation.doOnEnd
@@ -71,15 +69,12 @@ class MainActivity : AppCompatActivity() {
navController.navigate(state.route.name)
}
val effect by store.effects.collectAsState(initial = Effect.Empty)
- val context = LocalContext.current
LaunchedEffect(effect) {
when (effect) {
- is Effect.Error -> Toast.makeText(
- context,
- (effect as Effect.Error).message,
- Toast.LENGTH_SHORT
- ).show()
is Effect.Exit -> finish()
+ else -> {
+ // no-op
+ }
}
}
PihelperTheme {
diff --git a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
index 698d66d..95b4ff7 100644
--- a/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
+++ b/app/src/main/java/com/wbrawner/pihelper/MainScreen.kt
@@ -12,16 +12,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.material3.TopAppBarDefaults.smallTopAppBarColors
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.wbrawner.pihelper.shared.Action
+import com.wbrawner.pihelper.shared.Effect
import com.wbrawner.pihelper.shared.Status
import com.wbrawner.pihelper.shared.Store
import com.wbrawner.pihelper.ui.DayNightPreview
@@ -29,13 +29,43 @@ import com.wbrawner.pihelper.ui.PihelperTheme
import java.util.*
import kotlin.math.pow
import kotlin.math.roundToLong
+import com.wbrawner.pihelper.shared.State as PihelperState
+
+const val MAIN_SCREEN_TAG = "mainScreen"
+const val STATUS_TEXT_TAG = "statusText"
+const val ENABLE_BUTTON_TAG = "enableButton"
+const val DISABLE_TEN_BUTTON_TAG = "disableTenButton"
+const val DISABLE_THIRTY_BUTTON_TAG = "disableThirtyButton"
+const val DISABLE_FIVE_BUTTON_TAG = "disableFiveButton"
+const val DISABLE_CUSTOM_BUTTON_TAG = "disableCustomButton"
+const val DISABLE_CUSTOM_INPUT_TAG = "disableCustomInput"
+const val DISABLE_CUSTOM_INPUT_SECONDS_TAG = "disableCustomInputSeconds"
+const val DISABLE_CUSTOM_INPUT_MINUTES_TAG = "disableCustomInputMinutes"
+const val DISABLE_CUSTOM_INPUT_HOURS_TAG = "disableCustomInputHours"
+const val DISABLE_CUSTOM_CANCEL_BUTTON_TAG = "disableCustomCancelButton"
+const val DISABLE_CUSTOM_SUBMIT_BUTTON_TAG = "disableCustomSubmitButton"
+const val DISABLE_PERMANENT_BUTTON_TAG = "disablePermanentButton"
+
+@ExperimentalAnimationApi
+@Composable
+fun MainScreen(store: Store) {
+ val state by store.state.collectAsState()
+ val effect by store.effects.collectAsState(initial = Effect.Empty)
+ println(effect)
+ MainScreen(state = state, error = effect as? Effect.Error, dispatch = store::dispatch)
+}
@ExperimentalAnimationApi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun MainScreen(store: Store) {
- val state = store.state.collectAsState()
+fun MainScreen(
+ state: PihelperState,
+ error: Effect.Error? = null,
+ dispatch: (Action) -> Unit
+) {
+
Scaffold(
+ modifier = Modifier.testTag(MAIN_SCREEN_TAG),
topBar = {
TopAppBar(
title = { Text("Pi-helper") },
@@ -44,7 +74,7 @@ fun MainScreen(store: Store) {
titleContentColor = MaterialTheme.colorScheme.onBackground
),
actions = {
- IconButton(onClick = { store.dispatch(Action.About) }) {
+ IconButton(onClick = { dispatch(Action.About) }) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
@@ -64,19 +94,26 @@ fun MainScreen(store: Store) {
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
- val status = state.value.status
- LoadingSpinner(state.value.loading)
+ val status = state.status
+ LoadingSpinner(state.loading)
if (status != null) {
val enabled = status is Status.Enabled
StatusLabel(status)
AnimatedContent(targetState = enabled, contentAlignment = Alignment.Center) {
if (enabled) {
- DisableControls { duration -> store.dispatch(Action.Disable(duration)) }
+ DisableControls { duration -> dispatch(Action.Disable(duration)) }
} else {
- EnableControls { store.dispatch(Action.Enable) }
+ EnableControls { dispatch(Action.Enable) }
}
}
}
+ error?.let {
+ Text(
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Center,
+ text = "${it.message}"
+ )
+ }
}
}
}
@@ -97,6 +134,7 @@ fun StatusLabel(status: Status) {
else -> Color(0x00000000)
}
Text(
+ modifier = Modifier.testTag(STATUS_TEXT_TAG),
color = color,
fontWeight = FontWeight.Bold,
text = status.name.capitalize(Locale.US)
@@ -115,6 +153,7 @@ fun StatusLabel(status: Status) {
fun EnableControls(onClick: () -> Unit) {
Button(
modifier = Modifier
+ .testTag(ENABLE_BUTTON_TAG)
.fillMaxWidth()
.padding(16.dp),
colors = ButtonDefaults.buttonColors(
@@ -136,11 +175,26 @@ fun DisableControls(disable: (duration: Long?) -> Unit) {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
- PrimaryButton(text = "Disable for 10 seconds") { disable(10) }
- PrimaryButton(text = "Disable for 30 seconds") { disable(30) }
- PrimaryButton(text = "Disable for 5 minutes") { disable(300) }
- PrimaryButton(text = "Disable for custom time") { setDialogVisible(true) }
- PrimaryButton(text = "Disable permanently") { disable(null) }
+ PrimaryButton(
+ modifier = Modifier.testTag(DISABLE_TEN_BUTTON_TAG),
+ text = "Disable for 10 seconds"
+ ) { disable(10) }
+ PrimaryButton(
+ modifier = Modifier.testTag(DISABLE_THIRTY_BUTTON_TAG),
+ text = "Disable for 30 seconds"
+ ) { disable(30) }
+ PrimaryButton(
+ modifier = Modifier.testTag(DISABLE_FIVE_BUTTON_TAG),
+ text = "Disable for 5 minutes"
+ ) { disable(300) }
+ PrimaryButton(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_BUTTON_TAG),
+ text = "Disable for custom time"
+ ) { setDialogVisible(true) }
+ PrimaryButton(
+ modifier = Modifier.testTag(DISABLE_PERMANENT_BUTTON_TAG),
+ text = "Disable permanently"
+ ) { disable(null) }
CustomTimeDialog(dialogVisible, setDialogVisible) {
disable(it)
}
@@ -186,16 +240,22 @@ fun CustomTimeDialog(
shape = MaterialTheme.shapes.small,
onDismissRequest = { setVisible(false) },
dismissButton = {
- TextButton({ setVisible(false) }) {
+ TextButton(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_CANCEL_BUTTON_TAG),
+ onClick = { setVisible(false) }
+ ) {
Text("Cancel")
}
},
confirmButton = {
- TextButton(onClick = {
- // TODO: Move this math to the viewmodel or repository
- onTimeSelected(time.toLong() * (60.0.pow(duration.ordinal)).roundToLong())
- setVisible(false)
- }) {
+ TextButton(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_SUBMIT_BUTTON_TAG),
+ onClick = {
+ // TODO: Move this math to the store
+ onTimeSelected(time.toLong() * (60.0.pow(duration.ordinal)).roundToLong())
+ setVisible(false)
+ }
+ ) {
Text("Disable")
}
},
@@ -207,6 +267,7 @@ fun CustomTimeDialog(
verticalArrangement = Arrangement.Center
) {
OutlinedTextField(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_INPUT_TAG),
value = time,
onValueChange = { setTime(it) },
placeholder = { Text("Time to disable") }
@@ -216,16 +277,19 @@ fun CustomTimeDialog(
horizontalArrangement = Arrangement.Center
) {
DurationToggle(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_INPUT_SECONDS_TAG),
selected = duration == Duration.SECONDS,
onClick = { selectDuration(Duration.SECONDS) },
text = "Secs"
)
DurationToggle(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_INPUT_MINUTES_TAG),
selected = duration == Duration.MINUTES,
onClick = { selectDuration(Duration.MINUTES) },
text = "Mins"
)
DurationToggle(
+ modifier = Modifier.testTag(DISABLE_CUSTOM_INPUT_HOURS_TAG),
selected = duration == Duration.HOURS,
onClick = { selectDuration(Duration.HOURS) },
text = "Hours"
@@ -237,9 +301,14 @@ fun CustomTimeDialog(
}
@Composable
-fun DurationToggle(selected: Boolean, onClick: () -> Unit, text: String) {
+fun DurationToggle(
+ modifier: Modifier = Modifier,
+ selected: Boolean,
+ onClick: () -> Unit,
+ text: String
+) {
Row(
- modifier = Modifier.selectable(selected = selected, onClick = onClick),
+ modifier = modifier.selectable(selected = selected, onClick = onClick),
verticalAlignment = Alignment.CenterVertically
) {
Button(
diff --git a/app/src/main/res/drawable-v26/ic_shortcut_enable.xml b/app/src/main/res/drawable-v26/ic_shortcut_enable.xml
index 9400838..d620d4a 100644
--- a/app/src/main/res/drawable-v26/ic_shortcut_enable.xml
+++ b/app/src/main/res/drawable-v26/ic_shortcut_enable.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/drawable-v26/ic_shortcut_pause.xml b/app/src/main/res/drawable-v26/ic_shortcut_pause.xml
index 1c47093..c664143 100644
--- a/app/src/main/res/drawable-v26/ic_shortcut_pause.xml
+++ b/app/src/main/res/drawable-v26/ic_shortcut_pause.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/drawable/ic_pause_red.xml b/app/src/main/res/drawable/ic_pause_red.xml
new file mode 100644
index 0000000..4b3b5ac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pause_red.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_play_arrow.xml b/app/src/main/res/drawable/ic_play_arrow.xml
index 13af6d8..a3c077c 100644
--- a/app/src/main/res/drawable/ic_play_arrow.xml
+++ b/app/src/main/res/drawable/ic_play_arrow.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/app/src/main/res/drawable/ic_play_arrow_green.xml b/app/src/main/res/drawable/ic_play_arrow_green.xml
new file mode 100644
index 0000000..e624efc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play_arrow_green.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index 6bb7e56..a797d93 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -1,6 +1,7 @@
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index e0ec3a2..707095f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -9,7 +9,7 @@
#1B5E20
#FFFFFFFF
#000000
- #ffffff
+ #FFFBFE
@color/colorWhite
#00000000
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 427363a..b159f92 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -4,7 +4,7 @@
- @color/colorPrimaryDark
- @color/colorAccent
- @drawable/background_splash
- - @color/colorTransparent
+ - @color/colorWhite