Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions Browser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from copy import copy
from datetime import timedelta
from pathlib import Path
from typing import Any, Literal
from typing import Any, Literal, get_type_hints

from assertionengine import AssertionOperator
from overrides import overrides
Expand Down Expand Up @@ -64,7 +64,7 @@
)
from .keywords.crawling import Crawling
from .playwright import Playwright
from .python_arguments import add_argument_conversion
from .python_arguments import add_argument_conversion, converting_proxy
from .utils import (
AutoClosingLevel,
PlaywrightLogTypes,
Expand Down Expand Up @@ -912,21 +912,24 @@ def adopted_context_options(

They follow the rules of `New Context`: ``tracing`` gives the trace path or
folder, also when ``ROBOT_FRAMEWORK_BROWSER_TRACING`` is set.
``recordVideo`` gets the video folder, Browser's video folder by default, and
the size, and keeps its other keys, so an empty dictionary records with the
defaults. Without a ``size``, the video has the size of ``viewport``, the
viewport that the extension creates the context with, or 1280x720. A
relative ``path`` of ``recordHar`` becomes relative to the output directory.
``None`` means off. ``tracesDir`` is a new folder for Playwright's temporary
trace files, as Browser gives one to each browser that it launches.
``recordVideo``, ``recordHar`` and ``viewport`` are converted like the
arguments of `New Context`, so an unknown key fails. ``recordVideo`` gets
the video folder, Browser's video folder by default, and the size, so an
empty dictionary records with the defaults. Without a ``size``, the video
has the size of ``viewport``, the viewport that the extension creates the
context with, or 1280x720. A relative ``path`` of ``recordHar`` becomes
relative to the output directory. ``None`` means off. ``tracesDir`` is a
new folder for Playwright's temporary trace files, as Browser gives one to
each browser that it launches.

The keys can be passed to the extension function as keyword arguments:
``tracing`` for the ``tracing`` option of ``adoptContext``, ``recordVideo``
and ``recordHar`` for Playwright when the extension creates the context, and
``tracesDir`` when it launches a browser or an application. After the
extension returns, call ``register_adopted_context`` with these options.
"""
return self._playwright_state.adopted_context_options(
options = self._playwright_state.adopted_context_options
return converting_proxy(options, get_type_hints(options))(
tracing, recordVideo, recordHar, viewport
)

Expand Down
19 changes: 10 additions & 9 deletions Browser/keywords/playwright_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,8 @@ def new_context(
Make sure to close the context for the HAR to be saved.
- ``recordVideo``: Enables video recording for all pages into a folder. If not
specified videos are not recorded. Make sure to close the context for
videos to be saved. Video is not supported in remote browsers.
videos to be saved. Video is not supported in remote browsers. See
`RecordVideo` for the folder, size, frame rate and action annotations.
- ``reducedMotion``: Emulates the ``prefers-reduced-motion`` media feature,
supported values are ``reduce`` and ``no-preference``. Defaults to
``no-preference``.
Expand Down Expand Up @@ -1139,7 +1140,7 @@ def _register_context(
self,
context_id: str,
trace_file: str,
video_size: dict,
video_size: ViewportDimensions,
video: dict | None = None,
) -> str:
self.add_context_and_keyword_call_stack_to_trace(trace_file, context_id)
Expand All @@ -1148,9 +1149,9 @@ def _register_context(

def _record_video(
self,
record_video: Mapping[str, Any] | None,
record_video: RecordVideo | None,
viewport: Mapping[str, Any] | None,
) -> dict[str, Any] | None:
) -> RecordVideo | None:
if record_video is None:
return None
video_dir = record_video.get("dir")
Expand All @@ -1170,15 +1171,15 @@ def _video_size(
self,
record_video: Mapping[str, Any] | None,
viewport: Mapping[str, Any] | None,
) -> dict:
) -> ViewportDimensions:
size = (record_video or {}).get("size") or {}
if size.get("width") and size.get("height"):
return {"width": int(size["width"]), "height": int(size["height"])}
return dict(viewport) if viewport else {"width": 1280, "height": 720}
if viewport:
return {"width": viewport["width"], "height": viewport["height"]}
return {"width": 1280, "height": 720}

def _record_har(
self, record_har: Mapping[str, Any] | None
) -> dict[str, Any] | None:
def _record_har(self, record_har: RecordHar | None) -> RecordHar | None:
if record_har is None:
return None
if not record_har.get("path"):
Expand Down
47 changes: 45 additions & 2 deletions Browser/utils/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,16 +686,59 @@ class Dimensions(ViewportDimensions):
"""Dimensions of an object in pixels."""


class ShowActionsPosition(str, Enum):
top_left = "top-left"
top = "top"
top_right = "top-right"
bottom_left = "bottom-left"
bottom = "bottom"
bottom_right = "bottom-right"


class ShowActionsCursor(str, Enum):
pointer = "pointer"
none = "none"


class ShowActionsStyle(TypedDict, total=False):
point: str
highlight: str
title: str


class ShowActions(TypedDict, total=False):
duration: int
position: ShowActionsPosition
cursor: ShowActionsCursor
style: ShowActionsStyle


class RecordVideo(TypedDict, total=False):
"""Enables Video recording

| =Key= | =Description= |
| ``dir`` | Folder of the videos. |
| ``size`` | `ViewportDimensions` of the videos. |
| ``fps`` | Frame rate of the videos. Defaults to 25. Firefox and WebKit record at most 25 frames per second. |
| ``showActions`` | Annotates the interacted elements in the videos, with the keys below. |

Keys of ``showActions``:
| =Key= | =Description= |
| ``duration`` | How long each annotation is shown, in milliseconds. Defaults to 500. |
| ``position`` | Position of the action title: ``top-left``, ``top``, ``top-right`` (default), ``bottom-left``, ``bottom`` or ``bottom-right``. |
| ``cursor`` | ``pointer`` (default) shows a mouse pointer that moves from one action point to the next, ``none`` shows no cursor. |
| ``style`` | CSS declarations of the annotations: ``point`` for the marker on the action point and ``highlight`` for the box over the element, both shown only when given, and ``title`` for the action title. |

Examples:
| New Context recordVideo={'dir':'videos', 'size':{'width':400, 'height':200}}
| New Context recordVideo={'dir': 'd:/automation/video'}
| New Context recordVideo={'showActions': {'position': 'bottom', 'style': {'title': 'font-size: 32px'}}}
"""

dir: str
size: ViewportDimensions
fps: int
showActions: ShowActions


class RecordHar(TypedDict, total=False):
Expand Down Expand Up @@ -729,8 +772,8 @@ class AdoptedContextOptions(TypedDict):
"""

tracing: str | None
recordVideo: dict[str, Any] | None
recordHar: dict[str, Any] | None
recordVideo: RecordVideo | None
recordHar: RecordHar | None
tracesDir: str


Expand Down
9 changes: 9 additions & 0 deletions atest/test/01_Browser_Management/video.robot
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ Create Video With VideoSize
New Page file://${details}[video_path]
Get BoundingBox video ALL validate value['width'] == 300 and value['height'] == 200

Create Video With Action Annotations And Frame Rate
VAR &{show_actions} = position=bottom-left cursor=none style={'title': 'font-size: 32px'}
VAR &{record_video} = dir=annotated_video fps=${10} showActions=${show_actions}
New Context recordVideo=${record_video}
${details} = New Page ${LOGIN_URL}
Click input#login_button
Close Context
Wait Until File Exists ${details}[video_path]

Create Video With Viewport
[Documentation]
... LOG 4:3 INFO GLOB: *width="400" height="250"*.webm"*
Expand Down
45 changes: 45 additions & 0 deletions utest/test_adopted_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from Browser import Browser
from Browser.base.cache import ContextCache
from Browser.utils.data_types import KeywordCallStackEntry, TracingGroupMode
Expand Down Expand Up @@ -124,6 +126,24 @@ def test_video_keeps_its_other_keys(tmpdir, monkeypatch):
assert options["recordVideo"]["showActions"] == {"duration": 500}


def test_video_options_are_converted_like_new_context(tmpdir, monkeypatch):
browser = library(tmpdir, monkeypatch)

options = browser.adopted_context_options(
recordVideo={"fps": "10", "showActions": {"duration": "300"}}
)

assert options["recordVideo"]["fps"] == 10
assert options["recordVideo"]["showActions"] == {"duration": 300}


def test_deprecated_font_size_fails_like_new_context(tmpdir, monkeypatch):
browser = library(tmpdir, monkeypatch)

with pytest.raises(ValueError, match="RecordVideo"):
browser.adopted_context_options(recordVideo={"showActions": {"fontSize": 20}})


def test_video_folder_and_size_follow_new_context(tmpdir, monkeypatch):
browser = library(tmpdir, monkeypatch)

Expand Down Expand Up @@ -253,3 +273,28 @@ def test_new_persistent_context_resolves_a_relative_har_path(tmpdir, monkeypatch
assert options["recordHar"] == {
"path": str(Path(tmpdir, "persistent.har").resolve())
}


def test_new_context_sends_frame_rate_and_action_annotations(tmpdir, monkeypatch):
browser = library(tmpdir, monkeypatch)
show_actions = {
"duration": 300,
"position": "bottom-left",
"cursor": "none",
"style": {"title": "font-size: 32px"},
}

browser.new_context(recordVideo={"fps": 10, "showActions": show_actions})

options = sent_options(browser._playwright.stub.NewContext)
assert options["recordVideo"]["fps"] == 10
assert options["recordVideo"]["showActions"] == show_actions


def test_new_persistent_context_sends_action_annotations(tmpdir, monkeypatch):
browser = library(tmpdir, monkeypatch)

browser.new_persistent_context(recordVideo={"showActions": {"position": "top"}})

options = sent_options(browser._playwright.stub.NewPersistentContext)
assert options["recordVideo"]["showActions"] == {"position": "top"}
Loading