puppeteer/lib/Input.js
Joel Einbinder 3335d369d3 fix: typing emoji (#2824)
This changes sendCharacter to use document.execCommand instead of sending a `'char'` event from the protocol. This is more aligned with how input would come in from emoji keyboards, and removes the 3ish byte limit on characters that can be sent which prevented larger emoji from being rendered correctly.

Emoji will still fail to type correctly if typing them into an iframe that is in shadow dom.

fixes #1096
2018-07-31 11:22:26 -07:00

304 lines
7.6 KiB
JavaScript

/**
* 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.
*/
const {helper, assert} = require('./helper');
const keyDefinitions = require('./USKeyboardLayout');
/**
* @typedef {Object} KeyDescription
* @property {number} keyCode
* @property {string} key
* @property {string} text
* @property {string} code
* @property {number} location
*/
class Keyboard {
/**
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;
this._modifiers = 0;
this._pressedKeys = new Set();
}
/**
* @param {string} key
* @param {{text: string}=} options
*/
async down(key, options = { text: undefined }) {
const description = this._keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text;
await this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
key: description.key,
text: text,
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3
});
}
/**
* @param {string} key
* @return {number}
*/
_modifierBit(key) {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Meta')
return 4;
if (key === 'Shift')
return 8;
return 0;
}
/**
* @param {string} keyString
* @return {KeyDescription}
*/
_keyDescriptionForString(keyString) {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0
};
const definition = keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8)
description.text = '';
return description;
}
/**
* @param {string} key
*/
async up(key) {
const description = this._keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
this._pressedKeys.delete(description.code);
await this._client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this._modifiers,
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
await this._client.send('Input.insertText', {text: 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) {
if (keyDefinitions[char])
await this.press(char, {delay});
else
await this.sendCharacter(char);
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 {
/**
* @param {Puppeteer.CDPSession} 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
* @param {Object=} options
* @return {!Promise}
*/
async move(x, y, options = {}) {
const fromX = this._x, fromY = this._y;
this._x = x;
this._y = y;
const steps = options.steps || 1;
for (let i = 1; i <= steps; i++) {
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps),
modifiers: this._keyboard._modifiers
});
}
}
/**
* @param {number} x
* @param {number} y
* @param {!Object=} options
*/
async click(x, y, options = {}) {
this.move(x, y);
this.down(options);
if (typeof options.delay === 'number')
await new Promise(f => setTimeout(f, options.delay));
await this.up(options);
}
/**
* @param {!Object=} options
*/
async down(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._keyboard._modifiers,
clickCount: (options.clickCount || 1)
});
}
/**
* @param {!Object=} options
*/
async up(options = {}) {
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button: (options.button || 'left'),
x: this._x,
y: this._y,
modifiers: this._keyboard._modifiers,
clickCount: (options.clickCount || 1)
});
}
}
class Touchscreen {
/**
* @param {Puppeteer.CDPSession} client
* @param {Keyboard} keyboard
*/
constructor(client, keyboard) {
this._client = client;
this._keyboard = keyboard;
}
/**
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
// Touches appear to be lost during the first frame after navigation.
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
await this._client.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
});
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
});
}
}
module.exports = { Keyboard, Mouse, Touchscreen};
helper.tracePublicAPI(Keyboard);
helper.tracePublicAPI(Mouse);
helper.tracePublicAPI(Touchscreen);