Compose CardFields Instrumentations Tests - #1692
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The newly added instrumentation tests have compile-breaking missing imports (and one test name/behavior mismatch) that should be corrected before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds Jetpack Compose instrumentation coverage for the UIComponents CardFields Compose form, validating individual input behavior (card number, expiration, CVV) and cross-field interactions (auto-advance + validation-on-blur). This fits into the UIComponents module’s Compose-based card entry experience by improving confidence in accessibility-driven UI interactions and field state transitions.
Changes:
- Added new
androidTestCompose UI tests forCardNumberField,CardExpirationField, andCardCvvField. - Added a form-level
CardFieldstest suite covering auto-advance focus behavior and validation error display rules. - Updated UIComponents build + version catalog to include Compose UI test dependencies.
File summaries
| File | Description |
|---|---|
| UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardNumberFieldTest.kt | Adds instrumentation tests for card number input + brand icon detection. |
| UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardFieldsFormTest.kt | Adds end-to-end form tests for focus auto-advance and validation-on-blur behavior. |
| UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardExpirationFieldTest.kt | Adds instrumentation tests for expiration input sanitization/formatting behavior. |
| UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardCvvFieldTest.kt | Adds instrumentation tests for CVV input and the hint overlay UI. |
| UIComponents/build.gradle | Adds Compose UI test dependencies for androidTest and ui-test-manifest for debug. |
| gradle/libs.versions.toml | Adds version-catalog entries for Compose UI testing artifacts. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🔵 Needs a closer look
The new androidTests currently have compilation/runner consistency issues (e.g., missing assertDoesNotExist import and missing @RunWith(AndroidJUnit4ClassRunner::class) in the new test classes).
Review details
Suppressed comments (5)
Previously missed (3) — in code that hasn't changed since the last review.
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardFieldsFormTest.kt:24
- Add the AndroidJUnit4 test runner annotation (consistent with other UIComponents instrumentation tests, e.g. cardfields/CardFieldsTest.kt) so this test runs under the same runner configuration.
class CardFieldsFormTest {
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardNumberFieldTest.kt:20
- UIComponents instrumentation tests in this repo consistently declare @RunWith(AndroidJUnit4ClassRunner::class) (e.g. uicomponents/cardfields/CardFieldsTest.kt). Adding it here keeps runner behavior consistent for these Compose androidTests.
import androidx.test.platform.app.InstrumentationRegistry
import com.braintreepayments.api.uicomponents.R
import com.braintreepayments.api.uicomponents.cardfields.CardBrand
import org.junit.Rule
import org.junit.Test
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardExpirationFieldTest.kt:18
- UIComponents instrumentation tests in this repo consistently declare @RunWith(AndroidJUnit4ClassRunner::class) (e.g. uicomponents/cardfields/CardFieldsTest.kt). Adding it here keeps runner behavior consistent for these Compose androidTests.
import androidx.test.platform.app.InstrumentationRegistry
import com.braintreepayments.api.uicomponents.R
import org.junit.Rule
import org.junit.Test
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardFieldsFormTest.kt:13
assertDoesNotExist()is used in this test file but the corresponding Compose test import is missing, and the file also lacks the @RunWith(AndroidJUnit4ClassRunner::class) annotation used by other UIComponents androidTest classes (e.g. cardfields/CardFieldsTest.kt). This will currently fail compilation and may run under a different runner than the rest of the module.
import androidx.compose.ui.test.performTextInput
import androidx.test.platform.app.InstrumentationRegistry
import com.braintreepayments.api.uicomponents.R
import org.junit.Rule
import org.junit.Test
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardCvvFieldTest.kt:20
assertDoesNotExist()is used but not imported, and the class is missing the @RunWith(AndroidJUnit4ClassRunner::class) annotation that other UIComponents androidTest classes use. This will fail compilation and makes the runner inconsistent.
import androidx.test.platform.app.InstrumentationRegistry
import com.braintreepayments.api.uicomponents.R
import org.junit.Rule
import org.junit.Test
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
saralvasquez
left a comment
There was a problem hiding this comment.
Just a couple test cases I think could be good to add but otherwise these look great!
| } | ||
|
|
||
| @Test | ||
| fun tappingCloseButton_dismissesHintPopup() { |
There was a problem hiding this comment.
Do we also want a test that clicking anywhere on the screen outside the popup will dismiss it? I recall that being important functionality
| import org.junit.Test | ||
|
|
||
| /** | ||
| * Cross-field behavior owned by the [CardFields] composable + `CardFieldsViewModel`, |
There was a problem hiding this comment.
I noticed we don't have a test to see that the pay button becomes enabled after all fields are valid. Is that something that's testable since it's not technically part of card fields?
There was a problem hiding this comment.
So I was contemplating about that myself as well. Since the button is technically owned by a merchant, but i see that XML cardFields do have a test for the button, so I can add it here as well.
| } | ||
|
|
||
| @Test | ||
| fun typingDigits_updatesFieldText() { |
There was a problem hiding this comment.
Do we want a test here that passes a value that's too long like we do for expiration?
| import org.junit.Rule | ||
| import org.junit.Test | ||
|
|
||
| class CardCvvFieldTest { |
There was a problem hiding this comment.
Do we also want a test here that passes a value that's too long like for expiration? We also might want to check that the max value changes with the right card brand. But that might be better in the CardFieldsFormTest. What do you think?
There was a problem hiding this comment.
I think both of these scenarios are good to add. I think I can add a test for long value here, but the test for length change according to the brand in CardFieldsFormTest
| composeTestRule.onNodeWithContentDescription(str(R.string.cvv_accessibility)) | ||
| .performTextInput("123") | ||
|
|
||
| composeTestRule.onNodeWithContentDescription(str(R.string.cvv_accessibility)) |
There was a problem hiding this comment.
Possibly dumb question: What is R?
There was a problem hiding this comment.
Great question, R is autogenerated class, that assigns an id to your resources: https://developer.android.com/guide/topics/resources/providing-resources
so here we are just grabbing the string we defined that has an accessibility text from strings_card_fields.xml
bc55fd8 to
6dcd26d
Compare
There was a problem hiding this comment.
🟢 Approval recommended
Changes are additive test coverage + scoped dependency additions, with no impact to the shipped runtime behavior.
Review details
Suppressed comments (1)
UIComponents/src/androidTest/java/com/braintreepayments/api/uicomponents/compose/CardFieldsFormTest.kt:20
- In the KDoc bullet list, there’s an extra space before the colon in “Validation state machine :”, which reads like a typo.
* 2. **Validation state machine :** while the user is typing, an incomplete value
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One of the UIAutomator-based dismissal tests relies on a bottom-of-screen coordinate that can land on the system navigation bar and make CI runs flaky.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
| val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) | ||
| // Tap anywhere on the screen outside the popup to dismiss it | ||
| device.click(device.displayWidth / 2, device.displayHeight - 100) | ||
|
|
| androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" } | ||
| androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } | ||
| androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } | ||
| androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } |
There was a problem hiding this comment.
Was this needed to test Compose features/functionality? It seems like a new/different way of writing Android tests so I'm curious how the team usually goes about adopting new dependencies like this one.
I also see that there is a newer version out, albeit an alpha. The docs do say:
The API is under development, and we strongly recommend using it for any new development with UI Automator.
Maybe we should consider using the newer version in the style that Google is going to be developing for moving forward since we're introducing here for the first time.
There was a problem hiding this comment.
I needed to add this to test the behavior of tapping anywhere on the screen to close the cvv info popup, and I did tried to use 2.4.0, but that version resulted in
e: Class 'kotlin.reflect.KClass' was compiled with an incompatible version of Kotlin. The actual metadata version is 2.1.0, but the compiler version 1.9.0 can read versions up to 2.0.0.
So in order to use a new version, we would need to update Kotlin to at least 2.1.0. So I opted to using the olde version. Thoughts?
There was a problem hiding this comment.
Yea I guess we'll be stuck using the older one until we can determine if/when we can upgrade to Kotlin 2+
Summary of changes
AI Usage
Which AI Agent Was Used?
How was AI used?
Brainstroming and getting familiar with syntax to use to test Compose
Estimated AI Code Contribution
Checklist
Authors