1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.payments;
6 
7 import android.os.Handler;
8 import android.view.KeyEvent;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.view.inputmethod.EditorInfo;
12 import android.widget.CheckBox;
13 import android.widget.EditText;
14 import android.widget.Spinner;
15 import android.widget.TextView;
16 
17 import androidx.annotation.IntDef;
18 
19 import org.hamcrest.Matchers;
20 import org.junit.Assert;
21 import org.junit.runner.Description;
22 import org.junit.runners.model.Statement;
23 
24 import org.chromium.base.ThreadUtils;
25 import org.chromium.base.metrics.RecordHistogram;
26 import org.chromium.base.task.PostTask;
27 import org.chromium.base.test.util.CallbackHelper;
28 import org.chromium.base.test.util.Criteria;
29 import org.chromium.base.test.util.CriteriaHelper;
30 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
31 import org.chromium.base.test.util.UrlUtils;
32 import org.chromium.chrome.R;
33 import org.chromium.chrome.browser.autofill.CardUnmaskPrompt;
34 import org.chromium.chrome.browser.autofill.CardUnmaskPrompt.CardUnmaskObserverForTest;
35 import org.chromium.chrome.browser.autofill.prefeditor.EditorObserverForTest;
36 import org.chromium.chrome.browser.autofill.prefeditor.EditorTextField;
37 import org.chromium.chrome.browser.payments.ChromePaymentRequestFactory.ChromePaymentRequestDelegateImpl;
38 import org.chromium.chrome.browser.payments.ChromePaymentRequestFactory.ChromePaymentRequestDelegateImplObserverForTest;
39 import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.OptionSection;
40 import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.OptionSection.OptionRow;
41 import org.chromium.chrome.browser.payments.ui.PaymentRequestUI;
42 import org.chromium.chrome.browser.payments.ui.PaymentRequestUI.PaymentRequestObserverForTest;
43 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
44 import org.chromium.components.payments.AbortReason;
45 import org.chromium.components.payments.PayerData;
46 import org.chromium.components.payments.PaymentApp;
47 import org.chromium.components.payments.PaymentAppFactoryDelegate;
48 import org.chromium.components.payments.PaymentAppFactoryInterface;
49 import org.chromium.components.payments.PaymentAppService;
50 import org.chromium.components.payments.PaymentFeatureList;
51 import org.chromium.components.payments.PaymentRequestService;
52 import org.chromium.components.payments.PaymentRequestService.PaymentRequestServiceObserverForTest;
53 import org.chromium.content_public.browser.UiThreadTaskTraits;
54 import org.chromium.content_public.browser.WebContents;
55 import org.chromium.content_public.browser.test.util.DOMUtils;
56 import org.chromium.content_public.browser.test.util.JavaScriptUtils;
57 import org.chromium.payments.mojom.PaymentDetailsModifier;
58 import org.chromium.payments.mojom.PaymentItem;
59 import org.chromium.payments.mojom.PaymentMethodData;
60 import org.chromium.payments.mojom.PaymentOptions;
61 import org.chromium.payments.mojom.PaymentShippingOption;
62 import org.chromium.ui.modaldialog.ModalDialogProperties;
63 import org.chromium.ui.modelutil.PropertyModel;
64 
65 import java.lang.annotation.Retention;
66 import java.lang.annotation.RetentionPolicy;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Map;
71 import java.util.Set;
72 import java.util.UUID;
73 import java.util.concurrent.TimeoutException;
74 import java.util.concurrent.atomic.AtomicReference;
75 
76 /**
77  * Custom ActivityTestRule for integration test for payments.
78  */
79 public class PaymentRequestTestRule extends ChromeTabbedActivityTestRule
80         implements PaymentRequestObserverForTest, PaymentRequestServiceObserverForTest,
81                    ChromePaymentRequestDelegateImplObserverForTest, CardUnmaskObserverForTest,
82                    EditorObserverForTest {
83     @IntDef({AppPresence.NO_APPS, AppPresence.HAVE_APPS})
84     @Retention(RetentionPolicy.SOURCE)
85     public @interface AppPresence {
86         /** Flag for a factory without payment apps. */
87         public static final int NO_APPS = 0;
88 
89         /** Flag for a factory with payment apps. */
90         public static final int HAVE_APPS = 1;
91     }
92 
93     @IntDef({AppSpeed.FAST_APP, AppSpeed.SLOW_APP})
94     @Retention(RetentionPolicy.SOURCE)
95     public @interface AppSpeed {
96         /** Flag for installing a payment app that responds to its invocation fast. */
97         public static final int FAST_APP = 0;
98 
99         /** Flag for installing a payment app that responds to its invocation slowly. */
100         public static final int SLOW_APP = 1;
101     }
102 
103     @IntDef({FactorySpeed.FAST_FACTORY, FactorySpeed.SLOW_FACTORY})
104     @Retention(RetentionPolicy.SOURCE)
105     public @interface FactorySpeed {
106         /** Flag for a factory that immediately creates a payment app. */
107         public static final int FAST_FACTORY = 0;
108 
109         /** Flag for a factory that creates a payment app with a delay. */
110         public static final int SLOW_FACTORY = 1;
111     }
112 
113     /** The expiration month dropdown index for December. */
114     public static final int DECEMBER = 11;
115 
116     /** The expiration year dropdown index for the next year. */
117     public static final int NEXT_YEAR = 1;
118 
119     /**
120      * The billing address dropdown index for the first billing address. Index 0 is for the
121      * "Select" hint.
122      */
123     public static final int FIRST_BILLING_ADDRESS = 1;
124 
125     /** Command line flag to enable payment details modifiers in tests. */
126     public static final String ENABLE_WEB_PAYMENTS_MODIFIERS =
127             "enable-features=" + PaymentFeatureList.WEB_PAYMENTS_MODIFIERS;
128 
129     /** Command line flag to enable experimental web platform features in tests. */
130     public static final String ENABLE_EXPERIMENTAL_WEB_PLATFORM_FEATURES =
131             "enable-experimental-web-platform-features";
132 
133     final PaymentsCallbackHelper<PaymentRequestUI> mReadyForInput;
134     final PaymentsCallbackHelper<PaymentRequestUI> mReadyToPay;
135     final PaymentsCallbackHelper<PaymentRequestUI> mSelectionChecked;
136     final PaymentsCallbackHelper<PaymentRequestUI> mResultReady;
137     final PaymentsCallbackHelper<CardUnmaskPrompt> mReadyForUnmaskInput;
138     final PaymentsCallbackHelper<CardUnmaskPrompt> mReadyToUnmask;
139     final PaymentsCallbackHelper<CardUnmaskPrompt> mUnmaskValidationDone;
140     final PaymentsCallbackHelper<CardUnmaskPrompt> mSubmitRejected;
141     final CallbackHelper mReadyToEdit;
142     final CallbackHelper mEditorValidationError;
143     final CallbackHelper mEditorTextUpdate;
144     final CallbackHelper mDismissed;
145     final CallbackHelper mUnableToAbort;
146     final CallbackHelper mBillingAddressChangeProcessed;
147     final CallbackHelper mShowFailed;
148     final CallbackHelper mCanMakePaymentQueryResponded;
149     final CallbackHelper mHasEnrolledInstrumentQueryResponded;
150     final CallbackHelper mExpirationMonthChange;
151     final CallbackHelper mPaymentResponseReady;
152     final CallbackHelper mCompleteReplied;
153     final CallbackHelper mRendererClosedMojoConnection;
154     private ChromePaymentRequestDelegateImpl mChromePaymentRequestDelegateImpl;
155     PaymentRequestUI mUI;
156 
157     private final boolean mDelayStartActivity;
158 
159     private final AtomicReference<WebContents> mWebContentsRef;
160 
161     private final String mTestFilePath;
162 
163     private CardUnmaskPrompt mCardUnmaskPrompt;
164 
165     private final MainActivityStartCallback mCallback;
166 
167     /**
168      * Creates an instance of PaymentRequestTestRule.
169      * @param testFileName The file name of an test page in //components/test/data/payments,
170      *         'about:blank', or a data url which starts with 'data:'.
171      */
PaymentRequestTestRule(String testFileName)172     public PaymentRequestTestRule(String testFileName) {
173         this(testFileName, null);
174     }
175 
176     /**
177      * Creates an instance of PaymentRequestTestRule.
178      * @param testFileName The file name of an test page in //components/test/data/payments,
179      *         'about:blank', or a data url which starts with 'data:'.
180      * @param callback A callback that is invoked on the start of the main activity.
181      */
PaymentRequestTestRule(String testFileName, MainActivityStartCallback callback)182     public PaymentRequestTestRule(String testFileName, MainActivityStartCallback callback) {
183         this(testFileName, callback, false);
184     }
185 
186     /**
187      * Creates an instance of PaymentRequestTestRule.
188      * @param testFileName The file name of an test page in //components/test/data/payments,
189      *         'about:blank', or a data url which starts with 'data:'.
190      * @param callback A callback that is invoked on the start of the main activity.
191      * @param delayStartActivity Whether to delay the start of the main activity. When true, {@link
192      *         #startMainActivity()} needs to be called to start the main activity; otherwise, the
193      *         main activity would start automatically.
194      */
PaymentRequestTestRule( String testFileName, MainActivityStartCallback callback, boolean delayStartActivity)195     public PaymentRequestTestRule(
196             String testFileName, MainActivityStartCallback callback, boolean delayStartActivity) {
197         this(testFileName, /*pathPrefix=*/"components/test/data/payments/", callback,
198                 delayStartActivity);
199     }
200 
201     /**
202      * Creates an instance of PaymentRequestTestRule with a test page, which is specified by
203      * pathPrefix and testFileName combined into a path relative to the repository root. For
204      * example, if testFileName is "merchant.html", pathPrefix is "components/test/data/payments/",
205      * the method would look for a test page at "components/test/data/payments/merchant.html".
206      * This method is used by the //clank tests.
207      * @param testFileName The file name of the test page.
208      * @param pathPrefix The prefix path to testFileName.
209      * @param delayStartActivity Whether to delay the start of the main activity.
210      * @return The created instance.
211      */
createWithPathPrefix( String testFileName, String pathPrefix, boolean delayStartActivity)212     public static PaymentRequestTestRule createWithPathPrefix(
213             String testFileName, String pathPrefix, boolean delayStartActivity) {
214         assert pathPrefix.endsWith("/");
215         return new PaymentRequestTestRule(testFileName, pathPrefix, null, delayStartActivity);
216     }
217 
PaymentRequestTestRule(String testFilePath, String pathPrefix, MainActivityStartCallback callback, boolean delayStartActivity)218     private PaymentRequestTestRule(String testFilePath, String pathPrefix,
219             MainActivityStartCallback callback, boolean delayStartActivity) {
220         super();
221         mReadyForInput = new PaymentsCallbackHelper<>();
222         mReadyToPay = new PaymentsCallbackHelper<>();
223         mSelectionChecked = new PaymentsCallbackHelper<>();
224         mResultReady = new PaymentsCallbackHelper<>();
225         mReadyForUnmaskInput = new PaymentsCallbackHelper<>();
226         mReadyToUnmask = new PaymentsCallbackHelper<>();
227         mUnmaskValidationDone = new PaymentsCallbackHelper<>();
228         mSubmitRejected = new PaymentsCallbackHelper<>();
229         mReadyToEdit = new CallbackHelper();
230         mEditorValidationError = new CallbackHelper();
231         mEditorTextUpdate = new CallbackHelper();
232         mDismissed = new CallbackHelper();
233         mUnableToAbort = new CallbackHelper();
234         mBillingAddressChangeProcessed = new CallbackHelper();
235         mExpirationMonthChange = new CallbackHelper();
236         mPaymentResponseReady = new CallbackHelper();
237         mShowFailed = new CallbackHelper();
238         mCanMakePaymentQueryResponded = new CallbackHelper();
239         mHasEnrolledInstrumentQueryResponded = new CallbackHelper();
240         mCompleteReplied = new CallbackHelper();
241         mRendererClosedMojoConnection = new CallbackHelper();
242         mWebContentsRef = new AtomicReference<>();
243         if (testFilePath.equals("about:blank") || testFilePath.startsWith("data:")) {
244             mTestFilePath = testFilePath;
245         } else {
246             mTestFilePath = UrlUtils.getIsolatedTestFilePath(pathPrefix + testFilePath);
247         }
248         mCallback = callback;
249         mDelayStartActivity = delayStartActivity;
250     }
251 
startMainActivity()252     public void startMainActivity() {
253         startMainActivityWithURL(mTestFilePath);
254         try {
255             // TODO(crbug.com/1144303): Figure out what these tests need to wait on to not be flaky
256             // instead of sleeping.
257             Thread.sleep(2000);
258         } catch (Exception ex) {
259         }
260     }
261 
262     // public is used so as to be visible to the payment tests in //clank.
openPage()263     public void openPage() throws TimeoutException {
264         onMainActivityStarted();
265         ThreadUtils.runOnUiThreadBlocking(() -> {
266             mWebContentsRef.set(getActivity().getCurrentWebContents());
267             PaymentRequestUI.setEditorObserverForTest(PaymentRequestTestRule.this);
268             PaymentRequestUI.setPaymentRequestObserverForTest(PaymentRequestTestRule.this);
269             PaymentRequestService.setObserverForTest(PaymentRequestTestRule.this);
270             ChromePaymentRequestFactory.setChromePaymentRequestDelegateImplObserverForTest(
271                     PaymentRequestTestRule.this);
272             CardUnmaskPrompt.setObserverForTest(PaymentRequestTestRule.this);
273         });
274         assertWaitForPageScaleFactorMatch(1);
275     }
276 
getReadyForInput()277     public PaymentsCallbackHelper<PaymentRequestUI> getReadyForInput() {
278         return mReadyForInput;
279     }
getReadyToPay()280     public PaymentsCallbackHelper<PaymentRequestUI> getReadyToPay() {
281         return mReadyToPay;
282     }
getSelectionChecked()283     public PaymentsCallbackHelper<PaymentRequestUI> getSelectionChecked() {
284         return mSelectionChecked;
285     }
getResultReady()286     public PaymentsCallbackHelper<PaymentRequestUI> getResultReady() {
287         return mResultReady;
288     }
getReadyForUnmaskInput()289     public PaymentsCallbackHelper<CardUnmaskPrompt> getReadyForUnmaskInput() {
290         return mReadyForUnmaskInput;
291     }
getReadyToUnmask()292     public PaymentsCallbackHelper<CardUnmaskPrompt> getReadyToUnmask() {
293         return mReadyToUnmask;
294     }
getUnmaskValidationDone()295     public PaymentsCallbackHelper<CardUnmaskPrompt> getUnmaskValidationDone() {
296         return mUnmaskValidationDone;
297     }
getSubmitRejected()298     public PaymentsCallbackHelper<CardUnmaskPrompt> getSubmitRejected() {
299         return mSubmitRejected;
300     }
getReadyToEdit()301     public CallbackHelper getReadyToEdit() {
302         return mReadyToEdit;
303     }
getEditorValidationError()304     public CallbackHelper getEditorValidationError() {
305         return mEditorValidationError;
306     }
getEditorTextUpdate()307     public CallbackHelper getEditorTextUpdate() {
308         return mEditorTextUpdate;
309     }
getDismissed()310     public CallbackHelper getDismissed() {
311         return mDismissed;
312     }
getUnableToAbort()313     public CallbackHelper getUnableToAbort() {
314         return mUnableToAbort;
315     }
getBillingAddressChangeProcessed()316     public CallbackHelper getBillingAddressChangeProcessed() {
317         return mBillingAddressChangeProcessed;
318     }
getShowFailed()319     public CallbackHelper getShowFailed() {
320         return mShowFailed;
321     }
getCanMakePaymentQueryResponded()322     public CallbackHelper getCanMakePaymentQueryResponded() {
323         return mCanMakePaymentQueryResponded;
324     }
getHasEnrolledInstrumentQueryResponded()325     public CallbackHelper getHasEnrolledInstrumentQueryResponded() {
326         return mHasEnrolledInstrumentQueryResponded;
327     }
getExpirationMonthChange()328     public CallbackHelper getExpirationMonthChange() {
329         return mExpirationMonthChange;
330     }
getPaymentResponseReady()331     public CallbackHelper getPaymentResponseReady() {
332         return mPaymentResponseReady;
333     }
getCompleteReplied()334     public CallbackHelper getCompleteReplied() {
335         return mCompleteReplied;
336     }
getRendererClosedMojoConnection()337     public CallbackHelper getRendererClosedMojoConnection() {
338         return mRendererClosedMojoConnection;
339     }
getPaymentRequestUI()340     public PaymentRequestUI getPaymentRequestUI() {
341         return mUI;
342     }
343 
triggerUIAndWait(PaymentsCallbackHelper<PaymentRequestUI> helper)344     protected void triggerUIAndWait(PaymentsCallbackHelper<PaymentRequestUI> helper)
345             throws TimeoutException {
346         openPageAndClickNodeAndWait("buy", helper);
347         mUI = helper.getTarget();
348     }
349 
openPageAndClickNodeAndWait(String nodeId, CallbackHelper helper)350     protected void openPageAndClickNodeAndWait(String nodeId, CallbackHelper helper)
351             throws TimeoutException {
352         openPage();
353         clickNodeAndWait(nodeId, helper);
354     }
355 
openPageAndClickBuyAndWait(CallbackHelper helper)356     protected void openPageAndClickBuyAndWait(CallbackHelper helper) throws TimeoutException {
357         openPageAndClickNodeAndWait("buy", helper);
358     }
359 
openPageAndClickNode(String nodeId)360     protected void openPageAndClickNode(String nodeId) throws TimeoutException {
361         openPage();
362         clickNode(nodeId);
363     }
364 
triggerUIAndWait(String nodeId, PaymentsCallbackHelper<PaymentRequestUI> helper)365     protected void triggerUIAndWait(String nodeId, PaymentsCallbackHelper<PaymentRequestUI> helper)
366             throws TimeoutException {
367         openPageAndClickNodeAndWait(nodeId, helper);
368         mUI = helper.getTarget();
369     }
370 
reTriggerUIAndWait(String nodeId, PaymentsCallbackHelper<PaymentRequestUI> helper)371     protected void reTriggerUIAndWait(String nodeId,
372             PaymentsCallbackHelper<PaymentRequestUI> helper) throws TimeoutException {
373         clickNodeAndWait(nodeId, helper);
374         mUI = helper.getTarget();
375     }
376 
retryPaymentRequest(String validationErrors, CallbackHelper helper)377     protected void retryPaymentRequest(String validationErrors, CallbackHelper helper)
378             throws TimeoutException {
379         int callCount = helper.getCallCount();
380         JavaScriptUtils.executeJavaScriptAndWaitForResult(
381                 mWebContentsRef.get(), "retry(" + validationErrors + ");");
382         helper.waitForCallback(callCount);
383     }
384 
executeJavaScriptAndWaitForResult(String script)385     protected String executeJavaScriptAndWaitForResult(String script) throws TimeoutException {
386         return JavaScriptUtils.executeJavaScriptAndWaitForResult(mWebContentsRef.get(), script);
387     }
388 
389     // public is used so as to be visible to the payment tests in //clank.
runJavascriptWithAsyncResult(String script)390     public String runJavascriptWithAsyncResult(String script) throws TimeoutException {
391         return JavaScriptUtils.runJavascriptWithAsyncResult(mWebContentsRef.get(), script);
392     }
393 
394     /** Clicks on an HTML node. */
clickNodeAndWait(String nodeId, CallbackHelper helper)395     protected void clickNodeAndWait(String nodeId, CallbackHelper helper) throws TimeoutException {
396         int callCount = helper.getCallCount();
397         clickNode(nodeId);
398         helper.waitForCallback(callCount);
399     }
400 
401     /** Clicks on an HTML node. */
clickNode(String nodeId)402     protected void clickNode(String nodeId) throws TimeoutException {
403         DOMUtils.clickNode(mWebContentsRef.get(), nodeId);
404     }
405 
406     /** Clicks on an element in the payments UI. */
clickAndWait(int resourceId, CallbackHelper helper)407     protected void clickAndWait(int resourceId, CallbackHelper helper) throws TimeoutException {
408         int callCount = helper.getCallCount();
409         CriteriaHelper.pollUiThread(() -> {
410             boolean canClick = mUI.isAcceptingUserInput();
411             if (canClick) mUI.getDialogForTest().findViewById(resourceId).performClick();
412             Criteria.checkThat(canClick, Matchers.is(true));
413         });
414         helper.waitForCallback(callCount);
415     }
416 
417     /** Clicks on an element in the "Order summary" section of the payments UI. */
clickInOrderSummaryAndWait(CallbackHelper helper)418     protected void clickInOrderSummaryAndWait(CallbackHelper helper) throws TimeoutException {
419         int callCount = helper.getCallCount();
420         ThreadUtils.runOnUiThreadBlocking(() -> {
421             mUI.getOrderSummarySectionForTest().findViewById(R.id.payments_section).performClick();
422         });
423         helper.waitForCallback(callCount);
424     }
425 
426     /** Clicks on an element in the "Shipping address" section of the payments UI. */
clickInShippingAddressAndWait(final int resourceId, CallbackHelper helper)427     protected void clickInShippingAddressAndWait(final int resourceId, CallbackHelper helper)
428             throws TimeoutException {
429         int callCount = helper.getCallCount();
430         ThreadUtils.runOnUiThreadBlocking(() -> {
431             mUI.getShippingAddressSectionForTest().findViewById(resourceId).performClick();
432         });
433         helper.waitForCallback(callCount);
434     }
435 
436     /** Clicks on an element in the "Payment" section of the payments UI. */
clickInPaymentMethodAndWait(final int resourceId, CallbackHelper helper)437     protected void clickInPaymentMethodAndWait(final int resourceId, CallbackHelper helper)
438             throws TimeoutException {
439         int callCount = helper.getCallCount();
440         ThreadUtils.runOnUiThreadBlocking(() -> {
441             mUI.getPaymentMethodSectionForTest().findViewById(resourceId).performClick();
442         });
443         helper.waitForCallback(callCount);
444     }
445 
446     /** Clicks on an element in the "Contact Info" section of the payments UI. */
clickInContactInfoAndWait(final int resourceId, CallbackHelper helper)447     protected void clickInContactInfoAndWait(final int resourceId, CallbackHelper helper)
448             throws TimeoutException {
449         int callCount = helper.getCallCount();
450         ThreadUtils.runOnUiThreadBlocking(() -> {
451             mUI.getContactDetailsSectionForTest().findViewById(resourceId).performClick();
452         });
453         helper.waitForCallback(callCount);
454     }
455 
456     /** Clicks on an element in the editor UI for credit cards. */
clickInCardEditorAndWait(final int resourceId, CallbackHelper helper)457     protected void clickInCardEditorAndWait(final int resourceId, CallbackHelper helper)
458             throws TimeoutException {
459         int callCount = helper.getCallCount();
460         ThreadUtils.runOnUiThreadBlocking(
461                 () -> { mUI.getCardEditorDialog().findViewById(resourceId).performClick(); });
462         helper.waitForCallback(callCount);
463     }
464 
465     /** Clicks on an element in the editor UI. */
clickInEditorAndWait(final int resourceId, CallbackHelper helper)466     protected void clickInEditorAndWait(final int resourceId, CallbackHelper helper)
467             throws TimeoutException {
468         int callCount = helper.getCallCount();
469         ThreadUtils.runOnUiThreadBlocking(
470                 () -> { mUI.getEditorDialog().findViewById(resourceId).performClick(); });
471         helper.waitForCallback(callCount);
472     }
473 
clickAndroidBackButtonInEditorAndWait(CallbackHelper helper)474     protected void clickAndroidBackButtonInEditorAndWait(CallbackHelper helper)
475             throws TimeoutException {
476         int callCount = helper.getCallCount();
477         PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
478             mUI.getEditorDialog().dispatchKeyEvent(
479                     new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
480             mUI.getEditorDialog().dispatchKeyEvent(
481                     new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK));
482         });
483         helper.waitForCallback(callCount);
484     }
485 
486     /** Clicks on a button in the card unmask UI. */
clickCardUnmaskButtonAndWait(final int dialogButtonId, CallbackHelper helper)487     protected void clickCardUnmaskButtonAndWait(final int dialogButtonId, CallbackHelper helper)
488             throws TimeoutException {
489         int callCount = helper.getCallCount();
490         ThreadUtils.runOnUiThreadBlocking(() -> {
491             PropertyModel model = mCardUnmaskPrompt.getDialogForTest();
492             model.get(ModalDialogProperties.CONTROLLER).onClick(model, dialogButtonId);
493         });
494         helper.waitForCallback(callCount);
495     }
496 
497     /** Gets the retry error message. */
getRetryErrorMessage()498     protected String getRetryErrorMessage() {
499         return ThreadUtils.runOnUiThreadBlockingNoException(
500                 ()
501                         -> ((TextView) mUI.getDialogForTest().findViewById(R.id.retry_error))
502                                    .getText()
503                                    .toString());
504     }
505 
506     /** Gets the button state for the shipping summary section. */
getShippingAddressSectionButtonState()507     protected int getShippingAddressSectionButtonState() {
508         return ThreadUtils.runOnUiThreadBlockingNoException(
509                 () -> mUI.getShippingAddressSectionForTest().getEditButtonState());
510     }
511 
512     /** Gets the button state for the contact details section. */
getContactDetailsButtonState()513     protected int getContactDetailsButtonState() {
514         return ThreadUtils.runOnUiThreadBlockingNoException(
515                 () -> mUI.getContactDetailsSectionForTest().getEditButtonState());
516     }
517 
518     /** Returns the label of the payment app at the specified |index|. */
getPaymentAppLabel(final int index)519     protected String getPaymentAppLabel(final int index) {
520         return ThreadUtils.runOnUiThreadBlockingNoException(
521                 ()
522                         -> ((OptionSection) mUI.getPaymentMethodSectionForTest())
523                                    .getOptionLabelsForTest(index)
524                                    .getText()
525                                    .toString());
526     }
527 
528     /** Returns the label of the selected payment app. */
getSelectedPaymentAppLabel()529     protected String getSelectedPaymentAppLabel() {
530         return ThreadUtils.runOnUiThreadBlockingNoException(() -> {
531             OptionSection section = ((OptionSection) mUI.getPaymentMethodSectionForTest());
532             int size = section.getNumberOfOptionLabelsForTest();
533             for (int i = 0; i < size; i++) {
534                 if (section.getOptionRowAtIndex(i).isChecked()) {
535                     return section.getOptionRowAtIndex(i).getLabelText().toString();
536                 }
537             }
538             return null;
539         });
540     }
541 
542     /** Returns the total amount in order summary section. */
getOrderSummaryTotal()543     protected String getOrderSummaryTotal() {
544         return ThreadUtils.runOnUiThreadBlockingNoException(
545                 () -> mUI.getOrderSummaryTotalTextViewForTest().getText().toString());
546     }
547 
548     /** Returns the amount text corresponding to the line item at the specified |index|. */
getLineItemAmount(int index)549     protected String getLineItemAmount(int index) {
550         return ThreadUtils.runOnUiThreadBlockingNoException(
551                 ()
552                         -> mUI.getOrderSummarySectionForTest()
553                                    .getLineItemAmountForTest(index)
554                                    .getText()
555                                    .toString()
556                                    .trim());
557     }
558 
559     /** Returns the amount text corresponding to the line item at the specified |index|. */
getNumberOfLineItems()560     protected int getNumberOfLineItems() {
561         return ThreadUtils.runOnUiThreadBlockingNoException(
562                 () -> mUI.getOrderSummarySectionForTest().getNumberOfLineItemsForTest());
563     }
564 
565     /**
566      * Returns the label corresponding to the contact detail suggestion at the specified
567      * |suggestionIndex|.
568      */
getContactDetailsSuggestionLabel(final int suggestionIndex)569     protected String getContactDetailsSuggestionLabel(final int suggestionIndex) {
570         return ThreadUtils.runOnUiThreadBlockingNoException(
571                 ()
572                         -> ((OptionSection) mUI.getContactDetailsSectionForTest())
573                                    .getOptionLabelsForTest(suggestionIndex)
574                                    .getText()
575                                    .toString());
576     }
577 
578     /** Returns the number of payment apps. */
getNumberOfPaymentApps()579     protected int getNumberOfPaymentApps() {
580         return ThreadUtils.runOnUiThreadBlockingNoException(
581                 ()
582                         -> ((OptionSection) mUI.getPaymentMethodSectionForTest())
583                                    .getNumberOfOptionLabelsForTest());
584     }
585 
586     /**
587      * Returns the label corresponding to the payment method suggestion at the specified
588      * |suggestionIndex|.
589      */
getPaymentMethodSuggestionLabel(final int suggestionIndex)590     protected String getPaymentMethodSuggestionLabel(final int suggestionIndex) {
591         Assert.assertTrue(suggestionIndex < getNumberOfPaymentApps());
592 
593         return ThreadUtils.runOnUiThreadBlockingNoException(
594                 ()
595                         -> ((OptionSection) mUI.getPaymentMethodSectionForTest())
596                                    .getOptionLabelsForTest(suggestionIndex)
597                                    .getText()
598                                    .toString());
599     }
600 
601     /** Returns the number of contact detail suggestions. */
getNumberOfContactDetailSuggestions()602     protected int getNumberOfContactDetailSuggestions() {
603         return ThreadUtils.runOnUiThreadBlockingNoException(
604                 ()
605                         -> ((OptionSection) mUI.getContactDetailsSectionForTest())
606                                    .getNumberOfOptionLabelsForTest());
607     }
608 
609     /**
610      * Returns the label corresponding to the shipping address suggestion at the specified
611      * |suggestionIndex|.
612      */
getShippingAddressSuggestionLabel(final int suggestionIndex)613     protected String getShippingAddressSuggestionLabel(final int suggestionIndex) {
614         Assert.assertTrue(suggestionIndex < getNumberOfShippingAddressSuggestions());
615 
616         return ThreadUtils.runOnUiThreadBlockingNoException(
617                 ()
618                         -> mUI.getShippingAddressSectionForTest()
619                                    .getOptionLabelsForTest(suggestionIndex)
620                                    .getText()
621                                    .toString());
622     }
623 
getShippingAddressSummary()624     protected String getShippingAddressSummary() {
625         return ThreadUtils.runOnUiThreadBlockingNoException(
626                 ()
627                         -> mUI.getShippingAddressSectionForTest()
628                                    .getLeftSummaryLabelForTest()
629                                    .getText()
630                                    .toString());
631     }
632 
getShippingOptionSummary()633     protected String getShippingOptionSummary() {
634         return ThreadUtils.runOnUiThreadBlockingNoException(
635                 ()
636                         -> mUI.getShippingOptionSectionForTest()
637                                    .getLeftSummaryLabelForTest()
638                                    .getText()
639                                    .toString());
640     }
641 
getShippingOptionCostSummaryOnBottomSheet()642     protected String getShippingOptionCostSummaryOnBottomSheet() {
643         return ThreadUtils.runOnUiThreadBlockingNoException(
644                 ()
645                         -> mUI.getShippingOptionSectionForTest()
646                                    .getRightSummaryLabelForTest()
647                                    .getText()
648                                    .toString());
649     }
650 
getShippingAddressWarningLabel()651     protected String getShippingAddressWarningLabel() {
652         return ThreadUtils.runOnUiThreadBlockingNoException(() -> {
653             View view = mUI.getShippingAddressSectionForTest().findViewById(
654                     R.id.payments_warning_label);
655             return view != null && view instanceof TextView ? ((TextView) view).getText().toString()
656                                                             : null;
657         });
658     }
659 
660     protected String getShippingAddressDescriptionLabel() {
661         return ThreadUtils.runOnUiThreadBlockingNoException(() -> {
662             View view = mUI.getShippingAddressSectionForTest().findViewById(
663                     R.id.payments_description_label);
664             return view != null && view instanceof TextView ? ((TextView) view).getText().toString()
665                                                             : null;
666         });
667     }
668 
669     /** Returns the focused view in the card editor view. */
670     protected View getCardEditorFocusedView() {
671         return mUI.getCardEditorDialog().getCurrentFocus();
672     }
673 
674     /**
675      * Clicks on the label corresponding to the shipping address suggestion at the specified
676      * |suggestionIndex|.
677      */
678     protected void clickOnShippingAddressSuggestionOptionAndWait(
679             final int suggestionIndex, CallbackHelper helper) throws TimeoutException {
680         Assert.assertTrue(suggestionIndex < getNumberOfShippingAddressSuggestions());
681 
682         int callCount = helper.getCallCount();
683         ThreadUtils.runOnUiThreadBlocking(() -> {
684             ((OptionSection) mUI.getShippingAddressSectionForTest())
685                     .getOptionLabelsForTest(suggestionIndex)
686                     .performClick();
687         });
688         helper.waitForCallback(callCount);
689     }
690 
691     /**
692      * Clicks on the label corresponding to the payment method suggestion at the specified
693      * |suggestionIndex|.
694      */
695     protected void clickOnPaymentMethodSuggestionOptionAndWait(
696             final int suggestionIndex, CallbackHelper helper) throws TimeoutException {
697         Assert.assertTrue(suggestionIndex < getNumberOfPaymentApps());
698 
699         int callCount = helper.getCallCount();
700         ThreadUtils.runOnUiThreadBlocking(() -> {
701             ((OptionSection) mUI.getPaymentMethodSectionForTest())
702                     .getOptionLabelsForTest(suggestionIndex)
703                     .performClick();
704         });
705         helper.waitForCallback(callCount);
706     }
707 
708     /**
709      * Clicks on the label corresponding to the contact info suggestion at the specified
710      * |suggestionIndex|.
711      */
712     protected void clickOnContactInfoSuggestionOptionAndWait(
713             final int suggestionIndex, CallbackHelper helper) throws TimeoutException {
714         Assert.assertTrue(suggestionIndex < getNumberOfContactDetailSuggestions());
715 
716         int callCount = helper.getCallCount();
717         ThreadUtils.runOnUiThreadBlocking(() -> {
718             ((OptionSection) mUI.getContactDetailsSectionForTest())
719                     .getOptionLabelsForTest(suggestionIndex)
720                     .performClick();
721         });
722         helper.waitForCallback(callCount);
723     }
724 
725     /**
726      * Clicks on the edit icon corresponding to the payment method suggestion at the specified
727      * |suggestionIndex|.
728      */
729     protected void clickOnPaymentMethodSuggestionEditIconAndWait(
730             final int suggestionIndex, CallbackHelper helper) throws TimeoutException {
731         Assert.assertTrue(suggestionIndex < getNumberOfPaymentApps());
732 
733         int callCount = helper.getCallCount();
734         ThreadUtils.runOnUiThreadBlocking(() -> {
735             ((OptionSection) mUI.getPaymentMethodSectionForTest())
736                     .getOptionRowAtIndex(suggestionIndex)
737                     .getEditIconForTest()
738                     .performClick();
739         });
740         helper.waitForCallback(callCount);
741     }
742 
743     /**
744      * Returns the summary text of the shipping address section.
745      */
746     protected String getShippingAddressSummaryLabel() {
747         return getShippingAddressSummary();
748     }
749 
750     /**
751      * Returns the summary text of the shipping option section.
752      */
753     protected String getShippingOptionSummaryLabel() {
754         return getShippingOptionSummary();
755     }
756 
757     /**
758      * Returns the cost text of the shipping option section on the bottom sheet.
759      */
760     protected String getShippingOptionCostSummaryLabelOnBottomSheet() {
761         return getShippingOptionCostSummaryOnBottomSheet();
762     }
763 
764     /**
765      * Returns the number of shipping address suggestions.
766      */
767     protected int getNumberOfShippingAddressSuggestions() {
768         return ThreadUtils.runOnUiThreadBlockingNoException(
769                 ()
770                         -> ((OptionSection) mUI.getShippingAddressSectionForTest())
771                                    .getNumberOfOptionLabelsForTest());
772     }
773 
774     /** Returns the {@link OptionRow} at the given index for the shipping address section. */
775     protected OptionRow getShippingAddressOptionRowAtIndex(final int index) {
776         return ThreadUtils.runOnUiThreadBlockingNoException(
777                 ()
778                         -> ((OptionSection) mUI.getShippingAddressSectionForTest())
779                                    .getOptionRowAtIndex(index));
780     }
781 
782     /** Returns the selected spinner value in the editor UI for credit cards. */
783     protected String getSpinnerSelectionTextInCardEditor(final int dropdownIndex) {
784         return ThreadUtils.runOnUiThreadBlockingNoException(
785                 ()
786                         -> mUI.getCardEditorDialog()
787                                    .getDropdownFieldsForTest()
788                                    .get(dropdownIndex)
789                                    .getSelectedItem()
790                                    .toString());
791     }
792 
793     /** Returns the spinner value at the specified position in the editor UI for credit cards. */
794     protected String getSpinnerTextAtPositionInCardEditor(
795             final int dropdownIndex, final int itemPosition) {
796         return ThreadUtils.runOnUiThreadBlockingNoException(
797                 ()
798                         -> mUI.getCardEditorDialog()
799                                    .getDropdownFieldsForTest()
800                                    .get(dropdownIndex)
801                                    .getItemAtPosition(itemPosition)
802                                    .toString());
803     }
804 
805     /** Returns the number of items offered by the spinner in the editor UI for credit cards. */
806     protected int getSpinnerItemCountInCardEditor(final int dropdownIndex) {
807         return ThreadUtils.runOnUiThreadBlockingNoException(
808                 ()
809                         -> mUI.getCardEditorDialog()
810                                    .getDropdownFieldsForTest()
811                                    .get(dropdownIndex)
812                                    .getCount());
813     }
814 
815     /** Returns the error message visible to the user in the credit card unmask prompt. */
816     protected String getUnmaskPromptErrorMessage() {
817         return mCardUnmaskPrompt.getErrorMessage();
818     }
819 
820     /** Selects the spinner value in the editor UI for credit cards. */
821     protected void setSpinnerSelectionsInCardEditorAndWait(
822             final int[] selections, CallbackHelper helper) throws TimeoutException {
823         int callCount = helper.getCallCount();
824         ThreadUtils.runOnUiThreadBlocking(() -> {
825             List<Spinner> fields = mUI.getCardEditorDialog().getDropdownFieldsForTest();
826             for (int i = 0; i < selections.length && i < fields.size(); i++) {
827                 fields.get(i).setSelection(selections[i]);
828             }
829         });
830         helper.waitForCallback(callCount);
831     }
832 
833     /** Selects the spinner value in the editor UI. */
834     protected void setSpinnerSelectionInEditorAndWait(final int selection, CallbackHelper helper)
835             throws TimeoutException {
836         int callCount = helper.getCallCount();
837         ThreadUtils.runOnUiThreadBlocking(
838                 ()
839                         -> ((Spinner) mUI.getEditorDialog().findViewById(R.id.spinner))
840                                    .setSelection(selection));
841         helper.waitForCallback(callCount);
842     }
843 
844     /** Directly sets the text in the editor UI for credit cards. */
845     protected void setTextInCardEditorAndWait(final String[] values, CallbackHelper helper)
846             throws TimeoutException {
847         int callCount = helper.getCallCount();
848         ThreadUtils.runOnUiThreadBlocking(() -> {
849             ViewGroup contents = (ViewGroup) mUI.getCardEditorDialog().findViewById(R.id.contents);
850             Assert.assertNotNull(contents);
851             for (int i = 0, j = 0; i < contents.getChildCount() && j < values.length; i++) {
852                 View view = contents.getChildAt(i);
853                 if (view instanceof EditorTextField) {
854                     ((EditorTextField) view).getEditText().setText(values[j++]);
855                 }
856             }
857         });
858         helper.waitForCallback(callCount);
859     }
860 
861     /** Directly sets the text in the editor UI. */
862     protected void setTextInEditorAndWait(final String[] values, CallbackHelper helper)
863             throws TimeoutException {
864         int callCount = helper.getCallCount();
865         ThreadUtils.runOnUiThreadBlocking(() -> {
866             List<EditText> fields = mUI.getEditorDialog().getEditableTextFieldsForTest();
867             for (int i = 0; i < values.length; i++) {
868                 fields.get(i).requestFocus();
869                 fields.get(i).setText(values[i]);
870             }
871         });
872         helper.waitForCallback(callCount);
873     }
874 
875     /** Directly sets the checkbox selection in the editor UI for credit cards. */
876     protected void selectCheckboxAndWait(final int resourceId, final boolean isChecked,
877             CallbackHelper helper) throws TimeoutException {
878         int callCount = helper.getCallCount();
879         ThreadUtils.runOnUiThreadBlocking(
880                 ()
881                         -> ((CheckBox) mUI.getCardEditorDialog().findViewById(resourceId))
882                                    .setChecked(isChecked));
883         helper.waitForCallback(callCount);
884     }
885 
886     /** Directly sets the text in the card unmask UI. */
887     protected void setTextInCardUnmaskDialogAndWait(final int resourceId, final String input,
888             CallbackHelper helper) throws TimeoutException {
889         int callCount = helper.getCallCount();
890         ThreadUtils.runOnUiThreadBlocking(() -> {
891             EditText editText = mCardUnmaskPrompt.getDialogForTest()
892                                         .get(ModalDialogProperties.CUSTOM_VIEW)
893                                         .findViewById(resourceId);
894             editText.setText(input);
895             editText.getOnFocusChangeListener().onFocusChange(null, false);
896         });
897         helper.waitForCallback(callCount);
898     }
899 
900     /** Directly sets the text in the expired card unmask UI. */
901     protected void setTextInExpiredCardUnmaskDialogAndWait(final int[] resourceIds,
902             final String[] values, CallbackHelper helper) throws TimeoutException {
903         assert resourceIds.length == values.length;
904         int callCount = helper.getCallCount();
905         ThreadUtils.runOnUiThreadBlocking(() -> {
906             for (int i = 0; i < resourceIds.length; ++i) {
907                 EditText editText = mCardUnmaskPrompt.getDialogForTest()
908                                             .get(ModalDialogProperties.CUSTOM_VIEW)
909                                             .findViewById(resourceIds[i]);
910                 editText.setText(values[i]);
911                 editText.getOnFocusChangeListener().onFocusChange(null, false);
912             }
913         });
914         helper.waitForCallback(callCount);
915     }
916 
917     /** Focues a view and hits the "submit" button on the software keyboard. */
918     /* package */ void hitSoftwareKeyboardSubmitButtonAndWait(
919             final int resourceId, CallbackHelper helper) throws TimeoutException {
920         int callCount = helper.getCallCount();
921         ThreadUtils.runOnUiThreadBlocking(() -> {
922             EditText editText = mCardUnmaskPrompt.getDialogForTest()
923                                         .get(ModalDialogProperties.CUSTOM_VIEW)
924                                         .findViewById(resourceId);
925             editText.requestFocus();
926             editText.onEditorAction(EditorInfo.IME_ACTION_DONE);
927         });
928         helper.waitForCallback(callCount);
929     }
930 
931     /** Verifies the contents of the test webpage. */
932     protected void expectResultContains(final String[] contents) {
933         CriteriaHelper.pollInstrumentationThread(() -> {
934             try {
935                 String result = DOMUtils.getNodeContents(mWebContentsRef.get(), "result");
936                 Criteria.checkThat(
937                         "Cannot find 'result' node on test page", result, Matchers.notNullValue());
938                 for (int i = 0; i < contents.length; i++) {
939                     Criteria.checkThat(
940                             "Result '" + result + "' should contain '" + contents[i] + "'", result,
941                             Matchers.containsString(contents[i]));
942                 }
943             } catch (TimeoutException e2) {
944                 throw new CriteriaNotSatisfiedException(e2);
945             }
946         });
947     }
948 
949     /** Will fail if the OptionRow at |index| is not selected in Contact Details.*/
950     protected void expectContactDetailsRowIsSelected(final int index) {
951         CriteriaHelper.pollInstrumentationThread(() -> {
952             boolean isSelected = ((OptionSection) mUI.getContactDetailsSectionForTest())
953                                          .getOptionRowAtIndex(index)
954                                          .isChecked();
955             Criteria.checkThat("Contact Details row at " + index + " was not selected.", isSelected,
956                     Matchers.is(true));
957         });
958     }
959 
960     /** Will fail if the OptionRow at |index| is not selected in Shipping Address section.*/
961     protected void expectShippingAddressRowIsSelected(final int index) {
962         CriteriaHelper.pollInstrumentationThread(() -> {
963             boolean isSelected = ((OptionSection) mUI.getShippingAddressSectionForTest())
964                                          .getOptionRowAtIndex(index)
965                                          .isChecked();
966             Criteria.checkThat("Shipping Address row at " + index + " was not selected.",
967                     isSelected, Matchers.is(true));
968         });
969     }
970 
971     /** Will fail if the OptionRow at |index| is not selected in PaymentMethod section.*/
972     protected void expectPaymentMethodRowIsSelected(final int index) {
973         CriteriaHelper.pollInstrumentationThread(() -> {
974             boolean isSelected = ((OptionSection) mUI.getPaymentMethodSectionForTest())
975                                          .getOptionRowAtIndex(index)
976                                          .isChecked();
977             Criteria.checkThat("Payment Method row at " + index + " was not selected.", isSelected,
978                     Matchers.is(true));
979         });
980     }
981 
982     /**
983      * Asserts that only the specified reason for abort is logged.
984      *
985      * @param abortReason The only bucket in the abort histogram that should have a record.
986      */
987     protected void assertOnlySpecificAbortMetricLogged(int abortReason) {
988         for (int i = 0; i < AbortReason.MAX; ++i) {
989             Assert.assertEquals(
990                     String.format(Locale.getDefault(), "Found %d instead of %d", i, abortReason),
991                     (i == abortReason ? 1 : 0),
992                     RecordHistogram.getHistogramValueCountForTesting(
993                             "PaymentRequest.CheckoutFunnel.Aborted", i));
994         }
995     }
996 
997     /* package */ View getPaymentRequestView() {
998         return ThreadUtils.runOnUiThreadBlockingNoException(
999                 () -> mUI.getDialogForTest().findViewById(R.id.payment_request));
1000     }
1001 
1002     /* package */ View getCardUnmaskView() throws Throwable {
1003         return ThreadUtils.runOnUiThreadBlocking(
1004                 ()
1005                         -> mCardUnmaskPrompt.getDialogForTest()
1006                                    .get(ModalDialogProperties.CUSTOM_VIEW)
1007                                    .findViewById(R.id.autofill_card_unmask_prompt));
1008     }
1009 
1010     /* package */ View getEditorDialogView() throws Throwable {
1011         return ThreadUtils.runOnUiThreadBlocking(
1012                 () -> mUI.getEditorDialog().findViewById(R.id.editor_container));
1013     }
1014 
1015     /** Allows to skip UI into paymenthandler for"basic-card". */
1016     protected void enableSkipUIForBasicCard() {
1017         ThreadUtils.runOnUiThreadBlocking(
1018                 () -> mChromePaymentRequestDelegateImpl.setSkipUiForBasicCard());
1019     }
1020 
1021     @Override
1022     public void onPaymentRequestReadyForInput(PaymentRequestUI ui) {
1023         ThreadUtils.assertOnUiThread();
1024         // This happens when the payment request is created by a direct js function call rather than
1025         // calling the js function via triggerUIAndWait() which sets the mUI.
1026         if (mUI == null) mUI = ui;
1027         mReadyForInput.notifyCalled(ui);
1028     }
1029 
1030     @Override
1031     public void onEditorReadyToEdit() {
1032         ThreadUtils.assertOnUiThread();
1033         mReadyToEdit.notifyCalled();
1034     }
1035 
1036     @Override
1037     public void onEditorValidationError() {
1038         ThreadUtils.assertOnUiThread();
1039         mEditorValidationError.notifyCalled();
1040     }
1041 
1042     @Override
1043     public void onEditorTextUpdate() {
1044         ThreadUtils.assertOnUiThread();
1045         mEditorTextUpdate.notifyCalled();
1046     }
1047 
1048     @Override
1049     public void onPaymentRequestReadyToPay(PaymentRequestUI ui) {
1050         ThreadUtils.assertOnUiThread();
1051         // This happens when the payment request is created by a direct js function call rather than
1052         // calling the js function via triggerUIAndWait() which sets the mUI.
1053         if (mUI == null) mUI = ui;
1054         mReadyToPay.notifyCalled(ui);
1055     }
1056 
1057     @Override
1058     public void onPaymentRequestSelectionChecked(PaymentRequestUI ui) {
1059         ThreadUtils.assertOnUiThread();
1060         mSelectionChecked.notifyCalled(ui);
1061     }
1062 
1063     @Override
1064     public void onPaymentRequestResultReady(PaymentRequestUI ui) {
1065         ThreadUtils.assertOnUiThread();
1066         mResultReady.notifyCalled(ui);
1067     }
1068 
1069     @Override
1070     public void onEditorDismiss() {
1071         ThreadUtils.assertOnUiThread();
1072         mDismissed.notifyCalled();
1073     }
1074 
1075     @Override
1076     public void onCreatedChromePaymentRequestDelegateImpl(
1077             ChromePaymentRequestDelegateImpl delegateImpl) {
1078         ThreadUtils.assertOnUiThread();
1079         mChromePaymentRequestDelegateImpl = delegateImpl;
1080     }
1081 
1082     @Override
1083     public void onPaymentRequestServiceUnableToAbort() {
1084         ThreadUtils.assertOnUiThread();
1085         mUnableToAbort.notifyCalled();
1086     }
1087 
1088     @Override
1089     public void onPaymentRequestServiceBillingAddressChangeProcessed() {
1090         ThreadUtils.assertOnUiThread();
1091         mBillingAddressChangeProcessed.notifyCalled();
1092     }
1093 
1094     @Override
1095     public void onPaymentRequestServiceExpirationMonthChange() {
1096         ThreadUtils.assertOnUiThread();
1097         mExpirationMonthChange.notifyCalled();
1098     }
1099 
1100     @Override
1101     public void onPaymentRequestServiceShowFailed() {
1102         ThreadUtils.assertOnUiThread();
1103         mShowFailed.notifyCalled();
1104     }
1105 
1106     @Override
1107     public void onPaymentRequestServiceCanMakePaymentQueryResponded() {
1108         ThreadUtils.assertOnUiThread();
1109         mCanMakePaymentQueryResponded.notifyCalled();
1110     }
1111 
1112     @Override
1113     public void onPaymentRequestServiceHasEnrolledInstrumentQueryResponded() {
1114         ThreadUtils.assertOnUiThread();
1115         mHasEnrolledInstrumentQueryResponded.notifyCalled();
1116     }
1117 
1118     @Override
1119     public void onCardUnmaskPromptReadyForInput(CardUnmaskPrompt prompt) {
1120         ThreadUtils.assertOnUiThread();
1121         mReadyForUnmaskInput.notifyCalled(prompt);
1122         mCardUnmaskPrompt = prompt;
1123     }
1124 
1125     @Override
1126     public void onCardUnmaskPromptReadyToUnmask(CardUnmaskPrompt prompt) {
1127         ThreadUtils.assertOnUiThread();
1128         mReadyToUnmask.notifyCalled(prompt);
1129     }
1130 
1131     @Override
1132     public void onCardUnmaskPromptValidationDone(CardUnmaskPrompt prompt) {
1133         ThreadUtils.assertOnUiThread();
1134         mUnmaskValidationDone.notifyCalled(prompt);
1135     }
1136 
1137     @Override
1138     public void onCardUnmaskPromptSubmitRejected(CardUnmaskPrompt prompt) {
1139         ThreadUtils.assertOnUiThread();
1140         mSubmitRejected.notifyCalled(prompt);
1141     }
1142 
1143     @Override
1144     public void onPaymentResponseReady() {
1145         ThreadUtils.assertOnUiThread();
1146         mPaymentResponseReady.notifyCalled();
1147     }
1148 
1149     @Override
1150     public void onCompleteReplied() {
1151         ThreadUtils.assertOnUiThread();
1152         mCompleteReplied.notifyCalled();
1153     }
1154 
1155     @Override
1156     public void onRendererClosedMojoConnection() {
1157         ThreadUtils.assertOnUiThread();
1158         mRendererClosedMojoConnection.notifyCalled();
1159     }
1160 
1161     /**
1162      * Listens for UI notifications.
1163      */
1164     static class PaymentsCallbackHelper<T> extends CallbackHelper {
1165         private T mTarget;
1166 
1167         /**
1168          * Returns the UI that is ready for input.
1169          *
1170          * @return The UI that is ready for input.
1171          */
1172         public T getTarget() {
1173             return mTarget;
1174         }
1175 
1176         /**
1177          * Called when the UI is ready for input.
1178          *
1179          * @param target The UI that is ready for input.
1180          */
1181         public void notifyCalled(T target) {
1182             ThreadUtils.assertOnUiThread();
1183             mTarget = target;
1184             notifyCalled();
1185         }
1186     }
1187 
1188     /**
1189      * Adds a payment app factory for testing.
1190      *
1191      * @param appPresence  Whether the factory has apps.
1192      * @param factorySpeed How quick the factory creates apps.
1193      * @return The test factory. Can be ignored.
1194      */
1195     /* package */ TestFactory addPaymentAppFactory(
1196             @AppPresence int appPresence, @FactorySpeed int factorySpeed) {
1197         return addPaymentAppFactory("https://bobpay.com", appPresence, factorySpeed);
1198     }
1199 
1200     /**
1201      * Adds a payment app factory for testing.
1202      *
1203      * @param methodName   The name of the payment method used in the payment app.
1204      * @param appPresence  Whether the factory has apps.
1205      * @param factorySpeed How quick the factory creates apps.
1206      * @return The test factory. Can be ignored.
1207      */
1208     /* package */ TestFactory addPaymentAppFactory(
1209             String methodName, @AppPresence int appPresence, @FactorySpeed int factorySpeed) {
1210         return addPaymentAppFactory(methodName, appPresence, factorySpeed, AppSpeed.FAST_APP);
1211     }
1212 
1213     /**
1214      * Adds a payment app factory for testing.
1215      *
1216      * @param methodName   The name of the payment method used in the payment app.
1217      * @param appPresence  Whether the factory has apps.
1218      * @param factorySpeed How quick the factory creates apps.
1219      * @param appSpeed     How quick the app responds to "invoke".
1220      * @return The test factory. Can be ignored.
1221      */
1222     /* package */ TestFactory addPaymentAppFactory(String appMethodName, int appPresence,
1223             @FactorySpeed int factorySpeed, @AppSpeed int appSpeed) {
1224         TestFactory factory = new TestFactory(appMethodName, appPresence, factorySpeed, appSpeed);
1225         PaymentAppService.getInstance().addFactory(factory);
1226         return factory;
1227     }
1228 
1229     /** A payment app factory implementation for test. */
1230     /* package */ static final class TestFactory implements PaymentAppFactoryInterface {
1231         private final String mAppMethodName;
1232         private final @AppPresence int mAppPresence;
1233         private final @FactorySpeed int mFactorySpeed;
1234         private final @AppSpeed int mAppSpeed;
1235         private PaymentAppFactoryDelegate mDelegate;
1236 
1237         private TestFactory(String appMethodName, @AppPresence int appPresence,
1238                 @FactorySpeed int factorySpeed, @AppSpeed int appSpeed) {
1239             mAppMethodName = appMethodName;
1240             mAppPresence = appPresence;
1241             mFactorySpeed = factorySpeed;
1242             mAppSpeed = appSpeed;
1243         }
1244 
1245         @Override
1246         public void create(PaymentAppFactoryDelegate delegate) {
1247             Runnable createApp = () -> {
1248                 boolean canMakePayment =
1249                         delegate.getParams().getMethodData().containsKey(mAppMethodName);
1250                 delegate.onCanMakePaymentCalculated(canMakePayment);
1251                 if (canMakePayment && mAppPresence == AppPresence.HAVE_APPS) {
1252                     delegate.onPaymentAppCreated(new TestPay(mAppMethodName, mAppSpeed));
1253                 }
1254                 delegate.onDoneCreatingPaymentApps(this);
1255             };
1256             if (mFactorySpeed == FactorySpeed.FAST_FACTORY) {
1257                 createApp.run();
1258             } else {
1259                 new Handler().postDelayed(createApp, 100);
1260             }
1261             mDelegate = delegate;
1262         }
1263 
1264         /* package */ PaymentAppFactoryDelegate getDelegateForTest() {
1265             return mDelegate;
1266         }
1267     }
1268 
1269     /** A payment app implementation for test. */
1270     /* package */ static final class TestPay extends PaymentApp {
1271         private final String mDefaultMethodName;
1272         private final @AppSpeed int mAppSpeed;
1273 
1274         TestPay(String defaultMethodName, @AppSpeed int appSpeed) {
1275             super(/*id=*/UUID.randomUUID().toString(), /*label=*/defaultMethodName,
1276                     /*sublabel=*/null, /*icon=*/null);
1277             mDefaultMethodName = defaultMethodName;
1278             mAppSpeed = appSpeed;
1279         }
1280 
1281         @Override
1282         public Set<String> getInstrumentMethodNames() {
1283             Set<String> result = new HashSet<>();
1284             result.add(mDefaultMethodName);
1285             return result;
1286         }
1287 
1288         @Override
1289         public void invokePaymentApp(String id, String merchantName, String origin,
1290                 String iframeOrigin, byte[][] certificateChain,
1291                 Map<String, PaymentMethodData> methodData, PaymentItem total,
1292                 List<PaymentItem> displayItems, Map<String, PaymentDetailsModifier> modifiers,
1293                 PaymentOptions paymentOptions, List<PaymentShippingOption> shippingOptions,
1294                 InstrumentDetailsCallback detailsCallback) {
1295             Runnable respond = () -> {
1296                 detailsCallback.onInstrumentDetailsReady(mDefaultMethodName,
1297                         "{\"transaction\": 1337, \"total\": \"" + total.amount.value + "\"}",
1298                         new PayerData());
1299             };
1300             if (mAppSpeed == AppSpeed.FAST_APP) {
1301                 respond.run();
1302             } else {
1303                 new Handler().postDelayed(respond, 100);
1304             }
1305         }
1306 
1307         @Override
1308         public void dismissInstrument() {}
1309     }
1310 
1311     public void onMainActivityStarted() throws TimeoutException {
1312         if (mCallback != null) {
1313             mCallback.onMainActivityStarted();
1314         }
1315     }
1316 
1317     @Override
1318     public Statement apply(final Statement base, Description description) {
1319         return super.apply(new Statement() {
1320             @Override
1321             public void evaluate() throws Throwable {
1322                 if (!mDelayStartActivity) startMainActivity();
1323                 base.evaluate();
1324             }
1325         }, description);
1326     }
1327 
1328     /** The interface for being notified of the main activity startup. */
1329     public interface MainActivityStartCallback {
1330         /** Called when the main activity has started up. */
1331         void onMainActivityStarted() throws TimeoutException;
1332     }
1333 }
1334