diff options
20 files changed, 743 insertions, 119 deletions
diff --git a/chrome/test/chromedriver/chrome/stub_web_view.cc b/chrome/test/chromedriver/chrome/stub_web_view.cc index 12c6eee..57df23f 100644 --- a/chrome/test/chromedriver/chrome/stub_web_view.cc +++ b/chrome/test/chromedriver/chrome/stub_web_view.cc @@ -148,3 +148,21 @@ Status StubWebView::StartProfile() { Status StubWebView::EndProfile(scoped_ptr<base::Value>* profile_data) { return Status(kOk); } + +Status StubWebView::SynthesizeTapGesture(int x, + int y, + int tap_count, + bool is_long_press) { + return Status(kOk); +} + +Status StubWebView::SynthesizeScrollGesture(int x, + int y, + int xoffset, + int yoffset) { + return Status(kOk); +} + +Status StubWebView::SynthesizePinchGesture(int x, int y, double scale_factor) { + return Status(kOk); +} diff --git a/chrome/test/chromedriver/chrome/stub_web_view.h b/chrome/test/chromedriver/chrome/stub_web_view.h index 4b23879..1d85eb6 100644 --- a/chrome/test/chromedriver/chrome/stub_web_view.h +++ b/chrome/test/chromedriver/chrome/stub_web_view.h @@ -69,6 +69,15 @@ class StubWebView : public WebView { Status TakeHeapSnapshot(scoped_ptr<base::Value>* snapshot) override; Status StartProfile() override; Status EndProfile(scoped_ptr<base::Value>* profile_data) override; + Status SynthesizeTapGesture(int x, + int y, + int tap_count, + bool is_long_press) override; + Status SynthesizeScrollGesture(int x, + int y, + int xoffset, + int yoffset) override; + Status SynthesizePinchGesture(int x, int y, double scale_factor) override; private: std::string id_; diff --git a/chrome/test/chromedriver/chrome/web_view.h b/chrome/test/chromedriver/chrome/web_view.h index 47c0cca..f184e92 100644 --- a/chrome/test/chromedriver/chrome/web_view.h +++ b/chrome/test/chromedriver/chrome/web_view.h @@ -172,6 +172,18 @@ class WebView { // CPUProfile objects. The format for the captured profile is defined // (by DevTools) in protocol.json. virtual Status EndProfile(scoped_ptr<base::Value>* profile_data) = 0; + + virtual Status SynthesizeTapGesture(int x, + int y, + int tap_count, + bool is_long_press) = 0; + + virtual Status SynthesizeScrollGesture(int x, + int y, + int xoffset, + int yoffset) = 0; + + virtual Status SynthesizePinchGesture(int x, int y, double scale_factor) = 0; }; #endif // CHROME_TEST_CHROMEDRIVER_CHROME_WEB_VIEW_H_ diff --git a/chrome/test/chromedriver/chrome/web_view_impl.cc b/chrome/test/chromedriver/chrome/web_view_impl.cc index 712a36c..16e3df1 100644 --- a/chrome/test/chromedriver/chrome/web_view_impl.cc +++ b/chrome/test/chromedriver/chrome/web_view_impl.cc @@ -540,6 +540,39 @@ Status WebViewImpl::EndProfile(scoped_ptr<base::Value>* profile_data) { return status; } +Status WebViewImpl::SynthesizeTapGesture(int x, + int y, + int tap_count, + bool is_long_press) { + base::DictionaryValue params; + params.SetInteger("x", x); + params.SetInteger("y", y); + params.SetInteger("tapCount", tap_count); + if (is_long_press) + params.SetInteger("duration", 1500); + return client_->SendCommand("Input.synthesizeTapGesture", params); +} + +Status WebViewImpl::SynthesizeScrollGesture(int x, + int y, + int xoffset, + int yoffset) { + base::DictionaryValue params; + params.SetInteger("x", x); + params.SetInteger("y", y); + params.SetInteger("xdistance", xoffset); + params.SetInteger("ydistance", yoffset); + return client_->SendCommand("Input.synthesizeScrollGesture", params); +} + +Status WebViewImpl::SynthesizePinchGesture(int x, int y, double scale_factor) { + base::DictionaryValue params; + params.SetInteger("x", x); + params.SetInteger("y", y); + params.SetDouble("scaleFactor", scale_factor); + return client_->SendCommand("Input.synthesizePinchGesture", params); +} + Status WebViewImpl::CallAsyncFunctionInternal(const std::string& frame, const std::string& function, const base::ListValue& args, diff --git a/chrome/test/chromedriver/chrome/web_view_impl.h b/chrome/test/chromedriver/chrome/web_view_impl.h index 9fcadaf..dd04538 100644 --- a/chrome/test/chromedriver/chrome/web_view_impl.h +++ b/chrome/test/chromedriver/chrome/web_view_impl.h @@ -97,6 +97,15 @@ class WebViewImpl : public WebView { Status TakeHeapSnapshot(scoped_ptr<base::Value>* snapshot) override; Status StartProfile() override; Status EndProfile(scoped_ptr<base::Value>* profile_data) override; + Status SynthesizeTapGesture(int x, + int y, + int tap_count, + bool is_long_press) override; + Status SynthesizeScrollGesture(int x, + int y, + int xoffset, + int yoffset) override; + Status SynthesizePinchGesture(int x, int y, double scale_factor) override; private: Status TraverseHistoryWithJavaScript(int delta); diff --git a/chrome/test/chromedriver/client/chromedriver.py b/chrome/test/chromedriver/client/chromedriver.py index 53dcf49..e084e57 100644 --- a/chrome/test/chromedriver/client/chromedriver.py +++ b/chrome/test/chromedriver/client/chromedriver.py @@ -281,6 +281,10 @@ class ChromeDriver(object): def TouchMove(self, x, y): self.ExecuteCommand(Command.TOUCH_MOVE, {'x': x, 'y': y}) + def TouchScroll(self, element, xoffset, yoffset): + params = {'element': element._id, 'xoffset': xoffset, 'yoffset': yoffset} + self.ExecuteCommand(Command.TOUCH_SCROLL, params) + def TouchFlick(self, element, xoffset, yoffset, speed): params = { 'element': element._id, @@ -290,6 +294,10 @@ class ChromeDriver(object): } self.ExecuteCommand(Command.TOUCH_FLICK, params) + def TouchPinch(self, x, y, scale): + params = {'x': x, 'y': y, 'scale': scale} + self.ExecuteCommand(Command.TOUCH_PINCH, params) + def GetCookies(self): return self.ExecuteCommand(Command.GET_COOKIES) diff --git a/chrome/test/chromedriver/client/command_executor.py b/chrome/test/chromedriver/client/command_executor.py index fd4a464..4d3c314 100644 --- a/chrome/test/chromedriver/client/command_executor.py +++ b/chrome/test/chromedriver/client/command_executor.py @@ -149,6 +149,7 @@ class Command(object): # Custom Chrome commands. IS_LOADING = (_Method.GET, '/session/:sessionId/is_loading') + TOUCH_PINCH = (_Method.POST, '/session/:sessionId/touch/pinch') class CommandExecutor(object): diff --git a/chrome/test/chromedriver/client/webelement.py b/chrome/test/chromedriver/client/webelement.py index ebfbc7e..64d3b31 100644 --- a/chrome/test/chromedriver/client/webelement.py +++ b/chrome/test/chromedriver/client/webelement.py @@ -37,6 +37,12 @@ class WebElement(object): def SingleTap(self): self._Execute(Command.TOUCH_SINGLE_TAP) + def DoubleTap(self): + self._Execute(Command.TOUCH_DOUBLE_TAP) + + def LongPress(self): + self._Execute(Command.TOUCH_LONG_PRESS) + def Clear(self): self._Execute(Command.CLEAR_ELEMENT) diff --git a/chrome/test/chromedriver/element_commands.cc b/chrome/test/chromedriver/element_commands.cc index 0dcf7b3..b414cf7 100644 --- a/chrome/test/chromedriver/element_commands.cc +++ b/chrome/test/chromedriver/element_commands.cc @@ -16,6 +16,7 @@ #include "base/time/time.h" #include "base/values.h" #include "chrome/test/chromedriver/basic_types.h" +#include "chrome/test/chromedriver/chrome/browser_info.h" #include "chrome/test/chromedriver/chrome/chrome.h" #include "chrome/test/chromedriver/chrome/js.h" #include "chrome/test/chromedriver/chrome/status.h" @@ -191,13 +192,52 @@ Status ExecuteTouchSingleTap( session, web_view, element_id, &location); if (status.IsError()) return status; + if (session->chrome->GetBrowserInfo()->build_no < 2388) { + // TODO(samuong): remove this once we stop supporting M44. + std::list<TouchEvent> events; + events.push_back( + TouchEvent(kTouchStart, location.x, location.y)); + events.push_back( + TouchEvent(kTouchEnd, location.x, location.y)); + return web_view->DispatchTouchEvents(events); + } + return web_view->SynthesizeTapGesture(location.x, location.y, 1, false); +} - std::list<TouchEvent> events; - events.push_back( - TouchEvent(kTouchStart, location.x, location.y)); - events.push_back( - TouchEvent(kTouchEnd, location.x, location.y)); - return web_view->DispatchTouchEvents(events); +Status ExecuteTouchDoubleTap( + Session* session, + WebView* web_view, + const std::string& element_id, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value) { + if (session->chrome->GetBrowserInfo()->build_no < 2388) { + // TODO(samuong): remove this once we stop supporting M44. + return Status(kUnknownCommand, "Double tap command requires Chrome 44+"); + } + WebPoint location; + Status status = GetElementClickableLocation( + session, web_view, element_id, &location); + if (status.IsError()) + return status; + return web_view->SynthesizeTapGesture(location.x, location.y, 2, false); +} + +Status ExecuteTouchLongPress( + Session* session, + WebView* web_view, + const std::string& element_id, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value) { + if (session->chrome->GetBrowserInfo()->build_no < 2388) { + // TODO(samuong): remove this once we stop supporting M44. + return Status(kUnknownCommand, "Long press command requires Chrome 44+"); + } + WebPoint location; + Status status = GetElementClickableLocation( + session, web_view, element_id, &location); + if (status.IsError()) + return status; + return web_view->SynthesizeTapGesture(location.x, location.y, 1, true); } Status ExecuteFlick( diff --git a/chrome/test/chromedriver/element_commands.h b/chrome/test/chromedriver/element_commands.h index 6d94bf3..146f28a 100644 --- a/chrome/test/chromedriver/element_commands.h +++ b/chrome/test/chromedriver/element_commands.h @@ -76,6 +76,22 @@ Status ExecuteTouchSingleTap( const base::DictionaryValue& params, scoped_ptr<base::Value>* value); +// Double tap on the element. +Status ExecuteTouchDoubleTap( + Session* session, + WebView* web_view, + const std::string& element_id, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value); + +// Long press on the element. +Status ExecuteTouchLongPress( + Session* session, + WebView* web_view, + const std::string& element_id, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value); + // Touch flick starting on the element. Status ExecuteFlick( Session* session, diff --git a/chrome/test/chromedriver/server/http_handler.cc b/chrome/test/chromedriver/server/http_handler.cc index 65f05ee..5296a93 100644 --- a/chrome/test/chromedriver/server/http_handler.cc +++ b/chrome/test/chromedriver/server/http_handler.cc @@ -512,13 +512,16 @@ HttpHandler::HttpHandler( WrapToCommand("TouchMove", base::Bind(&ExecuteTouchMove))), CommandMapping(kPost, "session/:sessionId/touch/scroll", - base::Bind(&UnimplementedCommand)), + WrapToCommand("TouchScroll", + base::Bind(&ExecuteTouchScroll))), CommandMapping(kPost, "session/:sessionId/touch/doubleclick", - base::Bind(&UnimplementedCommand)), + WrapToCommand("TouchDoubleTap", + base::Bind(&ExecuteTouchDoubleTap))), CommandMapping(kPost, "session/:sessionId/touch/longclick", - base::Bind(&UnimplementedCommand)), + WrapToCommand("TouchLongPress", + base::Bind(&ExecuteTouchLongPress))), CommandMapping(kPost, "session/:sessionId/touch/flick", WrapToCommand("TouchFlick", base::Bind(&ExecuteFlick))), @@ -558,6 +561,10 @@ HttpHandler::HttpHandler( WrapToCommand( "SetAutoReporting", base::Bind(&ExecuteSetAutoReporting))), + CommandMapping(kPost, + "session/:sessionId/touch/pinch", + WrapToCommand("TouchPinch", + base::Bind(&ExecuteTouchPinch))), }; command_map_.reset( new CommandMap(commands, commands + arraysize(commands))); diff --git a/chrome/test/chromedriver/test/run_py_tests.py b/chrome/test/chromedriver/test/run_py_tests.py index ef0506d..3b2b6c5 100755 --- a/chrome/test/chromedriver/test/run_py_tests.py +++ b/chrome/test/chromedriver/test/run_py_tests.py @@ -54,6 +54,8 @@ _NEGATIVE_FILTER = [ # on developer workstations. 'ChromeDriverTest.testEmulateNetworkConditionsNameSpeed', 'ChromeDriverTest.testEmulateNetworkConditionsSpeed', + # crbug.com/469947 + 'ChromeDriverTest.testTouchPinch' ] _VERSION_SPECIFIC_FILTER = {} @@ -93,10 +95,13 @@ _OS_SPECIFIC_FILTER['mac'] = [ _DESKTOP_NEGATIVE_FILTER = [ # Desktop doesn't support touch (without --touch-events). - 'ChromeDriverTest.testSingleTapElement', - 'ChromeDriverTest.testTouchDownUpElement', + 'ChromeDriverTest.testTouchSingleTapElement', + 'ChromeDriverTest.testTouchDownMoveUpElement', + 'ChromeDriverTest.testTouchScrollElement', + 'ChromeDriverTest.testTouchDoubleTapElement', + 'ChromeDriverTest.testTouchLongPressElement', 'ChromeDriverTest.testTouchFlickElement', - 'ChromeDriverTest.testTouchMovedElement', + 'ChromeDriverTest.testTouchPinch', 'ChromeDriverAndroidTest.*', ] @@ -141,9 +146,25 @@ _ANDROID_NEGATIVE_FILTER['chrome'] = ( ] ) _ANDROID_NEGATIVE_FILTER['chrome_stable'] = ( - _ANDROID_NEGATIVE_FILTER['chrome']) + _ANDROID_NEGATIVE_FILTER['chrome'] + [ + # The stable channel Chrome for Android does not yet support Synthetic + # Gesture DevTools commands. + # TODO(samuong): reenable when it does. + 'ChromeDriverTest.testTouchScrollElement', + 'ChromeDriverTest.testTouchDoubleTapElement', + 'ChromeDriverTest.testTouchLongPressElement', + 'ChromeDriverTest.testTouchPinch', + ]) _ANDROID_NEGATIVE_FILTER['chrome_beta'] = ( - _ANDROID_NEGATIVE_FILTER['chrome']) + _ANDROID_NEGATIVE_FILTER['chrome'] + [ + # The beta channel Chrome for Android does not yet support Synthetic + # Gesture DevTools commands. + # TODO(samuong): reenable when it does. + 'ChromeDriverTest.testTouchScrollElement', + 'ChromeDriverTest.testTouchDoubleTapElement', + 'ChromeDriverTest.testTouchLongPressElement', + 'ChromeDriverTest.testTouchPinch', + ]) _ANDROID_NEGATIVE_FILTER['chrome_shell'] = ( _ANDROID_NEGATIVE_FILTER['chrome'] + [ # ChromeShell doesn't support multiple tabs. @@ -447,72 +468,6 @@ class ChromeDriverTest(ChromeDriverBaseTest): div.Click() self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) - def testSingleTapElement(self): - div = self._driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchend", function() {' - ' div.innerHTML="new<br>";' - '});' - 'return div;') - div.SingleTap() - self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) - - def testTouchDownUpElement(self): - div = self._driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchend", function() {' - ' div.innerHTML="new<br>";' - '});' - 'return div;') - loc = div.GetLocation() - self._driver.TouchDown(loc['x'], loc['y']) - self._driver.TouchUp(loc['x'], loc['y']) - self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) - - def testTouchFlickElement(self): - dx = 3 - dy = 4 - speed = 5 - flickTouchEventsPerSecond = 30 - moveEvents = int( - math.sqrt(dx * dx + dy * dy) * flickTouchEventsPerSecond / speed) - div = self._driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchstart", function() {' - ' div.innerHTML = "preMove0";' - '});' - 'div.addEventListener("touchmove", function() {' - ' res = div.innerHTML.match(/preMove(\d+)/);' - ' if (res != null) {' - ' div.innerHTML = "preMove" + (parseInt(res[1], 10) + 1);' - ' }' - '});' - 'div.addEventListener("touchend", function() {' - ' if (div.innerHTML == "preMove' + str(moveEvents) + '") {' - ' div.innerHTML = "new<br>";' - ' }' - '});' - 'return div;') - self._driver.TouchFlick(div, dx, dy, speed) - self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) - - def testTouchMovedElement(self): - div = self._driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchmove", function() {' - ' div.innerHTML="new<br>";' - '});' - 'return div;') - loc = div.GetLocation() - self._driver.TouchDown(loc['x'], loc['y']) - self._driver.TouchMove(loc['x'] + 1, loc['y'] + 1) - self._driver.TouchUp(loc['x'] + 1, loc['y'] + 1) - self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) - def testClickElementInSubFrame(self): self._driver.Load(self.GetHttpUrlForFile('/chromedriver/frame_test.html')) frame = self._driver.FindElement('tag name', 'iframe') @@ -988,6 +943,104 @@ class ChromeDriverTest(ChromeDriverBaseTest): 'document.querySelector("#outerDiv").style.display="None";') self.assertFalse(elem.IsDisplayed()) + def testTouchSingleTapElement(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + events = self._driver.FindElement('id', 'events') + events.SingleTap() + self.assertEquals('events: touchstart touchend', events.GetText()) + + def testTouchDownMoveUpElement(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + events = self._driver.FindElement('id', 'events') + location = events.GetLocation() + self._driver.TouchDown(location['x'], location['y']) + self.assertEquals('events: touchstart', events.GetText()) + self._driver.TouchMove(location['x'] + 1, location['y'] + 1) + self.assertEquals('events: touchstart touchmove', events.GetText()) + self._driver.TouchUp(location['x'] + 1, location['y'] + 1) + self.assertEquals('events: touchstart touchmove touchend', events.GetText()) + + def testTouchScrollElement(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + events = self._driver.FindElement('id', 'events') + self.assertTrue(events.IsDisplayed()) + xoffset = 0 + yoffset = self._driver.ExecuteScript('return window.screen.height * 2;') + self._driver.TouchScroll(events, xoffset, yoffset) + bottom = self._driver.FindElement('id', 'bottom') + self.assertTrue(bottom.IsDisplayed()) + + def testTouchDoubleTapElement(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + events = self._driver.FindElement('id', 'events') + events.DoubleTap() + self.assertEquals('events: touchstart touchend touchstart touchend', + events.GetText()) + + def testTouchLongPressElement(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + events = self._driver.FindElement('id', 'events') + events.LongPress() + self.assertEquals('events: touchstart touchcancel', events.GetText()) + + def testTouchFlickElement(self): + dx = 3 + dy = 4 + speed = 5 + flickTouchEventsPerSecond = 30 + moveEvents = int( + math.sqrt(dx * dx + dy * dy) * flickTouchEventsPerSecond / speed) + div = self._driver.ExecuteScript( + 'document.body.innerHTML = "<div>old</div>";' + 'var div = document.getElementsByTagName("div")[0];' + 'div.addEventListener("touchstart", function() {' + ' div.innerHTML = "preMove0";' + '});' + 'div.addEventListener("touchmove", function() {' + ' res = div.innerHTML.match(/preMove(\d+)/);' + ' if (res != null) {' + ' div.innerHTML = "preMove" + (parseInt(res[1], 10) + 1);' + ' }' + '});' + 'div.addEventListener("touchend", function() {' + ' if (div.innerHTML == "preMove' + str(moveEvents) + '") {' + ' div.innerHTML = "new<br>";' + ' }' + '});' + 'return div;') + self._driver.TouchFlick(div, dx, dy, speed) + self.assertEquals(1, len(self._driver.FindElements('tag name', 'br'))) + + def testTouchPinch(self): + self._driver.Load(self.GetHttpUrlForFile( + '/chromedriver/touch_action_tests.html')) + width_before_pinch = self._driver.ExecuteScript('return window.innerWidth;') + height_before_pinch = self._driver.ExecuteScript( + 'return window.innerHeight;') + self._driver.TouchPinch(width_before_pinch / 2, + height_before_pinch / 2, + 2.0) + width_after_pinch = self._driver.ExecuteScript('return window.innerWidth;') + self.assertAlmostEqual(2.0, float(width_before_pinch) / width_after_pinch) + + def testBrowserDoesntSupportSyntheticGestures(self): + # Current versions of stable and beta channel Chrome for Android do not + # support synthetic gesture commands in DevTools, so touch action tests have + # been disabled for chrome_stable and chrome_beta. + # TODO(samuong): when this test starts failing, re-enable touch tests and + # delete this test. + if _ANDROID_PACKAGE_KEY: + if _ANDROID_PACKAGE_KEY in ['chrome_stable', 'chrome_beta']: + self.assertRaisesRegexp(RuntimeError, + 'Server returned error: Not Implemented', + self._driver.TouchPinch, 1, 2, 3.0) + + class ChromeDriverAndroidTest(ChromeDriverBaseTest): """End to end tests for Android-specific tests.""" @@ -1279,35 +1332,6 @@ class MobileEmulationCapabilityTest(ChromeDriverBaseTest): div.Click() self.assertEquals(1, len(driver.FindElements('tag name', 'br'))) - def testSingleTapElement(self): - driver = self.CreateDriver( - mobile_emulation = {'deviceName': 'Google Nexus 5'}) - driver.Load('about:blank') - div = driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchend", function() {' - ' div.innerHTML="new<br>";' - '});' - 'return div;') - div.SingleTap() - self.assertEquals(1, len(driver.FindElements('tag name', 'br'))) - - def testTouchDownUpElement(self): - driver = self.CreateDriver( - mobile_emulation = {'deviceName': 'Google Nexus 5'}) - div = driver.ExecuteScript( - 'document.body.innerHTML = "<div>old</div>";' - 'var div = document.getElementsByTagName("div")[0];' - 'div.addEventListener("touchend", function() {' - ' div.innerHTML="new<br>";' - '});' - 'return div;') - loc = div.GetLocation() - driver.TouchDown(loc['x'], loc['y']) - driver.TouchUp(loc['x'], loc['y']) - self.assertEquals(1, len(driver.FindElements('tag name', 'br'))) - class ChromeDriverLogTest(unittest.TestCase): """Tests that chromedriver produces the expected log file.""" diff --git a/chrome/test/chromedriver/window_commands.cc b/chrome/test/chromedriver/window_commands.cc index 075f8b1..7c5c0ec 100644 --- a/chrome/test/chromedriver/window_commands.cc +++ b/chrome/test/chromedriver/window_commands.cc @@ -15,6 +15,7 @@ #include "base/values.h" #include "chrome/test/chromedriver/basic_types.h" #include "chrome/test/chromedriver/chrome/automation_extension.h" +#include "chrome/test/chromedriver/chrome/browser_info.h" #include "chrome/test/chromedriver/chrome/chrome.h" #include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h" #include "chrome/test/chromedriver/chrome/devtools_client.h" @@ -593,6 +594,53 @@ Status ExecuteTouchMove( return ExecuteTouchEvent(session, web_view, kTouchMove, params); } +Status ExecuteTouchScroll( + Session *session, + WebView* web_view, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value) { + if (session->chrome->GetBrowserInfo()->build_no < 2286) { + // TODO(samuong): remove this once we stop supporting M41. + return Status(kUnknownCommand, "Touch scroll action requires Chrome 42+"); + } + WebPoint location = session->mouse_position; + std::string element; + if (params.GetString("element", &element)) { + Status status = GetElementClickableLocation( + session, web_view, element, &location); + if (status.IsError()) + return status; + } + int xoffset; + if (!params.GetInteger("xoffset", &xoffset)) + return Status(kUnknownError, "'xoffset' must be an integer"); + int yoffset; + if (!params.GetInteger("yoffset", &yoffset)) + return Status(kUnknownError, "'yoffset' must be an integer"); + return web_view->SynthesizeScrollGesture( + location.x, location.y, xoffset, yoffset); +} + +Status ExecuteTouchPinch( + Session* session, + WebView* web_view, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value) { + if (session->chrome->GetBrowserInfo()->build_no < 2286) { + // TODO(samuong): remove this once we stop supporting M41. + return Status(kUnknownCommand, "Pinch action requires Chrome 42+"); + } + WebPoint location; + if (!params.GetInteger("x", &location.x)) + return Status(kUnknownError, "'x' must be an integer"); + if (!params.GetInteger("y", &location.y)) + return Status(kUnknownError, "'y' must be an integer"); + double scale_factor; + if (!params.GetDouble("scale", &scale_factor)) + return Status(kUnknownError, "'scale' must be an integer"); + return web_view->SynthesizePinchGesture(location.x, location.y, scale_factor); +} + Status ExecuteGetActiveElement( Session* session, WebView* web_view, diff --git a/chrome/test/chromedriver/window_commands.h b/chrome/test/chromedriver/window_commands.h index ec4a68c..fa36e2b 100644 --- a/chrome/test/chromedriver/window_commands.h +++ b/chrome/test/chromedriver/window_commands.h @@ -181,6 +181,19 @@ Status ExecuteTouchMove( const base::DictionaryValue& params, scoped_ptr<base::Value>* value); +// Do a swipe (scroll) gesture beginning at the element. +Status ExecuteTouchScroll( + Session *session, + WebView* web_view, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value); + +Status ExecuteTouchPinch( + Session* session, + WebView* web_view, + const base::DictionaryValue& params, + scoped_ptr<base::Value>* value); + Status ExecuteGetActiveElement( Session* session, WebView* web_view, diff --git a/chrome/test/data/chromedriver/touch_action_tests.html b/chrome/test/data/chromedriver/touch_action_tests.html new file mode 100644 index 0000000..9a1e15d --- /dev/null +++ b/chrome/test/data/chromedriver/touch_action_tests.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <title>Touch Action Test Page</title> + </head> + <body> + <div id="events">events: </div> + <div id="padding"> + We need some padding here so that the page is large enough to test swipe + and scroll actions. + </div> + <div id="bottom">This is the bottom of the page.</div> + <script> + events = document.getElementById('events'); + var eventTypes = ['touchstart', 'touchend', 'touchmove', 'touchcancel']; + var eventListener = function(evt) { + events.innerHTML += ' ' + evt.type; + }; + + for (var i = 0; i < eventTypes.length; i++) { + events.addEventListener(eventTypes[i], eventListener); + } + + var padding = document.getElementById('padding'); + padding.style.border = 'solid'; + padding.style.height = window.screen.height * 2 + 'px'; + </script> + </body> +</html> diff --git a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc index cbbd46a..cbae682 100644 --- a/content/browser/devtools/protocol/devtools_protocol_browsertest.cc +++ b/content/browser/devtools/protocol/devtools_protocol_browsertest.cc @@ -6,10 +6,12 @@ #include "base/command_line.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" +#include "base/values.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/web_contents.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/shell/browser/shell.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/compositor/compositor_switches.h" @@ -67,10 +69,6 @@ class DevToolsProtocolTest : public ContentBrowserTest, return false; } - scoped_ptr<base::DictionaryValue> result_; - scoped_refptr<DevToolsAgentHost> agent_host_; - - private: void SetUpOnMainThread() override { agent_host_ = DevToolsAgentHost::GetOrCreateFor(shell()->web_contents()); agent_host_->AttachClient(this); @@ -81,6 +79,10 @@ class DevToolsProtocolTest : public ContentBrowserTest, agent_host_ = NULL; } + scoped_ptr<base::DictionaryValue> result_; + scoped_refptr<DevToolsAgentHost> agent_host_; + + private: void DispatchProtocolMessage(DevToolsAgentHost* agent_host, const std::string& message) override { scoped_ptr<base::DictionaryValue> root( @@ -135,4 +137,128 @@ IN_PROC_BROWSER_TEST_F(CaptureScreenshotTest, MAYBE_CaptureScreenshot) { EXPECT_TRUE(std::abs(0x56-(int)SkColorGetB(color)) <= 1); } +class SyntheticGestureTest : public DevToolsProtocolTest { +#if !defined(OS_ANDROID) + protected: + void SetUpOnMainThread() override { + DevToolsProtocolTest::SetUpOnMainThread(); + + scoped_ptr<base::DictionaryValue> params(new base::DictionaryValue()); + params->SetInteger("width", 384); + params->SetInteger("height", 640); + params->SetDouble("deviceScaleFactor", 2.0); + params->SetBoolean("mobile", true); + params->SetBoolean("fitWindow", false); + params->SetBoolean("textAutosizing", true); + SendCommand("Page.setDeviceMetricsOverride", params.Pass()); + + params.reset(new base::DictionaryValue()); + params->SetBoolean("enabled", true); + params->SetString("configuration", "mobile"); + SendCommand("Page.setTouchEmulationEnabled", params.Pass()); + } +#endif +}; + +#if defined(OS_ANDROID) +// crbug.com/469947 +#define MAYBE_SynthesizePinchGesture DISABLED_SynthesizePinchGesture +#else +// crbug.com/460128 +#define MAYBE_SynthesizePinchGesture DISABLED_SynthesizePinchGesture +#endif +IN_PROC_BROWSER_TEST_F(SyntheticGestureTest, MAYBE_SynthesizePinchGesture) { + GURL test_url = GetTestUrl("devtools", "synthetic_gesture_tests.html"); + NavigateToURLBlockUntilNavigationsComplete(shell(), test_url, 1); + + int old_width; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(window.innerWidth)", &old_width)); + + int old_height; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(window.innerHeight)", &old_height)); + + scoped_ptr<base::DictionaryValue> params(new base::DictionaryValue()); + params->SetInteger("x", old_width / 2); + params->SetInteger("y", old_height / 2); + params->SetDouble("scaleFactor", 2.0); + SendCommand("Input.synthesizePinchGesture", params.Pass()); + + int new_width; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(window.innerWidth)", &new_width)); + ASSERT_DOUBLE_EQ(2.0, static_cast<double>(old_width) / new_width); + + int new_height; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(window.innerHeight)", &new_height)); + ASSERT_DOUBLE_EQ(2.0, static_cast<double>(old_height) / new_height); +} + +#if defined(OS_ANDROID) +#define MAYBE_SynthesizeScrollGesture SynthesizeScrollGesture +#else +// crbug.com/460128 +#define MAYBE_SynthesizeScrollGesture DISABLED_SynthesizeScrollGesture +#endif +IN_PROC_BROWSER_TEST_F(SyntheticGestureTest, MAYBE_SynthesizeScrollGesture) { + GURL test_url = GetTestUrl("devtools", "synthetic_gesture_tests.html"); + NavigateToURLBlockUntilNavigationsComplete(shell(), test_url, 1); + + int scroll_top; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(document.body.scrollTop)", &scroll_top)); + ASSERT_EQ(0, scroll_top); + + scoped_ptr<base::DictionaryValue> params(new base::DictionaryValue()); + params->SetInteger("x", 0); + params->SetInteger("y", 0); + params->SetInteger("xDistance", 0); + params->SetInteger("yDistance", -100); + SendCommand("Input.synthesizeScrollGesture", params.Pass()); + + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(document.body.scrollTop)", &scroll_top)); + ASSERT_EQ(100, scroll_top); +} + +#if defined(OS_ANDROID) +#define MAYBE_SynthesizeTapGesture SynthesizeTapGesture +#else +// crbug.com/460128 +#define MAYBE_SynthesizeTapGesture DISABLED_SynthesizeTapGesture +#endif +IN_PROC_BROWSER_TEST_F(SyntheticGestureTest, MAYBE_SynthesizeTapGesture) { + GURL test_url = GetTestUrl("devtools", "synthetic_gesture_tests.html"); + NavigateToURLBlockUntilNavigationsComplete(shell(), test_url, 1); + + int scroll_top; + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(document.body.scrollTop)", &scroll_top)); + ASSERT_EQ(0, scroll_top); + + scoped_ptr<base::DictionaryValue> params(new base::DictionaryValue()); + params->SetInteger("x", 16); + params->SetInteger("y", 16); + params->SetString("gestureSourceType", "touch"); + SendCommand("Input.synthesizeTapGesture", params.Pass()); + + // The link that we just tapped should take us to the bottom of the page. The + // new value of |document.body.scrollTop| will depend on the screen dimensions + // of the device that we're testing on, but in any case it should be greater + // than 0. + ASSERT_TRUE(content::ExecuteScriptAndExtractInt( + shell()->web_contents(), + "domAutomationController.send(document.body.scrollTop)", &scroll_top)); + ASSERT_GT(scroll_top, 0); +} + } // namespace content diff --git a/content/browser/devtools/protocol/input_handler.cc b/content/browser/devtools/protocol/input_handler.cc index 4c460e9..0f15928 100644 --- a/content/browser/devtools/protocol/input_handler.cc +++ b/content/browser/devtools/protocol/input_handler.cc @@ -6,14 +6,47 @@ #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" +#include "cc/output/compositor_frame_metadata.h" #include "content/browser/renderer_host/render_widget_host_impl.h" +#include "content/common/input/synthetic_pinch_gesture_params.h" +#include "content/common/input/synthetic_smooth_scroll_gesture_params.h" +#include "content/common/input/synthetic_tap_gesture_params.h" #include "third_party/WebKit/public/web/WebInputEvent.h" #include "ui/events/keycodes/dom4/keycode_converter.h" +#include "ui/gfx/geometry/point.h" namespace content { namespace devtools { namespace input { +namespace { + +gfx::Point CssPixelsToPoint(int x, int y, float page_scale_factor) { + return gfx::Point(x * page_scale_factor, y * page_scale_factor); +} + +gfx::Vector2d CssPixelsToVector2d(int x, int y, float page_scale_factor) { + return gfx::Vector2d(x * page_scale_factor, y * page_scale_factor); +} + +bool StringToGestureSourceType(const std::string& in, + SyntheticGestureParams::GestureSourceType& out) { + if (in == kGestureSourceTypeDefault) { + out = SyntheticGestureParams::GestureSourceType::DEFAULT_INPUT; + return true; + } else if (in == kGestureSourceTypeTouch) { + out = SyntheticGestureParams::GestureSourceType::TOUCH_INPUT; + return true; + } else if (in == kGestureSourceTypeMouse) { + out = SyntheticGestureParams::GestureSourceType::MOUSE_INPUT; + return true; + } else { + return false; + } +} + +} + typedef DevToolsProtocolClient::Response Response; namespace { @@ -87,7 +120,9 @@ bool SetMouseEventType(blink::WebMouseEvent* event, const std::string& type) { } // namespace InputHandler::InputHandler() - : host_(NULL) { + : host_(NULL), + page_scale_factor_(1.0), + weak_factory_(this) { } InputHandler::~InputHandler() { @@ -97,7 +132,14 @@ void InputHandler::SetRenderWidgetHost(RenderWidgetHostImpl* host) { host_ = host; } -void InputHandler::SetClient(scoped_ptr<DevToolsProtocolClient> client) { +void InputHandler::SetClient(scoped_ptr<Client> client) { + client_.swap(client); +} + +void InputHandler::OnSwapCompositorFrame( + const cc::CompositorFrameMetadata& frame_metadata) { + page_scale_factor_ = frame_metadata.page_scale_factor; + scrollable_viewport_size_ = frame_metadata.scrollable_viewport_size; } Response InputHandler::DispatchKeyEvent( @@ -260,7 +302,29 @@ Response InputHandler::SynthesizePinchGesture( double scale_factor, const int* relative_speed, const std::string* gesture_source_type) { - return Response::InternalError("Not yet implemented"); + if (!host_) + return Response::ServerError("Could not connect to view"); + + SyntheticPinchGestureParams gesture_params; + const int kDefaultRelativeSpeed = 800; + + gesture_params.scale_factor = scale_factor; + gesture_params.anchor = CssPixelsToPoint(x, y, page_scale_factor_); + gesture_params.relative_pointer_speed_in_pixels_s = + relative_speed ? *relative_speed : kDefaultRelativeSpeed; + + if (!StringToGestureSourceType( + gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault, + gesture_params.gesture_source_type)) { + return Response::InvalidParams("gestureSourceType"); + } + + host_->QueueSyntheticGesture( + SyntheticGesture::Create(gesture_params), + base::Bind(&InputHandler::SendSynthesizePinchGestureResponse, + weak_factory_.GetWeakPtr(), command_id)); + + return Response::OK(); } Response InputHandler::SynthesizeScrollGesture( @@ -274,7 +338,44 @@ Response InputHandler::SynthesizeScrollGesture( const bool* prevent_fling, const int* speed, const std::string* gesture_source_type) { - return Response::InternalError("Not yet implemented"); + if (!host_) + return Response::ServerError("Could not connect to view"); + + SyntheticSmoothScrollGestureParams gesture_params; + const bool kDefaultPreventFling = true; + const int kDefaultSpeed = 800; + + gesture_params.anchor = CssPixelsToPoint(x, y, page_scale_factor_); + gesture_params.prevent_fling = + prevent_fling ? *prevent_fling : kDefaultPreventFling; + gesture_params.speed_in_pixels_s = speed ? *speed : kDefaultSpeed; + + if (x_distance || y_distance) { + gesture_params.distances.push_back( + CssPixelsToVector2d(x_distance ? *x_distance : 0, + y_distance ? *y_distance : 0, + page_scale_factor_)); + } + + if (x_overscroll || y_overscroll) { + gesture_params.distances.push_back( + CssPixelsToVector2d(x_overscroll ? -*x_overscroll : 0, + y_overscroll ? -*y_overscroll : 0, + page_scale_factor_)); + } + + if (!StringToGestureSourceType( + gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault, + gesture_params.gesture_source_type)) { + return Response::InvalidParams("gestureSourceType"); + } + + host_->QueueSyntheticGesture( + SyntheticGesture::Create(gesture_params), + base::Bind(&InputHandler::SendSynthesizeScrollGestureResponse, + weak_factory_.GetWeakPtr(), command_id)); + + return Response::OK(); } Response InputHandler::SynthesizeTapGesture( @@ -284,7 +385,78 @@ Response InputHandler::SynthesizeTapGesture( const int* duration, const int* tap_count, const std::string* gesture_source_type) { - return Response::InternalError("Not yet implemented"); + if (!host_) + return Response::ServerError("Could not connect to view"); + + SyntheticTapGestureParams gesture_params; + const int kDefaultDuration = 50; + const int kDefaultTapCount = 1; + + gesture_params.position = CssPixelsToPoint(x, y, page_scale_factor_); + gesture_params.duration_ms = duration ? *duration : kDefaultDuration; + + if (!StringToGestureSourceType( + gesture_source_type ? *gesture_source_type : kGestureSourceTypeDefault, + gesture_params.gesture_source_type)) { + return Response::InvalidParams("gestureSourceType"); + } + + if (!tap_count) + tap_count = &kDefaultTapCount; + + for (int i = 0; i < *tap_count; i++) { + // If we're doing more than one tap, don't send the response to the client + // until we've completed the last tap. + bool is_last_tap = i == *tap_count - 1; + host_->QueueSyntheticGesture( + SyntheticGesture::Create(gesture_params), + base::Bind(&InputHandler::SendSynthesizeTapGestureResponse, + weak_factory_.GetWeakPtr(), command_id, is_last_tap)); + } + + return Response::OK(); +} + +void InputHandler::SendSynthesizePinchGestureResponse( + DevToolsCommandId command_id, + SyntheticGesture::Result result) { + if (result == SyntheticGesture::Result::GESTURE_FINISHED) { + client_->SendSynthesizePinchGestureResponse( + command_id, SynthesizePinchGestureResponse::Create()); + } else { + client_->SendError(command_id, + Response::InternalError(base::StringPrintf( + "Synthetic pinch failed, result was %d", result))); + } +} + +void InputHandler::SendSynthesizeScrollGestureResponse( + DevToolsCommandId command_id, + SyntheticGesture::Result result) { + if (result == SyntheticGesture::Result::GESTURE_FINISHED) { + client_->SendSynthesizeScrollGestureResponse( + command_id, SynthesizeScrollGestureResponse::Create()); + } else { + client_->SendError(command_id, + Response::InternalError(base::StringPrintf( + "Synthetic scroll failed, result was %d", result))); + } +} + +void InputHandler::SendSynthesizeTapGestureResponse( + DevToolsCommandId command_id, + bool send_success, + SyntheticGesture::Result result) { + if (result == SyntheticGesture::Result::GESTURE_FINISHED) { + if (send_success) { + client_->SendSynthesizeTapGestureResponse( + command_id, SynthesizeTapGestureResponse::Create()); + } + } else { + client_->SendError(command_id, + Response::InternalError(base::StringPrintf( + "Synthetic tap failed, result was %d", result))); + } } } // namespace input diff --git a/content/browser/devtools/protocol/input_handler.h b/content/browser/devtools/protocol/input_handler.h index cae6fb5..22a1ad7 100644 --- a/content/browser/devtools/protocol/input_handler.h +++ b/content/browser/devtools/protocol/input_handler.h @@ -5,7 +5,18 @@ #ifndef CONTENT_BROWSER_DEVTOOLS_PROTOCOL_INPUT_HANDLER_H_ #define CONTENT_BROWSER_DEVTOOLS_PROTOCOL_INPUT_HANDLER_H_ +#include "base/memory/weak_ptr.h" #include "content/browser/devtools/protocol/devtools_protocol_handler.h" +#include "content/browser/renderer_host/input/synthetic_gesture.h" +#include "ui/gfx/geometry/size_f.h" + +namespace cc { +class CompositorFrameMetadata; +} + +namespace gfx { +class Point; +} namespace content { @@ -22,7 +33,8 @@ class InputHandler { virtual ~InputHandler(); void SetRenderWidgetHost(RenderWidgetHostImpl* host); - void SetClient(scoped_ptr<DevToolsProtocolClient> client); + void SetClient(scoped_ptr<Client> client); + void OnSwapCompositorFrame(const cc::CompositorFrameMetadata& frame_metadata); Response DispatchKeyEvent(const std::string& type, const int* modifiers, @@ -81,7 +93,21 @@ class InputHandler { const std::string* gesture_source_type); private: + void SendSynthesizePinchGestureResponse(DevToolsCommandId command_id, + SyntheticGesture::Result result); + + void SendSynthesizeScrollGestureResponse(DevToolsCommandId command_id, + SyntheticGesture::Result result); + + void SendSynthesizeTapGestureResponse(DevToolsCommandId command_id, + bool send_success, + SyntheticGesture::Result result); + RenderWidgetHostImpl* host_; + scoped_ptr<Client> client_; + float page_scale_factor_; + gfx::SizeF scrollable_viewport_size_; + base::WeakPtrFactory<InputHandler> weak_factory_; DISALLOW_COPY_AND_ASSIGN(InputHandler); }; diff --git a/content/browser/devtools/render_frame_devtools_agent_host.cc b/content/browser/devtools/render_frame_devtools_agent_host.cc index 409ee0d..6f40d6f 100644 --- a/content/browser/devtools/render_frame_devtools_agent_host.cc +++ b/content/browser/devtools/render_frame_devtools_agent_host.cc @@ -494,6 +494,8 @@ void RenderFrameDevToolsAgentHost::OnSwapCompositorFrame( return; if (page_handler_) page_handler_->OnSwapCompositorFrame(get<1>(param).metadata); + if (input_handler_) + input_handler_->OnSwapCompositorFrame(get<1>(param).metadata); if (frame_trace_recorder_) { frame_trace_recorder_->OnSwapCompositorFrame( render_frame_host_, get<1>(param).metadata); @@ -506,6 +508,8 @@ void RenderFrameDevToolsAgentHost::SynchronousSwapCompositorFrame( return; if (page_handler_) page_handler_->OnSwapCompositorFrame(frame_metadata); + if (input_handler_) + input_handler_->OnSwapCompositorFrame(frame_metadata); if (frame_trace_recorder_) { frame_trace_recorder_->OnSwapCompositorFrame( render_frame_host_, frame_metadata); diff --git a/content/test/data/devtools/synthetic_gesture_tests.html b/content/test/data/devtools/synthetic_gesture_tests.html new file mode 100644 index 0000000..280153d --- /dev/null +++ b/content/test/data/devtools/synthetic_gesture_tests.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <title>Synthetic Gesture Test Page</title> + </head> + <body> + <a href="#bottom">Go to the bottom of the page</a> + <div id="padding"> + We need some padding here so that the page is large enough to test swipe + and scroll actions. + </div> + <div id="bottom"><a name="bottom">This is the bottom of the page.</a></div> + <script> + var padding = document.getElementById('padding'); + padding.style.border = 'solid'; + padding.style.height = window.screen.height * 2 + 'px'; + </script> + </body> +</html> |