import { expect, test } from '@playwright/test'; import type { BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; /** * Pond member management UI (issue #54): adding takes effect immediately (no * invitation), seat quotas disable the add action, and non-admin members see * the list read-only. Uses a second regular account (`fixture-editor`) as the * member whose access the owner (`fixture-user`) manages. */ async function createSharedPond(owner: BrowserContext): Promise<{ id: string; slug: string }> { const res = await owner.request.post('/api/v1/ponds', { data: { name: `Members ${Date.now()}` }, }); if (!res.ok()) throw new Error(`create pond failed: ${res.status()} ${await res.text()}`); return (await res.json()) as { id: string; slug: string }; } /** Site-admin only: change the instance-wide editor seat default. */ async function setEditorQuota(admin: BrowserContext, value: number): Promise { const res = await admin.request.patch('/api/v1/admin/settings', { data: { 'quota.editorsPerPond': value }, }); if (!res.ok()) throw new Error(`patch settings failed: ${res.status()} ${await res.text()}`); } test('adding a member takes effect immediately (second browser)', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const invitee = await contextForUser(browser, BASE_URL, 'fixture-editor'); const pond = await createSharedPond(owner); // Not a member yet: the pond hides itself from the invitee. expect((await invitee.request.get(`/api/v1/ponds/${pond.slug}`)).status()).toBe(404); const page = await owner.newPage(); await page.goto(`/p/${pond.slug}/settings`); await page.locator('.member-add__identifier').fill('fixture-editor'); await page.locator('.member-add__role').selectOption('reader'); await page.locator('.member-add__submit').click(); await expect(page.locator('.member-row[data-username="fixture-editor"]')).toBeVisible(); // Access is immediate for the second browser — no acceptance step. await expect .poll(async () => (await invitee.request.get(`/api/v1/ponds/${pond.slug}`)).status()) .toBe(200); await owner.close(); await invitee.close(); }); test('quota exhaustion disables the add action with an explanation', async ({ browser }) => { const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); try { await setEditorQuota(admin, 1); const pond = await createSharedPond(owner); const page = await owner.newPage(); await page.goto(`/p/${pond.slug}/settings`); // Fill the single editor seat. await page.locator('.member-add__identifier').fill('fixture-editor'); await page.locator('.member-add__role').selectOption('editor'); await page.locator('.member-add__submit').click(); await expect(page.locator('.member-row[data-username="fixture-editor"]')).toBeVisible(); // With the seat full, choosing the editor role blocks any further add. await page.locator('.member-add__role').selectOption('editor'); await expect(page.locator('.member-add__submit')).toBeDisabled(); await expect(page.locator('.member-add__quota-full')).toBeVisible(); } finally { await setEditorQuota(admin, 5); await admin.close(); await owner.close(); } }); test('non-admin members see the member list read-only', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const member = await contextForUser(browser, BASE_URL, 'fixture-editor'); const pond = await createSharedPond(owner); await owner.request.post(`/api/v1/ponds/${pond.id}/members`, { data: { usernameOrEmail: 'fixture-editor', role: 'reader' }, }); const page = await member.newPage(); await page.goto(`/p/${pond.slug}/settings`); // The membership is transparent — the owner is listed… await expect(page.locator('.member-row[data-username="fixture-user"]')).toBeVisible(); // …but a non-admin gets no add form and no per-member controls. await expect(page.locator('.member-add')).toHaveCount(0); await expect(page.locator('.member-row__remove')).toHaveCount(0); await expect(page.locator('.member-manager__note')).toBeVisible(); await owner.close(); await member.close(); });