blob: 7c322c3b9fc6f45b2c3b96d43ab4d438db0a6f80 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/bind.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/input/synthetic_touchpad_pinch_gesture.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/common/input/synthetic_pinch_gesture_params.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
namespace content {
namespace {
const char kTouchpadPinchDataURL[] =
"data:text/html;charset=utf-8,"
"<!DOCTYPE html>"
"<style>"
"html,body {"
" height: 100%;"
"}"
"</style>"
"<p>Hello.</p>"
"<script>"
" var resolveHandlerPromise = null;"
" var handlerPromise = new Promise(function(resolve) {"
" resolveHandlerPromise = resolve;"
" });"
" function preventPinchListener(e) {"
" e.preventDefault();"
" resolveHandlerPromise(e);"
" }"
" function allowPinchListener(e) {"
" resolveHandlerPromise(e);"
" }"
" function setListener(prevent) {"
" document.body.addEventListener("
" 'wheel',"
" (prevent ? preventPinchListener : allowPinchListener),"
" {passive: false});"
" }"
" function reset() {"
" document.body.removeEventListener("
" 'wheel', preventPinchListener, {passive: false});"
" document.body.removeEventListener("
" 'wheel', allowPinchListener, {passive: false});"
" handlerPromise = new Promise(function(resolve) {"
" resolveHandlerPromise = resolve;"
" });"
" }"
"</script>";
void PerformTouchpadPinch(WebContents* web_contents,
gfx::PointF position,
float scale_factor) {
RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From(
web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget());
SyntheticPinchGestureParams params;
params.gesture_source_type =
content::mojom::GestureSourceType::kTouchpadInput;
params.scale_factor = scale_factor;
params.anchor = position;
auto pinch_gesture = std::make_unique<SyntheticTouchpadPinchGesture>(params);
base::RunLoop run_loop;
widget_host->QueueSyntheticGesture(
std::move(pinch_gesture),
base::BindOnce(
[](base::OnceClosure quit_closure, SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
std::move(quit_closure).Run();
},
run_loop.QuitClosure()));
run_loop.Run();
}
} // namespace
class TouchpadPinchBrowserTest : public ContentBrowserTest,
public testing::WithParamInterface<bool> {
public:
TouchpadPinchBrowserTest() {
if (GetParam()) {
scoped_feature_list_.InitAndEnableFeature(
features::kTouchpadAsyncPinchEvents);
} else {
scoped_feature_list_.InitAndDisableFeature(
features::kTouchpadAsyncPinchEvents);
}
}
TouchpadPinchBrowserTest(const TouchpadPinchBrowserTest&) = delete;
TouchpadPinchBrowserTest& operator=(const TouchpadPinchBrowserTest&) = delete;
~TouchpadPinchBrowserTest() override = default;
protected:
void LoadURL() {
const GURL data_url(kTouchpadPinchDataURL);
EXPECT_TRUE(NavigateToURL(shell(), data_url));
HitTestRegionObserver observer(GetRenderWidgetHost()->GetFrameSinkId());
observer.WaitForHitTestData();
}
RenderWidgetHostImpl* GetRenderWidgetHost() {
return RenderWidgetHostImpl::From(shell()
->web_contents()
->GetRenderWidgetHostView()
->GetRenderWidgetHost());
}
// After adding a blocking event listener, we need to wait for the compositor
// thread to become aware of the listener. If it receives input events before
// that, the compositor thread would handle them.
void SynchronizeCompositorAndMainThreads() {
MainThreadFrameObserver observer(GetRenderWidgetHost());
observer.Wait();
}
void EnsureNoScaleChangeWhenCanceled(
base::OnceCallback<void(WebContents*, gfx::PointF)> send_events);
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All, TouchpadPinchBrowserTest, testing::Bool());
// Performing a touchpad pinch gesture should change the page scale.
IN_PROC_BROWSER_TEST_P(TouchpadPinchBrowserTest,
TouchpadPinchChangesPageScale) {
LoadURL();
content::TestPageScaleObserver scale_observer(shell()->web_contents());
const gfx::Rect contents_rect = shell()->web_contents()->GetContainerBounds();
const gfx::PointF pinch_position(contents_rect.width() / 2,
contents_rect.height() / 2);
PerformTouchpadPinch(shell()->web_contents(), pinch_position, 1.23);
scale_observer.WaitForPageScaleUpdate();
}
// We should offer synthetic wheel events to the page when a touchpad pinch
// is performed.
IN_PROC_BROWSER_TEST_P(TouchpadPinchBrowserTest, WheelListenerAllowingPinch) {
LoadURL();
ASSERT_TRUE(ExecJs(shell()->web_contents(), "setListener(false);"));
SynchronizeCompositorAndMainThreads();
content::TestPageScaleObserver scale_observer(shell()->web_contents());
const gfx::Rect contents_rect = shell()->web_contents()->GetContainerBounds();
const gfx::PointF pinch_position(contents_rect.width() / 2,
contents_rect.height() / 2);
PerformTouchpadPinch(shell()->web_contents(), pinch_position, 1.23);
// Ensure that the page saw the synthetic wheel.
ASSERT_EQ(false,
EvalJs(shell()->web_contents(),
"handlerPromise.then(function(e) {"
" window.domAutomationController.send(e.defaultPrevented);"
"});",
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
// Since the listener did not cancel the synthetic wheel, we should still
// change the page scale.
scale_observer.WaitForPageScaleUpdate();
}
// Ensures that the event(s) sent in |send_events| are cancelable by a
// wheel event listener and that doing so prevents any scale change.
void TouchpadPinchBrowserTest::EnsureNoScaleChangeWhenCanceled(
base::OnceCallback<void(WebContents*, gfx::PointF)> send_events) {
// Perform an initial pinch so we can figure out the page scale we're
// starting with for the test proper.
content::TestPageScaleObserver starting_scale_observer(
shell()->web_contents());
const gfx::Rect contents_rect = shell()->web_contents()->GetContainerBounds();
const gfx::PointF pinch_position(contents_rect.width() / 2,
contents_rect.height() / 2);
PerformTouchpadPinch(shell()->web_contents(), pinch_position, 1.23);
const float starting_scale_factor =
starting_scale_observer.WaitForPageScaleUpdate();
ASSERT_GT(starting_scale_factor, 0.f);
ASSERT_TRUE(ExecJs(shell()->web_contents(), "setListener(true);"));
SynchronizeCompositorAndMainThreads();
std::move(send_events).Run(shell()->web_contents(), pinch_position);
// Ensure the page handled a wheel event that it was able to cancel.
ASSERT_EQ(true,
EvalJs(shell()->web_contents(),
"handlerPromise.then(function(e) {"
" window.domAutomationController.send(e.defaultPrevented);"
"});",
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
// We'll check that the previous event(s) did not cause a scale change by
// performing another pinch that does change the scale.
ASSERT_TRUE(ExecJs(shell()->web_contents(),
"reset(); "
"setListener(false);"));
SynchronizeCompositorAndMainThreads();
content::TestPageScaleObserver scale_observer(shell()->web_contents());
PerformTouchpadPinch(shell()->web_contents(), pinch_position, 2.0);
ASSERT_EQ(false,
EvalJs(shell()->web_contents(),
"handlerPromise.then(function(e) {"
" window.domAutomationController.send(e.defaultPrevented);"
"});",
EXECUTE_SCRIPT_USE_MANUAL_REPLY));
const float last_scale_factor = scale_observer.WaitForPageScaleUpdate();
// The scale changes may be imprecise.
constexpr float kScaleEpsilon = 0.001;
EXPECT_NEAR(starting_scale_factor * 2.0, last_scale_factor, kScaleEpsilon);
}
// If the synthetic wheel event for a touchpad pinch is canceled, we should not
// change the page scale.
IN_PROC_BROWSER_TEST_P(TouchpadPinchBrowserTest, WheelListenerPreventingPinch) {
LoadURL();
EnsureNoScaleChangeWhenCanceled(
base::BindOnce([](WebContents* web_contents, gfx::PointF position) {
PerformTouchpadPinch(web_contents, position, 1.5);
}));
}
// If the synthetic wheel event for a touchpad double tap is canceled, we
// should not change the page scale.
// TODO(crbug.com/1328984): Re-enable this test
#if BUILDFLAG(IS_MAC)
#define MAYBE_WheelListenerPreventingDoubleTap \
DISABLED_WheelListenerPreventingDoubleTap
#else
#define MAYBE_WheelListenerPreventingDoubleTap WheelListenerPreventingDoubleTap
#endif
IN_PROC_BROWSER_TEST_P(TouchpadPinchBrowserTest,
MAYBE_WheelListenerPreventingDoubleTap) {
LoadURL();
blink::web_pref::WebPreferences prefs =
shell()->web_contents()->GetOrCreateWebPreferences();
prefs.double_tap_to_zoom_enabled = true;
shell()->web_contents()->SetWebPreferences(prefs);
EnsureNoScaleChangeWhenCanceled(
base::BindOnce([](WebContents* web_contents, gfx::PointF position) {
blink::WebGestureEvent double_tap_zoom(
blink::WebInputEvent::Type::kGestureDoubleTap,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests(),
blink::WebGestureDevice::kTouchpad);
double_tap_zoom.SetPositionInWidget(position);
double_tap_zoom.SetPositionInScreen(position);
double_tap_zoom.data.tap.tap_count = 1;
double_tap_zoom.SetNeedsWheelEvent(true);
SimulateGestureEvent(web_contents, double_tap_zoom,
ui::LatencyInfo(ui::SourceEventType::WHEEL));
}));
}
} // namespace content