import { expect, test } from '@playwright/test'; import type { BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; import { fixtureManifest, pluginZip, WELL_BEHAVED_SOURCE } from './plugin-fixtures'; /** * Plugin administration UI (issue #72): the Site-Admin installs a plugin, sees * its declared permissions surfaced in the list, switches its instance mode, * and activates it for one pond — the pond's effective plugin list reflects the * toggle. Mode propagation (required everywhere / disabled nowhere) and the * not-optional guard are pinned at the API level in `plugins.e2e.db.test.ts`. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; const PLUGIN_ID = 'e2e-admin-ui'; async function installFixture(context: BrowserContext): Promise { await context.request.delete(`/api/v1/admin/plugins/${PLUGIN_ID}`); const res = await context.request.post('/api/v1/admin/plugins', { multipart: { file: { name: `${PLUGIN_ID}.zip`, mimeType: 'application/zip', buffer: pluginZip( fixtureManifest(PLUGIN_ID, 'Admin UI Fixture', ['ui']), WELL_BEHAVED_SOURCE, ), }, }, }); expect(res.status(), await res.text()).toBe(201); } test('admin lists a plugin with its permissions, switches mode, and a pond activates it', async ({ browser, }) => { const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); await installFixture(admin); // The admin's personal pond — a pond they administer. const ponds = await (await admin.request.get('/api/v1/ponds')).json(); const pond = ponds.find((p: { type: string }) => p.type === 'personal'); const page = await admin.newPage(); try { await page.goto('/admin'); const item = page.locator(`.plugin-manager__item[data-plugin-id="${PLUGIN_ID}"]`); await expect(item).toBeVisible(); // Declared permission is surfaced prominently in the list (security.md). await expect(item.locator('.plugin-manager__permission', { hasText: 'ui' })).toBeVisible(); // Switch the instance mode to optional so it becomes a per-pond choice. await item.locator('.plugin-manager__mode').selectOption('optional'); await expect(item.locator('.plugin-manager__mode')).toHaveValue('optional'); // Not yet active in the pond. const before = await (await admin.request.get(`/api/v1/ponds/${pond.id}/plugins`)).json(); expect(before.map((p: { id: string }) => p.id)).not.toContain(PLUGIN_ID); // Toggle it on in the pond's settings. await page.goto(`/p/${pond.slug}/settings`); const toggle = page .locator(`.pond-plugins__item[data-plugin-id="${PLUGIN_ID}"]`) .locator('.pond-plugins__toggle'); await expect(toggle).toBeVisible(); // Controlled checkbox: click and let the effective-list poll confirm the // result, rather than check() which asserts an immediate DOM state change // the server round-trip hasn't produced yet. await toggle.click(); // The pond's effective plugin list now includes it. await expect .poll(async () => { const active = await (await admin.request.get(`/api/v1/ponds/${pond.id}/plugins`)).json(); return active.map((p: { id: string }) => p.id); }) .toContain(PLUGIN_ID); } finally { // Leave the instance clean for reruns. await admin.request.patch(`/api/v1/admin/plugins/${PLUGIN_ID}/mode`, { data: { mode: 'optional' }, }); await admin.request.delete(`/api/v1/admin/plugins/${PLUGIN_ID}`); await admin.close(); } });