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,99 @@
|
||||
import { test, expect, type Page, type Locator } from "@playwright/test";
|
||||
|
||||
async function createBoard(page: Page, title: string) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("board-title").fill(title);
|
||||
await page.getByTestId("template").selectOption("mad-sad-glad");
|
||||
await page.getByTestId("create").click();
|
||||
await page.waitForURL(/\/b\/.+/);
|
||||
return page.url();
|
||||
}
|
||||
|
||||
async function joinAs(page: Page, boardUrl: string, name: string) {
|
||||
await page.goto(boardUrl);
|
||||
await page.getByTestId("name").fill(name);
|
||||
await page.getByTestId("join").click();
|
||||
await expect(page.getByTestId("column").first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Escribe una nota privada y la publica (queda en el área pública).
|
||||
// Esperamos el conteo de notas privadas para evitar clics sobre elementos que re-renderizan.
|
||||
async function addPublicNote(col: Locator, text: string) {
|
||||
await col.getByTestId("note-input").fill(text);
|
||||
await col.getByTestId("note-input").press("Enter");
|
||||
await expect(col.getByTestId("private-note")).toHaveCount(1);
|
||||
await col.getByTestId("private-note").getByTestId("publish").click();
|
||||
await expect(col.getByTestId("private-note")).toHaveCount(0);
|
||||
}
|
||||
|
||||
// Arrastra una nota (desde su tirador) sobre el centro de otra (gesto de agrupar).
|
||||
async function dragOnto(page: Page, sourceNote: Locator, target: Locator) {
|
||||
const handle = await sourceNote.getByTestId("note-handle").boundingBox();
|
||||
const t = await target.boundingBox();
|
||||
if (!handle || !t) throw new Error("sin boundingBox");
|
||||
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2 + 8); // supera el umbral
|
||||
await page.mouse.move(t.x + t.width / 2, t.y + t.height / 2, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
test("agrupar dos notas arrastrando una sobre otra", async ({ page }) => {
|
||||
const url = await createBoard(page, "Retro grupos");
|
||||
await joinAs(page, url, "Ana");
|
||||
|
||||
const col = page.getByTestId("column").first();
|
||||
await addPublicNote(col, "primera");
|
||||
await addPublicNote(col, "segunda");
|
||||
await expect(col.getByTestId("note")).toHaveCount(2);
|
||||
|
||||
const notes = col.getByTestId("note");
|
||||
await dragOnto(page, notes.nth(1), notes.nth(0));
|
||||
|
||||
// aparece un grupo con dos notas dentro
|
||||
await expect(page.getByTestId("group")).toHaveCount(1);
|
||||
await expect(page.getByTestId("group").getByTestId("note")).toHaveCount(2);
|
||||
|
||||
// votar el grupo como unidad
|
||||
await page.getByTestId("group").getByTestId("vote").click();
|
||||
await expect(page.getByTestId("group").getByTestId("vote")).toHaveClass(/voted/);
|
||||
|
||||
// y nombrarlo
|
||||
await page.getByTestId("group-title").fill("Tema común");
|
||||
await page.getByTestId("group-title").blur();
|
||||
await expect(page.getByTestId("group-title")).toHaveValue("Tema común");
|
||||
});
|
||||
|
||||
test("multivoto: apilar varios votos en el mismo ítem y quitar", async ({ page }) => {
|
||||
const url = await createBoard(page, "Retro multivoto");
|
||||
await joinAs(page, url, "Ana");
|
||||
|
||||
const col = page.getByTestId("column").first();
|
||||
await addPublicNote(col, "favorita");
|
||||
await expect(col.getByTestId("note")).toHaveCount(1);
|
||||
|
||||
// dos votos al mismo ítem (multivoto activo por defecto)
|
||||
await page.getByTestId("vote").click();
|
||||
await page.getByTestId("vote").click();
|
||||
|
||||
// el moderador revela los totales -> debe verse 2
|
||||
await page.getByTestId("reveal-votes").click();
|
||||
await expect(page.getByTestId("vote")).toContainText("2");
|
||||
|
||||
// quitar uno -> 1
|
||||
await page.getByTestId("vote-remove").click();
|
||||
await expect(page.getByTestId("vote")).toContainText("1");
|
||||
});
|
||||
|
||||
test("el moderador cambia los ajustes en caliente (título y votos)", async ({ page }) => {
|
||||
const url = await createBoard(page, "Original");
|
||||
await joinAs(page, url, "Mod");
|
||||
|
||||
await page.getByTestId("settings").click();
|
||||
await page.getByTestId("settings-title").fill("Renombrada en vivo");
|
||||
await page.getByTestId("settings-votes").fill("9");
|
||||
await page.getByTestId("settings-save").click();
|
||||
|
||||
await expect(page.locator("header h1")).toHaveText("Renombrada en vivo");
|
||||
await expect(page.locator(".votes-pill")).toContainText("9");
|
||||
});
|
||||
Reference in New Issue
Block a user