blob: eaf07f94ce7df0bd6edcd91a4212507fab02087a [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 android.contentcaptureservice.cts;
import static android.contentcaptureservice.cts.Assertions.assertSessionId;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.ContentCaptureSessionId;
import android.view.contentcapture.ViewNode;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Used to assert Content Capture events by the event order.
*
* When the type is the same, the assertor will assert the content of the event.
* If the current event is different type, this event will be skipped, and then
* try to assert the next event. Through all events, the assertor will report an
* "not found" error.
*/
public class EventsAssertor {
private static final String TAG = "CtsContentCapture";
private final List<ContentCaptureEvent> mEvents;
private int mNextEvent = 0;
public EventsAssertor(@NonNull List<ContentCaptureEvent> events) {
mEvents = Collections.unmodifiableList(events);
}
@NonNull
public EventsAssertor isAtLeast(int size) {
assertThat(mEvents.size()).isAtLeast(size);
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_SESSION_RESUMED} event.
*/
@NonNull
public EventsAssertor assertSessionResumed() {
assertNextEvent((event) -> assertSessionLevelEvent(event),
ContentCaptureEvent.TYPE_SESSION_RESUMED, "no SESSION_RESUMED event");
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_SESSION_PAUSED} event.
*/
@NonNull
public EventsAssertor assertSessionPaused() {
assertNextEvent((event) -> assertSessionLevelEvent(event),
ContentCaptureEvent.TYPE_SESSION_PAUSED, "no SESSION_PAUSED event");
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_TREE_APPEARING} event.
*/
@NonNull
public EventsAssertor assertViewTreeStarted() {
assertNextEvent((event) -> assertSessionLevelEvent(event),
ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING, "no VIEW_TREE_APPEARING event");
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_TREE_APPEARED} event.
*/
@NonNull
public EventsAssertor assertViewTreeFinished() {
assertNextEvent((event) -> assertSessionLevelEvent(event),
ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED,
"no VIEW_TREE_APPEARED event");
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED}
* event for a decor view.
*
* <P>The decor view is typically internal, so there isn't much we can assert,
* other than its autofill id.
*/
@NonNull
public EventsAssertor assertDecorViewAppeared(@NonNull View expectedDecorView) {
assertNextEvent((event) -> assertEventId(event, expectedDecorView),
ContentCaptureEvent.TYPE_VIEW_APPEARED,
"no VIEW_APPEARED event for decor view");
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED}
* event, without checking for parent id.
*/
@NonNull
public EventsAssertor assertViewAppeared(@NonNull View expectedView) {
assertNextEvent((event) -> assertViewEvent(event, expectedView),
ContentCaptureEvent.TYPE_VIEW_APPEARED,
String.format("no VIEW_APPEARED event for %s", expectedView));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED} event.
*/
@NonNull
public EventsAssertor assertViewAppeared(@NonNull View expectedView,
@NonNull AutofillId expectedParentId) {
assertNextEvent((event) -> assertViewEvent(event, expectedView, expectedParentId),
ContentCaptureEvent.TYPE_VIEW_APPEARED,
String.format("no VIEW_APPEARED event for %s", expectedView));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED} event.
*/
@NonNull
public EventsAssertor assertViewAppeared(@NonNull ContentCaptureSessionId expectedSessionId,
@NonNull View expectedView, @NonNull AutofillId expectedParentId) {
assertViewAppeared(expectedView, expectedParentId);
assertSessionId(expectedSessionId, expectedView);
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED} event.
*/
@NonNull
public EventsAssertor assertViewAppeared(@NonNull ContentCaptureSessionId expectedSessionId,
@NonNull View expectedView) {
assertViewAppeared(expectedView);
assertSessionId(expectedSessionId, expectedView);
return this;
}
/**
* Asserts the {@code text} of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED}
* event.
*/
@NonNull
public EventsAssertor assertViewAppeared(@NonNull String text) {
assertNextEvent((event) -> assertEventText(event, text),
ContentCaptureEvent.TYPE_VIEW_APPEARED,
String.format("no TYPE_VIEW_APPEARED event with text: %s", text));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_DISAPPEARED}
* event for multiple views.
*/
@NonNull
public EventsAssertor assertViewDisappeared(@NonNull AutofillId autofillId) {
assertNextEvent((event) -> assertDisappearedEvent(event, autofillId),
ContentCaptureEvent.TYPE_VIEW_DISAPPEARED,
String.format("no VIEW_DISAPPEARED event for %s", autofillId));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_DISAPPEARED}
* event for multiple views.
*/
@NonNull
public EventsAssertor assertViewDisappeared(@NonNull AutofillId... autofillIds) {
assertNextEvent((event) -> assertDisappearedEvent(event, autofillIds),
ContentCaptureEvent.TYPE_VIEW_DISAPPEARED,
String.format("no VIEW_DISAPPEARED event for %s", Arrays.toString(autofillIds)));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_TEXT_CHANGED} event.
*/
@NonNull
public EventsAssertor assertViewTextChanged(@NonNull AutofillId expectedId,
@NonNull String expectedText) {
assertNextEvent((event) -> assertTextChangedEvent(event, expectedId, expectedText),
ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED,
String.format("no VIEW_TEXT_CHANGED event for %s", expectedId));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_APPEARED}
* event for a virtual node.
*/
@NonNull
public EventsAssertor assertVirtualViewAppeared(@NonNull ContentCaptureSession session,
@NonNull AutofillId parentId, int childId, String expectedText) {
final AutofillId expectedId = session.newAutofillId(parentId, childId);
assertNextEvent((event) -> assertVirtualViewEvent(event, expectedId, expectedText),
ContentCaptureEvent.TYPE_VIEW_APPEARED,
String.format("no VIEW_APPEARED event for %s", expectedId.toString()));
return this;
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_DISAPPEARED}
* event for a virtual node.
*/
@NonNull
public EventsAssertor assertVirtualViewDisappeared(AutofillId parentId,
ContentCaptureSession session, int childId) {
return assertViewDisappeared(session.newAutofillId(parentId, childId));
}
/**
* Asserts the contents of a {@link ContentCaptureEvent#TYPE_VIEW_DISAPPEARED}
* event for many virtual nodes.
*/
@NonNull
public EventsAssertor assertVirtualViewsDisappeared(AutofillId parentId,
ContentCaptureSession session, int... childId) {
final AutofillId[] ids = new AutofillId[childId.length];
for (int i = 0; i < childId.length; i++) {
ids[i] = session.newAutofillId(parentId, childId[i]);
}
return assertViewDisappeared(ids);
}
@Nullable
private String assertVirtualViewEvent(@NonNull ContentCaptureEvent event,
@NonNull AutofillId expectedId, @Nullable String expectedText) {
final ViewNode node = event.getViewNode();
assertThat(node).isNotNull();
if (!node.getAutofillId().equals(expectedId)) {
return String.format("wrong autofill id (expected %s, actual is %s) at %s",
expectedId, node.getAutofillId(), event);
}
if (expectedText != null && node.getText() != null) {
assertWithMessage("wrong text on %s", event)
.that(node.getText().toString()).isEqualTo(expectedText);
} else {
assertWithMessage("%s should not have text", node)
.that(node.getText()).isNull();
}
return null;
}
@Nullable
private String assertDisappearedEvent(@NonNull ContentCaptureEvent event,
@NonNull AutofillId expectedId) {
assertCommonViewDisappearedProperties(event);
assertWithMessage("wrong autofillId on event %s", event)
.that(event.getId()).isEqualTo(expectedId);
assertWithMessage("event %s should not have autofillIds", event)
.that(event.getIds()).isNull();
return null;
}
@Nullable
private String assertDisappearedEvent(@NonNull ContentCaptureEvent event,
@NonNull AutofillId[] expectedIds) {
final List<AutofillId> ids = event.getIds();
assertCommonViewDisappearedProperties(event);
assertWithMessage("no autofillIds on event %s", event).that(ids)
.isNotNull();
assertWithMessage("wrong autofillId on event %s", event)
.that(ids).containsExactly((Object[]) expectedIds).inOrder();
assertWithMessage("event %s should not have autofillId", event)
.that(event.getId()).isNull();
return null;
}
@Nullable
private String assertTextChangedEvent(@NonNull ContentCaptureEvent event,
@NonNull AutofillId expectedId, @NonNull String expectedText) {
assertWithMessage("Wrong id on %s", event).that(event.getId())
.isEqualTo(expectedId);
assertWithMessage("Wrong text on %s", event).that(event.getText().toString())
.isEqualTo(expectedText);
return null;
}
private void assertCommonViewDisappearedProperties(@NonNull ContentCaptureEvent event) {
assertWithMessage("event %s should not have a ViewNode", event)
.that(event.getViewNode()).isNull();
assertWithMessage("event %s should not have text", event)
.that(event.getText()).isNull();
}
@Nullable
private String assertViewEvent(@NonNull ContentCaptureEvent event, @NonNull View expectedView) {
return assertViewEvent(event, expectedView, /* expectedParentId */ null);
}
@Nullable
private String assertViewEvent(@NonNull ContentCaptureEvent event, @NonNull View expectedView,
@Nullable AutofillId expectedParentId) {
assertEvent(event, expectedView, expectedParentId, /* expectedText */ null);
return null;
}
@Nullable
private String assertViewEvent(@NonNull ContentCaptureEvent event, @NonNull View expectedView,
AutofillId expectedParentId, String expectedText) {
assertEvent(event, expectedView, expectedParentId, expectedText);
return null;
}
private String assertEventId(@NonNull ContentCaptureEvent event, View expectedView) {
final ViewNode node = event.getViewNode();
if (node == null) {
return String.format("node is null at %s", event);
}
if (expectedView != null && !node.getAutofillId().equals(expectedView.getAutofillId())) {
return String.format("wrong event id (expected %s, actual is %s) at %s",
expectedView.getAutofillId(), node.getAutofillId(), event);
}
return null;
}
private String assertEventText(@NonNull ContentCaptureEvent event, String expectedText) {
final ViewNode node = event.getViewNode();
if (node == null) {
return String.format("node is null at %s", event);
}
if (!TextUtils.equals(node.getText(), expectedText)) {
return String.format("wrong event text (expected %s, actual is %s) at %s",
expectedText, node.getText(), event);
}
return null;
}
private void assertEvent(@NonNull ContentCaptureEvent event, @Nullable View expectedView,
@Nullable AutofillId expectedParentId, @Nullable String expectedText) {
final ViewNode node = event.getViewNode();
assertThat(node).isNotNull();
assertWithMessage("wrong class on %s", event).that(node.getClassName())
.isEqualTo(expectedView.getClass().getName());
assertWithMessage("wrong autofill id on %s", event).that(node.getAutofillId())
.isEqualTo(expectedView.getAutofillId());
if (expectedParentId != null) {
assertWithMessage("wrong parent autofill id on %s", event)
.that(node.getParentAutofillId()).isEqualTo(expectedParentId);
}
if (expectedText != null) {
assertWithMessage("wrong text on %s", event).that(node.getText().toString())
.isEqualTo(expectedText);
}
}
@Nullable
private String assertSessionLevelEvent(@NonNull ContentCaptureEvent event) {
assertWithMessage("event %s should not have a ViewNode", event)
.that(event.getViewNode()).isNull();
assertWithMessage("event %s should not have text", event)
.that(event.getText()).isNull();
assertWithMessage("event %s should not have an autofillId", event)
.that(event.getId()).isNull();
assertWithMessage("event %s should not have autofillIds", event)
.that(event.getIds()).isNull();
return null;
}
private void assertNextEvent(@NonNull EventAssertion assertion, int expectedType,
@NonNull String errorFormat, @Nullable Object... errorArgs) {
if (mNextEvent >= mEvents.size()) {
throw new AssertionError("Reached the end of the events: "
+ String.format(errorFormat, errorArgs) + "\n. Events("
+ mEvents.size() + "): " + mEvents);
}
do {
final int index = mNextEvent++;
final ContentCaptureEvent event = mEvents.get(index);
if (event.getType() != expectedType) {
Log.w(TAG, "assertNextEvent(): ignoring event #" + index + "(" + event + ")");
continue;
}
// If returns an error message from getErrorMessage(), means the error can be ignored.
// The test will assert nex event.
// If directly throws AssertionError or exception, means the error cannot be ignored.
// The test will stop immediately.
final String error = assertion.getErrorMessage(event);
if (error == null) {
Log.d(TAG, "assertNextEvent(): match event in #" + index + ".");
return;
}
Log.w(TAG, "assertNextEvent(): ignoring event #" + index + "(" + event + "): "
+ error);
} while (mNextEvent < mEvents.size());
throw new AssertionError(String.format(errorFormat, errorArgs) + "\n. Events("
+ mEvents.size() + "): " + mEvents);
}
private interface EventAssertion {
@Nullable
String getErrorMessage(@NonNull ContentCaptureEvent event);
}
}