From 6d537eb61de621e712c373352e020fe8967d23c1 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 4 Jun 2026 23:12:56 +0300 Subject: [PATCH] feat!: Wait For Request also return more than uri return dict with url, method, headers and postData as keys Fixes: #4656 --- Browser/keywords/network.py | 16 +++++++++--- atest/test/03_Waiting/wait_for_http.robot | 32 +++++++++++++++++++++-- atest/test/variables.resource | 2 +- node/dynamic-test-app/src/login.tsx | 29 ++++++++++++++++++-- node/playwright-wrapper/network.ts | 21 ++++++++++++--- protobuf/playwright.proto | 2 +- 6 files changed, 89 insertions(+), 13 deletions(-) diff --git a/Browser/keywords/network.py b/Browser/keywords/network.py index de677a2bc..086b8b21c 100644 --- a/Browser/keywords/network.py +++ b/Browser/keywords/network.py @@ -116,9 +116,14 @@ def _wait_for_http_request(self, matcher, timeout): timeout=self.get_timeout(timeout), ) ) - logger.debug(response.log) - # Add format response back here - return response.body + logger.info(response.log) + try: + data = json.loads(response.json) + except json.decoder.JSONDecodeError: + logger.info(f"Failed to decode JSON: {response.json}") + data = response.json + logger.debug(f"Received request: {data}") + return data def _wait_for_http_response(self, matcher, timeout): body = "" @@ -142,9 +147,12 @@ def _wait_for_http_response(self, matcher, timeout): @keyword(tags=("Wait", "HTTP")) def wait_for_request( self, matcher: str | RegExp = "", timeout: timedelta | None = None - ) -> Any: + ) -> DotDict | Any: """Waits for request matching matcher to be made. + The returned object is a dictionary with keys: ``url``, ``method``, ``headers`` and ``postData``. + ``headers`` is a dictionary of request headers. ``postData`` is ``None`` if body is empty or the request body. + | =Arguments= | =Description= | | ``matcher`` | Request URL matcher. Can be a string (Glob-Pattern), JavaScript RegExp (encapsulated in / with following flags) or JavaScript arrow-function that receives the [https://playwright.dev/docs/api/class-request|Request] object and returns a boolean. By default (with empty string) matches first available request. For additional information, see the Playwright [https://playwright.dev/docs/api/class-page#page-wait-for-request|waitForRequest] documentation. | | ``timeout`` | Timeout supports Robot Framework time format. Uses default timeout if not set. | diff --git a/atest/test/03_Waiting/wait_for_http.robot b/atest/test/03_Waiting/wait_for_http.robot index 2c33a7254..20e623274 100644 --- a/atest/test/03_Waiting/wait_for_http.robot +++ b/atest/test/03_Waiting/wait_for_http.robot @@ -20,11 +20,20 @@ Wait For Request Synchronous Wait For Request Async ${promise} = Promise To Wait For Request matcher= timeout=3s Click \#delayed_request - Wait For ${promise} + ${data} = Wait For ${promise} + Should Contain ${data.url} /api/log/event + Should Be Equal ${data.method} POST + Should Not Be Empty ${data.headers} + Should Be Equal ${data.postData} ${None} + Length Should Be ${data} 4 Wait For Request Url Click \#delayed_request - Wait For Request matcher=${ROOT_URL}api/get/json timeout=1s + ${data} = Wait For Request matcher=${ROOT_URL}api/get/json timeout=1s + Should Contain ${data.url} /api/get/json + Should Be Equal ${data.method} GET + Should Not Be Empty ${data.headers} + Should Be Equal ${data.postData} ${None} Wait For Request Regex [Tags] no-docker-pr @@ -36,6 +45,25 @@ Wait For Request Predicate Wait For Request matcher=request => request.url().endsWith('api/get/json') && request.method() === 'GET' ... timeout=1s +Wait For Request With POST Method + Click id=delayed_request_post + ${data} = Wait For Request matcher=/\\/\\/local\\w+\\:\\d+\\/api\\/post\\/json/ timeout=1s + VAR ${post_data} = ${data.postData} + Should Be Equal ${post_data}[data] test + Should Be Equal ${post_data}[kala] salmon + Should Be Equal ${data.method} POST + Should Contain ${data.url} /api/post/json + Should Not Be Empty ${data.headers} + Length Should Be ${data} 4 + +Wait For Request With Invalid JSON + Click id=delayed_request_invalid_json_post + ${data} = Wait For Request matcher=/\\/\\/local\\w+\\:\\d+\\/api\\/post\\/invalid-json/ timeout=1s + Should Be Equal ${data.method} POST + Should Contain ${data.url} /api/post/invalid-json + Should Not Be Empty ${data.headers} + Should Be Equal ${data.postData} This is not valid JSON in body + Wait For Response Synchronous Click \#delayed_request ${data} = Wait For Response matcher=**/api/get/json timeout=1s diff --git a/atest/test/variables.resource b/atest/test/variables.resource index 8c63f8282..f345d52ae 100644 --- a/atest/test/variables.resource +++ b/atest/test/variables.resource @@ -47,5 +47,5 @@ ${SYS_VAR_CI} = False ${SYS_VAR_CLEAN} = ${True} ${NETWORK_IDLE_FILE_DATA} = ${EMPTY} ${INPUT_ELEMENT_COUNT_IN_LOGIN} = 5 -${BUTTON_ELEMENT_COUNT_IN_LOGIN} = ${16} +${BUTTON_ELEMENT_COUNT_IN_LOGIN} = ${18} ${PYTHON_314} = ${None} diff --git a/node/dynamic-test-app/src/login.tsx b/node/dynamic-test-app/src/login.tsx index 3bdf22be2..2743f5f1e 100644 --- a/node/dynamic-test-app/src/login.tsx +++ b/node/dynamic-test-app/src/login.tsx @@ -65,6 +65,27 @@ async function delayedRequestBig() { console.log(await fetch('/api/get/json/big')); } +async function delayedRequestPost() { + await sleep(200); + console.log( + await fetch('/api/post/json', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: 'test', kala: 'salmon' }), + }), + ); +} + +async function delayedRequestInvalidJsonPost() { + await sleep(200); + console.log( + await fetch('/api/post/invalid-json', { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: 'This is not valid JSON in body', + }), + ); +} function fileUploaded(uploadResultElement: React.RefObject, event: ChangeEvent) { const files = event.target.files; let fileNames = ''; @@ -225,6 +246,12 @@ export default function Site() { + + {' '} @@ -347,7 +374,6 @@ export default function Site() { -

