UNPKG

13.9 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the 'License');
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an 'AS IS' BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17const {helper, assert} = require('./helper');
18const keyDefinitions = require('./USKeyboardLayout');
19
20/**
21 * @typedef {Object} KeyDescription
22 * @property {number} keyCode
23 * @property {string} key
24 * @property {string} text
25 * @property {string} code
26 * @property {number} location
27 */
28
29class Keyboard {
30 /**
31 * @param {!Puppeteer.CDPSession} client
32 */
33 constructor(client) {
34 this._client = client;
35 this._modifiers = 0;
36 this._pressedKeys = new Set();
37 }
38
39 /**
40 * @param {string} key
41 * @param {{text: string}=} options
42 */
43 /* async */ down(key, options = { text: undefined }) {return (fn => {
44 const gen = fn.call(this);
45 return new Promise((resolve, reject) => {
46 function step(key, arg) {
47 let info, value;
48 try {
49 info = gen[key](arg);
50 value = info.value;
51 } catch (error) {
52 reject(error);
53 return;
54 }
55 if (info.done) {
56 resolve(value);
57 } else {
58 return Promise.resolve(value).then(
59 value => {
60 step('next', value);
61 },
62 err => {
63 step('throw', err);
64 });
65 }
66 }
67 return step('next');
68 });
69})(function*(){
70 const description = this._keyDescriptionForString(key);
71
72 const autoRepeat = this._pressedKeys.has(description.code);
73 this._pressedKeys.add(description.code);
74 this._modifiers |= this._modifierBit(description.key);
75
76 const text = options.text === undefined ? description.text : options.text;
77 (yield this._client.send('Input.dispatchKeyEvent', {
78 type: text ? 'keyDown' : 'rawKeyDown',
79 modifiers: this._modifiers,
80 windowsVirtualKeyCode: description.keyCode,
81 code: description.code,
82 key: description.key,
83 text: text,
84 unmodifiedText: text,
85 autoRepeat,
86 location: description.location,
87 isKeypad: description.location === 3
88 }));
89 });}
90
91 /**
92 * @param {string} key
93 * @return {number}
94 */
95 _modifierBit(key) {
96 if (key === 'Alt')
97 return 1;
98 if (key === 'Control')
99 return 2;
100 if (key === 'Meta')
101 return 4;
102 if (key === 'Shift')
103 return 8;
104 return 0;
105 }
106
107 /**
108 * @param {string} keyString
109 * @return {KeyDescription}
110 */
111 _keyDescriptionForString(keyString) {
112 const shift = this._modifiers & 8;
113 const description = {
114 key: '',
115 keyCode: 0,
116 code: '',
117 text: '',
118 location: 0
119 };
120
121 const definition = keyDefinitions[keyString];
122 assert(definition, `Unknown key: "${keyString}"`);
123
124 if (definition.key)
125 description.key = definition.key;
126 if (shift && definition.shiftKey)
127 description.key = definition.shiftKey;
128
129 if (definition.keyCode)
130 description.keyCode = definition.keyCode;
131 if (shift && definition.shiftKeyCode)
132 description.keyCode = definition.shiftKeyCode;
133
134 if (definition.code)
135 description.code = definition.code;
136
137 if (definition.location)
138 description.location = definition.location;
139
140 if (description.key.length === 1)
141 description.text = description.key;
142
143 if (definition.text)
144 description.text = definition.text;
145 if (shift && definition.shiftText)
146 description.text = definition.shiftText;
147
148 // if any modifiers besides shift are pressed, no text should be sent
149 if (this._modifiers & ~8)
150 description.text = '';
151
152 return description;
153 }
154
155 /**
156 * @param {string} key
157 */
158 /* async */ up(key) {return (fn => {
159 const gen = fn.call(this);
160 return new Promise((resolve, reject) => {
161 function step(key, arg) {
162 let info, value;
163 try {
164 info = gen[key](arg);
165 value = info.value;
166 } catch (error) {
167 reject(error);
168 return;
169 }
170 if (info.done) {
171 resolve(value);
172 } else {
173 return Promise.resolve(value).then(
174 value => {
175 step('next', value);
176 },
177 err => {
178 step('throw', err);
179 });
180 }
181 }
182 return step('next');
183 });
184})(function*(){
185 const description = this._keyDescriptionForString(key);
186
187 this._modifiers &= ~this._modifierBit(description.key);
188 this._pressedKeys.delete(description.code);
189 (yield this._client.send('Input.dispatchKeyEvent', {
190 type: 'keyUp',
191 modifiers: this._modifiers,
192 key: description.key,
193 windowsVirtualKeyCode: description.keyCode,
194 code: description.code,
195 location: description.location
196 }));
197 });}
198
199 /**
200 * @param {string} char
201 */
202 /* async */ sendCharacter(char) {return (fn => {
203 const gen = fn.call(this);
204 return new Promise((resolve, reject) => {
205 function step(key, arg) {
206 let info, value;
207 try {
208 info = gen[key](arg);
209 value = info.value;
210 } catch (error) {
211 reject(error);
212 return;
213 }
214 if (info.done) {
215 resolve(value);
216 } else {
217 return Promise.resolve(value).then(
218 value => {
219 step('next', value);
220 },
221 err => {
222 step('throw', err);
223 });
224 }
225 }
226 return step('next');
227 });
228})(function*(){
229 (yield this._client.send('Input.insertText', {text: char}));
230 });}
231
232 /**
233 * @param {string} text
234 * @param {{delay: (number|undefined)}=} options
235 */
236 /* async */ type(text, options) {return (fn => {
237 const gen = fn.call(this);
238 return new Promise((resolve, reject) => {
239 function step(key, arg) {
240 let info, value;
241 try {
242 info = gen[key](arg);
243 value = info.value;
244 } catch (error) {
245 reject(error);
246 return;
247 }
248 if (info.done) {
249 resolve(value);
250 } else {
251 return Promise.resolve(value).then(
252 value => {
253 step('next', value);
254 },
255 err => {
256 step('throw', err);
257 });
258 }
259 }
260 return step('next');
261 });
262})(function*(){
263 let delay = 0;
264 if (options && options.delay)
265 delay = options.delay;
266 for (const char of text) {
267 if (keyDefinitions[char])
268 (yield this.press(char, {delay}));
269 else
270 (yield this.sendCharacter(char));
271 if (delay)
272 (yield new Promise(f => setTimeout(f, delay)));
273 }
274 });}
275
276 /**
277 * @param {string} key
278 * @param {!Object=} options
279 */
280 /* async */ press(key, options) {return (fn => {
281 const gen = fn.call(this);
282 return new Promise((resolve, reject) => {
283 function step(key, arg) {
284 let info, value;
285 try {
286 info = gen[key](arg);
287 value = info.value;
288 } catch (error) {
289 reject(error);
290 return;
291 }
292 if (info.done) {
293 resolve(value);
294 } else {
295 return Promise.resolve(value).then(
296 value => {
297 step('next', value);
298 },
299 err => {
300 step('throw', err);
301 });
302 }
303 }
304 return step('next');
305 });
306})(function*(){
307 (yield this.down(key, options));
308 if (options && options.delay)
309 (yield new Promise(f => setTimeout(f, options.delay)));
310 (yield this.up(key));
311 });}
312}
313
314class Mouse {
315 /**
316 * @param {Puppeteer.CDPSession} client
317 * @param {!Keyboard} keyboard
318 */
319 constructor(client, keyboard) {
320 this._client = client;
321 this._keyboard = keyboard;
322 this._x = 0;
323 this._y = 0;
324 /** @type {'none'|'left'|'right'|'middle'} */
325 this._button = 'none';
326 }
327
328 /**
329 * @param {number} x
330 * @param {number} y
331 * @param {Object=} options
332 * @return {!Promise}
333 */
334 /* async */ move(x, y, options = {}) {return (fn => {
335 const gen = fn.call(this);
336 return new Promise((resolve, reject) => {
337 function step(key, arg) {
338 let info, value;
339 try {
340 info = gen[key](arg);
341 value = info.value;
342 } catch (error) {
343 reject(error);
344 return;
345 }
346 if (info.done) {
347 resolve(value);
348 } else {
349 return Promise.resolve(value).then(
350 value => {
351 step('next', value);
352 },
353 err => {
354 step('throw', err);
355 });
356 }
357 }
358 return step('next');
359 });
360})(function*(){
361 const fromX = this._x, fromY = this._y;
362 this._x = x;
363 this._y = y;
364 const steps = options.steps || 1;
365 for (let i = 1; i <= steps; i++) {
366 (yield this._client.send('Input.dispatchMouseEvent', {
367 type: 'mouseMoved',
368 button: this._button,
369 x: fromX + (this._x - fromX) * (i / steps),
370 y: fromY + (this._y - fromY) * (i / steps),
371 modifiers: this._keyboard._modifiers
372 }));
373 }
374 });}
375
376 /**
377 * @param {number} x
378 * @param {number} y
379 * @param {!Object=} options
380 */
381 /* async */ click(x, y, options = {}) {return (fn => {
382 const gen = fn.call(this);
383 return new Promise((resolve, reject) => {
384 function step(key, arg) {
385 let info, value;
386 try {
387 info = gen[key](arg);
388 value = info.value;
389 } catch (error) {
390 reject(error);
391 return;
392 }
393 if (info.done) {
394 resolve(value);
395 } else {
396 return Promise.resolve(value).then(
397 value => {
398 step('next', value);
399 },
400 err => {
401 step('throw', err);
402 });
403 }
404 }
405 return step('next');
406 });
407})(function*(){
408 this.move(x, y);
409 this.down(options);
410 if (typeof options.delay === 'number')
411 (yield new Promise(f => setTimeout(f, options.delay)));
412 (yield this.up(options));
413 });}
414
415 /**
416 * @param {!Object=} options
417 */
418 /* async */ down(options = {}) {return (fn => {
419 const gen = fn.call(this);
420 return new Promise((resolve, reject) => {
421 function step(key, arg) {
422 let info, value;
423 try {
424 info = gen[key](arg);
425 value = info.value;
426 } catch (error) {
427 reject(error);
428 return;
429 }
430 if (info.done) {
431 resolve(value);
432 } else {
433 return Promise.resolve(value).then(
434 value => {
435 step('next', value);
436 },
437 err => {
438 step('throw', err);
439 });
440 }
441 }
442 return step('next');
443 });
444})(function*(){
445 this._button = (options.button || 'left');
446 (yield this._client.send('Input.dispatchMouseEvent', {
447 type: 'mousePressed',
448 button: this._button,
449 x: this._x,
450 y: this._y,
451 modifiers: this._keyboard._modifiers,
452 clickCount: (options.clickCount || 1)
453 }));
454 });}
455
456 /**
457 * @param {!Object=} options
458 */
459 /* async */ up(options = {}) {return (fn => {
460 const gen = fn.call(this);
461 return new Promise((resolve, reject) => {
462 function step(key, arg) {
463 let info, value;
464 try {
465 info = gen[key](arg);
466 value = info.value;
467 } catch (error) {
468 reject(error);
469 return;
470 }
471 if (info.done) {
472 resolve(value);
473 } else {
474 return Promise.resolve(value).then(
475 value => {
476 step('next', value);
477 },
478 err => {
479 step('throw', err);
480 });
481 }
482 }
483 return step('next');
484 });
485})(function*(){
486 this._button = 'none';
487 (yield this._client.send('Input.dispatchMouseEvent', {
488 type: 'mouseReleased',
489 button: (options.button || 'left'),
490 x: this._x,
491 y: this._y,
492 modifiers: this._keyboard._modifiers,
493 clickCount: (options.clickCount || 1)
494 }));
495 });}
496}
497
498class Touchscreen {
499 /**
500 * @param {Puppeteer.CDPSession} client
501 * @param {Keyboard} keyboard
502 */
503 constructor(client, keyboard) {
504 this._client = client;
505 this._keyboard = keyboard;
506 }
507
508 /**
509 * @param {number} x
510 * @param {number} y
511 */
512 /* async */ tap(x, y) {return (fn => {
513 const gen = fn.call(this);
514 return new Promise((resolve, reject) => {
515 function step(key, arg) {
516 let info, value;
517 try {
518 info = gen[key](arg);
519 value = info.value;
520 } catch (error) {
521 reject(error);
522 return;
523 }
524 if (info.done) {
525 resolve(value);
526 } else {
527 return Promise.resolve(value).then(
528 value => {
529 step('next', value);
530 },
531 err => {
532 step('throw', err);
533 });
534 }
535 }
536 return step('next');
537 });
538})(function*(){
539 // Touches appear to be lost during the first frame after navigation.
540 // This waits a frame before sending the tap.
541 // @see https://crbug.com/613219
542 (yield this._client.send('Runtime.evaluate', {
543 expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
544 awaitPromise: true
545 }));
546
547 const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
548 (yield this._client.send('Input.dispatchTouchEvent', {
549 type: 'touchStart',
550 touchPoints,
551 modifiers: this._keyboard._modifiers
552 }));
553 (yield this._client.send('Input.dispatchTouchEvent', {
554 type: 'touchEnd',
555 touchPoints: [],
556 modifiers: this._keyboard._modifiers
557 }));
558 });}
559}
560
561module.exports = { Keyboard, Mouse, Touchscreen};
562helper.tracePublicAPI(Keyboard);
563helper.tracePublicAPI(Mouse);
564helper.tracePublicAPI(Touchscreen);