From da92c7b7b39007cce71231b969cd27bddca99ab7 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 16 Oct 2026 23:44:03 +0300 Subject: [PATCH 1/3] feat: accept fps and showActions in recordVideo RecordVideo declared only dir and size, so New Context and New Persistent Context rejected Playwright's fps and showActions options. Closes #5346 Co-Authored-By: Claude Opus 5.5 --- Browser/utils/data_types.py | 43 ++++++++++++++++++++ atest/test/01_Browser_Management/video.robot | 9 ++++ utest/test_adopted_context.py | 25 ++++++++++++ 3 files changed, 77 insertions(+) diff --git a/Browser/utils/data_types.py b/Browser/utils/data_types.py index 63aa6cb6f..16a6c7e58 100644 --- a/Browser/utils/data_types.py +++ b/Browser/utils/data_types.py @@ -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): diff --git a/atest/test/01_Browser_Management/video.robot b/atest/test/01_Browser_Management/video.robot index f8f656517..f821927c7 100644 --- a/atest/test/01_Browser_Management/video.robot +++ b/atest/test/01_Browser_Management/video.robot @@ -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"* diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index 22849382d..5758c9287 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -253,3 +253,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"} From 39204b8a40740ee8cbd8df6dcd995ff316d8637a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Fri, 16 Oct 2026 23:51:12 +0300 Subject: [PATCH 2/3] fix: convert adopted context options like New Context adopted_context_options passed recordVideo, recordHar and viewport on as given, so the values were not converted and unknown keys were kept, unlike New Context. They now go through the same Robot Framework conversion as the arguments of New Context, and AdoptedContextOptions types recordVideo and recordHar as RecordVideo and RecordHar. Co-Authored-By: Claude Opus 5.5 --- Browser/browser.py | 14 ++++++++------ Browser/keywords/playwright_state.py | 16 ++++++++-------- Browser/utils/data_types.py | 4 ++-- utest/test_adopted_context.py | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/Browser/browser.py b/Browser/browser.py index 713a18bd6..d3274300b 100755 --- a/Browser/browser.py +++ b/Browser/browser.py @@ -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 @@ -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, @@ -912,9 +912,10 @@ 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 + ``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 @@ -926,7 +927,8 @@ def adopted_context_options( ``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 ) diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index 1b44406b6..8f9022d67 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -1139,7 +1139,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) @@ -1148,9 +1148,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") @@ -1170,15 +1170,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"): diff --git a/Browser/utils/data_types.py b/Browser/utils/data_types.py index 16a6c7e58..d3afa38c7 100644 --- a/Browser/utils/data_types.py +++ b/Browser/utils/data_types.py @@ -772,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 diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index 5758c9287..69945668b 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -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 @@ -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_unknown_video_option_fails_like_new_context(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + with pytest.raises(ValueError, match="RecordVideo"): + browser.adopted_context_options(recordVideo={"framerate": 10}) + + def test_video_folder_and_size_follow_new_context(tmpdir, monkeypatch): browser = library(tmpdir, monkeypatch) From 7066ce5c5c9c6ed4874a003c2176ef348e190556 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 00:24:30 +0300 Subject: [PATCH 3/3] docs: point New Context to RecordVideo and test the deprecated fontSize The New Context docstring now points to RecordVideo for the new video options, and the failure test of adopted_context_options sends the deprecated showActions.fontSize, which RecordVideo leaves out. Co-Authored-By: Claude Opus 5.5 --- Browser/browser.py | 11 ++++++----- Browser/keywords/playwright_state.py | 3 ++- utest/test_adopted_context.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Browser/browser.py b/Browser/browser.py index d3274300b..0f0c90e59 100755 --- a/Browser/browser.py +++ b/Browser/browser.py @@ -915,11 +915,12 @@ def adopted_context_options( ``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. + 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`` diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index 8f9022d67..9dd23ee17 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -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``. diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index 69945668b..daae34cad 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -137,11 +137,11 @@ def test_video_options_are_converted_like_new_context(tmpdir, monkeypatch): assert options["recordVideo"]["showActions"] == {"duration": 300} -def test_unknown_video_option_fails_like_new_context(tmpdir, monkeypatch): +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={"framerate": 10}) + browser.adopted_context_options(recordVideo={"showActions": {"fontSize": 20}}) def test_video_folder_and_size_follow_new_context(tmpdir, monkeypatch):