Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 48 additions & 24 deletions app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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<MainActivity> {
// 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<MainActivity>(
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is with(...) doing differently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with(...) turns the argument into a receiver. That is, it makes "composeTestRule" part of the context of the lambda that runs. It means we don't need to write composeTestRule before each of the commands.

In this case it's making the code less verbose (at the expense of an increase in indentation).

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()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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) {
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T : Activity> runMediaStoreAutoDeleteScenarioTest(
mediaUri: Uri,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,26 @@ 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) {
Icons.Filled.Mic
} else {
Icons.Filled.MicOff
},
contentDescription = stringResource(id = R.string.audio_visualizer_icon)
contentDescription = stringResource(id = R.string.audio_visualizer_content_description)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion feature/preview/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
<string name="settings_content_description">Settings</string>
<string name="flip_camera_content_description">Flip Camera</string>

<string name="audio_visualizer_icon">An icon of a microphone</string>
<string name="audio_visualizer_content_description">An icon of a microphone</string>
<string name="audio_visualizer_recording_state_description">Audio recording in progress</string>
<string name="audio_visualizer_not_recording_state_description">Audio recording not in progress</string>
<string name="zoom_scale_text">%1$.2fx</string>

<string name="debug_text_physical_camera_id_prefix">Physical ID: </string>
Expand Down