Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tests): introduce tests #2

Merged
merged 5 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions __tests__/unit-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
} from "@jest/globals";
import type { AddressInfo } from "net";
import type { Socket as ClientSocket } from "socket.io-client";
import { ClientSocketManager } from "../src/index.ts";
import createPromiseResolvers from "./utils/promise-resolvers.ts";
import { httpServer, socketServer } from "./utils/server.ts";

describe("ClientSocketManager: unit tests", () => {
let httpServerAddr: AddressInfo | string | null = null;
let socketServerUri: string = "";
let socketManager: ClientSocketManager | null = null;

beforeAll(() => {
return new Promise<void>(resolve => {
httpServer.listen(3000, () => {
httpServerAddr = httpServer.address();

resolve();
});
});
});

afterAll(() => {
return new Promise<void>(resolve => {
void socketServer.close().then(() => {
httpServer.close(() => {
resolve();
});
});
});
});

beforeEach(() => {
if (!httpServerAddr) {
throw new Error(
`Expected valid http-server address. Received \`${httpServerAddr}\`.`,
);
}

socketServerUri =
typeof httpServerAddr === "string"
? httpServerAddr
: `http://localhost:${httpServerAddr.port}`;
});

afterEach(() => {
socketManager?.dispose();
socketManager = null;
});

it("should connect", async () => {
const connectResolver = createPromiseResolvers();

socketManager = new ClientSocketManager(socketServerUri, {
eventHandlers: {
onSocketConnection() {
connectResolver.resolve();
},
},
});

await connectResolver.promise;

expect(socketManager.connected).toBe(true);
});

it("should handle disconnection", async () => {
const connectResolver = createPromiseResolvers();
const disconnectResolver =
createPromiseResolvers<ClientSocket.DisconnectReason>();

socketManager = new ClientSocketManager(socketServerUri, {
eventHandlers: {
onSocketConnection() {
connectResolver.resolve();
},
onSocketDisconnection(reason) {
disconnectResolver.resolve(reason);
},
},
});

await connectResolver.promise;

socketManager.disconnect();

const clientReason = await disconnectResolver.promise;

expect(socketManager.connected).toBe(false);
expect(clientReason).toBe("io client disconnect");

connectResolver.renew();
disconnectResolver.renew();

socketManager.connect();

await connectResolver.promise;

expect(socketManager.connected).toBe(true);

const connectedSocketsMap = socketServer.of("/").sockets;
const clientSocket = connectedSocketsMap.get(socketManager.id!);

expect(clientSocket).toBeDefined();

clientSocket!.disconnect();

const serverReason = await disconnectResolver.promise;

expect(socketManager.connected).toBe(false);
expect(serverReason).toBe("io server disconnect");
});

it("should handle reconnection", async () => {
const connectResolver = createPromiseResolvers();
const serverDisconnectResolver = createPromiseResolvers();

socketManager = new ClientSocketManager(socketServerUri, {
eventHandlers: {
onSocketDisconnection(reason) {
if (reason === "io server disconnect") {
serverDisconnectResolver.resolve();
}
},
onSocketConnection() {
connectResolver.resolve();
},
},
});

await connectResolver.promise;

expect(socketManager.connected).toBe(true);

socketManager.disconnect();

// Should not reconnect when manually disconnected by client
expect(socketManager.connected).toBe(false);

connectResolver.renew();
socketManager.connect();

await connectResolver.promise;

expect(socketManager.connected).toBe(true);

const connectedSocketsMap = socketServer.of("/").sockets;
const clientSocket = connectedSocketsMap.get(socketManager.id!);

connectResolver.renew();

expect(clientSocket).toBeDefined();

clientSocket!.disconnect();

await serverDisconnectResolver.promise;

expect(socketManager.connected).toBe(false);

await connectResolver.promise;

// Should reconnect when manually disconnected by the server
expect(socketManager.connected).toBe(true);
});

it("should receive a message", async () => {
const connectResolver = createPromiseResolvers();
const messageResolver = createPromiseResolvers();
const anyMessageResolver = createPromiseResolvers();

const serverChannel = "server/message";
const serverMessage = "Hello from the server!";

socketManager = new ClientSocketManager(socketServerUri, {
eventHandlers: {
onSocketConnection() {
connectResolver.resolve();
},
onAnySubscribedMessageReceived(channel, received) {
anyMessageResolver.resolve();

expect(channel).toBe(serverChannel);
expect(received).toEqual([serverMessage]);
},
},
});

await connectResolver.promise;

expect(socketManager.connected).toBe(true);

socketManager.setChannelListener(serverChannel, msg => {
messageResolver.resolve();

expect(msg).toBe(serverMessage);
});

socketServer.emit(serverChannel, serverMessage);

await Promise.all([messageResolver.promise, anyMessageResolver.promise]);
});

it("should send a message", async () => {
const connectResolver = createPromiseResolvers();
const messageResolver = createPromiseResolvers();

const serverChannel = "server/message";
const clientMessage = "Hello from the client!";

socketManager = new ClientSocketManager(socketServerUri, {
eventHandlers: {
onSocketConnection() {
connectResolver.resolve();
},
},
});

await connectResolver.promise;

expect(socketManager.connected).toBe(true);

const connectedSocketsMap = socketServer.of("/").sockets;
const clientSocket = connectedSocketsMap.get(socketManager.id!);

expect(clientSocket).toBeDefined();

clientSocket!.on(serverChannel, msg => {
messageResolver.resolve();

expect(msg).toBe(clientMessage);
});

socketManager.emit(serverChannel, clientMessage);

await messageResolver.promise;
});
});
33 changes: 33 additions & 0 deletions __tests__/utils/promise-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const createPromiseResolvers = <T = void>() => {
const noop = () => void 0;

let _resolve: (value: T | PromiseLike<T>) => void = noop;
let _reject: (reason?: unknown) => void = noop;

const createPromise = () =>
new Promise<T>((res, rej) => {
_resolve = res;
_reject = rej;
});

let _promise = createPromise();

const renew = () => {
_promise = createPromise();
};

return {
get promise() {
return _promise;
},
get resolve() {
return _resolve;
},
get reject() {
return _reject;
},
renew,
};
};

