/** * D-series: /collective/manage → dissolve (Fase 10.3, CU-H08). * * ⚠️ This suite must NOT dissolve the seed collective. We create an auxiliary * collective per test, manipulate it, then either let the test dissolve it * (D-01) or clean it up in afterEach. */ import { test, expect } from '@playwright/test'; import { USERS } from '../fixtures/users.js'; import { loginAs } from '../fixtures/login.js'; import { closePool, sql } from '@colectivo/test-utils'; test.describe.configure({ mode: 'serial' }); const aux: { id: string | null } = { id: null }; const auxName = `Aux dissolve ${Date.now()}`; /** * Create an auxiliary collective owned (created_by) by Borja but with Ana * as the sole admin (Borja is dropped from membership). This lets Ana * dissolve it from the UI without touching the seed. */ async function createAuxCollective(): Promise { const r = await sql( `INSERT INTO public.collectives (name, emoji, created_by) VALUES ($1, '💥', $2) RETURNING id`, [auxName, USERS.borja.id] ); const id = r.rows[0].id as string; await sql( `INSERT INTO public.collective_members (collective_id, user_id, role) VALUES ($1, $2, 'admin'), ($1, $3, 'member')`, [id, USERS.ana.id, USERS.borja.id] ); return id; } test.describe('Dissolve collective (CU-H08)', () => { test.afterEach(async () => { if (aux.id) { // Best-effort cleanup if the test didn't dissolve it. await sql('DELETE FROM public.collectives WHERE id = $1', [aux.id]); aux.id = null; } }); test.afterAll(async () => { await closePool(); }); test('D-03: Borja (member) does NOT see the dissolve button', async ({ page }) => { aux.id = await createAuxCollective(); await loginAs(page, USERS.borja); // Switch active collective to the aux one in localStorage, then reload. await page.evaluate((id) => localStorage.setItem('activeCollectiveId', id), aux.id); await page.goto('/collective/manage'); // Make sure the page is hydrated by waiting on a known member row. await expect(page.getByTestId(`member-row-${USERS.ana.id}`)).toBeVisible({ timeout: 15_000 }); // Borja must not see the dissolve button (admin-gated). await expect(page.getByTestId('dissolve-collective-button')).toHaveCount(0); }); test('D-02: dissolve button disabled until input matches collective name exactly', async ({ page }) => { aux.id = await createAuxCollective(); await loginAs(page, USERS.ana); await page.evaluate((id) => localStorage.setItem('activeCollectiveId', id), aux.id); await page.goto('/collective/manage'); await page.getByTestId('dissolve-collective-button').click(); const confirm = page.getByTestId('dissolve-confirm-button'); await expect(confirm).toBeDisabled(); await page.getByTestId('dissolve-confirm-input').fill('wrong name'); await expect(confirm).toBeDisabled(); await page.getByTestId('dissolve-confirm-input').fill(auxName); await expect(confirm).toBeEnabled(); }); test('D-01: Ana dissolves auxiliary collective → redirect + row gone', async ({ page }) => { aux.id = await createAuxCollective(); await loginAs(page, USERS.ana); await page.evaluate((id) => localStorage.setItem('activeCollectiveId', id), aux.id); await page.goto('/collective/manage'); await page.getByTestId('dissolve-collective-button').click(); await page.getByTestId('dissolve-confirm-input').fill(auxName); await page.getByTestId('dissolve-confirm-button').click(); // After dissolve we either land on /lists (other collectives remain) or // /onboarding (no collectives). Ana still has the seed one, so /lists. await expect(page).toHaveURL(/\/(lists|onboarding)$/, { timeout: 15_000 }); const r = await sql('SELECT count(*)::int AS n FROM public.collectives WHERE id = $1', [ aux.id ]); expect(r.rows[0].n).toBe(0); // Test cleanup already happened via cascade. aux.id = null; }); });