familynido/web/familynido-web/e2e/multi-user-tasks.b.spec.ts
Pablo Fernández a308041d59 Initial commit
FamilyNido — a self-hosted PWA for a single household: shared calendar,
chores, meals, school agenda, health records and a family wall. One
instance per family, deployable with `docker compose` on any home
server.

Stack: .NET 10 (ASP.NET Core Minimal APIs) + EF Core 10 + PostgreSQL 16
on the backend, Angular 21 (standalone, signals, zoneless) + Tailwind
CSS v4 on the frontend, SignalR for realtime, optional OIDC alongside
local credentials, integration via a versioned `/api/v1/**` public API.

See README.md for the module overview and how to deploy.
2026-05-13 00:23:14 +02:00

60 lines
3.2 KiB
TypeScript

import { expect, test } from './fixtures';
/**
* Multi-user happy-path: tester A creates a one-off task assigned to tester
* B; B opens the app in a separate context, sees the task in `/tasks` →
* Hoy, and marks it complete; A reloads and sees the row rendered with
* line-through (B's completion is visible across contexts).
*
* Requires both seeded testers to exist in the same family — the
* `E2ETestDataSeeder` (Seed:E2E:Enabled=true) takes care of that locally
* and in CI.
*/
test('A assigns a task to B; B completes it; A sees the completion', async ({ page, pageB }) => {
const title = `E2E multi ${Date.now()}`;
const userBName = process.env['E2E_USER_B_NAME'] ?? 'Tester B';
// ── A: create the task assigned to B ──────────────────────────────────
await page.goto('/tasks');
await page.getByRole('button', { name: /nueva tarea/i }).click();
await page.getByPlaceholder('Fregar el baño').fill(title);
// The "Responsable" label sits next to the select but isn't wired up
// with `for=`, so getByLabel can't traverse to it. The select is the
// only one in the form that has a "Sin responsable" option.
const responsibleSelect = page.locator('select:has(option:has-text("Sin responsable"))');
await responsibleSelect.selectOption({ label: userBName });
await page.getByRole('button', { name: /^guardar$/i }).click();
// Confirm A's view persisted the task.
await page.getByRole('tab', { name: 'Todas' }).click();
const aRow = page.locator('li').filter({ hasText: title });
await expect(aRow).toBeVisible();
// ── B: independent context, sees the task and completes it ────────────
// B opens a fresh /tasks for the first time in this run; give the Hoy
// list a bit more headroom than the default 5s so the network round-trip
// and Angular hydration can settle. The first locator anchors on the
// "Completar" button so the assertion only resolves once the dayList
// has rendered; the click flips the button's aria-label to "Deshacer",
// so the post-click assertion drops the button anchor.
await pageB.goto('/tasks');
await pageB.getByRole('tab', { name: 'Hoy' }).click();
const bRowPending = pageB.locator('li')
.filter({ hasText: title })
.filter({ has: pageB.getByRole('button', { name: /completar/i }) });
await expect(bRowPending).toBeVisible({ timeout: 15000 });
await bRowPending.getByRole('button', { name: /completar/i }).click();
const bRow = pageB.locator('li').filter({ hasText: title });
await expect(bRow.locator('p').first()).toHaveClass(/line-through/);
// ── A: refresh, the completion is visible from this context too ───────
await page.reload();
await page.getByRole('tab', { name: 'Hoy' }).click();
const aRowAfter = page.locator('li').filter({ hasText: title });
await expect(aRowAfter).toBeVisible();
await expect(aRowAfter.locator('p').first()).toHaveClass(/line-through/);
// ── Cleanup so the suite stays idempotent ─────────────────────────────
await page.getByRole('tab', { name: 'Todas' }).click();
await aRow.getByRole('button', { name: /archivar/i }).click();
});