chromium/chrome/android/features/keyboard_accessory/javatests/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingTestHelper.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.keyboard_accessory;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;

import static org.hamcrest.core.AllOf.allOf;

import static org.chromium.autofill.mojom.FocusedFieldType.FILLABLE_NON_SEARCH_FIELD;
import static org.chromium.base.test.util.CriteriaHelper.pollInstrumentationThread;
import static org.chromium.base.test.util.CriteriaHelper.pollUiThread;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryTestHelper.accessoryStartedHiding;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryTestHelper.accessoryStartedShowing;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryTestHelper.accessoryViewFullyHidden;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryTestHelper.accessoryViewFullyShown;
import static org.chromium.ui.base.LocalizationUtils.setRtlForTesting;
import static org.chromium.ui.test.util.ViewUtils.VIEW_GONE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_INVISIBLE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_NULL;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;
import static org.chromium.ui.test.util.ViewUtils.waitForView;

import android.app.Activity;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.StringRes;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.BoundedMatcher;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Assert;

import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.ChromeKeyboardVisibilityDelegate;
import org.chromium.chrome.browser.ChromeWindow;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.autofill.AutofillTestHelper;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupView;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData.AccessorySheetData;
import org.chromium.chrome.browser.keyboard_accessory.data.PropertyProvider;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.AddressAccessorySheetCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.CreditCardAccessorySheetCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.sheet_tabs.PasswordAccessorySheetCoordinator;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.autofill.AutofillProfile;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.TestInputMethodManagerWrapper;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import org.chromium.ui.DropdownPopupWindowInterface;
import org.chromium.ui.test.util.ViewUtils;
import org.chromium.ui.widget.ChromeImageButton;

import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Helpers in this class simplify interactions with the Keyboard Accessory and the sheet below it.
 */
public class ManualFillingTestHelper {
    private static final String PASSWORD_NODE_ID = "password_field";
    private static final String USERNAME_NODE_ID = "username_field";
    private static final String SUBMIT_NODE_ID = "input_submit_button";
    private static final String NO_COMPLETION_FIELD_ID = "field_without_completion";

    private final ChromeTabbedActivityTestRule mActivityTestRule;
    private final AtomicReference<WebContents> mWebContentsRef = new AtomicReference<>();
    private TestInputMethodManagerWrapper mInputMethodManagerWrapper;

    private EmbeddedTestServer mEmbeddedTestServer;

    private RecyclerView mKeyboardAccessoryBarItems;

    public FakeKeyboard getKeyboard() {
        return (FakeKeyboard) mActivityTestRule.getKeyboardDelegate();
    }

    public ManualFillingTestHelper(ChromeTabbedActivityTestRule activityTestRule) {
        mActivityTestRule = activityTestRule;
    }

    public EmbeddedTestServer getOrCreateTestServer() {
        if (mEmbeddedTestServer == null) {
            mEmbeddedTestServer =
                    EmbeddedTestServer.createAndStartHTTPSServer(
                            InstrumentationRegistry.getInstrumentation().getContext(),
                            ServerCertificate.CERT_OK);
        }
        return mEmbeddedTestServer;
    }

    public void loadTestPage(boolean isRtl) {
        loadTestPage("/chrome/test/data/password/password_form.html", isRtl);
    }

    public void loadTestPage(String url, boolean isRtl) {
        loadTestPage(url, isRtl, false, FakeKeyboard::new);
    }

    public void loadTestPage(
            String url,
            boolean isRtl,
            boolean waitForNode,
            ChromeWindow.KeyboardVisibilityDelegateFactory keyboardDelegate) {
        getOrCreateTestServer();
        ChromeWindow.setKeyboardVisibilityDelegateFactory(keyboardDelegate);
        if (mActivityTestRule.getActivity() == null) {
            mActivityTestRule.startMainActivityWithURL(mEmbeddedTestServer.getURL(url));
        } else {
            mActivityTestRule.loadUrl(mEmbeddedTestServer.getURL(url));
        }
        setRtlForTesting(isRtl);
        updateWebContentsDependentState();
        cacheCredentials("[email protected]", "S3cr3t"); // Providing suggestions ensures visibility.
        if (waitForNode) DOMUtils.waitForNonZeroNodeBounds(mWebContentsRef.get(), PASSWORD_NODE_ID);
    }

    public void loadUrl(String url) {
        mActivityTestRule.loadUrl(mActivityTestRule.getTestServer().getURL(url));
        mWebContentsRef.set(mActivityTestRule.getWebContents());
    }

