Skip to content

Commit 36ff52e

Browse files
authored
chore: port update option for routeFromHar (#1392)
Fixes #1389. Ports: - [x] microsoft/playwright@6a8d835 (chore: allow updating har while routing (#15197))
1 parent b2e3b93 commit 36ff52e

File tree

11 files changed

+340
-55
lines changed

11 files changed

+340
-55
lines changed

playwright/_impl/_browser.py

+9-30
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import json
1717
from pathlib import Path
1818
from types import SimpleNamespace
19-
from typing import TYPE_CHECKING, Any, Dict, List, Pattern, Union, cast
19+
from typing import TYPE_CHECKING, Dict, List, Pattern, Union, cast
2020

2121
from playwright._impl._api_structures import (
2222
Geolocation,
@@ -38,11 +38,10 @@
3838
async_readfile,
3939
is_safe_close_error,
4040
locals_to_params,
41+
prepare_record_har_options,
4142
)
42-
from playwright._impl._local_utils import LocalUtils
4343
from playwright._impl._network import serialize_headers
4444
from playwright._impl._page import Page
45-
from playwright._impl._str_utils import escape_regex_flags
4645

4746
if TYPE_CHECKING: # pragma: no cover
4847
from playwright._impl._browser_type import BrowserType
@@ -64,7 +63,6 @@ def __init__(
6463
self._should_close_connection_on_close = False
6564

6665
self._contexts: List[BrowserContext] = []
67-
_utils: LocalUtils
6866
self._channel.on("close", lambda _: self._on_close())
6967

7068
def __repr__(self) -> str:
@@ -131,6 +129,7 @@ async def new_context(
131129
self._contexts.append(context)
132130
context._browser = self
133131
context._options = params
132+
context._set_browser_type(self._browser_type)
134133
return context
135134

136135
async def new_page(
@@ -177,6 +176,11 @@ async def new_page(
177176
context._owner_page = page
178177
return page
179178

179+
def _set_browser_type(self, browser_type: "BrowserType") -> None:
180+
self._browser_type = browser_type
181+
for context in self._contexts:
182+
context._set_browser_type(browser_type)
183+
180184
async def close(self) -> None:
181185
if self._is_closed_or_closing:
182186
return
@@ -224,32 +228,7 @@ async def normalize_context_params(is_sync: bool, params: Dict) -> None:
224228
if "extraHTTPHeaders" in params:
225229
params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"])
226230
if "recordHarPath" in params:
227-
recordHar: Dict[str, Any] = {"path": str(params["recordHarPath"])}
228-
params["recordHar"] = recordHar
229-
if "recordHarUrlFilter" in params:
230-
opt = params["recordHarUrlFilter"]
231-
if isinstance(opt, str):
232-
params["recordHar"]["urlGlob"] = opt
233-
if isinstance(opt, Pattern):
234-
params["recordHar"]["urlRegexSource"] = opt.pattern
235-
params["recordHar"]["urlRegexFlags"] = escape_regex_flags(opt)
236-
del params["recordHarUrlFilter"]
237-
if "recordHarMode" in params:
238-
params["recordHar"]["mode"] = params["recordHarMode"]
239-
del params["recordHarMode"]
240-
241-
new_content_api = None
242-
old_content_api = None
243-
if "recordHarContent" in params:
244-
new_content_api = params["recordHarContent"]
245-
del params["recordHarContent"]
246-
if "recordHarOmitContent" in params:
247-
old_content_api = params["recordHarOmitContent"]
248-
del params["recordHarOmitContent"]
249-
content = new_content_api or ("omit" if old_content_api else None)
250-
if content:
251-
params["recordHar"]["content"] = content
252-
231+
params["recordHar"] = prepare_record_har_options(params)
253232
del params["recordHarPath"]
254233
if "recordVideoDir" in params:
255234
params["recordVideo"] = {"dir": str(params["recordVideoDir"])}

playwright/_impl/_browser_context.py

+68-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@
1616
import json
1717
from pathlib import Path
1818
from types import SimpleNamespace
19-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union, cast
19+
from typing import (
20+
TYPE_CHECKING,
21+
Any,
22+
Callable,
23+
Dict,
24+
List,
25+
Optional,
26+
Pattern,
27+
Set,
28+
Union,
29+
cast,
30+
)
2031

2132
from playwright._impl._api_structures import (
2233
Cookie,
@@ -37,6 +48,7 @@
3748
from playwright._impl._frame import Frame
3849
from playwright._impl._har_router import HarRouter
3950
from playwright._impl._helper import (
51+
HarRecordingMetadata,
4052
RouteFromHarNotFoundPolicy,
4153
RouteHandler,
4254
RouteHandlerCallback,
@@ -47,6 +59,7 @@
4759
async_writefile,
4860
is_safe_close_error,
4961
locals_to_params,
62+
prepare_record_har_options,
5063
to_impl,
5164
)
5265
from playwright._impl._network import Request, Response, Route, serialize_headers
@@ -56,6 +69,7 @@
5669

5770
if TYPE_CHECKING: # pragma: no cover
5871
from playwright._impl._browser import Browser
72+
from playwright._impl._browser_type import BrowserType
5973

6074

6175
class BrowserContext(ChannelOwner):
@@ -85,6 +99,7 @@ def __init__(
8599
self._background_pages: Set[Page] = set()
86100
self._service_workers: Set[Worker] = set()
87101
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
102+
self._har_recorders: Dict[str, HarRecordingMetadata] = {}
88103
self._request: APIRequestContext = from_channel(
89104
initializer["APIRequestContext"]
90105
)
@@ -201,6 +216,14 @@ def pages(self) -> List[Page]:
201216
def browser(self) -> Optional["Browser"]:
202217
return self._browser
203218

219+
def _set_browser_type(self, browser_type: "BrowserType") -> None:
220+
self._browser_type = browser_type
221+
if self._options.get("recordHar"):
222+
self._har_recorders[""] = {
223+
"path": self._options["recordHar"]["path"],
224+
"content": self._options["recordHar"].get("content"),
225+
}
226+
204227
async def new_page(self) -> Page:
205228
if self._owner_page:
206229
raise Error("Please use browser.new_context()")
@@ -294,12 +317,37 @@ async def unroute(
294317
if len(self._routes) == 0:
295318
await self._disable_interception()
296319

320+
async def _record_into_har(
321+
self,
322+
har: Union[Path, str],
323+
page: Optional[Page] = None,
324+
url: Union[Pattern, str] = None,
325+
) -> None:
326+
params = {
327+
"options": prepare_record_har_options(
328+
{
329+
"recordHarPath": har,
330+
"recordHarContent": "attach",
331+
"recordHarMode": "minimal",
332+
"recordHarUrlFilter": url,
333+
}
334+
)
335+
}
336+
if page:
337+
params["page"] = page._channel
338+
har_id = await self._channel.send("harStart", params)
339+
self._har_recorders[har_id] = {"path": str(har), "content": "attach"}
340+
297341
async def route_from_har(
298342
self,
299343
har: Union[Path, str],
300-
url: URLMatch = None,
344+
url: Union[Pattern, str] = None,
301345
not_found: RouteFromHarNotFoundPolicy = None,
346+
update: bool = None,
302347
) -> None:
348+
if update:
349+
await self._record_into_har(har=har, page=None, url=url)
350+
return
303351
router = await HarRouter.create(
304352
local_utils=self._connection.local_utils,
305353
file=str(har),
@@ -338,13 +386,26 @@ def _on_close(self) -> None:
338386

339387
async def close(self) -> None:
340388
try:
341-
if self._options.get("recordHar"):
389+
for har_id, params in self._har_recorders.items():
342390
har = cast(
343-
Artifact, from_channel(await self._channel.send("harExport"))
344-
)
345-
await har.save_as(
346-
cast(Dict[str, str], self._options["recordHar"])["path"]
391+
Artifact,
392+
from_channel(
393+
await self._channel.send("harExport", {"harId": har_id})
394+
),
347395
)
396+
# Server side will compress artifact if content is attach or if file is .zip.
397+
is_compressed = params.get("content") == "attach" or params[
398+
"path"
399+
].endswith(".zip")
400+
need_compressed = params["path"].endswith(".zip")
401+
if is_compressed and not need_compressed:
402+
tmp_path = params["path"] + ".tmp"
403+
await har.save_as(tmp_path)
404+
await self._connection.local_utils.har_unzip(
405+
zipFile=tmp_path, harFile=params["path"]
406+
)
407+
else:
408+
await har.save_as(params["path"])
348409
await har.delete()
349410
await self._channel.send("close")
350411
await self._closed_future

playwright/_impl/_browser_type.py

+4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ async def launch(
9292
browser = cast(
9393
Browser, from_channel(await self._channel.send("launch", params))
9494
)
95+
browser._set_browser_type(self)
9596
return browser
9697

9798
async def launch_persistent_context(
@@ -154,6 +155,7 @@ async def launch_persistent_context(
154155
from_channel(await self._channel.send("launchPersistentContext", params)),
155156
)
156157
context._options = params
158+
context._set_browser_type(self)
157159
return context
158160

159161
async def connect_over_cdp(
@@ -174,6 +176,7 @@ async def connect_over_cdp(
174176
if default_context:
175177
browser._contexts.append(default_context)
176178
default_context._browser = browser
179+
browser._set_browser_type(self)
177180
return browser
178181

179182
async def connect(
@@ -230,6 +233,7 @@ def handle_transport_close() -> None:
230233

231234
transport.once("close", handle_transport_close)
232235

236+
browser._set_browser_type(self)
233237
return browser
234238

235239

playwright/_impl/_helper.py

+35
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
from playwright._impl._api_structures import NameValue
4343
from playwright._impl._api_types import Error, TimeoutError
44+
from playwright._impl._str_utils import escape_regex_flags
4445

4546
if sys.version_info >= (3, 8): # pragma: no cover
4647
from typing import Literal, TypedDict
@@ -85,6 +86,40 @@ class FallbackOverrideParameters(TypedDict, total=False):
8586
postData: Optional[Union[str, bytes]]
8687

8788

89+
class HarRecordingMetadata(TypedDict, total=False):
90+
path: str
91+
content: Optional[HarContentPolicy]
92+
93+
94+
def prepare_record_har_options(params: Dict) -> Dict[str, Any]:
95+
out_params: Dict[str, Any] = {"path": str(params["recordHarPath"])}
96+
if "recordHarUrlFilter" in params:
97+
opt = params["recordHarUrlFilter"]
98+
if isinstance(opt, str):
99+
out_params["urlGlob"] = opt
100+
if isinstance(opt, Pattern):
101+
out_params["urlRegexSource"] = opt.pattern
102+
out_params["urlRegexFlags"] = escape_regex_flags(opt)
103+
del params["recordHarUrlFilter"]
104+
if "recordHarMode" in params:
105+
out_params["mode"] = params["recordHarMode"]
106+
del params["recordHarMode"]
107+
108+
new_content_api = None
109+
old_content_api = None
110+
if "recordHarContent" in params:
111+
new_content_api = params["recordHarContent"]
112+
del params["recordHarContent"]
113+
if "recordHarOmitContent" in params:
114+
old_content_api = params["recordHarOmitContent"]
115+
del params["recordHarOmitContent"]
116+
content = new_content_api or ("omit" if old_content_api else None)
117+
if content:
118+
out_params["content"] = content
119+
120+
return out_params
121+
122+
88123
class ParsedMessageParams(TypedDict):
89124
type: str
90125
guid: str

playwright/_impl/_local_utils.py

+4
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ async def har_lookup(
5353
async def har_close(self, harId: str) -> None:
5454
params = locals_to_params(locals())
5555
await self._channel.send("harClose", params)
56+
57+
async def har_unzip(self, zipFile: str, harFile: str) -> None:
58+
params = locals_to_params(locals())
59+
await self._channel.send("harUnzip", params)

playwright/_impl/_page.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -605,9 +605,13 @@ async def unroute(
605605
async def route_from_har(
606606
self,
607607
har: Union[Path, str],
608-
url: URLMatch = None,
608+
url: Union[Pattern, str] = None,
609609
not_found: RouteFromHarNotFoundPolicy = None,
610+
update: bool = None,
610611
) -> None:
612+
if update:
613+
await self._browser_context._record_into_har(har=har, page=self, url=url)
614+
return
611615
router = await HarRouter.create(
612616
local_utils=self._connection.local_utils,
613617
file=str(har),

playwright/async_api/_generated.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -7744,8 +7744,9 @@ async def route_from_har(
77447744
self,
77457745
har: typing.Union[pathlib.Path, str],
77467746
*,
7747-
url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None,
7748-
not_found: Literal["abort", "fallback"] = None
7747+
url: typing.Union[str, typing.Pattern] = None,
7748+
not_found: Literal["abort", "fallback"] = None,
7749+
update: bool = None
77497750
) -> NoneType:
77507751
"""Page.route_from_har
77517752

@@ -7761,19 +7762,21 @@ async def route_from_har(
77617762
har : Union[pathlib.Path, str]
77627763
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a
77637764
relative path, then it is resolved relative to the current working directory.
7764-
url : Union[Callable[[str], bool], Pattern, str, NoneType]
7765+
url : Union[Pattern, str, NoneType]
77657766
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
77667767
will be surved from the HAR file. If not specified, all requests are served from the HAR file.
77677768
not_found : Union["abort", "fallback", NoneType]
77687769
- If set to 'abort' any request not found in the HAR file will be aborted.
77697770
- If set to 'fallback' missing requests will be sent to the network.
77707771

77717772
Defaults to abort.
7773+
update : Union[bool, NoneType]
7774+
If specified, updates the given HAR with the actual network information instead of serving from file.
77727775
"""
77737776

77747777
return mapping.from_maybe_impl(
77757778
await self._impl_obj.route_from_har(
7776-
har=har, url=self._wrap_handler(url), not_found=not_found
7779+
har=har, url=url, not_found=not_found, update=update
77777780
)
77787781
)
77797782

@@ -10477,8 +10480,9 @@ async def route_from_har(
1047710480
self,
1047810481
har: typing.Union[pathlib.Path, str],
1047910482
*,
10480-
url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None,
10481-
not_found: Literal["abort", "fallback"] = None
10483+
url: typing.Union[str, typing.Pattern] = None,
10484+
not_found: Literal["abort", "fallback"] = None,
10485+
update: bool = None
1048210486
) -> NoneType:
1048310487
"""BrowserContext.route_from_har
1048410488

@@ -10494,19 +10498,21 @@ async def route_from_har(
1049410498
har : Union[pathlib.Path, str]
1049510499
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a
1049610500
relative path, then it is resolved relative to the current working directory.
10497-
url : Union[Callable[[str], bool], Pattern, str, NoneType]
10501+
url : Union[Pattern, str, NoneType]
1049810502
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
1049910503
will be surved from the HAR file. If not specified, all requests are served from the HAR file.
1050010504
not_found : Union["abort", "fallback", NoneType]
1050110505
- If set to 'abort' any request not found in the HAR file will be aborted.
1050210506
- If set to 'fallback' falls through to the next route handler in the handler chain.
1050310507

1050410508
Defaults to abort.
10509+
update : Union[bool, NoneType]
10510+
If specified, updates the given HAR with the actual network information instead of serving from file.
1050510511
"""
1050610512

1050710513
return mapping.from_maybe_impl(
1050810514
await self._impl_obj.route_from_har(
10509-
har=har, url=self._wrap_handler(url), not_found=not_found
10515+
har=har, url=url, not_found=not_found, update=update
1051010516
)
1051110517
)
1051210518

0 commit comments

Comments
 (0)