blob: 6110b2620c8c4ae02d2a5670498dec692ad6c534 [file] [log] [blame]
// Copyright 2018 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.chromecast.base;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.BlockJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for behavior specific to Controller.
*/
@RunWith(BlockJUnit4ClassRunner.class)
public class ControllerTest {
// Convenience method to create a scope that mutates a list of strings on state transitions.
// When entering the state, it will append "enter ${id} ${data}" to the result list, where
// `data` is the String that is associated with the state activation. When exiting the state,
// it will append "exit ${id}" to the result list. This provides a readable way to track and
// verify the behavior of observers in response to the Observables they are linked to.
public static <T> Observer<T> report(List<String> result, String id) {
// Did you know that lambdas are awesome.
return (T data) -> {
result.add("enter " + id + ": " + data);
return () -> result.add("exit " + id);
};
}
@Test
public void testNoStateTransitionAfterRegisteringWithInactiveController() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
recorder.verify().end();
}
@Test
public void testStateIsopenedWhenControllerIsSet() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
// Activate the state by setting the controller.
controller.set("cool");
recorder.verify().opened("cool").end();
}
@Test
public void testBasicStateFromController() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
controller.set("fun");
// Deactivate the state by resetting the controller.
controller.reset();
recorder.verify().opened("fun").closed("fun").end();
}
@Test
public void testSetStateTwicePerformsImplicitReset() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
// Activate the state for the first time.
controller.set("first");
// Activate the state for the second time.
controller.set("second");
// If set() is called without a reset() in-between, the tracking state exits, then re-enters
// with the new data. So we expect to find an "exit" call between the two enter calls.
recorder.verify().opened("first").closed("first").opened("second").end();
}
@Test
public void testResetWhileStateIsNotopenedIsNoOp() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
controller.reset();
recorder.verify().end();
}
@Test
public void testMultipleStatesObservingSingleController() {
// Construct two states that subscribe the same Controller. Verify both observers' events
// are triggered.
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder1 = ReactiveRecorder.record(controller);
ReactiveRecorder recorder2 = ReactiveRecorder.record(controller);
// Activate the controller, which should propagate a state transition to both states.
// Both states should be updated, so we should get two enter events.
controller.set("neat");
controller.reset();
recorder1.verify().opened("neat").closed("neat").end();
recorder2.verify().opened("neat").closed("neat").end();
}
@Test
public void testNewStateIsActivatedImmediatelyIfObservingAlreadyActiveObservable() {
Controller<String> controller = new Controller<>();
controller.set("surprise");
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
recorder.verify().opened("surprise").end();
}
@Test
public void testNewStateIsNotActivatedIfObservingObservableThatHasBeenDeactivated() {
Controller<String> controller = new Controller<>();
controller.set("surprise");
controller.reset();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
recorder.verify().end();
}
@Test
public void testResetWhileAlreadyDeactivatedIsANoOp() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
controller.set("radical");
controller.reset();
// Resetting again after already resetting should not notify the observer.
controller.reset();
recorder.verify().opened("radical").closed("radical").end();
}
@Test
public void testClosedSubscriptionDoesNotGetNotifiedOfFutureActivations() {
Controller<String> a = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(a);
a.set("during temp");
a.reset();
recorder.unsubscribe();
a.set("after temp");
recorder.verify().opened("during temp").closed("during temp").end();
}
@Test
public void testClosedSubscriptionIsImplicitlyDeactivated() {
Controller<String> a = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(a);
a.set("implicitly reset this");
recorder.unsubscribe();
recorder.verify().opened("implicitly reset this").closed("implicitly reset this").end();
}
@Test
public void testCloseSubscriptionAfterDeactivatingSourceStateDoesNotCallExitHAndlerAgain() {
Controller<String> a = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(a);
a.set("and a one");
a.reset();
recorder.unsubscribe();
recorder.verify().opened("and a one").closed("and a one").end();
}
@Test
public void testSetControllerWithNullImplicitlyResets() {
Controller<String> a = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(a);
a.set("not null");
a.set(null);
recorder.verify().opened("not null").closed("not null").end();
}
@Test
public void testResetControllerInActivationHandler() {
Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>();
a.subscribe((String s) -> {
result.add("enter " + s);
a.reset();
result.add("after reset");
return () -> {
result.add("exit");
};
});
a.set("immediately retracted");
assertThat(result, contains("enter immediately retracted", "after reset", "exit"));
}
@Test
public void testSetControllerInActivationHandler() {
Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>();
a.subscribe(report(result, "weirdness"));
a.subscribe((String s) -> {
// If the activation handler always calls set() on the source controller, you will have
// an infinite loop, which is not cool. However, if the activation handler only
// conditionally calls set() on its source controller, then the case where set() is not
// called will break the loop. It is the responsibility of the programmer to solve the
// halting problem for activation handlers.
if (s.equals("first")) {
a.set("second");
}
return () -> {
result.add("haha");
};
});
a.set("first");
assertThat(result,
contains("enter weirdness: first", "haha", "exit weirdness",
"enter weirdness: second"));
}
@Test
public void testResetControllerInDeactivationHandler() {
Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>();
a.subscribe(report(result, "bizzareness"));
a.subscribe((String s) -> () -> a.reset());
a.set("yo");
a.reset();
// The reset() called by the deactivation handler should be a no-op.
assertThat(result, contains("enter bizzareness: yo", "exit bizzareness"));
}
@Test
public void testSetControllerInDeactivationHandler() {
Controller<String> a = new Controller<>();
List<String> result = new ArrayList<>();
a.subscribe(report(result, "astoundingness"));
a.subscribe((String s) -> () -> a.set("never mind"));
a.set("retract this");
a.reset();
// The set() called by the deactivation handler should immediately set the controller back.
assertThat(result,
contains("enter astoundingness: retract this", "exit astoundingness",
"enter astoundingness: never mind"));
}
@Test
public void testSetWithDuplicateValueIsNoOp() {
Controller<String> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
controller.set("stop copying me");
controller.set("stop copying me");
recorder.verify().opened("stop copying me").end();
}
@Test
public void testSetUnitControllerInActivatedStateIsNoOp() {
Controller<Unit> controller = new Controller<>();
ReactiveRecorder recorder = ReactiveRecorder.record(controller);
controller.set(Unit.unit());
recorder.verify().opened(Unit.unit()).end();
controller.set(Unit.unit());
recorder.verify().end();
}
}