diff --git a/py/private/_network_handlers.py b/py/private/_network_handlers.py index 7eaafdb0a4c5d..3c3ae69ecf91c 100644 --- a/py/private/_network_handlers.py +++ b/py/private/_network_handlers.py @@ -355,24 +355,42 @@ def provide_response(self, status=None, headers=None, body=None, reason_phrase=N self._stub = stub self._execute_provide_response() - def continue_request(self, **kwargs) -> None: + def continue_request( + self, + *, + url: str | None = None, + method: str | None = None, + headers: dict[str, Any] | None = None, + cookies: list | None = None, + body: str | None = None, + ) -> None: """Continue the intercepted request, applying any recorded mutations. - Keyword arguments are passed through to ``network.continueRequest`` and - override recorded mutations. Data URLs (``data:``) are skipped silently - because browsers do not create an interceptable request entry for them, - so calling ``network.continueRequest`` would raise "no such request". + Each keyword argument overrides the corresponding mutation recorded via + ``set_url``/``set_method``/``set_headers``/``set_cookies``/``set_body``. + Arguments use the same Python types as those setters and are translated + to the BiDi wire format automatically. Data URLs (``data:``) are + skipped silently because browsers do not create an interceptable request + entry for them, so calling ``network.continueRequest`` would raise + "no such request". + + Args: + url: Replacement request URL. + method: Replacement HTTP method. + headers: Replacement request headers as a name → value dict. + cookies: Replacement request cookies as a list of dicts. + body: Replacement request body string. """ self._handled = True if self.url.startswith("data:"): return - params = self._continue_params() - params.update(kwargs) + overrides = {"url": url, "method": method, "headers": headers, "cookies": cookies, "body": body} + params = self._continue_params({k: v for k, v in overrides.items() if v is not None}) self._conn.execute(command_builder("network.continueRequest", params)) - def _continue_params(self) -> dict: + def _continue_params(self, overrides: dict | None = None) -> dict: params: dict[str, Any] = {"request": self._request_id} - mutations = self._mutations + mutations = {**self._mutations, **(overrides or {})} if "url" in mutations: params["url"] = mutations["url"] if "method" in mutations: @@ -483,24 +501,38 @@ def set_body(self, body: str) -> None: self.body = body self._mutations["body"] = body - def continue_response(self, **kwargs) -> None: + def continue_response( + self, + *, + status: int | None = None, + reason_phrase: str | None = None, + headers: dict[str, Any] | None = None, + cookies: list | None = None, + ) -> None: """Continue the intercepted response, applying any recorded mutations. - Keyword arguments are passed through to ``network.continueResponse`` - and override recorded mutations. Data URLs (``data:``) are skipped - silently because browsers do not create an interceptable entry for - them. + Each keyword argument overrides the corresponding mutation recorded via + ``set_status``/``set_headers``/``set_cookies``. Arguments use the same + Python types as those setters and are translated to the BiDi wire format + automatically. Data URLs (``data:``) are skipped silently because + browsers do not create an interceptable entry for them. + + Args: + status: Replacement HTTP status code. + reason_phrase: Replacement HTTP reason phrase. + headers: Replacement response headers as a name → value dict. + cookies: Replacement set-cookie entries as a list of dicts. """ self._handled = True if self.url.startswith("data:"): return - params = self._continue_params() - params.update(kwargs) + overrides = {"status": status, "reason_phrase": reason_phrase, "headers": headers, "cookies": cookies} + params = self._continue_params({k: v for k, v in overrides.items() if v is not None}) self._conn.execute(command_builder("network.continueResponse", params)) - def _continue_params(self) -> dict: + def _continue_params(self, overrides: dict | None = None) -> dict: params: dict[str, Any] = {"request": self._request_id} - mutations = self._mutations + mutations = {**self._mutations, **(overrides or {})} if "status" in mutations: params["statusCode"] = mutations["status"] if "reason_phrase" in mutations: diff --git a/py/test/unit/selenium/webdriver/common/bidi_network_tests.py b/py/test/unit/selenium/webdriver/common/bidi_network_tests.py index 96648f2d859e7..a7e9f5be63bd2 100644 --- a/py/test/unit/selenium/webdriver/common/bidi_network_tests.py +++ b/py/test/unit/selenium/webdriver/common/bidi_network_tests.py @@ -209,6 +209,73 @@ def test_continue_request_sends_command_for_regular_urls(): assert conn.commands[0]["params"]["request"] == "request-id-2" +def test_continue_request_translates_explicit_args_to_wire_format(): + conn = FakeConnection() + params = {"request": {"url": "https://example.com/api", "request": "request-id-3"}} + request = Request(conn, params) + + request.continue_request( + url="https://example.com/redirected", + method="POST", + headers={"x-test": "1"}, + cookies=[{"name": "sid", "value": "abc"}], + body="payload", + ) + + sent = conn.commands_named("network.continueRequest")[0]["params"] + assert sent["url"] == "https://example.com/redirected" + assert sent["method"] == "POST" + assert sent["headers"] == [{"name": "x-test", "value": {"type": "string", "value": "1"}}] + assert sent["cookies"] == [{"name": "sid", "value": {"type": "string", "value": "abc"}}] + assert sent["body"] == {"type": "string", "value": "payload"} + + +def test_continue_request_explicit_args_override_recorded_mutations(): + conn = FakeConnection() + params = {"request": {"url": "https://example.com/api", "request": "request-id-4"}} + request = Request(conn, params) + + # A recorded mutation on a different field must survive when only one + # field is overridden via the keyword argument. + request.set_url("https://example.com/recorded") + request.set_method("GET") + request.continue_request(method="DELETE") + + sent = conn.commands_named("network.continueRequest")[0]["params"] + assert sent["method"] == "DELETE" + assert sent["url"] == "https://example.com/recorded" + + +def test_continue_request_keeps_falsy_body_override(): + conn = FakeConnection() + params = {"request": {"url": "https://example.com/api", "request": "request-id-6"}} + request = Request(conn, params) + + # An empty-string body is a valid value, not "unset": it must not be + # dropped by the None filter. + request.continue_request(body="") + + sent = conn.commands_named("network.continueRequest")[0]["params"] + assert sent["body"] == {"type": "string", "value": ""} + + +def test_continue_response_translates_explicit_args_to_wire_format(): + conn = FakeConnection() + params = {"request": {"request": "request-id-5"}, "response": {"url": "https://example.com/api"}} + response = Response(conn, params) + + response.continue_response( + status=503, + reason_phrase="Service Unavailable", + headers={"x-test": "1"}, + ) + + sent = conn.commands_named("network.continueResponse")[0]["params"] + assert sent["statusCode"] == 503 + assert sent["reasonPhrase"] == "Service Unavailable" + assert sent["headers"] == [{"name": "x-test", "value": {"type": "string", "value": "1"}}] + + def test_request_parses_event_properties(): request = Request(FakeConnection(), make_before_request_event())