Skip to content

Commit

Permalink
Relative Mouse Move, defaultBrowserType, TypedDict Converter (#469)
Browse files Browse the repository at this point in the history
* created TypedDictConverter

* fixed #470 defaultBrowserType

* fixed #467 relative mouse movement
  • Loading branch information
Snooz82 authored Nov 1, 2020
1 parent c8c8102 commit 7a01445
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 72 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install ffmpeg on unix-like
- name: Install ffmpeg and libgles on unix-like
run: |
sudo apt-get install ffmpeg
sudo apt-get install libgles2
if: matrix.os != 'windows-latest'
- name: Install python dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion Browser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ def keyword_error(self):
def _failure_screenshot_path(self):
test_name = BuiltIn().get_variable_value("${TEST NAME}")
return os.path.join(
BuiltIn().get_variable_value("${OUTPUTDIR}"),
self.outputdir,
test_name.replace(" ", "_") + "_FAILURE_SCREENSHOT_{index}",
).replace("\\", "\\\\")

Expand Down
2 changes: 1 addition & 1 deletion Browser/keywords/getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ def get_boundingbox(
with self.playwright.grpc_channel() as stub:
response = stub.GetBoundingBox(Request.ElementSelector(selector=selector))
parsed = json.loads(response.json)
logger.debug(parsed)
logger.debug(f"BoundingBox: {parsed}")
if key == BoundingBoxFields.ALL:
return int_dict_verify_assertion(
parsed, assertion_operator, assertion_expected, "BoundingBox is"
Expand Down
50 changes: 34 additions & 16 deletions Browser/keywords/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,25 +274,14 @@ def hover(
``selector`` Selector element to click.
See the `Finding elements` section for details about the selectors.
``button`` The button that shall be used for clicking.
``clickCount`` How many time shall be clicked.
``delay`` Time to wait between mouse-down and mouse-up and next click.
*Caution: be aware that if the delay leads to a total time that exceeds the timeout, the keyword fails*
``position_x`` & ``position_y`` A point to click relative to the
top-left corner of element bounding-box. Only positive values within the bounding-box are allowed.
If not specified, clicks to some visible point of the element.
*Caution: even with 0, 0 might click a few pixels off from the corner of the bounding-box. Click uses detection to find the first clickable point.*
``position_x`` & ``position_y`` A point to hover relative to the top-left corner of element bounding box.
If not specified, hovers over some visible point of the element.
Only positive values within the bounding-box are allowed.
``force`` Set to True to skip Playwright's [https://github.com/microsoft/playwright/blob/master/docs/actionability.md | Actionability checks].
``*modifiers``
Modifier keys to press. Ensures that only these modifiers are pressed
during the click, and then restores current modifiers back.
``*modifiers`` Modifier keys to press. Ensures that only these modifiers are
pressed during the hover, and then restores current modifiers back.
If not specified, currently pressed modifiers are used.
"""
with self.playwright.grpc_channel() as stub:
Expand Down Expand Up @@ -665,6 +654,35 @@ def _center_of_boundingbox(boundingbox: BoundingBox) -> Coordinates:
center["y"] = boundingbox["y"] + (boundingbox["height"] / 2)
return center

@keyword(tags=["Setter", "PageContent"])
def mouse_move_relative_to(
self, selector: str, x: float = 0.0, y: float = 0.0, steps: int = 1
):
"""Moves the mouse cursor relative to the selected element.
``x`` ``y`` are relative coordinates to the center of the elements bounding box.
``steps`` Number of intermediate steps for the mouse event.
This is sometime needed for websites to recognize the movement.
"""
with self.playwright.grpc_channel() as stub:
bbox = self.library.get_boundingbox(selector)
center = self._center_of_boundingbox(bbox)
body: MouseOptionsDict = {
"x": center["x"] + x,
"y": center["y"] + y,
"options": {"steps": steps},
}
logger.info(
f"Moving mouse relative to element center by x: {x}, y: {y} coordinates."
)
logger.debug(f"Element Center is: {center}")
logger.debug(
f"Mouse Position is: {{'x': {center['x'] + x}, 'y': {center['y'] + y}}}"
)
response = stub.MouseMove(Request().Json(body=json.dumps(body)))
logger.debug(response.log)

@keyword(tags=["Setter", "PageContent"])
def mouse_move(self, x: float, y: float, steps: int = 1):
"""Instead of selectors command mouse with coordinates.
Expand Down
65 changes: 35 additions & 30 deletions Browser/keywords/playwright_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
from ..generated.playwright_pb2 import Request
from ..utils import (
ColorScheme,
GeoLocation,
HttpCredentials,
Proxy,
SelectionType,
SupportedBrowsers,
ViewportDimensions,
convert_typed_dict,
find_by_id,
locals_to_params,
logger,
Expand Down Expand Up @@ -232,7 +236,7 @@ def new_browser(
executablePath: Optional[str] = None,
args: Optional[List[str]] = None,
ignoreDefaultArgs: Optional[List[str]] = None,
proxy: Optional[Dict] = None,
proxy: Optional[Proxy] = None,
downloadsPath: Optional[str] = None,
handleSIGINT: bool = True,
handleSIGTERM: bool = True,
Expand Down Expand Up @@ -292,6 +296,7 @@ def new_browser(
Useful so that you can see what is going on. Defaults to no delay.
"""
params = locals_to_params(locals())
params = convert_typed_dict(Proxy, params, "proxy")
if timeout:
params["timeout"] = self.convert_timeout(timeout)
params["slowMo"] = self.convert_timeout(slowMo)
Expand Down Expand Up @@ -319,25 +324,26 @@ def new_context(
hasTouch: bool = False,
javaScriptEnabled: bool = True,
timezoneId: Optional[str] = None,
geolocation: Optional[Dict] = None,
geolocation: Optional[GeoLocation] = None,
locale: Optional[str] = None,
permissions: Optional[List[str]] = None,
extraHTTPHeaders: Optional[Dict[str, str]] = None,
offline: bool = False,
httpCredentials: Optional[Dict] = None,
httpCredentials: Optional[HttpCredentials] = None,
colorScheme: Optional[ColorScheme] = None,
hideRfBrowser: bool = False,
defaultBrowserType: Optional[str] = None,
proxy: Optional[Proxy] = None,
videosPath: Optional[str] = None,
videoSize: Optional[Dict[str, int]] = None,
videoSize: Optional[ViewportDimensions] = None,
defaultBrowserType: Optional[SupportedBrowsers] = None,
hideRfBrowser: bool = False,
) -> str:
"""Create a new BrowserContext with specified options.
See `Browser, Context and Page` for more information about BrowserContext.
Returns a stable identifier for the created context
that can be used in `Switch Context`.
``acceptDownloads`` Whether to automatically downloadall the attachments.
``acceptDownloads`` Whether to automatically downloads all the attachments.
Defaults to False where all the downloads are canceled.
``ignoreHTTPSErrors`` Whether to ignore HTTPS errors during navigation.
Expand Down Expand Up @@ -367,7 +373,7 @@ def new_context(
See [https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/source/data/misc/metaZones.txt | ICU’s metaZones.txt]
for a list of supported timezone IDs.
``geolocation`` Sets the geolocation. No location is set be default.
``geolocation`` Sets the geolocation. No location is set by default.
- ``latitude`` <number> Latitude between -90 and 90.
- ``longitude`` <number> Longitude between -180 and 180.
- ``accuracy`` Optional <number> Non-negative accuracy value. Defaults to 0.
Expand Down Expand Up @@ -396,15 +402,30 @@ def new_context(
See [https://github.com/microsoft/playwright/blob/master/docs/api.md#pageemulatemediaoptions|emulateMedia(options)]
for more details. Defaults to ``light``.
``videosPath`` <str> Enables video recording for all pages to videosPath
``proxy`` Network proxy settings to use with this context.
Note that browser needs to be launched with the global proxy for this option to work.
If all contexts override the proxy, global proxy will be never used and can be any string
``videosPath`` Enables video recording for all pages to videosPath
folder. If not specified, videos are not recorded.
``videoSize`` <dictionary, int> Specifies dimensions of the automatically recorded
``videoSize`` Specifies dimensions of the automatically recorded
video. Can only be used if videosPath is set. If not specified the size will
be equal to viewport. If viewport is not configured explicitly the video size
defaults to 1280x720. Actual picture of the page will be scaled down if
necessary to fit specified size.
- Example {"width": 1280, "height": 720}
``defaultBrowserType`` If no browser is open and `New Context` opens a new browser
with defaults, it now uses this setting.
Very useful together with `Get Device` keyword:
Example:
| Test an iPhone
| ${device}= `Get Device` iPhone X
| `New Context` &{device} # unpacking here with &
| `New Page` http://example.com
A BrowserContext is the Playwright object that controls a single browser profile.
Within a context caches and cookies are shared.
See [https://github.com/microsoft/playwright/blob/master/docs/api.md#browsernewcontextoptions|Playwright browser.newContext]
Expand All @@ -413,21 +434,12 @@ def new_context(
If there's no open Browser this keyword will open one. Does not create pages.
"""
params = locals_to_params(locals())
if "geolocation" in params:
location = params["geolocation"]
latitude = location["latitude"]
location["latitude"] = float(latitude)
longitude = location["longitude"]
location["longitude"] = float(longitude)
accuracy = location.get("accuracy")
if accuracy:
location["accuracy"] = float(accuracy)
params = convert_typed_dict(GeoLocation, params, "geolocation")
params = convert_typed_dict(ViewportDimensions, params, "viewport")
params = convert_typed_dict(Proxy, params, "proxy")
params = convert_typed_dict(ViewportDimensions, params, "videoSize")
if not videosPath:
params.pop("videoSize", None)
if "videoSize" in params:
params = self._size_to_number(params, "videoSize")
if "viewport" in params:
params = self._size_to_number(params, "viewport")
options = json.dumps(params, default=str)
logger.info(options)
with self.playwright.grpc_channel() as stub:
Expand All @@ -449,13 +461,6 @@ def _get_video_size(self, params: dict) -> dict:
return params["viewport"]
return {"width": 1280, "height": 720}

def _size_to_number(self, params: dict, argument: str) -> dict:
width = int(params[argument]["width"])
height = int(params[argument]["height"])
params[argument]["width"] = width
params[argument]["height"] = height
return params

@keyword(tags=["Setter", "BrowserControl"])
def new_page(self, url: Optional[str] = None) -> str:
"""Open a new Page. A Page is the Playwright equivalent to a tab.
Expand Down
4 changes: 4 additions & 0 deletions Browser/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
CookieSameSite,
CookieType,
ElementState,
GeoLocation,
HttpCredentials,
Proxy,
RequestMethod,
SelectAttribute,
SelectionType,
SupportedBrowsers,
ViewportDimensions,
convert_typed_dict,
)
from .js_utilities import (
exec_scroll_function,
Expand Down
97 changes: 87 additions & 10 deletions Browser/utils/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,99 @@
# limitations under the License.

from enum import Enum, auto
from typing import Dict

from typing_extensions import TypedDict

BoundingBox = TypedDict(
"BoundingBox",
{"x": float, "y": float, "width": float, "height": float},
total=False,
)

Coordinates = TypedDict("Coordinates", {"x": float, "y": float}, total=False)
def convert_typed_dict(data_type, params: Dict, key: str) -> Dict:
if key not in params:
return params
dictionary = {k.lower(): v for k, v in params[key].items()}
struct = data_type.__annotations__
typed_dict = data_type()
for req_key in data_type.__required_keys__:
if req_key.lower() not in dictionary:
raise RuntimeError(
f"`{dictionary}` cannot be converted to {data_type.__name__}."
f"\nThe required key '{req_key}' in not set in given value."
f"\nExpected types: {data_type.__annotations__}"
)
typed_dict[req_key] = struct[req_key](dictionary[req_key.lower()])
for opt_key in data_type.__optional_keys__:
if opt_key.lower() not in dictionary:
continue
typed_dict[opt_key] = struct[opt_key](dictionary[opt_key.lower()])
params[key] = typed_dict
return params

MouseOptionsDict = TypedDict(
"MouseOptionsDict", {"x": float, "y": float, "options": dict}, total=False
)

ViewportDimensions = TypedDict("ViewportDimensions", {"width": int, "height": int})
class BoundingBox(TypedDict, total=False):
x: float
y: float
width: float
height: float


class Coordinates(TypedDict, total=False):
x: float
y: float


class MouseOptionsDict(TypedDict, total=False):
x: float
y: float
options: dict


class ViewportDimensions(TypedDict):
width: int
height: int


class HttpCredentials(TypedDict):
username: str
password: str


class _GeoCoordinated(TypedDict):
longitude: float
latitude: float


class GeoLocation(_GeoCoordinated, total=False):
"""Defines the geolocation.
- ``latitude`` Latitude between -90 and 90.
- ``longitude`` Longitude between -180 and 180.
- ``accuracy`` *Optional* Non-negative accuracy value. Defaults to 0.
Example usage: ``{'latitude': 59.95, 'longitude': 30.31667}``"""

accuracy: float


class _Server(TypedDict):
server: str


class Proxy(_Server, total=False):
"""Network proxy settings.
``server`` Proxy to be used for all requests. HTTP and SOCKS proxies are supported,
for example http://myproxy.com:3128 or socks5://myproxy.com:3128.
Short form myproxy.com:3128 is considered an HTTP proxy.
``bypass`` *Optional* coma-separated domains to bypass proxy,
for example ".com, chromium.org, .domain.com".
``username`` *Optional* username to use if HTTP proxy requires authentication.
``password`` *Optional* password to use if HTTP proxy requires authentication.
"""

bypass: str
Username: str
password: str


class SelectionType(Enum):
Expand Down
Loading

0 comments on commit 7a01445

Please sign in to comment.