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/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..18e8b62e9 --- /dev/null +++ b/playwright/_impl/_har_router.py @@ -0,0 +1,104 @@ +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": + 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 + # 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..dbe9dba55 --- /dev/null +++ b/tests/assets/har-sha1-main-response.txt @@ -0,0 +1 @@ +Hello, world \ No newline at end of file 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_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_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", 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..65c2cb395 100644 --- a/tests/async/test_har.py +++ b/tests/async/test_har.py @@ -12,12 +12,17 @@ # 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 flaky import flaky + +from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -225,3 +230,350 @@ 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/" + + +@flaky(max_runs=5) # Flaky upstream +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_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 479c97e0a..0cb43be9b 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -17,8 +17,12 @@ import re import zipfile from pathlib import Path +from typing import Any, cast -from playwright.sync_api import Browser +import pytest +from flaky import flaky + +from playwright.sync_api import Browser, BrowserContext, Error, Page, Route, expect from tests.server import Server @@ -211,3 +215,299 @@ 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/" + + +@flaky(max_runs=5) # Flaky upstream +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"