chromium/chrome/browser/password_check/android/javatests/src/org/chromium/chrome/browser/password_check/PasswordCheckViewTest.java

// Copyright 2020 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.password_check;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
import static org.chromium.base.test.util.CriteriaHelper.pollUiThread;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.CompromisedCredentialProperties.COMPROMISED_CREDENTIAL;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.CompromisedCredentialProperties.CREDENTIAL_HANDLER;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.CompromisedCredentialProperties.HAS_MANUAL_CHANGE_BUTTON;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.DELETION_CONFIRMATION_HANDLER;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.DELETION_ORIGIN;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.CHECK_PROGRESS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.CHECK_STATUS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.CHECK_TIMESTAMP;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.COMPROMISED_CREDENTIALS_COUNT;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.LAUNCH_ACCOUNT_CHECKUP_ACTION;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.RESTART_BUTTON_ACTION;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.SHOW_CHECK_SUBTITLE;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties.UNKNOWN_PROGRESS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.ITEMS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.VIEW_CREDENTIAL;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.VIEW_DIALOG_HANDLER;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_NO_PASSWORDS;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_OFFLINE;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_QUOTA_LIMIT;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_QUOTA_LIMIT_ACCOUNT_CHECK;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_SIGNED_OUT;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.ERROR_UNKNOWN;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.IDLE;
import static org.chromium.chrome.browser.password_check.PasswordCheckUIStatus.RUNNING;
import static org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager.VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS;

