Some checks failed
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m8s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Failing after 11s
CD / Promote to Int (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m40s
CI / Import/export fidelity gate (push) Successful in 54s
The pond switcher grows a "+ New pond" entry with an inline form (name + optional description, quota errors surfaced translated); the pond settings of shared ponds end in a danger section that moves the pond to the site-level trash after typing its name to confirm. Personal ponds keep hiding the section. .button--danger is now a solid red button (also fixes the admin restore button, which showed red text on the accent-green background). Manuals no longer call these actions API-only; covered by a members-pack e2e test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
139 lines
6.0 KiB
TypeScript
139 lines
6.0 KiB
TypeScript
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<void> {
|
|
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();
|
|
});
|
|
|
|
test('shared ponds are created from the switcher and deleted from settings', async ({
|
|
browser,
|
|
}) => {
|
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
|
const page = await owner.newPage();
|
|
await page.goto('/');
|
|
|
|
// Create via the "+ New pond" flow in the pond switcher.
|
|
const pondName = `Switcher ${Date.now()}`;
|
|
// The switcher only mounts once the ponds query resolves — wait for its
|
|
// dedicated class instead of grabbing the first generic menu trigger.
|
|
await page.locator('.pond-switcher__trigger').click();
|
|
await page.locator('.pond-switcher__create').click();
|
|
await page.locator('.pond-switcher__form input').first().fill(pondName);
|
|
await page.locator('.pond-switcher__actions button[type="submit"]').click();
|
|
// Creation lands on the new pond's home.
|
|
await expect(page).toHaveURL(/\/p\/switcher-/);
|
|
|
|
// Delete from the danger section — the button stays disabled until the
|
|
// typed confirmation matches the pond name exactly.
|
|
const slug = new URL(page.url()).pathname.split('/')[2];
|
|
await page.goto(`/p/${slug}/settings`);
|
|
const danger = page.locator('.pond-delete');
|
|
await expect(danger).toBeVisible();
|
|
await expect(danger.locator('.pond-delete__submit')).toBeDisabled();
|
|
await danger.locator('input').fill(pondName);
|
|
await danger.locator('.pond-delete__submit').click();
|
|
await expect(page).toHaveURL(/\/$|\/p\/fixture-user/);
|
|
expect((await owner.request.get(`/api/v1/ponds/${slug}`)).status()).toBe(404);
|
|
|
|
// The personal pond never offers deletion.
|
|
await page.goto('/p/fixture-user/settings');
|
|
await expect(page.locator('.pond-export')).toBeVisible();
|
|
await expect(page.locator('.pond-delete')).toHaveCount(0);
|
|
|
|
await owner.close();
|
|
});
|