| // Copyright 2016 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.base; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.shadows.ShadowLooper; |
| |
| import org.chromium.base.Promise.UnhandledRejectionException; |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| |
| /** Unit tests for {@link Promise}. */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE) |
| public class PromiseTest { |
| // We need a simple mutable reference type for testing. |
| private static class Value { |
| private int mValue; |
| |
| public int get() { |
| return mValue; |
| } |
| |
| public void set(int value) { |
| mValue = value; |
| } |
| } |
| |
| /** Tests that the callback is called on fulfillment. */ |
| @Test |
| public void callback() { |
| final Value value = new Value(); |
| |
| Promise<Integer> promise = new Promise<Integer>(); |
| promise.then(PromiseTest.<Integer>setValue(value, 1)); |
| |
| assertEquals(value.get(), 0); |
| |
| promise.fulfill(new Integer(1)); |
| assertEquals(value.get(), 1); |
| } |
| |
| /** Tests that multiple callbacks are called. */ |
| @Test |
| public void multipleCallbacks() { |
| final Value value = new Value(); |
| |
| Promise<Integer> promise = new Promise<Integer>(); |
| Callback<Integer> callback = new Callback<Integer>() { |
| @Override |
| public void onResult(Integer result) { |
| value.set(value.get() + 1); |
| } |
| }; |
| promise.then(callback); |
| promise.then(callback); |
| |
| assertEquals(value.get(), 0); |
| |
| promise.fulfill(new Integer(0)); |
| assertEquals(value.get(), 2); |
| } |
| |
| /** Tests that a callback is called immediately when given to a fulfilled Promise. */ |
| @Test |
| public void callbackOnFulfilled() { |
| final Value value = new Value(); |
| |
| Promise<Integer> promise = Promise.fulfilled(new Integer(0)); |
| assertEquals(value.get(), 0); |
| |
| promise.then(PromiseTest.<Integer>setValue(value, 1)); |
| |
| assertEquals(value.get(), 1); |
| } |
| |
| /** Tests that promises can chain synchronous functions correctly. */ |
| @Test |
| public void promiseChaining() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| final Value value = new Value(); |
| |
| promise.then(new Promise.Function<Integer, String>(){ |
| @Override |
| public String apply(Integer arg) { |
| return arg.toString(); |
| } |
| }).then(new Promise.Function<String, String>(){ |
| @Override |
| public String apply(String arg) { |
| return arg + arg; |
| } |
| }).then(new Callback<String>() { |
| @Override |
| public void onResult(String result) { |
| value.set(result.length()); |
| } |
| }); |
| |
| promise.fulfill(new Integer(123)); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(6, value.get()); |
| } |
| |
| /** Tests that promises can chain asynchronous functions correctly. */ |
| @Test |
| public void promiseChainingAsyncFunctions() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| final Value value = new Value(); |
| |
| final Promise<String> innerPromise = new Promise<String>(); |
| |
| promise.then(new Promise.AsyncFunction<Integer, String>() { |
| @Override |
| public Promise<String> apply(Integer arg) { |
| return innerPromise; |
| } |
| }).then(new Callback<String>(){ |
| @Override |
| public void onResult(String result) { |
| value.set(result.length()); |
| } |
| }); |
| |
| assertEquals(0, value.get()); |
| |
| promise.fulfill(5); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(0, value.get()); |
| |
| innerPromise.fulfill("abc"); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(3, value.get()); |
| } |
| |
| /** Tests that a Promise that does not use its result does not throw on rejection. */ |
| @Test |
| public void rejectPromiseNoCallbacks() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| |
| boolean caught = false; |
| try { |
| promise.reject(); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| } catch (UnhandledRejectionException e) { |
| caught = true; |
| } |
| assertFalse(caught); |
| } |
| |
| /** Tests that a Promise that uses its result throws on rejection if it has no handler. */ |
| @Test |
| public void rejectPromiseNoHandler() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| promise.then(PromiseTest.<Integer>identity()).then(PromiseTest.<Integer>pass()); |
| |
| boolean caught = false; |
| try { |
| promise.reject(); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| } catch (UnhandledRejectionException e) { |
| caught = true; |
| } |
| assertTrue(caught); |
| } |
| |
| /** Tests that a Promise that handles rejection does not throw on rejection. */ |
| @Test |
| public void rejectPromiseHandled() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| promise.then(PromiseTest.<Integer>identity()) |
| .then(PromiseTest.<Integer>pass(), PromiseTest.<Exception>pass()); |
| |
| boolean caught = false; |
| try { |
| promise.reject(); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| } catch (UnhandledRejectionException e) { |
| caught = true; |
| } |
| assertFalse(caught); |
| } |
| |
| /** Tests that rejections carry the exception information. */ |
| @Test |
| public void rejectionInformation() { |
| Promise<Integer> promise = new Promise<Integer>(); |
| promise.then(PromiseTest.<Integer>pass()); |
| |
| String message = "Promise Test"; |
| try { |
| promise.reject(new NegativeArraySizeException(message)); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| fail(); |
| } catch (UnhandledRejectionException e) { |
| assertTrue(e.getCause() instanceof NegativeArraySizeException); |
| assertEquals(e.getCause().getMessage(), message); |
| } |
| } |
| |
| /** Tests that rejections propagate. */ |
| @Test |
| public void rejectionChaining() { |
| final Value value = new Value(); |
| Promise<Integer> promise = new Promise<Integer>(); |
| |
| Promise<Integer> result = |
| promise.then(PromiseTest.<Integer>identity()).then(PromiseTest.<Integer>identity()); |
| |
| result.then(PromiseTest.<Integer>pass(), PromiseTest.<Exception>setValue(value, 5)); |
| |
| promise.reject(new Exception()); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| |
| assertEquals(value.get(), 5); |
| assertTrue(result.isRejected()); |
| } |
| |
| /** Tests that Promises get rejected if a Function throws. */ |
| @Test |
| public void rejectOnThrow() { |
| Value value = new Value(); |
| Promise<Integer> promise = new Promise<Integer>(); |
| promise.then(new Promise.Function<Integer, Integer>() { |
| @Override |
| public Integer apply(Integer argument) { |
| throw new IllegalArgumentException(); |
| } |
| }).then(PromiseTest.<Integer>pass(), PromiseTest.<Exception>setValue(value, 5)); |
| |
| promise.fulfill(0); |
| |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(value.get(), 5); |
| } |
| |
| /** Tests that Promises get rejected if an AsyncFunction throws. */ |
| @Test |
| public void rejectOnAsyncThrow() { |
| Value value = new Value(); |
| Promise<Integer> promise = new Promise<Integer>(); |
| |
| promise.then(new Promise.AsyncFunction<Integer, Integer>() { |
| @Override |
| public Promise<Integer> apply(Integer argument) { |
| throw new IllegalArgumentException(); |
| } |
| }).then(PromiseTest.<Integer>pass(), PromiseTest.<Exception>setValue(value, 5)); |
| |
| promise.fulfill(0); |
| |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(value.get(), 5); |
| } |
| |
| /** Tests that Promises get rejected if an AsyncFunction rejects. */ |
| @Test |
| public void rejectOnAsyncReject() { |
| Value value = new Value(); |
| Promise<Integer> promise = new Promise<Integer>(); |
| final Promise<Integer> inner = new Promise<Integer>(); |
| |
| promise.then(new Promise.AsyncFunction<Integer, Integer>() { |
| @Override |
| public Promise<Integer> apply(Integer argument) { |
| return inner; |
| } |
| }).then(PromiseTest.<Integer>pass(), PromiseTest.<Exception>setValue(value, 5)); |
| |
| promise.fulfill(0); |
| |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(value.get(), 0); |
| |
| inner.reject(); |
| |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| assertEquals(value.get(), 5); |
| } |
| |
| /** Convenience method that returns a Callback that does nothing with its result. */ |
| private static <T> Callback<T> pass() { |
| return new Callback<T>() { |
| @Override |
| public void onResult(T result) {} |
| }; |
| } |
| |
| /** Convenience method that returns a Function that just passes through its argument. */ |
| private static <T> Promise.Function<T, T> identity() { |
| return new Promise.Function<T, T>() { |
| @Override |
| public T apply(T argument) { |
| return argument; |
| } |
| }; |
| } |
| |
| /** Convenience method that returns a Callback that sets the given Value on execution. */ |
| private static <T> Callback<T> setValue(final Value toSet, final int value) { |
| return new Callback<T>() { |
| @Override |
| public void onResult(T result) { |
| toSet.set(value); |
| } |
| }; |
| } |
| } |