Commit a098b32c authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[PwdCheckAndroid] Add Menu to delete compromised credentials

This CL replaces the image view with the default more button in Chrome.
When clicked, it opens a menu that offers the delete option which
ends up asking the bridge to remove the selected credential.

Additionally, this CL fixes some enum declarations and provides a
minimal implementation for the PasswordCheckObserver in order to set
up and test the wiring for the event handling path.

Bug: 1106726, 1092444
Change-Id: I2ef8cf1fa037fbbd6e83097f6bbbd07fdfe65e7e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2306260Reviewed-by: default avatarIoana Pandele <ioanap@chromium.org>
Commit-Queue: Friedrich [CET] <fhorschig@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790768}
parent 183d178c
......@@ -36,7 +36,6 @@ android_library("public_java") {
"//third_party/android_deps:androidx_recyclerview_recyclerview_java",
]
sources = [
"java/src/org/chromium/chrome/browser/password_check/CompromisedCredential.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheck.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckFragmentView.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckPreference.java",
......@@ -46,7 +45,10 @@ android_library("public_java") {
android_library("public_ui_java") {
deps = []
sources = [ "java/src/org/chromium/chrome/browser/password_check/PasswordCheckComponentUi.java" ]
sources = [
"java/src/org/chromium/chrome/browser/password_check/CompromisedCredential.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckComponentUi.java",
]
}
junit_binary("password_check_junit_tests") {
......@@ -55,10 +57,12 @@ junit_binary("password_check_junit_tests") {
sources = [ "junit/src/org/chromium/chrome/browser/password_check/PasswordCheckControllerTest.java" ]
deps = [
":public_ui_java",
"internal:internal_java",
"internal:public_ui_factory_java",
"//base:base_java",
"//base:base_junit_test_support",
"//chrome/browser/password_check/android:public_java",
"//chrome/browser/password_check/android/internal:internal_java",
"//third_party/hamcrest:hamcrest_java",
"//third_party/junit",
"//ui/android:ui_full_java",
......@@ -72,20 +76,24 @@ android_library("test_java") {
deps = [
":public_java",
":public_ui_java",
"internal:internal_java",
"//base:base_java",
"//base:base_java_test_support",
"//chrome/android:chrome_java",
"//chrome/android:chrome_test_java",
"//chrome/android:chrome_test_util_java",
"//chrome/browser/flags:java",
"//chrome/browser/password_check/android/internal:internal_java",
"//chrome/browser/settings:test_support_java",
"//chrome/test/android:chrome_java_test_support",
"//components/browser_ui/widget/android:java",
"//components/embedder_support/android:util_java",
"//content/public/test/android:content_java_test_support",
"//third_party/android_deps:androidx_annotation_annotation_java",
"//third_party/android_deps:androidx_fragment_fragment_java",
"//third_party/android_deps:androidx_recyclerview_recyclerview_java",
"//third_party/android_deps:androidx_test_runner_java",
"//third_party/android_deps:espresso_java",
"//third_party/hamcrest:hamcrest_java",
"//third_party/junit",
"//third_party/mockito:mockito_java",
......
......@@ -47,6 +47,7 @@ android_library("internal_ui_factory_java") {
android_library("internal_java") {
deps = [
":java_resources",
":public_factory_java",
"//base:base_java",
"//base:jni_java",
"//chrome/android:chrome_app_java_resources",
......@@ -55,6 +56,7 @@ android_library("internal_java") {
"//chrome/browser/profiles/android:java",
"//chrome/browser/settings:java",
"//chrome/browser/ui/android/strings:ui_strings_grd",
"//components/browser_ui/widget/android:java",
"//components/embedder_support/android:util_java",
"//third_party/android_deps:androidx_annotation_annotation_java",
"//third_party/android_deps:androidx_fragment_fragment_java",
......
......@@ -50,14 +50,17 @@
</LinearLayout>
<ImageView
<org.chromium.components.browser_ui.widget.listmenu.ListMenuButton
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/credential_menu_button"
android:background="@null"
android:contentDescription="@string/more"
android:paddingEnd="@dimen/compromised_credential_row_more_padding_end"
android:paddingStart="@dimen/compromised_credential_row_more_padding_start"
android:layout_gravity="top"
android:layout_height="@dimen/compromised_credential_row_more_size"
android:layout_width="@dimen/compromised_credential_row_more_size"
android:src="@drawable/ic_more_vert_24dp" />
android:layout_width="wrap_content"
android:paddingEnd="@dimen/compromised_credential_row_more_padding_end"
android:paddingStart="@dimen/compromised_credential_row_more_padding_start"
android:src="@drawable/ic_more_vert_24dp"
app:tint="@color/default_icon_color_tint_list" />
</LinearLayout>
......@@ -20,12 +20,23 @@ import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
class PasswordCheckCoordinator implements PasswordCheckComponentUi {
private final PasswordCheckFragmentView mFragmentView;
/**
* Blueprint for a class that handles interactions with credentials.
*/
interface CredentialEventHandler {
/**
* Removes the given Credential from the password store.
* @param credential A {@link CompromisedCredential} to be removed.
*/
void onRemove(CompromisedCredential credential);
}
PasswordCheckCoordinator(PasswordCheckFragmentView fragmentView) {
mFragmentView = fragmentView;
PropertyModel model = PasswordCheckProperties.createDefaultModel();
PasswordCheckMediator mediator = new PasswordCheckMediator();
PasswordCheckCoordinator.setUpModelChangeProcessors(model, mFragmentView);
mediator.initialize(model);
mediator.initialize(model, PasswordCheckFactory.create());
}
// TODO(crbug.com/1101256): Move to view code.
......
......@@ -18,4 +18,9 @@ class PasswordCheckImpl implements PasswordCheck {
SettingsLauncher launcher = new SettingsLauncherImpl();
launcher.launchSettingsActivity(context, PasswordCheckFragmentView.class);
}
@Override
public void removeCredential(CompromisedCredential credential) {
// TODO(crbug.com/1106726): Call native method through bridge.
}
}
\ No newline at end of file
......@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.password_check;
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.HeaderProperties.CHECK_STATUS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.ITEMS;
......@@ -19,11 +20,13 @@ import java.util.List;
* Contains the logic for the PasswordCheck component. It sets the state of the model and reacts to
* events like clicks.
*/
class PasswordCheckMediator {
class PasswordCheckMediator implements PasswordCheckCoordinator.CredentialEventHandler {
private PropertyModel mModel;
private PasswordCheckComponentUi.Delegate mDelegate;
void initialize(PropertyModel model) {
void initialize(PropertyModel model, PasswordCheckComponentUi.Delegate delegate) {
mModel = model;
mDelegate = delegate;
}
void onCompromisedCredentialsAvailable(
......@@ -43,7 +46,13 @@ class PasswordCheckMediator {
.Builder(PasswordCheckProperties.CompromisedCredentialProperties
.ALL_KEYS)
.with(COMPROMISED_CREDENTIAL, credential)
.with(CREDENTIAL_HANDLER, this)
.build()));
}
}
@Override
public void onRemove(CompromisedCredential credential) {
mDelegate.removeCredential(credential);
}
}
......@@ -34,8 +34,11 @@ class PasswordCheckProperties {
static final PropertyModel
.ReadableObjectPropertyKey<CompromisedCredential> COMPROMISED_CREDENTIAL =
new PropertyModel.ReadableObjectPropertyKey<>("compromised_credential");
static final PropertyModel.ReadableObjectPropertyKey<
PasswordCheckCoordinator.CredentialEventHandler> CREDENTIAL_HANDLER =
new PropertyModel.ReadableObjectPropertyKey<>("credential_handler");
static final PropertyKey[] ALL_KEYS = {COMPROMISED_CREDENTIAL};
static final PropertyKey[] ALL_KEYS = {COMPROMISED_CREDENTIAL, CREDENTIAL_HANDLER};
private CompromisedCredentialProperties() {}
}
......
......@@ -5,16 +5,22 @@
package org.chromium.chrome.browser.password_check;
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.HeaderProperties.CHECK_STATUS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.ITEMS;
import static org.chromium.components.embedder_support.util.UrlUtilities.stripScheme;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.chromium.chrome.browser.password_check.PasswordCheckProperties.ItemType;
import org.chromium.chrome.browser.password_check.internal.R;
import org.chromium.components.browser_ui.widget.listmenu.BasicListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenu;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuButton;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuItemProperties;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
......@@ -27,9 +33,11 @@ import org.chromium.ui.modelutil.SimpleRecyclerViewMcp;
*/
class PasswordCheckViewBinder {
/**
* Called whenever a property in the given model changes. It updates the given view accordingly.
* @param model The observed {@link PropertyModel}. Its data need to be reflected in the view.
* @param view The {@link PasswordCheckFragmentView} to update.
* Called whenever a property in the given model changes. It updates the given view
* accordingly.
*
* @param model The observed {@link PropertyModel}. Its data is reflected in the view.
* @param view The {@link PasswordCheckFragmentView} to update.
* @param propertyKey The {@link PropertyKey} which changed.
*/
static void bindPasswordCheckView(
......@@ -47,7 +55,8 @@ class PasswordCheckViewBinder {
/**
* Factory used to create a new View inside the list inside the PasswordCheckFragmentView.
* @param parent The parent {@link ViewGroup} of the new item.
*
* @param parent The parent {@link ViewGroup} of the new item.
* @param itemType The type of View to create.
*/
private static PasswordCheckViewHolder createViewHolder(
......@@ -67,8 +76,9 @@ class PasswordCheckViewBinder {
/**
* This method creates a model change processor for each recycler view item when it is created.
* @param holder A {@link PasswordCheckViewHolder} holding the view and view binder for the MCP.
* @param item A {@link MVCListAdapter.ListItem} holding the {@link PropertyModel} for the MCP.
*
* @param holder A {@link PasswordCheckViewHolder} holding a view and view binder for the MCP.
* @param item A {@link MVCListAdapter.ListItem} holding a {@link PropertyModel} for the MCP.
*/
private static void connectPropertyModel(
PasswordCheckViewHolder holder, MVCListAdapter.ListItem item) {
......@@ -76,11 +86,12 @@ class PasswordCheckViewBinder {
}
/**
* Called whenever a credential is bound to this view holder. Please note that this method
* might be called on a recycled view with old data, so make sure to always reset unused
* properties to default values.
* @param model The model containing the data for the view
* @param view The view to be bound
* Called whenever a credential is bound to this view holder. Please note that this method might
* be called on a recycled view with old data, so make sure to always reset unused properties to
* default values.
*
* @param model The model containing the data for the view
* @param view The view to be bound
* @param propertyKey The key of the property to be bound
*/
private static void bindCredentialView(
......@@ -100,16 +111,27 @@ class PasswordCheckViewBinder {
reason.setText(credential.isPhished()
? R.string.password_check_credential_row_reason_phished
: R.string.password_check_credential_row_reason_leaked);
ListMenuButton more = view.findViewById(R.id.credential_menu_button);
more.setDelegate(() -> {
return createCredentialMenu(view.getContext(), model.get(COMPROMISED_CREDENTIAL),
model.get(CREDENTIAL_HANDLER));
});
} else if (propertyKey == CREDENTIAL_HANDLER) {
assert model.get(CREDENTIAL_HANDLER) != null;
// Is read-only and must therefore be bound initially, so no action required.
} else {
assert false : "Unhandled update to property:" + propertyKey;
}
}
/**
* Called whenever a property in the given model changes. It updates the given view accordingly.
* @param model The observed {@link PropertyModel}. Its data need to be reflected in the view.
* @param view The {@link View} of the header to update.
* @param key The {@link PropertyKey} which changed.
* Called whenever a property in the given model changes. It updates the given view
* accordingly.
*
* @param model The observed {@link PropertyModel}. Its data needs to be reflected in the view.
* @param view The {@link View} of the header to update.
* @param key The {@link PropertyKey} which changed.
*/
private static void bindHeaderView(PropertyModel model, View view, PropertyKey key) {
if (key == CHECK_STATUS) {
......@@ -120,4 +142,20 @@ class PasswordCheckViewBinder {
}
private PasswordCheckViewBinder() {}
private static ListMenu createCredentialMenu(Context context, CompromisedCredential credential,
PasswordCheckCoordinator.CredentialEventHandler credentialHandler) {
MVCListAdapter.ModelList menuItems = new MVCListAdapter.ModelList();
menuItems.add(
BasicListMenu.buildMenuListItem(org.chromium.chrome.R.string.remove, 0, 0, true));
ListMenu.Delegate delegate = (listModel) -> {
int textId = listModel.get(ListMenuItemProperties.TITLE_ID);
if (textId == org.chromium.chrome.R.string.remove) {
credentialHandler.onRemove(credential);
} else {
assert false : "No action defined for " + context.getString(textId);
}
};
return new BasicListMenu(context, menuItems, delegate);
}
}
......@@ -4,6 +4,8 @@
package org.chromium.chrome.browser.password_check;
import java.util.Objects;
/**
* This class holds the data used to represent a compromised credential in the Password Check
* settings screen.
......@@ -39,4 +41,18 @@ public class CompromisedCredential {
public boolean isPhished() {
return mPhished;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CompromisedCredential that = (CompromisedCredential) o;
return mPhished == that.mPhished && mUsername.equals(that.mUsername)
&& mPassword.equals(that.mPassword) && mOriginUrl.equals(that.mOriginUrl);
}
@Override
public int hashCode() {
return Objects.hash(mUsername, mPassword, mOriginUrl, mPhished);
}
}
......@@ -11,7 +11,7 @@ import android.content.Context;
* the compromised passwords and exposes actions that will help the users to make safer their
* credentials.
*/
public interface PasswordCheck {
public interface PasswordCheck extends PasswordCheckComponentUi.Delegate {
/**
* Initializes the PasswordCheck UI and launches it.
* @param context A {@link Context} to create views and retrieve resources.
......
......@@ -10,8 +10,20 @@ import android.view.MenuItem;
* This component is responsible for handling the UI logic for the password check.
*/
interface PasswordCheckComponentUi {
/**
* A delegate that handles native tasks for the UI component.
*/
interface Delegate {
/**
* Remove the given credential from the password store.
* @param credential A {@link CompromisedCredential}.
*/
void removeCredential(CompromisedCredential credential);
}
/**
* Handle the request of the user to show the help page for the Check Passwords view.
* @param item A {@link MenuItem}.
*/
public boolean handleHelp(MenuItem item);
boolean handleHelp(MenuItem item);
}
......@@ -4,17 +4,26 @@
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.withDecorView;
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.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
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.HeaderProperties.CHECK_STATUS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.ITEMS;
import static org.chromium.content_public.browser.test.util.CriteriaHelper.pollUiThread;
import static org.chromium.content_public.browser.test.util.TestThreadUtils.runOnUiThreadBlocking;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IdRes;
......@@ -34,8 +43,9 @@ import org.chromium.chrome.browser.password_check.PasswordCheckProperties.Header
import org.chromium.chrome.browser.password_check.internal.R;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.browser_ui.widget.listmenu.ListMenuButton;
import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.widget.ButtonCompat;
......@@ -57,6 +67,8 @@ public class PasswordCheckViewTest {
private PasswordCheckFragmentView mPasswordCheckView;
@Mock
private PasswordCheckComponentUi mComponentUi;
@Mock
private PasswordCheckCoordinator.CredentialEventHandler mMockHandler;
@Rule
public SettingsActivityTestRule<PasswordCheckFragmentView> mTestRule =
......@@ -70,7 +82,7 @@ public class PasswordCheckViewTest {
return mComponentUi;
});
mTestRule.startSettingsActivity();
TestThreadUtils.runOnUiThreadBlocking(() -> {
runOnUiThreadBlocking(() -> {
PasswordCheckCoordinator.setUpModelChangeProcessors(mModel, mPasswordCheckView);
});
}
......@@ -78,7 +90,7 @@ public class PasswordCheckViewTest {
@Test
@MediumTest
public void testDisplaysHeaderAndCredential() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
runOnUiThreadBlocking(() -> {
mModel.get(ITEMS).add(
new MVCListAdapter.ListItem(PasswordCheckProperties.ItemType.HEADER,
new PropertyModel.Builder(HeaderProperties.ALL_KEYS)
......@@ -103,7 +115,7 @@ public class PasswordCheckViewTest {
@Test
@MediumTest
public void testCrendentialDisplaysNameOriginAndReason() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
runOnUiThreadBlocking(() -> {
mModel.get(ITEMS).add(buildCredentialItem(PHISHED));
mModel.get(ITEMS).add(buildCredentialItem(LEAKED));
});
......@@ -122,11 +134,28 @@ public class PasswordCheckViewTest {
is(getString(R.string.password_check_credential_row_reason_leaked)));
}
private static MVCListAdapter.ListItem buildCredentialItem(CompromisedCredential credential) {
@Test
@MediumTest
public void testClickingDeleteInMoreMenuTriggersHandler() {
runOnUiThreadBlocking(() -> mModel.get(ITEMS).add(buildCredentialItem(ANA)));
pollUiThread(() -> Criteria.checkThat(getCredentials().getChildCount(), is(1)));
TouchCommon.singleClickView(getCredentialMoreButtonAt(0));
onView(withText(org.chromium.chrome.R.string.remove))
.inRoot(withDecorView(
not(is(mPasswordCheckView.getActivity().getWindow().getDecorView()))))
.perform(click());
verify(mMockHandler).onRemove(eq(ANA));
}
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(CREDENTIAL_HANDLER, mMockHandler)
.build());
}
......@@ -150,7 +179,7 @@ public class PasswordCheckViewTest {
return getCredentials().getChildAt(index).findViewById(R.id.credential_change_button);
}
private ImageView getCredentialMoreButtonAt(int index) {
private ListMenuButton getCredentialMoreButtonAt(int index) {
return getCredentials().getChildAt(index).findViewById(R.id.credential_menu_button);
}
......
......@@ -7,15 +7,20 @@ package org.chromium.chrome.browser.password_check;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.CheckStatus.SUCCESS;
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.HeaderProperties.CHECK_STATUS;
import static org.chromium.chrome.browser.password_check.PasswordCheckProperties.ITEMS;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.password_check.PasswordCheckProperties.ItemType;
......@@ -23,7 +28,7 @@ import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Arrays;
import java.util.Collections;
/**
* Controller tests verify that the PasswordCheck controller modifies the model if the API is used
......@@ -33,13 +38,19 @@ import java.util.Arrays;
public class PasswordCheckControllerTest {
private static final CompromisedCredential ANA =
new CompromisedCredential("https://m.a.xyz/", "Ana", "password", false);
private static final CompromisedCredential BOB =
new CompromisedCredential("https://www.b.ch/", "Baub", "DoneSth", true);
@Mock
private PasswordCheckComponentUi.Delegate mDelegate;
private final PasswordCheckMediator mMediator = new PasswordCheckMediator();
private final PropertyModel mModel = PasswordCheckProperties.createDefaultModel();
@Before
public void setUp() {
mMediator.initialize(mModel);
MockitoAnnotations.initMocks(this);
mMediator.initialize(mModel, mDelegate);
}
@Test
......@@ -49,11 +60,18 @@ public class PasswordCheckControllerTest {
@Test
public void testCreatesHeaderAndEntryForCredentials() {
mMediator.onCompromisedCredentialsAvailable(SUCCESS, Arrays.asList(ANA));
mMediator.onCompromisedCredentialsAvailable(SUCCESS, Collections.singletonList(ANA));
ListModel<MVCListAdapter.ListItem> itemList = mModel.get(ITEMS);
assertThat(itemList.get(0).type, is(ItemType.HEADER));
assertThat(itemList.get(0).model.get(CHECK_STATUS), is(SUCCESS));
assertThat(itemList.get(1).type, is(ItemType.COMPROMISED_CREDENTIAL));
assertThat(itemList.get(1).model.get(COMPROMISED_CREDENTIAL), is(ANA));
assertThat(itemList.get(1).model.get(CREDENTIAL_HANDLER), is(mMediator));
}
@Test
public void testRemovingElementTriggersDelegate() {
mMediator.onRemove(ANA);
verify(mDelegate).removeCredential(eq(ANA));
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment