mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: add comprehensive feature bundle tests (e2e + unit edge cases)
E2E tests cover feature API responses, tool route 501 guards for all 11 AI tools, batch endpoint guards, and GUI interactions (install prompt, settings panel, sidebar badges). Unit tests add edge cases for duplicate tool detection, unknown bundle/tool lookups, and TOOL_BUNDLE_MAP integrity.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// ─── Feature API tests ─────────────────────────────────────────────
|
||||
|
||||
test.describe("Feature API", () => {
|
||||
test("GET /api/v1/features returns all 6 bundles with correct shape", async ({ request }) => {
|
||||
const response = await request.get("/api/v1/features");
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const data = await response.json();
|
||||
expect(data.bundles).toHaveLength(6);
|
||||
|
||||
const expectedBundles = [
|
||||
"background-removal",
|
||||
"face-detection",
|
||||
"object-eraser-colorize",
|
||||
"upscale-enhance",
|
||||
"photo-restoration",
|
||||
"ocr",
|
||||
];
|
||||
for (const id of expectedBundles) {
|
||||
const bundle = data.bundles.find((b: any) => b.id === id);
|
||||
expect(bundle, `Bundle ${id} missing`).toBeDefined();
|
||||
expect(bundle.name).toBeTruthy();
|
||||
expect(bundle.description).toBeTruthy();
|
||||
expect(bundle.estimatedSize).toBeTruthy();
|
||||
expect(bundle.enablesTools).toBeInstanceOf(Array);
|
||||
expect(bundle.enablesTools.length).toBeGreaterThan(0);
|
||||
expect(["not_installed", "installed", "installing", "error"]).toContain(bundle.status);
|
||||
}
|
||||
});
|
||||
|
||||
test("each bundle has the correct tools", async ({ request }) => {
|
||||
const response = await request.get("/api/v1/features");
|
||||
const data = await response.json();
|
||||
|
||||
const toolMap: Record<string, string[]> = {
|
||||
"background-removal": ["remove-background", "passport-photo"],
|
||||
"face-detection": ["blur-faces", "red-eye-removal", "smart-crop"],
|
||||
"object-eraser-colorize": ["erase-object", "colorize"],
|
||||
"upscale-enhance": ["upscale", "enhance-faces", "noise-removal"],
|
||||
"photo-restoration": ["restore-photo"],
|
||||
ocr: ["ocr"],
|
||||
};
|
||||
|
||||
for (const [bundleId, expectedTools] of Object.entries(toolMap)) {
|
||||
const bundle = data.bundles.find((b: any) => b.id === bundleId);
|
||||
expect(bundle.enablesTools).toEqual(expectedTools);
|
||||
}
|
||||
});
|
||||
|
||||
test("POST install returns 404 for unknown bundle", async ({ request }) => {
|
||||
const response = await request.post("/api/v1/admin/features/nonexistent/install");
|
||||
expect(response.status()).toBe(404);
|
||||
});
|
||||
|
||||
test("POST uninstall returns 409 for not-installed bundle", async ({ request }) => {
|
||||
const response = await request.post("/api/v1/admin/features/background-removal/uninstall");
|
||||
expect(response.status()).toBe(409);
|
||||
});
|
||||
|
||||
test("GET disk-usage returns totalBytes", async ({ request }) => {
|
||||
const response = await request.get("/api/v1/admin/features/disk-usage");
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const data = await response.json();
|
||||
expect(typeof data.totalBytes).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tool route guard tests ─────────────────────────────────────────
|
||||
|
||||
test.describe("Tool route guards", () => {
|
||||
const pngBuffer = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
);
|
||||
|
||||
const aiTools = [
|
||||
{ tool: "remove-background", bundle: "background-removal" },
|
||||
{ tool: "upscale", bundle: "upscale-enhance" },
|
||||
{ tool: "blur-faces", bundle: "face-detection" },
|
||||
{ tool: "erase-object", bundle: "object-eraser-colorize" },
|
||||
{ tool: "ocr", bundle: "ocr" },
|
||||
{ tool: "colorize", bundle: "object-eraser-colorize" },
|
||||
{ tool: "enhance-faces", bundle: "upscale-enhance" },
|
||||
{ tool: "noise-removal", bundle: "upscale-enhance" },
|
||||
{ tool: "red-eye-removal", bundle: "face-detection" },
|
||||
{ tool: "restore-photo", bundle: "photo-restoration" },
|
||||
{ tool: "passport-photo", bundle: "background-removal" },
|
||||
];
|
||||
|
||||
for (const { tool, bundle } of aiTools) {
|
||||
test(`${tool} returns 501 FEATURE_NOT_INSTALLED with correct bundle`, async ({ request }) => {
|
||||
const response = await request.post(`/api/v1/tools/${tool}`, {
|
||||
multipart: {
|
||||
file: {
|
||||
name: "test.png",
|
||||
mimeType: "image/png",
|
||||
buffer: pngBuffer,
|
||||
},
|
||||
settings: JSON.stringify({}),
|
||||
},
|
||||
});
|
||||
expect(response.status()).toBe(501);
|
||||
const body = await response.json();
|
||||
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
||||
expect(body.feature).toBe(bundle);
|
||||
expect(body.featureName).toBeTruthy();
|
||||
expect(body.estimatedSize).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
test("non-AI tool works normally (resize)", async ({ request }) => {
|
||||
const response = await request.post("/api/v1/tools/resize", {
|
||||
multipart: {
|
||||
file: {
|
||||
name: "test.png",
|
||||
mimeType: "image/png",
|
||||
buffer: pngBuffer,
|
||||
},
|
||||
settings: JSON.stringify({ width: 100, height: 100, method: "fit" }),
|
||||
},
|
||||
});
|
||||
// Should succeed or fail with a processing error, NOT 501
|
||||
expect(response.status()).not.toBe(501);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Batch and pipeline guard tests ─────────────────────────────────
|
||||
|
||||
test.describe("Batch and pipeline guards", () => {
|
||||
const pngBuffer = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
);
|
||||
|
||||
test("batch endpoint returns 501 for uninstalled AI tool", async ({ request }) => {
|
||||
const response = await request.post("/api/v1/tools/remove-background/batch", {
|
||||
multipart: {
|
||||
"files[]": {
|
||||
name: "test.png",
|
||||
mimeType: "image/png",
|
||||
buffer: pngBuffer,
|
||||
},
|
||||
settings: JSON.stringify({}),
|
||||
},
|
||||
});
|
||||
expect(response.status()).toBe(501);
|
||||
const body = await response.json();
|
||||
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── GUI tests (Playwright page interactions) ───────────────────────
|
||||
|
||||
test.describe("Feature install UI", () => {
|
||||
test("uninstalled AI tool page shows install prompt", async ({ page }) => {
|
||||
await page.goto("/remove-background");
|
||||
await expect(page.getByText("Background Removal")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByText("additional download")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /enable/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test("non-AI tool page loads normally", async ({ page }) => {
|
||||
await page.goto("/resize");
|
||||
// Should show the normal tool UI, not an install prompt
|
||||
await expect(page.getByText("additional download")).not.toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
test("AI tools show download badge in sidebar", async ({ page }) => {
|
||||
await page.goto("/resize");
|
||||
// Wait for the sidebar to load
|
||||
await expect(page.locator("[data-testid='tool-panel']").or(page.locator("nav"))).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// The download icon should be visible near AI tools
|
||||
// Note: we can't easily test for the exact Download icon, but we can check the tool links exist
|
||||
const removeBackgroundLink = page.locator("a[href='/remove-background']");
|
||||
await expect(removeBackgroundLink).toBeVisible();
|
||||
});
|
||||
|
||||
test("settings dialog has AI Features section", async ({ page }) => {
|
||||
await page.goto("/resize");
|
||||
// Open settings - look for a settings button/gear icon
|
||||
const settingsButton = page
|
||||
.getByRole("button", { name: /settings/i })
|
||||
.or(page.locator("button[aria-label*='ettings']"));
|
||||
if (await settingsButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
await settingsButton.click();
|
||||
await expect(page.getByText("AI Features")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("AI Features settings shows all 6 bundles", async ({ page }) => {
|
||||
await page.goto("/resize");
|
||||
const settingsButton = page
|
||||
.getByRole("button", { name: /settings/i })
|
||||
.or(page.locator("button[aria-label*='ettings']"));
|
||||
if (await settingsButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
await settingsButton.click();
|
||||
// Click AI Features nav item
|
||||
const aiNav = page.getByText("AI Features");
|
||||
if (await aiNav.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await aiNav.click();
|
||||
// Should show all 6 bundles
|
||||
await expect(page.getByText("Background Removal")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await expect(page.getByText("Face Detection")).toBeVisible();
|
||||
await expect(page.getByText("Object Eraser & Colorize")).toBeVisible();
|
||||
await expect(page.getByText("Upscale & Enhance")).toBeVisible();
|
||||
await expect(page.getByText("Photo Restoration")).toBeVisible();
|
||||
await expect(page.getByText("OCR")).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -44,3 +44,58 @@ describe("Feature bundles", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feature bundle edge cases", () => {
|
||||
it("no duplicate tools across bundles", () => {
|
||||
const allTools: string[] = [];
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
for (const tool of bundle.enablesTools) {
|
||||
expect(allTools, `Tool ${tool} appears in multiple bundles`).not.toContain(tool);
|
||||
allTools.push(tool);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has a non-empty estimated size", () => {
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
expect(bundle.estimatedSize.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("getToolsForBundle returns empty array for unknown bundle", () => {
|
||||
expect(getToolsForBundle("nonexistent")).toEqual([]);
|
||||
});
|
||||
|
||||
it("getBundleForTool returns null for unknown tool", () => {
|
||||
expect(getBundleForTool("nonexistent-tool")).toBeNull();
|
||||
});
|
||||
|
||||
it("TOOL_BUNDLE_MAP has no undefined values", () => {
|
||||
for (const [tool, bundle] of Object.entries(TOOL_BUNDLE_MAP)) {
|
||||
expect(bundle, `Tool ${tool} has undefined bundle`).toBeDefined();
|
||||
expect(
|
||||
FEATURE_BUNDLES[bundle],
|
||||
`Bundle ${bundle} for tool ${tool} not in FEATURE_BUNDLES`,
|
||||
).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle id matches its key in FEATURE_BUNDLES", () => {
|
||||
for (const [key, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
||||
expect(bundle.id).toBe(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has a non-empty name and description", () => {
|
||||
for (const bundle of Object.values(FEATURE_BUNDLES)) {
|
||||
expect(bundle.name.length).toBeGreaterThan(0);
|
||||
expect(bundle.description.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("every bundle has at least one tool", () => {
|
||||
for (const [id, bundle] of Object.entries(FEATURE_BUNDLES)) {
|
||||
expect(bundle.enablesTools.length, `Bundle ${id} has no tools`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user