/*
* Copyright 2013 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package flow;
import android.content.Context;
import android.support.annotation.NonNull;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.MockitoAnnotations.initMocks;
public class FlowTest {
static class Uno {
}
static class Dos {
}
static class Tres {
}
@NotPersistent static class NoPersist extends TestKey {
NoPersist() {
super("NoPersist");
}
}
final TestKey able = new TestKey("Able");
final TestKey baker = new TestKey("Baker");
final TestKey charlie = new TestKey("Charlie");
final TestKey delta = new TestKey("Delta");
final TestKey noPersist = new NoPersist();
@Mock KeyManager keyManager;
History lastStack;
Direction lastDirection;
class FlowDispatcher implements Dispatcher {
@Override
public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {
lastStack = traversal.destination;
lastDirection = traversal.direction;
callback.onTraversalCompleted();
}
}
class AsyncDispatcher implements Dispatcher {
Traversal traversal;
TraversalCallback callback;
@Override
public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {
this.traversal = traversal;
this.callback = callback;
}
void fire() {
TraversalCallback oldCallback = callback;
callback = null;
traversal = null;
oldCallback.onTraversalCompleted();
;
}
void assertIdle() {
assertThat(callback).isNull();
assertThat(traversal).isNull();
}
void assertDispatching(Object newTop) {
assertThat(callback).isNotNull();
assertThat(traversal.destination.top()).isEqualTo(newTop);
}
}
@Before public void setUp() {
initMocks(this);
}
@Test public void oneTwoThree() {
History history = History.single(new Uno());
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
flow.set(new Dos());
assertThat(lastStack.top()).isInstanceOf(Dos.class);
assertThat(lastDirection).isSameAs(Direction.FORWARD);
flow.set(new Tres());
assertThat(lastStack.top()).isInstanceOf(Tres.class);
assertThat(lastDirection).isSameAs(Direction.FORWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isInstanceOf(Dos.class);
assertThat(lastDirection).isSameAs(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isInstanceOf(Uno.class);
assertThat(lastDirection).isSameAs(Direction.BACKWARD);
assertThat(flow.goBack()).isFalse();
}
@Test public void historyChangesAfterListenerCall() {
final History firstHistory = History.single(new Uno());
class Ourrobouros implements Dispatcher {
Flow flow = new Flow(keyManager, firstHistory);
{
flow.setDispatcher(this);
}
@Override
public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback onComplete) {
assertThat(firstHistory).hasSameSizeAs(flow.getHistory());
Iterator<Object> original = firstHistory.iterator();
for (Object o : flow.getHistory()) {
assertThat(o).isEqualTo(original.next());
}
onComplete.onTraversalCompleted();
}
}
Ourrobouros listener = new Ourrobouros();
listener.flow.set(new Dos());
}
@Test public void historyPushAllIsPushy() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, charlie)).build();
assertThat(history.size()).isEqualTo(3);
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(baker);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(able);
assertThat(flow.goBack()).isFalse();
}
@Test public void setHistoryWorks() {
History history = History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker)).build();
Flow flow = new Flow(keyManager, history);
FlowDispatcher dispatcher = new FlowDispatcher();
flow.setDispatcher(dispatcher);
History newHistory =
History.emptyBuilder().pushAll(Arrays.<Object>asList(charlie, delta)).build();
flow.setHistory(newHistory, Direction.FORWARD);
assertThat(lastDirection).isSameAs(Direction.FORWARD);
assertThat(lastStack.top()).isSameAs(delta);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isSameAs(charlie);
assertThat(flow.goBack()).isFalse();
}
@Test public void setObjectGoesBack() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, charlie, delta)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(4);
flow.set(charlie);
assertThat(lastStack.top()).isEqualTo(charlie);
assertThat(lastStack.size()).isEqualTo(3);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(baker);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(able);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isFalse();
}
@Test public void setObjectToMissingObjectPushes() {
History history = History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(2);
flow.set(charlie);
assertThat(lastStack.top()).isEqualTo(charlie);
assertThat(lastStack.size()).isEqualTo(3);
assertThat(lastDirection).isEqualTo(Direction.FORWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(baker);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(able);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isFalse();
}
@Test public void setObjectKeepsOriginal() {
History history = History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(2);
flow.set(new TestKey("Able"));
assertThat(lastStack.top()).isEqualTo(new TestKey("Able"));
assertThat(lastStack.top() == able).isTrue();
assertThat(lastStack.top()).isSameAs(able);
assertThat(lastStack.size()).isEqualTo(1);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
}
@Test public void replaceHistoryResultsInLengthOneHistory() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, charlie)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(3);
flow.replaceHistory(delta, Direction.REPLACE);
assertThat(lastStack.top()).isEqualTo(new TestKey("Delta"));
assertThat(lastStack.top() == delta).isTrue();
assertThat(lastStack.top()).isSameAs(delta);
assertThat(lastStack.size()).isEqualTo(1);
assertThat(lastDirection).isEqualTo(Direction.REPLACE);
}
@Test public void replaceTopDoesNotAlterHistoryLength() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, charlie)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(3);
flow.replaceTop(delta, Direction.REPLACE);
assertThat(lastStack.top()).isEqualTo(new TestKey("Delta"));
assertThat(lastStack.top() == delta).isTrue();
assertThat(lastStack.top()).isSameAs(delta);
assertThat(lastStack.size()).isEqualTo(3);
assertThat(lastDirection).isEqualTo(Direction.REPLACE);
}
@Test public void secondDispatcherIsBootstrapped() {
AsyncDispatcher firstDispatcher = new AsyncDispatcher();
History history = History.single(able);
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(firstDispatcher);
// Quick check that we bootstrapped (and test the test dispatcher).
firstDispatcher.assertDispatching(able);
firstDispatcher.fire();
firstDispatcher.assertIdle();
// No activity, dispatchers change. Maybe pause / resume. Maybe config change.
flow.removeDispatcher(firstDispatcher);
AsyncDispatcher secondDispatcher = new AsyncDispatcher();
flow.setDispatcher(secondDispatcher);
// New dispatcher is bootstrapped
secondDispatcher.assertDispatching(able);
secondDispatcher.fire();
secondDispatcher.assertIdle();
}
@Test public void hangingTraversalsSurviveDispatcherChange() {
AsyncDispatcher firstDispatcher = new AsyncDispatcher();
History history = History.single(able);
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(firstDispatcher);
firstDispatcher.fire();
// Start traversal to second screen.
flow.set(baker);
firstDispatcher.assertDispatching(baker);
// Dispatcher is removed before finishing baker--maybe it caused a configuration change.
flow.removeDispatcher(firstDispatcher);
// New dispatcher shows up, maybe from new activity after config change.
AsyncDispatcher secondDispatcher = new AsyncDispatcher();
flow.setDispatcher(secondDispatcher);
// New dispatcher is ignored until the in-progress baker traversal is done.
secondDispatcher.assertIdle();
// New dispatcher is bootstrapped with baker.
firstDispatcher.fire();
secondDispatcher.assertDispatching(baker);
// Confirm no redundant extra bootstrap traversals enqueued.
secondDispatcher.fire();
secondDispatcher.assertIdle();
}
@Test public void enqueuedTraversalsSurviveDispatcherChange() {
AsyncDispatcher firstDispatcher = new AsyncDispatcher();
History history = History.single(able);
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(firstDispatcher);
firstDispatcher.fire();
// Dispatcher is removed. Maybe we paused.
flow.removeDispatcher(firstDispatcher);
// A few traversals are enqueued because software.
flow.set(baker);
flow.set(charlie);
// New dispatcher shows up, we resumed.
AsyncDispatcher secondDispatcher = new AsyncDispatcher();
flow.setDispatcher(secondDispatcher);
// New dispatcher receives baker and charlie traversals and nothing else.
secondDispatcher.assertDispatching(baker);
secondDispatcher.fire();
secondDispatcher.assertDispatching(charlie);
secondDispatcher.fire();
secondDispatcher.assertIdle();
}
@SuppressWarnings({ "deprecation", "CheckResult" }) @Test public void setHistoryKeepsOriginals() {
TestKey able = new TestKey("Able");
TestKey baker = new TestKey("Baker");
TestKey charlie = new TestKey("Charlie");
TestKey delta = new TestKey("Delta");
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, charlie, delta)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(4);
TestKey echo = new TestKey("Echo");
TestKey foxtrot = new TestKey("Foxtrot");
History newHistory =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, baker, echo, foxtrot)).build();
flow.setHistory(newHistory, Direction.REPLACE);
assertThat(lastStack.size()).isEqualTo(4);
assertThat(lastStack.top()).isEqualTo(foxtrot);
flow.goBack();
assertThat(lastStack.size()).isEqualTo(3);
assertThat(lastStack.top()).isEqualTo(echo);
flow.goBack();
assertThat(lastStack.size()).isEqualTo(2);
assertThat(lastStack.top()).isSameAs(baker);
flow.goBack();
assertThat(lastStack.size()).isEqualTo(1);
assertThat(lastStack.top()).isSameAs(able);
}
static class Picky {
final String value;
Picky(String value) {
this.value = value;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Picky picky = (Picky) o;
return value.equals(picky.value);
}
@Override public int hashCode() {
return value.hashCode();
}
}
@Test public void setCallsEquals() {
History history = History.emptyBuilder()
.pushAll(Arrays.<Object>asList(new Picky("Able"), new Picky("Baker"), new Picky("Charlie"),
new Picky("Delta")))
.build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
assertThat(history.size()).isEqualTo(4);
flow.set(new Picky("Charlie"));
assertThat(lastStack.top()).isEqualTo(new Picky("Charlie"));
assertThat(lastStack.size()).isEqualTo(3);
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(new Picky("Baker"));
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isTrue();
assertThat(lastStack.top()).isEqualTo(new Picky("Able"));
assertThat(lastDirection).isEqualTo(Direction.BACKWARD);
assertThat(flow.goBack()).isFalse();
}
@Test public void incorrectFlowGetUsage() {
Context mockContext = Mockito.mock(Context.class);
//noinspection WrongConstant
Mockito.when(mockContext.getSystemService(Mockito.anyString())).thenReturn(null);
try {
Flow.get(mockContext);
fail("Flow was supposed to throw an exception on wrong usage");
} catch (IllegalStateException ignored) {
// That's good!
}
}
@Test public void defaultHistoryFilter() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, noPersist, charlie)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
List<Object> expected = History.emptyBuilder().pushAll(asList(able, charlie)).build().asList();
assertThat(flow.getFilteredHistory().asList()).isEqualTo(expected);
}
@Test public void customHistoryFilter() {
History history =
History.emptyBuilder().pushAll(Arrays.<Object>asList(able, noPersist, charlie)).build();
Flow flow = new Flow(keyManager, history);
flow.setDispatcher(new FlowDispatcher());
flow.setHistoryFilter(new HistoryFilter() {
@NonNull @Override public History scrubHistory(@NonNull History history) {
History.Builder builder = History.emptyBuilder();
final Iterator<Object> keys = history.reverseIterator();
while (keys.hasNext()) {
Object key = keys.next();
if (!key.equals(able)) {
builder.push(key);
}
}
return builder.build();
}
});
List<Object> expected =
History.emptyBuilder().pushAll(asList(noPersist, charlie)).build().asList();
assertThat(flow.getFilteredHistory().asList()).isEqualTo(expected);
}
}