diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Grip.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Grip.java index 758573f8e6d8..b2dfab0253e9 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Grip.java +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Grip.java @@ -23,7 +23,7 @@ import java.util.Set; * Provide methods for interacting with grips, including unpacking grips into Java * objects. */ -/* package */ final class Grip extends Actor { +public class Grip extends Actor { private static final class Cache extends HashMap { } @@ -159,6 +159,22 @@ import java.util.Set; } } + private static final class LongString { + private final int mLength; + private final String mInitial; + + public LongString(final int length, final @Nullable String initial) { + mLength = length; + mInitial = (initial != null && !initial.isEmpty()) ? initial.substring(0, 50) : null; + } + + @Override + public String toString() { + return String.format("[String(%d)]%s", mLength, + (mInitial != null) ? "(" + mInitial + "\u2026)" : ""); + } + } + /** * Unpack a received grip value into a Java object. The grip can be either a primitive * value, or a JSONObject that represents a live object on the server. @@ -166,8 +182,8 @@ import java.util.Set; * @param connection Connection associated with this grip. * @param value Grip value received from the server. */ - public static Object unpack(final RDPConnection connection, - final Object value) { + /* package */ static Object unpack(final @NonNull RDPConnection connection, + final @Nullable Object value) { return unpackGrip(new Cache(), connection, value); } @@ -181,7 +197,8 @@ import java.util.Set; } final JSONObject obj = (JSONObject) value; - switch (obj.optString("type")) { + final String type = obj.optString("type"); + switch (type) { case "null": case "undefined": return null; @@ -193,10 +210,12 @@ import java.util.Set; return Double.NaN; case "-0": return -0.0; + case "longString": + return new LongString(obj.optInt("length"), obj.optString("initial")); case "object": break; default: - throw new IllegalArgumentException(); + throw new IllegalArgumentException(String.valueOf(type)); } final String actor = obj.optString("actor", null); @@ -213,6 +232,10 @@ import java.util.Set; final Function output = new Function(userDisplayName); cache.put(actor, output); return output; + } else if ("Promise".equals(cls)) { + final Promise output = new Promise(connection, obj); + cache.put(actor, output); + return output; } final JSONObject preview = obj.optJSONObject("preview"); @@ -249,7 +272,7 @@ import java.util.Set; } }; - private Grip(final RDPConnection connection, final JSONObject grip) { + /* package */ Grip(final @NonNull RDPConnection connection, final @NonNull JSONObject grip) { super(connection, grip); } diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promise.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promise.java new file mode 100644 index 000000000000..5e6a6478f4af --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promise.java @@ -0,0 +1,111 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview.test.rdp; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.json.JSONObject; + +/** + * Object to represent a Promise object returned by JS. + */ +public final class Promise extends Grip { + private JSONObject mGrip; + private String mState; + private Object mValue; + private Object mReason; + + /* package */ Promise(final @NonNull RDPConnection connection, final @NonNull JSONObject grip) { + super(connection, grip); + setPromiseState(grip); + } + + /* package */ void setPromiseState(final @NonNull JSONObject grip) { + mGrip = grip; + + final JSONObject state = grip.optJSONObject("promiseState"); + mState = state.optString("state"); + if (isFulfilled()) { + mValue = Grip.unpack(connection, state.opt("value")); + } else if (isRejected()) { + mReason = Grip.unpack(connection, state.opt("reason")); + } + } + + /** + * Return whether this promise is pending. + * + * @return True if this promise is pending. + */ + public boolean isPending() { + return "pending".equals(mState); + } + + /** + * Return whether this promise is fulfilled. + * + * @return True if this promise is fulfilled. + */ + public boolean isFulfilled() { + return "fulfilled".equals(mState); + } + + /** + * Return the promise value, assuming it is fulfilled. + * + * @return Promise value. + */ + public @Nullable Object getValue() { + return mValue; + } + + /** + * Return whether this promise is rejected. + * + * @return True if this promise is rejected. + */ + public boolean isRejected() { + return "rejected".equals(mState); + } + + /** + * Return the promise reason, assuming it is rejected. + * + * @return Promise reason. + */ + public @Nullable Object getReason() { + return mReason; + } + + /** + * Get the value of a property in this promise object. + * + * @return Value or null if there is no such property. + */ + public @Nullable Object getProperty(final @NonNull String name) { + final JSONObject preview = mGrip.optJSONObject("preview"); + if (preview == null) { + return null; + } + final JSONObject ownProperties = preview.optJSONObject("ownProperties"); + if (ownProperties == null) { + return null; + } + final JSONObject prop = ownProperties.optJSONObject(name); + if (prop == null) { + return null; + } + return Grip.unpack(connection, prop.opt("value")); + } + + @Override + public String toString() { + return "[Promise(" + mState + ")]" + + (isFulfilled() ? "(" + mValue + ')' : "") + + (isRejected() ? "(" + mReason + ')' : ""); + } +} diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promises.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promises.java new file mode 100644 index 000000000000..bb7485d45a26 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Promises.java @@ -0,0 +1,100 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview.test.rdp; + +import android.support.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Provide access to the promises API. + */ +public final class Promises extends Actor { + private final ReplyParser PROMISE_LIST_PARSER = new ReplyParser() { + @Override + public boolean canParse(@NonNull JSONObject packet) { + return packet.has("promises"); + } + + @Override + public @NonNull Promise[] parse(@NonNull JSONObject packet) { + return getPromisesFromArray(packet.optJSONArray("promises"), + /* canCreate */ true); + } + }; + + private final Set mPromises = new HashSet<>(); + + /* package */ Promises(final RDPConnection connection, final String name) { + super(connection, name); + attach(); + } + + /** + * Attach to the promises API. + */ + private void attach() { + sendPacket("{\"type\":\"attach\"}", JSON_PARSER).get(); + } + + /** + * Detach from the promises API. + */ + public void detach() { + for (final Promise promise : mPromises) { + promise.release(); + } + sendPacket("{\"type\":\"detach\"}", JSON_PARSER).get(); + } + + /* package */ Promise[] getPromisesFromArray(final @NonNull JSONArray array, + final boolean canCreate) { + final Promise[] promises = new Promise[array.length()]; + for (int i = 0; i < promises.length; i++) { + final JSONObject grip = array.optJSONObject(i); + final Promise promise = (Promise) connection.getActor(grip); + if (promise != null) { + promise.setPromiseState(grip); + promises[i] = promise; + } else if (canCreate) { + promises[i] = new Promise(connection, grip); + } + } + return promises; + } + + /** + * Return a list of live promises. + * + * @returns List of promises. + */ + public @NonNull Promise[] listPromises() { + final Promise[] promises = sendPacket("{\"type\":\"listPromises\"}", + PROMISE_LIST_PARSER).get(); + mPromises.addAll(Arrays.asList(promises)); + return promises; + } + + @Override + protected void onPacket(final @NonNull JSONObject packet) { + final String type = packet.optString("type", null); + if ("new-promises".equals(type)) { + // We always call listPromises() to get updated Promises, + // so that means we shouldn't handle "new-promises" here. + } else if ("promises-settled".equals(type)) { + // getPromisesFromArray will update states for us. + getPromisesFromArray(packet.optJSONArray("data"), + /* canCreate */ false); + } else { + super.onPacket(packet); + } + } +} diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Tab.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Tab.java index c6596bb5d843..078c438ec6f3 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Tab.java +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rdp/Tab.java @@ -64,4 +64,15 @@ public final class Tab extends Actor { final Actor console = connection.getActor(name); return (console != null) ? (Console) console : new Console(connection, name); } + + /** + * Get the promises object for access to the promises API. + * + * @return Promises object. + */ + public Promises getPromises() { + final String name = mTab.optString("promisesActor", null); + final Actor promises = connection.getActor(name); + return (promises != null) ? (Promises) promises : new Promises(connection, name); + } }