#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Keyboard and Mouse module."""
import asyncio
from typing import Any, Dict, TYPE_CHECKING
from pyppeteer.connection import CDPSession
from pyppeteer.errors import PyppeteerError
from pyppeteer.us_keyboard_layout import keyDefinitions
from pyppeteer.util import merge_dict
if TYPE_CHECKING:
from typing import Set # noqa: F401
[docs]class Keyboard(object):
"""Keyboard class provides as api for managing a virtual keyboard.
The high level api is :meth:`type`, which takes raw characters and
generate proper keydown, keypress/input, and keyup events on your page.
For finer control, you can use :meth:`down`, :meth:`up`, and
:meth:`sendCharacter` 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:
.. code::
await page.keyboard.type('Hello, World!')
await page.keyboard.press('ArrowLeft')
await page.keyboard.down('Shift')
for i in ' World':
await page.keyboard.press('ArrowLeft')
await page.keyboard.up('Shift')
await page.keyboard.press('Backspace')
# Result text will end up saying 'Hello!'.
An example of pressing ``A``:
.. code::
await page.keyboard.down('Shift')
await page.keyboard.press('KeyA')
await page.keyboard.up('Shift')
"""
def __init__(self, client: CDPSession) -> None:
self._client = client
self._modifiers = 0
self._pressedKeys: Set[str] = set()
[docs] async def down(self, key: str, options: dict = None, **kwargs: Any
) -> None:
"""Dispatch a ``keydown`` event with ``key``.
If ``key`` is a single character and no modifier keys besides ``Shift``
are being held down, and a ``keypress``/``input`` event will also
generated. The ``text`` option can be specified to force an ``input``
event to be generated.
If ``key`` is a modifier key, like ``Shift``, ``Meta``, or ``Alt``,
subsequent key presses will be sent with that modifier active. To
release the modifier key, use :meth:`up` method.
:arg str key: Name of key to press, such as ``ArrowLeft``.
:arg dict options: Option can have ``text`` field, and if this option
specified, generate an input event with this text.
.. note::
Modifier keys DO influence :meth:`down`. Holding down ``shift``
will type the text in upper case.
"""
options = merge_dict(options, kwargs)
description = self._keyDescriptionForString(key)
autoRepeat = description['code'] in self._pressedKeys
self._pressedKeys.add(description['code'])
self._modifiers |= self._modifierBit(description['key'])
text = options.get('text')
if text is None:
text = description['text']
await self._client.send('Input.dispatchKeyEvent', {
'type': 'keyDown' if text else 'rawKeyDown',
'modifiers': self._modifiers,
'windowsVirtualKeyCode': description['keyCode'],
'code': description['code'],
'key': description['key'],
'text': text,
'unmodifiedText': text,
'autoRepeat': autoRepeat,
'location': description['location'],
'isKeypad': description['location'] == 3,
})
def _modifierBit(self, key: str) -> int:
if key == 'Alt':
return 1
if key == 'Control':
return 2
if key == 'Meta':
return 4
if key == 'Shift':
return 8
return 0
def _keyDescriptionForString(self, keyString: str) -> Dict: # noqa: C901
shift = self._modifiers & 8
description = {
'key': '',
'keyCode': 0,
'code': '',
'text': '',
'location': 0,
}
definition: Dict = keyDefinitions.get(keyString) # type: ignore
if not definition:
raise PyppeteerError(f'Unknown key: {keyString}')
if 'key' in definition:
description['key'] = definition['key']
if shift and definition.get('shiftKey'):
description['key'] = definition['shiftKey']
if 'keyCode' in definition:
description['keyCode'] = definition['keyCode']
if shift and definition.get('shiftKeyCode'):
description['keyCode'] = definition['shiftKeyCode']
if 'code' in definition:
description['code'] = definition['code']
if 'location' in definition:
description['location'] = definition['location']
if len(description['key']) == 1: # type: ignore
description['text'] = description['key']
if 'text' in definition:
description['text'] = definition['text']
if shift and definition.get('shiftText'):
description['text'] = definition['shiftText']
if self._modifiers & ~8:
description['text'] = ''
return description
[docs] async def up(self, key: str) -> None:
"""Dispatch a ``keyup`` event of the ``key``.
:arg str key: Name of key to release, such as ``ArrowLeft``.
"""
description = self._keyDescriptionForString(key)
self._modifiers &= ~self._modifierBit(description['key'])
if description['code'] in self._pressedKeys:
self._pressedKeys.remove(description['code'])
await self._client.send('Input.dispatchKeyEvent', {
'type': 'keyUp',
'modifiers': self._modifiers,
'key': description['key'],
'windowsVirtualKeyCode': description['keyCode'],
'code': description['code'],
'location': description['location'],
})
[docs] async def sendCharacter(self, char: str) -> None:
"""Send character into the page.
This method dispatches a ``keypress`` and ``input`` event. This does
not send a ``keydown`` or ``keyup`` event.
.. note::
Modifier keys DO NOT effect :meth:`sendCharacter`. Holding down
``shift`` will not type the text in upper case.
"""
await self._client.send('Input.dispatchKeyEvent', {
'type': 'char',
'modifiers': self._modifiers,
'text': char,
'key': char,
'unmodifiedText': char,
})
[docs] async def type(self, text: str, options: Dict = None, **kwargs: Any
) -> None:
"""Type characters into a focused element.
This method sends ``keydown``, ``keypress``/``input``, and ``keyup``
event for each character in the ``text``.
To press a special key, like ``Control`` or ``ArrowDown``, use
:meth:`press` method.
:arg str text: Text to type into a focused element.
:arg dict options: Options can have ``delay`` (int|float) field, which
specifies time to wait between key presses in milliseconds. Defaults
to 0.
.. note::
Modifier keys DO NOT effect :meth:`type`. Holding down ``shift``
will not type the text in upper case.
"""
options = merge_dict(options, kwargs)
delay = options.get('delay', 0)
for char in text:
if char in keyDefinitions:
await self.press(char, {'delay': delay})
else:
await self.sendCharacter(char)
if delay:
await asyncio.sleep(delay / 1000)
[docs] async def press(self, key: str, options: Dict = None, **kwargs: Any
) -> None:
"""Press ``key``.
If ``key`` is a single character and no modifier keys besides
``Shift`` are being held down, a ``keypress``/``input`` event will also
generated. The ``text`` option can be specified to force an input event
to be generated.
:arg str key: Name of key to press, such as ``ArrowLeft``.
This method accepts the following options:
* ``text`` (str): If specified, generates an input event with this
text.
* ``delay`` (int|float): Time to wait between ``keydown`` and
``keyup``. Defaults to 0.
.. note::
Modifier keys DO effect :meth:`press`. Holding down ``Shift`` will
type the text in upper case.
"""
options = merge_dict(options, kwargs)
await self.down(key, options)
if 'delay' in options:
await asyncio.sleep(options['delay'] / 1000)
await self.up(key)
[docs]class Mouse(object):
"""Mouse class."""
def __init__(self, client: CDPSession, keyboard: Keyboard) -> None:
self._client = client
self._keyboard = keyboard
self._x = 0.0
self._y = 0.0
self._button = 'none'
[docs] async def move(self, x: float, y: float, options: dict = None,
**kwargs: Any) -> None:
"""Move mouse cursor (dispatches a ``mousemove`` event).
Options can accepts ``steps`` (int) field. If this ``steps`` option
specified, Sends intermediate ``mousemove`` events. Defaults to 1.
"""
options = merge_dict(options, kwargs)
fromX = self._x
fromY = self._y
self._x = x
self._y = y
steps = options.get('steps', 1)
for i in range(1, steps + 1):
x = round(fromX + (self._x - fromX) * (i / steps))
y = round(fromY + (self._y - fromY) * (i / steps))
await self._client.send('Input.dispatchMouseEvent', {
'type': 'mouseMoved',
'button': self._button,
'x': x,
'y': y,
'modifiers': self._keyboard._modifiers,
})
[docs] async def click(self, x: float, y: float, options: dict = None,
**kwargs: Any) -> None:
"""Click button at (``x``, ``y``).
Shortcut to :meth:`move`, :meth:`down`, and :meth:`up`.
This method accepts the following options:
* ``button`` (str): ``left``, ``right``, or ``middle``, defaults to
``left``.
* ``clickCount`` (int): defaults to 1.
* ``delay`` (int|float): Time to wait between ``mousedown`` and
``mouseup`` in milliseconds. Defaults to 0.
"""
options = merge_dict(options, kwargs)
await self.move(x, y)
await self.down(options)
if options and options.get('delay'):
await asyncio.sleep(options.get('delay', 0) / 1000)
await self.up(options)
[docs] async def down(self, options: dict = None, **kwargs: Any) -> None:
"""Press down button (dispatches ``mousedown`` event).
This method accepts the following options:
* ``button`` (str): ``left``, ``right``, or ``middle``, defaults to
``left``.
* ``clickCount`` (int): defaults to 1.
"""
options = merge_dict(options, kwargs)
self._button = options.get('button', 'left')
await self._client.send('Input.dispatchMouseEvent', {
'type': 'mousePressed',
'button': self._button,
'x': self._x,
'y': self._y,
'modifiers': self._keyboard._modifiers,
'clickCount': options.get('clickCount') or 1,
})
[docs] async def up(self, options: dict = None, **kwargs: Any) -> None:
"""Release pressed button (dispatches ``mouseup`` event).
This method accepts the following options:
* ``button`` (str): ``left``, ``right``, or ``middle``, defaults to
``left``.
* ``clickCount`` (int): defaults to 1.
"""
options = merge_dict(options, kwargs)
self._button = 'none'
await self._client.send('Input.dispatchMouseEvent', {
'type': 'mouseReleased',
'button': options.get('button', 'left'),
'x': self._x,
'y': self._y,
'modifiers': self._keyboard._modifiers,
'clickCount': options.get('clickCount') or 1,
})
class Touchscreen(object):
"""Touchscreen class."""
def __init__(self, client: CDPSession, keyboard: Keyboard) -> None:
"""Make new touchscreen object."""
self._client = client
self._keyboard = keyboard
async def tap(self, x: float, y: float) -> None:
"""Tap (``x``, ``y``).
Dispatches a ``touchstart`` and ``touchend`` event.
"""
touchPoints = [{'x': round(x), 'y': round(y)}]
await self._client.send('Input.dispatchTouchEvent', {
'type': 'touchStart',
'touchPoints': touchPoints,
'modifiers': self._keyboard._modifiers,
})
await self._client.send('Input.dispatchTouchEvent', {
'type': 'touchEnd',
'touchPoints': [],
'modifiers': self._keyboard._modifiers,
})