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
3 changes: 3 additions & 0 deletions .gemini/styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ When reviewing a pull request, focus on the following key areas:
* **Descriptive Test Names:** Test function names must be clear, descriptive, and follow a consistent pattern.
* **Use Truth Assertions:** Always prefer [Truth](https://truth.dev/) assertions (`assertThat(...)`) over JUnit assertions (`assertEquals`, `assertTrue`, etc.). Truth provides more readable assertion chains and more informative failure messages. Avoid functions from `org.junit.Assert` such as `assertEquals`, `assertTrue`, `assertFalse`, `assertNull`, and `assertNotNull`.
* **Explicit Test Runners:** All test classes must be annotated with `@RunWith(...)` to explicitly declare which test runner should be used (e.g., `@RunWith(AndroidJUnit4::class)`, `@RunWith(RobolectricTestRunner::class)`, or `@RunWith(JUnit4::class)` for host tests with no Android dependencies).
* **Test Stability & Timeouts:**
* **Explicit Timeouts:** Avoid using `waitUntil` (or similar synchronization) without explicitly defining a `timeoutMillis`. Default timeouts are often too short for slower emulators (like API 28) or low-end devices, leading to flakiness.
* **Helper Functions for Waits:** If a wait condition is repeated (e.g., waiting for a specific UI element), extract it into a helper function (e.g., `waitForNodeWithTag`). This consolidates the logic and allows the timeout duration to be tuned centrally for that specific scenario.

6. **Documentation Sync**
* **Check for necessary updates:** Analyze if the PR's changes (e.g., adding a new feature, changing build logic, deprecating functionality) require updates to `README.md` or other documentation files.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ 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
Expand Down Expand Up @@ -79,12 +80,11 @@ class CachedImageCaptureDeviceTest {
.performClick()

// navigate to postcapture screen
composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_IMAGE).isDisplayed()
}
composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).isDisplayed()
}
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()
Expand Down Expand Up @@ -135,9 +135,10 @@ class CachedImageCaptureDeviceTest {
.assertExists()
.performClick()

composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(IMAGE_CAPTURE_FAILURE_TAG).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
IMAGE_CAPTURE_FAILURE_TAG,
timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS
)
uiDevice.pressBack()
}
Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT
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
Expand All @@ -70,14 +71,11 @@ class CachedVideoRecordingDeviceTest {
composeTestRule.longClickForVideoRecordingCheckingElapsedTime()

// navigate to postcapture screen
composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_VIDEO).isDisplayed()
}
composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(
BUTTON_POST_CAPTURE_EXIT
).isDisplayed()
}
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()
Expand Down Expand Up @@ -116,9 +114,10 @@ class CachedVideoRecordingDeviceTest {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed()
}
composeTestRule.longClickForVideoRecording()
composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(VIDEO_CAPTURE_FAILURE_TAG).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
VIDEO_CAPTURE_FAILURE_TAG,
VIDEO_CAPTURE_TIMEOUT_MILLIS
)
uiDevice.pressBack()
}
Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import android.provider.MediaStore
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isNotDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
Expand Down Expand Up @@ -63,7 +61,9 @@ import com.google.jetpackcamera.utils.unFocusQuickSetting
import com.google.jetpackcamera.utils.visitQuickSettings
import com.google.jetpackcamera.utils.wait
import com.google.jetpackcamera.utils.waitForCaptureButton
import com.google.jetpackcamera.utils.waitForCaptureModeToggleState
import com.google.jetpackcamera.utils.waitForNodeWithTag
import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -108,13 +108,20 @@ internal class CaptureModeSettingsTest {
setCaptureMode(captureMode)
}

waitUntil(DEFAULT_TIMEOUT_MILLIS) {
onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isDisplayed()
}
waitForNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON, DEFAULT_TIMEOUT_MILLIS)

onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertExists()
}

private fun flip(mode: CaptureMode): CaptureMode {
require(mode == CaptureMode.IMAGE_ONLY || mode == CaptureMode.VIDEO_ONLY)
return if (mode == CaptureMode.IMAGE_ONLY) {
CaptureMode.VIDEO_ONLY
} else {
CaptureMode.IMAGE_ONLY
}
}

@Test
fun can_set_capture_mode_in_quick_settings() {
runMainActivityScenarioTest {
Expand Down Expand Up @@ -393,18 +400,15 @@ internal class CaptureModeSettingsTest {

composeTestRule.initializeCaptureSwitch()
val initialCaptureMode = composeTestRule.getCaptureModeToggleState()
val targetCaptureMode = flip(initialCaptureMode)

// should be different from initial capture mode
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).performClick()
composeTestRule.waitUntil {
composeTestRule.getCaptureModeToggleState() != initialCaptureMode
}
composeTestRule.waitForCaptureModeToggleState(targetCaptureMode)

// should now be she same as the initial capture mode.
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).performClick()
composeTestRule.waitUntil {
composeTestRule.getCaptureModeToggleState() == initialCaptureMode
}
composeTestRule.waitForCaptureModeToggleState(initialCaptureMode)
}

