From d46f0c2c72d920ace2b1d70021dd672b237d6aa4 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 17:00:09 +0300 Subject: [PATCH 1/2] test: characterize the generated JavaScript extension keyword Pin how the generated keyword reads a streamed result, sends its varargs, takes its defaults from the JavaScript signature and sends a Test scoped timeout, before it is changed to call call_js_keyword. Co-Authored-By: Claude Opus 5.5 --- utest/test_js_extension_call.py | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/utest/test_js_extension_call.py b/utest/test_js_extension_call.py index 214854e05..474143834 100644 --- a/utest/test_js_extension_call.py +++ b/utest/test_js_extension_call.py @@ -1,5 +1,6 @@ import inspect import json +import math from contextlib import contextmanager from datetime import timedelta from types import SimpleNamespace @@ -12,6 +13,7 @@ class RecordingPlaywright: def __init__(self): self.requests = [] + self.responses = [] @contextmanager def grpc_channel(self): @@ -19,7 +21,7 @@ def grpc_channel(self): def CallExtensionKeyword(self, request): # noqa: N802 - the name of the gRPC method self.requests.append(request) - return [] + return self.responses def SetTimeout(self, request): # noqa: N802 - the name of the gRPC method return SimpleNamespace(log="") @@ -37,6 +39,10 @@ def library_with_recorded_calls( return library, library._playwright +def response(log: str, body_part: str = ""): + return SimpleNamespace(log=log, bodyPart=body_part, json="") + + def generated_keyword(library: Browser, name: str, arguments: str): component = LibraryComponent(library) library._jskeyword_call(component, name, arguments, "") @@ -85,3 +91,79 @@ def test_call_js_keyword_sends_the_arguments_as_given(tmpdir, monkeypatch): assert playwright.payload() == { "arguments": [["url", "https://a.b"], ["defaultTimeout", 5000]] } + + +def test_generated_keyword_joins_the_body_parts_of_a_chunked_result( + tmpdir, monkeypatch +): + library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) + playwright.responses = [ + response("ok chunk 0", '{"name": "J'), + response("ok", 'örn", "size": 2}'), + ] + + result = generated_keyword(library, "describe", "")() + + assert result == {"name": "Jörn", "size": 2} + + +def test_generated_keyword_returns_none_when_the_function_returns_nothing( + tmpdir, monkeypatch +): + library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) + playwright.responses = [response("ok")] + + assert generated_keyword(library, "closeAll", "")() is None + + +def test_generated_keyword_sends_its_varargs_as_one_args_list(tmpdir, monkeypatch): + library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) + keyword = generated_keyword(library, "typeAll", "selector, args, logger") + + keyword("#field", "first", "second") + + assert playwright.payload() == { + "arguments": [ + ["selector", "#field"], + ["args", ["first", "second"]], + ["logger", "RESERVED"], + ] + } + + +def test_generated_keyword_takes_its_defaults_from_the_javascript_signature( + tmpdir, monkeypatch +): + library, _ = library_with_recorded_calls(tmpdir, monkeypatch) + keyword = generated_keyword( + library, + "measure", + 'label = "width", count = 3, ratio = NaN, upper = Infinity, ' + "lower = -Infinity, strict = false, limit = null, unit = undefined", + ) + + defaults = { + name: parameter.default + for name, parameter in inspect.signature(keyword).parameters.items() + } + + assert math.isnan(defaults.pop("ratio")) + assert defaults == { + "label": "width", + "count": 3, + "upper": math.inf, + "lower": -math.inf, + "strict": False, + "limit": None, + "unit": None, + } + + +def test_generated_keyword_sends_a_test_scoped_timeout(tmpdir, monkeypatch): + library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) + keyword = generated_keyword(library, "openOwnContext", "defaultTimeout") + + library.set_browser_timeout(timedelta(milliseconds=1500), Scope.Test) + keyword() + + assert playwright.payload() == {"arguments": [["defaultTimeout", 1500]]} From abeffb226ac9f5577e738af7f062ad46dd7ffa07 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 17 Oct 2026 17:01:41 +0300 Subject: [PATCH 2/2] test: give the JavaScript extension call tests fixtures Co-Authored-By: Claude Opus 5.5 --- utest/test_js_extension_call.py | 63 ++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/utest/test_js_extension_call.py b/utest/test_js_extension_call.py index 474143834..4fe16bf57 100644 --- a/utest/test_js_extension_call.py +++ b/utest/test_js_extension_call.py @@ -5,6 +5,8 @@ from datetime import timedelta from types import SimpleNamespace +import pytest + from Browser import Browser from Browser.base import LibraryComponent from Browser.utils.data_types import Scope @@ -30,16 +32,20 @@ def payload(self) -> dict: return json.loads(self.requests[-1].arguments) -def library_with_recorded_calls( - tmpdir, monkeypatch -) -> tuple[Browser, RecordingPlaywright]: +@pytest.fixture +def playwright() -> RecordingPlaywright: + return RecordingPlaywright() + + +@pytest.fixture +def library(tmpdir, monkeypatch, playwright: RecordingPlaywright) -> Browser: monkeypatch.setattr(Browser, "_output_dir", tmpdir) library = Browser(timeout=timedelta(seconds=7)) - library._playwright = RecordingPlaywright() - return library, library._playwright + library._playwright = playwright + return library -def response(log: str, body_part: str = ""): +def extension_response(log: str, body_part: str = "") -> SimpleNamespace: return SimpleNamespace(log=log, bodyPart=body_part, json="") @@ -50,9 +56,8 @@ def generated_keyword(library: Browser, name: str, arguments: str): def test_generated_keyword_passes_the_library_timeout_as_default_timeout( - tmpdir, monkeypatch + library: Browser, playwright: RecordingPlaywright ): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) keyword = generated_keyword(library, "openOwnContext", "url, defaultTimeout") keyword("https://a.b") @@ -63,8 +68,9 @@ def test_generated_keyword_passes_the_library_timeout_as_default_timeout( } -def test_generated_keyword_reads_the_library_timeout_at_each_call(tmpdir, monkeypatch): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) +def test_generated_keyword_reads_the_library_timeout_at_each_call( + library: Browser, playwright: RecordingPlaywright +): keyword = generated_keyword(library, "openOwnContext", "defaultTimeout") library.set_browser_timeout(timedelta(seconds=3), Scope.Global) @@ -73,9 +79,9 @@ def test_generated_keyword_reads_the_library_timeout_at_each_call(tmpdir, monkey assert playwright.payload() == {"arguments": [["defaultTimeout", 3000]]} -def test_other_generated_keywords_send_only_their_arguments(tmpdir, monkeypatch): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) - +def test_other_generated_keywords_send_only_their_arguments( + library: Browser, playwright: RecordingPlaywright +): generated_keyword(library, "clickTwice", "selector, page")("#button") assert playwright.payload() == { @@ -83,9 +89,9 @@ def test_other_generated_keywords_send_only_their_arguments(tmpdir, monkeypatch) } -def test_call_js_keyword_sends_the_arguments_as_given(tmpdir, monkeypatch): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) - +def test_call_js_keyword_sends_the_arguments_as_given( + library: Browser, playwright: RecordingPlaywright +): library.call_js_keyword("openOwnContext", url="https://a.b", defaultTimeout=5000) assert playwright.payload() == { @@ -94,12 +100,11 @@ def test_call_js_keyword_sends_the_arguments_as_given(tmpdir, monkeypatch): def test_generated_keyword_joins_the_body_parts_of_a_chunked_result( - tmpdir, monkeypatch + library: Browser, playwright: RecordingPlaywright ): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) playwright.responses = [ - response("ok chunk 0", '{"name": "J'), - response("ok", 'örn", "size": 2}'), + extension_response("ok chunk 0", '{"name": "J'), + extension_response("ok", 'örn", "size": 2}'), ] result = generated_keyword(library, "describe", "")() @@ -108,16 +113,16 @@ def test_generated_keyword_joins_the_body_parts_of_a_chunked_result( def test_generated_keyword_returns_none_when_the_function_returns_nothing( - tmpdir, monkeypatch + library: Browser, playwright: RecordingPlaywright ): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) - playwright.responses = [response("ok")] + playwright.responses = [extension_response("ok")] assert generated_keyword(library, "closeAll", "")() is None -def test_generated_keyword_sends_its_varargs_as_one_args_list(tmpdir, monkeypatch): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) +def test_generated_keyword_sends_its_varargs_as_one_args_list( + library: Browser, playwright: RecordingPlaywright +): keyword = generated_keyword(library, "typeAll", "selector, args, logger") keyword("#field", "first", "second") @@ -132,9 +137,8 @@ def test_generated_keyword_sends_its_varargs_as_one_args_list(tmpdir, monkeypatc def test_generated_keyword_takes_its_defaults_from_the_javascript_signature( - tmpdir, monkeypatch + library: Browser, ): - library, _ = library_with_recorded_calls(tmpdir, monkeypatch) keyword = generated_keyword( library, "measure", @@ -159,8 +163,9 @@ def test_generated_keyword_takes_its_defaults_from_the_javascript_signature( } -def test_generated_keyword_sends_a_test_scoped_timeout(tmpdir, monkeypatch): - library, playwright = library_with_recorded_calls(tmpdir, monkeypatch) +def test_generated_keyword_sends_a_test_scoped_timeout( + library: Browser, playwright: RecordingPlaywright +): keyword = generated_keyword(library, "openOwnContext", "defaultTimeout") library.set_browser_timeout(timedelta(milliseconds=1500), Scope.Test)