From 87f36e486b73c25aee8c2ecdfaf9f64e174f7db9 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Sat, 25 Jun 2022 15:56:31 -0700 Subject: [PATCH 1/8] add impl and tests for route_from_har --- README.md | 2 +- playwright/_impl/_browser_context.py | 16 + playwright/_impl/_har_router.py | 106 ++++ playwright/_impl/_helper.py | 11 + playwright/_impl/_local_utils.py | 31 +- playwright/_impl/_network.py | 1 - playwright/_impl/_page.py | 16 + playwright/async_api/_generated.py | 82 ++- playwright/sync_api/_generated.py | 86 ++- scripts/expected_api_mismatch.txt | 4 - setup.py | 2 +- tests/assets/har-fulfill.har | 366 +++++++++++++ tests/assets/har-redirect.har | 620 ++++++++++++++++++++++ tests/assets/har-sha1-main-response.txt | 1 + tests/assets/har-sha1.har | 95 ++++ tests/async/test_defaultbrowsercontext.py | 9 + tests/async/test_har.py | 352 +++++++++++- tests/sync/test_har.py | 300 ++++++++++- 18 files changed, 2085 insertions(+), 15 deletions(-) create mode 100644 playwright/_impl/_har_router.py create mode 100644 tests/assets/har-fulfill.har create mode 100644 tests/assets/har-redirect.har create mode 100644 tests/assets/har-sha1-main-response.txt create mode 100644 tests/assets/har-sha1.har diff --git a/README.md b/README.md index 6a1f7da36..0eed3786f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 103.0.5060.53 | ✅ | ✅ | ✅ | +| Chromium 104.0.5112.20 | ✅ | ✅ | ✅ | | WebKit 15.4 | ✅ | ✅ | ✅ | | Firefox 100.0.2 | ✅ | ✅ | ✅ | diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index a5b81bb04..d8b172810 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -35,7 +35,9 @@ from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame +from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( + RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, TimeoutSettings, @@ -292,6 +294,20 @@ async def unroute( if len(self._routes) == 0: await self._disable_interception() + async def route_from_har( + self, + har: Union[Path, str], + url: URLMatch = None, + not_found: RouteFromHarNotFoundPolicy = None, + ) -> None: + router = await HarRouter.create( + local_utils=self._connection.local_utils, + file=str(har), + not_found_action=not_found or "abort", + url_matcher=url, + ) + await router.add_context_route(self) + async def _disable_interception(self) -> None: await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False)) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py new file mode 100644 index 000000000..8b061fb97 --- /dev/null +++ b/playwright/_impl/_har_router.py @@ -0,0 +1,106 @@ +import asyncio +import base64 +from typing import TYPE_CHECKING, Optional, cast + +from playwright._impl._api_structures import HeadersArray +from playwright._impl._helper import ( + HarLookupResult, + RouteFromHarNotFoundPolicy, + URLMatch, +) +from playwright._impl._local_utils import LocalUtils + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext + from playwright._impl._network import Route + from playwright._impl._page import Page + + +class HarRouter: + def __init__( + self, + local_utils: LocalUtils, + har_id: str, + not_found_action: RouteFromHarNotFoundPolicy, + url_matcher: Optional[URLMatch] = None, + ) -> None: + self._local_utils: LocalUtils = local_utils + self._har_id: str = har_id + self._not_found_action: RouteFromHarNotFoundPolicy = not_found_action + self._options_url_match: Optional[URLMatch] = url_matcher + + @staticmethod + async def create( + local_utils: LocalUtils, + file: str, + not_found_action: RouteFromHarNotFoundPolicy, + url_matcher: Optional[URLMatch] = None, + ) -> "HarRouter": + har_id = await local_utils._channel.send("harOpen", {"file": file}) + return HarRouter( + local_utils=local_utils, + har_id=har_id, + not_found_action=not_found_action, + url_matcher=url_matcher, + ) + + async def _handle(self, route: "Route") -> None: + request = route.request + response: HarLookupResult = await self._local_utils.har_lookup( + harId=self._har_id, + url=request.url, + method=request.method, + headers=await request.headers_array(), + postData=request.post_data_buffer, + isNavigationRequest=request.is_navigation_request(), + ) + action = response["action"] + if action == "redirect": + # debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); + redirect_url = response["redirectURL"] + assert redirect_url + await route._redirected_navigation_request(redirect_url) + return + + if action == "fulfill": + body = response["body"] + assert body is not None + await route.fulfill( + status=response.get("status"), + headers={ + v["name"]: v["value"] + for v in cast(HeadersArray, response.get("headers", [])) + }, + body=base64.b64decode(body), + ) + return + + if action == "error": + pass + # debugLogger.log('api', 'HAR: ' + response.message!); + # Report the error, but fall through to the default handler. + + if self._not_found_action == "abort": + await route.abort() + return + + await route.fallback() + + async def add_context_route(self, context: "BrowserContext") -> None: + await context.route( + url=self._options_url_match or "**/*", + handler=lambda route, _: asyncio.create_task(self._handle(route)), + ) + context.once("close", lambda _: self._dispose()) + + async def add_page_route(self, page: "Page") -> None: + await page.route( + url=self._options_url_match or "**/*", + handler=lambda route, _: asyncio.create_task(self._handle(route)), + ) + page.once("close", lambda _: self._dispose()) + + def _dispose(self) -> None: + asyncio.create_task( + self._local_utils._channel.send("harClose", {"harId": self._har_id}) + ) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 0a80e76e3..faa3de398 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -49,6 +49,7 @@ if TYPE_CHECKING: # pragma: no cover + from playwright._impl._api_structures import HeadersArray from playwright._impl._network import Request, Response, Route URLMatch = Union[str, Pattern, Callable[[str], bool]] @@ -67,6 +68,7 @@ ServiceWorkersPolicy = Literal["allow", "block"] HarMode = Literal["full", "minimal"] HarContentPolicy = Literal["attach", "embed", "omit"] +RouteFromHarNotFoundPolicy = Literal["abort", "fallback"] class ErrorPayload(TypedDict, total=False): @@ -135,6 +137,15 @@ def matches(self, url: str) -> bool: return False +class HarLookupResult(TypedDict, total=False): + action: Literal["error", "redirect", "fulfill", "noentry"] + message: Optional[str] + redirectURL: Optional[str] + status: Optional[int] + headers: Optional["HeadersArray"] + body: Optional[str] + + class TimeoutSettings: def __init__(self, parent: Optional["TimeoutSettings"]) -> None: self._parent = parent diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 999957582..a9d9395cc 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List +import base64 +from typing import Dict, List, Optional, cast -from playwright._impl._api_structures import NameValue +from playwright._impl._api_structures import HeadersArray, NameValue from playwright._impl._connection import ChannelOwner +from playwright._impl._helper import HarLookupResult, locals_to_params class LocalUtils(ChannelOwner): @@ -26,3 +28,28 @@ def __init__( async def zip(self, zip_file: str, entries: List[NameValue]) -> None: await self._channel.send("zip", {"zipFile": zip_file, "entries": entries}) + + async def har_open(self, file: str) -> None: + params = locals_to_params(locals()) + await self._channel.send("harOpen", params) + + async def har_lookup( + self, + harId: str, + url: str, + method: str, + headers: HeadersArray, + isNavigationRequest: bool, + postData: Optional[bytes] = None, + ) -> HarLookupResult: + params = locals_to_params(locals()) + if "postData" in params: + params["postData"] = base64.b64encode(params["postData"]).decode() + return cast( + HarLookupResult, + await self._channel.send_return_as_dict("harLookup", params), + ) + + async def har_close(self, harId: str) -> None: + params = locals_to_params(locals()) + await self._channel.send("harClose", params) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 88b820c58..7d5d398f2 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -355,7 +355,6 @@ async def continue_route() -> None: return continue_route() - # FIXME: Port corresponding tests, and call this method async def _redirected_navigation_request(self, url: str) -> None: self._check_not_handled() await self._race_with_page_close( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 1245ff819..c8066e496 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -53,6 +53,7 @@ from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser from playwright._impl._frame import Frame +from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( ColorScheme, DocumentLoadState, @@ -60,6 +61,7 @@ KeyboardModifier, MouseButton, ReducedMotion, + RouteFromHarNotFoundPolicy, RouteHandler, RouteHandlerCallback, TimeoutSettings, @@ -600,6 +602,20 @@ async def unroute( if len(self._routes) == 0: await self._disable_interception() + async def route_from_har( + self, + har: Union[Path, str], + url: URLMatch = None, + not_found: RouteFromHarNotFoundPolicy = None, + ) -> None: + router = await HarRouter.create( + local_utils=self._connection.local_utils, + file=str(har), + not_found_action=not_found or "abort", + url_matcher=url, + ) + await router.add_page_route(self) + async def _disable_interception(self) -> None: await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False)) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index a2f2198be..2af4a9a60 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -268,7 +268,9 @@ def timing(self) -> ResourceTiming: def headers(self) -> typing.Dict[str, str]: """Request.headers - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. + An object with the request HTTP headers. The header names are lower-cased. Note that this method does not return + security-related headers, including cookie-related ones. You can use `request.all_headers()` for complete list of + headers that include `cookie` information. Returns ------- @@ -411,7 +413,9 @@ def status_text(self) -> str: def headers(self) -> typing.Dict[str, str]: """Response.headers - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `response.all_headers()` instead. + An object with the response HTTP headers. The header names are lower-cased. Note that this method does not return + security-related headers, including cookie-related ones. You can use `response.all_headers()` for complete list + of headers that include `cookie` information. Returns ------- @@ -7736,6 +7740,43 @@ async def unroute( ) ) + async def route_from_har( + self, + har: typing.Union[pathlib.Path, str], + *, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None, + not_found: Literal["abort", "fallback"] = None + ) -> NoneType: + """Page.route_from_har + + If specified the network requests that are made in the page will be served from the HAR file. Read more about + [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + + Playwright will not serve requests intercepted by Service Worker from the HAR file. See + [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + + Parameters + ---------- + har : Union[pathlib.Path, str] + Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a + relative path, then it is resolved relative to the current working directory. + url : Union[Callable[[str], bool], Pattern, str, NoneType] + A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern + will be surved from the HAR file. If not specified, all requests are served from the HAR file. + not_found : Union["abort", "fallback", NoneType] + - If set to 'abort' any request not found in the HAR file will be aborted. + - If set to 'fallback' missing requests will be sent to the network. + + Defaults to abort. + """ + + return mapping.from_maybe_impl( + await self._impl_obj.route_from_har( + har=har, url=self._wrap_handler(url), not_found=not_found + ) + ) + async def screenshot( self, *, @@ -10432,6 +10473,43 @@ async def unroute( ) ) + async def route_from_har( + self, + har: typing.Union[pathlib.Path, str], + *, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None, + not_found: Literal["abort", "fallback"] = None + ) -> NoneType: + """BrowserContext.route_from_har + + If specified the network requests that are made in the context will be served from the HAR file. Read more about + [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + + Playwright will not serve requests intercepted by Service Worker from the HAR file. See + [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + + Parameters + ---------- + har : Union[pathlib.Path, str] + Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a + relative path, then it is resolved relative to the current working directory. + url : Union[Callable[[str], bool], Pattern, str, NoneType] + A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern + will be surved from the HAR file. If not specified, all requests are served from the HAR file. + not_found : Union["abort", "fallback", NoneType] + - If set to 'abort' any request not found in the HAR file will be aborted. + - If set to 'fallback' falls through to the next route handler in the handler chain. + + Defaults to abort. + """ + + return mapping.from_maybe_impl( + await self._impl_obj.route_from_har( + har=har, url=self._wrap_handler(url), not_found=not_found + ) + ) + def expect_event( self, event: str, predicate: typing.Callable = None, *, timeout: float = None ) -> AsyncEventContextManager: diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 56c7c954b..800816068 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -268,7 +268,9 @@ def timing(self) -> ResourceTiming: def headers(self) -> typing.Dict[str, str]: """Request.headers - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. + An object with the request HTTP headers. The header names are lower-cased. Note that this method does not return + security-related headers, including cookie-related ones. You can use `request.all_headers()` for complete list of + headers that include `cookie` information. Returns ------- @@ -413,7 +415,9 @@ def status_text(self) -> str: def headers(self) -> typing.Dict[str, str]: """Response.headers - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `response.all_headers()` instead. + An object with the response HTTP headers. The header names are lower-cased. Note that this method does not return + security-related headers, including cookie-related ones. You can use `response.all_headers()` for complete list + of headers that include `cookie` information. Returns ------- @@ -7762,6 +7766,45 @@ def unroute( ) ) + def route_from_har( + self, + har: typing.Union[pathlib.Path, str], + *, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None, + not_found: Literal["abort", "fallback"] = None + ) -> NoneType: + """Page.route_from_har + + If specified the network requests that are made in the page will be served from the HAR file. Read more about + [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + + Playwright will not serve requests intercepted by Service Worker from the HAR file. See + [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + + Parameters + ---------- + har : Union[pathlib.Path, str] + Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a + relative path, then it is resolved relative to the current working directory. + url : Union[Callable[[str], bool], Pattern, str, NoneType] + A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern + will be surved from the HAR file. If not specified, all requests are served from the HAR file. + not_found : Union["abort", "fallback", NoneType] + - If set to 'abort' any request not found in the HAR file will be aborted. + - If set to 'fallback' missing requests will be sent to the network. + + Defaults to abort. + """ + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.route_from_har( + har=har, url=self._wrap_handler(url), not_found=not_found + ) + ) + ) + def screenshot( self, *, @@ -10456,6 +10499,45 @@ def unroute( ) ) + def route_from_har( + self, + har: typing.Union[pathlib.Path, str], + *, + url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None, + not_found: Literal["abort", "fallback"] = None + ) -> NoneType: + """BrowserContext.route_from_har + + If specified the network requests that are made in the context will be served from the HAR file. Read more about + [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + + Playwright will not serve requests intercepted by Service Worker from the HAR file. See + [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using + request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + + Parameters + ---------- + har : Union[pathlib.Path, str] + Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a + relative path, then it is resolved relative to the current working directory. + url : Union[Callable[[str], bool], Pattern, str, NoneType] + A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern + will be surved from the HAR file. If not specified, all requests are served from the HAR file. + not_found : Union["abort", "fallback", NoneType] + - If set to 'abort' any request not found in the HAR file will be aborted. + - If set to 'fallback' falls through to the next route handler in the handler chain. + + Defaults to abort. + """ + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.route_from_har( + har=har, url=self._wrap_handler(url), not_found=not_found + ) + ) + ) + def expect_event( self, event: str, predicate: typing.Callable = None, *, timeout: float = None ) -> EventContextManager: diff --git a/scripts/expected_api_mismatch.txt b/scripts/expected_api_mismatch.txt index 75018a398..7e6b3cda6 100644 --- a/scripts/expected_api_mismatch.txt +++ b/scripts/expected_api_mismatch.txt @@ -18,7 +18,3 @@ Method not implemented: Error.name Method not implemented: Error.stack Method not implemented: Error.message Method not implemented: PlaywrightAssertions.expect - -# Pending 1.23 ports -Method not implemented: BrowserContext.route_from_har -Method not implemented: Page.route_from_har diff --git a/setup.py b/setup.py index c3376f354..5953e9960 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.23.0-beta-1656093125000" +driver_version = "1.23.0" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/assets/har-fulfill.har b/tests/assets/har-fulfill.har new file mode 100644 index 000000000..dc6b7c679 --- /dev/null +++ b/tests/assets/har-fulfill.har @@ -0,0 +1,366 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "111" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 111, + "mimeType": "text/html", + "compression": 0, + "text": "Hey
hello
" + }, + "headersSize": 65, + "bodySize": 170, + "redirectURL": "", + "_transferSize": 170 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.172Z", + "time": 7.132, + "request": { + "method": "POST", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background:cyan }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.174Z", + "time": 8.132, + "request": { + "method": "GET", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background: red }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572175.042, + "startedDateTime": "2022-06-10T04:27:32.175Z", + "time": 15.997, + "request": { + "method": "GET", + "url": "http://no.playwright/script.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 205, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "location", + "value": "http://no.playwright/script2.js" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 77, + "bodySize": 0, + "redirectURL": "http://no.playwright/script2.js", + "_transferSize": 77 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 7.673, + "receive": 8.324 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572181.822, + "startedDateTime": "2022-06-10T04:27:32.182Z", + "time": 6.735, + "request": { + "method": "GET", + "url": "http://no.playwright/script2.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 206, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "18" + }, + { + "name": "content-type", + "value": "text/javascript" + } + ], + "content": { + "size": 18, + "mimeType": "text/javascript", + "compression": 0, + "text": "window.value='foo'" + }, + "headersSize": 70, + "bodySize": 82, + "redirectURL": "", + "_transferSize": 82 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 6.735, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} diff --git a/tests/assets/har-redirect.har b/tests/assets/har-redirect.har new file mode 100644 index 000000000..b2e573310 --- /dev/null +++ b/tests/assets/har-redirect.har @@ -0,0 +1,620 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.42" + }, + "pages": [ + { + "startedDateTime": "2022-06-16T21:41:23.901Z", + "id": "page@8f314969edc000996eb5c2ab22f0e6b3", + "title": "Microsoft", + "pageTimings": { + "onContentLoad": 8363, + "onLoad": 8896 + } + } + ], + "entries": [ + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928357.437, + "startedDateTime": "2022-06-16T21:41:23.951Z", + "time": 93.99, + "request": { + "method": "GET", + "url": "https://theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": ":authority", + "value": "theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 644, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "content-length", + "value": "0" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "location", + "value": "http://www.theverge.com/" + }, + { + "name": "retry-after", + "value": "0" + }, + { + "name": "server", + "value": "Varnish" + }, + { + "name": "set-cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "0" + }, + { + "name": "x-served-by", + "value": "cache-pao17442-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.005867,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 425, + "bodySize": 0, + "redirectURL": "http://www.theverge.com/", + "_transferSize": 425 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0, + "connect": 34.151, + "ssl": 28.074, + "send": 0, + "wait": 27.549, + "receive": 4.216 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.65.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928427.603, + "startedDateTime": "2022-06-16T21:41:24.022Z", + "time": 44.39499999999999, + "request": { + "method": "GET", + "url": "http://www.theverge.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "www.theverge.com" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 423, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "4e0c1265-10f8-4cb1-a5de-1c3cf70b531c", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "Accept-Ranges", + "value": "bytes" + }, + { + "name": "Age", + "value": "2615" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "0" + }, + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "Location", + "value": "https://www.theverge.com/" + }, + { + "name": "Server", + "value": "nginx" + }, + { + "name": "Set-Cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "Set-Cookie", + "value": "vmidv1=4e0c1265-10f8-4cb1-a5de-1c3cf70b531c;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "Vary", + "value": "X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Accept-Encoding" + }, + { + "name": "Via", + "value": "1.1 varnish" + }, + { + "name": "X-Cache", + "value": "HIT" + }, + { + "name": "X-Cache-Hits", + "value": "2" + }, + { + "name": "X-Served-By", + "value": "cache-pao17450-PAO" + }, + { + "name": "X-Timer", + "value": "S1655415684.035748,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "text/html", + "compression": 0 + }, + "headersSize": 731, + "bodySize": 0, + "redirectURL": "https://www.theverge.com/", + "_transferSize": 731 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 2.742, + "connect": 10.03, + "ssl": 14.123, + "send": 0, + "wait": 15.023, + "receive": 2.477 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 80, + "_securityDetails": {} + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928455.901, + "startedDateTime": "2022-06-16T21:41:24.050Z", + "time": 50.29199999999999, + "request": { + "method": "GET", + "url": "https://www.theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41" + }, + { + "name": "_chorus_geoip_continent", + "value": "NA" + } + ], + "headers": [ + { + "name": ":authority", + "value": "www.theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41; _chorus_geoip_continent=NA" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 729, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "40d8fd14-5ac3-4757-9e9c-efb106e82d3a", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "age", + "value": "263" + }, + { + "name": "cache-control", + "value": "max-age=0, public, must-revalidate" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-length", + "value": "14" + }, + { + "name": "content-security-policy", + "value": "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob: ; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests" + }, + { + "name": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "etag", + "value": "W/\"d498ef668223d015000070a66a181e85\"" + }, + { + "name": "link", + "value": "; rel=preload; as=fetch; crossorigin" + }, + { + "name": "referrer-policy", + "value": "strict-origin-when-cross-origin" + }, + { + "name": "server", + "value": "nginx" + }, + { + "name": "set-cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "set-cookie", + "value": "vmidv1=40d8fd14-5ac3-4757-9e9c-efb106e82d3a;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "strict-transport-security", + "value": "max-age=31556952; preload" + }, + { + "name": "vary", + "value": "Accept-Encoding, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Origin, X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "1" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-download-options", + "value": "noopen" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-permitted-cross-domain-policies", + "value": "none" + }, + { + "name": "x-request-id", + "value": "97363ad70e272e63641c0bb784fa06a01b848dfd" + }, + { + "name": "x-runtime", + "value": "0.257911" + }, + { + "name": "x-served-by", + "value": "cache-pao17436-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.075077,VS0,VE1" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + } + ], + "content": { + "size": 14, + "mimeType": "text/html", + "compression": 0, + "text": "

