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
- 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
87 lines
3.5 KiB
TypeScript
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();
|
|
}
|
|
});
|