blob: a5aa2382bccf89054db6c42f6fcbda6018c3a10e [file] [log] [blame]
// 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.chromoting;
import android.graphics.PointF;
import org.junit.Assert;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chromoting.jni.TouchEventData;
import java.util.Arrays;
import java.util.LinkedList;
/**
* A mock implementation of {@link InputInjector} for testing purpose.
*/
public final class MockInputStub extends Assert implements InputStub {
/** The base class to store an event, which represents an user activity. */
public abstract static class Event<T> {
public void assertEventEquals(T other) {
if (other == this) {
return;
}
if (other == null) {
fail();
}
assertContentMatch(other);
}
protected abstract void assertContentMatch(T other);
}
/** A mouse event. */
public static final class MouseEvent extends Event<MouseEvent> {
private final int mX;
private final int mY;
private final int mButton;
private final boolean mDown;
public MouseEvent(int x, int y, int button, boolean down) {
mX = x;
mY = y;
mButton = button;
mDown = down;
}
@Override
protected void assertContentMatch(MouseEvent other) {
assertEquals(mX, other.mX);
assertEquals(mY, other.mY);
assertEquals(mButton, other.mButton);
assertEquals(mDown, other.mDown);
}
}
/** A mouse wheel event. */
public static final class WheelEvent extends Event<WheelEvent> {
private final int mDeltaX;
private final int mDeltaY;
public WheelEvent(int deltaX, int deltaY) {
mDeltaX = deltaX;
mDeltaY = deltaY;
}
@Override
protected void assertContentMatch(WheelEvent other) {
assertEquals(mDeltaX, other.mDeltaX);
assertEquals(mDeltaY, other.mDeltaY);
}
}
/** A keyboard event. */
public static final class KeyEvent extends Event<KeyEvent> {
private final int mScanCode;
private final int mKeyCode;
private final boolean mKeyDown;
public KeyEvent(int scanCode, int keyCode, boolean keyDown) {
mScanCode = scanCode;
mKeyCode = keyCode;
mKeyDown = keyDown;
}
@Override
protected void assertContentMatch(KeyEvent other) {
assertEquals(mScanCode, other.mScanCode);
assertEquals(mKeyCode, other.mKeyCode);
assertEquals(mKeyDown, other.mKeyDown);
}
}
/** A text event. */
public static final class TextEvent extends Event<TextEvent> {
public final String mText;
public TextEvent(String text) {
mText = text;
}
@Override
protected void assertContentMatch(TextEvent other) {
if (mText == null && other.mText == null) {
return;
}
if (mText == null || other.mText == null) {
fail();
}
assertEquals(mText, other.mText);
}
}
/** A touch event. */
public static final class TouchEvent extends Event<TouchEvent> {
/**
* A random number to represent an invalid touch id for {@link assertContentMatch}.
* Consumers can use this number to ignore the comparison of
* {@link TouchEventData#getTouchPointId()} when comparing.
*/
public static final int INVALID_ID = -883;
/**
* A random number to represent an invalid position for {@link #assertContentMatch}.
* Consumers can use this number to ignore certain float fields in an {@link TouchEventData}
* when comparing.
*/
public static final float INVALID_POSITION = -763.273f;
/**
* A number to represent an invalid angle in radians for {@link #assertContentMatch}.
* Consumers can use this number to ignore the comparison of
* {@link TouchEventData#getTouchPointAngle} when comparing.
*/
public static final float INVALID_RADIANS = 6.289f * (float) Math.PI;
// TouchEventData stores degrees instead of radians
private static final float INVALID_DEGREES = (float) Math.toDegrees(INVALID_RADIANS);
private final TouchEventData.EventType mEventType;
private final TouchEventData[] mData;
public TouchEvent(TouchEventData.EventType eventType, TouchEventData[] data) {
mEventType = eventType;
mData = (data == null ? null : Arrays.copyOf(data, data.length));
}
private static boolean idMatch(int left, int right) {
return left == right || left == INVALID_ID || right == INVALID_ID;
}
private static boolean positionMatch(float left, float right) {
return left == right || left == INVALID_POSITION || right == INVALID_POSITION;
}
private static boolean degreeMatch(float left, float right) {
return left == right || left == INVALID_DEGREES || right == INVALID_DEGREES;
}
private static void assertContentMatch(TouchEventData left, TouchEventData right) {
assertTrue(idMatch(left.getTouchPointId(), right.getTouchPointId()));
assertTrue(positionMatch(left.getTouchPointX(), right.getTouchPointX()));
assertTrue(positionMatch(left.getTouchPointY(), right.getTouchPointY()));
assertTrue(positionMatch(left.getTouchPointRadiusX(), right.getTouchPointRadiusX()));
assertTrue(positionMatch(left.getTouchPointRadiusY(), right.getTouchPointRadiusY()));
assertTrue(degreeMatch(left.getTouchPointAngle(), right.getTouchPointAngle()));
assertTrue(positionMatch(left.getTouchPointPressure(), right.getTouchPointPressure()));
}
private static void assertDataEquals(TouchEventData left, TouchEventData right) {
if (left == null && right == null) {
return;
}
if (left == null || right == null) {
fail();
}
assertContentMatch(left, right);
}
private int dataSize() {
return mData == null ? 0 : mData.length;
}
@Override
protected void assertContentMatch(TouchEvent other) {
assertEquals(mEventType, other.mEventType);
// An event with an empty mData array will match an event with a non-empty mData array.
// This is useful for tests which expect a TouchEvent with a certain EventType, but
// don't care about the content of mData.
if (dataSize() != 0 && other.dataSize() != 0) {
assertEquals(dataSize(), other.dataSize());
for (int i = 0; i < dataSize(); i++) {
assertDataEquals(mData[i], other.mData[i]);
}
}
}
}
private final LinkedList<MouseEvent> mMouseEvents;
private final LinkedList<WheelEvent> mWheelEvents;
private final LinkedList<KeyEvent> mKeyEvents;
private final LinkedList<TextEvent> mTextEvents;
private final LinkedList<TouchEvent> mTouchEvents;
public MockInputStub() {
mMouseEvents = new LinkedList<>();
mWheelEvents = new LinkedList<>();
mKeyEvents = new LinkedList<>();
mTextEvents = new LinkedList<>();
mTouchEvents = new LinkedList<>();
}
/**
* Compares the first |right|.length events with the ones in |left|, asserts they are equal, and
* consumes these events from |left|.
*/
private static <E extends Event<E>> void assertContains(
LinkedList<E> left, E[] right) {
if (left == null && right == null) {
return;
}
if (left == null || right == null) {
fail();
}
assertTrue("left.size() " + left.size() + " != right.length " + right.length,
left.size() >= right.length);
for (int i = 0; i < right.length; i++) {
E leftEvent = left.removeFirst();
if (leftEvent == null) {
assertNull(right[i]);
} else {
leftEvent.assertEventEquals(right[i]);
}
}
}
public void clear() {
mMouseEvents.clear();
mWheelEvents.clear();
mKeyEvents.clear();
mTextEvents.clear();
mTouchEvents.clear();
}
/**
* Compares the first |other|.length events with {@link #mMouseEvents} in this instance,
* asserts they are equal, and consumes these events in this instance.
*/
public void assertContainsMouseEvents(MouseEvent[] other) {
assertContains(mMouseEvents, other);
}
/**
* Compares the first |other|.length events with {@link #mTouchEvents} in this instance,
* asserts they are equal, and consumes these events in this instance.
*/
public void assertContainsTouchEvents(TouchEvent[] other) {
assertContains(mTouchEvents, other);
}
/** Asserts current instance is empty. */
public void assertEmpty() {
assertTrue(mMouseEvents.isEmpty());
assertTrue(mWheelEvents.isEmpty());
assertTrue(mKeyEvents.isEmpty());
assertTrue(mTextEvents.isEmpty());
assertTrue(mTouchEvents.isEmpty());
}
/**
* Checks whether the first two events in {@link #mTouchEvents} are representing a down and
* an up action at specified position |x|, |y|, and consumes these events.
*/
public void assertTapInjected(float x, float y) {
assertTouchEventInjected(TouchEventData.EventType.TOUCH_EVENT_START, x, y);
assertTouchEventInjected(TouchEventData.EventType.TOUCH_EVENT_END, x, y);
}
/**
* Checks whether the first event in {@link #mTouchEvents} is representing an event with type
* |eventType| and at specified position |x|, |y|, and consumes this event.
*/
public void assertTouchEventInjected(TouchEventData.EventType eventType, float x, float y) {
assertContainsTouchEvents(new TouchEvent[] {
new TouchEventBuilder()
.withEventType(eventType)
.withX(x)
.withY(y)
.append()
.build(),
});
}
/**
* Checks whether the first event in {@link #mTouchEvents} is representing an event with type
* |eventType|, and consumes this event.
*/
public void assertTouchEventInjected(TouchEventData.EventType eventType) {
assertContainsTouchEvents(new TouchEvent[] {
new TouchEventBuilder().withEventType(eventType).build(),
});
}
/**
* Checks whether the first two events in {@link #mMouseEvents} are representing a down and an
* up action with |button| at specified position |x|, |y|, and consumes these events.
*/
public void assertClickInjected(int button, int x, int y) {
assertContainsMouseEvents(new MouseEvent[] {
new MouseEvent(x, y, button, true), new MouseEvent(x, y, button, false),
});
}
public void assertTouchMoveEventInjected(
PointF[] initPositions, int stepX, int stepY, int moveCount) {
LinkedList<TouchEvent> events = new LinkedList<>();
assertTrue(initPositions != null && initPositions.length > 0);
for (int i = 0; i < initPositions.length; i++) {
assertNotNull(initPositions[i]);
events.add(new TouchEventBuilder()
.withEventType(TouchEventData.EventType.TOUCH_EVENT_START)
.withX(initPositions[i].x)
.withY(initPositions[i].y)
.append()
.build());
}
// These tests send a single event for each finger that is moved. E.g. if we are injecting
// a pan event with two fingers, then every two TouchEvents represents one 'frame' of the
// motion sequence. Here we determine which iteration this event represents so we can use
// that to accurately compare the locations with the expected values.
for (int i = 0; i < moveCount; i++) {
for (int j = 0; j < initPositions.length; j++) {
TouchEventBuilder builder = new TouchEventBuilder();
builder.withEventType(TouchEventData.EventType.TOUCH_EVENT_MOVE);
for (int k = 0; k < initPositions.length; k++) {
// We inject one finger at a time which means that finger will have the correct
// value but other fingers may still be stepSize behind it.
int currentStep = i - 1;
if (j >= k) {
currentStep++;
}
if (currentStep < 0) {
currentStep = 0;
}
builder.withX(initPositions[k].x + currentStep * stepX)
.withY(initPositions[k].y + currentStep * stepY)
.append();
}
events.add(builder.build());
}
}
assertContainsTouchEvents(events.toArray(new TouchEvent[0]));
}
/**
* Checks whether the first two events in {@link #mMouseEvents} are representing a down and an
* up action with right button at specified position |x|, |y|, and consumes these events.
*/
public void assertRightClickInjected(int x, int y) {
assertClickInjected(BUTTON_RIGHT, x, y);
}
// ---------------- Implementations of InputInjector ----------------------
@Override
public void sendMouseEvent(int x, int y, int whichButton, boolean buttonDown) {
mMouseEvents.add(new MouseEvent(x, y, whichButton, buttonDown));
}
@Override
public void sendMouseWheelEvent(int deltaX, int deltaY) {
mWheelEvents.add(new WheelEvent(deltaX, deltaY));
}
@Override
public boolean sendKeyEvent(int scanCode, int keyCode, boolean keyDown) {
mKeyEvents.add(new KeyEvent(scanCode, keyCode, keyDown));
// Note: This implementation is not consistent with jni.Client, which may return false when
// scanCode and keyCode cannot be mapped to a usb key code.
return true;
}
@Override
public void sendTextEvent(String text) {
mTextEvents.add(new TextEvent(text));
}
@SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY")
@Override
public void sendTouchEvent(TouchEventData.EventType eventType, TouchEventData[] data) {
assertNotNull(data);
assertTrue(data.length != 0);
for (int i = 0; i < data.length; i++) {
assertTrue(data[i].getTouchPointId() != TouchEvent.INVALID_ID);
assertTrue(data[i].getTouchPointX() != TouchEvent.INVALID_POSITION);
assertTrue(data[i].getTouchPointY() != TouchEvent.INVALID_POSITION);
assertTrue(data[i].getTouchPointRadiusX() != TouchEvent.INVALID_POSITION);
assertTrue(data[i].getTouchPointRadiusY() != TouchEvent.INVALID_POSITION);
assertTrue(data[i].getTouchPointAngle() != TouchEvent.INVALID_DEGREES);
assertTrue(data[i].getTouchPointPressure() != TouchEvent.INVALID_POSITION);
}
mTouchEvents.add(new TouchEvent(eventType, data));
}
}