diff --git a/docs/api.md b/docs/api.md
index 4f4c477e796..050816268c7 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -29,7 +29,7 @@
     + [page.$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
     + [page.$$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
     + [page.addScriptTag(url)](#pageaddscripttagurl)
-    + [page.click(selector)](#pageclickselector)
+    + [page.click(selector[, options])](#pageclickselector-options)
     + [page.close()](#pageclose)
     + [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args)
     + [page.evaluateOnInitialized(pageFunction, ...args)](#pageevaluateoninitializedpagefunction-args)
@@ -37,10 +37,12 @@
     + [page.frames()](#pageframes)
     + [page.goBack(options)](#pagegobackoptions)
     + [page.goForward(options)](#pagegoforwardoptions)
+    + [page.hover(selector)](#pagehoverselector)
     + [page.httpHeaders()](#pagehttpheaders)
     + [page.injectFile(filePath)](#pageinjectfilefilepath)
     + [page.keyboard](#pagekeyboard)
     + [page.mainFrame()](#pagemainframe)
+    + [page.mouse](#pagemouse)
     + [page.navigate(url, options)](#pagenavigateurl-options)
     + [page.pdf(options)](#pagepdfoptions)
     + [page.plainText()](#pageplaintext)
@@ -67,6 +69,11 @@
     + [keyboard.modifiers()](#keyboardmodifiers)
     + [keyboard.sendCharacter(char)](#keyboardsendcharacterchar)
     + [keyboard.up(key)](#keyboardupkey)
+  * [class: Mouse](#class-mouse)
+    + [mouse.down([options])](#mousedownoptions)
+    + [mouse.move(x, y)](#mousemovex-y)
+    + [mouse.press([options])](#mousepressoptions)
+    + [mouse.up([options])](#mouseupoptions)
   * [class: Dialog](#class-dialog)
     + [dialog.accept([promptText])](#dialogacceptprompttext)
     + [dialog.dismiss()](#dialogdismiss)
@@ -76,7 +83,9 @@
     + [frame.$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
     + [frame.$$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
     + [frame.childFrames()](#framechildframes)
+    + [frame.click(selector[, options])](#frameclickselector-options)
     + [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
+    + [frame.hover(selector)](#framehoverselector)
     + [frame.isDetached()](#frameisdetached)
     + [frame.isMainFrame()](#frameismainframe)
     + [frame.name()](#framename)
@@ -351,8 +360,11 @@ Shortcut for [page.mainFrame().$$(selector, pageFunction, ...args)](#pageselecto
 
 Adds a `<script></script>` tag to the page with the desired url. Alternatively, javascript could be injected to the page via `page.injectFile` method.
 
-#### page.click(selector)
+#### page.click(selector[, options])
 - `selector` <[string]> A query selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
+- `options` <[Object]>
+  - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
+  - `clickCount` <[number]> defaults to 1
 - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. Promise gets rejected if there's no element matching `selector`.
 
 #### page.close()
@@ -393,6 +405,10 @@ can not go back, resolves to null.
 
 Navigate to the next page in history.
 
+#### page.hover(selector)
+- `selector` <[string]> A query selector to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
+- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.
+
 #### page.httpHeaders()
 - returns: <[Object]> Key-value set of additional http headers which will be sent with every request.
 
@@ -409,6 +425,10 @@ Navigate to the next page in history.
 
 Page is guaranteed to have a main frame which persists during navigations.
 
+#### page.mouse
+
+- returns: <[Mouse]>
+
 #### page.navigate(url, options)
 - `url` <[string]> URL to navigate page to
 - `options` <[Object]> Navigation parameters which might have the following properties:
@@ -671,6 +691,39 @@ page.keyboard.sendCharacter('嗨');
 
 Dispatches a `keyup` event.
 
+### class: Mouse
+
+#### mouse.down([options])
+- `options` <[Object]>
+  - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
+  - `clickCount` <[number]> defaults to 1
+- returns: <[Promise]>
+
+Dispatches a `mousedown` event.
+
+#### mouse.move(x, y)
+- `x` <[number]>
+- `y` <[number]>
+- returns: <[Promise]>
+
+Dispatches a `mousemove` event.
+
+#### mouse.press([options])
+- `options` <[Object]>
+  - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
+  - `clickCount` <[number]> defaults to 1
+- returns: <[Promise]>
+
+Shortcut for [`mouse.down`](#mousedownkey) and [`mouse.up`](#mouseupkey).
+
+#### mouse.up([options])
+- `options` <[Object]>
+  - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
+  - `clickCount` <[number]> defaults to 1
+- returns: <[Promise]>
+
+Dispatches a `mouseup` event.
+
 ### class: Dialog
 
 [Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event.
@@ -749,6 +802,13 @@ browser.newPage().then(async page => {
 #### frame.childFrames()
 - returns: <[Array]<[Frame]>>
 
+#### frame.click(selector[, options])
+- `selector` <[string]> A query selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
+- `options` <[Object]>
+  - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`.
+  - `clickCount` <[number]> defaults to 1
+- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. Promise gets rejected if there's no element matching `selector`.
+
 #### frame.evaluate(pageFunction, ...args)
 - `pageFunction` <[function]> Function to be evaluated in browser context
 - `...args` <...[string]> Arguments to pass to  `pageFunction`
@@ -768,6 +828,10 @@ browser.newPage().then(async page =>
 });
 ```
 
+#### frame.hover(selector)
+- `selector` <[string]> A query selector to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
+- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.
+
 #### frame.isDetached()
 - returns: <[boolean]>
 
@@ -1011,3 +1075,4 @@ If there's already a header with name `name`, the header gets overwritten.
 [Element]: https://developer.mozilla.org/en-US/docs/Web/API/element "Element"
 [Keyboard]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-keyboard "Keyboard"
 [Dialog]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-dialog  "Dialog"
+[Mouse]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-mouse "Mouse"
diff --git a/lib/FrameManager.js b/lib/FrameManager.js
index 7e8ba3743cb..90ce6c87af7 100644
--- a/lib/FrameManager.js
+++ b/lib/FrameManager.js
@@ -20,20 +20,23 @@ let helper = require('./helper');
 class FrameManager extends EventEmitter {
   /**
    * @param {!Connection} client
+   * @param {!Mouse} mouse
    * @return {!Promise<!FrameManager>}
    */
-  static async create(client) {
+  static async create(client, mouse) {
     let mainFramePayload = await client.send('Page.getResourceTree');
-    return new FrameManager(client, mainFramePayload.frameTree);
+    return new FrameManager(client, mainFramePayload.frameTree, mouse);
   }
 
   /**
    * @param {!Connection} client
    * @param {!Object} frameTree
+   * @param {!Mouse} mouse
    */
-  constructor(client, frameTree) {
+  constructor(client, frameTree, mouse) {
     super();
     this._client = client;
+    this._mouse = mouse;
     /** @type {!Map<string, !Frame>} */
     this._frames = new Map();
     this._mainFrame = this._addFramesRecursively(null, frameTree);
@@ -405,6 +408,39 @@ class Frame {
     return this._frameManager._evaluateOnFrame(this, expression);
   }
 
+  /**
+   * @param {string} selector
+   * @return {!Promise}
+   */
+  async hover(selector) {
+    let center = await this.evaluate(selector => {
+      let element = document.querySelector(selector);
+      if (!element)
+        return null;
+      element.scrollIntoViewIfNeeded();
+      let rect = element.getBoundingClientRect();
+      return {
+        x: (rect.left + rect.right) / 2,
+        y: (rect.top + rect.bottom) / 2
+      };
+    }, selector);
+    if (!center)
+      throw new Error('No node found for selector: ' + selector);
+    await this._frameManager._mouse.move(center.x, center.y);
+  }
+
+  /**
+   * @param {string} selector
+   * @param {!Object=} options
+   * @return {!Promise}
+   */
+  async click(selector, options) {
+    await this.hover(selector);
+    await this._frameManager._mouse.press(options);
+    // This is a hack for now, to make clicking less race-prone
+    await this.evaluate(() => new Promise(f => requestAnimationFrame(f)));
+  }
+
   /**
    * @param {?Object} framePayload
    */
diff --git a/lib/Mouse.js b/lib/Mouse.js
new file mode 100644
index 00000000000..6826b17bdd1
--- /dev/null
+++ b/lib/Mouse.js
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2017 Google Inc. All rights reserved.
+ *
+ * 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.
+ */
+
+class Mouse {
+  /**
+   * @param {!Connection} client
+   * @param {!Keyboard} keyboard
+   */
+  constructor(client, keyboard) {
+    this._client = client;
+    this._keyboard = keyboard;
+    this._x = 0;
+    this._y = 0;
+    this._button = 'none';
+  }
+
+  /**
+   * @param {number} x
+   * @param {number} y
+   * @return {!Promise}
+   */
+  async move(x, y) {
+    this._x = x;
+    this._y = y;
+    await this._client.send('Input.dispatchMouseEvent', {
+      type: 'mouseMoved',
+      button: this._button,
+      x, y,
+      modifiers: this._modifiersMask()
+    });
+  }
+
+  /**
+   * @param {!Object=} options
+   */
+  async press(options) {
+    await this.down(options);
+    await this.up(options);
+  }
+
+  /**
+   * @param {!Object=} options
+   */
+  async down(options) {
+    if (!options)
+      options = {};
+    this._button = (options.button || 'left');
+    await this._client.send('Input.dispatchMouseEvent', {
+      type: 'mousePressed',
+      button: this._button,
+      x: this._x,
+      y: this._y,
+      modifiers: this._modifiersMask(),
+      clickCount: (options.clickCount || 1)
+    });
+  }
+
+  /**
+   * @param {!Object=} options
+   */
+  async up(options) {
+    if (!options)
+      options = {};
+    this._button = 'none';
+    await this._client.send('Input.dispatchMouseEvent', {
+      type: 'mouseReleased',
+      button: (options.button || 'left'),
+      x: this._x,
+      y: this._y,
+      modifiers: this._modifiersMask(),
+      clickCount: (options.clickCount || 1)
+    });
+  }
+
+  /**
+   * @return {number}
+   */
+  _modifiersMask() {
+    let modifiers = this._keyboard.modifiers();
+    let mask = 0;
+    if (modifiers.Alt)
+      mask += 1;
+    if (modifiers.Control)
+      mask += 2;
+    if (modifiers.Meta)
+      mask += 4;
+    if (modifiers.Shift)
+      mask += 8;
+    return mask;
+  }
+}
+
+module.exports = Mouse;
\ No newline at end of file
diff --git a/lib/Page.js b/lib/Page.js
index e7485b836df..b235a603b11 100644
--- a/lib/Page.js
+++ b/lib/Page.js
@@ -23,6 +23,7 @@ let Dialog = require('./Dialog');
 let EmulationManager = require('./EmulationManager');
 let FrameManager = require('./FrameManager');
 let Keyboard = require('./Keyboard');
+let Mouse = require('./Mouse');
 let helper = require('./helper');
 
 class Page extends EventEmitter {
@@ -40,9 +41,11 @@ class Page extends EventEmitter {
     ]);
     let userAgentExpression = helper.evaluationString(() => window.navigator.userAgent);
     let {result:{value: userAgent}} = await client.send('Runtime.evaluate', { expression: userAgentExpression, returnByValue: true });
-    let frameManager = await FrameManager.create(client);
+    let keyboard = new Keyboard(client);
+    let mouse = new Mouse(client, keyboard);
+    let frameManager = await FrameManager.create(client, mouse);
     let networkManager = new NetworkManager(client, userAgent);
-    let page = new Page(client, frameManager, networkManager, screenshotTaskQueue);
+    let page = new Page(client, frameManager, networkManager, screenshotTaskQueue, mouse, keyboard);
     // Initialize default page size.
     await page.setViewport({width: 400, height: 300});
     return page;
@@ -53,8 +56,10 @@ class Page extends EventEmitter {
    * @param {!FrameManager} frameManager
    * @param {!NetworkManager} networkManager
    * @param {!TaskQueue} screenshotTaskQueue
+   * @param {!Mouse} mouse
+   * @param {!Keyboard} keyboard
    */
-  constructor(client, frameManager, networkManager, screenshotTaskQueue) {
+  constructor(client, frameManager, networkManager, screenshotTaskQueue, mouse, keyboard) {
     super();
     this._client = client;
     this._frameManager = frameManager;
@@ -62,7 +67,8 @@ class Page extends EventEmitter {
     /** @type {!Map<string, function>} */
     this._inPageCallbacks = new Map();
 
-    this._keyboard = new Keyboard(this._client);
+    this._keyboard = keyboard;
+    this._mouse = mouse;
 
     this._screenshotTaskQueue = screenshotTaskQueue;
 
@@ -527,41 +533,28 @@ class Page extends EventEmitter {
     return nodeId;
   }
 
+  /**
+   * @return {!Mouse}
+   */
+  get mouse() {
+    return this._mouse;
+  }
+
   /**
    * @param {string} selector
+   * @param {!Object} options
    * @return {!Promise}
    */
-  async click(selector) {
-    let center = await this.evaluate(selector => {
-      let node = document.querySelector(selector);
-      if (!node)
-        return null;
-      let rect = node.getBoundingClientRect();
-      return {
-        x: (rect.left + rect.right) / 2,
-        y: (rect.top + rect.bottom) / 2
-      };
-    }, selector);
-    if (!center)
-      throw new Error('No node found for selector: ' + selector);
-    let x = Math.round(center.x);
-    let y = Math.round(center.y);
-    this._client.send('Input.dispatchMouseEvent', {
-      type: 'mouseMoved',
-      x, y
-    });
-    this._client.send('Input.dispatchMouseEvent', {
-      type: 'mousePressed',
-      button: 'left',
-      x, y,
-      clickCount: 1
-    });
-    await this._client.send('Input.dispatchMouseEvent', {
-      type: 'mouseReleased',
-      button: 'left',
-      x, y,
-      clickCount: 1
-    });
+  async click(selector, options) {
+    await this.mainFrame().click(selector, options);
+  }
+
+  /**
+   * @param {string} selector
+   * @param {!Promise}
+   */
+  async hover(selector) {
+    await this.mainFrame().hover(selector);
   }
 
   /**
diff --git a/phantom_shim/WebPage.js b/phantom_shim/WebPage.js
index 9a934cbecca..723a5e706e9 100644
--- a/phantom_shim/WebPage.js
+++ b/phantom_shim/WebPage.js
@@ -77,6 +77,9 @@ class WebPage {
         Backspace: ['Backspace'],
         Cut: ['Cut'],
         Paste: ['Paste']
+      },
+      modifier: {
+        shift: 'Shift'
       }
     };
   }
@@ -401,6 +404,8 @@ class WebPage {
   sendEvent(eventType, ...args) {
     if (eventType.startsWith('key'))
       this._sendKeyboardEvent.apply(this, arguments);
+    else
+      this._sendMouseEvent.apply(this, arguments);
   }
 
   /**
@@ -449,6 +454,40 @@ class WebPage {
     }
   }
 
+  /**
+   * @param {string} eventType
+   * @param {number} x
+   * @param {number} y
+   * @param {string|undefined} button
+   * @param {number|undefined} modifier
+   */
+  _sendMouseEvent(eventType, x, y, button, modifier) {
+    if (modifier)
+      await(this._page.keyboard.down(modifier));
+    await(this._page.mouse.move(x, y));
+    switch (eventType) {
+      case 'mousemove':
+        break;
+      case 'mousedown':
+        await(this._page.mouse.down({button}));
+        break;
+      case 'mouseup':
+        await(this._page.mouse.up({button}));
+        break;
+      case 'doubleclick':
+        await(this._page.mouse.press({button}));
+        await(this._page.mouse.press({button, clickCount: 2}));
+        break;
+      case 'click':
+        await(this._page.mouse.press({button}));
+        break;
+      case 'contextmenu':
+        await(this._page.mouse.press({button: 'right'}));
+        break;
+    }
+    if (modifier)
+      await(this._page.keyboard.up(modifier));
+  }
   /**
    * @param {string} html
    * @param {function()=} callback
diff --git a/test/assets/input/button.html b/test/assets/input/button.html
index 8b59f762d97..d4c6e13fd28 100644
--- a/test/assets/input/button.html
+++ b/test/assets/input/button.html
@@ -4,6 +4,7 @@
     <title>Button test</title>
   </head>
   <body>
+    <script src="mouse-helper.js"></script>
     <button onclick="clicked();">Click target</button>
     <script>
       window.result = 'Was not clicked';
diff --git a/test/assets/input/mouse-helper.js b/test/assets/input/mouse-helper.js
new file mode 100644
index 00000000000..c7ef8e2e11c
--- /dev/null
+++ b/test/assets/input/mouse-helper.js
@@ -0,0 +1,62 @@
+// This injects a box into the page that moves with the mouse;
+// Useful for debugging
+(function(){
+  let box = document.createElement('div');
+  box.classList.add('mouse-helper');
+  let styleElement = document.createElement('style');
+  styleElement.innerHTML = `
+  .mouse-helper {
+    pointer-events: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 20px;
+    height: 20px;
+    background: rgba(0,0,0,.4);
+    border: 1px solid white;
+    border-radius: 10px;
+    margin-left: -10px;
+    margin-top: -10px;
+    transition: background .2s, border-radius .2s, border-color .2s;
+  }
+  .mouse-helper.button-1 {
+    transition: none;
+    background: rgba(0,0,0,0.9);
+  }
+  .mouse-helper.button-2 {
+    transition: none;
+    border-color: rgba(0,0,255,0.9);
+  }
+  .mouse-helper.button-3 {
+    transition: none;
+    border-radius: 4px;
+  }
+  .mouse-helper.button-4 {
+    transition: none;
+    border-color: rgba(255,0,0,0.9);
+  }
+  .mouse-helper.button-5 {
+    transition: none;
+    border-color: rgba(0,255,0,0.9);
+  }
+  `;
+  document.head.appendChild(styleElement);
+  document.body.appendChild(box);
+  document.addEventListener('mousemove', event => {
+    box.style.left = event.pageX + 'px';
+    box.style.top = event.pageY + 'px';
+    updateButtons(event.buttons);
+  }, true);
+  document.addEventListener('mousedown', event => {
+    updateButtons(event.buttons);
+    box.classList.add('button-' + event.which);
+  }, true);
+  document.addEventListener('mouseup', event => {
+    updateButtons(event.buttons);
+    box.classList.remove('button-' + event.which);
+  }, true);
+  function updateButtons(buttons) {
+    for (let i = 0; i < 5; i++)
+      box.classList.toggle('button-' + i, buttons & (1 << i));
+  }
+})();
diff --git a/test/assets/input/scrollable.html b/test/assets/input/scrollable.html
new file mode 100644
index 00000000000..885d3739d5f
--- /dev/null
+++ b/test/assets/input/scrollable.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Scrollable test</title>
+  </head>
+  <body>
+    <script src='mouse-helper.js'></script>
+    <script>
+        for (let i = 0; i < 100; i++) {
+            let button = document.createElement('button');
+            button.textContent = i + ': not clicked';
+            button.id = 'button-' + i;
+            button.onclick = () => button.textContent = 'clicked';
+            button.oncontextmenu = event => {
+              event.preventDefault();
+              button.textContent = 'context menu';
+            }
+            document.body.appendChild(button);
+            document.body.appendChild(document.createElement('br'));
+        }
+    </script>
+  </body>
+</html>
\ No newline at end of file
diff --git a/test/assets/input/textarea.html b/test/assets/input/textarea.html
index b6360739bda..6d5be760740 100644
--- a/test/assets/input/textarea.html
+++ b/test/assets/input/textarea.html
@@ -5,6 +5,7 @@
   </head>
   <body>
     <textarea></textarea>
+    <script src='mouse-helper.js'></script>
     <script>
       window.result = '';
       let textarea = document.querySelector('textarea');
diff --git a/test/test.js b/test/test.js
index 58a40f921ea..5673f81373c 100644
--- a/test/test.js
+++ b/test/test.js
@@ -877,6 +877,88 @@ describe('Puppeteer', function() {
       expect(keyboard.modifiers().Shift).toBe(false);
       expect(keyboard.modifiers().Alt).toBe(false);
     }));
+    it('should resize the textarea', SX(async function(){
+      await page.navigate(PREFIX + '/input/textarea.html');
+      let {x, y, width, height} = await page.evaluate(dimensions);
+      let mouse = page.mouse;
+      await mouse.move(x + width - 4, y + height - 4);
+      await mouse.down();
+      await mouse.move(x + width + 100, y + height + 100);
+      await mouse.up();
+      let newDimensions = await page.evaluate(dimensions);
+      expect(newDimensions.width).toBe(width + 104);
+      expect(newDimensions.height).toBe(height + 104);
+    }));
+    it('should scroll and click the button', SX(async function(){
+      await page.navigate(PREFIX + '/input/scrollable.html');
+      await page.click('#button-5');
+      expect(await page.$('#button-5', button => button.textContent)).toBe('clicked');
+      await page.click('#button-80');
+      expect(await page.$('#button-80', button => button.textContent)).toBe('clicked');
+    }));
+    it('should select the text with mouse', SX(async function(){
+      await page.navigate(PREFIX + '/input/textarea.html');
+      await page.focus('textarea');
+      let text = 'This is the text that we are going to try to select. Let\'s see how it goes.';
+      await page.type(text);
+      await page.$('textarea', textarea => textarea.scrollTop = 0);
+      let {x, y} = await page.evaluate(dimensions);
+      await page.mouse.move(x + 2,y + 2);
+      await page.mouse.down();
+      await page.mouse.move(100,100);
+      await page.mouse.up();
+      expect(await page.evaluate(() => window.getSelection().toString())).toBe(text);
+    }));
+    it('should select the text by triple clicking', SX(async function(){
+      await page.navigate(PREFIX + '/input/textarea.html');
+      await page.focus('textarea');
+      let text = 'This is the text that we are going to try to select. Let\'s see how it goes.';
+      await page.type(text);
+      await page.click('textarea');
+      await page.click('textarea', {clickCount: 2});
+      await page.click('textarea', {clickCount: 3});
+      expect(await page.evaluate(() => window.getSelection().toString())).toBe(text);
+    }));
+    it('should trigger hover state', SX(async function(){
+      await page.navigate(PREFIX + '/input/scrollable.html');
+      await page.hover('#button-6');
+      expect(await page.$('button:hover', button => button.id)).toBe('button-6');
+      await page.hover('#button-2');
+      expect(await page.$('button:hover', button => button.id)).toBe('button-2');
+      await page.hover('#button-91');
+      expect(await page.$('button:hover', button => button.id)).toBe('button-91');
+    }));
+    it('should fire contextmenu event on right click', SX(async function(){
+      await page.navigate(PREFIX + '/input/scrollable.html');
+      await page.click('#button-8', {button: 'right'});
+      expect(await page.$('#button-8', button => button.textContent)).toBe('context menu');
+    }));
+    it('should set modifier keys on click', SX(async function(){
+      await page.navigate(PREFIX + '/input/scrollable.html');
+      await page.$('#button-3', button => button.addEventListener('mousedown', e => window.lastEvent = e, true));
+      let modifiers = {'Shift': 'shiftKey', 'Control': 'ctrlKey', 'Alt': 'altKey', 'Meta': 'metaKey'};
+      for (let modifier in modifiers) {
+        await page.keyboard.down(modifier);
+        await page.click('#button-3');
+        if (!(await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier])))
+          fail(modifiers[modifier] + ' should be true');
+        await page.keyboard.up(modifier);
+      }
+      await page.click('#button-3');
+      for (let modifier in modifiers) {
+        if ((await page.evaluate(mod => window.lastEvent[mod], modifiers[modifier])))
+          fail(modifiers[modifier] + ' should be false');
+      }
+    }));
+    function dimensions() {
+      let rect = document.querySelector('textarea').getBoundingClientRect();
+      return {
+        x: rect.left,
+        y: rect.top,
+        width: rect.width,
+        height: rect.height
+      };
+    }
   });
 
   // FIXME: remove this when crbug.com/741689 is fixed.
diff --git a/third_party/phantomjs/test/module/webpage/contextclick-event.js b/third_party/phantomjs/test/module/webpage/contextclick-event.js
index af046110d89..7716ba51f9f 100644
--- a/third_party/phantomjs/test/module/webpage/contextclick-event.js
+++ b/third_party/phantomjs/test/module/webpage/contextclick-event.js
@@ -1,11 +1,10 @@
-//! unsupported
 test(function () {
     var page = require('webpage').create();
 
     page.evaluate(function() {
         window.addEventListener('contextmenu', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.contextmenu = event;
+            window.loggedEvent.contextmenu = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
     });
     page.sendEvent('contextmenu', 42, 217);
@@ -20,7 +19,7 @@ test(function () {
     page.evaluate(function() {
         window.addEventListener('contextmenu', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.contextmenu = event;
+            window.loggedEvent.contextmenu = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
     });
     page.sendEvent('contextmenu', 100, 100, 'left', page.event.modifier.shift);
diff --git a/third_party/phantomjs/test/module/webpage/mouseclick-event.js b/third_party/phantomjs/test/module/webpage/mouseclick-event.js
index 44734bfa680..917c91a487b 100644
--- a/third_party/phantomjs/test/module/webpage/mouseclick-event.js
+++ b/third_party/phantomjs/test/module/webpage/mouseclick-event.js
@@ -1,15 +1,14 @@
-//! unsupported
 test(function () {
     var page = require('webpage').create();
 
     page.evaluate(function() {
         window.addEventListener('mousedown', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.mousedown = event;
+            window.loggedEvent.mousedown = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
         window.addEventListener('mouseup', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.mouseup = event;
+            window.loggedEvent.mouseup = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
     });
     page.sendEvent('click', 42, 217);
@@ -26,7 +25,7 @@ test(function () {
     page.evaluate(function() {
         window.addEventListener('click', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.click = event;
+            window.loggedEvent.click = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
     });
     page.sendEvent('click', 100, 100, 'left', page.event.modifier.shift);
diff --git a/third_party/phantomjs/test/module/webpage/mousedoubleclick-event.js b/third_party/phantomjs/test/module/webpage/mousedoubleclick-event.js
index 3a777f9a72a..f98dd2cd750 100644
--- a/third_party/phantomjs/test/module/webpage/mousedoubleclick-event.js
+++ b/third_party/phantomjs/test/module/webpage/mousedoubleclick-event.js
@@ -1,4 +1,3 @@
-//! unsupported
 test(function () {
     var page = require('webpage').create();
 
@@ -19,7 +18,7 @@ test(function () {
     page.evaluate(function() {
         window.addEventListener('dblclick', function(event) {
             window.loggedEvent = window.loggedEvent || {};
-            window.loggedEvent.dblclick = event;
+            window.loggedEvent.dblclick = {clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey};
         }, false);
     });
     page.sendEvent('doubleclick', 100, 100, 'left', page.event.modifier.shift);
diff --git a/third_party/phantomjs/test/module/webpage/mousedown-event.js b/third_party/phantomjs/test/module/webpage/mousedown-event.js
index ee67275b58b..e79fa85af45 100644
--- a/third_party/phantomjs/test/module/webpage/mousedown-event.js
+++ b/third_party/phantomjs/test/module/webpage/mousedown-event.js
@@ -1,11 +1,10 @@
-//! unsupported
 test(function () {
     var page = require('webpage').create();
 
     page.evaluate(function() {
         window.addEventListener('mousedown', function(event) {
             window.loggedEvent = window.loggedEvent || [];
-            window.loggedEvent.push(event);
+            window.loggedEvent.push({clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey});
         }, false);
     });
 
diff --git a/third_party/phantomjs/test/module/webpage/mousemove-event.js b/third_party/phantomjs/test/module/webpage/mousemove-event.js
index c2beaadca1e..ece419a0a21 100644
--- a/third_party/phantomjs/test/module/webpage/mousemove-event.js
+++ b/third_party/phantomjs/test/module/webpage/mousemove-event.js
@@ -1,11 +1,10 @@
-//! unsupported
 test(function () {
     var page = require('webpage').create();
 
     page.evaluate(function() {
         window.addEventListener('mousemove', function(event) {
             window.loggedEvent = window.loggedEvent || [];
-            window.loggedEvent.push(event);
+            window.loggedEvent.push({clientX: event.clientX, clientY: event.clientY});
         }, false);
     });
 
diff --git a/third_party/phantomjs/test/module/webpage/mouseup-event.js b/third_party/phantomjs/test/module/webpage/mouseup-event.js
index 80c3496eca4..e4b973821f5 100644
--- a/third_party/phantomjs/test/module/webpage/mouseup-event.js
+++ b/third_party/phantomjs/test/module/webpage/mouseup-event.js
@@ -1,4 +1,3 @@
-//! unsupported
 test(function () {
     var webpage = require('webpage');
     var page = webpage.create();
@@ -6,7 +5,7 @@ test(function () {
     page.evaluate(function() {
         window.addEventListener('mouseup', function(event) {
             window.loggedEvent = window.loggedEvent || [];
-            window.loggedEvent.push(event);
+            window.loggedEvent.push({clientX: event.clientX, clientY: event.clientY, shiftKey: event.shiftKey});
         }, false);
     });
 
diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js
index 47d0cc6ba92..13a9b8104e6 100644
--- a/utils/doclint/lint.js
+++ b/utils/doclint/lint.js
@@ -22,6 +22,7 @@ const EXCLUDE_METHODS = new Set([
   'Headers.fromPayload',
   'InterceptedRequest.constructor',
   'Keyboard.constructor',
+  'Mouse.constructor',
   'Page.constructor',
   'Page.create',
   'Request.constructor',