diff --git a/Browser/browser.py b/Browser/browser.py index 24ff6ef57..713a18bd6 100755 --- a/Browser/browser.py +++ b/Browser/browser.py @@ -78,15 +78,19 @@ # Importing this directly from .utils break the stub type checks from .utils.data_types import ( + AdoptedContextOptions, DelayedKeyword, HighLightElement, KeywordCallStackEntry, LambdaFunction, + RecordHar, + RecordVideo, RegExp, RobotTypeConverter, SelectionType, SupportedBrowsers, TracingGroupMode, + ViewportDimensions, ) from .version import __version__ as VERSION @@ -896,6 +900,54 @@ def call_js_keyword(self, keyword_name: str, **args) -> Any: return None return json.loads(last_json) + def adopted_context_options( + self, + tracing: bool | Path | None = None, + recordVideo: RecordVideo | None = None, + recordHar: RecordHar | None = None, + viewport: ViewportDimensions | None = None, + ) -> AdoptedContextOptions: + """Returns the tracing and recording options for a context that a JavaScript + extension creates and hands over with ``adoptContext``. + + 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. + + 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( + tracing, recordVideo, recordHar, viewport + ) + + def register_adopted_context( + self, + context_id: str, + options: AdoptedContextOptions, + video_path: str | None = None, + ) -> str: + """Registers a context that a JavaScript extension handed over with + ``adoptContext`` the way `New Context` registers its own. + + ``options`` are those of ``adopted_context_options``. With ``tracing``, the + keyword calls become groups in the trace. With ``recordVideo``, the log + embeds the video at ``video_path``. Returns ``video_path``, or an empty + string if there is no video. + """ + return self._playwright_state.register_adopted_context( + context_id, options, video_path + ) + @property def outputdir(self) -> str: if EXECUTION_CONTEXTS.current: diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index 960897f7a..1b44406b6 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -14,6 +14,7 @@ import json import os +from collections.abc import Mapping from contextlib import suppress from copy import copy from datetime import datetime, timedelta, timezone @@ -33,6 +34,7 @@ from ..base import LibraryComponent from ..generated.playwright_pb2 import Request from ..utils import ( + AdoptedContextOptions, ClientCertificate, ColorScheme, DownloadInfo, @@ -751,7 +753,8 @@ def new_context( - ``recordHar``: Enables [http://www.softwareishard.com/blog/har-12-spec/ | HAR] recording for all pages into a file. The ``path`` key must be a path to a file, for example ``recordHar={'path': - '${OUTPUT_DIR}/har.file'}``. If not specified, the HAR is not recorded. + '${OUTPUT_DIR}/har.file'}``. A relative path is relative to the output + directory. If not specified, the HAR is not recorded. 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 @@ -845,7 +848,11 @@ def new_context( "No browser was open. New browser was automatically opened " "when this context is created." ) - self.context_cache.add(response.id, self._get_video_size(params)) + self._register_context( + response.id, + trace_file, + self._video_size(params.get("recordVideo"), params.get("viewport")), + ) return response.id @keyword() @@ -960,8 +967,11 @@ def new_persistent_context( traceFile=str(trace_file), ) ) - self.add_context_and_keyword_call_stack_to_trace( - trace_file=trace_file, ctx_id=response.id + video_path = self._register_context( + response.id, + trace_file, + self._video_size(params.get("recordVideo"), params.get("viewport")), + json.loads(response.video), ) context_options = self._mask_credentials( json.loads(response.contextOptions) @@ -991,14 +1001,45 @@ def new_persistent_context( self.close_browser() logger.debug("Teardown: Closed new persistent context.") raise e - self.context_cache.add(response.id, self._get_video_size(params)) - video_path = self._embed_video(json.loads(response.video)) return ( browser_id, response.id, NewPageDetails(page_id=response.pageId, video_path=video_path), ) + def adopted_context_options( + self, + tracing: bool | Path | None = None, + recordVideo: RecordVideo | None = None, + recordHar: RecordHar | None = None, + viewport: ViewportDimensions | None = None, + ) -> AdoptedContextOptions: + return { + "tracing": self._resolve_trace_file(tracing) or None, + "recordVideo": self._record_video(recordVideo, viewport), + "recordHar": self._record_har(recordHar), + "tracesDir": str(self.traces_temp / str(uuid4())), + } + + def register_adopted_context( + self, + context_id: str, + options: AdoptedContextOptions, + video_path: str | None = None, + ) -> str: + record_video = options["recordVideo"] + video = ( + {"video_path": video_path, "contextUuid": context_id} + if record_video is not None + else None + ) + return self._register_context( + context_id, + options["tracing"] or "", + self._video_size(record_video, None), + video, + ) + def _resolve_trace_file(self, tracing: bool | Path | None) -> str: if isinstance(tracing, str): tracing = Path(tracing) @@ -1022,8 +1063,12 @@ def _resolve_trace_file(self, tracing: bool | Path | None) -> str: def _set_context_options(self, params, httpCredentials, storageState): params = convert_typed_dict(self.new_context.__annotations__, params) - params = self._set_video_path(params) - params = self._set_video_size_to_int(params) + if "recordVideo" in params: + params["recordVideo"] = self._record_video( + params["recordVideo"], params.get("viewport") + ) + if "recordHar" in params: + params["recordHar"] = self._record_har(params["recordHar"]) reduced_motion = str(params.get("reducedMotion")) reduced_motion = reduced_motion.replace("_", "-") params["reducedMotion"] = reduced_motion @@ -1056,17 +1101,13 @@ def _set_browser_options(self, params, browser, channel, slowMo, timeout): def _new_context(self, options: str, trace_file: Path | str): with self.playwright.grpc_channel() as stub: - context = stub.NewContext( + return stub.NewContext( Request().Context( rawOptions=options, defaultTimeout=int(self.timeout), traceFile=str(trace_file), ) ) - self.add_context_and_keyword_call_stack_to_trace( - trace_file=trace_file, ctx_id=context.id - ) - return context def add_context_and_keyword_call_stack_to_trace(self, trace_file, ctx_id): if ( @@ -1094,38 +1135,58 @@ def _mask_credentials(self, data: dict): data["httpCredentials"] = "XXX" return data - def _set_video_path(self, params: dict) -> dict: - record_video = params.get("recordVideo", {}) - video_path = record_video.get("dir") - if not record_video: - return params - if video_path is None: - vid_path = self.video_output - elif Path(video_path).is_absolute(): - vid_path = params["recordVideo"]["dir"] - else: - vid_path = self.video_output / video_path - params["recordVideo"]["dir"] = Path(vid_path).resolve().absolute() - return params - - def _get_record_video_size(self, params) -> tuple[int | None, int | None]: - width = params.get("recordVideo", {}).get("size", {}).get("width") - height = params.get("recordVideo", {}).get("size", {}).get("height") - return int(width) if width else None, int(height) if height else None - - def _set_video_size_to_int(self, params: dict) -> dict: - if "recordVideo" not in params: - return params - params["recordVideo"]["size"] = self._get_video_size(params) - return params + def _register_context( + self, + context_id: str, + trace_file: str, + video_size: dict, + video: dict | None = None, + ) -> str: + self.add_context_and_keyword_call_stack_to_trace(trace_file, context_id) + self.context_cache.add(context_id, video_size) + return self._embed_video(video) if video else "" - def _get_video_size(self, params: dict) -> dict: - width, height = self._get_record_video_size(params) - if width and height: - return {"width": width, "height": height} - if "viewport" in params: - return params["viewport"] - return {"width": 1280, "height": 720} + def _record_video( + self, + record_video: Mapping[str, Any] | None, + viewport: Mapping[str, Any] | None, + ) -> dict[str, Any] | None: + if record_video is None: + return None + video_dir = record_video.get("dir") + if video_dir is None: + resolved_dir = self.video_output + elif Path(video_dir).is_absolute(): + resolved_dir = Path(video_dir) + else: + resolved_dir = self.video_output / video_dir + return { + **record_video, + "dir": str(resolved_dir.resolve()), + "size": self._video_size(record_video, viewport), + } + + def _video_size( + self, + record_video: Mapping[str, Any] | None, + viewport: Mapping[str, Any] | None, + ) -> dict: + 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} + + def _record_har( + self, record_har: Mapping[str, Any] | None + ) -> dict[str, Any] | None: + if record_har is None: + return None + if not record_har.get("path"): + return {**record_har} + return { + **record_har, + "path": str(Path(self.outputdir, record_har["path"]).resolve()), + } @keyword(tags=("Setter", "BrowserControl")) def new_page( diff --git a/Browser/utils/__init__.py b/Browser/utils/__init__.py index 175609087..060bc755d 100644 --- a/Browser/utils/__init__.py +++ b/Browser/utils/__init__.py @@ -15,6 +15,7 @@ # flake8: noqa from .data_types import ( + AdoptedContextOptions, AutoClosingLevel, BrowserInfo, BoundingBox, diff --git a/Browser/utils/data_types.py b/Browser/utils/data_types.py index a1f58f0e3..63aa6cb6f 100644 --- a/Browser/utils/data_types.py +++ b/Browser/utils/data_types.py @@ -707,7 +707,8 @@ class RecordHar(TypedDict, total=False): `omitContent`: Optional setting to control whether to omit request content from the HAR. Default is False - `path`: Path on the filesystem to write the HAR file to. + `path`: Path on the filesystem to write the HAR file to. A relative path is + relative to the output directory. Example: | ${har} = Create Dictionary path=/path/to/har.file omitContent=True @@ -718,6 +719,21 @@ class RecordHar(TypedDict, total=False): path: str +class AdoptedContextOptions(TypedDict): + """Tracing and recording options of a context that a JavaScript extension + creates and hands over with ``adoptContext``, as ``adopted_context_options`` + returns them. ``None`` means off. + + The keys are named like the options they are meant for, so that they can be + passed on to the extension function as keyword arguments. + """ + + tracing: str | None + recordVideo: dict[str, Any] | None + recordHar: dict[str, Any] | None + tracesDir: str + + class _HttpCredentials(TypedDict): username: str | Secret password: str | Secret diff --git a/atest/library/AdoptingLibrary.py b/atest/library/AdoptingLibrary.py new file mode 100644 index 000000000..ddbd69a17 --- /dev/null +++ b/atest/library/AdoptingLibrary.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from robot.api.deco import keyword + +from Browser import Browser +from Browser.utils.data_types import RecordVideo + + +class AdoptingLibrary(Browser): + @keyword + def open_adopted_context_with_recording( + self, + user_data_dir: str, + url: str, + headless: bool, + tracing: Path, + record_video: RecordVideo, + ) -> str: + """Opens a persistent context in the JavaScript extension with the options of + ``adopted_context_options``, registers it with ``register_adopted_context`` + and returns the path of its video.""" + options = self.adopted_context_options( + tracing=tracing, recordVideo=record_video + ) + adopted = self.call_js_keyword( + "openAdoptedPersistentContextWithRecording", + userDataDir=user_data_dir, + url=url, + headless=headless, + **options, + ) + return self.register_adopted_context( + adopted["contextId"], options, adopted["videoPath"] + ) diff --git a/atest/library/os_wrapper.py b/atest/library/os_wrapper.py index 2a8afc802..095dc5123 100644 --- a/atest/library/os_wrapper.py +++ b/atest/library/os_wrapper.py @@ -167,10 +167,6 @@ def get_python_binary_path() -> str: return sys.executable -def is_python_314() -> bool: - return sys.version_info >= (3, 14) and sys.version_info < (3, 15) - - def get_python_version() -> str: return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" diff --git a/atest/test/01_Browser_Management/record_har.robot b/atest/test/01_Browser_Management/record_har.robot index 64cf06c5e..afca44f8b 100644 --- a/atest/test/01_Browser_Management/record_har.robot +++ b/atest/test/01_Browser_Management/record_har.robot @@ -31,3 +31,9 @@ No Har Created Close Context ${files} = List Files In Directory ${OUTPUT_DIR} har* Length Should Be ${files} 3 + +Relative Har Path Is Relative To The Output Directory + New Context recordHar={"path": "relative-path.har"} + New Page ${LOGIN_URL} + Close Context + File Should Not Be Empty ${OUTPUT_DIR}/relative-path.har diff --git a/atest/test/01_Browser_Management/tracing_groups.py b/atest/test/01_Browser_Management/tracing_groups.py index a43284a3b..593a3896b 100644 --- a/atest/test/01_Browser_Management/tracing_groups.py +++ b/atest/test/01_Browser_Management/tracing_groups.py @@ -10,6 +10,14 @@ def get_trace_lines(path: Path) -> list[dict]: ] +def get_trace_group_titles(path: Path) -> list[str]: + return [ + line["title"] + for line in get_trace_lines(path) + if line.get("method") == "tracingGroup" + ] + + def get_file_line(file: Union[Path, None], line: int, column: int) -> str: if file is None: return None diff --git a/atest/test/01_Browser_Management/video.robot b/atest/test/01_Browser_Management/video.robot index 3fa3b23e9..f8f656517 100644 --- a/atest/test/01_Browser_Management/video.robot +++ b/atest/test/01_Browser_Management/video.robot @@ -15,12 +15,7 @@ Create Video With Full Path Go To ${FRAMES_URL} Get Browser Catalog Verify Video Files 1 - IF ${PYTHON_314} - VAR ${expected_path} = ${OUTPUT_DIR}/video - ELSE - ${expected_path} = Convert To Path ${OUTPUT_DIR}/video - END - Should Be Equal ${record_video.dir} ${expected_path} + Should Be Equal ${record_video.dir} ${OUTPUT_DIR}/video Create Video With Relative Path [Documentation] @@ -36,12 +31,7 @@ Create Video With Relative Path Wait File Count In Directory ${OUTPUT_DIR}/browser/video/my_video ${1} Should Start With ${details}[video_path] ${OUTPUT_DIR}${/}browser${/}video${/}my_video Should End With ${details}[video_path] .webm - IF ${PYTHON_314} - VAR ${expected_path} = my_video - ELSE - ${expected_path} = Convert To Path ${OUTPUT_DIR}/browser/video/my_video - END - Should Be Equal ${record_video.dir} ${expected_path} + Should Be Equal ${record_video.dir} my_video New Context viewport={'width': 2048, 'height': 1200} New Page file://${details}[video_path] Get BoundingBox video ALL validate value['width'] == 1280 and value['height'] == 720 diff --git a/atest/test/05_JS_Tests/adopt.js b/atest/test/05_JS_Tests/adopt.js index 9fda4b99a..e9dd5d30d 100644 --- a/atest/test/05_JS_Tests/adopt.js +++ b/atest/test/05_JS_Tests/adopt.js @@ -42,9 +42,23 @@ async function adoptedContextOnCloseWasCalled() { return onCloseCalled; } +async function openAdoptedPersistentContextWithRecording(userDataDir, url, headless, tracing, recordVideo, tracesDir, playwright, adoptContext) { + const contextOptions = { recordVideo }; + const context = await playwright.chromium.launchPersistentContext(userDataDir, { + headless: String(headless).toLowerCase() !== 'false', + tracesDir, + ...contextOptions, + }); + const page = context.pages()[0]; + await page.goto(url); + const adopted = await adoptContext(context, { tracing, contextOptions }); + return { ...adopted, videoPath: await page.video().path() }; +} + exports.__esModule = true; exports.openAdoptedPersistentContext = openAdoptedPersistentContext; exports.openAdoptedPersistentContextWithTracingAndDownloads = openAdoptedPersistentContextWithTracingAndDownloads; exports.messageOfAFailedWait = messageOfAFailedWait; exports.adoptedContextIsClosed = adoptedContextIsClosed; exports.adoptedContextOnCloseWasCalled = adoptedContextOnCloseWasCalled; +exports.openAdoptedPersistentContextWithRecording = openAdoptedPersistentContextWithRecording; diff --git a/atest/test/05_JS_Tests/adopted_context_recording.robot b/atest/test/05_JS_Tests/adopted_context_recording.robot new file mode 100644 index 000000000..be2adb035 --- /dev/null +++ b/atest/test/05_JS_Tests/adopted_context_recording.robot @@ -0,0 +1,23 @@ +*** Settings *** +Library ../../library/AdoptingLibrary.py jsextension=${CURDIR}/adopt.js +Library ArchiveLibrary +Library Collections +Library OperatingSystem +Library ../01_Browser_Management/tracing_groups.py +Resource ../variables.resource + +Test Tags no-iframe + +*** Test Cases *** +A Library Built On Browser Traces And Records An Adopted Context + ${video} = Open Adopted Context With Recording + ... ${OUTPUT_DIR}/adopted-profile-recorded ${LOGIN_URL} ${HEADLESS} + ... ${OUTPUT_DIR}/adopted-recording.zip {} + Get Text h1 == Login Page + Close Browser + Should Contain ${video} browser${/}video + File Should Not Be Empty ${video} + Extract Zip File ${OUTPUT_DIR}/adopted-recording.zip ${OUTPUT_DIR}/adopted-recording + ${groups} = Get Trace Group Titles ${OUTPUT_DIR}/adopted-recording/trace.trace + Should Contain Match ${groups} AdoptingLibrary.Open Adopted Context With Recording* + Should Contain Match ${groups} AdoptingLibrary.Get Text* diff --git a/atest/test/__init__.robot b/atest/test/__init__.robot index 83db9d077..21ac636ee 100644 --- a/atest/test/__init__.robot +++ b/atest/test/__init__.robot @@ -14,8 +14,6 @@ Start Test Application ${port} = Start Test Server VAR ${SERVER_PORT} = ${port} scope=GLOBAL VAR ${SERVER} = localhost:${SERVER_PORT} scope=GLOBAL - ${python_version} = Is Python 314 - VAR ${PYTHON_314} = ${python_version} scope=GLOBAL ${rf_version} = Get Robot Version Set Suite Metadata Robot Framework Version ${rf_version} Log To Console RF Version: ${rf_version} diff --git a/atest/test/variables.resource b/atest/test/variables.resource index fad5f814d..494c4d9e0 100644 --- a/atest/test/variables.resource +++ b/atest/test/variables.resource @@ -50,4 +50,3 @@ ${SYS_VAR_CLEAN} = ${True} ${NETWORK_IDLE_FILE_DATA} = ${EMPTY} ${INPUT_ELEMENT_COUNT_IN_LOGIN} = 5 ${BUTTON_ELEMENT_COUNT_IN_LOGIN} = ${18} -${PYTHON_314} = ${None} diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py new file mode 100644 index 000000000..22849382d --- /dev/null +++ b/utest/test_adopted_context.py @@ -0,0 +1,255 @@ +import json +from contextlib import contextmanager +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock + +from Browser import Browser +from Browser.base.cache import ContextCache +from Browser.utils.data_types import KeywordCallStackEntry, TracingGroupMode + + +class FakePlaywright: + def __init__(self): + self.stub = MagicMock() + self.stub.NewContext.return_value = MagicMock(contextOptions="{}") + self.stub.NewPersistentContext.return_value = MagicMock( + contextOptions="{}", video="{}" + ) + self.stub.GetBrowserCatalog.return_value = MagicMock(json="[]") + + @contextmanager + def grpc_channel(self): + yield self.stub + + +def library(tmpdir, monkeypatch) -> Browser: + monkeypatch.setattr(Browser, "_output_dir", tmpdir) + monkeypatch.setattr(Browser, "_context_cache", ContextCache()) + monkeypatch.delenv("ROBOT_FRAMEWORK_BROWSER_TRACING", raising=False) + browser = Browser() + browser._playwright = FakePlaywright() + return browser + + +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 test_no_tracing_and_no_recording_by_default(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + options = browser.adopted_context_options() + + assert options["tracing"] is None + assert options["recordVideo"] is None + assert options["recordHar"] is None + + +def test_traces_dir_is_a_new_folder_in_browsers_temporary_trace_folder( + tmpdir, monkeypatch +): + browser = library(tmpdir, monkeypatch) + + first = browser.adopted_context_options()["tracesDir"] + second = browser.adopted_context_options()["tracesDir"] + + assert Path(first).parent == browser.traces_temp + assert first != second + + +def test_tracing_follows_new_context(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + assert browser.adopted_context_options(tracing=True)["tracing"] == str( + browser.traces_output + ) + assert browser.adopted_context_options(tracing=Path("trace.zip"))["tracing"] == str( + Path(tmpdir, "trace.zip").resolve() + ) + + +def test_relative_trace_folder_is_in_browsers_trace_folder(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + 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) + 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) + variables = {"${ROBOT_FRAMEWORK_BROWSER_TRACING}": "True"} + monkeypatch.setattr( + "Browser.keywords.playwright_state.EXECUTION_CONTEXTS", + SimpleNamespace(current=True), + ) + monkeypatch.setattr( + "Browser.keywords.playwright_state.BuiltIn", + lambda: SimpleNamespace(get_variable_value=variables.get), + ) + + 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) + + options = browser.adopted_context_options(recordVideo={}) + + assert options["recordVideo"] == { + "dir": str(browser.video_output.resolve()), + "size": {"width": 1280, "height": 720}, + } + + +def test_video_keeps_its_other_keys(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + options = browser.adopted_context_options( + recordVideo={"showActions": {"duration": 500}} + ) + + assert options["recordVideo"]["showActions"] == {"duration": 500} + + +def test_video_folder_and_size_follow_new_context(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + options = browser.adopted_context_options( + recordVideo={"dir": "demo", "size": {"width": "800", "height": "600"}} + ) + + assert options["recordVideo"] == { + "dir": str((browser.video_output / "demo").resolve()), + "size": {"width": 800, "height": 600}, + } + + +def test_absolute_video_folder_is_kept(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + videos = str(Path(tmpdir, "elsewhere", "videos").resolve()) + + options = browser.adopted_context_options(recordVideo={"dir": videos}) + + assert options["recordVideo"]["dir"] == videos + + +def test_video_without_size_has_the_size_of_the_viewport(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + options = browser.adopted_context_options( + recordVideo={}, viewport={"width": 1024, "height": 768} + ) + + 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) + browser.keyword_call_stack.append(keyword_call("Open Application")) + options = browser.adopted_context_options(tracing=True) + + browser.register_adopted_context("context=1", options) + + group = browser._playwright.stub.OpenTraceGroup.call_args.args[0] + assert (group.name, group.contextId) == ("Open Application", "context=1") + assert browser.tracing_contexts == ["context=1"] + + +def test_registered_context_without_tracing_gets_no_trace_groups(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + browser.keyword_call_stack.append(keyword_call("Open Application")) + + browser.register_adopted_context("context=1", browser.adopted_context_options()) + + assert not browser._playwright.stub.OpenTraceGroup.called + assert browser.tracing_contexts == [] + + +def test_playwright_tracing_group_mode_adds_no_trace_groups(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + browser.tracing_group_mode = TracingGroupMode.Playwright + browser.keyword_call_stack.append(keyword_call("Open Application")) + options = browser.adopted_context_options(tracing=True) + + browser.register_adopted_context("context=1", options) + + assert not browser._playwright.stub.OpenTraceGroup.called + assert browser.tracing_contexts == [] + + +def test_registered_context_embeds_its_video(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + 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, + } + + +def test_video_path_without_record_video_is_ignored(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + options = browser.adopted_context_options() + + video_path = browser.register_adopted_context("context=1", options, "page.webm") + + assert video_path == "" + + +def test_relative_har_path_is_relative_to_the_output_directory(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + options = browser.adopted_context_options( + recordHar={"path": "har/app.har", "omitContent": True} + ) + + assert options["recordHar"] == { + "path": str(Path(tmpdir, "har", "app.har").resolve()), + "omitContent": True, + } + + +def test_absolute_har_path_is_kept(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + har = str(Path(tmpdir, "elsewhere", "app.har").resolve()) + + options = browser.adopted_context_options(recordHar={"path": har}) + + assert options["recordHar"] == {"path": har} + + +def test_new_context_resolves_a_relative_har_path(tmpdir, monkeypatch): + browser = library(tmpdir, monkeypatch) + + 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) + + browser.new_persistent_context(recordHar={"path": "persistent.har"}) + + options = sent_options(browser._playwright.stub.NewPersistentContext) + assert options["recordHar"] == { + "path": str(Path(tmpdir, "persistent.har").resolve()) + } diff --git a/utest/test_get_video_size.py b/utest/test_get_video_size.py index 712613d63..6432bd956 100644 --- a/utest/test_get_video_size.py +++ b/utest/test_get_video_size.py @@ -11,15 +11,12 @@ def state(): def test_get_video_size(state: PlaywrightState): results = [ - state._get_video_size({}), - state._get_video_size({"recordVideo": {"path": "/path/to"}}), - state._get_video_size({"recordVideo": {"size": {"width": 12, "height": 11}}}), - state._get_video_size({"viewport": {"width": 123, "height": 111}}), - state._get_video_size( - { - "recordVideo": {"size": {"width": 12, "height": 11}}, - "viewport": {"width": 123, "height": 111}, - } + state._video_size(None, None), + state._video_size({"path": "/path/to"}, None), + state._video_size({"size": {"width": 12, "height": 11}}, None), + state._video_size(None, {"width": 123, "height": 111}), + state._video_size( + {"size": {"width": 12, "height": 11}}, {"width": 123, "height": 111} ), ] verify_all("Video size", results)