diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt index 9e9653a984e..18c6249ab65 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt @@ -14,6 +14,8 @@ import io.homeassistant.companion.android.frontend.download.DownloadResult import io.homeassistant.companion.android.frontend.download.FrontendDownloadManager import io.homeassistant.companion.android.frontend.error.FrontendConnectionError import io.homeassistant.companion.android.frontend.error.FrontendConnectionErrorStateProvider +import io.homeassistant.companion.android.frontend.externalbus.FrontendExternalBusRepository +import io.homeassistant.companion.android.frontend.externalbus.outgoing.ResultMessage import io.homeassistant.companion.android.frontend.gesture.FrontendGestureHandler import io.homeassistant.companion.android.frontend.gesture.GestureResult import io.homeassistant.companion.android.frontend.handler.FrontendBusObserver @@ -69,6 +71,7 @@ internal class FrontendViewModel @VisibleForTesting constructor( initialPath: String?, webViewClientFactory: HAWebViewClientFactory, private val frontendBusObserver: FrontendBusObserver, + private val externalBusRepository: FrontendExternalBusRepository, private val urlManager: FrontendUrlManager, private val connectivityCheckRepository: ConnectivityCheckRepository, private val permissionManager: PermissionManager, @@ -83,6 +86,7 @@ internal class FrontendViewModel @VisibleForTesting constructor( savedStateHandle: SavedStateHandle, webViewClientFactory: HAWebViewClientFactory, frontendBusObserver: FrontendBusObserver, + externalBusRepository: FrontendExternalBusRepository, urlManager: FrontendUrlManager, connectivityCheckRepository: ConnectivityCheckRepository, permissionManager: PermissionManager, @@ -94,6 +98,7 @@ internal class FrontendViewModel @VisibleForTesting constructor( initialPath = savedStateHandle.toRoute().path, webViewClientFactory = webViewClientFactory, frontendBusObserver = frontendBusObserver, + externalBusRepository = externalBusRepository, urlManager = urlManager, connectivityCheckRepository = connectivityCheckRepository, permissionManager = permissionManager, @@ -324,6 +329,24 @@ internal class FrontendViewModel @VisibleForTesting constructor( permissionManager.clearPendingPermissionRequest() } + /** + * Called by the host after the NFC tag-write flow completes. + * + * Sends a `result` response back to the frontend correlated by [messageId]. Matches the legacy + * behavior of always reporting `success = true` with an empty payload: the underlying + * `NfcSetupActivity` only returns a non-zero result code on successful write, and the frontend + * silently ignores responses whose id it no longer tracks. + * + * @param messageId The correlation id received back from the activity result. Corresponds to + * the id of the originating `tag/write` request on success, or `0` (`RESULT_CANCELED`) on + * cancellation. + */ + fun onNfcWriteCompleted(messageId: Int) { + viewModelScope.launch { + externalBusRepository.send(ResultMessage.success(messageId)) + } + } + /** * Handles a swipe gesture detected on the WebView. * @@ -426,6 +449,10 @@ internal class FrontendViewModel @VisibleForTesting constructor( handleDownloadResult(result.result) } + is FrontendHandlerEvent.WriteNfcTag -> { + _events.tryEmit(FrontendEvent.NavigateToNfcWrite(messageId = result.messageId, tagId = result.tagId)) + } + is FrontendHandlerEvent.ConfigSent, is FrontendHandlerEvent.UnknownMessage, -> { diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt index 7c532d855f1..8736fbd522e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt @@ -143,6 +143,21 @@ data class OpenAssistPayload( @SerialName("haptic") data class HapticMessage(override val id: Int? = null, val payload: HapticType) : IncomingExternalBusMessage +/** + * Message requesting the app to open the NFC tag-write flow. + * + * The optional [TagWritePayload.tag] is a pre-filled tag identifier. When null or missing, the + * user is prompted to enter/scan a tag manually. Once handled, a [io.homeassistant.companion.android.frontend.externalbus.outgoing.ResultMessage.success] + * should be sent back to the frontend with the [id]. + */ +@Serializable +@SerialName("tag/write") +data class TagWriteMessage(override val id: Int? = null, val payload: TagWritePayload = TagWritePayload()) : + IncomingExternalBusMessage + +@Serializable +data class TagWritePayload(val tag: String? = null) + /** * Message carrying blob data for a file download initiated by the frontend. * diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt index e7268d46ef5..0c7119d0903 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.encodeToJsonElement /** @@ -39,6 +40,13 @@ data class ResultMessage( companion object { + fun success(id: Int?): ResultMessage { + return ResultMessage( + id = id, + result = JsonObject(emptyMap()), + ) + } + /** * Creates a config response with app capabilities. * diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt index 25239d05650..5472d31d65f 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt @@ -70,4 +70,13 @@ sealed interface FrontendHandlerEvent { * The ViewModel should process the [result] to emit appropriate UI feedback. */ data class DownloadCompleted(val result: DownloadResult) : FrontendHandlerEvent + + /** + * Frontend requested the NFC tag-write flow to be launched. + * + * @param messageId The correlation id from the incoming `tag/write` message; used to respond back + * to the frontend once the flow completes. + * @param tagId Optional pre-filled tag identifier. When null, the user is prompted to enter one. + */ + data class WriteNfcTag(val messageId: Int, val tagId: String?) : FrontendHandlerEvent } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt index bd59f565e15..d6eb51708ef 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt @@ -16,6 +16,7 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.Incoming import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistSettingsMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenSettingsMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.TagWriteMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpdateMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage import io.homeassistant.companion.android.frontend.externalbus.outgoing.ConfigResult @@ -174,6 +175,14 @@ class FrontendMessageHandler @Inject constructor( is HapticMessage -> FrontendHandlerEvent.PerformHaptic(message.payload) + is TagWriteMessage -> { + Timber.d("Tag write request received with id: ${message.id}") + FrontendHandlerEvent.WriteNfcTag( + messageId = message.id ?: -1, + tagId = message.payload.tag, + ) + } + is HandleBlobMessage -> { Timber.d("handleBlob called with filename=${message.filename}") val result = downloadManager.handleBlob(data = message.data, filename = message.filename) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEvent.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEvent.kt index 7471d4f2b1d..0088d9deac7 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEvent.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEvent.kt @@ -44,4 +44,15 @@ sealed interface FrontendEvent { * for forwarding the user's selection back to the ViewModel via [FrontendViewModel.switchServer]. */ data object ShowServerSwitcher : FrontendEvent + + /** + * Navigate to the NFC tag-write flow. + * + * The host is responsible for launching the corresponding activity contract and forwarding the + * result back to the ViewModel via [FrontendViewModel.onNfcWriteCompleted]. + * + * @param messageId Correlation id from the originating `tag/write` request. + * @param tagId Optional pre-filled tag identifier. + */ + data class NavigateToNfcWrite(val messageId: Int, val tagId: String?) : FrontendEvent } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigation.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigation.kt index fbf57e49886..119361927f2 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigation.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendNavigation.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.frontend.navigation import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -18,6 +19,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager.Comp import io.homeassistant.companion.android.frontend.FrontendScreen import io.homeassistant.companion.android.frontend.FrontendViewModel import io.homeassistant.companion.android.launch.HAStartDestinationRoute +import io.homeassistant.companion.android.nfc.WriteNfcTag import io.homeassistant.companion.android.settings.SettingsActivity import io.homeassistant.companion.android.util.getActivity import io.homeassistant.companion.android.webview.WebViewActivity @@ -74,6 +76,10 @@ internal fun NavGraphBuilder.frontendScreen( composable { val viewModel: FrontendViewModel = hiltViewModel() + val nfcWriteLauncher = rememberLauncherForActivityResult(WriteNfcTag()) { messageId -> + viewModel.onNfcWriteCompleted(messageId) + } + FrontendEventHandler( events = viewModel.events, onShowSnackbar = onShowSnackbar, @@ -90,6 +96,9 @@ internal fun NavGraphBuilder.frontendScreen( }, onOpenExternalLink = onOpenExternalLink, onShowServerSwitcher = { onShowServerSwitcher(viewModel::switchServer) }, + onNavigateToNfcWrite = { messageId, tagId -> + nfcWriteLauncher.launch(WriteNfcTag.Input(tagId = tagId, messageId = messageId)) + }, ) FrontendScreen( @@ -131,6 +140,7 @@ internal fun FrontendEventHandler( onNavigateToAssist: (serverId: Int, pipelineId: String?, startListening: Boolean) -> Unit, onOpenExternalLink: suspend (Uri) -> Unit, onShowServerSwitcher: () -> Unit, + onNavigateToNfcWrite: (messageId: Int, tagId: String?) -> Unit, ) { val resources = LocalResources.current LaunchedEffect(Unit) { @@ -163,6 +173,10 @@ internal fun FrontendEventHandler( is FrontendEvent.ShowServerSwitcher -> { onShowServerSwitcher() } + + is FrontendEvent.NavigateToNfcWrite -> { + onNavigateToNfcWrite(event.messageId, event.tagId) + } } } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/nfc/NfcSetupActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/nfc/NfcSetupActivity.kt index de6f3fbdb45..91dd115bbce 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/nfc/NfcSetupActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/nfc/NfcSetupActivity.kt @@ -35,8 +35,8 @@ class NfcSetupActivity : BaseActivity() { } companion object { - const val EXTRA_TAG_VALUE = "tag_value" - const val EXTRA_MESSAGE_ID = "message_id" + private const val EXTRA_TAG_VALUE = "tag_value" + private const val EXTRA_MESSAGE_ID = "message_id" const val NAV_WELCOME = "nfc_welcome" const val NAV_READ = "nfc_read" diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/nfc/WriteNfcTag.kt b/app/src/main/kotlin/io/homeassistant/companion/android/nfc/WriteNfcTag.kt index 7b173ae7391..58cca407997 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/nfc/WriteNfcTag.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/nfc/WriteNfcTag.kt @@ -9,10 +9,7 @@ class WriteNfcTag : ActivityResultContract() { data class Input(val tagId: String? = null, val messageId: Int = -1) override fun createIntent(context: Context, input: Input): Intent { - return Intent(context, NfcSetupActivity::class.java).apply { - putExtra(NfcSetupActivity.EXTRA_MESSAGE_ID, input.messageId) - putExtra(NfcSetupActivity.EXTRA_TAG_VALUE, input.tagId) - } + return NfcSetupActivity.newInstance(context, input.tagId, input.messageId) } override fun parseResult(resultCode: Int, intent: Intent?): Int { diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt index 70509304fb7..8b10bfdc593 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt @@ -10,7 +10,9 @@ import io.homeassistant.companion.android.common.util.GestureDirection import io.homeassistant.companion.android.frontend.download.DownloadResult import io.homeassistant.companion.android.frontend.download.FrontendDownloadManager import io.homeassistant.companion.android.frontend.error.FrontendConnectionError +import io.homeassistant.companion.android.frontend.externalbus.FrontendExternalBusRepository import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticType +import io.homeassistant.companion.android.frontend.externalbus.outgoing.ResultMessage import io.homeassistant.companion.android.frontend.gesture.FrontendGestureHandler import io.homeassistant.companion.android.frontend.gesture.GestureResult import io.homeassistant.companion.android.frontend.handler.FrontendBusObserver @@ -41,6 +43,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonObject import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Assertions.assertTrue @@ -58,6 +61,7 @@ class FrontendViewModelTest { private val webViewClientFactory: HAWebViewClientFactory = mockk(relaxed = true) private val frontendBusObserver: FrontendBusObserver = mockk(relaxed = true) + private val externalBusRepository: FrontendExternalBusRepository = mockk(relaxed = true) private val urlManager: FrontendUrlManager = mockk(relaxed = true) private val connectivityCheckRepository: ConnectivityCheckRepository = mockk(relaxed = true) private val permissionManager: PermissionManager = mockk(relaxed = true) @@ -84,6 +88,7 @@ class FrontendViewModelTest { initialPath = path, webViewClientFactory = webViewClientFactory, frontendBusObserver = frontendBusObserver, + externalBusRepository = externalBusRepository, urlManager = urlManager, connectivityCheckRepository = connectivityCheckRepository, permissionManager = permissionManager, @@ -629,6 +634,44 @@ class FrontendViewModelTest { assertTrue(events.any { it is FrontendEvent.NavigateToAssistSettings }) job.cancel() } + + @Test + fun `Given WriteNfcTag handler event when collected then NavigateToNfcWrite is emitted`() = runTest { + val messageFlow = MutableSharedFlow() + every { frontendBusObserver.messageResults() } returns messageFlow + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + + val viewModel = createViewModel() + + viewModel.events.test { + advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds) + + messageFlow.emit(FrontendHandlerEvent.WriteNfcTag(messageId = 42, tagId = "tag-abc")) + + assertEquals(FrontendEvent.NavigateToNfcWrite(messageId = 42, tagId = "tag-abc"), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `Given onNfcWriteCompleted when called then sends empty-result ResultMessage back to frontend`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + + val viewModel = createViewModel() + + viewModel.onNfcWriteCompleted(messageId = 42) + advanceUntilIdle() + + coVerify { + externalBusRepository.send( + ResultMessage(id = 42, success = true, result = JsonObject(emptyMap())), + ) + } + } } @Nested diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt index 5396a19dbbc..2467cd90b36 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt @@ -102,6 +102,30 @@ class IncomingExternalBusMessageTest { assertEquals("file.pdf", blobMessage.filename) } + @Test + fun `Given tag-write JSON with tag then parses to TagWriteMessage with tag`() { + val json = """{"type":"tag/write","id":11,"payload":{"tag":"abc-123"}}""" + + val message = frontendExternalBusJson.decodeFromString(json) + + assertInstanceOf(TagWriteMessage::class.java, message) + val tagMessage = message as TagWriteMessage + assertEquals(11, tagMessage.id) + assertEquals("abc-123", tagMessage.payload.tag) + } + + @Test + fun `Given tag-write JSON without payload then parses to TagWriteMessage with null tag`() { + val json = """{"type":"tag/write","id":12}""" + + val message = frontendExternalBusJson.decodeFromString(json) + + assertInstanceOf(TagWriteMessage::class.java, message) + val tagMessage = message as TagWriteMessage + assertEquals(12, tagMessage.id) + assertNull(tagMessage.payload.tag) + } + @Test fun `Given unknown type JSON then parses to UnknownIncomingMessage`() { val json = """{"type":"future-feature","id":99,"payload":{"data":"something"}}""" diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt index 800640a2136..9df0c6bad54 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt @@ -21,6 +21,8 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssi import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistPayload import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistSettingsMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenSettingsMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.TagWriteMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.TagWritePayload import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpdateMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage import io.homeassistant.companion.android.frontend.externalbus.outgoing.OutgoingExternalBusMessage @@ -269,6 +271,49 @@ class FrontendMessageHandlerTest { } } + @Test + fun `Given tag write message with tag when messageResults then emits WriteNfcTag with tagId`() = runTest { + val message = TagWriteMessage(id = 42, payload = TagWritePayload(tag = "abc-123")) + every { externalBusRepository.incomingMessages() } returns flowOf(message) + + handler.messageResults().test { + val result = awaitItem() + assertTrue(result is FrontendHandlerEvent.WriteNfcTag) + val nfcEvent = result as FrontendHandlerEvent.WriteNfcTag + assertEquals(42, nfcEvent.messageId) + assertEquals("abc-123", nfcEvent.tagId) + expectNoEvents() + } + } + + @Test + fun `Given tag write message without tag when messageResults then emits WriteNfcTag with null tagId`() = runTest { + val message = TagWriteMessage(id = 7, payload = TagWritePayload(tag = null)) + every { externalBusRepository.incomingMessages() } returns flowOf(message) + + handler.messageResults().test { + val result = awaitItem() + assertTrue(result is FrontendHandlerEvent.WriteNfcTag) + val nfcEvent = result as FrontendHandlerEvent.WriteNfcTag + assertEquals(7, nfcEvent.messageId) + assertEquals(null, nfcEvent.tagId) + expectNoEvents() + } + } + + @Test + fun `Given tag write message without id when messageResults then emits WriteNfcTag with messageId -1`() = runTest { + val message = TagWriteMessage(id = null) + every { externalBusRepository.incomingMessages() } returns flowOf(message) + + handler.messageResults().test { + val result = awaitItem() + assertTrue(result is FrontendHandlerEvent.WriteNfcTag) + assertEquals(-1, (result as FrontendHandlerEvent.WriteNfcTag).messageId) + expectNoEvents() + } + } + @Test fun `Given unknown message when messageResults then emits UnknownMessage`() = runTest { val message = UnknownIncomingMessage(discriminator = "unknown-type", content = JsonPrimitive("unknown-type")) diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEventHandlerTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEventHandlerTest.kt index b9c7803733c..e7455f52bed 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEventHandlerTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/navigation/FrontendEventHandlerTest.kt @@ -49,6 +49,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = {}, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -77,6 +78,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = {}, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -107,6 +109,7 @@ class FrontendEventHandlerTest { }, onOpenExternalLink = {}, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -143,6 +146,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = {}, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -167,6 +171,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = { uri -> capturedUri = uri }, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -191,6 +196,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = {}, onShowServerSwitcher = { serverSwitcherShown = true }, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -218,6 +224,7 @@ class FrontendEventHandlerTest { onNavigateToAssist = { _, _, _ -> }, onOpenExternalLink = {}, onShowServerSwitcher = {}, + onNavigateToNfcWrite = { _, _ -> }, ) } @@ -228,4 +235,62 @@ class FrontendEventHandlerTest { assertEquals(true, settingsNavigated) assertEquals(SettingsActivity.Deeplink.Developer, deepLink) } + + @Test + fun `Given NavigateToNfcWrite event then onNavigateToNfcWrite is called with messageId and tagId`() = runTest { + var capturedMessageId: Int? = null + var capturedTagId: String? = "not-captured" + val events = MutableSharedFlow() + + composeTestRule.setContent { + FrontendEventHandler( + events = events, + onShowSnackbar = { _, _ -> false }, + onNavigateToSettings = {}, + onNavigateToAssist = { _, _, _ -> }, + onOpenExternalLink = {}, + onShowServerSwitcher = {}, + onNavigateToNfcWrite = { messageId, tagId -> + capturedMessageId = messageId + capturedTagId = tagId + }, + ) + } + + composeTestRule.waitForIdle() + events.emit(FrontendEvent.NavigateToNfcWrite(messageId = 42, tagId = "tag-abc")) + composeTestRule.waitForIdle() + + assertEquals(42, capturedMessageId) + assertEquals("tag-abc", capturedTagId) + } + + @Test + fun `Given NavigateToNfcWrite event without tagId then onNavigateToNfcWrite is called with null tagId`() = runTest { + var capturedMessageId: Int? = null + var capturedTagId: String? = "not-captured" + val events = MutableSharedFlow() + + composeTestRule.setContent { + FrontendEventHandler( + events = events, + onShowSnackbar = { _, _ -> false }, + onNavigateToSettings = {}, + onNavigateToAssist = { _, _, _ -> }, + onOpenExternalLink = {}, + onShowServerSwitcher = {}, + onNavigateToNfcWrite = { messageId, tagId -> + capturedMessageId = messageId + capturedTagId = tagId + }, + ) + } + + composeTestRule.waitForIdle() + events.emit(FrontendEvent.NavigateToNfcWrite(messageId = 7, tagId = null)) + composeTestRule.waitForIdle() + + assertEquals(7, capturedMessageId) + assertEquals(null, capturedTagId) + } }