Skip to content

Commit e57be96

Browse files
authored
test: page tests, part 1 (microsoft#45)
1 parent 81e49ed commit e57be96

26 files changed

+580
-77
lines changed

playwright/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
webkit = playwright_object.webkit
2121
devices = playwright_object.devices
2222
browser_types = playwright_object.browser_types
23+
Error = helper.Error
2324
TimeoutError = helper.TimeoutError
2425

25-
__all__ = ["browser_types", "chromium", "firefox", "webkit", "devices", "TimeoutError"]
26+
__all__ = [
27+
"browser_types",
28+
"chromium",
29+
"firefox",
30+
"webkit",
31+
"devices",
32+
"Error",
33+
"TimeoutError",
34+
]

playwright/browser_context.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
URLMatcher,
3131
)
3232
from playwright.network import Request, Route
33-
from playwright.page import BindingCall, Page
33+
from playwright.page import BindingCall, Page, wait_for_event
3434
from types import SimpleNamespace
3535
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
3636

@@ -85,14 +85,21 @@ def _on_binding(self, binding_call: BindingCall) -> None:
8585
func = self._bindings.get(binding_call._initializer["name"])
8686
if func is None:
8787
return
88-
binding_call.call(func)
88+
asyncio.ensure_future(binding_call.call(func))
8989

9090
def setDefaultNavigationTimeout(self, timeout: int) -> None:
91-
self._channel.send("setDefaultNavigationTimeoutNoReply", dict(timeout=timeout))
91+
self._timeout_settings.set_navigation_timeout(timeout)
92+
asyncio.ensure_future(
93+
self._channel.send(
94+
"setDefaultNavigationTimeoutNoReply", dict(timeout=timeout)
95+
)
96+
)
9297

9398
def setDefaultTimeout(self, timeout: int) -> None:
94-
self._timeout_settings.set_default_timeout(timeout)
95-
self._channel.send("setDefaultTimeoutNoReply", dict(timeout=timeout))
99+
self._timeout_settings.set_timeout(timeout)
100+
asyncio.ensure_future(
101+
self._channel.send("setDefaultTimeoutNoReply", dict(timeout=timeout))
102+
)
96103

97104
@property
98105
def pages(self) -> List[Page]:
@@ -175,15 +182,12 @@ async def unroute(self, match: URLMatch, handler: Optional[RouteHandler]) -> Non
175182
"setNetworkInterceptionEnabled", dict(enabled=False)
176183
)
177184

178-
async def waitForEvent(self, event: str) -> None:
179-
# TODO: implement timeout race
180-
future = self._scope._loop.create_future()
181-
self.once(event, lambda e: future.set_result(e))
182-
pending_event = PendingWaitEvent(event, future)
183-
self._pending_wait_for_events.append(pending_event)
184-
result = await future
185-
self._pending_wait_for_events.remove(pending_event)
186-
return result
185+
async def waitForEvent(
186+
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None
187+
) -> Any:
188+
return await wait_for_event(
189+
self, self._timeout_settings, event, predicate=predicate, timeout=timeout
190+
)
187191

188192
def _on_close(self):
189193
if self._browser:
@@ -192,9 +196,8 @@ def _on_close(self):
192196
for pending_event in self._pending_wait_for_events:
193197
if pending_event.event == BrowserContext.Events.Close:
194198
continue
195-
pending_event.future.set_exception(Error("Context closed"))
199+
pending_event.reject(False, "Context")
196200

197-
self._pending_wait_for_events.clear()
198201
self.emit(BrowserContext.Events.Close)
199202
self._scope.dispose()
200203

playwright/connection.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import asyncio
16+
import traceback
1617
from playwright.helper import parse_error, ParsedMessagePayload
1718
from playwright.transport import Transport
1819
from pyee import BaseEventEmitter
@@ -96,6 +97,12 @@ def create_remote_object(self, type: str, guid: str, initializer: Dict) -> Any:
9697
return result
9798

9899