import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Pair;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.annotation.IdRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.ScalableTimeout;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.password_check.PasswordCheckProperties.HeaderProperties;
import org.chromium.chrome.browser.password_manager.PasswordCheckReferrer;
import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager;
import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager.ReauthScope;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.listmenu.ListMenuButton;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.widget.ButtonCompat;
import org.chromium.url.GURL;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * View tests for the Password Check component ensure model changes are reflected in the check UI.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordCheckViewTest {
    private static final CompromisedCredential ANA =
            new CompromisedCredential(
                    "https://some-url.com/signin",
                    new GURL("https://some-url.com/"),
                    "Ana",
                    "some-url.com",
                    "Ana",
                    "password",
                    "https://some-url.com/.well-known/change-password",
                    "",
                    1,
                    1,
                    true,
                    false);
    private static final CompromisedCredential PHISHED =
            new CompromisedCredential(
                    "http://example.com/signin",
                    new GURL("http://example.com/"),
                    "",
                    "http://example.com",
                    "(No username)",
                    "DoSomething",
                    "http://example.com/.well-known/change-password",
                    "",
                    1,
                    1,
                    false,
                    true);
    private static final CompromisedCredential LEAKED =
            new CompromisedCredential(
                    "https://some-other-url.com/signin",
                    new GURL("https://some-other-url.com/"),
                    "AZiegler",
                    "some-other-url.com",
                    "AZiegler",
                    "N0M3rcy",
                    "",
                    "com.other.package",
                    1,
                    1,
                    true,
                    false);
    private static final CompromisedCredential LEAKED_AND_PHISHED =
            new CompromisedCredential(
                    "https://super-important.com/signin",
                    new GURL("https://super-important.com/"),
                    "HSong",
                    "super-important.com",
                    "HSong",
                    "N3rfTh1s",
                    "",
                    "com.important.super",
                    1,
                    1,
                    true,
                    true);

    private static final int LEAKS_COUNT = 2;

    private static final long S_TO_MS = 1000;
    private static final long MIN_TO_MS = 60 * S_TO_MS;
    private static final long H_TO_MS = 60 * MIN_TO_MS;
    private static final long DAY_TO_MS = 24 * H_TO_MS;

    private PropertyModel mModel;
    private PasswordCheckFragmentView mPasswordCheckView;

    @Mock private PasswordCheckComponentUi mComponentUi;
    @Mock private PasswordCheckCoordinator.CredentialEventHandler mMockHandler;
    @Mock private Runnable mMockLaunchCheckupInAccount;
    @Mock private Runnable mMockStartButtonCallback;

    @Rule
    public SettingsActivityTestRule<PasswordCheckFragmentView> mTestRule =
            new SettingsActivityTestRule<>(PasswordCheckFragmentView.class);

    @Before
    public void setUp() throws InterruptedException {
        MockitoAnnotations.initMocks(this);
        PasswordCheckComponentUiFactory.setCreationStrategy(
                (fragmentView, customTabIntentHelper, trustedIntentHelper, profile) -> {
                    mPasswordCheckView = (PasswordCheckFragmentView) fragmentView;
                    mPasswordCheckView.setComponentDelegate(mComponentUi);
                    return mComponentUi;
                });
        setUpUiLaunchedFromSettings();
        runOnUiThreadBlocking(
                () -> {
                    mModel = PasswordCheckProperties.createDefaultModel();
                    PasswordCheckCoordinator.setUpModelChangeProcessors(mModel, mPasswordCheckView);
                });
    }

    @Test
    @MediumTest
    public void testDisplaysHeaderAndCredential() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(RUNNING));
                    mModel.get(ITEMS).add(buildCredentialItem(ANA));
                });
        waitForListViewToHaveLength(2);
        // Has a change passwords button.
        assertNotNull(getCredentialChangeButtonAt(1));
        assertThat(getCredentialChangeButtonAt(1).getVisibility(), is(View.VISIBLE));
        assertThat(
                getCredentialChangeButtonAt(1).getText(),
                is(getString(R.string.password_check_credential_row_change_button_caption)));

        // Has a more button.
        assertNotNull(getCredentialMoreButtonAt(1));
        assertThat(getCredentialMoreButtonAt(1).getVisibility(), is(View.VISIBLE));
        assertThat(
                getCredentialMoreButtonAt(1).getContentDescription(), is(getString(R.string.more)));

        // Has a favicon.
        assertNotNull(getCredentialFaviconAt(1));
        assertThat(getCredentialFaviconAt(1).getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusIllustrationPositive() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, 0, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertIllustration(R.drawable.password_check_positive);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/1133604")
    public void testStatusIllustrationWarning() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, LEAKS_COUNT, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertIllustration(R.drawable.password_checkup_warning);
    }

    @Test
    @MediumTest
    public void testStatusIllustrationNeutral() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_OFFLINE));
                });
        waitForListViewToHaveLength(1);
        assertIllustration(R.drawable.password_check_neutral);
    }

    @Test
    @MediumTest
    public void testStatusDisplaysIconOnIdleNoLeaks() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, 0, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertDisplaysIcon(R.drawable.ic_check_circle_filled_green_24dp);
    }

    @Test
    @MediumTest
    public void testStatusDisplaysIconOnIdleWithLeaks() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, LEAKS_COUNT, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertDisplaysIcon(R.drawable.ic_warning_red_24dp);
    }

    @Test
    @MediumTest
    public void testStatusDisplaysIconOnError() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_OFFLINE));
                });
        waitForListViewToHaveLength(1);
        assertDisplaysIcon(R.drawable.ic_error_grey800_24dp_filled);
    }

    @Test
    @MediumTest
    public void testStatusDisplaysProgressBarOnRunning() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(RUNNING));
                });
        waitForListViewToHaveLength(1);
        assertThat(getHeaderIcon().getVisibility(), is(View.GONE));
        assertThat(getHeaderProgressBar().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusDisplaysClickableRestartAction() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, 0, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertThat(getActionButton().getVisibility(), is(View.VISIBLE));
        assertTrue(getActionButton().isClickable());
        getActionButton().callOnClick();
        waitForEvent(mMockStartButtonCallback).run();
    }

    @Test
    @MediumTest
    public void testStatusNotDisplaysRestartAction() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(RUNNING));
                });
        waitForListViewToHaveLength(1);
        assertThat(getActionButton().getVisibility(), is(View.GONE));
        assertFalse(getActionButton().isClickable());
    }

    @Test
    @MediumTest
    public void testStatusDisplaysRestartForOffline() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_OFFLINE));
                });
        waitForListViewToHaveLength(1);
        assertThat(getActionButton().getVisibility(), is(View.VISIBLE));
        assertTrue(getActionButton().isClickable());
    }

    @Test
    @MediumTest
    public void testStatusDoesNotDisplayRestartForNoPasswords() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_NO_PASSWORDS));
                });
        waitForListViewToHaveLength(1);
        assertThat(getActionButton().getVisibility(), is(View.GONE));
        assertFalse(getActionButton().isClickable());
    }

    @Test
    @MediumTest
    public void testStatusRunningText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(RUNNING, UNKNOWN_PROGRESS));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_initial_running)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusIdleNoLeaksText() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, 0, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_idle_no_leaks)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusIdleWithLeaksText() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, LEAKS_COUNT, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(
                        mPasswordCheckView
                                .getContext()
                                .getResources()
                                .getQuantityString(
                                        R.plurals.password_check_status_message_idle_with_leaks,
                                        LEAKS_COUNT,
                                        LEAKS_COUNT)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusErrorOfflineText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_OFFLINE));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_error_offline)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusErrorNoPasswordsText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_NO_PASSWORDS));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_error_no_passwords)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusErrorQuotaLimitText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_QUOTA_LIMIT));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_error_quota_limit)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusErrorQuotaLimitAccountCheckText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_QUOTA_LIMIT_ACCOUNT_CHECK));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(
                        getString(
                                        R.string
                                                .password_check_status_message_error_quota_limit_account_check)
                                .replace("<link>", "")
                                .replace("</link>", "")));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusErrorSignedOutText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_SIGNED_OUT));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_error_signed_out)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusErrorUnknownText() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_UNKNOWN));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderMessageText(),
                is(getString(R.string.password_check_status_message_error_unknown)));
        assertThat(getHeaderMessage().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderDescription().getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testStatusDisplaysSubtitleOnIdleNoLeaks() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, 0, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderSubtitle().getText(),
                is(getString(R.string.password_check_status_subtitle_no_findings)));
        assertThat(getHeaderSubtitle().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusDisplaysSubtitleOnIdleWithLeaks() {
        Long checkTimestamp = System.currentTimeMillis();
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(IDLE, LEAKS_COUNT, checkTimestamp));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderSubtitle().getText(),
                is(
                        getString(
                                R.string
                                        .password_check_status_subtitle_found_compromised_credentials)));
        assertThat(getHeaderSubtitle().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusDisplaysSubtitle() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_UNKNOWN, true));
                });
        waitForListViewToHaveLength(1);
        assertThat(
                getHeaderSubtitle().getText(),
                is(
                        getString(
                                R.string
                                        .password_check_status_subtitle_found_compromised_credentials)));
        assertThat(getHeaderSubtitle().getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testStatusNotDisplaysSubtitle() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildHeader(ERROR_UNKNOWN, false));
                });
        waitForListViewToHaveLength(1);
        assertThat(getHeaderSubtitle().getVisibility(), is(View.GONE));
    }

    @Test
    @SmallTest
    public void testGetTimestampStrings() {
        Resources res = mPasswordCheckView.getContext().getResources();
        assertThat(PasswordCheckViewBinder.getTimestamp(res, 10 * S_TO_MS), is("Just now"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, MIN_TO_MS), is("1 minute ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, 17 * MIN_TO_MS), is("17 minutes ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, H_TO_MS), is("1 hour ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, 13 * H_TO_MS), is("13 hours ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, DAY_TO_MS), is("1 day ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, 2 * DAY_TO_MS), is("2 days ago"));
        assertThat(PasswordCheckViewBinder.getTimestamp(res, 315 * DAY_TO_MS), is("315 days ago"));
    }

    @Test
    @MediumTest
    public void testCredentialDisplaysNameOriginAndReason() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildCredentialItem(PHISHED));
                    mModel.get(ITEMS).add(buildCredentialItem(LEAKED));
                    mModel.get(ITEMS).add(buildCredentialItem(LEAKED_AND_PHISHED));
                });
        waitForListViewToHaveLength(3);

        // The phished credential is rendered first:
        assertThat(getCredentialOriginAt(0).getText(), is(PHISHED.getDisplayOrigin()));
        assertThat(getCredentialUserAt(0).getText(), is(PHISHED.getDisplayUsername()));
        assertThat(
                getCredentialReasonAt(0).getText(),
                is(getString(R.string.password_check_credential_row_reason_phished)));
        assertThat(getCredentialChangeButtonAt(0).getVisibility(), is(View.VISIBLE));
        assertThat(getCredentialChangeHintAt(0).getVisibility(), is(View.GONE));

        // The leaked credential is rendered second:
        assertThat(getCredentialOriginAt(1).getText(), is(LEAKED.getDisplayOrigin()));
        assertThat(getCredentialUserAt(1).getText(), is(LEAKED.getDisplayUsername()));
        assertThat(
                getCredentialReasonAt(1).getText(),
                is(getString(R.string.password_check_credential_row_reason_leaked)));
        assertThat(getCredentialChangeButtonAt(1).getVisibility(), is(View.VISIBLE));
        assertThat(getCredentialChangeHintAt(1).getVisibility(), is(View.GONE));

        // The leaked and phished credential is rendered third:
        assertThat(getCredentialOriginAt(2).getText(), is(LEAKED_AND_PHISHED.getDisplayOrigin()));
        assertThat(getCredentialUserAt(2).getText(), is(LEAKED_AND_PHISHED.getDisplayUsername()));
        assertThat(
                getCredentialReasonAt(2).getText(),
                is(getString(R.string.password_check_credential_row_reason_leaked_and_phished)));
        assertThat(getCredentialChangeButtonAt(2).getVisibility(), is(View.VISIBLE));
        assertThat(getCredentialChangeHintAt(2).getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testHidesCredentialChangeButtonWithoutValidEntryPoint() {
        runOnUiThreadBlocking(
                () ->
                        mModel.get(ITEMS)
                                .add(
                                        new MVCListAdapter.ListItem(
                                                PasswordCheckProperties.ItemType
                                                        .COMPROMISED_CREDENTIAL,
                                                new PropertyModel.Builder(
                                                                PasswordCheckProperties
                                                                        .CompromisedCredentialProperties
                                                                        .ALL_KEYS)
                                                        .with(COMPROMISED_CREDENTIAL, ANA)
                                                        .with(HAS_MANUAL_CHANGE_BUTTON, false)
                                                        .with(CREDENTIAL_HANDLER, mMockHandler)
                                                        .build())));
        waitForListViewToHaveLength(1);

        // The credential has no change button:
        assertThat(getCredentialOriginAt(0).getText(), is(ANA.getDisplayOrigin()));
        assertThat(getCredentialUserAt(0).getText(), is(ANA.getDisplayUsername()));
        assertThat(getCredentialChangeButtonAt(0).getVisibility(), is(View.GONE));
        assertThat(getCredentialChangeHintAt(0).getVisibility(), is(View.VISIBLE));
    }

    @Test
    @MediumTest
    public void testCredentialDisplays() {
        runOnUiThreadBlocking(
                () -> {
                    mModel.get(ITEMS).add(buildCredentialItem(LEAKED));
                });
        pollUiThread(() -> Criteria.checkThat(getPasswordCheckViewList().getChildCount(), is(1)));

        // Origin and username.
        assertThat(getCredentialOriginAt(0).getText(), is(LEAKED.getDisplayOrigin()));
        assertThat(getCredentialUserAt(0).getText(), is(LEAKED.getDisplayUsername()));

        // Reason to show credential.
        assertThat(
                getCredentialReasonAt(0).getText(),
                is(getString(R.string.password_check_credential_row_reason_leaked)));

        // Change button without script.
        assertNotNull(getCredentialChangeButtonAt(0));
        assertThat(
                getCredentialChangeButtonAt(0).getText(),
                is(getString(R.string.password_check_credential_row_change_button_caption)));
        assertThat(getCredentialChangeHintAt(0).getVisibility(), is(View.GONE));
    }

    @Test
    @MediumTest
    public void testClickingChangePasswordTriggersHandler() {
        runOnUiThreadBlocking(() -> mModel.get(ITEMS).add(buildCredentialItem(ANA)));
        waitForListViewToHaveLength(1);

        TouchCommon.singleClickView(getCredentialChangeButtonAt(0));

        waitForEvent(mMockHandler).onChangePasswordButtonClick(eq(ANA));
    }

    @Test
    @MediumTest
    public void testClickingEditInMoreMenuTriggersHandler() {
        runOnUiThreadBlocking(() -> mModel.get(ITEMS).add(buildCredentialItem(ANA)));
        waitForListViewToHaveLength(1);

        TouchCommon.singleClickView(getCredentialMoreButtonAt(0));

        onView(withText(R.string.password_check_credential_menu_item_edit_button_caption))
                .inRoot(
                        withDecorView(
                                not(
                                        is(
                                                mPasswordCheckView
                                                        .getActivity()
                                                        .getWindow()
                                                        .getDecorView()))))
                .perform(click());

        waitForEvent(mMockHandler).onEdit(eq(ANA), eq(mPasswordCheckView.getContext()));
    }

    @Test
    @MediumTest
    public void testClickingDeleteInMoreMenuTriggersHandler() {
        runOnUiThreadBlocking(() -> mModel.get(ITEMS).add(buildCredentialItem(ANA)));
        waitForListViewToHaveLength(1);

        TouchCommon.singleClickView(getCredentialMoreButtonAt(0));

        onView(withText(R.string.password_check_credential_menu_item_remove_button_caption))
                .inRoot(
                        withDecorView(
                                not(
                                        is(
                                                mPasswordCheckView
                                                        .getActivity()
                                                        .getWindow()
                                                        .getDecorView()))))
                .perform(click());

        waitForEvent(mMockHandler).onRemove(eq(ANA));
    }

    @Test
    @MediumTest
    public void testClickingViewInMoreMenuTriggersHandler() {
        runOnUiThreadBlocking(() -> mModel.get(ITEMS).add(buildCredentialItem(ANA)));
        waitForListViewToHaveLength(1);

        TouchCommon.singleClickView(getCredentialMoreButtonAt(0));

        onView(withText(R.string.password_check_credential_menu_item_view_button_caption))
                .inRoot(
                        withDecorView(
                                not(
                                        is(
                                                mPasswordCheckView
                                                        .getActivity()
                                                        .getWindow()
                                                        .getDecorView()))))
                .perform(click());

        waitForEvent(mMockHandler).onView(eq(ANA));
    }

    @Test
    @MediumTest
    public void testConfirmingDeletionDialogTriggersHandler() {
        final AtomicInteger recordedConfirmation = new AtomicInteger(0);
        PasswordCheckDeletionDialogFragment.Handler fakeHandler =
                new PasswordCheckDeletionDialogFragment.Handler() {
                    @Override
                    public void onDismiss() {}

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        recordedConfirmation.incrementAndGet();
                    }
                };

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(DELETION_ORIGIN, ANA.getDisplayOrigin());
                    mModel.set(DELETION_CONFIRMATION_HANDLER, fakeHandler);
                });

        onView(withText(R.string.password_entry_edit_delete_credential_dialog_confirm))
                .inRoot(
                        withDecorView(
                                not(
                                        is(
                                                mPasswordCheckView
                                                        .getActivity()
                                                        .getWindow()
                                                        .getDecorView()))))
                .perform(click());

        assertThat(recordedConfirmation.get(), is(1));
    }

    @Test
    @MediumTest
    public void testCopyPasswordViewDialog() {
        PasswordCheckDeletionDialogFragment.Handler fakeHandler =
                new PasswordCheckDeletionDialogFragment.Handler() {
                    @Override
                    public void onDismiss() {}

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {}
                };
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthScope.ONE_AT_A_TIME);

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VIEW_CREDENTIAL, ANA);
                    mModel.set(VIEW_DIALOG_HANDLER, fakeHandler);
                });
        onView(withId(R.id.view_dialog_copy_button)).inRoot(isDialog()).perform(click());

        ClipboardManager clipboard =
                (ClipboardManager)
                        mPasswordCheckView
                                .getActivity()
                                .getApplicationContext()
                                .getSystemService(Context.CLIPBOARD_SERVICE);
        assertThat(
                clipboard.getPrimaryClip().getItemAt(0).getText().toString(),
                is(ANA.getPassword()));
    }

    @Test
    @MediumTest
    public void testCloseViewDialogTriggersHandler() {
        final AtomicInteger recordedClosure = new AtomicInteger(0);
        PasswordCheckDeletionDialogFragment.Handler fakeHandler =
                new PasswordCheckDeletionDialogFragment.Handler() {
                    @Override
                    public void onDismiss() {}

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        recordedClosure.incrementAndGet();
                    }
                };
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthScope.ONE_AT_A_TIME);

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VIEW_CREDENTIAL, ANA);
                    mModel.set(VIEW_DIALOG_HANDLER, fakeHandler);
                });

        onView(withText(R.string.close))
                .inRoot(
                        withDecorView(
                                not(
                                        is(
                                                mPasswordCheckView
                                                        .getActivity()
                                                        .getWindow()
                                                        .getDecorView()))))
                .perform(click());

        assertThat(recordedClosure.get(), is(1));
    }

    @Test
    @MediumTest
    public void testOnResumeViewDialogReauthenticationNeeded() {
        final AtomicInteger recordedDismiss = new AtomicInteger(0);
        PasswordCheckDeletionDialogFragment.Handler fakeHandler =
                new PasswordCheckDeletionDialogFragment.Handler() {
                    @Override
                    public void onDismiss() {
                        recordedDismiss.incrementAndGet();
                    }

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {}
                };
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthScope.ONE_AT_A_TIME);

        runOnUiThreadBlocking(
                () -> {
                    mModel.set(VIEW_CREDENTIAL, ANA);
                    mModel.set(VIEW_DIALOG_HANDLER, fakeHandler);
                });

        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis() - VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS,
                ReauthScope.ONE_AT_A_TIME);

        mTestRule.getFragment().onStop();
        mTestRule.getFragment().onResume();

        CriteriaHelper.pollInstrumentationThread(() -> recordedDismiss.get() == 1);
    }

    @Test
    @SmallTest
    public void testHelpHandlerCalled() {
        when(mComponentUi.handleHelp(any())).thenReturn(true);
        onView(withId(R.id.menu_id_targeted_help)).perform(click());
        verify(mComponentUi).handleHelp(any());
    }

    private MVCListAdapter.ListItem buildHeader(
            @PasswordCheckUIStatus int status,
            Integer compromisedCredentialsCount,
            Long checkTimestamp) {
        return buildHeader(status, compromisedCredentialsCount, checkTimestamp, null, true);
    }

    private MVCListAdapter.ListItem buildHeader(
            @PasswordCheckUIStatus int status, Pair<Integer, Integer> progress) {
        return buildHeader(status, null, null, progress, false);
    }

    private MVCListAdapter.ListItem buildHeader(
            @PasswordCheckUIStatus int status, boolean showStatusSubtitle) {
        return buildHeader(status, null, null, null, showStatusSubtitle);
    }

    private MVCListAdapter.ListItem buildHeader(@PasswordCheckUIStatus int status) {
        return buildHeader(status, null, null, null, false);
    }

    private MVCListAdapter.ListItem buildHeader(
            @PasswordCheckUIStatus int status,
            Integer compromisedCredentialsCount,
            Long checkTimestamp,
            Pair<Integer, Integer> progress,
            boolean showStatusSubtitle) {
        return new MVCListAdapter.ListItem(
                PasswordCheckProperties.ItemType.HEADER,
                new PropertyModel.Builder(HeaderProperties.ALL_KEYS)
                        .with(CHECK_PROGRESS, progress)
                        .with(CHECK_STATUS, status)
                        .with(CHECK_TIMESTAMP, checkTimestamp)
                        .with(COMPROMISED_CREDENTIALS_COUNT, compromisedCredentialsCount)
                        .with(LAUNCH_ACCOUNT_CHECKUP_ACTION, mMockLaunchCheckupInAccount)
                        .with(RESTART_BUTTON_ACTION, mMockStartButtonCallback)
                        .with(SHOW_CHECK_SUBTITLE, showStatusSubtitle)
                        .build());
    }

    private MVCListAdapter.ListItem buildCredentialItem(CompromisedCredential credential) {
        return new MVCListAdapter.ListItem(
                PasswordCheckProperties.ItemType.COMPROMISED_CREDENTIAL,
                new PropertyModel.Builder(
                                PasswordCheckProperties.CompromisedCredentialProperties.ALL_KEYS)
                        .with(COMPROMISED_CREDENTIAL, credential)
                        .with(HAS_MANUAL_CHANGE_BUTTON, true)
                        .with(CREDENTIAL_HANDLER, mMockHandler)
                        .build());
    }

    private void setUpUiLaunchedFromSettings() {
        Bundle fragmentArgs = new Bundle();
        fragmentArgs.putInt(
                PasswordCheckFragmentView.PASSWORD_CHECK_REFERRER,
                PasswordCheckReferrer.PASSWORD_SETTINGS);
        mTestRule.startSettingsActivity(fragmentArgs);
    }

    private void waitForListViewToHaveLength(int length) {
        pollUiThread(
                () -> Criteria.checkThat(getPasswordCheckViewList().getChildCount(), is(length)));
    }

    private void assertDisplaysIcon(int resourceId) {
        assertThat(getHeaderIcon().getVisibility(), is(View.VISIBLE));
        assertThat(getHeaderProgressBar().getVisibility(), is(View.GONE));
        Drawable icon = getHeaderIcon().getDrawable();
        int widthPx = icon.getIntrinsicWidth();
        int heightPx = icon.getIntrinsicHeight();
        assertTrue(
                getBitmap(
                                AppCompatResources.getDrawable(
                                        mPasswordCheckView.getContext(), resourceId),
                                widthPx,
                                heightPx)
                        .sameAs(getBitmap(icon, widthPx, heightPx)));
    }

    private void assertIllustration(int resourceId) {
        Drawable illustration =
                ((ImageView) getStatus().findViewById(R.id.check_status_illustration))
                        .getDrawable();
        int widthPx = illustration.getIntrinsicWidth();
        int heightPx = illustration.getIntrinsicHeight();
        assertTrue(
                getBitmap(
                                AppCompatResources.getDrawable(
                                        mPasswordCheckView.getContext(), resourceId),
                                widthPx,
                                heightPx)
                        .sameAs(getBitmap(illustration, widthPx, heightPx)));
    }

    private View getStatus() {
        return mPasswordCheckView.getListView().getChildAt(0);
    }

    private ImageView getHeaderIcon() {
        return getStatus().findViewById(R.id.check_status_icon);
    }

    private ProgressBar getHeaderProgressBar() {
        return getStatus().findViewById(R.id.check_status_progress);
    }

    private TextView getHeaderDescription() {
        return getStatus().findViewById(R.id.check_status_description);
    }

    private TextView getHeaderMessage() {
        return getStatus().findViewById(R.id.check_status_message);
    }

    private String getHeaderMessageText() {
        return getHeaderMessage().getText().toString();
    }

    private TextView getHeaderSubtitle() {
        return getStatus().findViewById(R.id.check_status_subtitle);
    }

    private ImageButton getActionButton() {
        return getStatus().findViewById(R.id.check_status_restart_button);
    }

    private RecyclerView getPasswordCheckViewList() {
        return mPasswordCheckView.getListView();
    }

    private TextView getCredentialOriginAt(int index) {
        return getPasswordCheckViewList().getChildAt(index).findViewById(R.id.credential_origin);
    }

    private TextView getCredentialUserAt(int index) {
        return getPasswordCheckViewList().getChildAt(index).findViewById(R.id.compromised_username);
    }

    private TextView getCredentialReasonAt(int index) {
        return getPasswordCheckViewList().getChildAt(index).findViewById(R.id.compromised_reason);
    }

    private ButtonCompat getCredentialChangeButtonAt(int index) {
        return getPasswordCheckViewList()
                .getChildAt(index)
                .findViewById(R.id.credential_change_button);
    }

    private TextView getCredentialChangeHintAt(int index) {
        return getPasswordCheckViewList()
                .getChildAt(index)
                .findViewById(R.id.credential_change_hint);
    }

    private ListMenuButton getCredentialMoreButtonAt(int index) {
        return getPasswordCheckViewList()
                .getChildAt(index)
                .findViewById(R.id.credential_menu_button);
    }

    private ImageView getCredentialFaviconAt(int index) {
        return getPasswordCheckViewList().getChildAt(index).findViewById(R.id.credential_favicon);
    }

    private String getString(@IdRes int stringResource) {
        return mTestRule.getActivity().getString(stringResource);
    }

    private static <T> T waitForEvent(T mock) {
        return verify(
                mock,
                timeout(ScalableTimeout.scaleTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL)));
    }

    private Bitmap getBitmap(Drawable drawable, int widthPx, int heightPx) {
        Bitmap bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, widthPx, heightPx);
        drawable.draw(canvas);
        return bitmap;
    }
}