hello

" + }, + "headersSize": 1742, + "bodySize": 48716, + "redirectURL": "", + "_transferSize": 48716 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0.016, + "connect": 24.487, + "ssl": 17.406, + "send": 0, + "wait": 8.383, + "receive": -1 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + } + ] + } +} diff --git a/tests/assets/har-sha1-main-response.txt b/tests/assets/har-sha1-main-response.txt new file mode 100644 index 000000000..a5c196677 --- /dev/null +++ b/tests/assets/har-sha1-main-response.txt @@ -0,0 +1 @@ +Hello, world diff --git a/tests/assets/har-sha1.har b/tests/assets/har-sha1.har new file mode 100644 index 000000000..850b06dd8 --- /dev/null +++ b/tests/assets/har-sha1.har @@ -0,0 +1,95 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "12" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 12, + "mimeType": "text/html", + "compression": 0, + "_file": "har-sha1-main-response.txt" + }, + "headersSize": 64, + "bodySize": 71, + "redirectURL": "", + "_transferSize": 71 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index ebe3a86c7..e47c4f8b0 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -18,6 +18,7 @@ import pytest from playwright._impl._api_types import Error +from playwright.async_api import expect @pytest.fixture() @@ -336,3 +337,11 @@ async def test_should_fire_close_event_for_a_persistent_context(launch_persisten async def test_should_support_reduced_motion(launch_persistent): (page, context) = await launch_persistent(reduced_motion="reduce") assert await page.evaluate("matchMedia('(prefers-reduced-motion: reduce)').matches") + + +async def test_should_support_har_option(browser, server, assetdir, launch_persistent): + (page, context) = await launch_persistent() + await page.route_from_har(har=assetdir / "har-fulfill.har") + await page.goto("http://no.playwright/") + assert await page.evaluate("window.value") == "foo" + await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") diff --git a/tests/async/test_har.py b/tests/async/test_har.py index 00d02d32d..33c5c593f 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import json import os import re import zipfile +from pathlib import Path -from playwright.async_api import Browser +import pytest + +from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -225,3 +229,349 @@ async def test_should_filter_by_regexp( log = data["log"] assert len(log["entries"]) == 1 assert log["entries"][0]["request"]["url"].endswith("har.html") + + +async def test_should_context_route_from_har_matching_the_method_and_following_redirects( + context: BrowserContext, assetdir: Path +) -> None: + await context.route_from_har(har=assetdir / "har-fulfill.har") + page = await context.new_page() + await page.goto("http://no.playwright/") + # HAR contains a redirect for the script that should be followed automatically. + assert await page.evaluate("window.value") == "foo" + # HAR contains a POST for the css file that should not be used. + await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +async def test_should_page_route_from_har_matching_the_method_and_following_redirects( + page: Page, assetdir: Path +) -> None: + await page.route_from_har(har=assetdir / "har-fulfill.har") + await page.goto("http://no.playwright/") + # HAR contains a redirect for the script that should be followed automatically. + assert await page.evaluate("window.value") == "foo" + # HAR contains a POST for the css file that should not be used. + await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +async def test_fallback_continue_should_continue_when_not_found_in_har( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har(har=assetdir / "har-fulfill.har", not_found="fallback") + page = await context.new_page() + await page.goto(server.PREFIX + "/one-style.html") + await expect(page.locator("body")).to_have_css( + "background-color", "rgb(255, 192, 203)" + ) + + +async def test_by_default_should_abort_requests_not_found_in_har( + context: BrowserContext, + server: Server, + assetdir: Path, + is_chromium: bool, + is_webkit: bool, +) -> None: + await context.route_from_har(har=assetdir / "har-fulfill.har") + page = await context.new_page() + + with pytest.raises(Error) as exc_info: + await page.goto(server.EMPTY_PAGE) + assert exc_info.value + if is_chromium: + assert "net::ERR_FAILED" in exc_info.value.message + elif is_webkit: + assert "Blocked by Web Inspector" in exc_info.value.message + else: + assert "NS_ERROR_FAILURE" in exc_info.value.message + + +async def test_fallback_continue_should_continue_requests_on_bad_har( + context: BrowserContext, server: Server, tmpdir: Path +) -> None: + path_to_invalid_har = tmpdir / "invalid.har" + with path_to_invalid_har.open("w") as f: + json.dump({"log": {}}, f) + await context.route_from_har(har=path_to_invalid_har, not_found="fallback") + page = await context.new_page() + await page.goto(server.PREFIX + "/one-style.html") + await expect(page.locator("body")).to_have_css( + "background-color", "rgb(255, 192, 203)" + ) + + +async def test_should_only_handle_requests_matching_url_filter( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-fulfill.har", not_found="fallback", url="**/*.js" + ) + page = await context.new_page() + + async def handler(route: Route): + assert route.request.url == "http://no.playwright/" + await route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + await context.route("http://no.playwright/", handler) + await page.goto("http://no.playwright/") + assert await page.evaluate("window.value") == "foo" + await expect(page.locator("body")).to_have_css( + "background-color", "rgba(0, 0, 0, 0)" + ) + + +async def test_should_only_handle_requests_matching_url_filter_no_fallback( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js") + page = await context.new_page() + + async def handler(route: Route): + assert route.request.url == "http://no.playwright/" + await route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + await context.route("http://no.playwright/", handler) + await page.goto("http://no.playwright/") + assert await page.evaluate("window.value") == "foo" + await expect(page.locator("body")).to_have_css( + "background-color", "rgba(0, 0, 0, 0)" + ) + + +async def test_should_only_handle_requests_matching_url_filter_no_fallback_page( + page: Page, server: Server, assetdir: Path +) -> None: + await page.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js") + + async def handler(route: Route): + assert route.request.url == "http://no.playwright/" + await route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + await page.route("http://no.playwright/", handler) + await page.goto("http://no.playwright/") + assert await page.evaluate("window.value") == "foo" + await expect(page.locator("body")).to_have_css( + "background-color", "rgba(0, 0, 0, 0)" + ) + + +async def test_should_support_regex_filter( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-fulfill.har", + url=re.compile(r".*(\.js|.*\.css|no.playwright\/)"), + ) + page = await context.new_page() + await page.goto("http://no.playwright/") + assert await page.evaluate("window.value") == "foo" + await expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +async def test_should_change_document_url_after_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har(har=assetdir / "har-redirect.har") + page = await context.new_page() + + async with page.expect_navigation() as navigation_info: + await asyncio.gather( + page.wait_for_url("https://www.theverge.com/"), + page.goto("https://theverge.com/"), + ) + + response = await navigation_info.value + await expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert await page.evaluate("window.location.href") == "https://www.theverge.com/" + + +async def test_should_change_document_url_after_redirected_navigation_on_click( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = await context.new_page() + await page.goto(server.EMPTY_PAGE) + await page.set_content('click me') + async with page.expect_navigation() as navigation_info: + await asyncio.gather( + page.wait_for_url("https://www.theverge.com/"), + page.click("text=click me"), + ) + + response = await navigation_info.value + await expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert await page.evaluate("window.location.href") == "https://www.theverge.com/" + + +async def test_should_go_back_to_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = await context.new_page() + await page.goto("https://theverge.com/") + await page.goto(server.EMPTY_PAGE) + await expect(page).to_have_url(server.EMPTY_PAGE) + + response = await page.go_back() + await expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert await page.evaluate("window.location.href") == "https://www.theverge.com/" + + +async def test_should_go_forward_to_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = await context.new_page() + await page.goto("https://theverge.com/") + await page.goto(server.EMPTY_PAGE) + await expect(page).to_have_url(server.EMPTY_PAGE) + await page.goto("https://theverge.com/") + await expect(page).to_have_url("https://www.theverge.com/") + await page.go_back() + await expect(page).to_have_url(server.EMPTY_PAGE) + response = await page.go_forward() + await expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert await page.evaluate("window.location.href") == "https://www.theverge.com/" + + +async def test_should_reload_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = await context.new_page() + await page.goto("https://theverge.com/") + await expect(page).to_have_url("https://www.theverge.com/") + response = await page.reload() + await expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert await page.evaluate("window.location.href") == "https://www.theverge.com/" + + +async def test_should_fulfill_from_har_with_content_in_a_file( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + await context.route_from_har(har=assetdir / "har-sha1.har") + page = await context.new_page() + await page.goto("http://no.playwright/") + assert await page.content() == "Hello, world" + + +async def test_should_round_trip_har_zip( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + + har_path = tmpdir / "har.zip" + context_1 = await browser.new_context( + record_har_mode="minimal", record_har_path=har_path + ) + page_1 = await context_1.new_page() + await page_1.goto(server.PREFIX + "/one-style.html") + await context_1.close() + + context_2 = await browser.new_context() + await context_2.route_from_har(har=har_path, not_found="abort") + page_2 = await context_2.new_page() + await page_2.goto(server.PREFIX + "/one-style.html") + assert "hello, world!" in await page_2.content() + await expect(page_2.locator("body")).to_have_css( + "background-color", "rgb(255, 192, 203)" + ) + + +async def test_should_round_trip_har_with_post_data( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish())) + fetch_function = """ + async (body) => { + const response = await fetch('/echo', { method: 'POST', body }); + return await response.text(); + }; + """ + har_path = tmpdir / "har.zip" + context_1 = await browser.new_context( + record_har_mode="minimal", record_har_path=har_path + ) + page_1 = await context_1.new_page() + await page_1.goto(server.EMPTY_PAGE) + + assert await page_1.evaluate(fetch_function, "1") == "1" + assert await page_1.evaluate(fetch_function, "2") == "2" + assert await page_1.evaluate(fetch_function, "3") == "3" + await context_1.close() + + context_2 = await browser.new_context() + await context_2.route_from_har(har=har_path, not_found="abort") + page_2 = await context_2.new_page() + await page_2.goto(server.EMPTY_PAGE) + assert await page_2.evaluate(fetch_function, "1") == "1" + assert await page_2.evaluate(fetch_function, "2") == "2" + assert await page_2.evaluate(fetch_function, "3") == "3" + with pytest.raises(Exception): + await page_2.evaluate(fetch_function, "4") + + +async def test_should_disambiguate_by_header( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + server.set_route( + "/echo", lambda req: (req.write(req.getHeader("baz").encode()), req.finish()) + ) + fetch_function = """ + async (bazValue) => { + const response = await fetch('/echo', { + method: 'POST', + body: '', + headers: { + foo: 'foo-value', + bar: 'bar-value', + baz: bazValue, + } + }); + return await response.text(); + }; + """ + har_path = tmpdir / "har.zip" + context_1 = await browser.new_context( + record_har_mode="minimal", record_har_path=har_path + ) + page_1 = await context_1.new_page() + await page_1.goto(server.EMPTY_PAGE) + + assert await page_1.evaluate(fetch_function, "baz1") == "baz1" + assert await page_1.evaluate(fetch_function, "baz2") == "baz2" + assert await page_1.evaluate(fetch_function, "baz3") == "baz3" + await context_1.close() + + context_2 = await browser.new_context() + await context_2.route_from_har(har=har_path) + page_2 = await context_2.new_page() + await page_2.goto(server.EMPTY_PAGE) + assert await page_2.evaluate(fetch_function, "baz1") == "baz1" + assert await page_2.evaluate(fetch_function, "baz2") == "baz2" + assert await page_2.evaluate(fetch_function, "baz3") == "baz3" + assert await page_2.evaluate(fetch_function, "baz4") == "baz1" diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index 479c97e0a..d36dcbef7 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -17,8 +17,11 @@ import re import zipfile from pathlib import Path +from typing import Any, cast -from playwright.sync_api import Browser +import pytest + +from playwright.sync_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -211,3 +214,298 @@ def test_should_filter_by_regexp(browser: Browser, server: Server, tmpdir: str) log = data["log"] assert len(log["entries"]) == 1 assert log["entries"][0]["request"]["url"].endswith("har.html") + + +def test_should_context_route_from_har_matching_the_method_and_following_redirects( + context: BrowserContext, assetdir: Path +) -> None: + context.route_from_har(har=assetdir / "har-fulfill.har") + page = context.new_page() + page.goto("http://no.playwright/") + # HAR contains a redirect for the script that should be followed automatically. + assert page.evaluate("window.value") == "foo" + # HAR contains a POST for the css file that should not be used. + expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +def test_should_page_route_from_har_matching_the_method_and_following_redirects( + page: Page, assetdir: Path +) -> None: + page.route_from_har(har=assetdir / "har-fulfill.har") + page.goto("http://no.playwright/") + # HAR contains a redirect for the script that should be followed automatically. + assert page.evaluate("window.value") == "foo" + # HAR contains a POST for the css file that should not be used. + expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +def test_fallback_continue_should_continue_when_not_found_in_har( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har(har=assetdir / "har-fulfill.har", not_found="fallback") + page = context.new_page() + page.goto(server.PREFIX + "/one-style.html") + expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + + +def test_by_default_should_abort_requests_not_found_in_har( + context: BrowserContext, + server: Server, + assetdir: Path, + is_chromium: bool, + is_webkit: bool, +) -> None: + context.route_from_har(har=assetdir / "har-fulfill.har") + page = context.new_page() + + with pytest.raises(Error) as exc_info: + page.goto(server.EMPTY_PAGE) + assert exc_info.value + if is_chromium: + assert "net::ERR_FAILED" in exc_info.value.message + elif is_webkit: + assert "Blocked by Web Inspector" in exc_info.value.message + else: + assert "NS_ERROR_FAILURE" in exc_info.value.message + + +def test_fallback_continue_should_continue_requests_on_bad_har( + context: BrowserContext, server: Server, tmpdir: Path +) -> None: + path_to_invalid_har = tmpdir / "invalid.har" + with path_to_invalid_har.open("w") as f: + json.dump({"log": {}}, f) + context.route_from_har(har=path_to_invalid_har, not_found="fallback") + page = context.new_page() + page.goto(server.PREFIX + "/one-style.html") + expect(page.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + + +def test_should_only_handle_requests_matching_url_filter( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har( + har=assetdir / "har-fulfill.har", not_found="fallback", url="**/*.js" + ) + page = context.new_page() + + def handler(route: Route) -> None: + assert route.request.url == "http://no.playwright/" + route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + context.route("http://no.playwright/", handler) + page.goto("http://no.playwright/") + assert page.evaluate("window.value") == "foo" + expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)") + + +def test_should_only_handle_requests_matching_url_filter_no_fallback( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js") + page = context.new_page() + + def handler(route: Route) -> None: + assert route.request.url == "http://no.playwright/" + route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + context.route("http://no.playwright/", handler) + page.goto("http://no.playwright/") + assert page.evaluate("window.value") == "foo" + expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)") + + +def test_should_only_handle_requests_matching_url_filter_no_fallback_page( + page: Page, server: Server, assetdir: Path +) -> None: + page.route_from_har(har=assetdir / "har-fulfill.har", url="**/*.js") + + def handler(route: Route) -> None: + assert route.request.url == "http://no.playwright/" + route.fulfill( + status=200, + content_type="text/html", + body='
hello
', + ) + + page.route("http://no.playwright/", handler) + page.goto("http://no.playwright/") + assert page.evaluate("window.value") == "foo" + expect(page.locator("body")).to_have_css("background-color", "rgba(0, 0, 0, 0)") + + +def test_should_support_regex_filter( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har( + har=assetdir / "har-fulfill.har", + url=re.compile(r".*(\.js|.*\.css|no.playwright\/)"), + ) + page = context.new_page() + page.goto("http://no.playwright/") + assert page.evaluate("window.value") == "foo" + expect(page.locator("body")).to_have_css("background-color", "rgb(255, 0, 0)") + + +def test_should_go_back_to_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = context.new_page() + page.goto("https://theverge.com/") + page.goto(server.EMPTY_PAGE) + expect(page).to_have_url(server.EMPTY_PAGE) + + response = page.go_back() + assert response + expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert page.evaluate("window.location.href") == "https://www.theverge.com/" + + +def test_should_go_forward_to_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = context.new_page() + page.goto("https://theverge.com/") + page.goto(server.EMPTY_PAGE) + expect(page).to_have_url(server.EMPTY_PAGE) + page.goto("https://theverge.com/") + expect(page).to_have_url("https://www.theverge.com/") + page.go_back() + expect(page).to_have_url(server.EMPTY_PAGE) + response = page.go_forward() + assert response + expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert page.evaluate("window.location.href") == "https://www.theverge.com/" + + +def test_should_reload_redirected_navigation( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har( + har=assetdir / "har-redirect.har", url=re.compile(r"/.*theverge.*/") + ) + page = context.new_page() + page.goto("https://theverge.com/") + expect(page).to_have_url("https://www.theverge.com/") + response = page.reload() + assert response + expect(page).to_have_url("https://www.theverge.com/") + assert response.request.url == "https://www.theverge.com/" + assert page.evaluate("window.location.href") == "https://www.theverge.com/" + + +def test_should_fulfill_from_har_with_content_in_a_file( + context: BrowserContext, server: Server, assetdir: Path +) -> None: + context.route_from_har(har=assetdir / "har-sha1.har") + page = context.new_page() + page.goto("http://no.playwright/") + assert page.content() == "Hello, world" + + +def test_should_round_trip_har_zip( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + + har_path = tmpdir / "har.zip" + context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) + page_1 = context_1.new_page() + page_1.goto(server.PREFIX + "/one-style.html") + context_1.close() + + context_2 = browser.new_context() + context_2.route_from_har(har=har_path, not_found="abort") + page_2 = context_2.new_page() + page_2.goto(server.PREFIX + "/one-style.html") + assert "hello, world!" in page_2.content() + expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + + +def test_should_round_trip_har_with_post_data( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + server.set_route( + "/echo", lambda req: (req.write(cast(Any, req).post_body), req.finish()) + ) + fetch_function = """ + async (body) => { + const response = await fetch('/echo', { method: 'POST', body }); + return response.text(); + }; + """ + har_path = tmpdir / "har.zip" + context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) + page_1 = context_1.new_page() + page_1.goto(server.EMPTY_PAGE) + + assert page_1.evaluate(fetch_function, "1") == "1" + assert page_1.evaluate(fetch_function, "2") == "2" + assert page_1.evaluate(fetch_function, "3") == "3" + context_1.close() + + context_2 = browser.new_context() + context_2.route_from_har(har=har_path, not_found="abort") + page_2 = context_2.new_page() + page_2.goto(server.EMPTY_PAGE) + assert page_2.evaluate(fetch_function, "1") == "1" + assert page_2.evaluate(fetch_function, "2") == "2" + assert page_2.evaluate(fetch_function, "3") == "3" + with pytest.raises(Exception): + page_2.evaluate(fetch_function, "4") + + +def test_should_disambiguate_by_header( + browser: Browser, server: Server, assetdir: Path, tmpdir: Path +) -> None: + server.set_route( + "/echo", + lambda req: (req.write(cast(str, req.getHeader("baz")).encode()), req.finish()), + ) + fetch_function = """ + async (bazValue) => { + const response = await fetch('/echo', { + method: 'POST', + body: '', + headers: { + foo: 'foo-value', + bar: 'bar-value', + baz: bazValue, + } + }); + return response.text(); + }; + """ + har_path = tmpdir / "har.zip" + context_1 = browser.new_context(record_har_mode="minimal", record_har_path=har_path) + page_1 = context_1.new_page() + page_1.goto(server.EMPTY_PAGE) + + assert page_1.evaluate(fetch_function, "baz1") == "baz1" + assert page_1.evaluate(fetch_function, "baz2") == "baz2" + assert page_1.evaluate(fetch_function, "baz3") == "baz3" + context_1.close() + + context_2 = browser.new_context() + context_2.route_from_har(har=har_path) + page_2 = context_2.new_page() + page_2.goto(server.EMPTY_PAGE) + assert page_2.evaluate(fetch_function, "baz1") == "baz1" + assert page_2.evaluate(fetch_function, "baz2") == "baz2" + assert page_2.evaluate(fetch_function, "baz3") == "baz3" + assert page_2.evaluate(fetch_function, "baz4") == "baz1" From 25dc4e5edb8af9521669ff21c6c1ea1299e5ab5a Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 27 Jun 2022 21:20:30 -0700 Subject: [PATCH 2/8] replace log comments --- playwright/_impl/_har_router.py | 11 +++++++---- playwright/_impl/_local_utils.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py index 8b061fb97..4566e4ea3 100644 --- a/playwright/_impl/_har_router.py +++ b/playwright/_impl/_har_router.py @@ -56,8 +56,12 @@ async def _handle(self, route: "Route") -> None: ) action = response["action"] if action == "redirect": - # debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); redirect_url = response["redirectURL"] + self._local_utils.log( + "debug", + "api", + f"HAR: {route.request.url} redirected to {redirect_url}", + ) assert redirect_url await route._redirected_navigation_request(redirect_url) return @@ -76,9 +80,8 @@ async def _handle(self, route: "Route") -> None: return if action == "error": - pass - # debugLogger.log('api', 'HAR: ' + response.message!); - # Report the error, but fall through to the default handler. + self._local_utils.log("debug", "api", f'HAR: {response.get("message")}') + # Report the error, but fall through to the default handler. if self._not_found_action == "abort": await route.abort() diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index a9d9395cc..aee834f1c 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -53,3 +53,20 @@ async def har_lookup( async def har_close(self, harId: str) -> None: params = locals_to_params(locals()) await self._channel.send("harClose", params) + + def log(self, level: str, method: str, message: str) -> None: + try: + self._channel._connection.wrap_api_call( + lambda: self._channel.send_no_reply( + method, + { + level: { + "phase": "log", + "message": message, + }, + }, + ), + True, + ) + except Exception: + pass From 54ab5b37aec7ff30a380006a2e3eb7c8fb6bab91 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 27 Jun 2022 21:29:31 -0700 Subject: [PATCH 3/8] stop pre-commit from corrupting our HAR files --- .pre-commit-config.yaml | 1 + tests/assets/har-sha1-main-response.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d694df95f..ee128c8f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer + exclude: tests/assets/har-sha1-main-response.txt - id: check-yaml - id: check-toml - id: requirements-txt-fixer diff --git a/tests/assets/har-sha1-main-response.txt b/tests/assets/har-sha1-main-response.txt index a5c196677..dbe9dba55 100644 --- a/tests/assets/har-sha1-main-response.txt +++ b/tests/assets/har-sha1-main-response.txt @@ -1 +1 @@ -Hello, world +Hello, world \ No newline at end of file From 3a53f4b83a8a748b7ae0f14c0f5b085e6fc0cb3d Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 27 Jun 2022 22:15:17 -0700 Subject: [PATCH 4/8] clean up bots and tests --- tests/async/test_accessibility.py | 19 ++++++++++--------- tests/async/test_har.py | 2 ++ tests/sync/test_accessibility.py | 22 ++++++++++------------ tests/sync/test_har.py | 2 ++ 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/async/test_accessibility.py b/tests/async/test_accessibility.py index 443e66462..6ffe2c041 100644 --- a/tests/async/test_accessibility.py +++ b/tests/async/test_accessibility.py @@ -209,28 +209,29 @@ async def test_accessibility_filtering_children_of_leaf_nodes_rich_text_editable ): await page.set_content( """ -
- Edit this image: my fake image -
""" +
+ Edit this image: my fake image +
+ """ ) if is_firefox: golden = { - "role": "textbox", + "role": "section", "name": "", - "value": "Edit this image: my fake image", - "children": [{"role": "text", "name": "my fake image"}], + "children": [ + {"role": "text leaf", "name": "Edit this image: "}, + {"role": "text", "name": "my fake image"}, + ], } else: golden = { - "role": "textbox", + "role": "generic", "name": "", - "multiline": True, "value": "Edit this image: ", "children": [ {"role": "text", "name": "Edit this image:"}, {"role": "img", "name": "my fake image"}, ], - "value": "Edit this image: ", } snapshot = await page.accessibility.snapshot() assert snapshot["children"][0] == golden diff --git a/tests/async/test_har.py b/tests/async/test_har.py index 33c5c593f..7b7a75b14 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -20,6 +20,7 @@ from pathlib import Path import pytest +from flaky import flaky from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -436,6 +437,7 @@ async def test_should_go_back_to_redirected_navigation( assert await page.evaluate("window.location.href") == "https://www.theverge.com/" +@flaky # Flaky upstream async def test_should_go_forward_to_redirected_navigation( context: BrowserContext, server: Server, assetdir: Path ) -> None: diff --git a/tests/sync/test_accessibility.py b/tests/sync/test_accessibility.py index 9c3d201c6..14b1347bc 100644 --- a/tests/sync/test_accessibility.py +++ b/tests/sync/test_accessibility.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict - import pytest from playwright.sync_api import Page @@ -218,29 +216,29 @@ def test_accessibility_filtering_children_of_leaf_nodes_rich_text_editable_field ) -> None: page.set_content( """ -
- Edit this image: my fake image -
""" +
+ Edit this image: my fake image +
+ """ ) - golden: Dict[str, Any] if is_firefox: golden = { - "role": "textbox", + "role": "section", "name": "", - "value": "Edit this image: my fake image", - "children": [{"role": "text", "name": "my fake image"}], + "children": [ + {"role": "text leaf", "name": "Edit this image: "}, + {"role": "text", "name": "my fake image"}, + ], } else: golden = { - "role": "textbox", + "role": "generic", "name": "", - "multiline": True, "value": "Edit this image: ", "children": [ {"role": "text", "name": "Edit this image:"}, {"role": "img", "name": "my fake image"}, ], - "value": "Edit this image: ", } snapshot = page.accessibility.snapshot() assert snapshot diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index d36dcbef7..0e13fd90e 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -20,6 +20,7 @@ from typing import Any, cast import pytest +from flaky import flaky from playwright.sync_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -373,6 +374,7 @@ def test_should_go_back_to_redirected_navigation( assert page.evaluate("window.location.href") == "https://www.theverge.com/" +@flaky # Flaky upstream def test_should_go_forward_to_redirected_navigation( context: BrowserContext, server: Server, assetdir: Path ) -> None: From 94d0577281d8d977d4f4f68e82a0db6075223271 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Mon, 27 Jun 2022 23:04:51 -0700 Subject: [PATCH 5/8] update test based on upstream --- tests/async/test_browsercontext_cookies.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/async/test_browsercontext_cookies.py b/tests/async/test_browsercontext_cookies.py index e0ac33740..b81037307 100644 --- a/tests/async/test_browsercontext_cookies.py +++ b/tests/async/test_browsercontext_cookies.py @@ -57,13 +57,23 @@ async def test_should_get_a_non_session_cookie(context, page, server, is_chromiu date, ) assert document_cookie == "username=John Doe" - assert await context.cookies() == [ + cookies = await context.cookies() + expires = cookies[0]["expires"] + del cookies[0]["expires"] + # Browsers start to cap cookies with 400 days max expires value. + # See https://github.com/httpwg/http-extensions/pull/1732 + # Chromium patch: https://chromium.googlesource.com/chromium/src/+/aaa5d2b55478eac2ee642653dcd77a50ac3faff6 + # We want to make sure that expires date is at least 400 days in future. + # We use 355 to prevent flakes and not think about timezones! + assert datetime.datetime.fromtimestamp( + expires + ) - datetime.datetime.now() > datetime.timedelta(days=355) + assert cookies == [ { "name": "username", "value": "John Doe", "domain": "localhost", "path": "/", - "expires": date / 1000, "httpOnly": False, "secure": False, "sameSite": "Lax" if is_chromium else "None", From 37e92aaec128e7787e0e20921d7b4afc93bd74c1 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Tue, 28 Jun 2022 00:24:05 -0700 Subject: [PATCH 6/8] increase the flaky threshold --- tests/async/test_har.py | 2 +- tests/sync/test_har.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/async/test_har.py b/tests/async/test_har.py index 7b7a75b14..65c2cb395 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -437,7 +437,7 @@ async def test_should_go_back_to_redirected_navigation( assert await page.evaluate("window.location.href") == "https://www.theverge.com/" -@flaky # Flaky upstream +@flaky(max_runs=5) # Flaky upstream async def test_should_go_forward_to_redirected_navigation( context: BrowserContext, server: Server, assetdir: Path ) -> None: diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index 0e13fd90e..0cb43be9b 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -374,7 +374,7 @@ def test_should_go_back_to_redirected_navigation( assert page.evaluate("window.location.href") == "https://www.theverge.com/" -@flaky # Flaky upstream +@flaky(max_runs=5) # Flaky upstream def test_should_go_forward_to_redirected_navigation( context: BrowserContext, server: Server, assetdir: Path ) -> None: From 5eaf5ab06464e0e9f69951082ed797821439189d Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Tue, 28 Jun 2022 08:07:30 -0700 Subject: [PATCH 7/8] Remove logs --- playwright/_impl/_har_router.py | 11 ++++------- playwright/_impl/_local_utils.py | 17 ----------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py index 4566e4ea3..8b061fb97 100644 --- a/playwright/_impl/_har_router.py +++ b/playwright/_impl/_har_router.py @@ -56,12 +56,8 @@ async def _handle(self, route: "Route") -> None: ) action = response["action"] if action == "redirect": + # debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); redirect_url = response["redirectURL"] - self._local_utils.log( - "debug", - "api", - f"HAR: {route.request.url} redirected to {redirect_url}", - ) assert redirect_url await route._redirected_navigation_request(redirect_url) return @@ -80,8 +76,9 @@ async def _handle(self, route: "Route") -> None: return if action == "error": - self._local_utils.log("debug", "api", f'HAR: {response.get("message")}') - # Report the error, but fall through to the default handler. + pass + # debugLogger.log('api', 'HAR: ' + response.message!); + # Report the error, but fall through to the default handler. if self._not_found_action == "abort": await route.abort() diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index aee834f1c..a9d9395cc 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -53,20 +53,3 @@ async def har_lookup( async def har_close(self, harId: str) -> None: params = locals_to_params(locals()) await self._channel.send("harClose", params) - - def log(self, level: str, method: str, message: str) -> None: - try: - self._channel._connection.wrap_api_call( - lambda: self._channel.send_no_reply( - method, - { - level: { - "phase": "log", - "message": message, - }, - }, - ), - True, - ) - except Exception: - pass From e060383569d769d6fe920305ad6a5d5ab8e7d4c4 Mon Sep 17 00:00:00 2001 From: "Ross A. Wollman" Date: Tue, 28 Jun 2022 10:56:27 -0700 Subject: [PATCH 8/8] check in changes from linter --- playwright/_impl/_har_router.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py index 8b061fb97..18e8b62e9 100644 --- a/playwright/_impl/_har_router.py +++ b/playwright/_impl/_har_router.py @@ -56,7 +56,6 @@ async def _handle(self, route: "Route") -> None: ) action = response["action"] if action == "redirect": - # debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); redirect_url = response["redirectURL"] assert redirect_url await route._redirected_navigation_request(redirect_url) @@ -77,8 +76,7 @@ async def _handle(self, route: "Route") -> None: if action == "error": pass - # debugLogger.log('api', 'HAR: ' + response.message!); - # Report the error, but fall through to the default handler. + # Report the error, but fall through to the default handler. if self._not_found_action == "abort": await route.abort()