export default createPromiseResolvers;
17 changes: 17 additions & 0 deletions __tests__/utils/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";

const app = express();

// eslint-disable-next-line @typescript-eslint/no-misused-promises
const httpServer = createServer(app);
const socketServer = new Server(httpServer, {
cors: { origin: "*" },
allowEIO3: true,
maxHttpBufferSize: 1e8,
httpCompression: false,
path: "/socket.io",
});

export { httpServer, socketServer };
9 changes: 8 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import jsLint from "@eslint/js";
import commentsPlugin from "eslint-plugin-eslint-comments";
import importPlugin from "eslint-plugin-import";
import jestPlugin from "eslint-plugin-jest";
import prettierRecommendedConfig from "eslint-plugin-prettier/recommended";
import { config, configs as tsLintConfigs } from "typescript-eslint";

Expand All @@ -11,7 +12,6 @@ export default config(
importPlugin.flatConfigs.typescript,
prettierRecommendedConfig,
{
ignores: ["**/*.test.ts"],
files: ["*.ts"],
},
{
Expand All @@ -26,6 +26,13 @@ export default config(
},
},
},
{
files: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
extends: [jestPlugin.configs["flat/recommended"]],
rules: {
"jest/prefer-importing-jest-globals": "error",
},
},
{
plugins: {
"eslint-comments": commentsPlugin,
Expand Down
15 changes: 15 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createJsWithTsEsmPreset, type JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
...createJsWithTsEsmPreset(),
verbose: true,
setupFilesAfterEnv: ["./jest.setup.ts"],
testPathIgnorePatterns: ["/node_modules/", "/dist/", "__tests__/utils"],
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
injectGlobals: false,
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.[jt]sx?$": "$1",
},
};

export default jestConfig;
1 change: 1 addition & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"scripts": {
"install": "cd playground && pnpm install",
"clear": "shx rm -rf dist",
"test": "echo \"\"",
"test": "jest",
"build:cjs": "tsc --project ./tsconfig.build-cjs.json",
"build:esm": "tsc --project ./tsconfig.build-esm.json",
"prebuild": "pnpm run clear",
Expand All @@ -33,27 +33,34 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@jest/globals": "^29.7.0",
"@lit/react": "^1.0.7",
"@types/eslint__js": "^8.42.3",
"@types/express": "^5.0.0",
"@types/node": "^20.17.16",
"eslint": "^9.19.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.2.3",
"express": "^4.21.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"nock": "^14.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"prettier-plugin-organize-imports": "^4.1.0",
"shx": "^0.3.4",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0"
},
"dependencies": {
"tslib": "^2.8.1"
},
"peerDependencies": {
"socket.io-client": "^4.8.1"
}
Expand Down
Loading