diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt new file mode 100644 index 00000000..b4b1556b --- /dev/null +++ b/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2024 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. + */ +import android.app.Activity +import android.net.Uri +import android.provider.MediaStore +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.isEnabled +import androidx.compose.ui.test.isNotEnabled +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.MainActivity +import com.google.jetpackcamera.feature.preview.R +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_CAPTURE_MODE_BUTTON +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_DROP_DOWN +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_HDR_BUTTON +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_RATIO_1_1_BUTTON +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_RATIO_BUTTON +import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON +import com.google.jetpackcamera.feature.preview.ui.CAPTURE_MODE_TOGGLE_BUTTON +import com.google.jetpackcamera.feature.preview.ui.FLIP_CAMERA_BUTTON +import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_UNSUPPORTED_CONCURRENT_CAMERA_TAG +import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_SUCCESS_TAG +import com.google.jetpackcamera.settings.model.ConcurrentCameraMode +import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS +import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS +import com.google.jetpackcamera.utils.assume +import com.google.jetpackcamera.utils.getResString +import com.google.jetpackcamera.utils.longClickForVideoRecording +import com.google.jetpackcamera.utils.runMediaStoreAutoDeleteScenarioTest +import com.google.jetpackcamera.utils.runScenarioTest +import com.google.jetpackcamera.utils.stateDescriptionMatches +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ConcurrentCameraTest { + @get:Rule + val permissionsRule: GrantPermissionRule = + GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray()) + + @get:Rule + val composeTestRule = createEmptyComposeRule() + + @Test + fun concurrentCameraMode_canBeEnabled() = runConcurrentCameraScenarioTest { + val concurrentCameraModes = mutableListOf() + with(composeTestRule) { + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists().apply { + // Check the original mode + fetchSemanticsNode().let { node -> + concurrentCameraModes.add(node.fetchConcurrentCameraMode()) + } + } + // Enable concurrent camera + .performClick().apply { + // Check the mode has changed + fetchSemanticsNode().let { node -> + concurrentCameraModes.add(node.fetchConcurrentCameraMode()) + } + } + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Assert that the flip camera button is visible + onNodeWithTag(FLIP_CAMERA_BUTTON) + .assertIsDisplayed() + } + + assertThat(concurrentCameraModes).containsExactly( + ConcurrentCameraMode.OFF, + ConcurrentCameraMode.DUAL + ).inOrder() + } + + @Test + fun concurrentCameraMode_whenEnabled_canBeDisabled() = + runConcurrentCameraScenarioTest { + val concurrentCameraModes = mutableListOf() + with(composeTestRule) { + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists().apply { + // Check the original mode + fetchSemanticsNode().let { node -> + concurrentCameraModes.add(node.fetchConcurrentCameraMode()) + } + } + // Enable concurrent camera + .performClick().apply { + // Check the mode has changed + fetchSemanticsNode().let { node -> + concurrentCameraModes.add(node.fetchConcurrentCameraMode()) + } + } + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Assert that the flip camera button is visible + onNodeWithTag(FLIP_CAMERA_BUTTON) + .assertIsDisplayed() + + // Enter quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + // Disable concurrent camera + .performClick().apply { + // Check the mode is back to OFF + fetchSemanticsNode().let { node -> + concurrentCameraModes.add(node.fetchConcurrentCameraMode()) + } + } + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Assert that the flip camera button is visible + onNodeWithTag(FLIP_CAMERA_BUTTON) + .assertIsDisplayed() + } + + assertThat(concurrentCameraModes).containsExactly( + ConcurrentCameraMode.OFF, + ConcurrentCameraMode.DUAL, + ConcurrentCameraMode.OFF + ).inOrder() + } + + @Test + fun concurrentCameraMode_whenEnabled_canFlipCamera() = + runConcurrentCameraScenarioTest { + with(composeTestRule) { + // Check device has multiple cameras + onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON) + .assertExists() + .assume(isEnabled()) { + "Device does not have multiple cameras." + } + + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + .assertConcurrentCameraMode(ConcurrentCameraMode.OFF) + // Enable concurrent camera + .performClick() + .assertConcurrentCameraMode(ConcurrentCameraMode.DUAL) + + onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON) + .assertExists() + .performClick() + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Assert that the flip camera button is visible + onNodeWithTag(FLIP_CAMERA_BUTTON) + .assertIsDisplayed() + } + } + + @Test + fun concurrentCameraMode_whenEnabled_canSwitchAspectRatio() = + runConcurrentCameraScenarioTest { + with(composeTestRule) { + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + .assertConcurrentCameraMode(ConcurrentCameraMode.OFF) + // Enable concurrent camera + .performClick() + .assertConcurrentCameraMode(ConcurrentCameraMode.DUAL) + + // Click the ratio button + composeTestRule.onNodeWithTag(QUICK_SETTINGS_RATIO_BUTTON) + .assertExists() + .performClick() + + // Click the 1:1 ratio button + composeTestRule.onNodeWithTag(QUICK_SETTINGS_RATIO_1_1_BUTTON) + .assertExists() + .performClick() + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Assert that the flip camera button is visible + onNodeWithTag(FLIP_CAMERA_BUTTON) + .assertIsDisplayed() + } + } + + @Test + fun concurrentCameraMode_whenEnabled_disablesOtherSettings() = + runConcurrentCameraScenarioTest { + with(composeTestRule) { + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + .assertConcurrentCameraMode(ConcurrentCameraMode.OFF) + // Enable concurrent camera + .performClick() + .assertConcurrentCameraMode(ConcurrentCameraMode.DUAL) + + // Assert the capture mode button is disabled + onNodeWithTag(QUICK_SETTINGS_CAPTURE_MODE_BUTTON) + .assertExists() + .assert(isNotEnabled()) + + // Assert the HDR button is disabled + onNodeWithTag(QUICK_SETTINGS_HDR_BUTTON) + .assertExists() + .assert(isNotEnabled()) + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON) + .assertExists() + .assert( + stateDescriptionMatches( + getResString(R.string.capture_mode_video_recording_content_description) + ) + ).performClick() + + waitUntil { + onNodeWithTag(IMAGE_CAPTURE_UNSUPPORTED_CONCURRENT_CAMERA_TAG).isDisplayed() + } + } + } + + @Test + fun concurrentCameraMode_canRecordVideo() = runConcurrentCameraScenarioTest( + mediaUriForSavedFiles = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + ) { + with(composeTestRule) { + onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + .assertConcurrentCameraMode(ConcurrentCameraMode.OFF) + // Enable concurrent camera + .performClick() + .assertConcurrentCameraMode(ConcurrentCameraMode.DUAL) + + // Exit quick settings + onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + longClickForVideoRecording() + + waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { + composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() + } + } + } + + // Ensures the app has launched and checks that the device supports concurrent camera before + // running the test. + // This test will start with quick settings visible + private inline fun runConcurrentCameraScenarioTest( + mediaUriForSavedFiles: Uri? = null, + expectedMediaFiles: Int = 1, + crossinline block: ActivityScenario.() -> Unit + ) { + val wrappedBlock: ActivityScenario.() -> Unit = { + // Wait for the capture button to be displayed + composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { + composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() + } + + // /////////////////////////////////////////////////// + // Check that the device supports concurrent camera // + // /////////////////////////////////////////////////// + // Navigate to quick settings + composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) + .assertExists() + .performClick() + + // Check that the concurrent camera button is enabled + composeTestRule.onNodeWithTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON) + .assertExists() + .assume(isEnabled()) { + "Device does not support concurrent camera." + } + + // /////////////////////////////////////////////////// + // Run the actual test // + // /////////////////////////////////////////////////// + block() + } + + if (mediaUriForSavedFiles != null) { + runMediaStoreAutoDeleteScenarioTest( + mediaUri = mediaUriForSavedFiles, + expectedNumFiles = expectedMediaFiles, + block = wrappedBlock + ) + } else { + runScenarioTest(wrappedBlock) + } + } + + context(SemanticsNodeInteractionsProvider) + private fun SemanticsNode.fetchConcurrentCameraMode(): ConcurrentCameraMode { + config[SemanticsProperties.ContentDescription].any { description -> + when (description) { + getResString(R.string.quick_settings_concurrent_camera_off_description) -> + return ConcurrentCameraMode.OFF + + getResString(R.string.quick_settings_concurrent_camera_dual_description) -> + return ConcurrentCameraMode.DUAL + + else -> false + } + } + throw AssertionError("Unable to determine concurrent camera mode from quick settings") + } + + context(SemanticsNodeInteractionsProvider) + private fun SemanticsNodeInteraction.assertConcurrentCameraMode( + mode: ConcurrentCameraMode + ): SemanticsNodeInteraction { + assertThat(fetchSemanticsNode().fetchConcurrentCameraMode()) + .isEqualTo(mode) + return this + } +} diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index 41a910e7..a537bb82 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -19,12 +19,10 @@ import android.app.Activity import android.net.Uri import android.os.Environment import android.provider.MediaStore -import androidx.compose.ui.test.ComposeTimeoutException 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.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule @@ -38,11 +36,11 @@ import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS -import com.google.jetpackcamera.utils.VIDEO_DURATION_MILLIS import com.google.jetpackcamera.utils.deleteFilesInDirAfterTimestamp import com.google.jetpackcamera.utils.doesImageFileExist import com.google.jetpackcamera.utils.getIntent import com.google.jetpackcamera.utils.getTestUri +import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.runMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult import org.junit.Rule @@ -70,7 +68,7 @@ internal class VideoRecordingDeviceTest { composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } - longClickForVideoRecording() + composeTestRule.longClickForVideoRecording() composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() } @@ -88,7 +86,7 @@ internal class VideoRecordingDeviceTest { composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } - longClickForVideoRecording() + composeTestRule.longClickForVideoRecording() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) Truth.assertThat(doesImageFileExist(uri, "video")).isTrue() @@ -106,7 +104,7 @@ internal class VideoRecordingDeviceTest { composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } - longClickForVideoRecording() + composeTestRule.longClickForVideoRecording() composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { composeTestRule.onNodeWithTag(VIDEO_CAPTURE_FAILURE_TAG).isDisplayed() } @@ -143,30 +141,6 @@ internal class VideoRecordingDeviceTest { Truth.assertThat(doesImageFileExist(uri, "image")).isFalse() } - private fun longClickForVideoRecording() { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performTouchInput { - down(center) - } - idleForVideoDuration() - composeTestRule.onNodeWithTag(CAPTURE_BUTTON) - .assertExists() - .performTouchInput { - up() - } - } - - private fun idleForVideoDuration() { - // TODO: replace with a check for the timestamp UI of the video duration - try { - composeTestRule.waitUntil(timeoutMillis = VIDEO_DURATION_MILLIS) { - composeTestRule.onNodeWithTag("dummyTagForLongPress").isDisplayed() - } - } catch (e: ComposeTimeoutException) { - } - } - companion object { val DIR_PATH: String = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).path 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 e3be80e9..b6e0388d 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -17,13 +17,26 @@ package com.google.jetpackcamera.utils import android.content.Context import androidx.annotation.StringRes +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.ComposeTimeoutException import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.printToString +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider +import com.google.jetpackcamera.MainActivity +import com.google.jetpackcamera.feature.preview.R +import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON +import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON +import com.google.jetpackcamera.settings.model.LensFacing import org.junit.AssumptionViolatedException /** @@ -84,6 +97,70 @@ fun SemanticsNodeInteraction.assume( return this } +fun ComposeTestRule.longClickForVideoRecording() { + onNodeWithTag(CAPTURE_BUTTON) + .assertExists() + .performTouchInput { + down(center) + } + idleForVideoDuration() + onNodeWithTag(CAPTURE_BUTTON) + .assertExists() + .performTouchInput { + up() + } +} + +private 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) { + } +} + +context(ActivityScenario) +fun ComposeTestRule.getCurrentLensFacing(): LensFacing { + var needReturnFromQuickSettings = false + onNodeWithContentDescription(R.string.quick_settings_dropdown_closed_description).apply { + if (isDisplayed()) { + performClick() + needReturnFromQuickSettings = true + } + } + + onNodeWithContentDescription(R.string.quick_settings_dropdown_open_description).assertExists( + "LensFacing can only be retrieved from PreviewScreen or QuickSettings screen" + ) + + try { + return onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON).fetchSemanticsNode( + "Flip camera button is not visible when expected." + ).let { node -> + node.config[SemanticsProperties.ContentDescription].any { description -> + when (description) { + getResString(R.string.quick_settings_front_camera_description) -> + return@let LensFacing.FRONT + + getResString(R.string.quick_settings_back_camera_description) -> + return@let LensFacing.BACK + + else -> false + } + } + throw AssertionError("Unable to determine lens facing from quick settings") + } + } finally { + if (needReturnFromQuickSettings) { + onNodeWithContentDescription(R.string.quick_settings_dropdown_open_description) + .assertExists() + .performClick() + } + } +} + internal fun buildGeneralErrorMessage( errorMessage: String, nodeInteraction: SemanticsNodeInteraction 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 c5f973b6..bf5e8e4f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -23,18 +23,11 @@ import android.net.Uri import android.provider.MediaStore import android.util.Log import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.SemanticsMatcher import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertWithMessage -import com.google.jetpackcamera.MainActivity -import com.google.jetpackcamera.feature.preview.R -import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON -import com.google.jetpackcamera.settings.model.LensFacing import java.io.File import java.net.URLConnection import java.util.concurrent.TimeoutException @@ -154,46 +147,6 @@ suspend inline fun ActivityScenario.pollResult( ) } -context(ActivityScenario) -fun ComposeTestRule.getCurrentLensFacing(): LensFacing { - var needReturnFromQuickSettings = false - onNodeWithContentDescription(R.string.quick_settings_dropdown_closed_description).apply { - if (isDisplayed()) { - performClick() - needReturnFromQuickSettings = true - } - } - - onNodeWithContentDescription(R.string.quick_settings_dropdown_open_description).assertExists( - "LensFacing can only be retrieved from PreviewScreen or QuickSettings screen" - ) - - try { - return onNodeWithTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON).fetchSemanticsNode( - "Flip camera button is not visible when expected." - ).let { node -> - node.config[SemanticsProperties.ContentDescription].any { description -> - when (description) { - getResString(R.string.quick_settings_front_camera_description) -> - return@let LensFacing.FRONT - - getResString(R.string.quick_settings_back_camera_description) -> - return@let LensFacing.BACK - - else -> false - } - } - throw AssertionError("Unable to determine lens facing from quick settings") - } - } finally { - if (needReturnFromQuickSettings) { - onNodeWithContentDescription(R.string.quick_settings_dropdown_open_description) - .assertExists() - .performClick() - } - } -} - fun getTestUri(directoryPath: String, timeStamp: Long, suffix: String): Uri { return Uri.fromFile( File( @@ -244,3 +197,8 @@ fun getIntent(uri: Uri, action: String): Intent { intent.putExtra(MediaStore.EXTRA_OUTPUT, uri) return intent } + +fun stateDescriptionMatches(expected: String?) = SemanticsMatcher("stateDescription is $expected") { + SemanticsProperties.StateDescription in it.config && + (it.config[SemanticsProperties.StateDescription] == expected) +}