Skip to content

Commit

Permalink
feat(Input): Add keyboard methods to elementHandle (#801)
Browse files Browse the repository at this point in the history
This patch:
- adds input methods to ElementHandle, such as ElementHandle.type and ElementHandle.press
- changes `page.type` to accept selector as the first argument
- removes `page.press` method. The `page.press` is rarely used and doesn't operate with selectors; if there's a need to press a button, `page.keyboard.press` should be used.

BREAKING CHANGE: `page.type` is changed, `page.press` is removed.

Fixes #241.
  • Loading branch information
JoelEinbinder authored and aslushnikov committed Oct 7, 2017
1 parent 0d0f9b7 commit 0af0d7d
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 86 deletions.
91 changes: 70 additions & 21 deletions docs/api.md
Expand Up @@ -59,7 +59,6 @@
+ [page.mouse](#pagemouse)
+ [page.pdf(options)](#pagepdfoptions)
+ [page.plainText()](#pageplaintext)
+ [page.press(key[, options])](#pagepresskey-options)
+ [page.reload(options)](#pagereloadoptions)
+ [page.screenshot([options])](#pagescreenshotoptions)
+ [page.select(selector, ...values)](#pageselectselector-values)
Expand All @@ -74,7 +73,7 @@
+ [page.title()](#pagetitle)
+ [page.touchscreen](#pagetouchscreen)
+ [page.tracing](#pagetracing)
+ [page.type(text, options)](#pagetypetext-options)
+ [page.type(selector, text[, options])](#pagetypeselector-text-options)
+ [page.url()](#pageurl)
+ [page.viewport()](#pageviewport)
+ [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args)
Expand All @@ -83,7 +82,9 @@
+ [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options)
* [class: Keyboard](#class-keyboard)
+ [keyboard.down(key[, options])](#keyboarddownkey-options)
+ [keyboard.press(key[, options])](#keyboardpresskey-options)
+ [keyboard.sendCharacter(char)](#keyboardsendcharacterchar)
+ [keyboard.type(text, options)](#keyboardtypetext-options)
+ [keyboard.up(key)](#keyboardupkey)
* [class: Mouse](#class-mouse)
+ [mouse.click(x, y, [options])](#mouseclickx-y-options)
Expand Down Expand Up @@ -139,12 +140,15 @@
+ [elementHandle.click([options])](#elementhandleclickoptions)
+ [elementHandle.dispose()](#elementhandledispose)
+ [elementHandle.executionContext()](#elementhandleexecutioncontext)
+ [elementHandle.focus()](#elementhandlefocus)
+ [elementHandle.getProperties()](#elementhandlegetproperties)
+ [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname)
+ [elementHandle.hover()](#elementhandlehover)
+ [elementHandle.jsonValue()](#elementhandlejsonvalue)
+ [elementHandle.press(key[, options])](#elementhandlepresskey-options)
+ [elementHandle.tap()](#elementhandletap)
+ [elementHandle.toString()](#elementhandletostring)
+ [elementHandle.type(text[, options])](#elementhandletypetext-options)
+ [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths)
* [class: Request](#class-request)
+ [request.abort()](#requestabort)
Expand Down Expand Up @@ -759,15 +763,6 @@ The `format` options are:
#### page.plainText()
- returns: <[Promise]<[string]>> Returns page's inner text.

#### page.press(key[, options])
- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/)
- `options` <[Object]>
- `text` <[string]> If specified, generates an input event with this text.
- `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
- returns: <[Promise]>

Shortcut for [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey).

#### page.reload(options)
- `options` <[Object]> Navigation parameters which might have the following properties:
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout.
Expand Down Expand Up @@ -896,19 +891,20 @@ Shortcut for [page.mainFrame().title()](#frametitle).
#### page.tracing
- returns: <[Tracing]>

#### page.type(text, options)
#### page.type(selector, text[, options])
- `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
- `text` <[string]> A text to type into a focused element.
- `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
- returns: <[Promise]>

Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

To press a special key, use [`page.press`](#pagepresskey-options).
To press a special key, like `Control` or `ArrowDown`, use [`keyboard.press`](#pagekeyboardpresskey-options).

```js
page.type('Hello'); // Types instantly
page.type('World', {delay: 100}); // Types slower, like a user
page.type('#mytextarea', 'Hello'); // Types instantly
page.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user
```

#### page.url()
Expand Down Expand Up @@ -1003,21 +999,21 @@ Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitf

### class: Keyboard

Keyboard provides an api for managing a virtual keyboard. The high level api is [`page.type`](#pagetypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.
Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page.

For finer control, you can use [`keyboard.down`](#keyboarddownkey-options), [`keyboard.up`](#keyboardupkey), and [`keyboard.sendCharacter`](#keyboardsendcharacterchar) to manually fire events as if they were generated from a real keyboard.

An example of holding down `Shift` in order to select and delete some text:
```js
page.type('Hello World!');
page.press('ArrowLeft');
page.keyboard.type('Hello World!');
page.keyboard.press('ArrowLeft');

page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
page.press('ArrowLeft');
page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');

page.press('Backspace');
page.keyboard.press('Backspace');
// Result text will end up saying 'Hello!'
```

Expand All @@ -1035,6 +1031,15 @@ If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, subsequent key

After the key is pressed once, subsequent calls to [`keyboard.down`](#keyboarddownkey-options) will have [repeat](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat) set to true. To release the key, use [`keyboard.up`](#keyboardupkey).

#### keyboard.press(key[, options])
- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/)
- `options` <[Object]>
- `text` <[string]> If specified, generates an input event with this text.
- `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
- returns: <[Promise]>

Shortcut for [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey).

#### keyboard.sendCharacter(char)
- `char` <[string]> Character to send into the page.
- returns: <[Promise]>
Expand All @@ -1045,6 +1050,21 @@ Dispatches a `keypress` and `input` event. This does not send a `keydown` or `ke
page.keyboard.sendCharacter('');
```

#### keyboard.type(text, options)
- `text` <[string]> A text to type into a focused element.
- `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
- returns: <[Promise]>

Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

To press a special key, like `Control` or `ArrowDown`, use [`keyboard.press`](#keyboardpresskey-options).

```js
page.keyboard.type('Hello'); // Types instantly
page.keyboard.type('World', {delay: 100}); // Types slower, like a user
```

#### keyboard.up(key)
- `key` <[string]> Name of key to release, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/)
- returns: <[Promise]>
Expand Down Expand Up @@ -1396,7 +1416,7 @@ const twoHandle = await executionContext.evaluateHandle(() => 2);
const result = await executionContext.evaluate((a, b) => a + b, oneHandle, twoHandle);
await oneHandle.dispose();
await twoHandle.dispose();
console.log(result); // prints '3'.
console.log(result); // prints '3'.
```

#### executionContext.evaluateHandle(pageFunction, ...args)
Expand Down Expand Up @@ -1527,6 +1547,11 @@ The `elementHandle.dispose` method stops referencing the element handle.
#### elementHandle.executionContext()
- returns: [ExecutionContext]

#### elementHandle.focus()
- returns: <[Promise]>

Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element.

#### elementHandle.getProperties()
- returns: <[Promise]<[Map]<[string], [JSHandle]>>>

Expand Down Expand Up @@ -1563,6 +1588,15 @@ Returns a JSON representation of the object. The JSON is generated by running [`

> **NOTE** The method will throw if the referenced object is not stringifiable.
#### elementHandle.press(key[, options])
- `key` <[string]> Name of key to press, such as `ArrowLeft`. See [KeyboardEvent.key](https://www.w3.org/TR/uievents-key/)
- `options` <[Object]>
- `text` <[string]> If specified, generates an input event with this text.
- `delay` <[number]> Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
- returns: <[Promise]>

Focuses the element, and then uses [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey).

#### elementHandle.tap()
- returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM.

Expand All @@ -1572,6 +1606,21 @@ If the element is detached from DOM, the method throws an error.
#### elementHandle.toString()
- returns: <[string]>

#### elementHandle.type(text[, options])
- `text` <[string]> A text to type into a focused element.
- `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
- returns: <[Promise]>

Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text.

To press a special key, like `Control` or `ArrowDown`, use [`elementHandle.press`](#elementhandlepresskey-options).

```js
elementHandle.type('Hello'); // Types instantly
elementHandle.type('World', {delay: 100}); // Types slower, like a user
```

#### 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]>
Expand Down
39 changes: 31 additions & 8 deletions lib/ElementHandle.js
Expand Up @@ -22,13 +22,14 @@ class ElementHandle extends JSHandle {
* @param {!ExecutionContext} context
* @param {!Session} client
* @param {!Object} remoteObject
* @param {!Mouse} mouse
* @param {!Touchscreen} touchscreen;
* @param {!Page} page
*/
constructor(context, client, remoteObject, mouse, touchscreen) {
constructor(context, client, remoteObject, page) {
super(context, client, remoteObject);
this._mouse = mouse;
this._touchscreen = touchscreen;
this._client = client;
this._remoteObject = remoteObject;
this._page = page;
this._disposed = false;
}

/**
Expand Down Expand Up @@ -63,15 +64,15 @@ class ElementHandle extends JSHandle {

async hover() {
const {x, y} = await this._visibleCenter();
await this._mouse.move(x, y);
await this._page.mouse.move(x, y);
}

/**
* @param {!Object=} options
*/
async click(options) {
const {x, y} = await this._visibleCenter();
await this._mouse.click(x, y, options);
await this._page.mouse.click(x, y, options);
}

/**
Expand All @@ -86,7 +87,29 @@ class ElementHandle extends JSHandle {

async tap() {
const {x, y} = await this._visibleCenter();
await this._touchscreen.tap(x, y);
await this._page.touchscreen.tap(x, y);
}

async focus() {
await this.executionContext().evaluate(element => element.focus(), this);
}

/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options) {
await this.focus();
await this._page.keyboard.type(text, options);
}

/**
* @param {string} key
* @param {!Object=} options
*/
async press(key, options) {
await this.focus();
await this._page.keyboard.press(key, options);
}
}

Expand Down
22 changes: 8 additions & 14 deletions lib/FrameManager.js
Expand Up @@ -23,15 +23,12 @@ const ElementHandle = require('./ElementHandle');
class FrameManager extends EventEmitter {
/**
* @param {!Session} client
* @param {!Object} frameTree
* @param {!Mouse} mouse
* @param {!Touchscreen} touchscreen
* @param {!Page} page
*/
constructor(client, mouse, touchscreen) {
constructor(client, page) {
super();
this._client = client;
this._mouse = mouse;
this._touchscreen = touchscreen;
this._page = page;
/** @type {!Map<string, !Frame>} */
this._frames = new Map();
/** @type {!Map<string, !ExecutionContext>} */
Expand Down Expand Up @@ -67,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, this._touchscreen, parentFrame, frameId);
const frame = new Frame(this._client, this._page, parentFrame, frameId);
this._frames.set(frame._id, frame);
this.emit(FrameManager.Events.FrameAttached, frame);
}
Expand All @@ -94,7 +91,7 @@ class FrameManager extends EventEmitter {
frame._id = framePayload.id;
} else {
// Initial main frame navigation.
frame = new Frame(this._client, this._mouse, this._touchscreen, null, framePayload.id);
frame = new Frame(this._client, this._page, null, framePayload.id);
}
this._frames.set(framePayload.id, frame);
this._mainFrame = frame;
Expand Down Expand Up @@ -141,7 +138,7 @@ class FrameManager extends EventEmitter {
const context = this._contextIdToContext.get(contextId);
console.assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
if (remoteObject.subtype === 'node')
return new ElementHandle(context, this._client, remoteObject, this._mouse, this._touchscreen);
return new ElementHandle(context, this._client, remoteObject, this._page);
return new JSHandle(context, this._client, remoteObject);
}

Expand Down Expand Up @@ -178,15 +175,12 @@ FrameManager.Events = {
class Frame {
/**
* @param {!Session} client
* @param {!Mouse} mouse
* @param {!Touchscreen} touchscreen
* @param {?Frame} parentFrame
* @param {string} frameId
*/
constructor(client, mouse, touchscreen, parentFrame, frameId) {
constructor(client, page, parentFrame, frameId) {
this._client = client;
this._mouse = mouse;
this._touchscreen = touchscreen;
this._page = page;
this._parentFrame = parentFrame;
this._url = '';
this._id = frameId;
Expand Down
26 changes: 26 additions & 0 deletions lib/Input.js
Expand Up @@ -88,6 +88,32 @@ class Keyboard {
unmodifiedText: char
});
}

/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options) {
let delay = 0;
if (options && options.delay)
delay = options.delay;
for (const char of text) {
await this.press(char, {text: char, delay});
if (delay)
await new Promise(f => setTimeout(f, delay));
}
}

/**
* @param {string} key
* @param {!Object=} options
*/
async press(key, options) {
await this.down(key, options);
if (options && options.delay)
await new Promise(f => setTimeout(f, options.delay));
await this.up(key);
}
}

class Mouse {
Expand Down

0 comments on commit 0af0d7d

Please sign in to comment.