Commit 33c5fe60 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Commit Bot

[Autofill Assistant] Add animations when setting carousel chips.

This CL adds animations when carousels chips are changed. When setting
the new list of chips, we compute the minimum set of operations (insert,
delete, substitute) required to transform the old list into the new one
(also known as Levenshtein Distance) and we apply those operations.

Animations themselves are automatically added by the RecyclerView
containing the chips.

Videos:
 - Before: http://go/aa-carousel-before
 - After: http://go/aa-carousel-after

Change-Id: Iaaf78e3a8280fead319dea338bd2a0eb5042aff6
Reviewed-on: https://chromium-review.googlesource.com/c/1491618
Commit-Queue: Jordan Demeulenaere <jdemeulenaere@chromium.org>
Reviewed-by: default avatarStephane Zermatten <szermatt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#636769}
parent 573ba3d1
...@@ -238,8 +238,14 @@ class AutofillAssistantUiController implements AssistantCoordinator.Delegate { ...@@ -238,8 +238,14 @@ class AutofillAssistantUiController implements AssistantCoordinator.Delegate {
} }
private void setChips(AssistantCarouselModel model, List<AssistantChip> chips) { private void setChips(AssistantCarouselModel model, List<AssistantChip> chips) {
model.getChipsModel().set(chips);
model.set(ALIGNMENT, computeAlignment(chips)); model.set(ALIGNMENT, computeAlignment(chips));
// We apply the minimum set of operations on the current chips to transform it in the target
// list of chips. When testing for chip equivalence, we only compare their type and text but
// all substitutions will still be applied so we are sure we display the given {@code chips}
// with their associated callbacks.
EditDistance.transform(model.getChipsModel(), chips,
(a, b) -> a.getType() == b.getType() && a.getText().equals(b.getText()));
} }
@CalledByNative @CalledByNative
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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.autofill_assistant;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.ui.modelutil.ListModel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The <a href="https://en.wikipedia.org/wiki/Edit_distance">edit distance</a> is a mean of
* quantifying the distance/cost of two sequences (e.g. words or lists) by computing the minimum
* number of operations required to transform a source sequence into a target sequence.
*
* <p>One of the most popular example is the <a
* href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein Distance</a> whose
* supported operations are item insertion, item deletion and item substitution.
*/
public final class EditDistance {
/**
* An {@link Equivalence} class to test for equivalence between 2 items of the same type.
*/
interface Equivalence<T> {
/** Return whether {@code a} and {@code b} are equivalent. */
boolean apply(T a, T b);
}
/**
* Apply the set of operations with minimum cost necessary to transform {@code source} into
* {@code target} using the sets of edit operations defined by Levenshtein (insertion, deletion
* and substitution).
*
* <p>Insertions and deletions always have a cost of 1. Substituting a value by another costs 0
* if both values are equivalent according to {@code equivalence}, and it costs 1 otherwise.
*/
public static <T> void transform(
ListModel<T> source, List<T> target, Equivalence<T> equivalence) {
applyOperations(source, target, computeOperations(source, target, equivalence));
}
/**
* Compute the minimum cost set of operations to perform on {@code source} to transform it into
* {@code target}. Elements are compared using {@code equivalence} when testing for equivalence.
*
* <p>The positions used in the resulting list of operations are relative to the {@code source}
* sequence. For instance, let's say we have:
* <pre>
* source = [1, 2, 3]
* target = [2, 4, 3]
* </pre>
*
* then a minimum cost set of operations is:
* <pre>
* [
* Substitution(index = 2, value = 3)], // cost = 0
* Insertion(index = 2, value = 4), // cost = 1 total cost = 2
* Deletion(index = 0) // cost = 1
* ]
* </pre>
*
* <p>Note that there can be multiple solutions and there is currently no warranty on which
* one will be returned. For instance, another minimal set of operations for this example could
* be:
* <pre>
* [
* Substitution(index = 0, value = 2), // cost = 1
* Substitution(index = 1, value = 4)], // cost = 1 total cost = 2
* Substitution(index = 2, value = 3) // cost = 0
* ]
* </pre>
*/
private static <T> List<Operation> computeOperations(
ListModel<T> source, List<T> target, Equivalence<T> equivalence) {
int n = source.size();
int m = target.size();
// cache[i][j] stores the Levenshtein distance between source[0;i[ and target[0;j[, as
// well as references to other cache entries and an operation (insert, substitute, delete)
// to be able to reconstruct the optimal set of operations to perform on source such that it
// is equal to target.
CacheEntry[][] cache = new CacheEntry[n + 1][m + 1];
// Transforming empty list into empty list costs 0.
cache[0][0] = new CacheEntry(0, null, null);
// Cache[i][0].cost is the cost of transforming source[0;i[ into the empty list.
for (int i = 1; i <= n; i++) {
cache[i][0] = new CacheEntry(i, Operation.deletion(i - 1), cache[i - 1][0]);
}
// Cache[0][j].cost is the cost of transforming the empty list into target[0;j[.
for (int j = 1; j <= m; j++) {
cache[0][j] = new CacheEntry(j, Operation.insertion(0, j - 1), cache[0][j - 1]);
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
T aValue = source.get(i - 1);
T bValue = target.get(j - 1);
// 1) Substitution.
CacheEntry previousEntry = cache[i - 1][j - 1];
Operation operation = Operation.substitution(i - 1, j - 1);
int substitutionCost = equivalence.apply(aValue, bValue) ? 0 : 1;
int cost = previousEntry.mCost + substitutionCost;
// 2) Deletion.
CacheEntry deletionEntry = cache[i - 1][j];
if (deletionEntry.mCost + 1 < cost) {
cost = deletionEntry.mCost + 1;
operation = Operation.deletion(i - 1);
previousEntry = deletionEntry;
}
// 3) Insertion.
CacheEntry insertionEntry = cache[i][j - 1];
if (insertionEntry.mCost + 1 < cost) {
cost = insertionEntry.mCost + 1;
operation = Operation.insertion(i, j - 1);
previousEntry = insertionEntry;
}
cache[i][j] = new CacheEntry(cost, operation, previousEntry);
}
}
// Return the list of operations.
ArrayList<Operation> result = new ArrayList<>();
CacheEntry current = cache[n][m];
while (current != null) {
if (current.mOperation != null) {
result.add(current.mOperation);
}
current = current.mPreviousEntry;
}
sortOperations(result);
// TODO(crbug.com/806868): We might want to merge some operations (e.g. insertion of items
// at the same index or removal of a range) instead of inserting and removing items one by
// one.
return result;
}
/**
* Sort the operations such that we can apply them in the resulting order.
*
* <p>The logic is the following:
* 1) Apply all substitutions first, in any order.
* 2) Apply insertions and deletions altogether, such that:
* - deletions and insertions at index i will be performed before deletions and insertions at
* index j if i > j.
* - deletions at index i will be performed insertions at index i.
* - insertion at index i of target[j] will be performed before insertion at index i of
* target[k] if j > k.
*/
private static void sortOperations(List<Operation> operations) {
Collections.sort(operations, (a, b) -> {
// We perform substitutions first.
int c = ApiCompatibilityUtils.compareBoolean(
b.mType == Operation.Type.SUBSTITUTION, a.mType == Operation.Type.SUBSTITUTION);
if (c != 0) {
return c;
}
// We apply deletions and insertions in decreasing index order.
c = Integer.compare(b.mSourceIndex, a.mSourceIndex);
if (c != 0) {
return c;
}
// When the indices are equal, we first perform the deletion at that index.
c = ApiCompatibilityUtils.compareBoolean(
b.mType == Operation.Type.DELETION, a.mType == Operation.Type.DELETION);
if (c != 0) {
return c;
}
// If we need to insert multiple values at the same index, we do it in decreasing
// order of target index.
return Integer.compare(b.mTargetIndex, b.mTargetIndex);
});
}
/**
* Apply operations on {@code source}.
*/
private static <T> void applyOperations(
ListModel<T> source, List<T> target, List<Operation> operations) {
for (int i = 0; i < operations.size(); i++) {
Operation operation = operations.get(i);
switch (operation.mType) {
case INSERTION:
source.add(operation.mSourceIndex, target.get(operation.mTargetIndex));
break;
case SUBSTITUTION:
source.update(operation.mSourceIndex, target.get(operation.mTargetIndex));
break;
case DELETION:
source.removeAt(operation.mSourceIndex);
break;
}
}
}
/** An operation that can be applied to a sequence. */
private static class Operation {
enum Type { INSERTION, SUBSTITUTION, DELETION }
/** The type of this operation. */
final Type mType;
/**
* An index from the source sequence that represents:
* - at which index a value should be inserted if mType == INSERTION.
* - at which index we should change the value if mType == SUBSTITUTION.
* - at which index a value should be deleted if mType == DELETION.
*/
final int mSourceIndex;
/**
* An index from the target sequence that represents:
* - the index of the value to insert in the source sequence if mType == INSERTION.
* - the index of the new value if mType == SUBSTITUTION.
* - (invalid index = -1) if mType == DELETION.
*/
final int mTargetIndex;
Operation(Type type, int sourceIndex, int targetIndex) {
this.mType = type;
this.mSourceIndex = sourceIndex;
this.mTargetIndex = targetIndex;
}
static Operation insertion(int sourceIndex, int targetIndex) {
return new Operation(Type.INSERTION, sourceIndex, targetIndex);
}
static Operation substitution(int sourceIndex, int targetIndex) {
return new Operation(Type.SUBSTITUTION, sourceIndex, targetIndex);
}
static Operation deletion(int sourceIndex) {
return new Operation(Type.DELETION, sourceIndex, -1);
}
}
/** Util class used to store the result of a DP sub problem in #computeOperations().*/
private static class CacheEntry {
final int mCost;
final Operation mOperation;
final CacheEntry mPreviousEntry;
CacheEntry(int cost, Operation operation, CacheEntry previousEntry) {
this.mCost = cost;
this.mOperation = operation;
this.mPreviousEntry = previousEntry;
}
}
}
...@@ -9,6 +9,7 @@ import android.graphics.Rect; ...@@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
...@@ -36,10 +37,6 @@ public class AssistantCarouselCoordinator { ...@@ -36,10 +37,6 @@ public class AssistantCarouselCoordinator {
mView = new RecyclerView(context); mView = new RecyclerView(context);
mView.setLayoutManager(mLayoutManager); mView.setLayoutManager(mLayoutManager);
mView.addItemDecoration(new SpaceItemDecoration(context)); mView.addItemDecoration(new SpaceItemDecoration(context));
mView.getItemAnimator().setAddDuration(0);
mView.getItemAnimator().setChangeDuration(0);
mView.getItemAnimator().setMoveDuration(0);
mView.getItemAnimator().setRemoveDuration(0);
mView.setAdapter(new RecyclerViewAdapter<>( mView.setAdapter(new RecyclerViewAdapter<>(
new SimpleRecyclerViewMcp<>(model.getChipsModel(), AssistantChip::getType, new SimpleRecyclerViewMcp<>(model.getChipsModel(), AssistantChip::getType,
AssistantChipViewHolder::bind), AssistantChipViewHolder::bind),
...@@ -125,7 +122,9 @@ public class AssistantCarouselCoordinator { ...@@ -125,7 +122,9 @@ public class AssistantCarouselCoordinator {
@Override @Override
public void getItemOffsets( public void getItemOffsets(
Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mCentered && parent.getAdapter().getItemCount() == 1) { // We use RecyclerView.State#getItemCount() as it returns the correct value when the
// carousel is being animated.
if (mCentered && state.getItemCount() == 1) {
// We have one view and want it horizontally centered. By this time the parent // We have one view and want it horizontally centered. By this time the parent
// measured width is correct (as it matches its parent), but we need to explicitly // measured width is correct (as it matches its parent), but we need to explicitly
// measure how big the chip view wants to be. // measure how big the chip view wants to be.
...@@ -133,7 +132,7 @@ public class AssistantCarouselCoordinator { ...@@ -133,7 +132,7 @@ public class AssistantCarouselCoordinator {
view.measure( view.measure(
View.MeasureSpec.makeMeasureSpec(availableWidth, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(availableWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec( View.MeasureSpec.makeMeasureSpec(
parent.getMeasuredHeight(), View.MeasureSpec.AT_MOST)); parent.getMeasuredHeight(), View.MeasureSpec.UNSPECIFIED));
int margin = (availableWidth - view.getMeasuredWidth()) / 2; int margin = (availableWidth - view.getMeasuredWidth()) / 2;
outRect.left = margin; outRect.left = margin;
...@@ -142,6 +141,18 @@ public class AssistantCarouselCoordinator { ...@@ -142,6 +141,18 @@ public class AssistantCarouselCoordinator {
} }
int position = parent.getChildAdapterPosition(view); int position = parent.getChildAdapterPosition(view);
// If old position != NO_POSITION, it means the carousel is being animated and we should
// use that position in our logic.
ViewHolder viewHolder = parent.getChildViewHolder(view);
if (viewHolder != null && viewHolder.getOldPosition() != RecyclerView.NO_POSITION) {
position = viewHolder.getOldPosition();
}
if (position == RecyclerView.NO_POSITION) {
return;
}
int left; int left;
int right; int right;
if (position == 0) { if (position == 0) {
...@@ -150,7 +161,7 @@ public class AssistantCarouselCoordinator { ...@@ -150,7 +161,7 @@ public class AssistantCarouselCoordinator {
left = mInnerSpacePx; left = mInnerSpacePx;
} }
if (position == parent.getAdapter().getItemCount() - 1) { if (position == state.getItemCount() - 1) {
right = mOuterSpacePx; right = mOuterSpacePx;
} else { } else {
right = mInnerSpacePx; right = mInnerSpacePx;
......
...@@ -149,6 +149,7 @@ chrome_java_sources = [ ...@@ -149,6 +149,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantMetrics.java", "java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantMetrics.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantPreferencesUtil.java", "java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantPreferencesUtil.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiController.java", "java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiController.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/EditDistance.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java", "java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselCoordinator.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselModel.java", "java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselModel.java",
...@@ -1941,6 +1942,7 @@ chrome_test_java_sources = [ ...@@ -1941,6 +1942,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewTest.java", "javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetModernViewTest.java", "javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetModernViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTest.java", "javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/EditDistanceTest.java",
"javatests/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java", "javatests/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkBridgeTest.java", "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkBridgeTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkModelTest.java", "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkModelTest.java",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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.autofill_assistant;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.ui.modelutil.ListModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Unit test suite for {@link EditDistance}.
*/
@RunWith(JUnit4.class)
public class EditDistanceTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Test
@SmallTest
public void testEmptySource() {
ListModel<Integer> spiedListModel = createSpiedListModel();
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(0, 3);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testEmptyTarget() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Collections.emptyList());
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testEmptySourceAndEmptyTarget() {
testTransformation(createSpiedListModel(), Collections.emptyList());
}
@Test
@SmallTest
public void testEqualSourceAndTarget() {
testTransformation(createSpiedListModel(1, 2, 3), Arrays.asList(1, 2, 3));
}
@Test
@SmallTest
public void testInsertBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testInsertMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(1, 2);
}
@Test
@SmallTest
public void testInsertEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(2, 3);
}
@Test
@SmallTest
public void testDeleteBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3));
verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testDeleteMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 3));
verify(spiedListModel).removeAt(1);
}
@Test
@SmallTest
public void testDeleteEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2));
verify(spiedListModel).removeAt(2);
}
@Test
@SmallTest
public void testSubstitutionBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(0, 1);
}
@Test
@SmallTest
public void testSubstitutionMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 0, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(1, 2);
}
@Test
@SmallTest
public void testSubstitutionEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(2, 3);
}
@Test
@SmallTest
public void testMultipleInsert() {
ListModel<Integer> spiedListModel = createSpiedListModel(3, 6);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(2, 8);
inOrder.verify(spiedListModel).add(2, 7);
inOrder.verify(spiedListModel).add(1, 5);
inOrder.verify(spiedListModel).add(1, 4);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testMultipleDelete() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6, 7, 8);
testTransformation(spiedListModel, Arrays.asList(3, 6));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(7);
inOrder.verify(spiedListModel).removeAt(6);
inOrder.verify(spiedListModel).removeAt(4);
inOrder.verify(spiedListModel).removeAt(3);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testMultipleSubstitutions() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 0, 3, 0, 0, 6, 0, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
verify(spiedListModel).update(0, 1);
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(6, 7);
verify(spiedListModel).update(7, 8);
}
@Test
@SmallTest
public void testPopAndAppend() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3, 4));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(3, 4);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testAppendAndPop() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(0, 1, 2));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).add(0, 0);
}
@Test
@SmallTest
public void testMixedOperations() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6);
testTransformation(spiedListModel, Arrays.asList(2, 8, 4, 5, 7, 6));
// 0-cost substitutions.
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(5, 6);
verify(spiedListModel).update(2, 8);
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(5, 7);
inOrder.verify(spiedListModel).removeAt(0);
}
private void testTransformation(ListModel<Integer> source, List<Integer> target) {
EditDistance.transform(source, target, Integer::equals);
List<Integer> sourceList = new ArrayList<>();
for (Integer value : source) {
sourceList.add(value);
}
Assert.assertEquals(target, sourceList);
}
private static ListModel<Integer> createSpiedListModel(Integer... values) {
ListModel<Integer> model = new ListModel<>();
model.set(values);
return Mockito.spy(model);
}
}
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