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>
This commit is contained in:
62
apps/web/tests/e2e/leave-collective.test.ts
Normal file
62
apps/web/tests/e2e/leave-collective.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user