chore: migrate src/Input to typescript (#5710)

* chore: migrate src/Input to typescript

This moves `Keyboard`, `Mouse` and `Touchscreen` to TypeScript. We gain
some nice TS benefits here; by creating a type for all the keycodes we
support we can type the input args as that rather than `string` which
will hopefully save some users some debugging once we ship our TS types
in a future version.

* Remove from externs file

* Update utils/doclint/check_public_api/index.js

Co-Authored-By: Mathias Bynens <mathias@qiwi.be>

Co-authored-by: Mathias Bynens <mathias@qiwi.be>
This commit is contained in:
Jack Franklin 2020-04-22 15:44:04 +01:00 committed by GitHub
parent 11bc5a6450
commit 133abb07cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 100 deletions

View File

@ -14,36 +14,22 @@
* limitations under the License.
*/
const {assert} = require('./helper');
// CDPSession is used only as a typedef
// eslint-disable-next-line no-unused-vars
const {CDPSession} = require('./Connection');
const {keyDefinitions} = require('./USKeyboardLayout');
import {assert} from './helper';
import {CDPSession} from './Connection';
import {keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout';
/**
* @typedef {Object} KeyDescription
* @property {number} keyCode
* @property {string} key
* @property {string} text
* @property {string} code
* @property {number} location
*/
type KeyDescription = Required<Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>>;
class Keyboard {
/**
* @param {!CDPSession} client
*/
constructor(client) {
export class Keyboard {
_client: CDPSession;
_modifiers = 0;
_pressedKeys = new Set<string>();
constructor(client: CDPSession) {
this._client = client;
this._modifiers = 0;
this._pressedKeys = new Set();
}
/**
* @param {string} key
* @param {{text?: string}=} options
*/
async down(key, options = {text: undefined}) {
async down(key: KeyInput, options: { text?: string } = {text: undefined}): Promise<void> {
const description = this._keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
@ -65,11 +51,7 @@ class Keyboard {
});
}
/**
* @param {string} key
* @return {number}
*/
_modifierBit(key) {
private _modifierBit(key: string): number {
if (key === 'Alt')
return 1;
if (key === 'Control')
@ -81,11 +63,7 @@ class Keyboard {
return 0;
}
/**
* @param {string} keyString
* @return {KeyDescription}
*/
_keyDescriptionForString(keyString) {
private _keyDescriptionForString(keyString: KeyInput): KeyDescription {
const shift = this._modifiers & 8;
const description = {
key: '',
@ -129,10 +107,7 @@ class Keyboard {
return description;
}
/**
* @param {string} key
*/
async up(key) {
async up(key: KeyInput): Promise<void> {
const description = this._keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
@ -147,21 +122,18 @@ class Keyboard {
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
async sendCharacter(char: string): Promise<void> {
await this._client.send('Input.insertText', {text: char});
}
/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options) {
private charIsKey(char: string): char is KeyInput {
return !!keyDefinitions[char];
}
async type(text: string, options: {delay?: number}): Promise<void> {
const delay = (options && options.delay) || null;
for (const char of text) {
if (keyDefinitions[char]) {
if (this.charIsKey(char)) {
await this.press(char, {delay});
} else {
if (delay)
@ -171,11 +143,7 @@ class Keyboard {
}
}
/**
* @param {string} key
* @param {!{delay?: number, text?: string}=} options
*/
async press(key, options = {}) {
async press(key: KeyInput, options: {delay?: number; text?: string} = {}): Promise<void> {
const {delay = null} = options;
await this.down(key, options);
if (delay)
@ -184,26 +152,30 @@ class Keyboard {
}
}
class Mouse {
type MouseButton = 'none' | 'left' | 'right' | 'middle';
type MouseButtonInput = Exclude<MouseButton, 'none'>;
interface MouseOptions {
button?: MouseButtonInput;
clickCount?: number;
}
export class Mouse {
_client: CDPSession;
_keyboard: Keyboard;
_x = 0;
_y = 0;
_button: MouseButton = 'none';
/**
* @param {CDPSession} client
* @param {!Keyboard} keyboard
*/
constructor(client, keyboard) {
constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client;
this._keyboard = keyboard;
this._x = 0;
this._y = 0;
/** @type {'none'|'left'|'right'|'middle'} */
this._button = 'none';
}
/**
* @param {number} x
* @param {number} y
* @param {!{steps?: number}=} options
*/
async move(x, y, options = {}) {
async move(x: number, y: number, options: {steps?: number} = {}): Promise<void> {
const {steps = 1} = options;
const fromX = this._x, fromY = this._y;
this._x = x;
@ -219,12 +191,7 @@ class Mouse {
}
}
/**
* @param {number} x
* @param {number} y
* @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async click(x, y, options = {}) {
async click(x: number, y: number, options: MouseOptions & {delay?: number} = {}): Promise<void> {
const {delay = null} = options;
if (delay !== null) {
await Promise.all([
@ -242,10 +209,7 @@ class Mouse {
}
}
/**
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async down(options = {}) {
async down(options: MouseOptions = {}): Promise<void> {
const {button = 'left', clickCount = 1} = options;
this._button = button;
await this._client.send('Input.dispatchMouseEvent', {
@ -261,7 +225,7 @@ class Mouse {
/**
* @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
*/
async up(options = {}) {
async up(options: MouseOptions = {}): Promise<void> {
const {button = 'left', clickCount = 1} = options;
this._button = 'none';
await this._client.send('Input.dispatchMouseEvent', {
@ -275,12 +239,11 @@ class Mouse {
}
}
class Touchscreen {
/**
* @param {CDPSession} client
* @param {Keyboard} keyboard
*/
constructor(client, keyboard) {
export class Touchscreen {
_client: CDPSession;
_keyboard: Keyboard;
constructor(client: CDPSession, keyboard: Keyboard) {
this._client = client;
this._keyboard = keyboard;
}
@ -289,7 +252,7 @@ class Touchscreen {
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
async tap(x: number, y: number): Promise<void> {
// Touches appear to be lost during the first frame after navigation.
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
@ -311,5 +274,3 @@ class Touchscreen {
});
}
}
module.exports = {Keyboard, Mouse, Touchscreen};

View File

@ -17,6 +17,7 @@
import {helper, assert, debugError} from './helper';
import {ExecutionContext} from './ExecutionContext';
import {CDPSession} from './Connection';
import {KeyInput} from './USKeyboardLayout';
interface BoxModel {
content: Array<{x: number; y: number}>;
@ -326,7 +327,7 @@ export class ElementHandle extends JSHandle {
await this._page.keyboard.type(text, options);
}
async press(key: string, options?: {delay?: number; text?: string}): Promise<void> {
async press(key: KeyInput, options?: {delay?: number; text?: string}): Promise<void> {
await this.focus();
await this._page.keyboard.press(key, options);
}

View File

@ -14,7 +14,8 @@
* limitations under the License.
*/
interface KeyDefinition {
export interface KeyDefinition {
keyCode?: number;
shiftKeyCode?: number;
key?: string;
@ -25,7 +26,9 @@
location?: number;
}
export const keyDefinitions: Readonly<Record<string, KeyDefinition>> = {
export type KeyInput = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'Power'|'Eject'|'Abort'|'Help'|'Backspace'|'Tab'|'Numpad5'|'NumpadEnter'|'Enter'|'\r'|'\n'|'ShiftLeft'|'ShiftRight'|'ControlLeft'|'ControlRight'|'AltLeft'|'AltRight'|'Pause'|'CapsLock'|'Escape'|'Convert'|'NonConvert'|'Space'|'Numpad9'|'PageUp'|'Numpad3'|'PageDown'|'End'|'Numpad1'|'Home'|'Numpad7'|'ArrowLeft'|'Numpad4'|'Numpad8'|'ArrowUp'|'ArrowRight'|'Numpad6'|'Numpad2'|'ArrowDown'|'Select'|'Open'|'PrintScreen'|'Insert'|'Numpad0'|'Delete'|'NumpadDecimal'|'Digit0'|'Digit1'|'Digit2'|'Digit3'|'Digit4'|'Digit5'|'Digit6'|'Digit7'|'Digit8'|'Digit9'|'KeyA'|'KeyB'|'KeyC'|'KeyD'|'KeyE'|'KeyF'|'KeyG'|'KeyH'|'KeyI'|'KeyJ'|'KeyK'|'KeyL'|'KeyM'|'KeyN'|'KeyO'|'KeyP'|'KeyQ'|'KeyR'|'KeyS'|'KeyT'|'KeyU'|'KeyV'|'KeyW'|'KeyX'|'KeyY'|'KeyZ'|'MetaLeft'|'MetaRight'|'ContextMenu'|'NumpadMultiply'|'NumpadAdd'|'NumpadSubtract'|'NumpadDivide'|'F1'|'F2'|'F3'|'F4'|'F5'|'F6'|'F7'|'F8'|'F9'|'F10'|'F11'|'F12'|'F13'|'F14'|'F15'|'F16'|'F17'|'F18'|'F19'|'F20'|'F21'|'F22'|'F23'|'F24'|'NumLock'|'ScrollLock'|'AudioVolumeMute'|'AudioVolumeDown'|'AudioVolumeUp'|'MediaTrackNext'|'MediaTrackPrevious'|'MediaStop'|'MediaPlayPause'|'Semicolon'|'Equal'|'NumpadEqual'|'Comma'|'Minus'|'Period'|'Slash'|'Backquote'|'BracketLeft'|'Backslash'|'BracketRight'|'Quote'|'AltGraph'|'Props'|'Cancel'|'Clear'|'Shift'|'Control'|'Alt'|'Accept'|'ModeChange'|' '|'Print'|'Execute'|'\u0000'|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z'|'Meta'|'*'|'+'|'-'|'/'|';'|'='|','|'.'|'`'|'['|'\\'|']'|'\''|'Attn'|'CrSel'|'ExSel'|'EraseEof'|'Play'|'ZoomOut'|')'|'!'|'@'|'#'|'$'|'%'|'^'|'&'|'('|'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z'|':'|'<'|'_'|'>'|'?'|'~'|'{'|'|'|'}'|'"'|'SoftLeft'|'SoftRight'|'Camera'|'Call'|'EndCall'|'VolumeDown'|'VolumeUp';
export const keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = {
'0': {'keyCode': 48, 'key': '0', 'code': 'Digit0'},
'1': {'keyCode': 49, 'key': '1', 'code': 'Digit1'},
'2': {'keyCode': 50, 'key': '2', 'code': 'Digit2'},

4
src/externs.d.ts vendored
View File

@ -1,16 +1,12 @@
import { Browser as RealBrowser, BrowserContext as RealBrowserContext} from './Browser.js';
import {Target as RealTarget} from './Target.js';
import {Page as RealPage} from './Page.js';
import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js';
import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js';
import {DOMWorld as RealDOMWorld} from './DOMWorld.js';
import { NetworkManager as RealNetworkManager, Request as RealRequest, Response as RealResponse } from './NetworkManager.js';
import * as child_process from 'child_process';
declare global {
module Puppeteer {
export class Mouse extends RealMouse {}
export class Keyboard extends RealKeyboard {}
export class Touchscreen extends RealTouchscreen {}
export class Browser extends RealBrowser {}
export class BrowserContext extends RealBrowserContext {}
export class Target extends RealTarget {}

View File

@ -184,6 +184,15 @@ function checkSources(sources) {
return new Documentation.Type(typeName, []);
}
/**
* @param {!ts.Symbol} symbol
* @return {boolean}
*/
function symbolHasPrivateModifier(symbol) {
const modifiers = symbol.valueDeclaration.modifiers || [];
return modifiers.some(modifier => modifier.kind === ts.SyntaxKind.PrivateKeyword);
}
/**
* @param {string} className
* @param {!ts.Symbol} symbol
@ -194,8 +203,14 @@ function checkSources(sources) {
const members = classEvents.get(className) || [];
for (const [name, member] of symbol.members || []) {
if (name.startsWith('_'))
/* Before TypeScript we denoted private methods with an underscore
* but in TypeScript we use the private keyword
* hence we check for either here.
*/
if (name.startsWith('_') || symbolHasPrivateModifier(member))
continue;
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
const signature = memberType.getCallSignatures()[0];
if (signature)

View File

@ -271,6 +271,30 @@ function compareDocumentations(actual, expected) {
actualName: 'Object',
expectedName: 'CommandParameters[T]'
}],
['Method ElementHandle.press() key', {
actualName: 'string',
expectedName: 'KeyInput'
}],
['Method Keyboard.down() key', {
actualName: 'string',
expectedName: 'KeyInput'
}],
['Method Keyboard.press() key', {
actualName: 'string',
expectedName: 'KeyInput'
}],
['Method Keyboard.up() key', {
actualName: 'string',
expectedName: 'KeyInput'
}],
['Method Mouse.down() options', {
actualName: 'Object',
expectedName: 'MouseOptions'
}],
['Method Mouse.up() options', {
actualName: 'Object',
expectedName: 'MouseOptions'
}],
]);
const expectedForSource = expectedNamingMismatches.get(source);
@ -295,12 +319,15 @@ function compareDocumentations(actual, expected) {
const actualName = actual.name.replace(/[\? ]/g, '');
// TypeScript likes to add some spaces
const expectedName = expected.name.replace(/\ /g, '');
if (expectedName !== actualName) {
const namingMismatchIsExpected = namingMisMatchInTypeIsExpected(source, actualName, expectedName);
if (!namingMismatchIsExpected)
if (expectedName !== actualName && !namingMismatchIsExpected)
errors.push(`${source} ${actualName} != ${expectedName}`);
}
/* If we got a naming mismatch and it was expected, don't check the properties
* as they will likely be considered "wrong" by DocLint too.
*/
if (namingMismatchIsExpected) return;
const actualPropertiesMap = new Map(actual.properties.map(property => [property.name, property.type]));
const expectedPropertiesMap = new Map(expected.properties.map(property => [property.name, property.type]));
const propertiesDiff = diff(Array.from(actualPropertiesMap.keys()).sort(), Array.from(expectedPropertiesMap.keys()).sort());