Skip to content

Commit

Permalink
Add initial CustomerSheet end-to-end tests. (#8525)
Browse files Browse the repository at this point in the history
* Add initial `CustomerSheet` end-to-end tests.

* Conditionally click the payment selection if needed.

* Remove `SetupIntent` tests & move `CustomerAdapter` to own flow.
  • Loading branch information
samer-stripe committed May 24, 2024
1 parent 77c3231 commit 9468357
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.stripe.android.lpm

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stripe.android.BasePlaygroundTest
import com.stripe.android.paymentsheet.example.playground.settings.Country
import com.stripe.android.paymentsheet.example.playground.settings.CountrySettingsDefinition
import com.stripe.android.paymentsheet.example.playground.settings.CustomerSettingsDefinition
import com.stripe.android.paymentsheet.example.playground.settings.CustomerSheetPaymentMethodModeDefinition
import com.stripe.android.paymentsheet.example.playground.settings.CustomerType
import com.stripe.android.paymentsheet.example.playground.settings.PaymentMethodMode
import com.stripe.android.test.core.TestParameters
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
internal class TestCardInCustomerSheet : BasePlaygroundTest() {
@Test
fun testCard() {
testDriver.savePaymentMethodInCustomerSheet(
TestParameters.create(paymentMethodCode = "card").copyPlaygroundSettings { settings ->
settings[CustomerSettingsDefinition] = CustomerType.NEW
settings[CountrySettingsDefinition] = Country.US
settings[CustomerSheetPaymentMethodModeDefinition] = PaymentMethodMode.CreateAndAttach
},
populateCustomLpmFields = {
populateCardDetails()
},
)
}

@Test
fun testCardWithNonUsMerchant() {
testDriver.savePaymentMethodInCustomerSheet(
TestParameters.create(paymentMethodCode = "card").copyPlaygroundSettings { settings ->
settings[CustomerSettingsDefinition] = CustomerType.NEW
settings[CountrySettingsDefinition] = Country.FR
settings[CustomerSheetPaymentMethodModeDefinition] = PaymentMethodMode.CreateAndAttach
},
populateCustomLpmFields = {
populateCardDetails()
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ internal class FieldPopulator(
}

fun populateFields() {
selectors.formElement.waitFor()

populateCustomLpmFields()

selectors.composeTestRule.waitForIdle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
Expand Down Expand Up @@ -195,7 +196,7 @@ internal class PlaygroundTestDriver(
// and click the payment method.
addPaymentMethodNode().performClick()
}
selectors.paymentSelection.click()
clickPaymentSelection()

val fieldPopulator = FieldPopulator(
selectors,
Expand Down Expand Up @@ -251,7 +252,7 @@ internal class PlaygroundTestDriver(
// and click the payment method.
addPaymentMethodNode().performClick()
}
selectors.paymentSelection.click()
clickPaymentSelection()

val fieldPopulator = FieldPopulator(
selectors,
Expand Down Expand Up @@ -313,6 +314,50 @@ internal class PlaygroundTestDriver(
teardown()
}

fun savePaymentMethodInCustomerSheet(
testParameters: TestParameters,
values: FieldPopulator.Values = FieldPopulator.Values(),
populateCustomLpmFields: FieldPopulator.() -> Unit = {},
): PlaygroundState? {
setup(
testParameters.copyPlaygroundSettings { settings ->
settings.updateConfigurationData { configurationData ->
configurationData.copy(
integrationType = PlaygroundConfigurationData.IntegrationType.CustomerSheet
)
}
}
)

launchCustomerSheet()

clickPaymentSelection()

val fieldPopulator = FieldPopulator(
selectors,
testParameters,
populateCustomLpmFields,
{},
values,
)
fieldPopulator.populateFields()

val result = playgroundState

pressCustomerSheetSave()

waitForManageSavedPaymentMethods()

pressCustomerSheetConfirm()

Espresso.onIdle()
composeTestRule.waitForIdle()

teardown()

return result
}

private fun pressMultiStepSelect() {
selectors.multiStepSelect.click()
waitForNotPlaygroundActivity()
Expand All @@ -325,6 +370,20 @@ internal class PlaygroundTestDriver(
}
}

private fun pressCustomerSheetSave() {
Espresso.onIdle()
composeTestRule.waitForIdle()

selectors.customerSheetSaveButton.click()
}

private fun pressCustomerSheetConfirm() {
Espresso.onIdle()
composeTestRule.waitForIdle()

selectors.customerSheetConfirmButton.click()
}

/**
* This will open the payment sheet complete flow from the playground with a new or
* guest user and complete the confirmation including any browser interactions.
Expand All @@ -340,7 +399,7 @@ internal class PlaygroundTestDriver(
setup(testParameters)
launchComplete()

selectors.paymentSelection.click()
clickPaymentSelection()

FieldPopulator(
selectors,
Expand Down Expand Up @@ -393,7 +452,7 @@ internal class PlaygroundTestDriver(
waitForAddPaymentMethodNode()
addPaymentMethodNode().performClick()

selectors.paymentSelection.click()
clickPaymentSelection()

FieldPopulator(
selectors,
Expand Down Expand Up @@ -515,7 +574,7 @@ internal class PlaygroundTestDriver(
private fun confirmExternalPaymentMethod(
button: ComposeButton,
) {
selectors.paymentSelection.click()
clickPaymentSelection()

pressBuy()

Expand Down Expand Up @@ -576,7 +635,7 @@ internal class PlaygroundTestDriver(
setup(testParameters)
launchComplete()

selectors.paymentSelection.click()
clickPaymentSelection()

FieldPopulator(
selectors = selectors,
Expand Down Expand Up @@ -632,7 +691,7 @@ internal class PlaygroundTestDriver(
// and click the payment method.
addPaymentMethodNode().performClick()
}
selectors.paymentSelection.click()
clickPaymentSelection()

FieldPopulator(
selectors = selectors,
Expand Down Expand Up @@ -717,7 +776,7 @@ internal class PlaygroundTestDriver(
internal fun pressSelection() {
composeTestRule.waitForIdle()

selectors.paymentSelection.click()
clickPaymentSelection()
}

internal fun scrollToBottom() {
Expand All @@ -739,6 +798,14 @@ internal class PlaygroundTestDriver(
.performClick()
}

private fun clickPaymentSelection() {
selectors.formElement.waitFor()
selectors.paymentSelection.click()

Espresso.onIdle()
composeTestRule.waitForIdle()
}

/**
* Here we wait for an activity different from the playground to be in view. We
* don't specifically look for PaymentSheetActivity or PaymentOptionsActivity because
Expand Down Expand Up @@ -840,6 +907,17 @@ internal class PlaygroundTestDriver(
}
}

private fun launchCustomerSheet() {
selectors.reload.click()
Espresso.onIdle()
selectors.composeTestRule.waitForIdle()

selectors.multiStepSelect.waitForEnabled()
selectors.multiStepSelect.click()

waitForNotPlaygroundActivity()
}

private fun doAuthorization() {
selectors.apply {
val checkoutMode =
Expand Down Expand Up @@ -1039,6 +1117,7 @@ internal class PlaygroundTestDriver(

internal fun teardown() {
application?.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks)
playgroundState = null
currentActivity = null
}

Expand All @@ -1058,6 +1137,14 @@ internal class PlaygroundTestDriver(
composeTestRule.waitUntilAtLeastOneExists(hasTestTag(ADD_PAYMENT_METHOD_NODE_TAG), 5000L)
}

@OptIn(ExperimentalTestApi::class)
private fun waitForManageSavedPaymentMethods() {
composeTestRule.waitUntilAtLeastOneExists(
hasText("Manage your payment methods"),
DEFAULT_UI_TIMEOUT.inWholeMilliseconds
)
}

private companion object {
const val ADD_PAYMENT_METHOD_NODE_TAG = "${SAVED_PAYMENT_METHOD_CARD_TEST_TAG}_+ Add"
const val FINANCIAL_CONNECTIONS_ACTIVITY =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.stripe.android.test.core.ui

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.stripe.android.paymentsheet.ui.FORM_ELEMENT_TEST_TAG
import com.stripe.android.test.core.DEFAULT_UI_TIMEOUT

class FormElement(
private val composeTestRule: ComposeTestRule
) {
@OptIn(ExperimentalTestApi::class)
fun waitFor() {
composeTestRule.waitUntilExactlyOneExists(
hasTestTag(FORM_ELEMENT_TEST_TAG),
DEFAULT_UI_TIMEOUT.inWholeMilliseconds
)
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,29 @@
package com.stripe.android.test.core.ui

import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.test.platform.app.InstrumentationRegistry
import com.stripe.android.paymentsheet.ui.TEST_TAG_LIST
import com.stripe.android.test.core.DEFAULT_UI_TIMEOUT

class PaymentSelection(val composeTestRule: ComposeTestRule, val paymentMethodCode: String) {
fun click() {
val resource = InstrumentationRegistry.getInstrumentation().targetContext.resources
if (composeTestRule.onAllNodes(hasTestTag(TEST_TAG_LIST)).fetchSemanticsNodes().isNotEmpty()) {
val paymentMethodMatcher = hasTestTag(TEST_TAG_LIST + paymentMethodCode)

try {
// If we don't find the node, it means that there's only one payment method available
// and we don't show the payment method carousel as a result.
composeTestRule.waitUntil(timeoutMillis = DEFAULT_UI_TIMEOUT.inWholeMilliseconds) {
composeTestRule
.onAllNodesWithTag(TEST_TAG_LIST)
.fetchSemanticsNodes().size == 1
}
} catch (_: ComposeTimeoutException) {
return
}

val paymentMethodMatcher = hasTestTag(TEST_TAG_LIST + paymentMethodCode)
composeTestRule.onNodeWithTag(TEST_TAG_LIST, true)
.performScrollToNode(paymentMethodMatcher)
composeTestRule.waitForIdle()
composeTestRule
.onNode(paymentMethodMatcher)
.assertIsDisplayed()
.assertIsEnabled()
.performClick()
composeTestRule.onNodeWithTag(TEST_TAG_LIST, true)
.performScrollToNode(paymentMethodMatcher)
composeTestRule.waitForIdle()
composeTestRule
.onNode(paymentMethodMatcher)
.assertIsDisplayed()
.assertIsEnabled()
.performClick()

composeTestRule.waitForIdle()
composeTestRule.waitForIdle()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import com.google.common.truth.Truth.assertThat
import com.stripe.android.customersheet.ui.CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG
import com.stripe.android.customersheet.ui.CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG
import com.stripe.android.model.PaymentMethod.Type.Blik
import com.stripe.android.model.PaymentMethod.Type.CashAppPay
import com.stripe.android.paymentsheet.example.playground.RELOAD_TEST_TAG
Expand Down Expand Up @@ -59,6 +61,8 @@ internal class Selectors(
testParameters.paymentMethodCode
)

val formElement = FormElement(composeTestRule)

val mandateText = composeTestRule.onNodeWithTag(MANDATE_TEST_TAG)

val buyButton = BuyButton(
Expand All @@ -74,6 +78,16 @@ internal class Selectors(
}
)

val customerSheetSaveButton = ComposeButton(
composeTestRule,
hasTestTag(CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG)
)

val customerSheetConfirmButton = ComposeButton(
composeTestRule,
hasTestTag(CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG)
)

val externalPaymentMethodSucceedButton = ComposeButton(
composeTestRule,
hasTestTag(FawryActivity.COMPLETED_BUTTON_TEST_TAG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,16 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
val localPlaygroundSettings = playgroundSettings ?: return@setContent

val playgroundState by viewModel.state.collectAsState()
val customerAdapter by viewModel.customerAdapter.collectAsState()

val customerSheet = playgroundState?.asCustomerState()?.let { customerPlaygroundState ->
rememberCustomerSheet(
configuration = customerPlaygroundState.customerSheetConfiguration(),
customerAdapter = customerPlaygroundState.adapter,
callback = viewModel::onCustomerSheetCallback
)
customerAdapter?.let { adapter ->
rememberCustomerSheet(
configuration = customerPlaygroundState.customerSheetConfiguration(),
customerAdapter = adapter,
callback = viewModel::onCustomerSheetCallback
)
}
}

PlaygroundTheme(
Expand Down
Loading

0 comments on commit 9468357

Please sign in to comment.