dorfteich/apps/web/e2e/plugin-admin.spec.ts
Claude Fable 5 46292c7447
All checks were successful
CI / Lint, typecheck, test (push) Successful in 2m56s
CD / Build and push images (push) Successful in 3m11s
CD / Deploy to Test (push) Successful in 8s
CI / Auth e2e pack (push) Successful in 4m12s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 13s
CI / Import/export fidelity gate (push) Successful in 42s
CI / Build container images (push) Has been skipped
Add plugin administration UI: instance modes and pond activation (#72)
- api: PATCH /admin/plugins/:id/mode (Site Admin) switches
  disabled/optional/required; new PluginPondController exposes
  GET /ponds/:id/plugins (effective list: required + optional-enabled,
  pond read access — the SPA loads it per pond), GET .../plugins/settings
  and PUT .../plugins/:pluginId (Pond Admin) to toggle optional plugins.
  Toggling a non-optional plugin is refused (plugin_not_optional).
  Install/uninstall/mode/toggle are audit-logged.
- web: PluginManager in the admin area lists installed plugins with their
  declared permissions surfaced prominently (security.md), an upload
  control that shows validation errors, a mode switch with an impact
  hint, and a link to the sandbox preview. PondPluginSettings adds a
  per-pond optional-plugin toggle section to pond settings.
- shared: PondPluginSetting, mode/toggle input schemas, plugin_not_optional
  error code + de/en messages, plugins i18n (admin/mode/pond).
- tests: api db test covers mode switching, per-pond activation, the
  required-everywhere and disabled-nowhere propagation, and the
  not-optional guard; e2e plugin-admin pack drives the admin list,
  permission display, mode switch, and pond toggle end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 09:32:16 +02:00

87 lines
3.5 KiB
TypeScript

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<void> {
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();
}
});