first commit

This commit is contained in:
Matt Kane
2026-04-01 10:44:22 +01:00
commit 43fcb9a131
1789 changed files with 395041 additions and 0 deletions

228
e2e/tests/plugins.spec.ts Normal file
View File

@@ -0,0 +1,228 @@
/**
* Plugins Manager E2E Tests
*
* Tests the plugins manager admin page at /plugins-manager.
* Verifies that the page renders, displays plugin info, and
* supports enable/disable toggling.
*
* This test exists because the plugins page previously crashed due to
* incorrect API response envelope unwrapping (fetchPlugins returned
* { items: [...] } instead of [...]) -- a bug that component tests
* never caught because they mock fetchPlugins at the module level.
*
* The e2e fixture configures a color plugin (id: "color", version: "0.0.1").
*/
import { test, expect } from "../fixtures";
test.describe("Plugins Manager", () => {
test.beforeEach(async ({ admin }) => {
await admin.devBypassAuth();
});
test.describe("Page rendering", () => {
test("displays the plugins page with at least one plugin", async ({ admin, page }) => {
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
// Page title
await admin.expectPageTitle("Plugins");
// Should show the plugin count
await expect(page.locator("text=/\\d+ plugin/")).toBeVisible({ timeout: 10000 });
// Should have at least one plugin card (the color plugin from the fixture)
const pluginCards = page.locator(".rounded-lg.border.bg-kumo-base");
await expect(pluginCards.first()).toBeVisible({ timeout: 10000 });
});
test("does not show a crash or error state", async ({ admin, page }) => {
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
// The bug that prompted this test caused the page to show an error.
// Verify no error state is visible.
await expect(page.locator("text=Failed to load plugins")).not.toBeVisible();
});
});
test.describe("Plugin card info", () => {
test("shows plugin name, version, and toggle", async ({ admin, page }) => {
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
// Find the color plugin card
const colorCard = page
.locator(".rounded-lg.border.bg-kumo-base", { hasText: "color" })
.first();
await expect(colorCard).toBeVisible({ timeout: 10000 });
// Plugin name
await expect(colorCard.locator("h3")).toContainText("color");
// Version badge
await expect(colorCard.locator("text=v0.0.1")).toBeVisible();
// Enable/disable switch
const toggle = colorCard.locator("button[role='switch']");
await expect(toggle).toBeVisible();
});
});
test.describe("Enable / disable toggle", () => {
test.skip("can disable a plugin and see the Disabled badge", async ({ admin, page }) => {
// TODO: Enable/disable only works for marketplace plugins, not config plugins.
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
const colorCard = page
.locator(".rounded-lg.border.bg-kumo-base", { hasText: "color" })
.first();
await expect(colorCard).toBeVisible({ timeout: 10000 });
const toggle = colorCard.locator("button[role='switch']");
await expect(toggle).toBeVisible({ timeout: 5000 });
// If the plugin is currently disabled, enable it first
const isChecked = await toggle.getAttribute("aria-checked");
if (isChecked !== "true") {
await toggle.click();
await page.waitForTimeout(2000);
await admin.waitForLoading();
}
// Now disable the plugin
await toggle.click();
await page.waitForTimeout(2000);
await admin.waitForLoading();
// The "Disabled" badge should appear on the card
await expect(colorCard.getByText("Disabled")).toBeVisible({ timeout: 10000 });
// The toggle should now be unchecked
await expect(toggle).toHaveAttribute("aria-checked", "false");
});
test.skip("can re-enable a disabled plugin", async ({ admin, page }) => {
// TODO: Enable/disable only works for marketplace plugins, not config plugins.
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
const colorCard = page
.locator(".rounded-lg.border.bg-kumo-base", { hasText: "color" })
.first();
await expect(colorCard).toBeVisible({ timeout: 10000 });
const toggle = colorCard.locator("button[role='switch']");
await expect(toggle).toBeVisible({ timeout: 5000 });
// Ensure the plugin is disabled first
const isChecked = await toggle.getAttribute("aria-checked");
if (isChecked === "true") {
await toggle.click();
await page.waitForTimeout(2000);
await admin.waitForLoading();
}
// Verify it shows "Disabled"
await expect(colorCard.getByText("Disabled")).toBeVisible({ timeout: 10000 });
// Re-enable it
await toggle.click();
await page.waitForTimeout(2000);
await admin.waitForLoading();
// The "Disabled" badge should be gone
await expect(colorCard.getByText("Disabled")).not.toBeVisible({ timeout: 10000 });
// Toggle should be checked again
await expect(toggle).toHaveAttribute("aria-checked", "true");
});
});
test.describe("Expand details", () => {
test("expand button reveals details section", async ({ admin, page }) => {
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
const colorCard = page
.locator(".rounded-lg.border.bg-kumo-base", { hasText: "color" })
.first();
await expect(colorCard).toBeVisible({ timeout: 10000 });
// Find and click the expand button
const expandBtn = colorCard.locator("button[aria-label='Expand details']");
await expect(expandBtn).toBeVisible();
await expandBtn.click();
// The details section should now be visible (it has a border-t class)
const detailsSection = colorCard.locator(".border-t.px-4");
await expect(detailsSection).toBeVisible({ timeout: 5000 });
// Collapse button should now be present
await expect(colorCard.locator("button[aria-label='Collapse details']")).toBeVisible();
});
test("collapse button hides the details section", async ({ admin, page }) => {
await admin.goto("/plugins-manager");
await admin.waitForShell();
await admin.waitForLoading();
const colorCard = page
.locator(".rounded-lg.border.bg-kumo-base", { hasText: "color" })
.first();
await expect(colorCard).toBeVisible({ timeout: 10000 });
// Expand first
await colorCard.locator("button[aria-label='Expand details']").click();
const detailsSection = colorCard.locator(".border-t.px-4");
await expect(detailsSection).toBeVisible({ timeout: 5000 });
// Now collapse
await colorCard.locator("button[aria-label='Collapse details']").click();
// Details should be hidden
await expect(detailsSection).not.toBeVisible({ timeout: 5000 });
// Expand button should be back
await expect(colorCard.locator("button[aria-label='Expand details']")).toBeVisible();
});
});
test.describe("API integration", () => {
test("plugins API returns the correct envelope shape", async ({ page, serverInfo }) => {
// Directly verify the API response shape that caused the original bug.
// fetchPlugins expected { data: { items: [...] } } but was getting
// the items at a different nesting level.
const res = await page.request.get("/_emdash/api/admin/plugins", {
headers: {
"X-EmDash-Request": "1",
Authorization: `Bearer ${serverInfo.token}`,
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
// The response should have { data: { items: [...] } }
expect(body.data).toBeDefined();
expect(body.data.items).toBeDefined();
expect(Array.isArray(body.data.items)).toBe(true);
expect(body.data.items.length).toBeGreaterThan(0);
// Each plugin should have the expected shape
const plugin = body.data.items[0];
expect(plugin.id).toBeDefined();
expect(plugin.name).toBeDefined();
expect(plugin.version).toBeDefined();
expect(typeof plugin.enabled).toBe("boolean");
expect(Array.isArray(plugin.capabilities)).toBe(true);
});
});
});