    public void updateWebContentsDependentState() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChromeActivity activity = mActivityTestRule.getActivity();
                    mWebContentsRef.set(activity.getActivityTab().getWebContents());
                    // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard
                    // is never brought up.
                    final ImeAdapter imeAdapter = ImeAdapter.fromWebContents(mWebContentsRef.get());
                    mInputMethodManagerWrapper = TestInputMethodManagerWrapper.create(imeAdapter);
                    imeAdapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper);
                });
    }

    public void clear() {
        ChromeWindow.resetKeyboardVisibilityDelegateFactory();
    }

    // --------------------------------------
    // Helpers interacting with the web page.
    // --------------------------------------

    public WebContents getWebContents() {
        return mWebContentsRef.get();
    }

    ManualFillingCoordinator getManualFillingCoordinator() {
        return (ManualFillingCoordinator)
                mActivityTestRule.getActivity().getManualFillingComponent();
    }

    public RecyclerView getAccessoryBarView() {
        final ViewGroup keyboardAccessory =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                mActivityTestRule
                                        .getActivity()
                                        .findViewById(R.id.keyboard_accessory));
        assert keyboardAccessory != null;
        return keyboardAccessory.findViewById(R.id.bar_items_view);
    }

    public void focusPasswordField() throws TimeoutException {
        focusPasswordField(true);
    }

    public void focusPasswordField(boolean useFakeKeyboard) throws TimeoutException {
        DOMUtils.focusNode(mActivityTestRule.getWebContents(), PASSWORD_NODE_ID);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivityTestRule.getWebContents().scrollFocusedEditableNodeIntoView();
                });

        ChromeKeyboardVisibilityDelegate keyboard;
        if (useFakeKeyboard) {
            keyboard = getKeyboard();
        } else {
            keyboard = (ChromeKeyboardVisibilityDelegate) mActivityTestRule.getKeyboardDelegate();
        }
        keyboard.showKeyboard(mActivityTestRule.getActivity().getCurrentFocus());
    }

    public String getPasswordText() throws TimeoutException {
        return DOMUtils.getNodeValue(mWebContentsRef.get(), PASSWORD_NODE_ID);
    }

    public String getFieldText(String nodeId) throws TimeoutException {
        return DOMUtils.getNodeValue(mWebContentsRef.get(), nodeId);
    }

    public void clickEmailField(boolean forceAccessory) throws TimeoutException {
        // TODO(fhorschig): This should be |focusNode|. Change with autofill popup deprecation.
        DOMUtils.clickNode(mWebContentsRef.get(), USERNAME_NODE_ID);
        if (forceAccessory) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        getManualFillingCoordinator().getMediatorForTesting().show(true);
                    });
        }
        getKeyboard().showKeyboard(mActivityTestRule.getActivity().getCurrentFocus());
    }

    public void clickFieldWithoutCompletion() throws TimeoutException {
        DOMUtils.waitForNonZeroNodeBounds(mWebContentsRef.get(), PASSWORD_NODE_ID);
        DOMUtils.focusNode(mWebContentsRef.get(), NO_COMPLETION_FIELD_ID);
        DOMUtils.clickNode(mWebContentsRef.get(), NO_COMPLETION_FIELD_ID);
    }

    public void clickNodeAndShowKeyboard(String node, long focusedFieldId) throws TimeoutException {
        clickNodeAndShowKeyboard(node, focusedFieldId, FILLABLE_NON_SEARCH_FIELD);
    }

    public void clickNodeAndShowKeyboard(String node, long focusedFieldId, int focusedFieldType)
            throws TimeoutException {
        clickNode(node, focusedFieldId, focusedFieldType);
        getKeyboard().showKeyboard(mActivityTestRule.getActivity().getCurrentFocus());
    }

    public void clickNode(String node, long focusedFieldId, int focusedFieldType)
            throws TimeoutException {
        DOMUtils.clickNode(mWebContentsRef.get(), node);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ManualFillingComponentBridge.notifyFocusedFieldType(
                            mActivityTestRule.getWebContents(), focusedFieldId, focusedFieldType);
                });
    }

    /**
     * Although the submit button has no effect, it takes the focus from the input field and should
     * hide the keyboard.
     */
    public void clickSubmit() throws TimeoutException {
        DOMUtils.clickNode(mWebContentsRef.get(), SUBMIT_NODE_ID);
        getKeyboard().hideSoftKeyboardOnly(null);
    }

    // ---------------------------------
    // Helpers to wait for accessory UI.
    // ---------------------------------

    public void waitForKeyboardToDisappear() {
        pollUiThread(
                () -> {
                    Activity activity = mActivityTestRule.getActivity();
                    return !getKeyboard()
                            .isSoftKeyboardShowing(activity, activity.getCurrentFocus());
                });
    }

    public void waitForKeyboardAccessoryToDisappear() {
        pollInstrumentationThread(() -> accessoryStartedHiding(getKeyboardAccessoryBar()));
        pollUiThread(() -> accessoryViewFullyHidden(mActivityTestRule.getActivity()));
    }

    public void waitForKeyboardAccessoryToBeShown() {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        waitForKeyboardAccessoryToBeShown(false);
    }

    public void waitForKeyboardToShow() {
        CriteriaHelper.pollUiThread(
                () -> {
                    boolean isKeyboardShowing =
                            mActivityTestRule
                                    .getKeyboardDelegate()
                                    .isKeyboardShowing(
                                            mActivityTestRule.getActivity(),
                                            mActivityTestRule.getActivity().getTabsView());
                    Criteria.checkThat(isKeyboardShowing, Matchers.is(true));
                });
    }

    public void waitForKeyboardAccessoryToBeShown(boolean waitForSuggestionsToLoad) {
        pollInstrumentationThread(() -> accessoryStartedShowing(getKeyboardAccessoryBar()));
        pollUiThread(() -> accessoryViewFullyShown(mActivityTestRule.getActivity()));
        if (waitForSuggestionsToLoad) {
            pollUiThread(
                    () -> {
                        return getFirstAccessorySuggestion() != null;
                    },
                    "Waited for suggestions that never appeared.");
        }
    }

    public DropdownPopupWindowInterface waitForAutofillPopup(String filterInput) {
        final WebContents webContents = mActivityTestRule.getActivity().getCurrentWebContents();
        final View view = webContents.getViewAndroidDelegate().getContainerView();

        // Wait for InputConnection to be ready and fill the filterInput. Then wait for the anchor.
        pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mInputMethodManagerWrapper.getShowSoftInputCounter(), Matchers.is(1));
                });
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ImeAdapter.fromWebContents(webContents).setComposingTextForTest(filterInput, 4);
                });
        pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Autofill Popup anchor view was never added.",
                            view.findViewById(R.id.dropdown_popup_window),
                            Matchers.notNullValue());
                });
        View anchorView = view.findViewById(R.id.dropdown_popup_window);

        Assert.assertTrue(anchorView.getTag() instanceof DropdownPopupWindowInterface);
        final DropdownPopupWindowInterface popup =
                (DropdownPopupWindowInterface) anchorView.getTag();
        pollUiThread(
                () -> {
                    Criteria.checkThat(popup.isShowing(), Matchers.is(true));
                    Criteria.checkThat(popup.getListView(), Matchers.notNullValue());
                    Criteria.checkThat(popup.getListView().getHeight(), Matchers.not(0));
                });
        return popup;
    }

    public PasswordAccessorySheetCoordinator getOrCreatePasswordAccessorySheet() {
        return (PasswordAccessorySheetCoordinator)
                getManualFillingCoordinator()
                        .getMediatorForTesting()
                        .getOrCreateSheet(mWebContentsRef.get(), AccessoryTabType.PASSWORDS);
    }

    public AddressAccessorySheetCoordinator getOrCreateAddressAccessorySheet() {
        return (AddressAccessorySheetCoordinator)
                getManualFillingCoordinator()
                        .getMediatorForTesting()
                        .getOrCreateSheet(mWebContentsRef.get(), AccessoryTabType.ADDRESSES);
    }

    public CreditCardAccessorySheetCoordinator getOrCreateCreditCardAccessorySheet() {
        return (CreditCardAccessorySheetCoordinator)
                getManualFillingCoordinator()
                        .getMediatorForTesting()
                        .getOrCreateSheet(mWebContentsRef.get(), AccessoryTabType.CREDIT_CARDS);
    }

    private KeyboardAccessoryCoordinator getKeyboardAccessoryBar() {
        return getManualFillingCoordinator().getMediatorForTesting().getKeyboardAccessory();
    }

    private View getFirstAccessorySuggestion() {
        ViewGroup recyclerView = getAccessoryBarView();
        assert recyclerView != null;
        View view = recyclerView.getChildAt(0);
        return isAssignableFrom(KeyboardAccessoryButtonGroupView.class).matches(view) ? null : view;
    }

    // ----------------------------------
    // Helpers to set up the native side.
    // ----------------------------------

    /**
     * Calls cacheCredentials with two simple credentials.
     * @see ManualFillingTestHelper#cacheCredentials(String, String)
     */
    public void cacheTestCredentials() {
        cacheCredentials(
                new String[] {"[email protected]", "[email protected]"},
                new String[] {"TestPassword", "SomeReallyLongPassword"},
                false);
    }

    /**
     * @see ManualFillingTestHelper#cacheCredentials(String, String)
     * @param username A {@link String} to be used as display text for a username chip.
     * @param password A {@link String} to be used as display text for a password chip.
     */
    public void cacheCredentials(String username, String password) {
        cacheCredentials(new String[] {username}, new String[] {password}, false);
    }

    /**
     * Creates credential pairs from these strings and writes them into the cache of the native
     * controller. The controller will only refresh this cache on page load.
     *
     * @param usernames {@link String}s to be used as display text for username chips.
     * @param passwords {@link String}s to be used as display text for password chips.
     * @param originDenylisted boolean indicating whether password saving is disabled for the
     *     origin.
     */
    public void cacheCredentials(String[] usernames, String[] passwords, boolean originDenylisted) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ManualFillingComponentBridge.cachePasswordSheetData(
                            mActivityTestRule.getWebContents(),
                            usernames,
                            passwords,
                            originDenylisted);
                });
    }

    public static void createAutofillTestProfiles() throws TimeoutException {
        new AutofillTestHelper()
                .setProfile(
                        AutofillProfile.builder()
                                .setFullName("Johnathan Smithonian-Jackson")
                                .setCompanyName("Acme Inc")
                                .setStreetAddress("1 Main\nApt A")
                                .setRegion("CA")
                                .setLocality("San Francisco")
                                .setPostalCode("94102")
                                .setCountryCode("US")
                                .setPhoneNumber("(415) 888-9999")
                                .setEmailAddress("[email protected]")
                                .setLanguageCode("en")
                                .build());
        new AutofillTestHelper()
                .setProfile(
                        AutofillProfile.builder()
                                .setFullName("Jane Erika Donovanova")
                                .setCompanyName("Acme Inc")
                                .setStreetAddress("1 Main\nApt A")
                                .setRegion("CA")
                                .setLocality("San Francisco")
                                .setPostalCode("94102")
                                .setCountryCode("US")
                                .setPhoneNumber("(415) 999-0000")
                                .setEmailAddress("[email protected]")
                                .setLanguageCode("en")
                                .build());
        new AutofillTestHelper()
                .setProfile(
                        AutofillProfile.builder()
                                .setFullName("Marcus McSpartangregor")
                                .setCompanyName("Acme Inc")
                                .setStreetAddress("1 Main\nApt A")
                                .setRegion("CA")
                                .setLocality("San Francisco")
                                .setPostalCode("94102")
                                .setCountryCode("US")
                                .setPhoneNumber("(415) 999-0000")
                                .setEmailAddress("[email protected]")
                                .setLanguageCode("en")
                                .build());
    }

    public static void disableServerPredictions() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ManualFillingComponentBridge.disableServerPredictionsForTesting();
                });
    }

    // --------------------------------------------------
    // Generic helpers to match, check or wait for views.
    // TODO(fhorschig): Consider Moving to ViewUtils.
    // --------------------------------------------------

    /**
     * Use in a |onView().perform| action to select the tab at |tabIndex| for the found tab layout.
     * @param tabIndex The index to be selected.
     * @return The action executed by |perform|.
     */
    public static ViewAction selectTabAtPosition(int tabIndex) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return allOf(
                        isDisplayed(), isAssignableFrom(KeyboardAccessoryButtonGroupView.class));
            }

            @Override
            public String getDescription() {
                return "with tab at index " + tabIndex;
            }

            @Override
            public void perform(UiController uiController, View view) {
                KeyboardAccessoryButtonGroupView buttonGroupView =
                        (KeyboardAccessoryButtonGroupView) view;
                if (tabIndex >= buttonGroupView.getButtons().size()) {
                    throw new PerformException.Builder()
                            .withCause(new Throwable("No button at index " + tabIndex))
                            .build();
                }
                PostTask.runOrPostTask(
                        TaskTraits.UI_DEFAULT,
                        () -> buttonGroupView.getButtons().get(tabIndex).performClick());
            }
        };
    }

    /**
     * Use in a |onView().perform| action to select the tab at |tabIndex| for the found tab layout.
     * @param tabIndex The index to be selected.
     * @return The action executed by |perform|.
     */
    public static ViewAction selectTabWithDescription(@StringRes int descriptionResId) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return allOf(
                        isDisplayed(), isAssignableFrom(KeyboardAccessoryButtonGroupView.class));
            }

            @Override
            public String getDescription() {
                return "with tab with matching description.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                String descriptionToMatch = view.getContext().getString(descriptionResId);
                KeyboardAccessoryButtonGroupView buttonGroupView =
                        (KeyboardAccessoryButtonGroupView) view;
                for (int buttonIndex = 0;
                        buttonIndex < buttonGroupView.getButtons().size();
                        buttonIndex++) {
                    final ChromeImageButton button = buttonGroupView.getButtons().get(buttonIndex);
                    if (descriptionToMatch.equals(button.getContentDescription())) {
                        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, button::performClick);
                        return;
                    }
                }
                throw new PerformException.Builder()
                        .withCause(
                                new Throwable("No button with description: " + descriptionToMatch))
                        .build();
            }
        };
    }

    /**
     * Use in a |onView().perform| action to scroll to the end of a {@link RecyclerView}.
     * @return The action executed by |perform|.
     */
    public static ViewAction scrollToLastElement() {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return allOf(isDisplayed(), isAssignableFrom(RecyclerView.class));
            }

            @Override
            public String getDescription() {
                return "scrolling to end of view";
            }

            @Override
            public void perform(UiController uiController, View view) {
                RecyclerView recyclerView = (RecyclerView) view;
                int itemCount = recyclerView.getAdapter().getItemCount();
                if (itemCount <= 0) {
                    throw new PerformException.Builder()
                            .withCause(new Throwable("RecyclerView has no items."))
                            .build();
                }
                recyclerView.scrollToPosition(itemCount - 1);
            }
        };
    }

    /**
     * Matches any {@link TextView} which applies a {@link PasswordTransformationMethod}.
     * @return The matcher checking the transformation method.
     */
    public static Matcher<View> isTransformed() {
        return new BoundedMatcher<View, TextView>(TextView.class) {
            @Override
            public boolean matchesSafely(TextView textView) {
                return textView.getTransformationMethod() instanceof PasswordTransformationMethod;
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("is a transformed password.");
            }
        };
    }

    /**
     * Use like {@link androidx.test.espresso.Espresso#onView}. It waits for a view matching
     * the given |matcher| to be displayed and allows to chain checks/performs on the result.
     * @param matcher The matcher matching exactly the view that is expected to be displayed.
     * @return An interaction on the view matching |matcher|.
     */
    public static ViewInteraction whenDisplayed(Matcher<View> matcher) {
        return onViewWaiting(allOf(matcher, isDisplayed()));
    }

    public ViewInteraction waitForViewOnRoot(View root, Matcher<View> matcher) {
        waitForView((ViewGroup) root, allOf(matcher, isDisplayed()));
        return onView(matcher);
    }

    public ViewInteraction waitForViewOnActivityRoot(Matcher<View> matcher) {
        return waitForViewOnRoot(
                mActivityTestRule.getActivity().findViewById(android.R.id.content).getRootView(),
                matcher);
    }

    public static void waitToBeHidden(Matcher<View> matcher) {
        ViewUtils.waitForViewCheckingState(matcher, VIEW_INVISIBLE | VIEW_NULL | VIEW_GONE);
    }

    public String getAttribute(String node, String attribute)
            throws InterruptedException, TimeoutException {
        return DOMUtils.getNodeAttribute(attribute, mWebContentsRef.get(), node, String.class);
    }

    // --------------------------------------------
    // Helpers that force override the native side.
    // TODO(fhorschig): Search alternatives.
    // --------------------------------------------

    public void addGenerationButton() {
        PropertyProvider<KeyboardAccessoryData.Action[]> generationActionProvider =
                new PropertyProvider<>(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC);
        getManualFillingCoordinator()
                .registerActionProvider(mWebContentsRef.get(), generationActionProvider);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    generationActionProvider.notifyObservers(
                            new KeyboardAccessoryData.Action[] {
                                new KeyboardAccessoryData.Action(
                                        AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, result -> {})
                            });
                });
    }

    public void signalAutoGenerationStatus(boolean available) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ManualFillingComponentBridge.signalAutoGenerationStatus(
                            mActivityTestRule.getWebContents(), available);
                });
    }

    public void registerSheetDataProvider(@AccessoryTabType int tabType) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyProvider<AccessorySheetData> sheetDataProvider =
                            new PropertyProvider<>();
                    getManualFillingCoordinator()
                            .registerSheetDataProvider(
                                    mWebContentsRef.get(), tabType, sheetDataProvider);
                });
    }
}