diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f4d291703..80010bcb4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -133,6 +133,7 @@ dependencies { androidTestImplementation(libs.androidx.uiautomator) androidTestImplementation(libs.camera.lifecycle) // to reset CameraX between tests androidTestImplementation(libs.truth) + androidTestImplementation(libs.testParameterInjector) androidTestImplementation(project(":ui:components:capture")) androidTestUtil(libs.androidx.orchestrator) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt deleted file mode 100644 index 20bd7076b..000000000 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (C) 2025 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 - -import android.app.Activity -import android.net.Uri -import android.provider.MediaStore -import androidx.compose.ui.test.hasContentDescription -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.isNotDisplayed -import androidx.compose.ui.test.junit4.createEmptyComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule -import androidx.test.uiautomator.UiDevice -import com.google.common.truth.Truth -import com.google.jetpackcamera.feature.postcapture.ui.BUTTON_POST_CAPTURE_EXIT -import com.google.jetpackcamera.feature.postcapture.ui.VIEWER_POST_CAPTURE_IMAGE -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.utils.APP_START_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.IMAGE_PREFIX -import com.google.jetpackcamera.utils.MESSAGE_DISAPPEAR_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.PICTURES_DIR_PATH -import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS -import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.cacheExtra -import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp -import com.google.jetpackcamera.utils.doesFileExist -import com.google.jetpackcamera.utils.doesMediaExist -import com.google.jetpackcamera.utils.getMultipleImageCaptureIntent -import com.google.jetpackcamera.utils.getSingleImageCaptureIntent -import com.google.jetpackcamera.utils.getTestUri -import com.google.jetpackcamera.utils.runMainActivityScenarioTest -import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult -import com.google.jetpackcamera.utils.waitForCaptureButton -import com.google.jetpackcamera.utils.waitForNodeWithTag -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class CachedImageCaptureDeviceTest { - - @get:Rule - val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) - - @get:Rule - val composeTestRule = createEmptyComposeRule() - - private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val uiDevice = UiDevice.getInstance(instrumentation) - - @Test - fun imageCapture_success_navigatesPostCapture() = - runMainActivityScenarioTest(extras = cacheExtra) { - // Wait for the capture button to be displayed - composeTestRule.waitForCaptureButton() - - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performClick() - - // navigate to postcapture screen - composeTestRule.waitForNodeWithTag( - VIEWER_POST_CAPTURE_IMAGE, - timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS - ) - composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) - - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() - composeTestRule.waitForCaptureButton() - } - - // identical to image_capture_external - @Test - fun singleImageCapture_withIntent_savesImmediate() { - val timeStamp = System.currentTimeMillis() - val uri = getTestUri(PICTURES_DIR_PATH, timeStamp, "jpg") - val result = - runMainActivityScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performClick() - } - - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) - - Truth.assertThat( - doesMediaExist(uri, IMAGE_PREFIX) - ).isTrue() - deleteFilesInDirAfterTimestamp(PICTURES_DIR_PATH, instrumentation, timeStamp) - } - - @Test - fun image_capture_external_illegal_uri() { - val uri = Uri.parse("asdfasdf") - val result = - runMainActivityScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performClick() - - composeTestRule.waitForNodeWithTag( - IMAGE_CAPTURE_FAILURE_TAG, - timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS - ) - uiDevice.pressBack() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) - Truth.assertThat(doesFileExist(uri)).isFalse() - } - - @Test - fun multipleImageCapture_ExternalIntent_doesntNavigatePostCapture_returnsResultOk() { - val timeStamp = System.currentTimeMillis() - val uriStrings = arrayListOf() - for (i in 1..3) { - val uri = getTestUri(PICTURES_DIR_PATH, timeStamp + i.toLong(), "jpg") - uriStrings.add(uri.toString()) - } - val result = - runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent( - uriStrings, - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA - ), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - repeat(2) { - clickCaptureAndWaitUntilMessageDisappears( - IMAGE_CAPTURE_TIMEOUT_MILLIS, - IMAGE_CAPTURE_SUCCESS_TAG - ) - } - clickCapture() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) - for (string in uriStrings) { - Truth.assertThat( - doesMediaExist(Uri.parse(string), IMAGE_PREFIX) - ).isTrue() - } - deleteFilesInDirAfterTimestamp(PICTURES_DIR_PATH, instrumentation, timeStamp) - } - - @Test - fun multipleImageCaptureExternal_withNullUriList_returnsResultOk() { - val timeStamp = System.currentTimeMillis() - val result = - runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - repeat(2) { - clickCaptureAndWaitUntilMessageDisappears( - IMAGE_CAPTURE_TIMEOUT_MILLIS, - IMAGE_CAPTURE_SUCCESS_TAG - ) - } - uiDevice.pressBack() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) - Truth.assertThat(result.resultData.getStringArrayListExtra(MediaStore.EXTRA_OUTPUT)?.size) - .isEqualTo(2) - deleteFilesInDirAfterTimestamp(PICTURES_DIR_PATH, instrumentation, timeStamp) - } - - @Test - fun multipleImageCaptureExternal_withNullUriList_returnsResultCancel() { - val result = - runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent( - null, - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA - ), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - uiDevice.pressBack() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) - } - - @Test - fun multipleImageCaptureExternal_withIllegalUri_returnsResultOk() { - val timeStamp = System.currentTimeMillis() - val uriStrings = arrayListOf() - uriStrings.add("illegal_uri") - uriStrings.add(getTestUri(PICTURES_DIR_PATH, timeStamp, "jpg").toString()) - val result = - runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent( - uriStrings, - MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA - ), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - clickCaptureAndWaitUntilMessageDisappears( - IMAGE_CAPTURE_TIMEOUT_MILLIS, - IMAGE_CAPTURE_FAILURE_TAG - ) - clickCapture() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) - Truth.assertThat( - doesMediaExist(Uri.parse(uriStrings[1]), IMAGE_PREFIX) - ).isTrue() - deleteFilesInDirAfterTimestamp(PICTURES_DIR_PATH, instrumentation, timeStamp) - } - - private fun clickCaptureAndWaitUntilMessageDisappears(msgTimeOut: Long, msgTag: String) { - clickCapture() - composeTestRule.waitUntil(timeoutMillis = msgTimeOut) { - composeTestRule.onNodeWithTag(testTag = msgTag, useUnmergedTree = true).isDisplayed() - } - val dismissButtonMatcher = - hasContentDescription(value = "dismiss", substring = true, ignoreCase = true) - composeTestRule.waitUntil(timeoutMillis = msgTimeOut) { - composeTestRule.onNode(dismissButtonMatcher, useUnmergedTree = true).isDisplayed() - } - composeTestRule.onNode(dismissButtonMatcher, useUnmergedTree = true) - .performClick() - composeTestRule.waitUntil(timeoutMillis = MESSAGE_DISAPPEAR_TIMEOUT_MILLIS) { - val node = composeTestRule.onNodeWithTag(testTag = msgTag, useUnmergedTree = true) - node.isNotDisplayed() - } - } - - private fun clickCapture() { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performClick() - } -} diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt deleted file mode 100644 index 4728de0f6..000000000 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2025 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 - -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 -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule -import androidx.test.uiautomator.UiDevice -import com.google.common.truth.Truth -import com.google.jetpackcamera.feature.postcapture.ui.BUTTON_POST_CAPTURE_EXIT -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.utils.APP_START_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.MOVIES_DIR_PATH -import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS -import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.VIDEO_PREFIX -import com.google.jetpackcamera.utils.cacheExtra -import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp -import com.google.jetpackcamera.utils.doesMediaExist -import com.google.jetpackcamera.utils.getSingleImageCaptureIntent -import com.google.jetpackcamera.utils.getTestUri -import com.google.jetpackcamera.utils.longClickForVideoRecording -import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedTime -import com.google.jetpackcamera.utils.runMainActivityScenarioTest -import com.google.jetpackcamera.utils.runScenarioTestForResult -import com.google.jetpackcamera.utils.waitForCaptureButton -import com.google.jetpackcamera.utils.waitForNodeWithTag -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class CachedVideoRecordingDeviceTest { - @get:Rule - val permissionsRule: GrantPermissionRule = - GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) - - @get:Rule - val composeTestRule = createEmptyComposeRule() - - private val instrumentation = InstrumentationRegistry.getInstrumentation() - private val uiDevice = UiDevice.getInstance(instrumentation) - - @Test - fun videoCapture_success_navigatesPostcapture(): Unit = - runMainActivityScenarioTest(cacheExtra) { - // Wait for the capture button to be displayed - composeTestRule.waitForCaptureButton() - composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - - // navigate to postcapture screen - composeTestRule.waitForNodeWithTag( - VIEWER_POST_CAPTURE_VIDEO, - VIDEO_CAPTURE_TIMEOUT_MILLIS - ) - composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) - - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() - composeTestRule.waitForCaptureButton() - // no need to delete - } - - @Test - fun pressedVideoCapture_withIntent_savesImmediate() { - val timeStamp = System.currentTimeMillis() - val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") - val result = - runScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - cacheExtra - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) - Truth.assertThat(doesMediaExist(uri, VIDEO_PREFIX)).isTrue() - deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) - } - - @Test - fun video_capture_external_illegal_uri() { - val uri = Uri.parse("asdfasdf") - val result = - runScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) - ) { - // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } - composeTestRule.longClickForVideoRecording() - composeTestRule.waitForNodeWithTag( - VIDEO_CAPTURE_FAILURE_TAG, - VIDEO_CAPTURE_TIMEOUT_MILLIS - ) - uiDevice.pressBack() - } - Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) - Truth.assertThat(doesMediaExist(uri, VIDEO_PREFIX)).isFalse() - } -} diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index 0139e3f8c..970a97e84 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -25,16 +25,17 @@ import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import androidx.test.uiautomator.UiDevice import com.google.common.truth.Truth +import com.google.jetpackcamera.feature.postcapture.ui.VIEWER_POST_CAPTURE_IMAGE 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 import com.google.jetpackcamera.utils.IMAGE_PREFIX @@ -47,6 +48,7 @@ import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp import com.google.jetpackcamera.utils.doesFileExist import com.google.jetpackcamera.utils.doesMediaExist import com.google.jetpackcamera.utils.ensureTagNotAppears +import com.google.jetpackcamera.utils.expectedNumFiles import com.google.jetpackcamera.utils.getMultipleImageCaptureIntent import com.google.jetpackcamera.utils.getSingleImageCaptureIntent import com.google.jetpackcamera.utils.getTestUri @@ -54,11 +56,13 @@ import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) +@RunWith(TestParameterInjector::class) internal class ImageCaptureDeviceTest { // TODO(b/319733374): Return bitmap for external mediastore capture without URI @@ -72,10 +76,15 @@ internal class ImageCaptureDeviceTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() private val uiDevice = UiDevice.getInstance(instrumentation) + @TestParameter + lateinit var cacheParam: CacheParam + @Test fun image_capture_button() = runMainActivityMediaStoreAutoDeleteScenarioTest( mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - filePrefix = FILE_PREFIX + filePrefix = FILE_PREFIX, + expectedNumFiles = cacheParam.expectedNumFiles(), + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -85,13 +94,17 @@ internal class ImageCaptureDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() .performClick() - composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) + + verifyImageCaptureSuccess() } @Test fun image_capture_volumeUp() = runMainActivityMediaStoreAutoDeleteScenarioTest( mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - filePrefix = FILE_PREFIX + filePrefix = FILE_PREFIX, + expectedNumFiles = cacheParam.expectedNumFiles(), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -100,13 +113,16 @@ internal class ImageCaptureDeviceTest { uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_UP) - composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) + verifyImageCaptureSuccess() } @Test fun image_capture_volumeDown() = runMainActivityMediaStoreAutoDeleteScenarioTest( mediaUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - filePrefix = FILE_PREFIX + filePrefix = FILE_PREFIX, + expectedNumFiles = cacheParam.expectedNumFiles(), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -114,7 +130,7 @@ internal class ImageCaptureDeviceTest { } uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) - composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) + verifyImageCaptureSuccess() } @Test @@ -123,7 +139,8 @@ internal class ImageCaptureDeviceTest { val uri = getTestUri(PICTURES_DIR_PATH, timeStamp, "jpg") val result = runMainActivityScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE), + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -148,7 +165,9 @@ internal class ImageCaptureDeviceTest { val uri = Uri.parse("asdfasdf") val result = runMainActivityScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -175,7 +194,9 @@ internal class ImageCaptureDeviceTest { val uri = getTestUri(PICTURES_DIR_PATH, timeStamp, "mp4") val result = runMainActivityScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -208,7 +229,8 @@ internal class ImageCaptureDeviceTest { getMultipleImageCaptureIntent( uriStrings, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA - ) + ), + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -236,7 +258,9 @@ internal class ImageCaptureDeviceTest { val timeStamp = System.currentTimeMillis() val result = runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -260,7 +284,9 @@ internal class ImageCaptureDeviceTest { fun multipleImageCaptureExternal_withNullUriList_returnsResultCancel() { val result = runMainActivityScenarioTestForResult( - getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -282,7 +308,9 @@ internal class ImageCaptureDeviceTest { getMultipleImageCaptureIntent( uriStrings, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA - ) + ), + extras = cacheParam.extras + ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -324,4 +352,23 @@ internal class ImageCaptureDeviceTest { .assertExists() .performClick() } + + private fun verifyImageCaptureSuccess() { + when (cacheParam) { + CacheParam.NO_CACHE -> { + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_SUCCESS_TAG, + IMAGE_CAPTURE_TIMEOUT_MILLIS + ) + } + + CacheParam.WITH_CACHE -> { + // navigate to postcapture screen + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_IMAGE, + timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + } + } + } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt index a80529f3a..5c0c8ec05 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt @@ -123,6 +123,29 @@ class PostCaptureTest { composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).assertExists().performClick() } + @Test + fun captureImage_navigatesPostcapture_canExit() = runMainActivityScenarioTest(cacheExtra) { + // Wait for the capture button to be displayed + composeTestRule.waitForCaptureButton() + + assertThat(newImageMediaExists()).isFalse() + + composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() + + // navigate to postcapture screen + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_IMAGE, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + + // attempt to exit postcapture + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) + composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() + composeTestRule.waitForCaptureButton() + + assertThat(newImageMediaExists()).isFalse() + } + @Test fun captureImage_navigatesPostcapture_canSaveCachedImage() = runMainActivityScenarioTest(cacheExtra) { diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index bd5557934..86dde9dc8 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -22,15 +22,16 @@ 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 -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import androidx.test.uiautomator.UiDevice import com.google.common.truth.Truth +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 import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS @@ -38,6 +39,7 @@ import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.VIDEO_PREFIX import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp import com.google.jetpackcamera.utils.doesMediaExist +import com.google.jetpackcamera.utils.expectedNumFiles import com.google.jetpackcamera.utils.getSingleImageCaptureIntent import com.google.jetpackcamera.utils.getTestUri import com.google.jetpackcamera.utils.longClickForVideoRecording @@ -47,11 +49,13 @@ import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenari import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) +@RunWith(TestParameterInjector::class) internal class VideoRecordingDeviceTest { @get:Rule val permissionsRule: GrantPermissionRule = @@ -63,9 +67,14 @@ internal class VideoRecordingDeviceTest { private val instrumentation = InstrumentationRegistry.getInstrumentation() private val uiDevice = UiDevice.getInstance(instrumentation) + @TestParameter + lateinit var cacheParam: CacheParam + @Test fun pressed_video_capture(): Unit = runMainActivityMediaStoreAutoDeleteScenarioTest( - mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + expectedNumFiles = cacheParam.expectedNumFiles(), + extras = cacheParam.extras ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed @@ -73,14 +82,17 @@ internal class VideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - composeTestRule.waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) + + verifyVideoCaptureSuccess() deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } @Test fun drag_to_lock_pressed_video_capture(): Unit = runMainActivityMediaStoreAutoDeleteScenarioTest( - mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + expectedNumFiles = cacheParam.expectedNumFiles(), + extras = cacheParam.extras ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed @@ -94,10 +106,7 @@ internal class VideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitForNodeWithTag( - VIDEO_CAPTURE_SUCCESS_TAG, - VIDEO_CAPTURE_TIMEOUT_MILLIS - ) + verifyVideoCaptureSuccess() deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } @@ -108,7 +117,8 @@ internal class VideoRecordingDeviceTest { val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = runScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -127,7 +137,8 @@ internal class VideoRecordingDeviceTest { val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = runScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -150,7 +161,8 @@ internal class VideoRecordingDeviceTest { val uri = Uri.parse("asdfasdf") val result = runScenarioTestForResult( - getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) + getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { @@ -166,4 +178,23 @@ internal class VideoRecordingDeviceTest { Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) Truth.assertThat(doesMediaExist(uri, VIDEO_PREFIX)).isFalse() } + + private fun verifyVideoCaptureSuccess() { + when (cacheParam) { + CacheParam.NO_CACHE -> { + composeTestRule.waitForNodeWithTag( + VIDEO_CAPTURE_SUCCESS_TAG, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + } + + CacheParam.WITH_CACHE -> { + // navigate to postcapture screen + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_VIDEO, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + } + } + } } 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 d505ddcae..bf1a9a2f6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -97,6 +97,16 @@ const val COMPONENT_PACKAGE_NAME = "com.google.jetpackcamera" const val COMPONENT_CLASS = "com.google.jetpackcamera.MainActivity" private const val TAG = "UiTestUtil" +internal enum class CacheParam(val extras: Bundle?) { + NO_CACHE(null), + WITH_CACHE(cacheExtra) +} + +internal fun CacheParam.expectedNumFiles() = when (this) { + CacheParam.NO_CACHE -> 1 + CacheParam.WITH_CACHE -> 0 +} + inline fun runMainActivityMediaStoreAutoDeleteScenarioTest( mediaUri: Uri, filePrefix: String = "", @@ -114,11 +124,15 @@ inline fun runMainActivityMediaStoreAutoDeleteScenarioTest( mediaUri = mediaUri, instrumentation = instrumentation, filePrefix = filePrefix - ).take(expectedNumFiles) - .collect { - Log.d(debugTag, "Discovered new media store file: ${it.first}") - insertedMediaStoreEntries[it.first] = it.second + ).apply { + if (expectedNumFiles > 0) { + take(expectedNumFiles) + .collect { + Log.d(debugTag, "Discovered new media store file: ${it.first}") + insertedMediaStoreEntries[it.first] = it.second + } } + } } var succeeded = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb3ef7362..3a48567b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,7 @@ junit = "4.13.2" protobuf = "4.31.1" robolectric = "4.15.1" truth = "1.4.4" +testParameterInjector = "1.21" playServices = "18.9.0" playServicesCoroutines = "1.10.2" playServicesTasks = "18.4.0" @@ -103,7 +104,7 @@ play-services-base = { module = "com.google.android.gms:play-services-base", ver kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutines" } play-services-camera-low-light-boost = { module = "com.google.android.gms:play-services-camera-low-light-boost", version.ref = "cameraLowLightBoost" } play-services-tasks = { module = "com.google.android.gms:play-services-tasks", version.ref = "playServicesTasks" } - +testParameterInjector = { group = "com.google.testparameterinjector", name = "test-parameter-injector", version.ref = "testParameterInjector" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" }