Skip to content

Commit

Permalink
add sync tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt committed Oct 21, 2024
1 parent 7c963ed commit 367892a
Show file tree
Hide file tree
Showing 3 changed files with 364 additions and 0 deletions.
14 changes: 14 additions & 0 deletions tests/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ def done_cb(task: asyncio.Task) -> None:
future.add_done_callback(done_cb)
yield cb_wrapper

@contextlib.contextmanager
def expect_websocket(
self,
) -> Generator[ExpectResponse["WebSocketProtocol"], None, None]:
future = self.wait_for_web_socket()

cb_wrapper: ExpectResponse["WebSocketProtocol"] = ExpectResponse()

def done_cb(_: asyncio.Future) -> None:
cb_wrapper._value = future.result()

future.add_done_callback(done_cb)
yield cb_wrapper

def set_auth(self, path: str, username: str, password: str) -> None:
self.auth[path] = (username, password)

Expand Down
34 changes: 34 additions & 0 deletions tests/sync/test_page_request_gc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from playwright.sync_api import Page
from tests.server import Server


def test_should_work(page: Page, server: Server) -> None:
page.evaluate(
"""() => {
globalThis.objectToDestroy = { hello: 'world' };
globalThis.weakRef = new WeakRef(globalThis.objectToDestroy);
}"""
)
page.request_gc()
assert page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"}

page.request_gc()
assert page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"}

page.evaluate("() => globalThis.objectToDestroy = null")
page.request_gc()
assert page.evaluate("() => globalThis.weakRef.deref()") is None
316 changes: 316 additions & 0 deletions tests/sync/test_route_web_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import re
import time
from typing import Any, Awaitable, Callable, Literal, Optional, Union

from playwright.sync_api import Frame, Page, WebSocketRoute
from tests.server import Server, WebSocketProtocol


def assert_equal(
actual_cb: Callable[[], Union[Any, Awaitable[Any]]], expected: Any
) -> None:
__tracebackhide__ = True
start_time = time.time()
attempts = 0
while True:
actual = actual_cb()
if actual == expected:
return
attempts += 1
if time.time() - start_time > 10:
raise TimeoutError(f"Timed out after 10 seconds. Last actual was: {actual}")
time.sleep(0.1)


def setup_ws(
target: Union[Page, Frame],
port: int,
protocol: Union[Literal["blob"], Literal["arraybuffer"]],
) -> None:
target.goto("about:blank")
target.evaluate(
"""({ port, binaryType }) => {
window.log = [];
window.ws = new WebSocket('ws://localhost:' + port + '/ws');
window.ws.binaryType = binaryType;
window.ws.addEventListener('open', () => window.log.push('open'));
window.ws.addEventListener('close', event => window.log.push(`close code=${event.code} reason=${event.reason} wasClean=${event.wasClean}`));
window.ws.addEventListener('error', event => window.log.push(`error`));
window.ws.addEventListener('message', async event => {
let data;
if (typeof event.data === 'string')
data = event.data;
else if (event.data instanceof Blob)
data = 'blob:' + event.data.text();
else
data = 'arraybuffer:' + (new Blob([event.data])).text();
window.log.push(`message: data=${data} origin=${event.origin} lastEventId=${event.lastEventId}`);
});
window.wsOpened = new Promise(f => window.ws.addEventListener('open', () => f()));
}""",
{"port": port, "binaryType": protocol},
)


def test_should_work_with_ws_close(page: Page, server: Server) -> None:
route: Optional["WebSocketRoute"] = None

def _handle_ws(ws: WebSocketRoute) -> None:
ws.connect_to_server()
nonlocal route
route = ws

page.route_web_socket(re.compile(".*"), _handle_ws)

with server.expect_websocket() as ws_task:
setup_ws(page, server.PORT, "blob")
page.evaluate("window.wsOpened")
ws = ws_task.value
assert route
route.send("hello")
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=hello origin=ws://localhost:{server.PORT} lastEventId=",
],
)

closed_event = []
ws.events.once("close", lambda code, reason: closed_event.append((code, reason)))
route.close(code=3009, reason="oops")
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=hello origin=ws://localhost:{server.PORT} lastEventId=",
"close code=3009 reason=oops wasClean=true",
],
)
assert closed_event == [(3009, "oops")]


def test_should_pattern_match(page: Page, server: Server) -> None:
page.route_web_socket(re.compile(r".*/ws$"), lambda ws: ws.connect_to_server())
page.route_web_socket(
"**/mock-ws", lambda ws: ws.on_message(lambda message: ws.send("mock-response"))
)

