From 0f48a481f542a2f068fb0e53caeac0291565594a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 19:37:38 +0300 Subject: [PATCH 1/3] fix: register a persistent context only after its url opened New Persistent Context added the context to the context cache and embedded its video before navigating to url. When the navigation failed, the log showed a video of a context that was closed again, and the cache entry stayed. The trace groups are still added before the navigation, so it is traced; the cache entry and the video follow it. Co-Authored-By: Claude Opus 5.5 --- Browser/keywords/playwright_state.py | 20 ++++++++++++++------ utest/test_adopted_context.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index 9dd23ee17..32989205d 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -968,12 +968,7 @@ def new_persistent_context( traceFile=str(trace_file), ) ) - video_path = self._register_context( - response.id, - trace_file, - self._video_size(params.get("recordVideo"), params.get("viewport")), - json.loads(response.video), - ) + self.add_context_and_keyword_call_stack_to_trace(trace_file, response.id) context_options = self._mask_credentials( json.loads(response.contextOptions) ) @@ -1002,6 +997,11 @@ def new_persistent_context( self.close_browser() logger.debug("Teardown: Closed new persistent context.") raise e + video_path = self._remember_context( + response.id, + self._video_size(params.get("recordVideo"), params.get("viewport")), + json.loads(response.video), + ) return ( browser_id, response.id, @@ -1144,6 +1144,14 @@ def _register_context( video: dict | None = None, ) -> str: self.add_context_and_keyword_call_stack_to_trace(trace_file, context_id) + return self._remember_context(context_id, video_size, video) + + def _remember_context( + self, + context_id: str, + video_size: ViewportDimensions, + video: dict | None = None, + ) -> str: self.context_cache.add(context_id, video_size) return self._embed_video(video) if video else "" diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index daae34cad..f79406be0 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -298,3 +298,28 @@ def test_new_persistent_context_sends_action_annotations(tmpdir, monkeypatch): options = sent_options(browser._playwright.stub.NewPersistentContext) assert options["recordVideo"]["showActions"] == {"position": "top"} + + +def test_new_persistent_context_registers_nothing_when_its_url_fails_to_open( + tmpdir, monkeypatch, caplog +): + caplog.set_level("INFO") + browser = library(tmpdir, monkeypatch) + browser.keyword_call_stack.append(keyword_call("New Persistent Context")) + stub = browser._playwright.stub + stub.NewPersistentContext.return_value = MagicMock( + id="context=1", + browserId="browser=1", + contextOptions="{}", + video=json.dumps({"video_path": "page.webm", "contextUuid": "context=1"}), + ) + stub.GoTo.side_effect = RuntimeError("net::ERR_CONNECTION_REFUSED") + + with pytest.raises(RuntimeError, match="ERR_CONNECTION_REFUSED"): + browser.new_persistent_context( + url="http://localhost:1", tracing=True, recordVideo={} + ) + + assert stub.OpenTraceGroup.call_args.args[0].contextId == "context=1" + assert browser._playwright_state.context_cache.get("context=1") is None + assert " Date: Sat, 17 Oct 2026 19:38:01 +0300 Subject: [PATCH 2/3] fix: trace a context registered outside a keyword without failing With tracing_group_mode Browser, registering a traced context read the last entry of the keyword call stack, so New Context, New Persistent Context or register_adopted_context called from plain Python, where the stack is empty, raised IndexError. The context is now traced without trace groups, as in Full mode. Co-Authored-By: Claude Opus 5.5 --- Browser/keywords/playwright_state.py | 5 ++++- utest/test_adopted_context.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index 32989205d..b5e0264d4 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -1117,7 +1117,10 @@ def add_context_and_keyword_call_stack_to_trace(self, trace_file, ctx_id): ): return None self.library.tracing_contexts.append(ctx_id) - if self.library.tracing_group_mode == TracingGroupMode.Browser: + if ( + self.library.tracing_group_mode == TracingGroupMode.Browser + and self.library.keyword_call_stack + ): return self.open_trace_group( **self.library._trace_group_arguments( self.library.keyword_call_stack[-1] diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index f79406be0..b0f8950e1 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -210,6 +210,19 @@ def test_playwright_tracing_group_mode_adds_no_trace_groups(tmpdir, monkeypatch) assert browser.tracing_contexts == [] +def test_registered_context_outside_a_keyword_is_traced_without_trace_groups( + tmpdir, monkeypatch +): + browser = library(tmpdir, monkeypatch) + browser.tracing_group_mode = TracingGroupMode.Browser + 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 == ["context=1"] + + def test_registered_context_embeds_its_video(tmpdir, monkeypatch): browser = library(tmpdir, monkeypatch) options = browser.adopted_context_options(recordVideo={}) From 2e4b15467f974768bf987499564a2c66f02ef378 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 19:41:57 +0300 Subject: [PATCH 3/3] test: split the persistent context registration tests One test per behaviour: the size is cached and the video embedded once the url opened, the context is traced before the url opens, and nothing is cached when it fails to open. _remember_context is renamed _cache_context, and an empty keyword call stack returns explicitly. Co-Authored-By: Claude Opus 5.5 --- Browser/keywords/playwright_state.py | 13 ++++---- utest/test_adopted_context.py | 49 ++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/Browser/keywords/playwright_state.py b/Browser/keywords/playwright_state.py index b5e0264d4..beef0cbce 100755 --- a/Browser/keywords/playwright_state.py +++ b/Browser/keywords/playwright_state.py @@ -997,7 +997,7 @@ def new_persistent_context( self.close_browser() logger.debug("Teardown: Closed new persistent context.") raise e - video_path = self._remember_context( + video_path = self._cache_context( response.id, self._video_size(params.get("recordVideo"), params.get("viewport")), json.loads(response.video), @@ -1117,10 +1117,9 @@ def add_context_and_keyword_call_stack_to_trace(self, trace_file, ctx_id): ): return None self.library.tracing_contexts.append(ctx_id) - if ( - self.library.tracing_group_mode == TracingGroupMode.Browser - and self.library.keyword_call_stack - ): + if self.library.tracing_group_mode == TracingGroupMode.Browser: + if not self.library.keyword_call_stack: + return None return self.open_trace_group( **self.library._trace_group_arguments( self.library.keyword_call_stack[-1] @@ -1147,9 +1146,9 @@ def _register_context( video: dict | None = None, ) -> str: self.add_context_and_keyword_call_stack_to_trace(trace_file, context_id) - return self._remember_context(context_id, video_size, video) + return self._cache_context(context_id, video_size, video) - def _remember_context( + def _cache_context( self, context_id: str, video_size: ViewportDimensions, diff --git a/utest/test_adopted_context.py b/utest/test_adopted_context.py index b0f8950e1..b240283e7 100644 --- a/utest/test_adopted_context.py +++ b/utest/test_adopted_context.py @@ -313,12 +313,7 @@ def test_new_persistent_context_sends_action_annotations(tmpdir, monkeypatch): assert options["recordVideo"]["showActions"] == {"position": "top"} -def test_new_persistent_context_registers_nothing_when_its_url_fails_to_open( - tmpdir, monkeypatch, caplog -): - caplog.set_level("INFO") - browser = library(tmpdir, monkeypatch) - browser.keyword_call_stack.append(keyword_call("New Persistent Context")) +def persistent_context_with_video(browser: Browser) -> MagicMock: stub = browser._playwright.stub stub.NewPersistentContext.return_value = MagicMock( id="context=1", @@ -326,13 +321,47 @@ def test_new_persistent_context_registers_nothing_when_its_url_fails_to_open( contextOptions="{}", video=json.dumps({"video_path": "page.webm", "contextUuid": "context=1"}), ) + return stub + + +def test_new_persistent_context_caches_and_embeds_its_video_once_its_url_opened( + tmpdir, monkeypatch, caplog +): + caplog.set_level("INFO") + browser = library(tmpdir, monkeypatch) + persistent_context_with_video(browser) + + browser.new_persistent_context(url="http://localhost:1", recordVideo={}) + + assert browser._playwright_state.context_cache.get("context=1") == { + "width": 1280, + "height": 720, + } + assert "