Skip to content

Commit d9847e6

Browse files
authored
feat(wait helper): add wait helper for client-side waitFor* methods (microsoft#74)
1 parent 59e6351 commit d9847e6

File tree

8 files changed

+197
-86
lines changed

8 files changed

+197
-86
lines changed

playwright/browser_context.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
URLMatcher,
3030
)
3131
from playwright.network import Request, Route, serialize_headers
32-
from playwright.page import BindingCall, Page, wait_for_event
32+
from playwright.page import BindingCall, Page
33+
from playwright.wait_helper import WaitHelper
3334

3435
if TYPE_CHECKING: # pragma: no cover
3536
from playwright.browser import Browser
@@ -185,9 +186,17 @@ async def unroute(
185186
async def waitForEvent(
186187
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None
187188
) -> Any:
188-
return await wait_for_event(
189-
self, self._timeout_settings, event, predicate=predicate, timeout=timeout
189+
if timeout is None:
190+
timeout = self._timeout_settings.timeout()
191+
wait_helper = WaitHelper()
192+
wait_helper.reject_on_timeout(
193+
timeout, f'Timeout while waiting for event "${event}"'
190194
)
195+
if event != BrowserContext.Events.Close:
196+
wait_helper.reject_on_event(
197+
self, BrowserContext.Events.Close, Error("Context closed")
198+
)
199+
return await wait_helper.wait_for_event(self, event, predicate)
191200

192201
def _on_close(self) -> None:
193202
self._is_closed_or_closing = True

playwright/frame.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,19 @@
3232
)
3333
from playwright.helper import (
3434
DocumentLoadState,
35+
Error,
3536
FilePayload,
37+
FrameNavigatedEvent,
3638
KeyboardModifier,
3739
MouseButton,
40+
URLMatch,
41+
URLMatcher,
3842
is_function_body,
3943
locals_to_params,
4044
)
4145
from playwright.js_handle import JSHandle, parse_result, serialize_argument
4246
from playwright.network import Response
47+
from playwright.wait_helper import WaitHelper
4348

4449
if sys.version_info >= (3, 8): # pragma: no cover
4550
from typing import Literal
@@ -68,27 +73,30 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
6873
lambda params: self._on_load_state(params.get("add"), params.get("remove")),
6974
)
7075
self._channel.on(
71-
"navigated",
72-
lambda params: self._on_frame_navigated(params["url"], params["name"]),
76+
"navigated", lambda params: self._on_frame_navigated(params),
7377
)
7478

75-
def _on_load_state(self, add: str = None, remove: str = None) -> None:
79+
def _on_load_state(
80+
self, add: DocumentLoadState = None, remove: DocumentLoadState = None
81+
) -> None:
7682
if add:
7783
self._load_states.add(add)
7884
self._event_emitter.emit("loadstate", add)
7985
elif remove and remove in self._load_states:
8086
self._load_states.remove(remove)
8187

82-
def _on_frame_navigated(self, url: str, name: str) -> None:
83-
self._url = url
84-
self._name = name
85-
self._page.emit("framenavigated", self)
88+
def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None:
89+
self._url = event["url"]
90+
self._name = event["name"]
91+
self._event_emitter.emit("navigated", event)
92+
if "error" not in event and self._page:
93+
self._page.emit("framenavigated", self)
8694

8795
async def goto(
8896
self,
8997
url: str,
9098
timeout: int = None,
91-
waitUntil: DocumentLoadState = None,
99+
waitUntil: DocumentLoadState = "load",
92100
referer: str = None,
93101
) -> Optional[Response]:
94102
return cast(
@@ -98,33 +106,54 @@ async def goto(
98106
),
99107
)
100108

