Sternboard: free retro board, no signup (initial)
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.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { startTestServer, createBoard, join, type TestServer } from "./helpers.js";
|
||||
|
||||
let server: TestServer;
|
||||
beforeAll(async () => {
|
||||
server = await startTestServer();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
describe("Socket.IO — flujo de retro", () => {
|
||||
it("oculta las notas de otros hasta revelar", async () => {
|
||||
const board = await createBoard(server.url);
|
||||
const alice = await join(server.url, board.boardId, "alice", "Alice");
|
||||
const bob = await join(server.url, board.boardId, "bob", "Bob");
|
||||
const colId = alice.last!.columns[0].id;
|
||||
|
||||
alice.emit("note:add", { columnId: colId, text: "secreto de Alice" });
|
||||
|
||||
const bobState = await bob.waitFor((s) => s.notes.length === 1);
|
||||
expect(bobState.notes[0].text).toBe(""); // oculta para Bob
|
||||
expect(bobState.notes[0].authorName).toBe("Alice");
|
||||
|
||||
const aliceState = await alice.waitFor((s) => s.notes.length === 1);
|
||||
expect(aliceState.notes[0].text).toBe("secreto de Alice"); // visible para la autora
|
||||
|
||||
alice.close();
|
||||
bob.close();
|
||||
});
|
||||
|
||||
it("revela las notas a todos cuando el moderador lo pide", async () => {
|
||||
const board = await createBoard(server.url);
|
||||
const alice = await join(server.url, board.boardId, "alice", "Alice");
|
||||
const bob = await join(server.url, board.boardId, "bob", "Bob");
|
||||
const colId = alice.last!.columns[0].id;
|
||||
|
||||
alice.emit("note:add", { columnId: colId, text: "visible tras revelar" });
|
||||
await bob.waitFor((s) => s.notes.length === 1);
|
||||
|
||||
// Bob (sin token) no puede revelar; Alice (moderadora) sí
|
||||
bob.emit("board:reveal", { revealed: true });
|
||||
alice.emit("board:reveal", { revealed: true, moderatorToken: board.moderatorToken });
|
||||
|
||||
const revealed = await bob.waitFor((s) => s.revealed === true);
|
||||
expect(revealed.notes[0].text).toBe("visible tras revelar");
|
||||
|
||||
alice.close();
|
||||
bob.close();
|
||||
});
|
||||
|
||||
it("respeta el límite de votos por persona", async () => {
|
||||
const board = await createBoard(server.url, { votesPerUser: 2 });
|
||||
const alice = await join(server.url, board.boardId, "alice", "Alice");
|
||||
const colId = alice.last!.columns[0].id;
|
||||
|
||||
for (const txt of ["a", "b", "c"]) alice.emit("note:add", { columnId: colId, text: txt });
|
||||
const withNotes = await alice.waitFor((s) => s.notes.length === 3);
|
||||
const ids = withNotes.notes.map((n) => n.id);
|
||||
const mine = (s: { votes: { participantId: string }[] }) =>
|
||||
s.votes.filter((v) => v.participantId === "alice").length;
|
||||
|
||||
// vota las dos primeras (confirmando cada una)
|
||||
alice.emit("vote:toggle", { noteId: ids[0] });
|
||||
await alice.waitFor((s) => mine(s) === 1);
|
||||
alice.emit("vote:toggle", { noteId: ids[1] });
|
||||
await alice.waitFor((s) => mine(s) === 2);
|
||||
|
||||
// el tercer voto se rechaza (cupo = 2)
|
||||
alice.emit("vote:toggle", { noteId: ids[2] });
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
expect(mine(alice.last!)).toBe(2);
|
||||
|
||||
// quitar un voto libera cupo
|
||||
alice.emit("vote:toggle", { noteId: ids[0] });
|
||||
const afterUnvote = await alice.waitFor((s) => mine(s) === 1);
|
||||
expect(afterUnvote.votes).toHaveLength(1);
|
||||
|
||||
alice.close();
|
||||
});
|
||||
|
||||
it("comparte el timer (endsAt) con todos los participantes", async () => {
|
||||
const board = await createBoard(server.url);
|
||||
const alice = await join(server.url, board.boardId, "alice", "Alice");
|
||||
const bob = await join(server.url, board.boardId, "bob", "Bob");
|
||||
|
||||
alice.emit("timer:start", { seconds: 300, moderatorToken: board.moderatorToken });
|
||||
const bobState = await bob.waitFor((s) => s.timerRunning);
|
||||
expect(bobState.timerEndsAt).toBeTruthy();
|
||||
expect(new Date(bobState.timerEndsAt!).getTime()).toBeGreaterThan(Date.now());
|
||||
|
||||
alice.close();
|
||||
bob.close();
|
||||
});
|
||||
|
||||
it("crea action items visibles para todos", async () => {
|
||||
const board = await createBoard(server.url);
|
||||
const alice = await join(server.url, board.boardId, "alice", "Alice");
|
||||
const bob = await join(server.url, board.boardId, "bob", "Bob");
|
||||
|
||||
alice.emit("actionitem:add", { text: "documentar el deploy" });
|
||||
const bobState = await bob.waitFor((s) => s.actionItems.length === 1);
|
||||
expect(bobState.actionItems[0].text).toBe("documentar el deploy");
|
||||
|
||||
alice.close();
|
||||
bob.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user