page.goto("about:blank")
with server.expect_websocket() as ws_info:
page.evaluate(
"""async ({ port }) => {
window.log = [];
window.ws1 = new WebSocket('ws://localhost:' + port + '/ws');
window.ws1.addEventListener('message', event => window.log.push(`ws1:${event.data}`));
window.ws2 = new WebSocket('ws://localhost:' + port + '/something/something/mock-ws');
window.ws2.addEventListener('message', event => window.log.push(`ws2:${event.data}`));
await Promise.all([
new Promise(f => window.ws1.addEventListener('open', f)),
new Promise(f => window.ws2.addEventListener('open', f)),
]);
}""",
{"port": server.PORT},
)
ws = ws_info.value
ws.events.on("message", lambda payload, isBinary: ws.sendMessage(b"response"))

page.evaluate("window.ws1.send('request')")
assert_equal(lambda: page.evaluate("window.log"), ["ws1:response"])

page.evaluate("window.ws2.send('request')")
assert_equal(
lambda: page.evaluate("window.log"), ["ws1:response", "ws2:mock-response"]
)


def test_should_work_with_server(page: Page, server: Server) -> None:
route = None

def _handle_ws(ws: WebSocketRoute) -> None:
server = ws.connect_to_server()

def _ws_on_message(message: Union[str, bytes]) -> None:
if message == "to-respond":
ws.send("response")
return
if message == "to-block":
return
if message == "to-modify":
server.send("modified")
return
server.send(message)

ws.on_message(_ws_on_message)

def _server_on_message(message: Union[str, bytes]) -> None:
if message == "to-block":
return
if message == "to-modify":
ws.send("modified")
return
ws.send(message)

server.on_message(_server_on_message)
server.send("fake")
nonlocal route
route = ws

page.route_web_socket(re.compile(".*"), _handle_ws)
log = []

def _once_web_socket_connection(ws: WebSocketProtocol) -> None:
ws.events.on(
"message", lambda data, is_binary: log.append(f"message: {data.decode()}")
)
ws.events.on(
"close",
lambda code, reason: log.append(f"close: code={code} reason={reason}"),
)

server.once_web_socket_connection(_once_web_socket_connection)

with server.expect_websocket() as ws_info:
setup_ws(page, server.PORT, "blob")
page.evaluate("window.wsOpened")
ws = ws_info.value
assert_equal(lambda: log, ["message: fake"])

ws.sendMessage(b"to-modify")
ws.sendMessage(b"to-block")
ws.sendMessage(b"pass-server")
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=modified origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=pass-server origin=ws://localhost:{server.PORT} lastEventId=",
],
)

page.evaluate(
"""() => {
window.ws.send('to-respond');
window.ws.send('to-modify');
window.ws.send('to-block');
window.ws.send('pass-client');
}"""
)
assert_equal(
lambda: log, ["message: fake", "message: modified", "message: pass-client"]
)
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=modified origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=pass-server origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
],
)
assert route
route.send("another")
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=modified origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=pass-server origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=another origin=ws://localhost:{server.PORT} lastEventId=",
],
)

page.evaluate(
"""() => {
window.ws.send('pass-client-2');
}"""
)
assert_equal(
lambda: log,
[
"message: fake",
"message: modified",
"message: pass-client",
"message: pass-client-2",
],
)

page.evaluate(
"""() => {
window.ws.close(3009, 'problem');
}"""
)
assert_equal(
lambda: log,
[
"message: fake",
"message: modified",
"message: pass-client",
"message: pass-client-2",
"close: code=3009 reason=problem",
],
)


def test_should_work_without_server(page: Page, server: Server) -> None:
route = None

def _handle_ws(ws: WebSocketRoute) -> None:
def _ws_on_message(message: Union[str, bytes]) -> None:
if message == "to-respond":
ws.send("response")

ws.on_message(_ws_on_message)
nonlocal route
route = ws

page.route_web_socket(re.compile(".*"), _handle_ws)
setup_ws(page, server.PORT, "blob")

page.evaluate(
"""async () => {
await window.wsOpened;
window.ws.send('to-respond');
window.ws.send('to-block');
window.ws.send('to-respond');
}"""
)

assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
],
)
assert route
route.send("another")
# wait for the message to be processed
page.wait_for_timeout(100)
route.close(code=3008, reason="oops")
assert_equal(
lambda: page.evaluate("window.log"),
[
"open",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=response origin=ws://localhost:{server.PORT} lastEventId=",
f"message: data=another origin=ws://localhost:{server.PORT} lastEventId=",
"close code=3008 reason=oops wasClean=true",
],
)

0 comments on commit 367892a

Please sign in to comment.