From 2a4654d92e6fbcd969d86bdb8fc0022a735676df Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 19:56:38 +0300 Subject: [PATCH 1/4] test: give the adopted context tests a browser fixture Co-Authored-By: Claude Opus 5.5 --- utest/test_adopted_context.py | 103 +++++++++++----------------------- 1 file changed, 32 insertions(+), 71 deletions(-) diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index b240283e7..a47235d3a 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -25,7 +25,8 @@ def grpc_channel(self): yield self.stub -def library(tmpdir, monkeypatch) -> Browser: +@pytest.fixture +def browser(tmpdir, monkeypatch) -> Browser: monkeypatch.setattr(Browser, "_output_dir", tmpdir) monkeypatch.setattr(Browser, "_context_cache", ContextCache()) monkeypatch.delenv("ROBOT_FRAMEWORK_BROWSER_TRACING", raising=False) @@ -42,9 +43,7 @@ def sent_options(request) -> dict: return json.loads(request.call_args.args[0].rawOptions) -def test_no_tracing_and_no_recording_by_default(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_no_tracing_and_no_recording_by_default(browser: Browser): options = browser.adopted_context_options() assert options["tracing"] is None @@ -53,10 +52,8 @@ def test_no_tracing_and_no_recording_by_default(tmpdir, monkeypatch): def test_traces_dir_is_a_new_folder_in_browsers_temporary_trace_folder( - tmpdir, monkeypatch + browser: Browser, ): - browser = library(tmpdir, monkeypatch) - first = browser.adopted_context_options()["tracesDir"] second = browser.adopted_context_options()["tracesDir"] @@ -64,9 +61,7 @@ def test_traces_dir_is_a_new_folder_in_browsers_temporary_trace_folder( assert first != second -def test_tracing_follows_new_context(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_tracing_follows_new_context(browser: Browser, tmpdir): assert browser.adopted_context_options(tracing=True)["tracing"] == str( browser.traces_output ) @@ -75,23 +70,19 @@ def test_tracing_follows_new_context(tmpdir, monkeypatch): ) -def test_relative_trace_folder_is_in_browsers_trace_folder(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_relative_trace_folder_is_in_browsers_trace_folder(browser: Browser): options = browser.adopted_context_options(tracing=Path("adopted")) assert options["tracing"] == str((browser.traces_output / "adopted").resolve()) -def test_tracing_follows_the_global_setting(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_tracing_follows_the_global_setting(browser: Browser, monkeypatch): monkeypatch.setenv("ROBOT_FRAMEWORK_BROWSER_TRACING", "True") assert browser.adopted_context_options()["tracing"] == str(browser.traces_output) -def test_tracing_follows_the_robot_framework_variable(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_tracing_follows_the_robot_framework_variable(browser: Browser, monkeypatch): variables = {"${ROBOT_FRAMEWORK_BROWSER_TRACING}": "True"} monkeypatch.setattr( "Browser.keywords.playwright_state.EXECUTION_CONTEXTS", @@ -105,9 +96,7 @@ def test_tracing_follows_the_robot_framework_variable(tmpdir, monkeypatch): assert browser.adopted_context_options()["tracing"] == str(browser.traces_output) -def test_empty_video_options_record_with_the_defaults(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_empty_video_options_record_with_the_defaults(browser: Browser): options = browser.adopted_context_options(recordVideo={}) assert options["recordVideo"] == { @@ -116,9 +105,7 @@ def test_empty_video_options_record_with_the_defaults(tmpdir, monkeypatch): } -def test_video_keeps_its_other_keys(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_video_keeps_its_other_keys(browser: Browser): options = browser.adopted_context_options( recordVideo={"showActions": {"duration": 500}} ) @@ -126,9 +113,7 @@ 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) - +def test_video_options_are_converted_like_new_context(browser: Browser): options = browser.adopted_context_options( recordVideo={"fps": "10", "showActions": {"duration": "300"}} ) @@ -137,16 +122,12 @@ def test_video_options_are_converted_like_new_context(tmpdir, monkeypatch): assert options["recordVideo"]["showActions"] == {"duration": 300} -def test_deprecated_font_size_fails_like_new_context(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_deprecated_font_size_fails_like_new_context(browser: Browser): 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) - +def test_video_folder_and_size_follow_new_context(browser: Browser): options = browser.adopted_context_options( recordVideo={"dir": "demo", "size": {"width": "800", "height": "600"}} ) @@ -157,8 +138,7 @@ def test_video_folder_and_size_follow_new_context(tmpdir, monkeypatch): } -def test_absolute_video_folder_is_kept(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_absolute_video_folder_is_kept(browser: Browser, tmpdir): videos = str(Path(tmpdir, "elsewhere", "videos").resolve()) options = browser.adopted_context_options(recordVideo={"dir": videos}) @@ -166,9 +146,7 @@ def test_absolute_video_folder_is_kept(tmpdir, monkeypatch): assert options["recordVideo"]["dir"] == videos -def test_video_without_size_has_the_size_of_the_viewport(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_video_without_size_has_the_size_of_the_viewport(browser: Browser): options = browser.adopted_context_options( recordVideo={}, viewport={"width": 1024, "height": 768} ) @@ -176,8 +154,7 @@ def test_video_without_size_has_the_size_of_the_viewport(tmpdir, monkeypatch): assert options["recordVideo"]["size"] == {"width": 1024, "height": 768} -def test_registered_context_gets_the_keyword_calls_as_trace_groups(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_registered_context_gets_the_keyword_calls_as_trace_groups(browser: Browser): browser.keyword_call_stack.append(keyword_call("Open Application")) options = browser.adopted_context_options(tracing=True) @@ -188,8 +165,7 @@ def test_registered_context_gets_the_keyword_calls_as_trace_groups(tmpdir, monke assert browser.tracing_contexts == ["context=1"] -def test_registered_context_without_tracing_gets_no_trace_groups(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_registered_context_without_tracing_gets_no_trace_groups(browser: Browser): browser.keyword_call_stack.append(keyword_call("Open Application")) browser.register_adopted_context("context=1", browser.adopted_context_options()) @@ -198,8 +174,7 @@ def test_registered_context_without_tracing_gets_no_trace_groups(tmpdir, monkeyp assert browser.tracing_contexts == [] -def test_playwright_tracing_group_mode_adds_no_trace_groups(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_playwright_tracing_group_mode_adds_no_trace_groups(browser: Browser): browser.tracing_group_mode = TracingGroupMode.Playwright browser.keyword_call_stack.append(keyword_call("Open Application")) options = browser.adopted_context_options(tracing=True) @@ -211,9 +186,8 @@ def test_playwright_tracing_group_mode_adds_no_trace_groups(tmpdir, monkeypatch) def test_registered_context_outside_a_keyword_is_traced_without_trace_groups( - tmpdir, monkeypatch + browser: Browser, ): - browser = library(tmpdir, monkeypatch) browser.tracing_group_mode = TracingGroupMode.Browser options = browser.adopted_context_options(tracing=True) @@ -223,8 +197,7 @@ def test_registered_context_outside_a_keyword_is_traced_without_trace_groups( assert browser.tracing_contexts == ["context=1"] -def test_registered_context_embeds_its_video(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_registered_context_embeds_its_video(browser: Browser): options = browser.adopted_context_options(recordVideo={}) video = str(Path(options["recordVideo"]["dir"], "page.webm")) @@ -237,8 +210,7 @@ def test_registered_context_embeds_its_video(tmpdir, monkeypatch): } -def test_video_path_without_record_video_is_ignored(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_video_path_without_record_video_is_ignored(browser: Browser): options = browser.adopted_context_options() video_path = browser.register_adopted_context("context=1", options, "page.webm") @@ -246,9 +218,9 @@ def test_video_path_without_record_video_is_ignored(tmpdir, monkeypatch): assert video_path == "" -def test_relative_har_path_is_relative_to_the_output_directory(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_relative_har_path_is_relative_to_the_output_directory( + browser: Browser, tmpdir +): options = browser.adopted_context_options( recordHar={"path": "har/app.har", "omitContent": True} ) @@ -259,8 +231,7 @@ def test_relative_har_path_is_relative_to_the_output_directory(tmpdir, monkeypat } -def test_absolute_har_path_is_kept(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_absolute_har_path_is_kept(browser: Browser, tmpdir): har = str(Path(tmpdir, "elsewhere", "app.har").resolve()) options = browser.adopted_context_options(recordHar={"path": har}) @@ -268,18 +239,14 @@ def test_absolute_har_path_is_kept(tmpdir, monkeypatch): assert options["recordHar"] == {"path": har} -def test_new_context_resolves_a_relative_har_path(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_new_context_resolves_a_relative_har_path(browser: Browser, tmpdir): browser.new_context(recordHar={"path": "context.har"}) options = sent_options(browser._playwright.stub.NewContext) assert options["recordHar"] == {"path": str(Path(tmpdir, "context.har").resolve())} -def test_new_persistent_context_resolves_a_relative_har_path(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_new_persistent_context_resolves_a_relative_har_path(browser: Browser, tmpdir): browser.new_persistent_context(recordHar={"path": "persistent.har"}) options = sent_options(browser._playwright.stub.NewPersistentContext) @@ -288,8 +255,7 @@ def test_new_persistent_context_resolves_a_relative_har_path(tmpdir, monkeypatch } -def test_new_context_sends_frame_rate_and_action_annotations(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) +def test_new_context_sends_frame_rate_and_action_annotations(browser: Browser): show_actions = { "duration": 300, "position": "bottom-left", @@ -304,9 +270,7 @@ def test_new_context_sends_frame_rate_and_action_annotations(tmpdir, monkeypatch assert options["recordVideo"]["showActions"] == show_actions -def test_new_persistent_context_sends_action_annotations(tmpdir, monkeypatch): - browser = library(tmpdir, monkeypatch) - +def test_new_persistent_context_sends_action_annotations(browser: Browser): browser.new_persistent_context(recordVideo={"showActions": {"position": "top"}}) options = sent_options(browser._playwright.stub.NewPersistentContext) @@ -325,10 +289,9 @@ def persistent_context_with_video(browser: Browser) -> MagicMock: def test_new_persistent_context_caches_and_embeds_its_video_once_its_url_opened( - tmpdir, monkeypatch, caplog + browser: Browser, caplog ): caplog.set_level("INFO") - browser = library(tmpdir, monkeypatch) persistent_context_with_video(browser) browser.new_persistent_context(url="http://localhost:1", recordVideo={}) @@ -340,8 +303,7 @@ def test_new_persistent_context_caches_and_embeds_its_video_once_its_url_opened( assert " Date: Sat, 17 Oct 2026 19:57:49 +0300 Subject: [PATCH 2/4] test: pin adopted context options to what New Context sends Compare adopted_context_options with the recordVideo, recordHar and trace file that New Context sends for the same arguments, so the two cannot drift apart. New Context with an empty recordVideo records into the video folder, and a registered adopted context embeds its video in the log. Co-Authored-By: Claude Opus 5.5 --- utest/test_adopted_context.py | 48 +++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index a47235d3a..a1b706218 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -197,17 +197,18 @@ def test_registered_context_outside_a_keyword_is_traced_without_trace_groups( assert browser.tracing_contexts == ["context=1"] -def test_registered_context_embeds_its_video(browser: Browser): +def test_registered_context_embeds_its_video(browser: Browser, caplog): + caplog.set_level("INFO") options = browser.adopted_context_options(recordVideo={}) video = str(Path(options["recordVideo"]["dir"], "page.webm")) video_path = browser.register_adopted_context("context=1", options, video) assert video_path == video - assert browser._playwright_state.context_cache.get("context=1") == { - "width": 1280, - "height": 720, - } + assert ( + '' + ) in caplog.text def test_video_path_without_record_video_is_ignored(browser: Browser): @@ -326,3 +327,40 @@ def test_new_persistent_context_is_not_cached_when_its_url_fails_to_open( assert browser._playwright_state.context_cache.get("context=1") is None assert " Date: Sat, 17 Oct 2026 19:58:55 +0300 Subject: [PATCH 3/4] test: move the trace helpers to atest/library get_trace_group_titles and its siblings are used by suites in two folders, so they belong with the other shared Python keywords. Their annotations now say that get_file_line can return None. Co-Authored-By: Claude Opus 5.5 --- .../tracing_groups.py | 10 ++++++---- atest/test/01_Browser_Management/tracing_groups.robot | 2 +- atest/test/05_JS_Tests/adopted_context_recording.robot | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) rename atest/{test/01_Browser_Management => library}/tracing_groups.py (54%) diff --git a/atest/test/01_Browser_Management/tracing_groups.py b/atest/library/tracing_groups.py similarity index 54% rename from atest/test/01_Browser_Management/tracing_groups.py rename to atest/library/tracing_groups.py index 593a3896b..a9968350b 100644 --- a/atest/test/01_Browser_Management/tracing_groups.py +++ b/atest/library/tracing_groups.py @@ -1,9 +1,9 @@ import json from pathlib import Path -from typing import Union def get_trace_lines(path: Path) -> list[dict]: + """Returns the ``before`` entries of a Playwright trace file.""" with path.open(encoding="utf-8") as file: return [ json.loads(line) for line in file.readlines() if '"type":"before"' in line @@ -11,6 +11,7 @@ def get_trace_lines(path: Path) -> list[dict]: def get_trace_group_titles(path: Path) -> list[str]: + """Returns the titles of the trace groups in a Playwright trace file, in order.""" return [ line["title"] for line in get_trace_lines(path) @@ -18,8 +19,9 @@ def get_trace_group_titles(path: Path) -> list[str]: ] -def get_file_line(file: Union[Path, None], line: int, column: int) -> str: +def get_file_line(file: Path | None, line: int, column: int) -> str | None: + """Returns line ``line`` of ``file``, or ``None`` without a file.""" if file is None: return None - with file.open(encoding="utf-8") as file: - return file.readlines()[line - 1] + with file.open(encoding="utf-8") as source: + return source.readlines()[line - 1] diff --git a/atest/test/01_Browser_Management/tracing_groups.robot b/atest/test/01_Browser_Management/tracing_groups.robot index 98914f3a0..22563127c 100644 --- a/atest/test/01_Browser_Management/tracing_groups.robot +++ b/atest/test/01_Browser_Management/tracing_groups.robot @@ -3,7 +3,7 @@ Resource ../variables.resource Library Browser auto_delete_passed_tracing=True tracing_group_mode=Full Library OperatingSystem Library ArchiveLibrary -Library tracing_groups.py +Library ../../library/tracing_groups.py Library Collections *** Variables *** diff --git a/atest/test/05_JS_Tests/adopted_context_recording.robot b/atest/test/05_JS_Tests/adopted_context_recording.robot index be2adb035..6cd574575 100644 --- a/atest/test/05_JS_Tests/adopted_context_recording.robot +++ b/atest/test/05_JS_Tests/adopted_context_recording.robot @@ -3,7 +3,7 @@ Library ../../library/AdoptingLibrary.py jsextension=${CURDIR}/adopt.js Library ArchiveLibrary Library Collections Library OperatingSystem -Library ../01_Browser_Management/tracing_groups.py +Library ../../library/tracing_groups.py Resource ../variables.resource Test Tags no-iframe From a0253b35ee877fb61b804dd0e93cc563e4c7bdab Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 20:01:55 +0300 Subject: [PATCH 4/4] test: pin the cached video size of every context route The video tests used the 1280x720 default, which is also what the size falls back to without a viewport or size, so dropping either at any call site went unnoticed. New Context, New Persistent Context and a registered adopted context now each show they cache the size of their viewport, or the size given, and the embedded video uses it. Co-Authored-By: Claude Opus 5.5 --- utest/test_adopted_context.py | 68 +++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index a1b706218..7dc1706e4 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -35,12 +35,23 @@ def browser(tmpdir, monkeypatch) -> Browser: return browser +VIEWPORT = {"width": 800, "height": 600} +VIDEO_SIZES = [ + ({}, {"width": 800, "height": 600}), + ({"size": {"width": 640, "height": 480}}, {"width": 640, "height": 480}), +] + + def keyword_call(name: str) -> KeywordCallStackEntry: return {"name": name, "file": "", "line": 0, "kwname": name, "args": []} -def sent_options(request) -> dict: - return json.loads(request.call_args.args[0].rawOptions) +def sent_request(stub_method): + return stub_method.call_args.args[0] + + +def sent_options(stub_method) -> dict: + return json.loads(sent_request(stub_method).rawOptions) def test_no_tracing_and_no_recording_by_default(browser: Browser): @@ -199,14 +210,14 @@ def test_registered_context_outside_a_keyword_is_traced_without_trace_groups( def test_registered_context_embeds_its_video(browser: Browser, caplog): caplog.set_level("INFO") - options = browser.adopted_context_options(recordVideo={}) + options = browser.adopted_context_options(recordVideo={}, viewport=VIEWPORT) video = str(Path(options["recordVideo"]["dir"], "page.webm")) video_path = browser.register_adopted_context("context=1", options, video) assert video_path == video assert ( - '