Skip to content

Commit 03babe9

Browse files
authored
feat(expect): add types expectations into the page & browser context. (#88)
1 parent 7f93f70 commit 03babe9

File tree

10 files changed

+310
-48
lines changed

10 files changed

+310
-48
lines changed

client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ def main() -> None:
2222
"<button id=button onclick=\"window.open('http://webkit.org', '_blank')\">Click me</input>"
2323
)
2424

25-
with page.expect_event("popup") as popup:
25+
with page.expect_popup() as popup_info:
2626
page.click("#button")
27-
print(popup.value)
27+
print(popup_info.value)
2828

2929
print("Contexts in browser: %d" % len(browser.contexts))
3030
print("Creating context...")

playwright/browser_context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
1818

1919
from playwright.connection import ChannelOwner, ConnectionScope, from_channel
20+
from playwright.event_context_manager import AsyncEventContextManager
2021
from playwright.helper import (
2122
Cookie,
2223
Error,
@@ -212,3 +213,13 @@ async def close(self) -> None:
212213
return
213214
self._is_closed_or_closing = True
214215
await self._channel.send("close")
216+
217+
def expect_event(
218+
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None,
219+
) -> AsyncEventContextManager:
220+
return AsyncEventContextManager(self, event, predicate, timeout)
221+
222+
def expect_page(
223+
self, predicate: Callable[[Page], bool] = None, timeout: int = None,
224+
) -> AsyncEventContextManager[Page]:
225+
return AsyncEventContextManager(self, "page", predicate, timeout)

playwright/event_context_manager.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 asyncio
16+
from typing import Any, Callable, Generic, Optional, TypeVar, cast
17+
18+
from playwright.connection import ChannelOwner
19+
from playwright.wait_helper import WaitHelper
20+
21+
T = TypeVar("T")
22+
23+
24+
class AsyncEventInfo(Generic[T]):
25+
def __init__(
26+
self,
27+
channel_owner: ChannelOwner,
28+
event: str,
29+
predicate: Callable[[T], bool] = None,
30+
timeout: int = None,
31+
) -> None:
32+
self._value: Optional[T] = None
33+
wait_helper = WaitHelper()
34+
wait_helper.reject_on_timeout(
35+
timeout or 30000, f'Timeout while waiting for event "${event}"'
36+
)
37+
self._future = asyncio.get_event_loop().create_task(
38+
wait_helper.wait_for_event(channel_owner, event, predicate)
39+
)
40+
41+
@property
42+
async def value(self) -> T:
43+
if not self._value:
44+
self._value = await self._future
45+
return cast(T, self._value)
46+
47+
48+
class AsyncEventContextManager(Generic[T]):
49+
def __init__(
50+
self,
51+
channel_owner: ChannelOwner,
52+
event: str,
53+
predicate: Callable[[T], bool] = None,
54+
timeout: int = None,
55+
) -> None:
56+
self._event = AsyncEventInfo(channel_owner, event, predicate, timeout)
57+
58+
async def __aenter__(self) -> AsyncEventInfo[T]:
59+
return self._event
60+
61+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
62+
await self._event.value

playwright/file_chooser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import TYPE_CHECKING, List, Union
15+
from typing import TYPE_CHECKING, Any, List, Union
1616

1717
from playwright.helper import FilePayload
1818

@@ -25,6 +25,7 @@ class FileChooser:
2525
def __init__(
2626
self, page: "Page", element_handle: "ElementHandle", is_multiple: bool
2727
) -> None:
28+
self._sync_owner: Any = None
2829
self._page = page
2930
self._element_handle = element_handle
3031
self._is_multiple = is_multiple

playwright/page.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
from_channel,
2626
from_nullable_channel,
2727
)
28+
from playwright.console_message import ConsoleMessage
29+
from playwright.dialog import Dialog
30+
from playwright.download import Download
2831
from playwright.element_handle import ElementHandle, ValuesToSelect
32+
from playwright.event_context_manager import AsyncEventContextManager
2933
from playwright.file_chooser import FileChooser
3034
from playwright.frame import Frame
3135
from playwright.helper import (
@@ -99,7 +103,7 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
99103
self._main_frame: Frame = from_channel(initializer["mainFrame"])
100104
self._main_frame._page = self
101105
self._frames = [self._main_frame]
102-
self._viewport_size = initializer["viewportSize"]
106+
self._viewport_size = initializer.get("viewportSize")
103107
self._is_closed = False
104108
self._workers: List[Worker] = list()
105109
self._bindings: Dict[str, Any] = dict()
@@ -704,6 +708,51 @@ async def pdf(
704708
binary = await self._channel.send("pdf", locals_to_params(locals()))
705709
return base64.b64decode(binary)
706710

711+
def expect_event(
712+
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None,
713+
) -> AsyncEventContextManager:
714+
return AsyncEventContextManager(self, event, predicate, timeout)
715+
716+
def expect_console_message(
717+
self, predicate: Callable[[ConsoleMessage], bool] = None, timeout: int = None,
718+
) -> AsyncEventContextManager[ConsoleMessage]:
719+
return AsyncEventContextManager(self, "console", predicate, timeout)
720+
721+
def expect_dialog(
722+
self, predicate: Callable[[Dialog], bool] = None, timeout: int = None,
723+
) -> AsyncEventContextManager[Dialog]:
724+
return AsyncEventContextManager(self, "dialog", predicate, timeout)
725+
726+
def expect_download(
727+
self, predicate: Callable[[Download], bool] = None, timeout: int = None,
728+
) -> AsyncEventContextManager[Download]:
729+
return AsyncEventContextManager(self, "download", predicate, timeout)
730+
731+
def expect_file_chooser(
732+
self, predicate: Callable[[FileChooser], bool] = None, timeout: int = None,
733+
) -> AsyncEventContextManager[FileChooser]:
734+
return AsyncEventContextManager(self, "filechooser", predicate, timeout)
735+
736+
def expect_request(
737+
self, predicate: Callable[[Request], bool] = None, timeout: int = None,
738+
) -> AsyncEventContextManager[Request]:
739+
return AsyncEventContextManager(self, "request", predicate, timeout)
740+
741+
def expect_response(
742+
self, predicate: Callable[[Response], bool] = None, timeout: int = None,
743+
) -> AsyncEventContextManager[Response]:
744+
return AsyncEventContextManager(self, "response", predicate, timeout)
745+
746+
def expect_popup(
747+
self, predicate: Callable[["Page"], bool] = None, timeout: int = None,
748+
) -> AsyncEventContextManager["Page"]:
749+
return AsyncEventContextManager(self, "popup", predicate, timeout)
750+
751+
def expect_worker(
752+
self, predicate: Callable[[Worker], bool] = None, timeout: int = None,
753+
) -> AsyncEventContextManager[Worker]:
754+
return AsyncEventContextManager(self, "worker", predicate, timeout)
755+
707756

708757
class BindingCall(ChannelOwner):
709758
def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None:

playwright/sync.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import sys
1616
import typing
1717

18-
from playwright.sync_base import SyncBase, mapping
18+
from playwright.sync_base import EventContextManager, SyncBase, mapping
1919

2020
if sys.version_info >= (3, 8): # pragma: no cover
2121
from typing import Literal
@@ -31,6 +31,7 @@
3131
from playwright.dialog import Dialog as DialogAsync
3232
from playwright.download import Download as DownloadAsync
3333
from playwright.element_handle import ElementHandle as ElementHandleAsync
34+
from playwright.file_chooser import FileChooser as FileChooserAsync
3435
from playwright.frame import Frame as FrameAsync
3536
from playwright.helper import (
3637
ConsoleMessageLocation,
@@ -770,6 +771,67 @@ def snapshot(
770771
mapping.register(AccessibilityAsync, Accessibility)
771772

772773

774+
class FileChooser(SyncBase):
775+
def __init__(self, obj: FileChooserAsync):
776+
super().__init__(obj)
777+
778+
def as_async(self) -> FileChooserAsync:
779+
return self._async_obj
780+
781+
@classmethod
782+
def _from_async(cls, obj: FileChooserAsync) -> "FileChooser":
783+
if not obj._sync_owner:
784+
obj._sync_owner = cls(obj)
785+
return obj._sync_owner
786+
787+
@classmethod
788+
def _from_async_nullable(
789+
cls, obj: FileChooserAsync = None
790+
) -> typing.Optional["FileChooser"]:
791+
return FileChooser._from_async(obj) if obj else None
792+
793+
@classmethod
794+
def _from_async_list(
795+
cls, items: typing.List[FileChooserAsync]
796+
) -> typing.List["FileChooser"]:
797+
return list(map(lambda a: FileChooser._from_async(a), items))
798+
799+
@classmethod
800+
def _from_async_dict(
801+
cls, map: typing.Dict[str, FileChooserAsync]
802+
) -> typing.Dict[str, "FileChooser"]:
803+
return {name: FileChooser._from_async(value) for name, value in map.items()}
804+
805+
@property
806+
def page(self) -> "Page":
807+
return Page._from_async(self._async_obj.page)
808+
809+
@property
810+
def element(self) -> "ElementHandle":
811+
return ElementHandle._from_async(self._async_obj.element)
812+
813+
@property
814+
def isMultiple(self) -> bool:
815+
return self._async_obj.isMultiple
816+
817+
def setFiles(
818+
self,
819+
files: typing.Union[
820+
str, FilePayload, typing.List[str], typing.List[FilePayload]
821+
],
822+
timeout: int = None,
823+
noWaitAfter: bool = None,
824+
) -> NoneType:
825+
return self._sync(
826+
self._async_obj.setFiles(
827+
files=files, timeout=timeout, noWaitAfter=noWaitAfter
828+
)
829+
)
830+
831+
832+
mapping.register(FileChooserAsync, FileChooser)
833+
834+
773835
class Frame(SyncBase):
774836
def __init__(self, obj: FrameAsync):
775837
super().__init__(obj)
@@ -2119,6 +2181,62 @@ def pdf(
21192181
)
21202182
)
21212183

2184+
def expect_console_message(
2185+
self,
2186+
predicate: typing.Union[typing.Callable[["ConsoleMessage"], bool]] = None,
2187+
timeout: int = None,
2188+
) -> EventContextManager["ConsoleMessage"]:
2189+
return EventContextManager(self, "console", predicate, timeout)
2190+
2191+
def expect_dialog(
2192+
self,
2193+
predicate: typing.Union[typing.Callable[["Dialog"], bool]] = None,
2194+
timeout: int = None,
2195+
) -> EventContextManager["Dialog"]:
2196+
return EventContextManager(self, "dialog", predicate, timeout)
2197+
2198+
def expect_download(
2199+
self,
2200+
predicate: typing.Union[typing.Callable[["Download"], bool]] = None,
2201+
timeout: int = None,
2202+
) -> EventContextManager["Download"]:
2203+
return EventContextManager(self, "download", predicate, timeout)
2204+
2205+
def expect_file_chooser(
2206+
self,
2207+
predicate: typing.Union[typing.Callable[["FileChooser"], bool]] = None,
2208+
timeout: int = None,
2209+
) -> EventContextManager["FileChooser"]:
2210+
return EventContextManager(self, "filechooser", predicate, timeout)
2211+
2212+
def expect_request(
2213+
self,
2214+
predicate: typing.Union[typing.Callable[["Request"], bool]] = None,
2215+
timeout: int = None,
2216+
) -> EventContextManager["Request"]:
2217+
return EventContextManager(self, "request", predicate, timeout)
2218+
2219+
def expect_response(
2220+
self,
2221+
predicate: typing.Union[typing.Callable[["Response"], bool]] = None,
2222+
timeout: int = None,
2223+
) -> EventContextManager["Response"]:
2224+
return EventContextManager(self, "response", predicate, timeout)
2225+
2226+
def expect_popup(
2227+
self,
2228+
predicate: typing.Union[typing.Callable[["Page"], bool]] = None,
2229+
timeout: int = None,
2230+
) -> EventContextManager["Page"]:
2231+
return EventContextManager(self, "popup", predicate, timeout)
2232+
2233+
def expect_worker(
2234+
self,
2235+
predicate: typing.Union[typing.Callable[["Worker"], bool]] = None,
2236+
timeout: int = None,
2237+
) -> EventContextManager["Worker"]:
2238+
return EventContextManager(self, "worker", predicate, timeout)
2239+
21222240

21232241
mapping.register(PageAsync, Page)
21242242

@@ -2244,6 +2362,13 @@ def waitForEvent(
22442362
def close(self) -> NoneType:
22452363
return self._sync(self._async_obj.close())
22462364

2365+
def expect_page(
2366+
self,
2367+
predicate: typing.Union[typing.Callable[["Page"], bool]] = None,
2368+
timeout: int = None,
2369+
) -> EventContextManager["Page"]:
2370+
return EventContextManager(self, "page", predicate, timeout)
2371+
22472372

22482373
mapping.register(BrowserContextAsync, BrowserContext)
22492374

0 commit comments

Comments
 (0)