Servidor Fastify + Socket.IO + Drizzle/Postgres; front Vite+React (EN/ES, tema claro/oscuro). Tableros por columnas con plantillas, notas privadas/públicas, agrupación, votos configurables (multivoto), timer compartido con alarma, fases, action items, export Markdown/JSON e import, autodestrucción, gestión de columnas. Imagen única (front servido por el servidor) y CI que la construye y publica.
86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import { buildServer, type BuiltServer } from "../src/app.js";
|
|
import { io as ioc, type Socket } from "socket.io-client";
|
|
import type { BoardState } from "../src/room.js";
|
|
|
|
export type TestServer = BuiltServer & { port: number; url: string };
|
|
|
|
export async function startTestServer(): Promise<TestServer> {
|
|
const server = buildServer({ cleanup: false });
|
|
const port = await server.listen(0); // puerto efímero
|
|
return Object.assign(server, { port, url: `http://localhost:${port}` });
|
|
}
|
|
|
|
export async function createBoard(
|
|
url: string,
|
|
body: Record<string, unknown> = {},
|
|
): Promise<{ boardId: string; moderatorToken: string; expiresAt: string | null }> {
|
|
const res = await fetch(`${url}/api/boards`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ templateId: "mad-sad-glad", votesPerUser: 3, ...body }),
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
// Cliente de test: mantiene el último estado recibido para evitar carreras
|
|
// (el estado buscado puede haber llegado antes de adjuntar el listener).
|
|
export class Client {
|
|
last: BoardState | null = null;
|
|
constructor(public socket: Socket) {
|
|
socket.on("board:state", (s: BoardState) => (this.last = s));
|
|
}
|
|
|
|
emit(event: string, payload: Record<string, unknown>) {
|
|
this.socket.emit(event, payload);
|
|
}
|
|
|
|
// Resuelve cuando el último estado (presente o futuro) cumple el predicado.
|
|
waitFor(predicate: (s: BoardState) => boolean, timeoutMs = 10000): Promise<BoardState> {
|
|
if (this.last && predicate(this.last)) return Promise.resolve(this.last);
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.socket.off("board:state", handler);
|
|
reject(new Error("waitFor timeout"));
|
|
}, timeoutMs);
|
|
const handler = (s: BoardState) => {
|
|
if (predicate(s)) {
|
|
clearTimeout(timer);
|
|
this.socket.off("board:state", handler);
|
|
resolve(s);
|
|
}
|
|
};
|
|
this.socket.on("board:state", handler);
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this.socket.close();
|
|
}
|
|
}
|
|
|
|
// Conecta, hace join y devuelve un Client ya con su primer estado.
|
|
export function join(
|
|
url: string,
|
|
boardId: string,
|
|
participantId: string,
|
|
name: string,
|
|
): Promise<Client> {
|
|
return new Promise((resolve, reject) => {
|
|
const socket = ioc(url, { transports: ["websocket"], forceNew: true });
|
|
const client = new Client(socket);
|
|
const timer = setTimeout(() => reject(new Error("join timeout")), 10000);
|
|
socket.on("connect", () => socket.emit("board:join", { boardId, participantId, name }));
|
|
socket.once("error", (e) => {
|
|
clearTimeout(timer);
|
|
reject(new Error(`join error: ${JSON.stringify(e)}`));
|
|
});
|
|
const ready = (s: BoardState) => {
|
|
clearTimeout(timer);
|
|
socket.off("board:state", ready);
|
|
client.last = s;
|
|
resolve(client);
|
|
};
|
|
socket.on("board:state", ready);
|
|
});
|
|
}
|