Corp/frontend/src/lib/websocketManager.test.js
lizerk 2768d2f1b3
Some checks failed
Deploy production / deploy (push) Failing after 14m49s
Restore reliable chat realtime and media playback
2026-09-10 20:26:58 +03:00

204 lines
5.9 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { apiClient } from "@/lib/api";
test("multiple realtime subscribers share one WebSocket connection", async () => {
const originalWindow = globalThis.window;
const originalLocalStorage = globalThis.localStorage;
const originalWebSocket = globalThis.WebSocket;
const originalRequest = apiClient.request;
class FakeWebSocket {
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;
static instances = [];
constructor(url) {
this.url = url;
this.readyState = FakeWebSocket.CONNECTING;
this.sent = [];
FakeWebSocket.instances.push(this);
}
open() {
this.readyState = FakeWebSocket.OPEN;
this.onopen?.();
}
send(payload) {
this.sent.push(JSON.parse(payload));
}
receive(payload) {
this.onmessage?.({ data: JSON.stringify(payload) });
}
close() {
this.readyState = FakeWebSocket.CLOSED;
this.onclose?.({ code: 1000, reason: "", wasClean: true });
}
}
globalThis.window = { addEventListener() {} };
globalThis.localStorage = {
getItem(key) {
return key === "access_token" ? "test-token" : null;
},
};
globalThis.WebSocket = FakeWebSocket;
apiClient.request = async (endpoint) => {
assert.equal(endpoint, "/api/auth/ws-ticket/");
return { ticket: "single-use-ticket", expires_in: 30 };
};
try {
const { default: manager } = await import("./websocketManager.ts");
const unsubscribeMessages = manager.subscribe(() => {});
const unsubscribeStatus = manager.subscribeStatus(() => {});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(FakeWebSocket.instances.length, 1);
assert.match(FakeWebSocket.instances[0].url, /ticket=single-use-ticket/);
assert.doesNotMatch(FakeWebSocket.instances[0].url, /token=/);
const socket = FakeWebSocket.instances[0];
socket.open();
manager.setActiveChat(114);
assert.equal(FakeWebSocket.instances.length, 1);
assert.deepEqual(socket.sent.at(-1), {
action: "open_chat",
chat_id: 114,
load_history: false,
});
socket.receive({ type: "ping", timestamp: "2026-08-17T12:00:00Z" });
assert.deepEqual(socket.sent.at(-1), {
action: "pong",
timestamp: "2026-08-17T12:00:00Z",
});
unsubscribeMessages();
unsubscribeStatus();
} finally {
globalThis.window = originalWindow;
globalThis.localStorage = originalLocalStorage;
globalThis.WebSocket = originalWebSocket;
apiClient.request = originalRequest;
}
});
test("connection recovers immediately when browser comes back online", async () => {
const originalWindow = globalThis.window;
const originalDocument = globalThis.document;
const originalLocalStorage = globalThis.localStorage;
const originalWebSocket = globalThis.WebSocket;
const originalRequest = apiClient.request;
const originalSetTimeout = globalThis.setTimeout;
const originalClearTimeout = globalThis.clearTimeout;
const originalSetInterval = globalThis.setInterval;
const originalClearInterval = globalThis.clearInterval;
const listeners = new Map();
const scheduledTimeouts = [];
const statuses = [];
class FakeWebSocket {
static OPEN = 1;
static instances = [];
constructor(url) {
this.url = url;
this.readyState = 0;
FakeWebSocket.instances.push(this);
}
open() {
this.readyState = FakeWebSocket.OPEN;
this.onopen?.();
}
send() {}
fail() {
this.readyState = 3;
this.onclose?.({ code: 1006, reason: "network", wasClean: false });
}
close() {
this.readyState = 3;
}
}
globalThis.window = {
addEventListener(type, handler) {
listeners.set(type, handler);
},
};
globalThis.document = {
visibilityState: "visible",
addEventListener(type, handler) {
listeners.set(type, handler);
},
};
globalThis.localStorage = {
getItem(key) {
return key === "access_token" ? "access" : null;
},
};
globalThis.WebSocket = FakeWebSocket;
globalThis.setTimeout = (handler, delay) => {
const timer = { handler, delay, cleared: false };
scheduledTimeouts.push(timer);
return timer;
};
globalThis.clearTimeout = (timer) => {
timer.cleared = true;
};
globalThis.setInterval = () => ({ interval: true });
globalThis.clearInterval = () => {};
apiClient.request = async () => ({
ticket: "recovery-ticket",
expires_in: 30,
});
try {
const { WebSocketManager } = await import("./websocketManager.ts");
const manager = new WebSocketManager();
const unsubscribe = manager.subscribe(() => {});
const unsubscribeStatus = manager.subscribeStatus((status) =>
statuses.push(status),
);
await new Promise((resolve) => originalSetTimeout(resolve, 0));
assert.equal(FakeWebSocket.instances.length, 1);
FakeWebSocket.instances[0].open();
FakeWebSocket.instances[0].fail();
assert.equal(statuses.at(-1).lastCloseCode, 1006);
assert.equal(statuses.at(-1).lastCloseReason, "network");
assert.equal(scheduledTimeouts.length, 1);
assert.ok(scheduledTimeouts[0].delay >= 1600);
assert.ok(scheduledTimeouts[0].delay <= 2400);
listeners.get("online")?.();
await new Promise((resolve) => originalSetTimeout(resolve, 0));
assert.equal(scheduledTimeouts[0].cleared, true);
assert.equal(FakeWebSocket.instances.length, 2);
unsubscribe();
unsubscribeStatus();
} finally {
globalThis.window = originalWindow;
globalThis.document = originalDocument;
globalThis.localStorage = originalLocalStorage;
globalThis.WebSocket = originalWebSocket;
globalThis.setTimeout = originalSetTimeout;
globalThis.clearTimeout = originalClearTimeout;
globalThis.setInterval = originalSetInterval;
globalThis.clearInterval = originalClearInterval;
apiClient.request = originalRequest;
}
});