109+
def _setup_navigation_wait_helper(self, timeout: int = None) -> WaitHelper:
110+
wait_helper = WaitHelper()
111+
wait_helper.reject_on_event(
112+
self._page, "close", Error("Navigation failed because page was closed!")
113+
)
114+
wait_helper.reject_on_event(
115+
self._page, "crash", Error("Navigation failed because page crashed!")
116+
)
117+
wait_helper.reject_on_event(
118+
self._page,
119+
"framedetached",
120+
Error("Navigating frame was detached!"),
121+
lambda frame: frame == self,
122+
)
123+
if timeout is None:
124+
timeout = self._page._timeout_settings.navigation_timeout()
125+
wait_helper.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.")
126+
return wait_helper
127+
101128
async def waitForNavigation(
102129
self,
103130
timeout: int = None,
104-
waitUntil: DocumentLoadState = None,
105-
url: str = None, # TODO: add url, callback
131+
waitUntil: DocumentLoadState = "load",
132+
url: URLMatch = None,
106133
) -> Optional[Response]:
107-
return cast(
108-
Optional[Response],
109-
from_nullable_channel(
110-
await self._channel.send(
111-
"waitForNavigation", locals_to_params(locals())
112-
)
113-
),
134+
wait_helper = self._setup_navigation_wait_helper(timeout)
135+
matcher = URLMatcher(url) if url else None
136+
event = await wait_helper.wait_for_event(
137+
self._event_emitter,
138+
"navigated",
139+
lambda event: not matcher or matcher.matches(event["url"]),
114140
)
115-
116-
async def waitForLoadState(self, state: str = "load", timeout: int = None) -> None:
141+
if "newDocument" in event and "request" in event["newDocument"]:
142+
request = from_channel(event["newDocument"]["request"])
143+
return await request.response()
144+
if "error" in event:
145+
raise Error(event["error"])
146+
return None
147+
148+
async def waitForLoadState(
149+
self, state: DocumentLoadState = "load", timeout: int = None
150+
) -> None:
117151
if state in self._load_states:
118152
return
119-
future = self._scope._loop.create_future()
120-
121-
def loadstate(s: str) -> None:
122-
if state == s:
123-
future.set_result(None)
124-
self._event_emitter.remove_listener("loadstate", loadstate)
125-
126-
self._event_emitter.on("loadstate", loadstate)
127-
await future
153+
wait_helper = self._setup_navigation_wait_helper(timeout)
154+
await wait_helper.wait_for_event(
155+
self._event_emitter, "loadstate", lambda s: s == state
156+
)
128157

129158
async def frameElement(self) -> ElementHandle:
130159
return from_channel(await self._channel.send("frameElement"))

playwright/helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ class ParsedMessagePayload(TypedDict, total=False):
106106
error: ErrorPayload
107107

108108

109+
class Document(TypedDict):
110+
request: Optional[Any]
111+
112+
113+
class FrameNavigatedEvent(TypedDict):
114+
url: str
115+
name: str
116+
newDocument: Optional[Document]
117+
error: Optional[str]
118+
119+
109120
class URLMatcher:
110121
def __init__(self, match: URLMatch) -> None:
111122
self._callback: Optional[Callable[[str], bool]] = None

playwright/network.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ def postData(self) -> Optional[str]:
5959
def headers(self) -> Dict[str, str]:
6060
return parse_headers(self._initializer["headers"])
6161

62-
@property
6362
async def response(self) -> Optional["Response"]:
6463
return from_nullable_channel(await self._channel.send("response"))
6564

playwright/page.py

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
PendingWaitEvent,
4141
RouteHandler,
4242
RouteHandlerEntry,
43-
TimeoutError,
4443
TimeoutSettings,
4544
URLMatch,
4645
URLMatcher,
@@ -53,6 +52,7 @@
5352
from playwright.input import Keyboard, Mouse
5453
from playwright.js_handle import JSHandle, serialize_argument
5554
from playwright.network import Request, Response, Route, serialize_headers
55+
from playwright.wait_helper import WaitHelper
5656
from playwright.worker import Worker
5757

5858
if sys.version_info >= (3, 8): # pragma: no cover
@@ -103,6 +103,7 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
103103
self._pending_wait_for_events: List[PendingWaitEvent] = list()
104104
self._routes: List[RouteHandlerEntry] = list()
105105
self._owned_context: Optional["BrowserContext"] = None
106+
self._timeout_settings = TimeoutSettings(None)
106107

107108
self._channel.on(
108109
"bindingCall",
@@ -370,25 +371,27 @@ async def goto(
370371
self,
371372
url: str,
372373
timeout: int = None,
373-
waitUntil: DocumentLoadState = None,
374+
waitUntil: DocumentLoadState = "load",
374375
referer: str = None,
375376
) -> Optional[Response]:
376377
return await self._main_frame.goto(**locals_to_params(locals()))
377378

378379
async def reload(
379-
self, timeout: int = None, waitUntil: DocumentLoadState = None,
380+
self, timeout: int = None, waitUntil: DocumentLoadState = "load",
380381
) -> Optional[Response]:
381382
return from_nullable_channel(
382383
await self._channel.send("reload", locals_to_params(locals()))
383384
)
384385

385-
async def waitForLoadState(self, state: str = "load", timeout: int = None) -> None:
386+
async def waitForLoadState(
387+
self, state: DocumentLoadState = "load", timeout: int = None
388+
) -> None:
386389
return await self._main_frame.waitForLoadState(**locals_to_params(locals()))
387390

388391
async def waitForNavigation(
389392
self,
390393
timeout: int = None,
391-
waitUntil: DocumentLoadState = None,
394+
waitUntil: DocumentLoadState = "load",
392395
url: str = None, # TODO: add url, callback
393396
) -> Optional[Response]:
394397
return await self._main_frame.waitForNavigation(**locals_to_params(locals()))
@@ -440,9 +443,17 @@ def my_predicate(response: Response) -> bool:
440443
async def waitForEvent(
441444
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None
442445
) -> Any:
443-
return await wait_for_event(
444-
self, self._timeout_settings, event, predicate=predicate, timeout=timeout
446+
if timeout is None:
447+
timeout = self._timeout_settings.timeout()
448+
wait_helper = WaitHelper()
449+
wait_helper.reject_on_timeout(
450+
timeout, f'Timeout while waiting for event "${event}"'
445451
)
452+
if event != Page.Events.Crash:
453+
wait_helper.reject_on_event(self, Page.Events.Crash, Error("Page crashed"))
454+
if event != Page.Events.Close:
455+
wait_helper.reject_on_event(self, Page.Events.Close, Error("Page closed"))
456+
return await wait_helper.wait_for_event(self, event, predicate)
446457

447458
async def goBack(
448459
self, timeout: int = None, waitUntil: DocumentLoadState = None,
@@ -480,20 +491,19 @@ async def addInitScript(self, source: str = None, path: str = None) -> None:
480491
raise Error("Either path or source parameter must be specified")
481492
await self._channel.send("addInitScript", dict(source=source))
482493

483-
async def route(self, match: URLMatch, handler: RouteHandler) -> None:
484-
self._routes.append(RouteHandlerEntry(URLMatcher(match), handler))
494+
async def route(self, url: URLMatch, handler: RouteHandler) -> None:
495+
self._routes.append(RouteHandlerEntry(URLMatcher(url), handler))
485496
if len(self._routes) == 1:
486497
await self._channel.send(
487498
"setNetworkInterceptionEnabled", dict(enabled=True)
488499
)
489500

490501
async def unroute(
491-
self, match: URLMatch, handler: Optional[RouteHandler] = None
502+
self, url: URLMatch, handler: Optional[RouteHandler] = None
492503
) -> None:
493504
self._routes = list(
494505
filter(
495-
lambda r: r.matcher.match != match
496-
or (handler and r.handler != handler),
506+
lambda r: r.matcher.match != url or (handler and r.handler != handler),
497507
self._routes,
498508
)
499509
)
@@ -713,39 +723,3 @@ async def call(self, func: FunctionWithSource) -> None:
713723
asyncio.ensure_future(
714724
self._channel.send("reject", dict(error=serialize_error(e, tb)))
715725
)
716-
717-
718-
async def wait_for_event(
719-
target: Union[Page, "BrowserContext"],
720-
timeout_settings: TimeoutSettings,
721-
event: str,
722-
predicate: Callable[[Any], bool] = None,
723-
timeout: int = None,
724-
) -> Any:
725-
if timeout is None:
726-
timeout = timeout_settings.timeout()
727-
if timeout == 0:
728-
timeout = 3600 * 24 * 7 * 30 * 365
729-
timeout_future: asyncio.Future = asyncio.ensure_future(
730-
asyncio.sleep(timeout / 1000)
731-
)
732-
733-
future = target._scope._loop.create_future()
734-
735-
def listener(e: Any = None) -> None:
736-
if not predicate or predicate(e):
737-
future.set_result(e)
738-
739-
target.on(event, listener)
740-
741-
pending_event = PendingWaitEvent(event, future, timeout_future)
742-
target._pending_wait_for_events.append(pending_event)
743-
done, _ = await asyncio.wait(
744-
{timeout_future, future}, return_when=asyncio.FIRST_COMPLETED
745-
)
746-
target.remove_listener(event, listener)
747-
target._pending_wait_for_events.remove(pending_event)
748-
if future in done:
749-
timeout_future.cancel()
750-
return future.result()
751-
raise TimeoutError("Timeout exceeded")

0 commit comments

Comments
 (0)