summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/websockets/example/tutorial
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/websockets/example/tutorial')
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css105
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js45
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico2
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py65
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html10
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js53
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py190
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js83
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py198
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html15
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js93
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt1
15 files changed, 938 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css
new file mode 100644
index 0000000000..27f0baf6e4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.css
@@ -0,0 +1,105 @@
+/* General layout */
+
+body {
+ background-color: white;
+ display: flex;
+ flex-direction: column-reverse;
+ justify-content: center;
+ align-items: center;
+ margin: 0;
+ min-height: 100vh;
+}
+
+/* Action buttons */
+
+.actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-items: flex-end;
+ width: 720px;
+ height: 100px;
+}
+
+.action {
+ color: darkgray;
+ font-family: "Helvetica Neue", sans-serif;
+ font-size: 20px;
+ line-height: 20px;
+ font-weight: 300;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ padding: 20px;
+ width: 120px;
+}
+
+.action:hover {
+ background-color: darkgray;
+ color: white;
+ font-weight: 700;
+}
+
+.action[href=""] {
+ display: none;
+}
+
+/* Connect Four board */
+
+.board {
+ background-color: blue;
+ display: flex;
+ flex-direction: row;
+ padding: 0 10px;
+ position: relative;
+}
+
+.board::before,
+.board::after {
+ background-color: blue;
+ content: "";
+ height: 720px;
+ width: 20px;
+ position: absolute;
+}
+
+.board::before {
+ left: -20px;
+}
+
+.board::after {
+ right: -20px;
+}
+
+.column {
+ display: flex;
+ flex-direction: column-reverse;
+ padding: 10px;
+}
+
+.cell {
+ border-radius: 50%;
+ width: 80px;
+ height: 80px;
+ margin: 10px 0;
+}
+
+.empty {
+ background-color: white;
+}
+
+.column:hover .empty {
+ background-color: lightgray;
+}
+
+.column:hover .empty ~ .empty {
+ background-color: white;
+}
+
+.red {
+ background-color: red;
+}
+
+.yellow {
+ background-color: yellow;
+}
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js
new file mode 100644
index 0000000000..cb5eb9fa27
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.js
@@ -0,0 +1,45 @@
+const PLAYER1 = "red";
+
+const PLAYER2 = "yellow";
+
+function createBoard(board) {
+ // Inject stylesheet.
+ const linkElement = document.createElement("link");
+ linkElement.href = import.meta.url.replace(".js", ".css");
+ linkElement.rel = "stylesheet";
+ document.head.append(linkElement);
+ // Generate board.
+ for (let column = 0; column < 7; column++) {
+ const columnElement = document.createElement("div");
+ columnElement.className = "column";
+ columnElement.dataset.column = column;
+ for (let row = 0; row < 6; row++) {
+ const cellElement = document.createElement("div");
+ cellElement.className = "cell empty";
+ cellElement.dataset.column = column;
+ columnElement.append(cellElement);
+ }
+ board.append(columnElement);
+ }
+}
+
+function playMove(board, player, column, row) {
+ // Check values of arguments.
+ if (player !== PLAYER1 && player !== PLAYER2) {
+ throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`);
+ }
+ const columnElement = board.querySelectorAll(".column")[column];
+ if (columnElement === undefined) {
+ throw new RangeError("column must be between 0 and 6.");
+ }
+ const cellElement = columnElement.querySelectorAll(".cell")[row];
+ if (cellElement === undefined) {
+ throw new RangeError("row must be between 0 and 5.");
+ }
+ // Place checker in cell.
+ if (!cellElement.classList.replace("empty", player)) {
+ throw new Error("cell must be empty.");
+ }
+}
+
+export { PLAYER1, PLAYER2, createBoard, playMove };
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py
new file mode 100644
index 0000000000..0a61e7c7ee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/connect4.py
@@ -0,0 +1,62 @@
+__all__ = ["PLAYER1", "PLAYER2", "Connect4"]
+
+PLAYER1, PLAYER2 = "red", "yellow"
+
+
+class Connect4:
+ """
+ A Connect Four game.
+
+ Play moves with :meth:`play`.
+
+ Get past moves with :attr:`moves`.
+
+ Check for a victory with :attr:`winner`.
+
+ """
+
+ def __init__(self):
+ self.moves = []
+ self.top = [0 for _ in range(7)]
+ self.winner = None
+
+ @property
+ def last_player(self):
+ """
+ Player who played the last move.
+
+ """
+ return PLAYER1 if len(self.moves) % 2 else PLAYER2
+
+ @property
+ def last_player_won(self):
+ """
+ Whether the last move is winning.
+
+ """
+ b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2])
+ return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9])
+
+ def play(self, player, column):
+ """
+ Play a move in a column.
+
+ Returns the row where the checker lands.
+
+ Raises :exc:`RuntimeError` if the move is illegal.
+
+ """
+ if player == self.last_player:
+ raise RuntimeError("It isn't your turn.")
+
+ row = self.top[column]
+ if row == 6:
+ raise RuntimeError("This slot is full.")
+
+ self.moves.append((player, column, row))
+ self.top[column] += 1
+
+ if self.winner is None and self.last_player_won:
+ self.winner = self.last_player
+
+ return row
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico
new file mode 100644
index 0000000000..602c14e4eb
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico
@@ -0,0 +1,2 @@
+ h& ��( >��-=��v=��`@��B��U=���;���;���;���B��#J��VE���A���=���<���;���<���R��WN���I���E���E��%>���;���;���;���=��*\��VU���Q���M���N��$K��cE���A���=���D���`����q9 ^���]���Z���U��$R��dM���I���F���g��j�j1��i0��k1Cf��b���_��#[��eU���Q���N���R���t8w�k2��i0��k19d��J]���Y���U���R���|>s�v:��r7��k3�����k2u�j50f��da���]���^����Gk�~@��z>��w<Ҧs@�l2��q��zE�h��;p��ēN��I���C��Bѳ�@�x<��o5��M��o7�ȔOJ��M���J���F(�A��z=��v:��p5ɪw3ϘS%ŒP���L���JﳂC��~A��{>ɪwDʖS�ŒP���M���J���CŻ�D͗UBƔPyÏMY̙f����������������( @ <��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��U��U��U��U��U��U��U��U��B��<���<���<���;���<���<��M<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��<��A��3<���;���;���;���;���;���;���;���U��U��U��U��U��U��U��U��U��U��U��U��U��U��U��>��>��>��>��>��>��>��F��3A���?���>���;���;���;���;���;���;���<���<��<��<��<��<��<��<��<��<��<��<��<��<��<��=��=��=��=��=��=��J��4F���D���B���?���>���<���;���;���;���;���;���>��->��>��>��>��>��>��>��>��>��>��>��>��>��<��<��<��<��<��M��5J���G���E���C���B���@���=���<���;���;���;���;���=���=��=��=��=��=��=��=��=��=��=��=��=��=��U��U��U��U��R��5N���K���I���G���E���C���B���B��A��'<���;���;���;���;���;���<��Y<��<��<��<��<��<��<��<��<��<��<��<��<��<��U��6S���Q���N���M���K���I���H���H��H��B��U@���>���<���;���;���;���;���<���U��U��U��U��U��U��U��U��U��U���j2�j2Z��6V���T���R���P���O���M���L���L��L��H��JE���A���@���=���<���;���;���;���;���<���<��<��<��<��<��<��<��<��<���k2^��1Z���X���V���T���S���P���P���P��P��K��KH���F���C���B���@���=���<���@���a���}�k��zOŜj2$�j2�j2�j2�j2�j2�j2�j2�j2�i1_���^���\���Z���X���V���V���V��V��M��LL���I���G���F���C���B���@����m��i1��i0��i0��i0��k2u�k2�k2�k2�k2�k2�k2�k2�k2�k1b���_���]���[���Z���Z���Z��Z��T��LR���N���M���J���I���H���G��sG���r6/�j0��i0��i0��i0��i1��i1�i1�i1�i1�i1�i1�i1�i1�k1d���a���`���]���]���]��]��V��MU���R���Q���N���M���K���J��rJ��J���r7p�l2��j1��i0��i0��k1��k1�k1�k1�k1�k1�k1�k1�k1��U���f��sa���a��_a��a��\��NW���V���T���R���P���O���M��qM��M���t8e�q6��o4��l2��j1��i0��k1X�k1�k1�k1�k1�k1�k1�k1�k1��U��U��U��U��U��Ub��F^���\���Z���X���W���T���T��pT��T���{<f�w;��s8��q6��o5��k2��j1�q9 �q9�q9�j2W��U��U��U��U��U�i0�i0�i0�i0�i0�i0a���_���^���[���Z���X���W��pW��W���~@g�z>��x<��v:��s8��r7��p5��l2B�l2�l2�j1}�i0��j0���U��U��U��U�i0�i0�i0�i0�i0���d���a���`���^���[���\��o\��\����Eg��B��~@��z=��y<��v:��t9��r7S�r7�r7�k3~�i0��|J��i0��i0��i0�i0�i0�j1�j1�j1�j1�j1�j1g���c���a���_���_��n_��_����JS��E���C���B��|?��z=��x<��v;R�v;�v;�n4�l2���\�����è���i0�i0�i0�i0�k1�k1�k1�k1�k1�k1���i��ff���d��Bd��d��ĝN
+��I���H���E���C��A��|?��~?Q�~?�~?�t8�q6��n4��t=�Ѽ���N��j1љj1�j1�j1�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5�o5M`��J���H���G���D���C���BQ��B��B�z>��t9��s8��q6��m3��l3��k1��k19�k1�k1�k1�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5�s5ÐO���M���J���H���G���G^��G��G�}?��z=��y<��u9��s8��o5��o4��o5>�o5�o5�o5�o5�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;�u;ƑO�ÐO���L���J���I���G2��G��C���B��|?��z=��v:��u9��t7��s5>�s5�s5�s5�s5�s5�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?�}?ʕTtőO�ÐN���M���I���I귆Gʵ�D���C��~@��|?��z=��x<�u;=�u;�u;�u;�u;�u;�u;��D��D��D��D��D��D��D��D��D��D��D��D̙W#ɖS�ŒP�ÐN���K���J���H���F���D���C��}@��|?�}?=�}?�}?�}?�}?�}?�}?�}?��I��I��I��I��I��I��I��I��I��I��I��I��I̘T�ǓQ�őO�ÏN���L���J���I���F���D���C�D<��D��D��D��D��D��D��D��D’U’U’U’U’U’U’U’U’U’U’U’U’U��U̗T�ǔQ�ŒP�ÏN���K���J���H���F鶄I8��I��I��I��I��I��I��I��I��I˘WOȓQ�œP�ÑN���Lھ�L�’U’U’U’U’U’U’U’U’U’U’U�����������������?��?����������0��`������@������������ ��0 ����0�� ��?�����������������������
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py
new file mode 100644
index 0000000000..3b0fbd7868
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/app.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+import asyncio
+import itertools
+import json
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+async def handler(websocket):
+ # Initialize a Connect Four game.
+ game = Connect4()
+
+ # Players take alternate turns, using the same browser.
+ turns = itertools.cycle([PLAYER1, PLAYER2])
+ player = next(turns)
+
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ event = {
+ "type": "error",
+ "message": str(exc),
+ }
+ await websocket.send(json.dumps(event))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ await websocket.send(json.dumps(event))
+
+ # Alternate turns.
+ player = next(turns)
+
+
+async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html
new file mode 100644
index 0000000000..8e38e89922
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js
new file mode 100644
index 0000000000..dd28f9a6a8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step1/main.js
@@ -0,0 +1,53 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py
new file mode 100644
index 0000000000..2693d4304d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/app.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+import secrets
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+JOIN = {}
+
+WATCH = {}
+
+
+async def error(websocket, message):
+ """
+ Send an error message.
+
+ """
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def replay(websocket, game):
+ """
+ Send previous moves.
+
+ """
+ # Make a copy to avoid an exception if game.moves changes while iteration
+ # is in progress. If a move is played while replay is running, moves will
+ # be sent out of order but each move will be sent once and eventually the
+ # UI will be consistent.
+ for player, column, row in game.moves.copy():
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def play(websocket, game, player, connected):
+ """
+ Receive and process moves from a player.
+
+ """
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ await error(websocket, str(exc))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+
+async def start(websocket):
+ """
+ Handle a connection from the first player: start a new game.
+
+ """
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access tokens.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ watch_key = secrets.token_urlsafe(12)
+ WATCH[watch_key] = game, connected
+
+ try:
+ # Send the secret access tokens to the browser of the first player,
+ # where they'll be used for building "join" and "watch" links.
+ event = {
+ "type": "init",
+ "join": join_key,
+ "watch": watch_key,
+ }
+ await websocket.send(json.dumps(event))
+ # Receive and process moves from the first player.
+ await play(websocket, game, PLAYER1, connected)
+ finally:
+ del JOIN[join_key]
+ del WATCH[watch_key]
+
+
+async def join(websocket, join_key):
+ """
+ Handle a connection from the second player: join an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send the first move, in case the first player already played it.
+ await replay(websocket, game)
+ # Receive and process moves from the second player.
+ await play(websocket, game, PLAYER2, connected)
+ finally:
+ connected.remove(websocket)
+
+
+async def watch(websocket, watch_key):
+ """
+ Handle a connection from a spectator: watch an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = WATCH[watch_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send previous moves, in case the game already started.
+ await replay(websocket, game)
+ # Keep the connection open, but don't receive any messages.
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+
+async def handler(websocket):
+ """
+ Handle a connection and dispatch it according to who is connecting.
+
+ """
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ elif "watch" in event:
+ # Spectator watches an existing game.
+ await watch(websocket, event["watch"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+
+async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html
new file mode 100644
index 0000000000..1a16f72a25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="actions">
+ <a class="action new" href="/">New</a>
+ <a class="action join" href="">Join</a>
+ <a class="action watch" href="">Watch</a>
+ </div>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js
new file mode 100644
index 0000000000..d38a0140ac
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step2/main.js
@@ -0,0 +1,83 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else if (params.has("watch")) {
+ // Spectator watches an existing game.
+ event.watch = params.get("watch");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "init":
+ // Create links for inviting the second player and spectators.
+ document.querySelector(".join").href = "?join=" + event.join;
+ document.querySelector(".watch").href = "?watch=" + event.watch;
+ break;
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // Don't send moves for a spectator watching a game.
+ const params = new URLSearchParams(window.location.search);
+ if (params.has("watch")) {
+ return;
+ }
+
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ initGame(websocket);
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile
new file mode 100644
index 0000000000..2e35818f67
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/Procfile
@@ -0,0 +1 @@
+web: python app.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py
new file mode 100644
index 0000000000..c2ee020d20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/app.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+
+import asyncio
+import json
+import os
+import secrets
+import signal
+
+import websockets
+
+from connect4 import PLAYER1, PLAYER2, Connect4
+
+
+JOIN = {}
+
+WATCH = {}
+
+
+async def error(websocket, message):
+ """
+ Send an error message.
+
+ """
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def replay(websocket, game):
+ """
+ Send previous moves.
+
+ """
+ # Make a copy to avoid an exception if game.moves changes while iteration
+ # is in progress. If a move is played while replay is running, moves will
+ # be sent out of order but each move will be sent once and eventually the
+ # UI will be consistent.
+ for player, column, row in game.moves.copy():
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+
+
+async def play(websocket, game, player, connected):
+ """
+ Receive and process moves from a player.
+
+ """
+ async for message in websocket:
+ # Parse a "play" event from the UI.
+ event = json.loads(message)
+ assert event["type"] == "play"
+ column = event["column"]
+
+ try:
+ # Play the move.
+ row = game.play(player, column)
+ except RuntimeError as exc:
+ # Send an "error" event if the move was illegal.
+ await error(websocket, str(exc))
+ continue
+
+ # Send a "play" event to update the UI.
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+ # If move is winning, send a "win" event.
+ if game.winner is not None:
+ event = {
+ "type": "win",
+ "player": game.winner,
+ }
+ websockets.broadcast(connected, json.dumps(event))
+
+
+async def start(websocket):
+ """
+ Handle a connection from the first player: start a new game.
+
+ """
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access tokens.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ watch_key = secrets.token_urlsafe(12)
+ WATCH[watch_key] = game, connected
+
+ try:
+ # Send the secret access tokens to the browser of the first player,
+ # where they'll be used for building "join" and "watch" links.
+ event = {
+ "type": "init",
+ "join": join_key,
+ "watch": watch_key,
+ }
+ await websocket.send(json.dumps(event))
+ # Receive and process moves from the first player.
+ await play(websocket, game, PLAYER1, connected)
+ finally:
+ del JOIN[join_key]
+ del WATCH[watch_key]
+
+
+async def join(websocket, join_key):
+ """
+ Handle a connection from the second player: join an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send the first move, in case the first player already played it.
+ await replay(websocket, game)
+ # Receive and process moves from the second player.
+ await play(websocket, game, PLAYER2, connected)
+ finally:
+ connected.remove(websocket)
+
+
+async def watch(websocket, watch_key):
+ """
+ Handle a connection from a spectator: watch an existing game.
+
+ """
+ # Find the Connect Four game.
+ try:
+ game, connected = WATCH[watch_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+ # Send previous moves, in case the game already started.
+ await replay(websocket, game)
+ # Keep the connection open, but don't receive any messages.
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+
+async def handler(websocket):
+ """
+ Handle a connection and dispatch it according to who is connecting.
+
+ """
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ elif "watch" in event:
+ # Spectator watches an existing game.
+ await watch(websocket, event["watch"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+
+async def main():
+ # Set the stop condition when receiving SIGTERM.
+ loop = asyncio.get_running_loop()
+ stop = loop.create_future()
+ loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
+
+ port = int(os.environ.get("PORT", "8001"))
+ async with websockets.serve(handler, "", port):
+ await stop
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html
new file mode 100644
index 0000000000..1a16f72a25
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Connect Four</title>
+ </head>
+ <body>
+ <div class="actions">
+ <a class="action new" href="/">New</a>
+ <a class="action join" href="">Join</a>
+ <a class="action watch" href="">Watch</a>
+ </div>
+ <div class="board"></div>
+ <script src="main.js" type="module"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js
new file mode 100644
index 0000000000..3000fa2f78
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/main.js
@@ -0,0 +1,93 @@
+import { createBoard, playMove } from "./connect4.js";
+
+function getWebSocketServer() {
+ if (window.location.host === "python-websockets.github.io") {
+ return "wss://websockets-tutorial.herokuapp.com/";
+ } else if (window.location.host === "localhost:8000") {
+ return "ws://localhost:8001/";
+ } else {
+ throw new Error(`Unsupported host: ${window.location.host}`);
+ }
+}
+
+function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else if (params.has("watch")) {
+ // Spectator watches an existing game.
+ event.watch = params.get("watch");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+function showMessage(message) {
+ window.setTimeout(() => window.alert(message), 50);
+}
+
+function receiveMoves(board, websocket) {
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ switch (event.type) {
+ case "init":
+ // Create links for inviting the second player and spectators.
+ document.querySelector(".join").href = "?join=" + event.join;
+ document.querySelector(".watch").href = "?watch=" + event.watch;
+ break;
+ case "play":
+ // Update the UI with the move.
+ playMove(board, event.player, event.column, event.row);
+ break;
+ case "win":
+ showMessage(`Player ${event.player} wins!`);
+ // No further messages are expected; close the WebSocket connection.
+ websocket.close(1000);
+ break;
+ case "error":
+ showMessage(event.message);
+ break;
+ default:
+ throw new Error(`Unsupported event type: ${event.type}.`);
+ }
+ });
+}
+
+function sendMoves(board, websocket) {
+ // Don't send moves for a spectator watching a game.
+ const params = new URLSearchParams(window.location.search);
+ if (params.has("watch")) {
+ return;
+ }
+
+ // When clicking a column, send a "play" event for a move in that column.
+ board.addEventListener("click", ({ target }) => {
+ const column = target.dataset.column;
+ // Ignore clicks outside a column.
+ if (column === undefined) {
+ return;
+ }
+ const event = {
+ type: "play",
+ column: parseInt(column, 10),
+ };
+ websocket.send(JSON.stringify(event));
+ });
+}
+
+window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket(getWebSocketServer());
+ initGame(websocket);
+ receiveMoves(board, websocket);
+ sendMoves(board, websocket);
+});
diff --git a/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt
new file mode 100644
index 0000000000..14774b465e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt
@@ -0,0 +1 @@
+websockets