diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt index 3af4d125f..2511d113f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt @@ -15,31 +15,32 @@ */ package com.google.jetpackcamera +import android.provider.MediaStore 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 import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.feature.preview.ui.AMPLITUDE_HOT_TAG +import com.google.jetpackcamera.feature.preview.R import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON +import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS -import com.google.jetpackcamera.utils.runScenarioTest +import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.VIDEO_RECORDING_START_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.idleForVideoDuration +import com.google.jetpackcamera.utils.onNodeWithStateDescription +import com.google.jetpackcamera.utils.runMediaStoreAutoDeleteScenarioTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@RequiresDevice class VideoAudioTest { @get:Rule val permissionsRule: GrantPermissionRule = @@ -48,31 +49,54 @@ class VideoAudioTest { @get:Rule val composeTestRule = createEmptyComposeRule() - private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - @Before fun setUp() { - assertThat(uiDevice.isScreenOn).isTrue() + with(UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())) { + assertThat(isScreenOn).isTrue() + } } @Test - fun audioIncomingWhenEnabled() { - runScenarioTest { - // 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() + fun audioIncomingWhenEnabled() = runMediaStoreAutoDeleteScenarioTest( + mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + ) { + // check audio visualizer composable for muted/unmuted icon. + // icon will only be unmuted if audio is nonzero + with(composeTestRule) { + waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { + onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } - // record video - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists().performTouchInput { longClick(durationMillis = 5000) } + // start recording video + onNodeWithTag(CAPTURE_BUTTON) + .assertExists() + .performTouchInput { + down(center) + } + + try { + // assert hot amplitude tag visible + waitUntil(timeoutMillis = VIDEO_RECORDING_START_TIMEOUT_MILLIS) { + onNodeWithStateDescription( + R.string.audio_visualizer_recording_state_description + ).isDisplayed() + } - // assert hot amplitude tag visible - uiDevice.wait( - Until.findObject(By.res(AMPLITUDE_HOT_TAG)), - 5000 - ) + // Ensure we record long enough to create a successful recording + idleForVideoDuration() + } finally { + // finish recording video + onNodeWithTag(CAPTURE_BUTTON) + .assertExists() + .performTouchInput { + up() + } + + // Wait for recording to finish + waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { + onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() + } + } } } } 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 b6e0388d7..d4588bb3f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -59,6 +59,19 @@ fun SemanticsNodeInteractionsProvider.onNodeWithContentDescription( label = getResString(strRes) ) +/** + * Allows searching for a node by [SemanticsProperties.StateDescription] using an integer string + * resource. + */ +fun SemanticsNodeInteractionsProvider.onNodeWithStateDescription( + @StringRes strRes: Int +): SemanticsNodeInteraction = onNode( + SemanticsMatcher.expectValue( + SemanticsProperties.StateDescription, + expectedValue = getResString(strRes) + ) +) + /** * Fetch a string resources from a [SemanticsNodeInteractionsProvider] context. */ @@ -111,13 +124,11 @@ fun ComposeTestRule.longClickForVideoRecording() { } } -private fun ComposeTestRule.idleForVideoDuration() { +fun ComposeTestRule.idleForVideoDuration() { // TODO: replace with a check for the timestamp UI of the video duration try { - waitUntil(timeoutMillis = VIDEO_DURATION_MILLIS) { - onNodeWithTag("dummyTagForLongPress").isDisplayed() - } - } catch (e: ComposeTimeoutException) { + waitUntil(timeoutMillis = VIDEO_DURATION_MILLIS, condition = { false }) + } catch (_: ComposeTimeoutException) { } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt index bf5e8e4f0..635243f2b 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.withTimeoutOrNull const val APP_START_TIMEOUT_MILLIS = 10_000L const val IMAGE_CAPTURE_TIMEOUT_MILLIS = 5_000L const val VIDEO_CAPTURE_TIMEOUT_MILLIS = 5_000L +const val VIDEO_RECORDING_START_TIMEOUT_MILLIS = 2_000L const val VIDEO_DURATION_MILLIS = 2_000L inline fun runMediaStoreAutoDeleteScenarioTest( mediaUri: Uri, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt index 8382af48b..751f028f9 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt @@ -151,16 +151,18 @@ fun AmplitudeVisualizer( } ) + val audioVisualizerStateDescription = if (audioAmplitude != 0.0) { + stringResource(id = R.string.audio_visualizer_recording_state_description) + } else { + stringResource(id = R.string.audio_visualizer_not_recording_state_description) + } Icon( modifier = Modifier .align(Alignment.Center) .size((0.5 * size).dp) - .apply { - if (audioAmplitude != 0.0) { - testTag(AMPLITUDE_HOT_TAG) - } else { - testTag(AMPLITUDE_NONE_TAG) - } + .testTag(AUDIO_VISUALIZER_TAG) + .semantics { + stateDescription = audioVisualizerStateDescription }, tint = Color.Black, imageVector = if (audioAmplitude != 0.0) { @@ -168,7 +170,7 @@ fun AmplitudeVisualizer( } else { Icons.Filled.MicOff }, - contentDescription = stringResource(id = R.string.audio_visualizer_icon) + contentDescription = stringResource(id = R.string.audio_visualizer_content_description) ) } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt index dcea4ec69..67173ec43 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/TestTags.kt @@ -29,8 +29,7 @@ const val VIDEO_CAPTURE_FAILURE_TAG = "VideoCaptureFailureTag" const val PREVIEW_DISPLAY = "PreviewDisplay" const val SCREEN_FLASH_OVERLAY = "ScreenFlashOverlay" const val SETTINGS_BUTTON = "SettingsButton" -const val AMPLITUDE_NONE_TAG = "AmplitudeNoneTag" -const val AMPLITUDE_HOT_TAG = "AmplitudeHotTag" +const val AUDIO_VISUALIZER_TAG = "AudioVisualizerTag" const val HDR_IMAGE_UNSUPPORTED_ON_DEVICE_TAG = "HdrImageUnsupportedOnDeviceTag" const val HDR_IMAGE_UNSUPPORTED_ON_LENS_TAG = "HdrImageUnsupportedOnLensTag" const val HDR_IMAGE_UNSUPPORTED_ON_SINGLE_STREAM_TAG = "HdrImageUnsupportedOnSingleStreamTag" diff --git a/feature/preview/src/main/res/values/strings.xml b/feature/preview/src/main/res/values/strings.xml index 10b96b849..5bb603c85 100644 --- a/feature/preview/src/main/res/values/strings.xml +++ b/feature/preview/src/main/res/values/strings.xml @@ -23,7 +23,9 @@ Settings Flip Camera - An icon of a microphone + An icon of a microphone + Audio recording in progress + Audio recording not in progress %1$.2fx Physical ID: