diff --git a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt index 742c2ae3c..57f0e4ca6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera import android.os.Build -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -28,7 +27,6 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat import com.google.common.truth.TruthJUnit.assume -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON @@ -37,6 +35,7 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONF import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runMainActivityScenarioTest +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -74,9 +73,7 @@ class BackgroundDeviceTest { @Test fun background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() backgroundThenForegroundApp() } @@ -84,9 +81,7 @@ class BackgroundDeviceTest { @Test fun flipCamera_then_background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -109,9 +104,7 @@ class BackgroundDeviceTest { @Test fun setAspectRatio_then_background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -147,9 +140,7 @@ class BackgroundDeviceTest { assumeSupportsSingleStream() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt index 90758b0a8..6649d27c5 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt @@ -17,7 +17,6 @@ package com.google.jetpackcamera import android.os.Build import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -35,7 +34,6 @@ import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.SCREEN_FLASH_OVERLAY import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS @@ -46,6 +44,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.setFlashMode +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Before import org.junit.Rule @@ -72,9 +71,7 @@ internal class FlashDeviceTest { @Test fun set_flash_on() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.ON) } @@ -82,9 +79,7 @@ internal class FlashDeviceTest { @Test fun set_flash_auto() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.AUTO) } @@ -92,9 +87,7 @@ internal class FlashDeviceTest { @Test fun set_flash_off() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.OFF) } @@ -102,9 +95,7 @@ internal class FlashDeviceTest { @Test fun set_flash_low_light_boost() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.LOW_LIGHT_BOOST) } @@ -124,9 +115,7 @@ internal class FlashDeviceTest { assumeHalStableOnImageCapture() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has a back camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() @@ -152,9 +141,7 @@ internal class FlashDeviceTest { filePrefix = "JCA" ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has a front camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() @@ -195,9 +182,7 @@ internal class FlashDeviceTest { mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has the target lens facing camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index 970a97e84..3bda5787f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -34,7 +34,6 @@ import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.CacheParam import com.google.jetpackcamera.utils.FILE_PREFIX import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS @@ -55,6 +54,7 @@ import com.google.jetpackcamera.utils.getTestUri import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector @@ -87,9 +87,7 @@ internal class ImageCaptureDeviceTest { extras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -107,9 +105,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_UP) @@ -125,9 +121,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) verifyImageCaptureSuccess() @@ -143,9 +137,7 @@ internal class ImageCaptureDeviceTest { extras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -170,9 +162,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -199,9 +189,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecording() @@ -233,9 +221,7 @@ internal class ImageCaptureDeviceTest { extras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -263,9 +249,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -289,9 +273,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) @@ -313,9 +295,7 @@ internal class ImageCaptureDeviceTest { ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, IMAGE_CAPTURE_FAILURE_TAG diff --git a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt index db57373c0..cdaa1cac0 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt @@ -15,7 +15,6 @@ */ package com.google.jetpackcamera -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -33,13 +32,13 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTON import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.DEFAULT_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.onNodeWithText import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.searchForQuickSetting +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear import org.junit.Rule @@ -61,9 +60,7 @@ class NavigationTest { @Test fun backAfterReturnFromSettings_doesNotReturnToSettings() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // open quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN).assertExists().performClick() @@ -97,9 +94,7 @@ class NavigationTest { @Test fun returnFromSettings_afterFlipCamera_returnsToPreview() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // If flipping the camera is available, flip it. Otherwise skip test. composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON) @@ -132,9 +127,7 @@ class NavigationTest { @Test fun backFromQuickSettings_returnToPreview() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to the quick settings screen composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -154,9 +147,7 @@ class NavigationTest { @Test fun backFromQuickSettingsExpended_returnToQuickSettings() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to the quick settings screen composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt index e829e1d8e..297eb3ca7 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt @@ -87,9 +87,7 @@ class PermissionsTest { @Test fun allPermissions_alreadyGranted_screenNotShown() { runMainActivityScenarioTest { - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt index d3b9a7837..a66587cdc 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt @@ -68,14 +68,13 @@ import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_STREAM_CONFI import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_DURATION_TAG import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_QUALITY_TAG import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_STABILIZATION_TAG -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.DEFAULT_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.selectLensFacing import com.google.jetpackcamera.utils.visitSettingDialog import com.google.jetpackcamera.utils.visitSettingsScreen +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -105,9 +104,7 @@ class SettingsDeviceTest(private val lensFacing: LensFacing) { action: ComposeTestRule.() -> Unit ): Unit = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.visitSettingsScreen { // Ensure appropriate lens facing is selected diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt index 626249b9f..3b02122b4 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt @@ -15,7 +15,6 @@ */ package com.google.jetpackcamera -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithTag @@ -30,10 +29,10 @@ import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.ui.components.capture.AMPLITUDE_HOT_TAG import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.debugExtra import com.google.jetpackcamera.utils.runMainActivityScenarioTest +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -61,9 +60,7 @@ class VideoAudioTest { runMainActivityScenarioTest(debugExtra) { // check audio visualizer composable for muted/unmuted icon. // icon will only be unmuted if audio is nonzero - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // record video composeTestRule.onNodeWithTag(CAPTURE_BUTTON) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index 86dde9dc8..3dfc40ffa 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -18,7 +18,6 @@ package com.google.jetpackcamera import android.app.Activity import android.net.Uri import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -30,7 +29,6 @@ import com.google.jetpackcamera.feature.postcapture.ui.VIEWER_POST_CAPTURE_VIDEO import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.CacheParam import com.google.jetpackcamera.utils.IMAGE_PREFIX import com.google.jetpackcamera.utils.MOVIES_DIR_PATH @@ -48,6 +46,7 @@ import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector @@ -78,9 +77,7 @@ internal class VideoRecordingDeviceTest { ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() verifyVideoCaptureSuccess() @@ -96,13 +93,11 @@ internal class VideoRecordingDeviceTest { ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.pressAndDragToLockVideoRecording() // stop recording - // fixme: this shouldnt need two clicks + // fixme: this shouldn't need two clicks composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() @@ -121,9 +116,7 @@ internal class VideoRecordingDeviceTest { activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) @@ -141,9 +134,7 @@ internal class VideoRecordingDeviceTest { activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // start recording composeTestRule.tapStartLockedVideoRecording() @@ -165,9 +156,7 @@ internal class VideoRecordingDeviceTest { activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecording() composeTestRule.waitForNodeWithTag( VIDEO_CAPTURE_FAILURE_TAG, diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt index 4a9e56f72..09c6f0472 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -144,9 +144,9 @@ fun ComposeTestRule.wait(timeoutMillis: Long) { } } fun ComposeTestRule.waitForCaptureButton(timeoutMillis: Long = APP_START_TIMEOUT_MILLIS) { - // Wait for the capture button to be displayed + // Wait for the capture button to be displayed and enabled waitUntil(timeoutMillis = timeoutMillis) { - onNodeWithTag(CAPTURE_BUTTON).isDisplayed() + onNode(hasTestTag(CAPTURE_BUTTON) and isEnabled()).isDisplayed() } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 18fc38af3..6d8334c98 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -47,6 +47,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -67,6 +68,8 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -191,9 +194,9 @@ internal fun CaptureButton( captureButtonUiState: CaptureButtonUiState, captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE ) { - var currentUiState = rememberUpdatedState(captureButtonUiState) + val currentUiState = rememberUpdatedState(captureButtonUiState) val firstKeyPressed = remember { mutableStateOf(null) } - val isLongPressing = remember { mutableStateOf(false) } + val isLongPressing = remember { mutableStateOf(false) } var longPressJob by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val longPressTimeout = LocalViewConfiguration.current.longPressTimeoutMillis @@ -201,14 +204,16 @@ internal fun CaptureButton( LaunchedEffect(captureButtonUiState) { if (captureButtonUiState is CaptureButtonUiState.Enabled.Idle) { onLockVideoRecording(false) - } else if (captureButtonUiState is CaptureButtonUiState.Enabled.Recording.LockedRecording) { + } else if (captureButtonUiState + is CaptureButtonUiState.Enabled.Recording.LockedRecording + ) { longPressJob = null isLongPressing.value = false firstKeyPressed.value = null } } fun onLongPress() { - if (isLongPressing.value == false) { + if (!isLongPressing.value) { when (val current = currentUiState.value) { is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { CaptureMode.STANDARD, @@ -229,6 +234,7 @@ internal fun CaptureButton( } fun onPress(captureSource: CaptureSource) { + if (!captureButtonUiState.isEnabled) return if (firstKeyPressed.value == null) { firstKeyPressed.value = captureSource longPressJob = scope.launch { @@ -292,6 +298,37 @@ internal fun CaptureButton( ) } +/** + * A composable that returns a debounced boolean state for whether the capture button should be + * visually disabled. + * + * While the button's semantics and pointer input are disabled immediately, the visual change + * to a disabled appearance is delayed. If the button becomes enabled again within this period, + * the distracting flicker is avoided. + * + * @param captureButtonUiState The current UI state of the capture button. + * @param delayMillis The duration to wait before visually disabling the button. + * @return A [State] holding `true` if the button should be visually disabled, `false` otherwise. + */ +@Composable +private fun rememberDebouncedVisuallyDisabled( + captureButtonUiState: CaptureButtonUiState, + delayMillis: Long = 1000L +): State { + val isVisuallyDisabled = remember { + mutableStateOf(!captureButtonUiState.isEnabled) + } + LaunchedEffect(captureButtonUiState) { + if (!captureButtonUiState.isEnabled) { + delay(delayMillis) + isVisuallyDisabled.value = true + } else { + isVisuallyDisabled.value = false + } + } + return isVisuallyDisabled +} + @Composable private fun CaptureButton( modifier: Modifier = Modifier, @@ -314,10 +351,23 @@ private fun CaptureButton( val currentUiState = rememberUpdatedState(captureButtonUiState) val switchWidth = (captureButtonSize * LOCK_SWITCH_WIDTH_SCALE) - val currentColor = LocalContentColor.current var relativeCaptureButtonBounds by remember { mutableStateOf(null) } + val isVisuallyDisabled by rememberDebouncedVisuallyDisabled( + captureButtonUiState = captureButtonUiState + ) + + val animatedColor by animateColorAsState( + targetValue = if (isVisuallyDisabled) { + LocalContentColor.current.copy(alpha = 0.38f) + } else { + LocalContentColor.current + }, + animationSpec = tween(durationMillis = if (isVisuallyDisabled) 1000 else 300), + label = "Capture Button Color" + ) + fun shouldBeLocked(): Boolean = switchPosition > MINIMUM_LOCK_THRESHOLD fun setLockSwitchPosition(positionX: Float, offsetX: Float) { @@ -348,12 +398,8 @@ private fun CaptureButton( LOCK_SWITCH_POSITION_ON } } - CaptureButtonRing( - modifier = modifier - .onSizeChanged { - relativeCaptureButtonBounds = - Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) - } + val gestureModifier = if (captureButtonUiState.isEnabled) { + Modifier .pointerInput(Unit) { detectTapGestures( // onLongPress cannot be null, otherwise it won't detect the release if the @@ -410,9 +456,24 @@ private fun CaptureButton( } } ) - }, + } + } else { + Modifier + } + CaptureButtonRing( + modifier = modifier + .onSizeChanged { + relativeCaptureButtonBounds = + Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) + } + .semantics { + if (!captureButtonUiState.isEnabled) { + disabled() + } + } + .then(gestureModifier), captureButtonSize = captureButtonSize, - color = currentColor + color = animatedColor ) { if (useLockSwitch) { LockSwitchCaptureButtonNucleus( @@ -658,6 +719,19 @@ private fun CaptureButtonNucleus( } } +@Preview +@Composable +private fun CaptureButtonUnavailablePreview() { + CaptureButton( + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Unavailable + ) +} + @Preview @Composable private fun IdleStandardCaptureButtonPreview() { @@ -682,6 +756,63 @@ private fun IdleImageCaptureButtonPreview() { } } +@Preview +@Composable +private fun IdleVideoOnlyCaptureButtonPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleStandardCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.STANDARD, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleImageCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.IMAGE_ONLY, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleVideoOnlyCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.VIDEO_ONLY, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + @Preview @Composable private fun PressedImageCaptureButtonPreview() { diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index a3bc22753..1b06c7872 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -617,7 +617,9 @@ fun CaptureButton( modifier = modifier.testTag(CAPTURE_BUTTON), onIncrementZoom = onIncrementZoom, onImageCapture = { - if (captureButtonUiState is CaptureButtonUiState.Enabled) { + if (captureButtonUiState is CaptureButtonUiState.Enabled && + captureButtonUiState.isEnabled + ) { onCaptureImage(context.contentResolver) } if (isQuickSettingsOpen) { @@ -625,7 +627,9 @@ fun CaptureButton( } }, onStartRecording = { - if (captureButtonUiState is CaptureButtonUiState.Enabled) { + if (captureButtonUiState is CaptureButtonUiState.Enabled && + captureButtonUiState.isEnabled + ) { onStartVideoRecording() } }, diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt index af2208710..04d3837b4 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt @@ -26,28 +26,41 @@ import com.google.jetpackcamera.model.CaptureMode */ sealed interface CaptureButtonUiState { /** - * The capture button is unavailable and should be disabled. - * This is used when the camera is not ready or an operation is in progress that - * precludes capturing. + * Whether the capture button is enabled and can be interacted with. */ - data object Unavailable : CaptureButtonUiState + val isEnabled: Boolean + + /** + * The capture button is unavailable and should not be shown or interacted with. + */ + data object Unavailable : CaptureButtonUiState { + override val isEnabled: Boolean = false + } /** * The capture button is enabled and ready for user interaction. */ sealed interface Enabled : CaptureButtonUiState { + override val isEnabled: Boolean get() = true + /** * The button is in an idle state, ready to start a capture. * * @param captureMode The current [CaptureMode] (e.g., [CaptureMode.IMAGE] or * [CaptureMode.VIDEO]) to indicate the button's primary action. + * @property isEnabled Whether the button is enabled for interaction. */ - data class Idle(val captureMode: CaptureMode) : Enabled + data class Idle( + val captureMode: CaptureMode, + override val isEnabled: Boolean = true + ) : Enabled /** * The button is in a video recording state. */ sealed interface Recording : Enabled { + override val isEnabled: Boolean get() = true + /** * The user is actively pressing the button to record video (press-and-hold). */ diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt index 5f68b1243..108b65e34 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt @@ -39,21 +39,26 @@ fun CaptureButtonUiState.Companion.from( cameraAppSettings: CameraAppSettings, cameraState: CameraState, lockedState: Boolean -): CaptureButtonUiState = when (cameraState.videoRecordingState) { - // if not currently recording, check capturemode to determine idle capture button UI - is VideoRecordingState.Inactive -> - CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) +): CaptureButtonUiState = if (cameraState.isCameraRunning) { + when (cameraState.videoRecordingState) { + // if not currently recording, check capturemode to determine idle capture button UI + is VideoRecordingState.Inactive -> + CaptureButtonUiState + .Enabled.Idle(captureMode = cameraAppSettings.captureMode) - // display different capture button UI depending on if recording is pressed or locked - is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused -> - if (lockedState) { - CaptureButtonUiState.Enabled.Recording.LockedRecording - } else { - CaptureButtonUiState.Enabled.Recording.PressedRecording - } + // display different capture button UI depending on if recording is pressed or locked + is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused -> + if (lockedState) { + CaptureButtonUiState.Enabled.Recording.LockedRecording + } else { + CaptureButtonUiState.Enabled.Recording.PressedRecording + } - is VideoRecordingState.Starting -> - CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + is VideoRecordingState.Starting -> + CaptureButtonUiState + .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + } +} else { + CaptureButtonUiState + .Enabled.Idle(captureMode = cameraAppSettings.captureMode, isEnabled = false) } diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt new file mode 100644 index 000000000..70c755780 --- /dev/null +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.uistateadapter.capture + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.camera.CameraState +import com.google.jetpackcamera.core.camera.VideoRecordingState +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class CaptureButtonUiStateAdapterTest { + private val defaultCameraAppSettings = CameraAppSettings(captureMode = CaptureMode.STANDARD) + private val defaultCameraState = CameraState(isCameraRunning = true) + + @Test + fun from_cameraNotRunning_returnsIdleAndDisabled() { + val cameraState = CameraState(isCameraRunning = false) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Enabled.Idle::class.java) + assertThat(uiState.isEnabled).isFalse() + assertThat((uiState as CaptureButtonUiState.Enabled.Idle).captureMode) + .isEqualTo(CaptureMode.STANDARD) + } + + @Test + fun from_cameraRunning_recordingInactive_returnsIdleAndEnabled() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Inactive() + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Enabled.Idle::class.java) + assertThat(uiState.isEnabled).isTrue() + assertThat((uiState as CaptureButtonUiState.Enabled.Idle).captureMode) + .isEqualTo(CaptureMode.STANDARD) + } + + @Test + fun from_cameraRunning_recordingPressed_returnsPressedRecording() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Active.Recording(0L, 0.0, 0L) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState) + .isInstanceOf(CaptureButtonUiState.Enabled.Recording.PressedRecording::class.java) + assertThat(uiState.isEnabled).isTrue() + } + + @Test + fun from_cameraRunning_recordingLocked_returnsLockedRecording() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Active.Recording(0L, 0.0, 0L) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = true + ) + + assertThat(uiState) + .isInstanceOf(CaptureButtonUiState.Enabled.Recording.LockedRecording::class.java) + assertThat(uiState.isEnabled).isTrue() + } + + @Test + fun from_cameraRunning_recordingStarting_returnsIdleAndEnabled() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Starting(null) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Enabled.Idle::class.java) + assertThat(uiState.isEnabled).isTrue() + } +}