Repository: govuk-one-login/mobile-android-cri-orchestrator
Pull request #195: feat: add ViewModel for SelectDocumentScreen
Author: login: obinns-dd
Labels: name: enhancement
description: New feature or request
Created: 2025-03-19T17:02:09Z | Merged: 2025-03-24T13:40:01Z

# DCMAW-8054: Android | Document Selection | Do you have a passport screen

https://govukverify.atlassian.net/browse/DCMAW-8054


Implemented the logic for `SelectPassportScreen`.
This includes analytics and document selection, but not navigation.

[//]: # (e.g. "- Create 'androidLibrary' Gradle module.")

## Evidence of the change

See unit tests included with this change.

[//]: # (Screenshots / uploaded videos go here)

## Checklist

- [x] Check against acceptance criteria
- [x] Add automated tests
- [x] Self-review code



Diff:
diff --git a/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/analytics/SelectDocumentScreenId.kt b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/analytics/SelectDocumentScreenId.kt
new file mode 100644
index 000000000..5375860b1
--- /dev/null
+++ b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/analytics/SelectDocumentScreenId.kt
@@ -0,0 +1,9 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics
+
+import uk.gov.onelogin.criorchestrator.libraries.analytics.ScreenId
+
+enum class SelectDocumentScreenId(
+    override val rawId: String,
+) : ScreenId {
+    SelectPassport(rawId = "ccb1caa0-0331-4122-9f35-78ce28e0b4b5"),
+}
diff --git a/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreen.kt b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreen.kt
index 58c1dd751..98efe3d39 100644
--- a/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreen.kt
+++ b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreen.kt
@@ -2,9 +2,17 @@ package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
 
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.collections.immutable.PersistentList
 import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toPersistentList
 import uk.gov.android.ui.componentsv2.inputs.radio.RadioSelectionTitle
 import uk.gov.android.ui.componentsv2.inputs.radio.TitleType
 import uk.gov.android.ui.patterns.leftalignedscreen.LeftAlignedScreen
@@ -18,9 +26,43 @@ import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
 import uk.gov.onelogin.criorchestrator.libraries.composeutils.LightDarkBothLocalesPreview
 
 @Composable
-fun SelectPassportScreen(modifier: Modifier = Modifier) {
+internal fun SelectPassportScreen(
+    viewModel: SelectPassportViewModel,
+    modifier: Modifier = Modifier,
+) {
+    val state by viewModel.state.collectAsStateWithLifecycle()
+
+    LaunchedEffect(Unit) {
+        viewModel.onScreenStart()
+    }
+    SelectPassportScreenContent(
+        title = stringResource(state.titleId),
+        modifier = modifier,
+        readMoreButtonTitle = stringResource(state.readMoreButtonTextId),
+        onReadMoreClick = viewModel::onReadMoreClick,
+        items =
+            state.options
+                .map { stringResource(it) }
+                .toPersistentList(),
+        confirmButtonText = stringResource(state.buttonTextId),
+        onConfirmSelection = viewModel::onConfirmSelection,
+    )
+}
+
+@Composable
+@Suppress("LongParameterList")
+internal fun SelectPassportScreenContent(
+    title: String,
+    readMoreButtonTitle: String,
+    onReadMoreClick: () -> Unit,
+    items: PersistentList<String>,
+    confirmButtonText: String,
+    onConfirmSelection: (Int) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    var selectedItem by remember { mutableStateOf<Int?>(null) }
     LeftAlignedScreen(
-        title = stringResource(R.string.selectdocument_passport_title),
+        title = title,
         modifier = modifier,
         body =
             persistentListOf(
@@ -34,8 +76,8 @@ fun SelectPassportScreen(modifier: Modifier = Modifier) {
                 ),
                 Text(stringResource(R.string.selectdocument_passport_expiry)),
                 SecondaryButton(
-                    text = stringResource(R.string.selectdocument_passport_readmore_button),
-                    onClick = { },
+                    text = readMoreButtonTitle,
+                    onClick = onReadMoreClick,
                 ),
                 Selection(
                     title =
@@ -43,28 +85,42 @@ fun SelectPassportScreen(modifier: Modifier = Modifier) {
                             stringResource(R.string.selectdocument_passport_title),
                             TitleType.Heading,
                         ),
-                    items =
-                        persistentListOf(
-                            stringResource(R.string.selectdocument_passport_selection_yes),
-                            stringResource(R.string.selectdocument_passport_selection_no),
-                        ),
-                    selectedItem = null,
-                    onItemSelected = { },
+                    items = items,
+                    selectedItem = selectedItem,
+                    onItemSelected = {
+                        selectedItem = it
+                    },
                 ),
             ),
         primaryButton =
             LeftAlignedScreenButton(
-                text = stringResource(R.string.selectdocument_passport_continuebutton),
+                text = confirmButtonText,
                 onClick = {
+                    selectedItem?.let {
+                        onConfirmSelection(it)
+                    }
                 },
+                enabled = selectedItem != null,
             ),
     )
 }
 
 @LightDarkBothLocalesPreview
 @Composable
+// DCMAW-8054 | AC5: Welsh translation
 internal fun PreviewPassportSelectionScreen() {
     GdsTheme {
-        SelectPassportScreen()
+        SelectPassportScreenContent(
+            title = stringResource(R.string.selectdocument_passport_title),
+            readMoreButtonTitle = stringResource(R.string.selectdocument_passport_readmore_button),
+            onReadMoreClick = { },
+            items =
+                listOf(
+                    R.string.selectdocument_passport_selection_yes,
+                    R.string.selectdocument_passport_selection_no,
+                ).map { stringResource(it) }.toPersistentList(),
+            confirmButtonText = stringResource(R.string.selectdocument_passport_continuebutton),
+            onConfirmSelection = { },
+        )
     }
 }
diff --git a/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportState.kt b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportState.kt
new file mode 100644
index 000000000..146aa3e26
--- /dev/null
+++ b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportState.kt
@@ -0,0 +1,20 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.annotation.StringRes
+import kotlinx.collections.immutable.PersistentList
+
+data class SelectPassportState(
+    @StringRes val titleId: Int,
+    @StringRes val readMoreButtonTextId: Int,
+    val options: PersistentList<Int>,
+    @StringRes val buttonTextId: Int,
+    var selection: PassportSelection,
+)
+
+sealed class PassportSelection {
+    data object Unselected : PassportSelection()
+
+    data object Selected : PassportSelection()
+
+    data object NotSelected : PassportSelection()
+}
diff --git a/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
new file mode 100644
index 000000000..9d67f9d8e
--- /dev/null
+++ b/features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
@@ -0,0 +1,52 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    private val _state =
+        MutableStateFlow(
+            SelectPassportState(
+                titleId = R.string.selectdocument_passport_title,
+                readMoreButtonTextId = R.string.selectdocument_passport_readmore_button,
+                options =
+                    persistentListOf(
+                        R.string.selectdocument_passport_selection_yes,
+                        R.string.selectdocument_passport_selection_no,
+                    ),
+                buttonTextId = R.string.selectdocument_passport_continuebutton,
+                selection = PassportSelection.Unselected,
+            ),
+        )
+    val state: StateFlow<SelectPassportState> = _state
+
+    fun onScreenStart() {
+        analytics.trackScreen(
+            SelectDocumentScreenId.SelectPassport,
+            state.value.titleId,
+        )
+    }
+
+    fun onReadMoreClick() {
+        analytics.trackButtonEvent(state.value.readMoreButtonTextId)
+    }
+
+    fun onConfirmSelection(selectedIndex: Int) {
+        analytics.trackFormSubmission(
+            buttonText = state.value.buttonTextId,
+            response = state.value.options[selectedIndex],
+        )
+
+        when (selectedIndex) {
+            0 -> _state.value = _state.value.copy(selection = PassportSelection.Selected)
+            1 -> _state.value = _state.value.copy(selection = PassportSelection.NotSelected)
+        }
+    }
+}
diff --git a/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt b/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
new file mode 100644
index 000000000..3cf86e016
--- /dev/null
+++ b/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
@@ -0,0 +1,169 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import android.content.Context
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onSibling
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeUp
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+
+@RunWith(AndroidJUnit4::class)
+class SelectPassportScreenTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private lateinit var readMoreButton: SemanticsMatcher
+    private lateinit var yesOption: SemanticsMatcher
+    private lateinit var noOption: SemanticsMatcher
+    private lateinit var confirmButton: SemanticsMatcher
+
+    private val viewModel =
+        spy(
+            SelectPassportViewModel(
+                analytics = mock(),
+            ),
+        )
+
+    @Before
+    fun setUp() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        readMoreButton = hasText(context.getString(R.string.selectdocument_passport_readmore_button))
+        yesOption = hasText(context.getString(R.string.selectdocument_passport_selection_yes))
+        noOption = hasText(context.getString(R.string.selectdocument_passport_selection_no))
+        confirmButton = hasText(context.getString(R.string.selectdocument_passport_continuebutton))
+
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+    }
+
+    @Test
+    fun `when screen is started, it calls the view model`() {
+        verify(viewModel).onScreenStart()
+    }
+
+    @Test
+    fun `when screen is started, no item is selected`() {
+        swipeToAdditionalContent()
+
+        verify(viewModel).onScreenStart()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+    }
+
+    @Test
+    fun `when read more is selected, it calls the view model`() {
+        composeTestRule
+            .onNode(readMoreButton)
+            .performClick()
+
+        verify(viewModel).onReadMoreClick()
+    }
+
+    @Test
+    fun `when yes is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    @Test
+    fun `when no is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    // DCMAW-8054 | AC4: User doesn’t select an option
+    @Test
+    fun `when user has not made choice yet, the confirm button is disabled`() {
+        composeTestRule
+            .onNode(confirmButton)
+            .performClick()
+
+        composeTestRule
+            .onNode(confirmButton)
+            .assertIsNotEnabled()
+
+        verify(viewModel, never()).onConfirmSelection(any())
+    }
+
+    @Test
+    fun `when yes is selected and confirm is tapped, it calls the view model`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .performClick()
+
+        composeTestRule
+            .onNode(confirmButton)
+            .assertIsEnabled()
+            .performClick()
+
+        verify(viewModel).onConfirmSelection(0)
+    }
+
+    @Test
+    fun `when no is selected and confirm is tapped, it calls the view model`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .performClick()
+
+        composeTestRule
+            .onNode(confirmButton)
+            .assertIsEnabled()
+            .performClick()
+
+        verify(viewModel).onConfirmSelection(1)
+    }
+
+    private fun swipeToAdditionalContent() {
+        composeTestRule
+            .onNode(
+                readMoreButton,
+            ).onParent()
+            .performTouchInput {
+                swipeUp()
+            }
+    }
+}
diff --git a/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt b/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
new file mode 100644
index 000000000..ed85d375b
--- /dev/null
+++ b/features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
@@ -0,0 +1,94 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+import uk.gov.onelogin.criorchestrator.libraries.testing.MainDispatcherExtension
+
+@ExtendWith(MainDispatcherExtension::class)
+class SelectPassportViewModelTest {
+    private val analyticsLogger = mock<SelectDocumentAnalytics>()
+
+    private val viewModel by lazy {
+        SelectPassportViewModel(
+            analytics =
+            analyticsLogger,
+        )
+    }
+
+    // DCMAW-8054 | AC1: Viewing passport screen
+    @Test
+    fun `when screen starts, it sends analytics`() {
+        viewModel.onScreenStart()
+
+        verify(analyticsLogger)
+            .trackScreen(
+                id = SelectDocumentScreenId.SelectPassport,
+                title = R.string.selectdocument_passport_title,
+            )
+    }
+
+    // DCMAW-8054 | AC2: User has a passport
+    // DCMAW-8054 | AC3: User doesn’t have a passport
+    @ParameterizedTest
+    @ValueSource(ints = [0, 1])
+    fun `when selection is confirmed, it sends analytics`(selection: Int) {
+        viewModel.onConfirmSelection(selection)
+
+        val item =
+            listOf(
+                R.string.selectdocument_passport_selection_yes,
+                R.string.selectdocument_passport_selection_no,
+            )[selection]
+
+        verify(analyticsLogger)
+            .trackFormSubmission(
+                R.string.selectdocument_passport_continuebutton,
+                item,
+            )
+    }
+
+    @Test
+    fun `when passport is selected, the state is updated`() {
+        runTest {
+            viewModel.onConfirmSelection(0)
+            assertEquals(PassportSelection.Selected, viewModel.state.first().selection)
+        }
+    }
+
+    @Test
+    fun `when passport is not selected, the state is updated`() {
+        runTest {
+            viewModel.onConfirmSelection(1)
+            assertEquals(PassportSelection.NotSelected, viewModel.state.first().selection)
+        }
+    }
+
+    @Test
+    fun `when the read more button is pressed, it sends analytics`() {
+        viewModel.onReadMoreClick()
+
+        verify(analyticsLogger)
+            .trackButtonEvent(R.string.selectdocument_passport_readmore_button)
+    }
+
+    // DCMAW-8054 | AC8: User wants to learn about different document options
+    @Disabled("This AC has not yet been implemented")
+    @Test
+    fun `when the read more button is pressed, the user sees the types of photo ID screen`() {
+        viewModel.onReadMoreClick()
+
+        assertTrue(false)
+    }
+}
diff --git a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[1.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_cy.png b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[1.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_cy.png
index a2b541d12..cd8dd62f4 100644
--- a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[1.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_cy.png
+++ b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[1.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_cy.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e2b4b76a99c4d21a052af50999c3bab085f3a0db8d5f54cda8d3b30fa3dd7396
-size 78961
+oid sha256:6030c42f40376a76f6139eee7180fbd5b6681d454d83e4c1c5dad68d75b1c10f
+size 78826
diff --git a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[2.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_dark_night.png b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[2.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_dark_night.png
index 95a92bf3c..207733d97 100644
--- a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[2.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_dark_night.png
+++ b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[2.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_dark_night.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:ae1ca566725cf37c422da238a67da3d12ae2c8053ab98b916c210f6697ff0214
-size 66488
+oid sha256:0f2507da1f5c25803248baa2426d5fb9a2a9986dd01b2a81ee9b1425df16ae60
+size 66213
diff --git a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[3.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_light.png b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[3.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_light.png
index d22b3dba7..746c9c497 100644
--- a/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[3.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_light.png
+++ b/features/select-doc/internal/src/test/snapshots/images/uk.gov.onelogin.criorchestrator.features.selectdoc.internal_ScreenshotTest_screenshot[3.uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport.SelectPassportScreenKt_PreviewPassportSelectionScreen_]_light.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:396c4e9a29918c373da7a9a8f1c5c29240aadcf3d0626cdd4991657bc2eebb37
-size 68114
+oid sha256:3d9d78161f8875c654cadfbfaa8b673bf01ab2c1ef808c334b5f64d8754679f5
+size 67858
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9f763cf99..6fc6b3f03 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -37,7 +37,7 @@ testparameterinjector = "1.18"
 turbine = "1.2.0"
 uk-gov-logging = "0.19.0" # https://github.com/orgs/govuk-one-login/packages?repo_name=mobile-android-logging
 uk-gov-networking = "0.3.4"
-uk-gov-ui = "7.11.0"
+uk-gov-ui = "7.12.0"
 
 [libraries]
 android-build-tool = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
diff --git a/libraries/analytics/src/main/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/Analytics.kt b/libraries/analytics/src/main/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/Analytics.kt
index dd5d77368..99070271c 100644
--- a/libraries/analytics/src/main/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/Analytics.kt
+++ b/libraries/analytics/src/main/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/Analytics.kt
@@ -26,6 +26,17 @@ abstract class Analytics(
         ),
     )
 
+    fun trackFormSubmission(
+        @StringRes buttonText: Int,
+        @StringRes response: Int,
+    ) = analyticsLogger.logEventV3Dot1(
+        TrackEvent.Form(
+            text = resourceProvider.getEnglishString(buttonText),
+            response = resourceProvider.getEnglishString(response),
+            params = requiredParameters,
+        ),
+    )
+
     fun trackScreen(
         id: ScreenId,
         @StringRes title: Int,
diff --git a/libraries/analytics/src/test/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/AnalyticsTest.kt b/libraries/analytics/src/test/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/AnalyticsTest.kt
index f489eccbb..8ae0c4f82 100644
--- a/libraries/analytics/src/test/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/AnalyticsTest.kt
+++ b/libraries/analytics/src/test/kotlin/uk/gov/onelogin/criorchestrator/libraries/analytics/AnalyticsTest.kt
@@ -48,6 +48,9 @@ class AnalyticsTest {
         @StringRes
         private val buttonText = R.string.test_button_text
 
+        @StringRes
+        private val formResponse = R.string.test_form_response
+
         private val screenId = TestScreenId.ExampleScreen
 
         @StringRes
@@ -61,6 +64,20 @@ class AnalyticsTest {
         )
     }
 
+    private fun verifyTrackFormEvent() {
+        verify(analyticsLogger).logEventV3Dot1(
+            TrackEvent.Form(
+                text = "Continue",
+                response = "Yes",
+                params =
+                    RequiredParameters(
+                        taxonomyLevel2 = TaxonomyLevel2.DOCUMENT_CHECKING_APP,
+                        taxonomyLevel3 = TaxonomyLevel3.UNDEFINED,
+                    ),
+            ),
+        )
+    }
+
     private fun verifyTrackButtonEvent() {
         verify(analyticsLogger).logEventV3Dot1(
             TrackEvent.Button(
@@ -102,6 +119,26 @@ class AnalyticsTest {
         verifyTrackButtonEvent()
     }
 
+    @Test
+    @Config(qualifiers = "en")
+    fun `given english locale, track form tracks correctly`() {
+        analytics.trackFormSubmission(
+            buttonText = buttonText,
+            response = formResponse,
+        )
+        verifyTrackFormEvent()
+    }
+
+    @Test
+    @Config(qualifiers = "cy")
+    fun `given welsh locale, track form tracks correctly`() {
+        analytics.trackFormSubmission(
+            buttonText = buttonText,
+            response = formResponse,
+        )
+        verifyTrackFormEvent()
+    }
+
     @Test
     @Config(qualifiers = "en")
     fun `given english locale, track screen tracks correctly`() {
diff --git a/libraries/analytics/src/testFixtures/res/values/strings.xml b/libraries/analytics/src/testFixtures/res/values/strings.xml
index 489dc9674..0c09d401f 100644
--- a/libraries/analytics/src/testFixtures/res/values/strings.xml
+++ b/libraries/analytics/src/testFixtures/res/values/strings.xml
@@ -2,6 +2,7 @@
 <resources>
     <string name="test_string">test string</string>
 
+    <string name="test_form_response">Yes</string>
     <string name="test_button_text">Continue</string>
     <string name="test_screen_title">Welcome to this app</string>
 


Reviews:
user: login: jonnyandrew
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:34:37Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:43:30Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:28:38Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:34:47Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:08:18Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:11:42Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:12:22Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:32:49Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:42:56Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T12:15:46Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:33:26Z
user: login: emanuel-gds
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:35:50Z
user: login: obinns-dd
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:38:31Z

Review comments:
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
+
+    val buttonTextId = R.string.selectdocument_passport_continuebutton
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: jonnyandrew
body: The emerging convention for view models is to expose a single `Flow` of state that can be consumed in the screen like so:

```
val state by viewModel.state.collectAsState()
```

There's an example here: https://developer.android.com/develop/ui/compose/architecture#example

And a few other usages in the project.
created_at: 2025-03-20T14:18:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: jonnyandrew
body: We might be able to simplify this by keeping the UI state in the Composables if possible (https://developer.android.com/develop/ui/compose/state-hoisting#composables-as-state-owner)

Then the view model only needs to expose a single function for selection:

```kt
fun onConfirmSelection(selectedIndex: Int)
```
created_at: 2025-03-20T14:23:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: jonnyandrew
body: I appreciate the intention of hoisting these variables as they're used in both the UI and the analytics.

However I'm unsure on the value of doing this when the variables held in `R.string` are also shared across the whole module, as a source of truth.

The downside of hoisting these up into the view model is that we need to drill them down through the layers of Composables, which could quickly become unweildy as a pattern (when Composables are deeply nested).
created_at: 2025-03-20T14:26:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
+
+    val buttonTextId = R.string.selectdocument_passport_continuebutton
+
+    fun onScreenStart() {
+        analytics.trackScreen(
+            SelectDocumentScreenId.SelectPassport,
+            titleId,
+        )
+    }
+
+    fun readMoreButtonAction() {
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: jonnyandrew
body: (nitpick)

```suggestion
    fun onReadMoreClick() {
```
created_at: 2025-03-20T14:27:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import android.content.Context
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+
+@RunWith(AndroidJUnit4::class)
+class SelectPassportScreenTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private lateinit var readMoreButton: SemanticsMatcher
+    private lateinit var yesOption: SemanticsMatcher
+    private lateinit var noOption: SemanticsMatcher
+    private lateinit var confirmButton: SemanticsMatcher
+
+    private val viewModel =
+        spy(
+            SelectPassportViewModel(
+                analytics = mock(),
+            ),
+        )
+
+    @Before
+    fun setUp() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        readMoreButton = hasText(context.getString(R.string.selectdocument_passport_readmore_button))
+        yesOption = hasContentDescription(context.getString(R.string.selectdocument_passport_selection_yes))
+        noOption = hasContentDescription(context.getString(R.string.selectdocument_passport_selection_no))
+        confirmButton = hasText(context.getString(R.string.selectdocument_passport_continuebutton))
+    }
+
+    @Test
+    fun `when screen started, it calls the view model`() {
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+
+        verify(viewModel).onScreenStart()
+    }
+
+    @Test
+    fun `when read more is selected, it calls the view model`() {
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+
+        composeTestRule
+            .onNode(readMoreButton)
+            .performClick()
+
+        verify(viewModel).readMoreButtonAction()
+    }
+
+    @Ignore("Radio buttons are not appearing in node hierarchy - further investigation required")
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
user: login: jonnyandrew
body: TODO
created_at: 2025-03-20T14:27:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+import uk.gov.onelogin.criorchestrator.libraries.testing.MainDispatcherExtension
+
+@ExtendWith(MainDispatcherExtension::class)
+class SelectPassportViewModelTest {
+    private val analyticsLogger = mock<SelectDocumentAnalytics>()
+
+    private val viewModel by lazy {
+        SelectPassportViewModel(
+            analytics =
+            analyticsLogger,
+        )
+    }
+
+    @Test
+    fun `when screen starts, it sends analytics`() {
+        viewModel.onScreenStart()
+
+        verify(analyticsLogger)
+            .trackScreen(
+                id = SelectDocumentScreenId.SelectPassport,
+                title = R.string.selectdocument_passport_title,
+            )
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
user: login: jonnyandrew
body: Please can we also add an integration test for the analytics that contributes to the analytics test report? (See the other `..AnalyticsTest` classes for how that's done)
created_at: 2025-03-20T14:31:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: obinns-dd
body: The thinking was that if we change a string in the view, we can't end up with mismatching analytics events being sent 👀 it felt a bit safer.
created_at: 2025-03-20T14:43:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
+
+    val buttonTextId = R.string.selectdocument_passport_continuebutton
+
+    fun onScreenStart() {
+        analytics.trackScreen(
+            SelectDocumentScreenId.SelectPassport,
+            titleId,
+        )
+    }
+
+    fun readMoreButtonAction() {
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: obinns-dd
body: Done! 
created_at: 2025-03-20T17:28:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import android.content.Context
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+
+@RunWith(AndroidJUnit4::class)
+class SelectPassportScreenTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private lateinit var readMoreButton: SemanticsMatcher
+    private lateinit var yesOption: SemanticsMatcher
+    private lateinit var noOption: SemanticsMatcher
+    private lateinit var confirmButton: SemanticsMatcher
+
+    private val viewModel =
+        spy(
+            SelectPassportViewModel(
+                analytics = mock(),
+            ),
+        )
+
+    @Before
+    fun setUp() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        readMoreButton = hasText(context.getString(R.string.selectdocument_passport_readmore_button))
+        yesOption = hasContentDescription(context.getString(R.string.selectdocument_passport_selection_yes))
+        noOption = hasContentDescription(context.getString(R.string.selectdocument_passport_selection_no))
+        confirmButton = hasText(context.getString(R.string.selectdocument_passport_continuebutton))
+    }
+
+    @Test
+    fun `when screen started, it calls the view model`() {
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+
+        verify(viewModel).onScreenStart()
+    }
+
+    @Test
+    fun `when read more is selected, it calls the view model`() {
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+
+        composeTestRule
+            .onNode(readMoreButton)
+            .performClick()
+
+        verify(viewModel).readMoreButtonAction()
+    }
+
+    @Ignore("Radio buttons are not appearing in node hierarchy - further investigation required")
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
user: login: obinns-dd
body: Fixed 🙌🏼 
created_at: 2025-03-21T12:34:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: obinns-dd
body: Agree, I've done that 🙂 
created_at: 2025-03-21T13:08:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,168 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import android.content.Context
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onSibling
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeUp
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+
+@RunWith(AndroidJUnit4::class)
+class SelectPassportScreenTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private lateinit var readMoreButton: SemanticsMatcher
+    private lateinit var yesOption: SemanticsMatcher
+    private lateinit var noOption: SemanticsMatcher
+    private lateinit var confirmButton: SemanticsMatcher
+
+    private val viewModel =
+        spy(
+            SelectPassportViewModel(
+                analytics = mock(),
+            ),
+        )
+
+    @Before
+    fun setUp() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        readMoreButton = hasText(context.getString(R.string.selectdocument_passport_readmore_button))
+        yesOption = hasText(context.getString(R.string.selectdocument_passport_selection_yes))
+        noOption = hasText(context.getString(R.string.selectdocument_passport_selection_no))
+        confirmButton = hasText(context.getString(R.string.selectdocument_passport_continuebutton))
+
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+    }
+
+    @Test
+    fun `when screen is started, it calls the view model`() {
+        verify(viewModel).onScreenStart()
+    }
+
+    @Test
+    fun `when screen is started, no item is selected`() {
+        swipeToAdditionalContent()
+
+        verify(viewModel).onScreenStart()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+    }
+
+    @Test
+    fun `when read more is selected, it calls the view model`() {
+        composeTestRule
+            .onNode(readMoreButton)
+            .performClick()
+
+        verify(viewModel).onReadMoreClick()
+    }
+
+    @Test
+    fun `when yes is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    @Test
+    fun `when no is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    // DCMAW-8054 | AC4: User doesn’t select an option
+    @Ignore("This AC has not yet been implemented")
+    @Test
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
user: login: obinns-dd
body: TODO: implement this Acceptance Criteria
created_at: 2025-03-21T14:11:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+import uk.gov.onelogin.criorchestrator.libraries.testing.MainDispatcherExtension
+
+@ExtendWith(MainDispatcherExtension::class)
+class SelectPassportViewModelTest {
+    private val analyticsLogger = mock<SelectDocumentAnalytics>()
+
+    private val viewModel by lazy {
+        SelectPassportViewModel(
+            analytics =
+            analyticsLogger,
+        )
+    }
+
+    // DCMAW-8054 | AC1: Viewing passport screen
+    @Test
+    fun `when screen starts, it sends analytics`() {
+        viewModel.onScreenStart()
+
+        verify(analyticsLogger)
+            .trackScreen(
+                id = SelectDocumentScreenId.SelectPassport,
+                title = R.string.selectdocument_passport_title,
+            )
+    }
+
+    // DCMAW-8054 | AC2: User has a passport
+    // DCMAW-8054 | AC3: User doesn’t have a passport
+    @ParameterizedTest
+    @ValueSource(ints = [0, 1])
+    fun `when selection is confirmed, it sends analytics`(selection: Int) {
+        viewModel.onConfirmSelection(selection)
+
+        val item =
+            listOf(
+                R.string.selectdocument_passport_selection_yes,
+                R.string.selectdocument_passport_selection_no,
+            )[selection]
+
+        verify(analyticsLogger)
+            .trackFormSubmission(
+                R.string.selectdocument_passport_continuebutton,
+                item,
+            )
+    }
+
+    @Test
+    fun `when the read more button is pressed, it sends analytics`() {
+        viewModel.onReadMoreClick()
+
+        verify(analyticsLogger)
+            .trackButtonEvent(R.string.selectdocument_passport_readmore_button)
+    }
+
+    // DCMAW-8054 | AC8: User wants to learn about different document options
+    @Disabled("This AC has not yet been implemented")
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
user: login: obinns-dd
body: This will be implemented in a follow-up pull request containing navigation.
created_at: 2025-03-21T14:12:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,168 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import android.content.Context
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onSibling
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeUp
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+
+@RunWith(AndroidJUnit4::class)
+class SelectPassportScreenTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private lateinit var readMoreButton: SemanticsMatcher
+    private lateinit var yesOption: SemanticsMatcher
+    private lateinit var noOption: SemanticsMatcher
+    private lateinit var confirmButton: SemanticsMatcher
+
+    private val viewModel =
+        spy(
+            SelectPassportViewModel(
+                analytics = mock(),
+            ),
+        )
+
+    @Before
+    fun setUp() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        readMoreButton = hasText(context.getString(R.string.selectdocument_passport_readmore_button))
+        yesOption = hasText(context.getString(R.string.selectdocument_passport_selection_yes))
+        noOption = hasText(context.getString(R.string.selectdocument_passport_selection_no))
+        confirmButton = hasText(context.getString(R.string.selectdocument_passport_continuebutton))
+
+        composeTestRule.setContent {
+            SelectPassportScreen(
+                viewModel = viewModel,
+            )
+        }
+    }
+
+    @Test
+    fun `when screen is started, it calls the view model`() {
+        verify(viewModel).onScreenStart()
+    }
+
+    @Test
+    fun `when screen is started, no item is selected`() {
+        swipeToAdditionalContent()
+
+        verify(viewModel).onScreenStart()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .onSibling()
+            .assertIsNotSelected()
+    }
+
+    @Test
+    fun `when read more is selected, it calls the view model`() {
+        composeTestRule
+            .onNode(readMoreButton)
+            .performClick()
+
+        verify(viewModel).onReadMoreClick()
+    }
+
+    @Test
+    fun `when yes is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(yesOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    @Test
+    fun `when no is tapped, it is selected`() {
+        swipeToAdditionalContent()
+
+        composeTestRule
+            .onNode(noOption, useUnmergedTree = true)
+            .performClick()
+            .onSibling()
+            .assertIsSelected()
+    }
+
+    // DCMAW-8054 | AC4: User doesn’t select an option
+    @Ignore("This AC has not yet been implemented")
+    @Test
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportScreenTest.kt
user: login: obinns-dd
body: Done! ✅ 
created_at: 2025-03-24T11:32:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+import uk.gov.onelogin.criorchestrator.libraries.testing.MainDispatcherExtension
+
+@ExtendWith(MainDispatcherExtension::class)
+class SelectPassportViewModelTest {
+    private val analyticsLogger = mock<SelectDocumentAnalytics>()
+
+    private val viewModel by lazy {
+        SelectPassportViewModel(
+            analytics =
+            analyticsLogger,
+        )
+    }
+
+    @Test
+    fun `when screen starts, it sends analytics`() {
+        viewModel.onScreenStart()
+
+        verify(analyticsLogger)
+            .trackScreen(
+                id = SelectDocumentScreenId.SelectPassport,
+                title = R.string.selectdocument_passport_title,
+            )
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
user: login: obinns-dd
body: I've just taken a look: I'm a little nervous about how we've implemented those tests.

It feels like they're quite tightly coupled to functionality that is declared in the logging package.
For example: `parameters["screen_id"]`, `it.parameters["screen_name"]`.
If these strings are changed centrally, we could end up with a whole bunch of broken tests.

I think we need to focus on testing that our code sends the right data to the logger, but not the internal implementation of that logger.


created_at: 2025-03-24T11:42:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
+
+    val buttonTextId = R.string.selectdocument_passport_continuebutton
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: obinns-dd
body: I've moved this state into the view, so I think this is fine now?
created_at: 2025-03-24T12:15:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import androidx.lifecycle.ViewModel
+import kotlinx.collections.immutable.persistentListOf
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+
+internal class SelectPassportViewModel(
+    private val analytics: SelectDocumentAnalytics,
+) : ViewModel() {
+    val titleId = R.string.selectdocument_passport_title
+    val readMoreButtonTextId = R.string.selectdocument_passport_readmore_button
+
+    val options =
+        persistentListOf(
+            R.string.selectdocument_passport_selection_yes,
+            R.string.selectdocument_passport_selection_no,
+        )
+    var selectedIndex: Int? = null
+        private set
+
+    val buttonTextId = R.string.selectdocument_passport_continuebutton
path: features/select-doc/internal/src/main/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModel.kt
user: login: obinns-dd
body: update: now using Flow of state
created_at: 2025-03-24T13:33:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,76 @@
+package uk.gov.onelogin.criorchestrator.features.selectdoc.internal.passport
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.R
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentAnalytics
+import uk.gov.onelogin.criorchestrator.features.selectdoc.internal.analytics.SelectDocumentScreenId
+import uk.gov.onelogin.criorchestrator.libraries.testing.MainDispatcherExtension
+
+@ExtendWith(MainDispatcherExtension::class)
+class SelectPassportViewModelTest {
+    private val analyticsLogger = mock<SelectDocumentAnalytics>()
+
+    private val viewModel by lazy {
+        SelectPassportViewModel(
+            analytics =
+            analyticsLogger,
+        )
+    }
+
+    @Test
+    fun `when screen starts, it sends analytics`() {
+        viewModel.onScreenStart()
+
+        verify(analyticsLogger)
+            .trackScreen(
+                id = SelectDocumentScreenId.SelectPassport,
+                title = R.string.selectdocument_passport_title,
+            )
path: features/select-doc/internal/src/test/kotlin/uk/gov/onelogin/criorchestrator/features/selectdoc/internal/passport/SelectPassportViewModelTest.kt
user: login: obinns-dd
body: Let's pick this one up separately, happy to raise a follow up pull request if we want to add it 🙂 
created_at: 2025-03-24T13:38:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-19T17:09:53Z
body: Analytics test report is [available to download].

[available to download]: https://github.com/govuk-one-login/mobile-android-cri-orchestrator/actions/runs/14036372711/artifacts/2807809378
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-24T13:31:01Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=mobile-android-cri-orchestrator&pullRequest=195) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=mobile-android-cri-orchestrator&pullRequest=195&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=mobile-android-cri-orchestrator&pullRequest=195&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=mobile-android-cri-orchestrator&pullRequest=195&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [76.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=mobile-android-cri-orchestrator&pullRequest=195&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=mobile-android-cri-orchestrator&pullRequest=195&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=mobile-android-cri-orchestrator&pullRequest=195)


author_association: NONE