- { const opts: { [k: string]: any } = { @@ -113,11 +113,26 @@ export async function waitForResponse(request: pb.Request_HttpCapture, page: Pag logger.info(`responseChunks.length: ${responseChunks.length}`); return responseChunks; } -export async function waitForRequest(request: pb.Request_HttpCapture, page: Page): Promise { +export async function waitForRequest(request: pb.Request_HttpCapture, page: Page): Promise { const urlOrPredicate = deserializeUrlOrPredicate(request.urlOrPredicate); const timeout = request.timeout; const result = await page.waitForRequest(urlOrPredicate, { timeout }); - return stringResponse(result.url(), 'Request completed within timeout.'); + let postData; + try { + postData = JSON.parse(result.postData() || 'null'); + } catch (e) { + logger.info(`Failed to parse postData as JSON: ${String(e)}, using raw postData`); + postData = result.postData(); + } + const jsonData = JSON.stringify({ + url: result.url(), + method: result.method(), + headers: result.headers(), + postData: postData, + }); + logger.info(`waitForRequest received: ${result.url()} method: ${result.method()}`); + const matcherStr = typeof urlOrPredicate === 'string' ? urlOrPredicate : urlOrPredicate.toString(); + return jsonResponse(jsonData, `Request completed within timeout ${timeout}ms by using matcher: ${matcherStr}`); } export async function waitForNavigation(request: pb.Request_UrlOptions, page: Page): Promise { diff --git a/protobuf/playwright.proto b/protobuf/playwright.proto index 37b83fd22..228332324 100644 --- a/protobuf/playwright.proto +++ b/protobuf/playwright.proto @@ -561,7 +561,7 @@ service Playwright { rpc GetBoundingBox(Request.ElementSelector) returns (Response.Json); /* Makes a `fetch` request in the browser */ rpc HttpRequest(Request.HttpRequest) returns (Response.Json); - rpc WaitForRequest(Request.HttpCapture) returns (Response.String); + rpc WaitForRequest(Request.HttpCapture) returns (Response.Json); rpc WaitForResponse(Request.HttpCapture) returns (stream Response.Json); rpc WaitForDownload(Request.DownloadOptions) returns (Response.Json); rpc WaitForNavigation(Request.UrlOptions) returns (Response.Empty);