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": "
Heyhello
"
+ },
+ "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:

-
"""
+
+ Edit this 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:

-
"""
+
+ Edit this 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"