Skip to content

Commit

Permalink
Merge branch 'main' into read-stream
Browse files Browse the repository at this point in the history
  • Loading branch information
BdEgh authored Mar 18, 2024
2 parents e54eb53 + 812f8f9 commit 02974eb
Show file tree
Hide file tree
Showing 22 changed files with 207 additions and 92 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->122.0.6261.39<!-- GEN:stop --> ||||
| Chromium <!-- GEN:chromium-version -->123.0.6312.4<!-- GEN:stop --> ||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->122.0<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->123.0<!-- GEN:stop --> ||||

## Documentation

Expand Down
20 changes: 10 additions & 10 deletions local-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ auditwheel==6.0.0
autobahn==23.1.2
black==24.2.0
flake8==7.0.0
flaky==3.7.0
flaky==3.8.0
mypy==1.8.0
objgraph==3.6.0
objgraph==3.6.1
Pillow==10.2.0
pixelmatch==0.3.0
pre-commit==3.4.0
pyOpenSSL==23.2.0
pytest==8.0.0
pyOpenSSL==24.0.0
pytest==8.1.1
pytest-asyncio==0.21.1
pytest-cov==4.1.0
pytest-repeat==0.9.3
pytest-timeout==2.2.0
pytest-timeout==2.3.1
pytest-xdist==3.5.0
requests==2.31.0
service_identity==24.1.0
setuptools==68.2.2
twisted==23.10.0
types-pyOpenSSL==24.0.0.20240130
types-requests==2.31.0.10
wheel==0.41.2
setuptools==69.1.1
twisted==24.3.0
types-pyOpenSSL==24.0.0.20240311
types-requests==2.31.0.20240311
wheel==0.42.0
2 changes: 1 addition & 1 deletion playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def __init__(
)
self._channel.on(
"route",
lambda params: asyncio.create_task(
lambda params: self._loop.create_task(
self._on_route(
from_channel(params.get("route")),
)
Expand Down
47 changes: 33 additions & 14 deletions playwright/_impl/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self, connection: "Connection", object: "ChannelOwner") -> None:
self._connection = connection
self._guid = object._guid
self._object = object
self.on("error", lambda exc: self._connection._on_event_listener_error(exc))

async def send(self, method: str, params: Dict = None) -> Any:
return await self._connection.wrap_api_call(
Expand All @@ -77,13 +78,13 @@ async def inner_send(
) -> Any:
if params is None:
params = {}
callback = self._connection._send_message_to_server(
self._object, method, _filter_none(params)
)
if self._connection._error:
error = self._connection._error
self._connection._error = None
raise error
callback = self._connection._send_message_to_server(
self._object, method, _filter_none(params)
)
done, _ = await asyncio.wait(
{
self._connection._transport.on_error_future,
Expand Down Expand Up @@ -334,20 +335,22 @@ def _send_message_to_server(
"line": frames[0]["line"],
"column": frames[0]["column"],
}
if len(frames) > 0
if frames
else None
)
metadata = {
"wallTime": int(datetime.datetime.now().timestamp() * 1000),
"apiName": stack_trace_information["apiName"],
"internal": not stack_trace_information["apiName"],
}
if location:
metadata["location"] = location # type: ignore
message = {
"id": id,
"guid": object._guid,
"method": method,
"params": self._replace_channels_with_guids(params),
"metadata": {
"wallTime": int(datetime.datetime.now().timestamp() * 1000),
"apiName": stack_trace_information["apiName"],
"location": location,
"internal": not stack_trace_information["apiName"],
},
"metadata": metadata,
}
if self._tracing_count > 0 and frames and object._guid != "localUtils":
self.local_utils.add_stack_to_tracing_no_reply(id, frames)
Expand Down Expand Up @@ -414,10 +417,22 @@ def dispatch(self, msg: ParsedMessagePayload) -> None:
try:
if self._is_sync:
for listener in object._channel.listeners(method):
# Event handlers like route/locatorHandlerTriggered require us to perform async work.
# In order to report their potential errors to the user, we need to catch it and store it in the connection
def _done_callback(future: asyncio.Future) -> None:
exc = future.exception()
if exc:
self._on_event_listener_error(exc)

def _listener_with_error_handler_attached(params: Any) -> None:
potential_future = listener(params)
if asyncio.isfuture(potential_future):
potential_future.add_done_callback(_done_callback)

# Each event handler is a potentilly blocking context, create a fiber for each
# and switch to them in order, until they block inside and pass control to each
# other and then eventually back to dispatcher as listener functions return.
g = EventGreenlet(listener)
g = EventGreenlet(_listener_with_error_handler_attached)
if should_replace_guids_with_channels:
g.switch(self._replace_guids_with_channels(params))
else:
Expand All @@ -430,9 +445,13 @@ def dispatch(self, msg: ParsedMessagePayload) -> None:
else:
object._channel.emit(method, params)
except BaseException as exc:
print("Error occurred in event listener", file=sys.stderr)
traceback.print_exc()
self._error = exc
self._on_event_listener_error(exc)

def _on_event_listener_error(self, exc: BaseException) -> None:
print("Error occurred in event listener", file=sys.stderr)
traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr)
# Save the error to throw at the next API call. This "replicates" unhandled rejection in Node.js.
self._error = exc

def _create_remote_object(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
Expand Down
12 changes: 11 additions & 1 deletion playwright/_impl/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,20 @@ async def _handle_internal(self, route: "Route") -> bool:

self._handled_count += 1
if self._is_sync:
handler_finished_future = route._loop.create_future()

def _handler() -> None:
try:
self.handler(route, route.request) # type: ignore
handler_finished_future.set_result(None)
except Exception as e:
handler_finished_future.set_exception(e)

# As with event handlers, each route handler is a potentially blocking context
# so it needs a fiber.
g = RouteGreenlet(lambda: self.handler(route, route.request)) # type: ignore
g = RouteGreenlet(_handler)
g.switch()
await handler_finished_future
else:
coro_or_future = self.handler(route, route.request) # type: ignore
if coro_or_future:
Expand Down
16 changes: 12 additions & 4 deletions playwright/_impl/_js_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
# limitations under the License.

import collections.abc
import datetime
import math
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from urllib.parse import ParseResult, urlparse, urlunparse
Expand Down Expand Up @@ -128,8 +128,13 @@ def serialize_value(
return dict(v="-0")
if math.isnan(value):
return dict(v="NaN")
if isinstance(value, datetime):
return dict(d=value.isoformat())
if isinstance(value, datetime.datetime):
# Node.js Date objects are always in UTC.
return {
"d": datetime.datetime.strftime(
value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ"
)
}
if isinstance(value, bool):
return {"b": value}
if isinstance(value, (int, float)):
Expand Down Expand Up @@ -205,7 +210,10 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any:
return a

if "d" in value:
return datetime.fromisoformat(value["d"][:-1])
# Node.js Date objects are always in UTC.
return datetime.datetime.strptime(
value["d"], "%Y-%m-%dT%H:%M:%S.%fZ"
).replace(tzinfo=datetime.timezone.utc)

if "o" in value:
o: Dict = {}
Expand Down
5 changes: 4 additions & 1 deletion playwright/_impl/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ def _target_closed_future(self) -> asyncio.Future:
return page._closed_or_crashed_future

def _safe_page(self) -> "Optional[Page]":
return cast("Frame", from_channel(self._initializer["frame"]))._page
frame = from_nullable_channel(self._initializer.get("frame"))
if not frame:
return None
return cast("Frame", frame)._page


class Route(ChannelOwner):
Expand Down
4 changes: 2 additions & 2 deletions playwright/_impl/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,13 @@ def __init__(
)
self._channel.on(
"locatorHandlerTriggered",
lambda params: asyncio.create_task(
lambda params: self._loop.create_task(
self._on_locator_handler_triggered(params["uid"])
),
)
self._channel.on(
"route",
lambda params: asyncio.create_task(
lambda params: self._loop.create_task(
self._on_route(from_channel(params["route"]))
),
)
Expand Down
46 changes: 24 additions & 22 deletions playwright/async_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -11678,35 +11678,37 @@ async def add_locator_handler(
) -> None:
"""Page.add_locator_handler

Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like
click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your
test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent
dialogs behave this way. In this case, `page.add_locator_handler()` allows handling an overlay during an
action that it would block.

This method registers a handler for an overlay that is executed once the locator is visible on the page. The
handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for
nondeterministic interstitial pages or dialogs, like a cookie consent dialog.

Note that execution time of the handler counts towards the timeout of the action/assertion that executed the
handler.

You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a
handler must not require another handler to run.

**NOTE** Running the interceptor will alter your page state mid-test. For example it will change the currently
focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained
and do not rely on the focus and mouse state. <br /> <br /> For example, consider a test that calls
When testing a web page, sometimes unexpected overlays like a coookie consent dialog appear and block actions you
want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time,
making them tricky to handle in automated tests.

This method lets you set up a special function, called a handler, that activates when it detects that overlay is
visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.

Things to keep in mind:
- When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as
a part of your normal test flow, instead of using `page.add_locator_handler()`.
- Playwright checks for the overlay every time before executing or retrying an action that requires an
[actionability check](https://playwright.dev/python/docs/actionability), or before performing an auto-waiting assertion check. When overlay
is visible, Playwright calls the handler first, and then proceeds with the action/assertion.
- The execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
If your handler takes too long, it might cause timeouts.
- You can register multiple handlers. However, only a single handler will be running at a time. Make sure the
actions within a handler don't depend on another handler.

**NOTE** Running the handler will alter your page state mid-test. For example it will change the currently focused
element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on
the focus and mouse state being unchanged. <br /> <br /> For example, consider a test that calls
`locator.focus()` followed by `keyboard.press()`. If your handler clicks a button between these two
actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use
`locator.press()` instead to avoid this problem. <br /> <br /> Another example is a series of mouse
actions, where `mouse.move()` is followed by `mouse.down()`. Again, when the handler runs between
these two actions, the mouse position will be wrong during the mouse down. Prefer methods like
`locator.click()` that are self-contained.
these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions like
`locator.click()` that do not rely on the state being unchanged by a handler.

**Usage**

An example that closes a cookie dialog when it appears:
An example that closes a cookie consent dialog when it appears:

```py
# Setup the handler.
Expand Down
46 changes: 24 additions & 22 deletions playwright/sync_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -11761,35 +11761,37 @@ def set_checked(
def add_locator_handler(self, locator: "Locator", handler: typing.Callable) -> None:
"""Page.add_locator_handler

Sometimes, the web page can show an overlay that obstructs elements behind it and prevents certain actions, like
click, from completing. When such an overlay is shown predictably, we recommend dismissing it as a part of your
test flow. However, sometimes such an overlay may appear non-deterministically, for example certain cookies consent
dialogs behave this way. In this case, `page.add_locator_handler()` allows handling an overlay during an
action that it would block.

This method registers a handler for an overlay that is executed once the locator is visible on the page. The
handler should get rid of the overlay so that actions blocked by it can proceed. This is useful for
nondeterministic interstitial pages or dialogs, like a cookie consent dialog.

Note that execution time of the handler counts towards the timeout of the action/assertion that executed the
handler.

You can register multiple handlers. However, only a single handler will be running at a time. Any actions inside a
handler must not require another handler to run.

**NOTE** Running the interceptor will alter your page state mid-test. For example it will change the currently
focused element and move the mouse. Make sure that the actions that run after the interceptor are self-contained
and do not rely on the focus and mouse state. <br /> <br /> For example, consider a test that calls
When testing a web page, sometimes unexpected overlays like a coookie consent dialog appear and block actions you
want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time,
making them tricky to handle in automated tests.

This method lets you set up a special function, called a handler, that activates when it detects that overlay is
visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there.

Things to keep in mind:
- When an overlay is shown predictably, we recommend explicitly waiting for it in your test and dismissing it as
a part of your normal test flow, instead of using `page.add_locator_handler()`.
- Playwright checks for the overlay every time before executing or retrying an action that requires an
[actionability check](https://playwright.dev/python/docs/actionability), or before performing an auto-waiting assertion check. When overlay
is visible, Playwright calls the handler first, and then proceeds with the action/assertion.
- The execution time of the handler counts towards the timeout of the action/assertion that executed the handler.
If your handler takes too long, it might cause timeouts.
- You can register multiple handlers. However, only a single handler will be running at a time. Make sure the
actions within a handler don't depend on another handler.

**NOTE** Running the handler will alter your page state mid-test. For example it will change the currently focused
element and move the mouse. Make sure that actions that run after the handler are self-contained and do not rely on
the focus and mouse state being unchanged. <br /> <br /> For example, consider a test that calls
`locator.focus()` followed by `keyboard.press()`. If your handler clicks a button between these two
actions, the focused element most likely will be wrong, and key press will happen on the unexpected element. Use
`locator.press()` instead to avoid this problem. <br /> <br /> Another example is a series of mouse
actions, where `mouse.move()` is followed by `mouse.down()`. Again, when the handler runs between
these two actions, the mouse position will be wrong during the mouse down. Prefer methods like
`locator.click()` that are self-contained.
these two actions, the mouse position will be wrong during the mouse down. Prefer self-contained actions like
`locator.click()` that do not rely on the state being unchanged by a handler.

**Usage**

An example that closes a cookie dialog when it appears:
An example that closes a cookie consent dialog when it appears:

```py
# Setup the handler.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools==68.2.2", "setuptools-scm==8.0.4", "wheel==0.41.2", "auditwheel==5.4.0"]
requires = ["setuptools==68.2.2", "setuptools-scm==8.0.4", "wheel==0.42.0", "auditwheel==5.4.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
InWheel = None
from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand

driver_version = "1.42.0-alpha-2024-02-19"
driver_version = "1.42.1"


def extractall(zip: zipfile.ZipFile, path: str) -> None:
Expand Down Expand Up @@ -219,10 +219,10 @@ def _download_and_extract_local_driver(
include_package_data=True,
install_requires=[
"greenlet==3.0.3",
"pyee==11.0.1",
"pyee==11.1.0",
],
# TODO: Can be removed once we migrate to pypa/build or pypa/installer.
setup_requires=["setuptools-scm==8.0.4", "wheel==0.41.2"],
setup_requires=["setuptools-scm==8.0.4", "wheel==0.42.0"],
classifiers=[
"Topic :: Software Development :: Testing",
"Topic :: Internet :: WWW/HTTP :: Browsers",
Expand Down
Loading

0 comments on commit 02974eb

Please sign in to comment.