Skip to content

Commit 06e0d62

Browse files
committed
test: cover websocket support with tests (#271)
1 parent 2ed1b07 commit 06e0d62

File tree

10 files changed

+314
-11
lines changed

10 files changed

+314
-11
lines changed

local-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
autobahn==20.7.1
12
pytest==6.1.0
23
pytest-asyncio==0.14.0
34
pytest-cov==2.10.1

playwright/async_api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,54 @@ def url(self) -> str:
506506
"""
507507
return mapping.from_maybe_impl(self._impl_obj.url)
508508

509+
async def waitForEvent(
510+
self,
511+
event: str,
512+
predicate: typing.Union[typing.Callable[[typing.Any], bool]] = None,
513+
timeout: int = None,
514+
) -> typing.Any:
515+
"""WebSocket.waitForEvent
516+
517+
Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value. Will throw an error if the webSocket is closed before the event
518+
is fired.
519+
520+
Parameters
521+
----------
522+
event : str
523+
Event name, same one would pass into `webSocket.on(event)`.
524+
525+
Returns
526+
-------
527+
Any
528+
Promise which resolves to the event data value.
529+
"""
530+
return mapping.from_maybe_impl(
531+
await self._impl_obj.waitForEvent(
532+
event=event, predicate=self._wrap_handler(predicate), timeout=timeout
533+
)
534+
)
535+
536+
def expect_event(
537+
self,
538+
event: str,
539+
predicate: typing.Union[typing.Callable[[typing.Any], bool]] = None,
540+
timeout: int = None,
541+
) -> AsyncEventContextManager:
542+
return AsyncEventContextManager(
543+
self._impl_obj.waitForEvent(event, predicate, timeout)
544+
)
545+
546+
def isClosed(self) -> bool:
547+
"""WebSocket.isClosed
548+
549+
Indicates that the web socket has been closed.
550+
551+
Returns
552+
-------
553+
bool
554+
"""
555+
return mapping.from_maybe_impl(self._impl_obj.isClosed())
556+
509557

510558
mapping.register(WebSocketImpl, WebSocket)
511559

playwright/network.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
import mimetypes
1818
from pathlib import Path
1919
from types import SimpleNamespace
20-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
20+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast
2121
from urllib import parse
2222

2323
from playwright.connection import ChannelOwner, from_channel, from_nullable_channel
24+
from playwright.event_context_manager import EventContextManagerImpl
2425
from playwright.helper import (
2526
ContinueParameters,
2627
Error,
@@ -29,6 +30,7 @@
2930
ResourceTiming,
3031
locals_to_params,
3132
)
33+
from playwright.wait_helper import WaitHelper
3234

3335
if TYPE_CHECKING: # pragma: no cover
3436
from playwright.frame import Frame
@@ -271,6 +273,7 @@ def __init__(
271273
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
272274
) -> None:
273275
super().__init__(parent, type, guid, initializer)
276+
self._is_closed = False
274277
self._channel.on(
275278
"frameSent",
276279
lambda params: self._on_frame_sent(params["opcode"], params["data"]),
@@ -282,12 +285,40 @@ def __init__(
282285
self._channel.on(
283286
"error", lambda params: self.emit(WebSocket.Events.Error, params["error"])
284287
)
285-
self._channel.on("close", lambda params: self.emit(WebSocket.Events.Close))
288+
self._channel.on("close", lambda params: self._on_close())
286289

287290
@property
288291
def url(self) -> str:
289292
return self._initializer["url"]
290293

294+
async def waitForEvent(
295+
self, event: str, predicate: Callable[[Any], bool] = None, timeout: int = None
296+
) -> Any:
297+
if timeout is None:
298+
timeout = cast(Any, self._parent)._timeout_settings.timeout()
299+
wait_helper = WaitHelper(self._loop)
300+
wait_helper.reject_on_timeout(
301+
timeout, f'Timeout while waiting for event "${event}"'
302+
)
303+
if event != WebSocket.Events.Close:
304+
wait_helper.reject_on_event(
305+
self, WebSocket.Events.Close, Error("Socket closed")
306+
)
307+
if event != WebSocket.Events.Error:
308+
wait_helper.reject_on_event(
309+
self, WebSocket.Events.Error, Error("Socket error")
310+
)
311+
wait_helper.reject_on_event(self._parent, "close", Error("Page closed"))
312+
return await wait_helper.wait_for_event(self, event, predicate)
313+
314+
def expect_event(
315+
self,
316+
event: str,
317+
predicate: Callable[[Any], bool] = None,
318+
timeout: int = None,
319+
) -> EventContextManagerImpl:
320+
return EventContextManagerImpl(self.waitForEvent(event, predicate, timeout))
321+
291322
def _on_frame_sent(self, opcode: int, data: str) -> None:
292323
if opcode == 2:
293324
self.emit(WebSocket.Events.FrameSent, base64.b64decode(data))
@@ -300,6 +331,13 @@ def _on_frame_received(self, opcode: int, data: str) -> None:
300331
else:
301332
self.emit(WebSocket.Events.FrameReceived, data)
302333

334+
def isClosed(self) -> bool:
335+
return self._is_closed
336+
337+
def _on_close(self) -> None:
338+
self._is_closed = True
339+
self.emit(WebSocket.Events.Close)
340+
303341

304342
def serialize_headers(headers: Dict[str, str]) -> List[Header]:
305343
return [{"name": name, "value": value} for name, value in headers.items()]

playwright/page.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,6 @@ def _add_event_handler(self, event: str, k: Any, v: Any) -> None:
294294
self._channel.send_no_reply(
295295
"setFileChooserInterceptedNoReply", {"intercepted": True}
296296
)
297-
if event == Page.Events.WebSocket and len(self.listeners(event)) == 0:
298-
self._channel.send_no_reply(
299-
"setWebSocketFramesReportingEnabledNoReply", {"enabled": True}
300-
)
301297
super()._add_event_handler(event, k, v)
302298

303299
def remove_listener(self, event: str, f: Any) -> None:
@@ -306,9 +302,6 @@ def remove_listener(self, event: str, f: Any) -> None:
306302
self._channel.send_no_reply(
307303
"setFileChooserInterceptedNoReply", {"intercepted": False}
308304
)
309-
# Note: we do not stop reporting web socket frames, since
310-
# user might not listen to 'websocket' anymore, but still have
311-
# a functioning WebSocket object.
312305

313306
@property
314307
def context(self) -> "BrowserContext":

playwright/sync_api.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,58 @@ def url(self) -> str:
512512
"""
513513
return mapping.from_maybe_impl(self._impl_obj.url)
514514

515+
def waitForEvent(
516+
self,
517+
event: str,
518+
predicate: typing.Union[typing.Callable[[typing.Any], bool]] = None,
519+
timeout: int = None,
520+
) -> typing.Any:
521+
"""WebSocket.waitForEvent
522+
523+
Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value. Will throw an error if the webSocket is closed before the event
524+
is fired.
525+
526+
Parameters
527+
----------
528+
event : str
529+
Event name, same one would pass into `webSocket.on(event)`.
530+
531+
Returns
532+
-------
533+
Any
534+
Promise which resolves to the event data value.
535+
"""
536+
return mapping.from_maybe_impl(
537+
self._sync(
538+
self._impl_obj.waitForEvent(
539+
event=event,
540+
predicate=self._wrap_handler(predicate),
541+
timeout=timeout,
542+
)
543+
)
544+
)
545+
546+
def expect_event(
547+
self,
548+
event: str,
549+
predicate: typing.Union[typing.Callable[[typing.Any], bool]] = None,
550+
timeout: int = None,
551+
) -> EventContextManager:
552+
return EventContextManager(
553+
self._loop, self._impl_obj.waitForEvent(event, predicate, timeout)
554+
)
555+
556+
def isClosed(self) -> bool:
557+
"""WebSocket.isClosed
558+
559+
Indicates that the web socket has been closed.
560+
561+
Returns
562+
-------
563+
bool
564+
"""
565+
return mapping.from_maybe_impl(self._impl_obj.isClosed())
566+
515567

516568
mapping.register(WebSocketImpl, WebSocket)
517569

scripts/documentation_provider.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ def print_entry(
8989
or super_clazz["methods"].get(method_name)
9090
)
9191
fqname = f"{class_name}.{method_name}"
92+
93+
if not method:
94+
self.errors.add(f"Method not documented: {fqname}")
95+
return
96+
9297
indent = " " * 8
9398
print(f'{indent}"""{class_name}.{original_method_name}')
9499
if method.get("comment"):

scripts/expected_api_mismatch.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ Method not implemented: Download.createReadStream
1919
Method not implemented: Logger.isEnabled
2020
Method not implemented: Logger.log
2121
Method not implemented: Page.coverage
22-
Method not implemented: WebSocket.isClosed
23-
Method not implemented: WebSocket.waitForEvent
2422

2523
# Parameter overloads
2624
Parameter not documented: BrowserContext.waitForEvent(predicate=)
@@ -30,6 +28,8 @@ Parameter not documented: Page.waitForEvent(timeout=)
3028
Parameter not documented: Page.waitForRequest(predicate=)
3129
Parameter not documented: Page.waitForResponse(predicate=)
3230
Parameter not documented: Selectors.register(path=)
31+
Parameter not documented: WebSocket.waitForEvent(timeout=)
32+
Parameter not documented: WebSocket.waitForEvent(predicate=)
3333

3434
# Documented as Dict / Any
3535
Parameter type mismatch in BrowserContext.setGeolocation(geolocation=): documented as Optional[Dict], code has Optional[{"latitude": float, "longitude": float, "accuracy": Optional[float]}]
@@ -42,6 +42,7 @@ Parameter type mismatch in Page.viewportSize(return=): documented as Optional[Di
4242
Parameter type mismatch in Page.waitForEvent(return=): documented as Dict, code has Any
4343
Parameter type mismatch in Request.failure(return=): documented as Optional[Dict], code has Optional[{"errorText": str}]
4444
Parameter type mismatch in Response.json(return=): documented as Any, code has Union[Dict, List]
45+
Parameter type mismatch in WebSocket.waitForEvent(return=): documented as Dict, code has Any
4546

4647
# Pathlib
4748
Parameter type mismatch in BrowserType.launch(executablePath=): documented as Optional[str], code has Union[str, pathlib.Path, NoneType]
@@ -118,3 +119,4 @@ Method not implemented: BrowserType.connect
118119
# OptionsOr
119120
Parameter not implemented: Page.waitForEvent(optionsOrPredicate=)
120121
Parameter not implemented: BrowserContext.waitForEvent(optionsOrPredicate=)
122+
Parameter not implemented: WebSocket.waitForEvent(optionsOrPredicate=)

tests/async/test_websocket.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 pytest
16+
17+
from playwright import Error
18+
19+
20+
async def test_should_work(page, ws_server):
21+
value = await page.evaluate(
22+
"""port => {
23+
let cb;
24+
const result = new Promise(f => cb = f);
25+
const ws = new WebSocket('ws://localhost:' + port + '/ws');
26+
ws.addEventListener('message', data => { ws.close(); cb(data.data); });
27+
return result;
28+
}""",
29+
ws_server.PORT,
30+
)
31+
assert value == "incoming"
32+
pass
33+
34+
35+
async def test_should_emit_close_events(page, ws_server):
36+
async with page.expect_event("websocket") as ws_info:
37+
await page.evaluate(
38+
"""port => {
39+
let cb;
40+
const result = new Promise(f => cb = f);
41+
const ws = new WebSocket('ws://localhost:' + port + '/ws');
42+
ws.addEventListener('message', data => { ws.close(); cb(data.data); });
43+
return result;
44+
}""",
45+
ws_server.PORT,
46+
)
47+
ws = await ws_info.value
48+
assert ws.url == f"ws://localhost:{ws_server.PORT}/ws"
49+
if not ws.isClosed():
50+
await ws.waitForEvent("close")
51+
assert ws.isClosed()
52+
53+
54+
async def test_should_emit_frame_events(page, ws_server):
55+
sent = []
56+
received = []
57+
58+
def on_web_socket(ws):
59+
ws.on("framesent", lambda payload: sent.append(payload))
60+
ws.on("framereceived", lambda payload: received.append(payload))
61+
62+
page.on("websocket", on_web_socket)
63+
async with page.expect_event("websocket") as ws_info:
64+
await page.evaluate(
65+
"""port => {
66+
const ws = new WebSocket('ws://localhost:' + port + '/ws');
67+
ws.addEventListener('open', () => {
68+
ws.send('echo-text');
69+
});
70+
}""",
71+
ws_server.PORT,
72+
)
73+
ws = await ws_info.value
74+
if not ws.isClosed():
75+
await ws.waitForEvent("close")
76+
77+
assert sent == ["echo-text"]
78+
assert received == ["incoming", "text"]
79+
80+
81+
async def test_should_emit_binary_frame_events(page, ws_server):
82+
sent = []
83+
received = []
84+
85+
def on_web_socket(ws):
86+
ws.on("framesent", lambda payload: sent.append(payload))
87+
ws.on("framereceived", lambda payload: received.append(payload))
88+
89+
page.on("websocket", on_web_socket)
90+
async with page.expect_event("websocket") as ws_info:
91+
await page.evaluate(
92+
"""port => {
93+
const ws = new WebSocket('ws://localhost:' + port + '/ws');
94+
ws.addEventListener('open', () => {
95+
const binary = new Uint8Array(5);
96+
for (let i = 0; i < 5; ++i)
97+
binary[i] = i;
98+
ws.send(binary);
99+
ws.send('echo-bin');
100+
});
101+
}""",
102+
ws_server.PORT,
103+
)
104+
ws = await ws_info.value
105+
if not ws.isClosed():
106+
await ws.waitForEvent("close")
107+
assert sent == [b"\x00\x01\x02\x03\x04", "echo-bin"]
108+
assert received == ["incoming", b"\x04\x02"]
109+
110+
111+
async def test_should_reject_wait_for_event_on_close_and_error(page, ws_server):
112+
async with page.expect_event("websocket") as ws_info:
113+
await page.evaluate(
114+
"""port => {
115+
window.ws = new WebSocket('ws://localhost:' + port + '/ws');
116+
}""",
117+
ws_server.PORT,
118+
)
119+
ws = await ws_info.value
120+
await ws.waitForEvent("framereceived")
121+
with pytest.raises(Error) as exc_info:
122+
async with ws.expect_event("framesent"):
123+
await page.evaluate("window.ws.close()")
124+
assert exc_info.value.message == "Socket closed"

0 commit comments

Comments
 (0)