// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content_public.browser.ViewEventSink.InternalAccessDelegate;
import org.chromium.content_public.browser.test.util.WebContentsUtils;
import org.chromium.content_shell_apk.ContentShellActivityTestRule;
import org.chromium.content_shell_apk.ContentShellActivityTestRule.RerunWithUpdatedContainerView;
/** Tests that we can scroll and fling a ContentView running inside ContentShell. */
@RunWith(BaseJUnit4ClassRunner.class)
public class ContentViewScrollingTest {
@Rule
public ContentShellActivityTestRule mActivityTestRule = new ContentShellActivityTestRule();
private static final String LARGE_PAGE =
UrlUtils.encodeHtmlDataUri(
"<html><head><meta name=\"viewport\" content=\"width=device-width,"
+ " initial-scale=2.0, maximum-scale=2.0\" /><style>body { width: 5000px;"
+ " height: 5000px; }</style></head><body>Lorem ipsum dolor sit amet,"
+ " consectetur adipiscing elit.</body></html>");
/**
* InternalAccessDelegate to ensure AccessibilityEvent notifications (Eg:TYPE_VIEW_SCROLLED)
* are being sent properly on scrolling a page.
*/
static class TestInternalAccessDelegate implements InternalAccessDelegate {
private boolean mScrollChanged;
private final Object mLock = new Object();
@Override
public boolean super_onKeyUp(int keyCode, KeyEvent event) {
return false;
}
@Override
public boolean super_dispatchKeyEvent(KeyEvent event) {
return false;
}
@Override
public boolean super_onGenericMotionEvent(MotionEvent event) {
return false;
}
@Override
public void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix) {
synchronized (mLock) {
mScrollChanged = true;
}
}
/**
* @return Whether OnScrollChanged() has been called.
*/
public boolean isScrollChanged() {
synchronized (mLock) {
return mScrollChanged;
}
}
}
private RenderCoordinatesImpl mCoordinates;
private void waitForScroll(final boolean hugLeft, final boolean hugTop) {
CriteriaHelper.pollInstrumentationThread(
() -> {
// Scrolling and flinging don't result in exact coordinates.
final int minThreshold = 5;
final int maxThreshold = 100;
if (hugLeft) {
Criteria.checkThat(
mCoordinates.getScrollXPixInt(), Matchers.lessThan(minThreshold));
} else {
Criteria.checkThat(
mCoordinates.getScrollXPixInt(),
Matchers.greaterThan(maxThreshold));
}
if (hugTop) {
Criteria.checkThat(
mCoordinates.getScrollYPixInt(), Matchers.lessThan(minThreshold));
} else {
Criteria.checkThat(
mCoordinates.getScrollYPixInt(),
Matchers.greaterThan(maxThreshold));
}
});
}
private void waitForScrollToPosition(final int x, final int y) {
CriteriaHelper.pollInstrumentationThread(
() -> {
// Scrolling and flinging don't result in exact coordinates.
final int threshold = 5;
Criteria.checkThat(
mCoordinates.getScrollXPixInt(),
Matchers.allOf(
Matchers.lessThan(x + threshold),
Matchers.greaterThan(x - threshold)));
Criteria.checkThat(
mCoordinates.getScrollYPixInt(),
Matchers.allOf(
Matchers.lessThan(y + threshold),
Matchers.greaterThan(y - threshold)));
});
}
private void waitForViewportInitialization() {
CriteriaHelper.pollInstrumentationThread(
() -> {
Criteria.checkThat(
mCoordinates.getLastFrameViewportWidthPixInt(), Matchers.not(0));
});
}
private void fling(final int vx, final int vy) {
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
mActivityTestRule
.getWebContents()
.getEventForwarder()
.startFling(
SystemClock.uptimeMillis(), vx, vy, false, true);
}
});
}
private void scrollTo(final int x, final int y) {
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
mActivityTestRule.getContainerView().scrollTo(x, y);
}
});
}
private void scrollBy(final int dx, final int dy) {
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
mActivityTestRule.getContainerView().scrollBy(dx, dy);
}
});
}
private void scrollWithJoystick(final float deltaAxisX, final float deltaAxisY) {
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
// Synthesize joystick motion event and send to content layer.
MotionEvent leftJoystickMotionEvent =
MotionEvent.obtain(
0,
SystemClock.uptimeMillis(),
MotionEvent.ACTION_MOVE,
deltaAxisX,
deltaAxisY,
0);
leftJoystickMotionEvent.setSource(
leftJoystickMotionEvent.getSource()
| InputDevice.SOURCE_CLASS_JOYSTICK);
mActivityTestRule
.getContainerView()
.onGenericMotionEvent(leftJoystickMotionEvent);
}
});
}
@Before
public void setUp() {
mActivityTestRule.launchContentShellWithUrl(LARGE_PAGE);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
mCoordinates = mActivityTestRule.getRenderCoordinates();
WebContentsUtils.reportAllFrameSubmissions(mActivityTestRule.getWebContents(), true);
waitForViewportInitialization();
Assert.assertEquals(0, mCoordinates.getScrollXPixInt());
Assert.assertEquals(0, mCoordinates.getScrollYPixInt());
}
@Test
@SmallTest
@Feature({"Main"})
@DisabledTest(message = "Test is flaky. crbug.com/1058233")
public void testFling() {
// Scaling the initial velocity by the device scale factor ensures that
// it's of sufficient magnitude for all displays densities.
float deviceScaleFactor =
InstrumentationRegistry.getInstrumentation()
.getTargetContext()
.getResources()
.getDisplayMetrics()
.density;
int velocity = (int) (1000 * deviceScaleFactor);
// Vertical fling to lower-left.
fling(0, -velocity);
waitForScroll(true, false);
// Horizontal fling to lower-right.
fling(-velocity, 0);
waitForScroll(false, false);
// Vertical fling to upper-right.
fling(0, velocity);
waitForScroll(false, true);
// Horizontal fling to top-left.
fling(velocity, 0);
waitForScroll(true, true);
// Diagonal fling to bottom-right.
fling(-velocity, -velocity);
waitForScroll(false, false);
}
@Test
@SmallTest
@Feature({"Main"})
@DisabledTest(message = "Test is flaky. crbug.com/1132544")
public void testFlingDistance() {
// Scaling the initial velocity by the device scale factor ensures that
// it's of sufficient magnitude for all displays densities.
float deviceScaleFactor =
InstrumentationRegistry.getInstrumentation()
.getTargetContext()
.getResources()
.getDisplayMetrics()
.density;
int velocity = (int) (1000 * deviceScaleFactor);
// Expected total fling distance calculated by FlingCurve with initial
// velocity 1000.
int expected_dist = (int) (194 * deviceScaleFactor);
// Vertical fling to lower-left.
fling(0, -velocity);
waitForScrollToPosition(0, expected_dist);
// Horizontal fling to lower-right.
fling(-velocity, 0);
waitForScrollToPosition(expected_dist, expected_dist);
// Vertical fling to upper-right.
fling(0, velocity);
waitForScrollToPosition(expected_dist, 0);
// Horizontal fling to top-left.
fling(velocity, 0);
waitForScrollToPosition(0, 0);
}
@Test
@SmallTest
@RerunWithUpdatedContainerView
@Feature({"Main"})
public void testScrollTo() {
// Vertical scroll to lower-left.
scrollTo(0, 2500);
waitForScroll(true, false);
// Horizontal scroll to lower-right.
scrollTo(2500, 2500);
waitForScroll(false, false);
// Vertical scroll to upper-right.
scrollTo(2500, 0);
waitForScroll(false, true);
// Horizontal scroll to top-left.
scrollTo(0, 0);
waitForScroll(true, true);
// Diagonal scroll to bottom-right.
scrollTo(2500, 2500);
waitForScroll(false, false);
}
@Test
@SmallTest
@RerunWithUpdatedContainerView
@Feature({"Main"})
public void testScrollBy() {
scrollTo(0, 0);
waitForScroll(true, true);
// No scroll
scrollBy(0, 0);
waitForScroll(true, true);
// Vertical scroll to lower-left.
scrollBy(0, 2500);
waitForScroll(true, false);
// Horizontal scroll to lower-right.
scrollBy(2500, 0);
waitForScroll(false, false);
// Vertical scroll to upper-right.
scrollBy(0, -2500);
waitForScroll(false, true);
// Horizontal scroll to top-left.
scrollBy(-2500, 0);
waitForScroll(true, true);
// Diagonal scroll to bottom-right.
scrollBy(2500, 2500);
waitForScroll(false, false);
}
@Test
@SmallTest
@Feature({"Main"})
public void testJoystickScroll() {
scrollTo(0, 0);
waitForScroll(true, true);
// Scroll with X axis in deadzone and the Y axis active.
// Only the Y axis should have an effect, arriving at lower-left.
scrollWithJoystick(0.1f, 1f);
waitForScroll(true, false);
// Scroll with Y axis in deadzone and the X axis active.
scrollWithJoystick(1f, -0.1f);
waitForScroll(false, false);
// Vertical scroll to upper-right.
scrollWithJoystick(0, -0.75f);
waitForScroll(false, true);
// Horizontal scroll to top-left.
scrollWithJoystick(-0.75f, 0);
waitForScroll(true, true);
// Diagonal scroll to bottom-right.
scrollWithJoystick(1f, 1f);
waitForScroll(false, false);
}
/**
* To ensure the device properly responds to bounds-exceeding scrolls, e.g., overscroll
* effects are properly initialized.
*/
@Test
@SmallTest
@RerunWithUpdatedContainerView
@Feature({"Main"})
public void testOverScroll() {
// Overscroll lower-left.
scrollTo(-10000, 10000);
waitForScroll(true, false);
// Overscroll lower-right.
scrollTo(10000, 10000);
waitForScroll(false, false);
// Overscroll upper-right.
scrollTo(10000, -10000);
waitForScroll(false, true);
// Overscroll top-left.
scrollTo(-10000, -10000);
waitForScroll(true, true);
// Diagonal overscroll lower-right.
scrollTo(10000, 10000);
waitForScroll(false, false);
}
/**
* To ensure the AccessibilityEvent notifications (Eg:TYPE_VIEW_SCROLLED) are being sent
* properly on scrolling a page.
*/
@Test
@SmallTest
@RerunWithUpdatedContainerView
@Feature({"Main"})
public void testOnScrollChanged() {
final int scrollToX = mCoordinates.getScrollXPixInt() + 2500;
final int scrollToY = mCoordinates.getScrollYPixInt() + 2500;
final TestInternalAccessDelegate accessDelegate = new TestInternalAccessDelegate();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
new Runnable() {
@Override
public void run() {
mActivityTestRule
.getViewEventSink()
.setAccessDelegate(accessDelegate);
}
});
scrollTo(scrollToX, scrollToY);
waitForScroll(false, false);
CriteriaHelper.pollInstrumentationThread(accessDelegate::isScrollChanged);
}
}