From ab2ef3e80339710125334f0bfc1bdd6392cd8154 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Fri, 6 Sep 2019 11:50:40 +0200 Subject: [PATCH] Add tests and expect library --- jest.config.js | 38 ++++----- src/network/{local.ts => LocalClient.ts} | 5 +- src/network/{peer.ts => NetworkPeer.ts} | 5 +- src/network/PeerClient.ts | 35 ++++++++ src/network/{server.ts => PeerServer.ts} | 12 ++- src/network/client.ts | 26 ------ src/network/index.ts | 8 +- src/network/types.ts | 2 +- src/testing/EventHook.ts | 63 +++++++++++++++ ...ataConnection.ts => MockDataConnection.ts} | 16 ++-- src/testing/{LocalPeer.ts => MockPeer.ts} | 39 +++++---- src/testing/index.ts | 2 + src/tests/unit/network.spec.ts | 80 +++++++++++++++++++ src/tests/unit/server.spec.ts | 23 ------ src/utils/index.ts | 1 + src/utils/timeout.ts | 9 +++ 16 files changed, 256 insertions(+), 108 deletions(-) rename src/network/{local.ts => LocalClient.ts} (74%) rename src/network/{peer.ts => NetworkPeer.ts} (80%) create mode 100644 src/network/PeerClient.ts rename src/network/{server.ts => PeerServer.ts} (94%) delete mode 100644 src/network/client.ts create mode 100644 src/testing/EventHook.ts rename src/testing/{LocalDataConnection.ts => MockDataConnection.ts} (94%) rename src/testing/{LocalPeer.ts => MockPeer.ts} (60%) create mode 100644 src/testing/index.ts create mode 100644 src/tests/unit/network.spec.ts delete mode 100644 src/tests/unit/server.spec.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/timeout.ts diff --git a/jest.config.js b/jest.config.js index 0d19e00..c1b9492 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,37 +1,27 @@ module.exports = { - moduleFileExtensions: [ - 'js', - 'jsx', - 'json', - 'vue', - 'ts', - 'tsx' - ], + moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], transform: { - '^.+\\.vue$': 'vue-jest', - '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', - '^.+\\.tsx?$': 'ts-jest' + "^.+\\.vue$": "vue-jest", + ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": + "jest-transform-stub", + "^.+\\.tsx?$": "ts-jest" }, - transformIgnorePatterns: [ - '/node_modules/' - ], + transformIgnorePatterns: ["/node_modules/"], moduleNameMapper: { - '^@/(.*)$': '/src/$1' + "^@/(.*)$": "/src/$1" }, - snapshotSerializers: [ - 'jest-serializer-vue' - ], + snapshotSerializers: ["jest-serializer-vue"], testMatch: [ - '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" ], - testURL: 'http://localhost/', + testURL: "http://localhost/", watchPlugins: [ - 'jest-watch-typeahead/filename', - 'jest-watch-typeahead/testname' + "jest-watch-typeahead/filename", + "jest-watch-typeahead/testname" ], globals: { - 'ts-jest': { + "ts-jest": { babelConfig: true } } -} +}; diff --git a/src/network/local.ts b/src/network/LocalClient.ts similarity index 74% rename from src/network/local.ts rename to src/network/LocalClient.ts index 6f5d4dc..154d818 100644 --- a/src/network/local.ts +++ b/src/network/LocalClient.ts @@ -1,14 +1,17 @@ import { PeerMetadata, NetworkMessage } from "./types"; +import EventEmitter from "eventemitter3"; -export class LocalClient { +export class LocalClient extends EventEmitter { public metadata: PeerMetadata; public receiver!: (data: NetworkMessage) => void; public constructor(metadata: PeerMetadata) { + super(); this.metadata = metadata; } public receive(data: NetworkMessage) { + this.emit("data", data); //TODO } diff --git a/src/network/peer.ts b/src/network/NetworkPeer.ts similarity index 80% rename from src/network/peer.ts rename to src/network/NetworkPeer.ts index d8402b9..b889266 100644 --- a/src/network/peer.ts +++ b/src/network/NetworkPeer.ts @@ -1,9 +1,11 @@ import Peer, { DataConnection } from "peerjs"; import { NetworkMessage } from "./types"; +import EventEmitter from "eventemitter3"; -export class NetworkPeer { +export class NetworkPeer extends EventEmitter { protected peer: Peer; protected constructor(peer?: Peer) { + super(); this.peer = peer ? peer : new Peer(); this.peer.on("open", function(id) { console.info("Peer ID assigned: %s", id); @@ -11,7 +13,6 @@ export class NetworkPeer { } protected send(conn: DataConnection, data: T) { - //TODO Debugging support? conn.send(data); } } diff --git a/src/network/PeerClient.ts b/src/network/PeerClient.ts new file mode 100644 index 0000000..f15a3dd --- /dev/null +++ b/src/network/PeerClient.ts @@ -0,0 +1,35 @@ +import NetworkPeer from "./NetworkPeer"; +import Peer, { DataConnection } from "peerjs"; +import { PeerMetadata, NetworkMessage } from "./types"; + +export class PeerClient extends NetworkPeer { + private connection?: DataConnection; + private metadata: PeerMetadata; + + public constructor(metadata: PeerMetadata, customPeer?: Peer) { + super(customPeer); + this.metadata = metadata; + } + + public connect(peerid: string) { + this.connection = this.peer.connect(peerid, { + metadata: this.metadata, + reliable: true + }); + if (!this.connection) { + throw new Error("Could not connect"); + } + this.connection.on("open", () => { + this.emit("connected"); + }); + this.connection.on("data", data => { + this._received(data); + }); + } + + private _received(data: NetworkMessage) { + this.emit("data", data); + } +} + +export default PeerClient; diff --git a/src/network/server.ts b/src/network/PeerServer.ts similarity index 94% rename from src/network/server.ts rename to src/network/PeerServer.ts index 1867bd0..ba0049e 100644 --- a/src/network/server.ts +++ b/src/network/PeerServer.ts @@ -1,4 +1,3 @@ -import NetworkPeer from "./peer"; import Peer, { DataConnection } from "peerjs"; import { RoomInfo, @@ -16,7 +15,7 @@ import { NetworkPlayer, AckMessage } from "./types"; -import LocalClient from "./local"; +import { NetworkPeer, LocalClient } from "."; // Increment name, add number at the end if not present // Examples: @@ -56,7 +55,9 @@ export class PeerServer extends NetworkPeer { info: roomInfo, players }; - this.peer.on("connection", this._connection.bind(this)); + this.peer.on("connection", conn => { + this._connection(conn); + }); } private _connection(conn: DataConnection) { @@ -77,7 +78,10 @@ export class PeerServer extends NetworkPeer { // Check if there is already a player called that way if (this.room.players[metadata.name]) { // Force rename - const newname = nextName(metadata.name); + let newname = metadata.name; + while (this.room.players[newname]) { + newname = nextName(metadata.name); + } this.send(conn, { kind: "rename", oldname: metadata.name, diff --git a/src/network/client.ts b/src/network/client.ts deleted file mode 100644 index 7019928..0000000 --- a/src/network/client.ts +++ /dev/null @@ -1,26 +0,0 @@ -import NetworkPeer from "./peer"; -import Peer, { DataConnection } from "peerjs"; -import { PeerMetadata, NetworkMessage } from "./types"; - -export class PeerClient extends NetworkPeer { - private connection: DataConnection; - public constructor( - peerid: string, - metadata: PeerMetadata, - customPeer?: Peer - ) { - super(customPeer); - this.connection = this.peer.connect(peerid, { - metadata, - reliable: true - }); - this.connection.on("open", () => { - console.info("Connected to server"); - }); - this.connection.on("data", this._received.bind(this)); - } - - private _received(data: NetworkMessage) {} -} - -export default PeerClient; diff --git a/src/network/index.ts b/src/network/index.ts index da0b10c..66b428b 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -1,5 +1,5 @@ -export * from "./client"; -export * from "./local"; -export * from "./peer"; -export * from "./server"; +export * from "./PeerClient"; +export * from "./LocalClient"; +export * from "./NetworkPeer"; +export * from "./PeerServer"; export * from "./types"; diff --git a/src/network/types.ts b/src/network/types.ts index d5b2033..e6991af 100644 --- a/src/network/types.ts +++ b/src/network/types.ts @@ -1,5 +1,5 @@ import { DataConnection } from "peerjs"; -import LocalClient from "./local"; +import LocalClient from "./LocalClient"; export interface LocalPlayer { kind: "local"; diff --git a/src/testing/EventHook.ts b/src/testing/EventHook.ts new file mode 100644 index 0000000..2ba2333 --- /dev/null +++ b/src/testing/EventHook.ts @@ -0,0 +1,63 @@ +import EventEmitter from "eventemitter3"; +import { withTimeout } from "@/utils"; + +interface HookedEvent { + date: Date; + arguments: any[]; +} + +const defaultCmp = () => {}; + +export class EventHook extends EventEmitter { + public backlog: Record = {}; + + public hookEmitter(emitter: EventEmitter, event: string, id: string) { + emitter.on(event, this._receivedEmitter.bind(this, id)); + } + + public expect( + id: string, + timeout: number = 1000, + cmp: (...args: any) => void = defaultCmp + ): Promise { + // Build promise + const promise = new Promise((resolve, reject) => { + const getLastEvent = () => { + // Take first element in the backlog + let event = this.backlog[id].shift(); + if (!event) { + // Should never happen + return reject(`undefined event in list (??)`); + } + // Send to check function, then resolve + resolve(cmp(...event.arguments)); + return; + }; + if (this.backlog[id] && this.backlog[id].length > 0) { + // Event is already in the backlog + getLastEvent(); + } else { + // If the event hasn't fired yet, subscribe to the next time it will + this.once(id, getLastEvent); + } + }); + + // Return promise with added timeout + return withTimeout(promise, timeout); + } + + // When events are received + private _receivedEmitter(id: string, ...args: any) { + const event: HookedEvent = { + date: new Date(), + arguments: args + }; + // Create backlog if it doesn't exist + if (!this.backlog[id]) { + this.backlog[id] = []; + } + // Push to backlog and emit event (for pending expects) + this.backlog[id].push(event); + this.emit(id, event); + } +} diff --git a/src/testing/LocalDataConnection.ts b/src/testing/MockDataConnection.ts similarity index 94% rename from src/testing/LocalDataConnection.ts rename to src/testing/MockDataConnection.ts index ee6e83f..92cda23 100644 --- a/src/testing/LocalDataConnection.ts +++ b/src/testing/MockDataConnection.ts @@ -1,19 +1,19 @@ import EventEmitter from "eventemitter3"; -import LocalPeer from "./LocalPeer"; +import MockPeer from "./MockPeer"; import { PeerConnectOption } from "peerjs"; -export default class LocalDataConnection extends EventEmitter { +export class MockDataConnection extends EventEmitter { public options: PeerConnectOption; public peer: string; - public provider: LocalPeer; + public provider: MockPeer; public open: boolean = false; - private _connection?: LocalDataConnection; + private _connection?: MockDataConnection; public id: string; public label: string; public constructor( peer: string, - provider: LocalPeer, + provider: MockPeer, options?: PeerConnectOption ) { super(); @@ -57,7 +57,7 @@ export default class LocalDataConnection extends EventEmitter { this.emit("close"); } - public _setRemote(connection: LocalDataConnection) { + public _setRemote(connection: MockDataConnection) { this._connection = connection; this._connection.on("open", () => { if (!this.open) { @@ -68,7 +68,7 @@ export default class LocalDataConnection extends EventEmitter { } public _configureDataChannel( - mocks: Record, + mocks: Record, options?: PeerConnectOption ) { if (!this._connection && this.peer in mocks) { @@ -217,3 +217,5 @@ export default class LocalDataConnection extends EventEmitter { throw new Error("not implemented"); } } + +export default MockDataConnection; diff --git a/src/testing/LocalPeer.ts b/src/testing/MockPeer.ts similarity index 60% rename from src/testing/LocalPeer.ts rename to src/testing/MockPeer.ts index 661fec1..ee02a24 100644 --- a/src/testing/LocalPeer.ts +++ b/src/testing/MockPeer.ts @@ -1,30 +1,35 @@ import EventEmitter from "eventemitter3"; import Peer, { PeerConnectOption, PeerJSOption } from "peerjs"; -import LocalDataConnection from "./LocalDataConnection"; +import MockDataConnection from "./MockDataConnection"; -let mocks: Record = {}; - -export default class LocalPeer extends EventEmitter { - public connections: Record; +export class MockPeer extends EventEmitter { + public connections: Record; public id: string; public options: PeerJSOption; public disconnected: boolean; public destroyed: boolean; public prototype: unknown; + private mocks: Record; - public constructor(id: string, options?: PeerJSOption) { + public constructor( + mockSource: Record, + id: string, + options?: PeerJSOption + ) { super(); this.id = id; this.options = options ? options : {}; this.connections = {}; this.disconnected = false; this.destroyed = false; - mocks[id] = this; + this.mocks = mockSource; + this.mocks[id] = this; } - public connect(id: string, options?: PeerConnectOption): LocalDataConnection { - let connection = new LocalDataConnection(id, this, options); - connection._configureDataChannel(mocks, options); + public connect(id: string, options?: PeerConnectOption): MockDataConnection { + let connection = new MockDataConnection(id, this, options); + // hacky way to avoid race conditions + setTimeout(() => connection._configureDataChannel(this.mocks, options), 1); this._addConnection(id, connection); return connection; } @@ -41,7 +46,7 @@ export default class LocalPeer extends EventEmitter { this.disconnect(); } - private _addConnection(peer: string, connection: LocalDataConnection) { + private _addConnection(peer: string, connection: MockDataConnection) { if (!this.connections[peer]) { this.connections[peer] = []; } @@ -50,10 +55,10 @@ export default class LocalPeer extends EventEmitter { public _negotiate( peer: string, - remoteConnection: LocalDataConnection, + remoteConnection: MockDataConnection, options?: PeerConnectOption ) { - let localConnection = new LocalDataConnection(peer, this, options); + let localConnection = new MockDataConnection(peer, this, options); localConnection._setRemote(remoteConnection); this._addConnection(peer, localConnection); this.emit("connection", localConnection); @@ -73,9 +78,9 @@ export default class LocalPeer extends EventEmitter { public getConnection( peerId: string, connectionId: string - ): LocalDataConnection | null { - if (peerId in mocks) { - let map = mocks[peerId].connections; + ): MockDataConnection | null { + if (peerId in this.mocks) { + let map = this.mocks[peerId].connections; if (connectionId in map) { return map[connectionId][0]; } @@ -86,3 +91,5 @@ export default class LocalPeer extends EventEmitter { public listAllPeers() {} } + +export default MockPeer; diff --git a/src/testing/index.ts b/src/testing/index.ts new file mode 100644 index 0000000..46190f0 --- /dev/null +++ b/src/testing/index.ts @@ -0,0 +1,2 @@ +export * from "./MockDataConnection"; +export * from "./MockPeer"; diff --git a/src/tests/unit/network.spec.ts b/src/tests/unit/network.spec.ts new file mode 100644 index 0000000..743e757 --- /dev/null +++ b/src/tests/unit/network.spec.ts @@ -0,0 +1,80 @@ +import Peer from "peerjs"; +import { MockPeer } from "@/testing"; +import { PeerServer, LocalClient, PeerClient, NetworkMessage } from "@/network"; +import { EventHook } from "@/testing/EventHook"; + +const sampleRoom = () => ({ + max_players: 3, + password: "" +}); + +function mockSource(): Record { + return {}; +} + +function createServer( + mockSource: Record, + id: string = "test-server", + name: string = "server-client" +) { + const serverPeer = new MockPeer(mockSource, id, {}); + const serverPlayer = new LocalClient({ + name: name + }); + return new PeerServer(sampleRoom(), serverPlayer, serverPeer as Peer); +} + +function createClient( + mockSource: Record, + id: string = "test-client", + name: string = "client-peer" +) { + const clientPeer = new MockPeer(mockSource, id, {}); + return new PeerClient({ name }, clientPeer as Peer); +} + +function messageKind(kind: string): (msg: NetworkMessage) => void { + return msg => { + expect(msg.kind).toEqual(kind); + }; +} + +describe("network/PeerServer", () => { + test("Create server", () => { + const mox = mockSource(); + createServer(mox); + }); + + test("Test client join", async () => { + const mox = mockSource(); + const hook = new EventHook(); + createServer(mox); + const client = createClient(mox); + hook.hookEmitter(client, "data", "client-data"); + client.connect("test-server"); + await hook.expect("client-data", 1000, messageKind("room-info")); + await hook.expect("client-data", 1000, messageKind("player-joined")); + }); + + test("Test multiple clients", async () => { + const mox = mockSource(); + const hook = new EventHook(); + createServer(mox); + const client1 = createClient(mox); + const client2 = createClient(mox); + hook.hookEmitter(client1, "data", "client1-data"); + hook.hookEmitter(client2, "data", "client2-data"); + client1.connect("test-server"); + await hook.expect("client1-data", 1000, messageKind("room-info")); + await hook.expect("client1-data", 1000, messageKind("player-joined")); + client2.connect("test-server"); + await hook.expect("client2-data", 1000, messageKind("rename")); + await hook.expect("client2-data", 1000, messageKind("room-info")); + await hook.expect("client2-data", 1000, messageKind("player-joined")); + await hook.expect("client1-data", 1000, messageKind("player-joined")); + }); +}); + +describe("network/PeerClient", () => { + //TODO Client tests +}); diff --git a/src/tests/unit/server.spec.ts b/src/tests/unit/server.spec.ts deleted file mode 100644 index 581aa91..0000000 --- a/src/tests/unit/server.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import LocalPeer from "@/testing/LocalPeer"; -import Peer from "peerjs"; -import { PeerServer, LocalClient, PeerClient } from "@/network"; - -describe("network/PeerServer", () => { - test("Create server and client", () => { - const serverPeer = new LocalPeer("test-server", {}); - const clientPeer = new LocalPeer("test-client", {}); - const serverPlayer = new LocalClient({ - name: "server-client" - }); - const roomInfo = { - max_players: 3, - password: "" - }; - const server = new PeerServer(roomInfo, serverPlayer, serverPeer as Peer); - const client = new PeerClient( - "test-server", - { name: "test" }, - clientPeer as Peer - ); - }); -}); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1a5ee3d --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./timeout"; diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts new file mode 100644 index 0000000..55ca11b --- /dev/null +++ b/src/utils/timeout.ts @@ -0,0 +1,9 @@ +export function withTimeout(promise: Promise, ms: number): Promise { + const timeout = new Promise((resolve, reject) => { + let id = setTimeout(() => { + clearTimeout(id); + reject("Timed out after " + ms + "ms."); + }, ms); + }); + return Promise.race([promise, timeout]); +}