100+
class ProtocolCallback:
101+
def __init__(self, loop: asyncio.AbstractEventLoop):
102+
self.stack_trace = "".join(traceback.format_stack()[-10:])
103+
self.future = loop.create_future()
104+
105+
99106
class Connection:
100107
def __init__(
101108
self,
@@ -111,7 +118,7 @@ def __init__(
111118
self._loop = loop
112119
self._objects: Dict[str, ChannelOwner] = dict()
113120
self._scopes: Dict[str, ConnectionScope] = dict()
114-
self._callbacks: Dict[int, asyncio.Future] = dict()
121+
self._callbacks: Dict[int, ProtocolCallback] = dict()
115122
self._root_scope = self.create_scope("", None)
116123
self._object_factory = object_factory
117124

@@ -134,9 +141,9 @@ async def _send_message_to_server(
134141
params=self._replace_channels_with_guids(params),
135142
)
136143
self._transport.send(message)
137-
callback = self._loop.create_future()
144+
callback = ProtocolCallback(self._loop)
138145
self._callbacks[id] = callback
139-
return await callback
146+
return await callback.future
140147

141148
def _dispatch(self, msg: ParsedMessagePayload):
142149

@@ -145,10 +152,12 @@ def _dispatch(self, msg: ParsedMessagePayload):
145152
callback = self._callbacks.pop(id)
146153
error = msg.get("error")
147154
if error:
148-
callback.set_exception(parse_error(error))
155+
parsed_error = parse_error(error)
156+
parsed_error.stack = callback.stack_trace
157+
callback.future.set_exception(parsed_error)
149158
else:
150159
result = self._replace_guids_with_channels(msg.get("result"))
151-
callback.set_result(result)
160+
callback.future.set_result(result)
152161
return
153162

154163
guid = msg["guid"]

playwright/dialog.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from playwright.connection import ChannelOwner, ConnectionScope
16+
from playwright.helper import locals_to_params
1617
from typing import Dict
1718

1819

@@ -33,7 +34,7 @@ def defaultValue(self) -> str:
3334
return self._initializer["defaultValue"]
3435

3536
async def accept(self, prompt_text: str = None) -> None:
36-
await self._channel.send("accept", dict(promptText=prompt_text))
37+
await self._channel.send("accept", locals_to_params(locals()))
3738

3839
async def dismiss(self) -> None:
3940
await self._channel.send("dismiss")

playwright/helper.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import asyncio
1616
import fnmatch
1717
import re
18+
import traceback
1819

1920
from typing import (
2021
Any,
@@ -40,7 +41,7 @@
4041
from playwright.network import Route, Request
4142

4243
Cookie = List[Dict[str, Union[str, int, bool]]]
43-
URLMatch = Union[str, Callable[[str], bool]]
44+
URLMatch = Union[str, Pattern, Callable[[str], bool]]
4445
RouteHandler = Callable[["Route", "Request"], None]
4546
FunctionWithSource = Callable[[Dict], Any]
4647

@@ -101,8 +102,10 @@ def __init__(self, match: URLMatch):
101102
self._callback: Optional[Callable[[str], bool]] = None
102103
self._regex_obj: Optional[Pattern] = None
103104
if isinstance(match, str):
104-
regex = "(?:http://|https://)" + fnmatch.translate(match)
105+
regex = fnmatch.translate(match)
105106
self._regex_obj = re.compile(regex)
107+
elif isinstance(match, Pattern):
108+
self._regex_obj = match
106109
else:
107110
self._callback = match
108111
self.match = match
@@ -111,16 +114,35 @@ def matches(self, url: str) -> bool:
111114
if self._callback:
112115
return self._callback(url)
113116
if self._regex_obj:
114-
return cast(bool, self._regex_obj.match(url))
117+
return cast(bool, self._regex_obj.search(url))
115118
return False
116119

117120

118121
class TimeoutSettings:
119122
def __init__(self, parent: Optional["TimeoutSettings"]) -> None:
120123
self._parent = parent
124+
self._timeout = 30000
125+
self._navigation_timeout = 30000
121126

122-
def set_default_timeout(self, timeout):
123-
self.timeout = timeout
127+
def set_timeout(self, timeout: int):
128+
self._timeout = timeout
129+
130+
def timeout(self) -> int:
131+
if self._timeout is not None:
132+
return self._timeout
133+
if self._parent:
134+
return self._parent.timeout()
135+
return 30000
136+
137+
def set_navigation_timeout(self, navigation_timeout: int):
138+
self._navigation_timeout = navigation_timeout
139+
140+
def navigation_timeout(self) -> int:
141+
if self._navigation_timeout is not None:
142+
return self._navigation_timeout
143+
if self._parent:
144+
return self._parent.navigation_timeout()
145+
return 30000
124146

125147

126148
class Error(Exception):
@@ -133,11 +155,11 @@ class TimeoutError(Error):
133155
pass
134156

135157

136-
def serialize_error(ex: Exception) -> ErrorPayload:
137-
return dict(message=str(ex))
158+
def serialize_error(ex: Exception, tb) -> ErrorPayload:
159+
return dict(message=str(ex), stack="".join(traceback.format_tb(tb)))
138160

139161

140-
def parse_error(error: ErrorPayload):
162+
def parse_error(error: ErrorPayload) -> Error:
141163
base_error_class = Error
142164
if error.get("name") == "TimeoutError":
143165
base_error_class = TimeoutError
@@ -164,9 +186,22 @@ def locals_to_params(args: Dict) -> Dict:
164186

165187

166188
class PendingWaitEvent:
167-
def __init__(self, event: str, future: asyncio.Future):
189+
def __init__(
190+
self, event: str, future: asyncio.Future, timeout_future: asyncio.Future
191+
):
168192
self.event = event
169193
self.future = future
194+
self.timeout_future = timeout_future
195+
196+
def reject(self, is_crash: bool, target: str):
197+
self.timeout_future.cancel()
198+
if self.event == "close" and not is_crash:
199+
return
200+
if self.event == "crash" and is_crash:
201+
return
202+
self.future.set_exception(
203+
Error(f"{target} crashed" if is_crash else f"{target} closed")
204+
)
170205

171206

172207
class RouteHandlerEntry:

0 commit comments

Comments
 (0)