mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
- Unit: 1,353 tests (42 files) — +256 new tests covering AI bridge modules, image-engine sharpen/optimize-for-web, Zustand stores, and icon-map validation - Integration: 1,640 tests (57 files) — +826 new tests across all tool routes, pipeline/progress/batch infrastructure, user-files, edit-metadata, and a 321-test cross-format matrix - E2E-Docker: 389 passing (20 spec files) — 6 new spec files for batch processing, format conversion, layout, optimization, watermark/overlay, and pipeline chains. Tests verified against fresh Docker container with all 6 AI bundles installed. Bug fixes discovered during testing: - fix(compress): SVG/BMP/exotic formats crashed Sharp encoder — added format-safety fallback to PNG - fix(rate-limit): increase default login attempt limit from 10 to 500 per minute — previous value caused false test failures and is too restrictive for a self-hosted app - fix(auth.setup): wait for consent button visibility before clicking to prevent flaky E2E-Docker auth setup
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
|
|
let _token: string | undefined;
|
|
|
|
async function getToken(request: import("@playwright/test").APIRequestContext): Promise<string> {
|
|
if (_token) return _token;
|
|
const res = await request.post("/api/auth/login", {
|
|
data: { username: "admin", password: "admin" },
|
|
});
|
|
const body = await res.json();
|
|
_token = body.token as string;
|
|
return _token;
|
|
}
|
|
|
|
interface BundleInfo {
|
|
id: string;
|
|
status: string;
|
|
}
|
|
|
|
async function fetchBundleStatuses(
|
|
request: import("@playwright/test").APIRequestContext,
|
|
): Promise<BundleInfo[]> {
|
|
const token = await getToken(request);
|
|
const res = await request.get("/api/v1/features", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!res.ok()) return [];
|
|
const data = await res.json();
|
|
return data.bundles as BundleInfo[];
|
|
}
|
|
|
|
async function isBundleInstalled(
|
|
request: import("@playwright/test").APIRequestContext,
|
|
bundleId: string,
|
|
): Promise<boolean> {
|
|
const bundles = await fetchBundleStatuses(request);
|
|
const bundle = bundles.find((b) => b.id === bundleId);
|
|
return bundle?.status === "installed";
|
|
}
|
|
|
|
// ─── Feature API tests ─────────────────────────────────────────────
|
|
|
|
test.describe("Feature API", () => {
|
|
test("GET /api/v1/features returns all 6 bundles with correct shape", async ({ request }) => {
|
|
const token = await getToken(request);
|
|
const response = await request.get("/api/v1/features", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
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 token = await getToken(request);
|
|
const response = await request.get("/api/v1/features", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
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 token = await getToken(request);
|
|
const response = await request.post("/api/v1/admin/features/nonexistent/install", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(response.status()).toBe(404);
|
|
});
|
|
|
|
test("POST uninstall returns 409 for not-installed bundle", async ({ request }) => {
|
|
const installed = await isBundleInstalled(request, "background-removal");
|
|
if (installed) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
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 token = await getToken(request);
|
|
const response = await request.get("/api/v1/admin/features/disk-usage", {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
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 installed = await isBundleInstalled(request, bundle);
|
|
if (installed) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
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 installed = await isBundleInstalled(request, "background-removal");
|
|
if (installed) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
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, request }) => {
|
|
const installed = await isBundleInstalled(request, "background-removal");
|
|
if (installed) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
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, request }) => {
|
|
const installed = await isBundleInstalled(request, "background-removal");
|
|
if (installed) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
});
|