From 64124df62f4e81999fe1a0ab45c6fb9718a0e413 Mon Sep 17 00:00:00 2001 From: JoelEinbinder Date: Fri, 1 Sep 2017 19:03:51 -0700 Subject: [PATCH] [api] add touchScreen.tap (#639) This patch: - adds `page.touchscreen` namespace, similar to `page.mouse` and `page.keyboard`. - adds tapping to multiple layers: - `page.touchscreen.tap` - `page.tap` - convenience method which accepts selector - `elementHandle.tap` Fixes #568 and #569. --- docs/api.md | 31 ++++++++++++++++++++++ lib/ElementHandle.js | 9 ++++++- lib/FrameManager.js | 16 ++++++----- lib/Input.js | 32 +++++++++++++++++++++- lib/Page.js | 22 ++++++++++++++-- test/assets/input/touches.html | 35 +++++++++++++++++++++++++ test/test.js | 11 ++++++++ utils/doclint/check_public_api/index.js | 1 + 8 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 test/assets/input/touches.html diff --git a/docs/api.md b/docs/api.md index de388e24..9dd9282e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -65,7 +65,9 @@ + [page.setRequestInterceptionEnabled(value)](#pagesetrequestinterceptionenabledvalue) + [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + [page.setViewport(viewport)](#pagesetviewportviewport) + + [page.tap(selector)](#pagetapselector) + [page.title()](#pagetitle) + + [page.touchscreen](#pagetouchscreen) + [page.tracing](#pagetracing) + [page.type(text, options)](#pagetypetext-options) + [page.url()](#pageurl) @@ -83,6 +85,8 @@ + [mouse.down([options])](#mousedownoptions) + [mouse.move(x, y, [options])](#mousemovex-y-options) + [mouse.up([options])](#mouseupoptions) + * [class: Touchscreen](#class-touchscreen) + + [touchscreen.tap(x, y)](#touchscreentapx-y) * [class: Tracing](#class-tracing) + [tracing.start(options)](#tracingstartoptions) + [tracing.stop()](#tracingstop) @@ -113,6 +117,7 @@ + [elementHandle.dispose()](#elementhandledispose) + [elementHandle.evaluate(pageFunction, ...args)](#elementhandleevaluatepagefunction-args) + [elementHandle.hover()](#elementhandlehover) + + [elementHandle.tap()](#elementhandletap) + [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) * [class: Request](#class-request) + [request.abort()](#requestabort) @@ -778,11 +783,21 @@ puppeteer.launch().then(async browser => { In the case of multiple pages in a single browser, each page can have its own viewport size. +#### page.tap(selector) +- `selector` <[string]> A [selector] to search for element to tap. If there are multiple elements satisfying the selector, the first will be tapped. +- returns: <[Promise]> + +This method fetches an element with `selector`, scrolls it into view if needed, and then uses [page.touchscreen](#pagetouchscreen) to tap in the center of the element. +If there's no element matching `selector`, the method throws an error. + #### page.title() - returns: <[Promise]<[string]>> Returns page's title. Shortcut for [page.mainFrame().title()](#frametitle). +#### page.touchscreen +- returns: <[Touchscreen]> + #### page.tracing - returns: <[Tracing]> @@ -978,6 +993,15 @@ Dispatches a `mousemove` event. Dispatches a `mouseup` event. +### class: Touchscreen + +#### touchscreen.tap(x, y) +- `x` <[number]> +- `y` <[number]> +- returns: <[Promise]> + +Dispatches a `touchstart` and `touchend` event. + ### class: Tracing You can use [`tracing.start`](#tracingstartoptions) and [`tracing.stop`](#tracingstop) to create a trace file which can be opened in Chrome DevTools or [timeline viewer](https://chromedevtools.github.io/timeline-viewer/). @@ -1266,6 +1290,12 @@ The element will be passed as the first argument to `pageFunction`, followed by This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element. If the element is detached from DOM, the method throws an error. +#### elementHandle.tap() +- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM. + +This method scrolls element into view if needed, and then uses [touchscreen.tap](#touchscreentapx-y) to tap in the center of the element. +If the element is detached from DOM, the method throws an error. + #### elementHandle.uploadFile(...filePaths) - `...filePaths` <...[string]> Sets the value of the file input these paths. If some of the `filePaths` are relative paths, then they are resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). - returns: <[Promise]> @@ -1388,3 +1418,4 @@ Contains the URL of the response. [ElementHandle]: #class-elementhandle "ElementHandle" [UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail" [Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" +[Touchscreen]: #class-touchscreen "Touchscreen" diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index 697e4a40..4c2ba64c 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -21,11 +21,13 @@ class ElementHandle { * @param {!Connection} client * @param {!Object} remoteObject * @param {!Mouse} mouse + * @param {!Touchscreen} touchscreen; */ - constructor(client, remoteObject, mouse) { + constructor(client, remoteObject, mouse, touchscreen) { this._client = client; this._remoteObject = remoteObject; this._mouse = mouse; + this._touchscreen = touchscreen; this._disposed = false; } @@ -96,6 +98,11 @@ class ElementHandle { const objectId = this._remoteObject.objectId; return this._client.send('DOM.setFileInputFiles', { objectId, files }); } + + async tap() { + const {x, y} = await this._visibleCenter(); + await this._touchscreen.tap(x, y); + } } module.exports = ElementHandle; diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 5e632b03..14882ec0 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -24,11 +24,13 @@ class FrameManager extends EventEmitter { * @param {!Session} client * @param {!Object} frameTree * @param {!Mouse} mouse + * @param {!Touchscreen} touchscreen */ - constructor(client, mouse) { + constructor(client, mouse, touchscreen) { super(); this._client = client; this._mouse = mouse; + this._touchscreen = touchscreen; /** @type {!Map} */ this._frames = new Map(); @@ -62,7 +64,7 @@ class FrameManager extends EventEmitter { return; console.assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new Frame(this._client, this._mouse, parentFrame, frameId); + const frame = new Frame(this._client, this._mouse, this._touchscreen, parentFrame, frameId); this._frames.set(frame._id, frame); this.emit(FrameManager.Events.FrameAttached, frame); } @@ -89,7 +91,7 @@ class FrameManager extends EventEmitter { frame._id = framePayload.id; } else { // Initial main frame navigation. - frame = new Frame(this._client, this._mouse, null, framePayload.id); + frame = new Frame(this._client, this._mouse, this._touchscreen, null, framePayload.id); } this._frames.set(framePayload.id, frame); this._mainFrame = frame; @@ -154,12 +156,14 @@ class Frame { /** * @param {!Session} client * @param {!Mouse} mouse + * @param {!Touchscreen} touchscreen * @param {?Frame} parentFrame * @param {string} frameId */ - constructor(client, mouse, parentFrame, frameId) { + constructor(client, mouse, touchscreen, parentFrame, frameId) { this._client = client; this._mouse = mouse; + this._touchscreen = touchscreen; this._parentFrame = parentFrame; this._url = ''; this._id = frameId; @@ -190,7 +194,7 @@ class Frame { async $(selector) { const remoteObject = await this._rawEvaluate(selector => document.querySelector(selector), selector); if (remoteObject.subtype === 'node') - return new ElementHandle(this._client, remoteObject, this._mouse); + return new ElementHandle(this._client, remoteObject, this._mouse, this._touchscreen); await helper.releaseObject(this._client, remoteObject); return null; } @@ -225,7 +229,7 @@ class Frame { const releasePromises = [helper.releaseObject(this._client, remoteObject)]; for (const property of properties) { if (property.enumerable && property.value.subtype === 'node') - result.push(new ElementHandle(this._client, property.value, this._mouse)); + result.push(new ElementHandle(this._client, property.value, this._mouse, this._touchscreen)); else releasePromises.push(helper.releaseObject(this._client, property.value)); } diff --git a/lib/Input.js b/lib/Input.js index 8e93e478..ef6a7e43 100644 --- a/lib/Input.js +++ b/lib/Input.js @@ -169,6 +169,35 @@ class Mouse { } } +class Touchscreen { + /** + * @param {Session} client + * @param {Keyboard} keyboard + */ + constructor(client, keyboard) { + this._client = client; + this._keyboard = keyboard; + } + + /** + * @param {number} x + * @param {number} y + */ + async tap(x, y) { + const touchPoints = [{x: Math.round(x), y: Math.round(y)}]; + await this._client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints, + modifiers: this._keyboard._modifiers + }); + await this._client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + modifiers: this._keyboard._modifiers + }); + } +} + const keys = { 'Cancel': 3, 'Help': 6, @@ -288,6 +317,7 @@ function codeForKey(key) { return 0; } -module.exports = { Keyboard, Mouse }; +module.exports = { Keyboard, Mouse, Touchscreen}; helper.tracePublicAPI(Keyboard); helper.tracePublicAPI(Mouse); +helper.tracePublicAPI(Touchscreen); diff --git a/lib/Page.js b/lib/Page.js index 90e75373..8af47b21 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -22,7 +22,7 @@ const NavigatorWatcher = require('./NavigatorWatcher'); const Dialog = require('./Dialog'); const EmulationManager = require('./EmulationManager'); const FrameManager = require('./FrameManager'); -const {Keyboard, Mouse} = require('./Input'); +const {Keyboard, Mouse, Touchscreen} = require('./Input'); const Tracing = require('./Tracing'); const helper = require('./helper'); @@ -60,7 +60,8 @@ class Page extends EventEmitter { this._client = client; this._keyboard = new Keyboard(client); this._mouse = new Mouse(client, this._keyboard); - this._frameManager = new FrameManager(client, this._mouse); + this._touchscreen = new Touchscreen(client, this._keyboard); + this._frameManager = new FrameManager(client, this._mouse, this._touchscreen); this._networkManager = new NetworkManager(client); this._emulationManager = new EmulationManager(client); this._tracing = new Tracing(client); @@ -105,6 +106,23 @@ class Page extends EventEmitter { return this._keyboard; } + /** + * @return {!Touchscreen} + */ + get touchscreen() { + return this._touchscreen; + } + + /** + * @param {string} selector + */ + async tap(selector) { + const handle = await this.$(selector); + console.assert(handle, 'No node found for selector: ' + selector); + await handle.tap(); + await handle.dispose(); + } + /** * @return {!Tracing} */ diff --git a/test/assets/input/touches.html b/test/assets/input/touches.html new file mode 100644 index 00000000..4392cfac --- /dev/null +++ b/test/assets/input/touches.html @@ -0,0 +1,35 @@ + + + + Touch test + + + + + + + \ No newline at end of file diff --git a/test/test.js b/test/test.js index fb83be8c..c0d56913 100644 --- a/test/test.js +++ b/test/test.js @@ -1509,6 +1509,17 @@ describe('Page', function() { [200, 300] ]); })); + it('should tap the button', SX(async function() { + await page.goto(PREFIX + '/input/button.html'); + await page.tap('button'); + expect(await page.evaluate(() => result)).toBe('Clicked'); + })); + it('should report touches', SX(async function() { + await page.goto(PREFIX + '/input/touches.html'); + const button = await page.$('button'); + await button.tap(); + expect(await page.evaluate(() => getResult())).toEqual(['Touchstart: 0', 'Touchend: 0']); + })); function dimensions() { const rect = document.querySelector('textarea').getBoundingClientRect(); return { diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index 3d482d44..d7205ce0 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -44,6 +44,7 @@ const EXCLUDE_METHODS = new Set([ 'Headers.fromPayload', 'Keyboard.constructor', 'Mouse.constructor', + 'Touchscreen.constructor', 'Tracing.constructor', 'Page.constructor', 'Page.create',