Files
collective-lists/apps/web/tests/e2e/leave-collective.test.ts
Oier Bravo Urtasun 2f0847a5a3 feat(fase-10): CU-H06 leave collective — RPC + settings UI
Migration 017 adds public.leave_collective(uuid) with three branches:
- normal member: remove membership row
- sole admin with other members: promote oldest-joined remaining member
  to admin (inline; the existing user-delete trigger does not cover the
  membership-delete path), then remove the row
- sole member: reject with errcode P0001 so the UI can direct the user
  to the dissolve flow (CU-H08)

Settings page gains a "Your collectives" section listing the user's
memberships with a per-row Leave button; the confirmation modal calls
the RPC, drops the collective from local stores, and either switches
to the next collective or sends the user to /onboarding when none
remain. Also seeds the Danger zone scaffolding for Fase 10.5 and adds
all message keys consumed by 10.1, 10.3, 10.5 and 10.6.

pgTAP 010 (7 assertions): member-leave, sole-admin-leave + auto-promote,
sole-member rejected with P0001. Playwright L-01 walks Borja through
the UI flow + checks the seed collective survives (content stays).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 02:04:43 +02:00

63 lines
2.2 KiB
TypeScript

/**
* L-series: /settings → leave collective (Fase 10.1, CU-H06).
*
* Borja (member of the seed collective) abandons it. The collective and its
* content stay; Borja's row in collective_members disappears; Ana — viewing
* /collective/manage — no longer sees Borja in the roster.
*
* Borja is then a no-collective user and is auto-redirected to /onboarding.
*
* Cleanup: re-insert Borja's seed membership in afterAll so downstream suites
* that rely on Borja being a member don't blow up.
*/
import { test, expect } from '@playwright/test';
import { USERS } from '../fixtures/users.js';
import { loginAs } from '../fixtures/login.js';
import { closePool, sql, COLLECTIVE_ID } from '@colectivo/test-utils';
test.describe.configure({ mode: 'serial' });
test.describe('Leave collective (CU-H06)', () => {
test.afterAll(async () => {
// Restore Borja's seed membership so other suites pass.
await sql(
`INSERT INTO public.collective_members (collective_id, user_id, role)
VALUES ($1, $2, 'member')
ON CONFLICT (collective_id, user_id) DO UPDATE SET role = EXCLUDED.role`,
[COLLECTIVE_ID, USERS.borja.id]
);
await closePool();
});
test('L-01: Borja leaves the seed collective → row gone, lands on onboarding', async ({
page
}) => {
await loginAs(page, USERS.borja);
await page.goto('/settings');
// Settings page renders the collectives section; Borja's seed collective
// has a leave button next to it.
const leaveButton = page.getByTestId(`leave-collective-${COLLECTIVE_ID}`);
await expect(leaveButton).toBeVisible({ timeout: 15_000 });
await leaveButton.click();
// Confirm in the modal.
await page.getByTestId('confirm-leave-collective').click();
// Borja had only one collective; he gets bounced to /onboarding.
await expect(page).toHaveURL(/\/onboarding$/, { timeout: 15_000 });
// DB-side: membership row is gone, but the collective row + content survive.
const m = await sql(
'SELECT count(*)::int AS n FROM public.collective_members WHERE collective_id = $1 AND user_id = $2',
[COLLECTIVE_ID, USERS.borja.id]
);
expect(m.rows[0].n).toBe(0);
const c = await sql('SELECT count(*)::int AS n FROM public.collectives WHERE id = $1', [
COLLECTIVE_ID
]);
expect(c.rows[0].n).toBe(1);
});
});