@Test
Expand All @@ -413,6 +417,7 @@ internal class CaptureModeSettingsTest {
composeTestRule.waitForCaptureButton()
composeTestRule.initializeCaptureSwitch()
val initialCaptureMode = composeTestRule.getCaptureModeToggleState()
val targetCaptureMode = flip(initialCaptureMode)
val captureToggleNode = composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON)
val toggleNodeWidth = captureToggleNode.fetchSemanticsNode().size.width.toFloat()
val offsetToSwitch = when (initialCaptureMode) {
Expand Down Expand Up @@ -442,9 +447,7 @@ internal class CaptureModeSettingsTest {
captureToggleNode.performTouchInput {
up()
}
composeTestRule.waitUntil {
initialCaptureMode != composeTestRule.getCaptureModeToggleState()
}
composeTestRule.waitForCaptureModeToggleState(targetCaptureMode)
}

@Test
Expand All @@ -459,17 +462,13 @@ internal class CaptureModeSettingsTest {
// start recording
composeTestRule.tapStartLockedVideoRecording()
// check that recording
composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isNotDisplayed()
}
composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_MODE_TOGGLE_BUTTON)

// stop recording
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick()

composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isDisplayed() &&
composeTestRule.getCaptureModeToggleState() == CaptureMode.VIDEO_ONLY
}
composeTestRule.waitForNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON)
composeTestRule.waitForCaptureModeToggleState(CaptureMode.VIDEO_ONLY)

deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsNodeInteraction
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
Expand All @@ -33,7 +32,6 @@ import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.MainActivity
import com.google.jetpackcamera.model.ConcurrentCameraMode
import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUS_CAPTURE_MODE
import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON
import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN
Expand All @@ -44,7 +42,6 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTO
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONFIG_BUTTON
import com.google.jetpackcamera.ui.components.capture.R
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.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.assume
Expand All @@ -53,6 +50,8 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT
import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.stateDescriptionMatches
import com.google.jetpackcamera.utils.waitForCaptureButton
import com.google.jetpackcamera.utils.waitForNodeWithTag
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -280,9 +279,7 @@ class ConcurrentCameraTest {

longClickForVideoRecordingCheckingElapsedTime()

waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed()
}
waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS)
}
}

Expand All @@ -296,9 +293,7 @@ class ConcurrentCameraTest {
) {
val wrappedBlock: ActivityScenario<MainActivity>.() -> Unit = {
// Wait for the capture button to be displayed
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed()
}
composeTestRule.waitForCaptureButton()

// ///////////////////////////////////////////////////
// Check that the device supports concurrent camera //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package com.google.jetpackcamera

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
Expand All @@ -39,6 +37,8 @@ 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 com.google.jetpackcamera.utils.waitForNodeWithTag
import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -73,9 +73,7 @@ class DebugHideComponentsTest {

composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick()

composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isNotDisplayed()
}
composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_BUTTON)
composeTestRule.onNodeWithTag(ZOOM_BUTTON_ROW_TAG).assertDoesNotExist()
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist()
composeTestRule.onNodeWithTag(AMPLITUDE_NONE_TAG).assertDoesNotExist()
Expand All @@ -87,9 +85,7 @@ class DebugHideComponentsTest {

composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick()

composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed()
}
composeTestRule.waitForNodeWithTag(CAPTURE_BUTTON)
composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists()
composeTestRule.onNodeWithTag(DEBUG_OVERLAY_BUTTON).assertExists()
composeTestRule.onNodeWithTag(LOGICAL_CAMERA_ID_TAG).assertExists()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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.waitForNodeWithTag
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -141,9 +142,7 @@ internal class FlashDeviceTest {
.assertExists()
.performClick()

composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed()
}
composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS)
}

@Test
Expand Down Expand Up @@ -172,13 +171,15 @@ internal class FlashDeviceTest {
.assertExists()
.performClick()

composeTestRule.waitUntil(timeoutMillis = SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(SCREEN_FLASH_OVERLAY).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
SCREEN_FLASH_OVERLAY,
SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS
)

composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
IMAGE_CAPTURE_SUCCESS_TAG,
IMAGE_CAPTURE_TIMEOUT_MILLIS
)
}

@Test
Expand Down Expand Up @@ -209,8 +210,9 @@ internal class FlashDeviceTest {
composeTestRule.setFlashMode(FlashMode.ON)

composeTestRule.longClickForVideoRecordingCheckingElapsedTime()
composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
VIDEO_CAPTURE_SUCCESS_TAG,
VIDEO_CAPTURE_TIMEOUT_MILLIS
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package com.google.jetpackcamera
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.test.click
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isNotDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
Expand All @@ -37,6 +35,8 @@ import com.google.jetpackcamera.utils.debugExtra
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.wait
import com.google.jetpackcamera.utils.waitForCaptureButton
import com.google.jetpackcamera.utils.waitForNodeWithTag
import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -58,9 +58,7 @@ class FocusMeteringTest {
// Hide all components so we don't accidentally tap on them
composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick()

composeTestRule.waitUntil {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isNotDisplayed()
}
composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_BUTTON)

// Define the four quadrants of the screen
val quadrants = listOf(
Expand All @@ -79,11 +77,10 @@ class FocusMeteringTest {
performTouchInput { click(position = percentOffset(x, y)) }

// Wait for the focus metering indicator to be visible
composeTestRule.waitUntil(FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(
FOCUS_METERING_INDICATOR_TAG
).isDisplayed()
}
composeTestRule.waitForNodeWithTag(
FOCUS_METERING_INDICATOR_TAG,
FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS
)

composeTestRule.waitUntil(FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS) {
composeTestRule.onAllNodesWithTag(FOCUS_METERING_INDICATOR_TAG).run {
Expand Down
Loading
Loading