feat(Input): Add keyboard methods to elementHandle (#801)

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.
This commit is contained in:
JoelEinbinder 2017-10-07 00:28:24 -07:00 committed by Andrey Lushnikov
parent 0d0f9b7984
commit 0af0d7dba5
6 changed files with 161 additions and 86 deletions

View File

@ -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)
@ -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)
@ -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)
@ -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)
@ -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.
@ -896,7 +891,8 @@ 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.
@ -904,11 +900,11 @@ Shortcut for [page.mainFrame().title()](#frametitle).
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()
@ -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!'
```
@ -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]>
@ -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]>
@ -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)
@ -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]>>>
@ -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.
@ -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]>

View File

@ -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;
}
/**
@ -63,7 +64,7 @@ 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);
}
/**
@ -71,7 +72,7 @@ class ElementHandle extends JSHandle {
*/
async click(options) {
const {x, y} = await this._visibleCenter();
await this._mouse.click(x, y, options);
await this._page.mouse.click(x, y, options);
}
/**
@ -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);
}
}

View File

@ -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>} */
@ -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);
}
@ -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;
@ -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);
}
@ -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;

View File

@ -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 {

View File

@ -63,7 +63,7 @@ class Page extends EventEmitter {
this._keyboard = new Keyboard(client);
this._mouse = new Mouse(client, this._keyboard);
this._touchscreen = new Touchscreen(client, this._keyboard);
this._frameManager = new FrameManager(client, this._mouse, this._touchscreen);
this._frameManager = new FrameManager(client, this);
this._networkManager = new NetworkManager(client);
this._emulationManager = new EmulationManager(client);
this._tracing = new Tracing(client);
@ -711,7 +711,7 @@ class Page extends EventEmitter {
async focus(selector) {
const handle = await this.$(selector);
console.assert(handle, 'No node found for selector: ' + selector);
await this.evaluate(element => element.focus(), handle);
await handle.focus();
await handle.dispose();
}
@ -739,31 +739,14 @@ class Page extends EventEmitter {
}
/**
* @param {string} selector
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options) {
let delay = 0;
if (options && options.delay)
delay = options.delay;
let last;
for (const char of text) {
last = this.press(char, {text: char, delay});
if (delay)
await new Promise(f => setTimeout(f, delay));
}
await last;
}
/**
* @param {string} text
* @param {!Object=} options
*/
async press(key, options) {
this._keyboard.down(key, options);
if (options && options.delay)
await new Promise(f => setTimeout(f, options.delay));
await this._keyboard.up(key);
async type(selector, text, options) {
const handle = await this.$(selector);
await handle.type(text, options);
await handle.dispose();
}
/**

View File

@ -1426,8 +1426,9 @@ describe('Page', function() {
}));
it('should type into the textarea', SX(async function() {
await page.goto(PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.type('Type in this text!');
const textarea = await page.$('textarea');
await textarea.type('Type in this text!');
expect(await page.evaluate(() => result)).toBe('Type in this text!');
}));
it('should click the button after navigation ', SX(async function() {
@ -1452,29 +1453,28 @@ describe('Page', function() {
}));
it('should move with the arrow keys', SX(async function(){
await page.goto(PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.type('Hello World!');
await page.type('textarea', 'Hello World!');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!');
for (let i = 0; i < 'World!'.length; i++)
page.press('ArrowLeft');
await page.type('inserted ');
page.keyboard.press('ArrowLeft');
await page.keyboard.type('inserted ');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello inserted World!');
page.keyboard.down('Shift');
for (let i = 0; i < 'inserted '.length; i++)
page.press('ArrowLeft');
page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');
await page.press('Backspace');
await page.keyboard.press('Backspace');
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('Hello World!');
}));
it('should send a character with Page.press', SX(async function() {
it('should send a character with ElementHandle.press', SX(async function() {
await page.goto(PREFIX + '/input/textarea.html');
await page.focus('textarea');
await page.press('a', {text: 'f'});
const textarea = await page.$('textarea');
await textarea.press('a', {text: 'f'});
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('f');
await page.evaluate(() => window.addEventListener('keydown', e => e.preventDefault(), true));
await page.press('a', {text: 'y'});
await textarea.press('a', {text: 'y'});
expect(await page.evaluate(() => document.querySelector('textarea').value)).toBe('f');
}));
it('should send a character with sendCharacter', SX(async function() {
@ -1519,12 +1519,12 @@ describe('Page', function() {
}));
it('should send proper codes while typing', SX(async function(){
await page.goto(PREFIX + '/input/keyboard.html');
await page.type('!');
await page.keyboard.type('!');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: ! 49 []',
'Keypress: ! 33 33 33 []',
'Keyup: ! 49 []'].join('\n'));
await page.type('^');
await page.keyboard.type('^');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: ^ 54 []',
'Keypress: ^ 94 94 94 []',
@ -1534,7 +1534,7 @@ describe('Page', function() {
await page.goto(PREFIX + '/input/keyboard.html');
const keyboard = page.keyboard;
await keyboard.down('Shift');
await page.type('~');
await page.keyboard.type('~');
expect(await page.evaluate(() => getResult())).toBe(
[ 'Keydown: Shift 16 [Shift]',
'Keydown: ~ 192 [Shift]', // 192 is ` keyCode
@ -1555,7 +1555,7 @@ describe('Page', function() {
Promise.resolve().then(() => event.preventDefault());
}, false);
});
await page.type('Hello World!');
await page.keyboard.type('Hello World!');
expect(await page.evaluate(() => textarea.value)).toBe('He Wrd!');
}));
it('keyboard.modifiers()', SX(async function(){
@ -1603,7 +1603,7 @@ describe('Page', function() {
await page.goto(PREFIX + '/input/textarea.html');
await page.focus('textarea');
const 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.keyboard.type(text);
await page.evaluate(() => document.querySelector('textarea').scrollTop = 0);
const {x, y} = await page.evaluate(dimensions);
await page.mouse.move(x + 2,y + 2);
@ -1616,7 +1616,7 @@ describe('Page', function() {
await page.goto(PREFIX + '/input/textarea.html');
await page.focus('textarea');
const 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.keyboard.type(text);
await page.click('textarea');
await page.click('textarea', {clickCount: 2});
await page.click('textarea', {clickCount: 3});
@ -1659,7 +1659,7 @@ describe('Page', function() {
await page.evaluate(() => document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true));
await page.keyboard.down('a', {text: 'a'});
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(false);
await page.press('a');
await page.keyboard.press('a');
expect(await page.evaluate(() => window.lastEvent.repeat)).toBe(true);
}));
// @see https://github.com/GoogleChrome/puppeteer/issues/206