Skip to content

Commit 91c4fc3

Browse files
committed
chore(roll): roll Playwright to 1.45.0-alpha-2024-06-14
1 parent f8c8882 commit 91c4fc3

20 files changed

+1423
-84
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
44

55
| | Linux | macOS | Windows |
66
| :--- | :---: | :---: | :---: |
7-
| Chromium <!-- GEN:chromium-version -->125.0.6422.26<!-- GEN:stop --> ||||
7+
| Chromium <!-- GEN:chromium-version -->127.0.6533.5<!-- GEN:stop --> ||||
88
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> ||||
9-
| Firefox <!-- GEN:firefox-version -->125.0.1<!-- GEN:stop --> ||||
9+
| Firefox <!-- GEN:firefox-version -->127.0<!-- GEN:stop --> ||||
1010

1111
## Documentation
1212

playwright/_impl/_api_structures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class HttpCredentials(TypedDict, total=False):
6363
username: str
6464
password: str
6565
origin: Optional[str]
66+
send: Optional[Literal["always", "unauthorized"]]
6667

6768

6869
class LocalStorageEntry(TypedDict):

playwright/_impl/_browser_context.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from playwright._impl._artifact import Artifact
4141
from playwright._impl._cdp_session import CDPSession
42+
from playwright._impl._clock import Clock
4243
from playwright._impl._connection import (
4344
ChannelOwner,
4445
from_channel,
@@ -114,6 +115,7 @@ def __init__(
114115
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
115116
self._har_recorders: Dict[str, HarRecordingMetadata] = {}
116117
self._request: APIRequestContext = from_channel(initializer["requestContext"])
118+
self._clock = Clock(self)
117119
self._channel.on(
118120
"bindingCall",
119121
lambda params: self._on_binding(from_channel(params["binding"])),
@@ -519,6 +521,10 @@ async def close(self, reason: str = None) -> None:
519521
self._close_reason = reason
520522
self._close_was_called = True
521523

524+
await self._channel._connection.wrap_api_call(
525+
lambda: self.request.dispose(reason=reason), True
526+
)
527+
522528
async def _inner_close() -> None:
523529
for har_id, params in self._har_recorders.items():
524530
har = cast(
@@ -679,3 +685,7 @@ def tracing(self) -> Tracing:
679685
@property
680686
def request(self) -> "APIRequestContext":
681687
return self._request
688+
689+
@property
690+
def clock(self) -> Clock:
691+
return self._clock

playwright/_impl/_clock.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import datetime
16+
from typing import TYPE_CHECKING, Dict, Union
17+
18+
if TYPE_CHECKING:
19+
from playwright._impl._browser_context import BrowserContext
20+
21+
22+
class Clock:
23+
def __init__(self, browser_context: "BrowserContext") -> None:
24+
self._browser_context = browser_context
25+
self._loop = browser_context._loop
26+
27+
async def install(self, time: Union[int, str, datetime.datetime] = None) -> None:
28+
await self._browser_context._channel.send(
29+
"clockInstall", parse_time(time) if time is not None else {}
30+
)
31+
32+
async def fast_forward(
33+
self,
34+
ticks: Union[int, str],
35+
) -> None:
36+
await self._browser_context._channel.send(
37+
"clockFastForward", parse_ticks(ticks)
38+
)
39+
40+
async def pause_at(
41+
self,
42+
time: Union[int, str, datetime.datetime],
43+
) -> None:
44+
await self._browser_context._channel.send("clockPauseAt", parse_time(time))
45+
46+
async def resume(
47+
self,
48+
) -> None:
49+
await self._browser_context._channel.send("clockResume")
50+
51+
async def run_for(
52+
self,
53+
ticks: Union[int, str],
54+
) -> None:
55+
await self._browser_context._channel.send("clockRunFor", parse_ticks(ticks))
56+
57+
async def set_fixed_time(
58+
self,
59+
time: Union[int, str, datetime.datetime],
60+
) -> None:
61+
await self._browser_context._channel.send("clockSetFixedTime", parse_time(time))
62+
63+
async def set_system_time(
64+
self,
65+
time: Union[int, str, datetime.datetime],
66+
) -> None:
67+
await self._browser_context._channel.send(
68+
"clockSetSystemTime", parse_time(time)
69+
)
70+
71+
72+
def parse_time(time: Union[int, str, datetime.datetime]) -> Dict[str, Union[int, str]]:
73+
if isinstance(time, int):
74+
return {"timeNumber": time}
75+
if isinstance(time, str):
76+
return {"timeString": time}
77+
return {"timeNumber": int(time.timestamp())}
78+
79+
80+
def parse_ticks(ticks: Union[int, str]) -> Dict[str, Union[int, str]]:
81+
if isinstance(ticks, int):
82+
return {"ticksNumber": ticks}
83+
return {"ticksString": ticks}

playwright/_impl/_fetch.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from playwright._impl._helper import (
3535
Error,
3636
NameValue,
37+
TargetClosedError,
3738
async_readfile,
3839
async_writefile,
3940
is_file_payload,
@@ -93,9 +94,16 @@ def __init__(
9394
) -> None:
9495
super().__init__(parent, type, guid, initializer)
9596
self._tracing: Tracing = from_channel(initializer["tracing"])
97+
self._close_reason: Optional[str] = None
9698

97-
async def dispose(self) -> None:
98-
await self._channel.send("dispose")
99+
async def dispose(self, reason: str = None) -> None:
100+
self._close_reason = reason
101+
try:
102+
await self._channel.send("dispose", {"reason": reason})
103+
except Error as e:
104+
if is_target_closed_error(e):
105+
return
106+
raise e
99107
self._tracing._reset_stack_counter()
100108

101109
async def delete(
@@ -313,6 +321,8 @@ async def _inner_fetch(
313321
ignoreHTTPSErrors: bool = None,
314322
maxRedirects: int = None,
315323
) -> "APIResponse":
324+
if self._close_reason:
325+
raise TargetClosedError(self._close_reason)
316326
assert (
317327
(1 if data else 0) + (1 if form else 0) + (1 if multipart else 0)
318328
) <= 1, "Only one of 'data', 'form' or 'multipart' can be specified"

playwright/_impl/_helper.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@
3737
from urllib.parse import urljoin
3838

3939
from playwright._impl._api_structures import NameValue
40-
from playwright._impl._errors import Error, TargetClosedError, TimeoutError
40+
from playwright._impl._errors import (
41+
Error,
42+
TargetClosedError,
43+
TimeoutError,
44+
is_target_closed_error,
45+
rewrite_error,
46+
)
4147
from playwright._impl._glob import glob_to_regex
4248
from playwright._impl._greenlets import RouteGreenlet
4349
from playwright._impl._str_utils import escape_regex_flags
@@ -287,6 +293,13 @@ async def handle(self, route: "Route") -> bool:
287293
# If the handler was stopped (without waiting for completion), we ignore all exceptions.
288294
if self._ignore_exception:
289295
return False
296+
if is_target_closed_error(e):
297+
# We are failing in the handler because the target close closed.
298+
# Give user a hint!
299+
raise rewrite_error(
300+
e,
301+
f"\"{str(e)}\" while running route callback.\nConsider awaiting `await page.unroute_all(behavior='ignoreErrors')`\nbefore the end of the test to ignore remaining routes in flight.",
302+
)
290303
raise e
291304
finally:
292305
handler_invocation.complete.set_result(None)

playwright/_impl/_page.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
ViewportSize,
4444
)
4545
from playwright._impl._artifact import Artifact
46+
from playwright._impl._clock import Clock
4647
from playwright._impl._connection import (
4748
ChannelOwner,
4849
from_channel,
@@ -336,6 +337,10 @@ def _on_video(self, params: Any) -> None:
336337
def context(self) -> "BrowserContext":
337338
return self._browser_context
338339

340+
@property
341+
def clock(self) -> Clock:
342+
return self._browser_context.clock
343+
339344
async def opener(self) -> Optional["Page"]:
340345
if self._opener and self._opener.is_closed():
341346
return None

playwright/_impl/_set_input_files_helpers.py

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@
1515
import collections.abc
1616
import os
1717
from pathlib import Path
18-
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, TypedDict, Union, cast
18+
from typing import (
19+
TYPE_CHECKING,
20+
Dict,
21+
List,
22+
Optional,
23+
Sequence,
24+
Tuple,
25+
TypedDict,
26+
Union,
27+
cast,
28+
)
1929

2030
from playwright._impl._connection import Channel, from_channel
2131
from playwright._impl._helper import Error
@@ -31,10 +41,20 @@
3141

3242
class InputFilesList(TypedDict, total=False):
3343
streams: Optional[List[Channel]]
44+
directoryStream: Optional[Channel]
45+
localDirectory: Optional[str]
3446
localPaths: Optional[List[str]]
3547
payloads: Optional[List[Dict[str, Union[str, bytes]]]]
3648

3749

50+
def _list_files(directory: str) -> List[str]:
51+
files = []
52+
for root, _, filenames in os.walk(directory):
53+
for filename in filenames:
54+
files.append(os.path.join(root, filename))
55+
return files
56+
57+
3858
async def convert_input_files(
3959
files: Union[
4060
str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload]
@@ -50,31 +70,51 @@ async def convert_input_files(
5070
if any([isinstance(item, (str, Path)) for item in items]):
5171
if not all([isinstance(item, (str, Path)) for item in items]):
5272
raise Error("File paths cannot be mixed with buffers")
73+
74+
(local_paths, local_directory) = resolve_paths_and_directory_for_input_files(
75+
cast(Sequence[Union[str, Path]], items)
76+
)
77+
5378
if context._channel._connection.is_remote:
79+
files_to_stream = cast(
80+
List[str],
81+
(_list_files(local_directory) if local_directory else local_paths),
82+
)
5483
streams = []
55-
for item in items:
56-
assert isinstance(item, (str, Path))
57-
last_modified_ms = int(os.path.getmtime(item) * 1000)
58-
stream: WritableStream = from_channel(
59-
await context._connection.wrap_api_call(
60-
lambda: context._channel.send(
61-
"createTempFile",
62-
{
63-
"name": os.path.basename(cast(str, item)),
64-
"lastModifiedMs": last_modified_ms,
65-
},
66-
)
67-
)
84+
result = await context._connection.wrap_api_call(
85+
lambda: context._channel.send_return_as_dict(
86+
"createTempFiles",
87+
{
88+
"rootDirName": (
89+
os.path.basename(local_directory)
90+
if local_directory
91+
else None
92+
),
93+
"items": list(
94+
map(
95+
lambda file: dict(
96+
name=(
97+
os.path.relpath(file, local_directory)
98+
if local_directory
99+
else os.path.basename(file)
100+
),
101+
lastModifiedMs=int(os.path.getmtime(file) * 1000),
102+
),
103+
files_to_stream,
104+
)
105+
),
106+
},
68107
)
69-
await stream.copy(item)
108+
)
109+
for i, file in enumerate(result["writableStreams"]):
110+
stream: WritableStream = from_channel(file)
111+
await stream.copy(files_to_stream[i])
70112
streams.append(stream._channel)
71-
return InputFilesList(streams=streams)
72-
return InputFilesList(
73-
localPaths=[
74-
str(Path(cast(Union[str, Path], item)).absolute().resolve())
75-
for item in items
76-
]
77-
)
113+
return InputFilesList(
114+
streams=None if local_directory else streams,
115+
directoryStream=result.get("rootDir"),
116+
)
117+
return InputFilesList(localPaths=local_paths, localDirectory=local_directory)
78118

79119
file_payload_exceeds_size_limit = (
80120
sum([len(f.get("buffer", "")) for f in items if not isinstance(f, (str, Path))])
@@ -95,3 +135,21 @@ async def convert_input_files(
95135
for item in cast(List[FilePayload], items)
96136
]
97137
)
138+
139+
140+
def resolve_paths_and_directory_for_input_files(
141+
items: Sequence[Union[str, Path]]
142+
) -> Tuple[Optional[List[str]], Optional[str]]:
143+
local_paths: Optional[List[str]] = None
144+
local_directory: Optional[str] = None
145+
for item in items:
146+
if os.path.isdir(item):
147+
if local_directory:
148+
raise Error("Multiple directories are not supported")
149+
local_directory = str(Path(item).resolve())
150+
else:
151+
local_paths = local_paths or []
152+
local_paths.append(str(Path(item).resolve()))
153+
if local_paths and local_directory:
154+
raise Error("File paths must be all files or a single directory")
155+
return (local_paths, local_directory)

0 commit comments

Comments
 (0)