From 251445b0924c29617c89a561eecfc90673e04937 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Wed, 15 Apr 2026 23:11:40 +0800 Subject: [PATCH] fix(noise-removal): write denoised output to workspace path and add e2e test --- apps/api/src/routes/tools/noise-removal.ts | 28 ++-- tests/e2e/noise-removal.spec.ts | 150 +++++++++++++++++++++ 2 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 tests/e2e/noise-removal.spec.ts diff --git a/apps/api/src/routes/tools/noise-removal.ts b/apps/api/src/routes/tools/noise-removal.ts index 089aaa17..dba9d1f3 100644 --- a/apps/api/src/routes/tools/noise-removal.ts +++ b/apps/api/src/routes/tools/noise-removal.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { noiseRemoval } from "@ashim/ai"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; @@ -107,6 +108,11 @@ export function registerNoiseRemoval(app: FastifyInstance) { onProgress, ); + const ext = result.format === "jpeg" ? "jpg" : result.format; + const outputFilename = `${filename.replace(/\.[^.]+$/, "")}_denoised.${ext}`; + const outputPath = join(workspacePath, "output", outputFilename); + await writeFile(outputPath, result.buffer); + if (clientJobId) { updateSingleFileProgress({ jobId: clientJobId, @@ -115,22 +121,12 @@ export function registerNoiseRemoval(app: FastifyInstance) { }); } - const CONTENT_TYPES: Record = { - png: "image/png", - jpeg: "image/jpeg", - jpg: "image/jpeg", - webp: "image/webp", - }; - const contentType = CONTENT_TYPES[result.format] || "image/png"; - const ext = result.format === "jpeg" ? "jpg" : result.format; - const outputFilename = `${filename.replace(/\.[^.]+$/, "")}_denoised.${ext}`; - - return reply - .header("Content-Type", contentType) - .header("Content-Disposition", `attachment; filename="${outputFilename}"`) - .header("X-Image-Width", String(result.width)) - .header("X-Image-Height", String(result.height)) - .send(result.buffer); + return reply.send({ + jobId, + downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(outputFilename)}`, + originalSize: fileBuffer.length, + processedSize: result.buffer.length, + }); } catch (err) { request.log.error({ err, toolId: "noise-removal" }, "Noise removal failed"); return reply.status(422).send({ diff --git a/tests/e2e/noise-removal.spec.ts b/tests/e2e/noise-removal.spec.ts new file mode 100644 index 00000000..3a8f6a8b --- /dev/null +++ b/tests/e2e/noise-removal.spec.ts @@ -0,0 +1,150 @@ +import path from "node:path"; +import { expect, test } from "./helpers"; + +// --------------------------------------------------------------------------- +// Noise Removal tool - e2e tests. +// Covers UI rendering, quick/balanced tier processing, and error display. +// --------------------------------------------------------------------------- + +function fixturePath(name: string): string { + return path.join(process.cwd(), "tests", "fixtures", name); +} + +async function uploadFile(page: import("@playwright/test").Page, filePath: string) { + const fileChooserPromise = page.waitForEvent("filechooser"); + const dropzone = page.locator("[class*='border-dashed']").first(); + await dropzone.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(filePath); + await page.waitForTimeout(500); +} + +async function removeNoiseAndWait(page: import("@playwright/test").Page) { + await page.getByTestId("noise-removal-submit").click(); + await expect(page.getByTestId("noise-removal-download")).toBeVisible({ timeout: 120_000 }); +} + +test.describe("Noise Removal tool", () => { + test("page loads with correct UI sections", async ({ loggedInPage: page }) => { + await page.goto("/noise-removal"); + + // Tier buttons + await expect(page.getByRole("button", { name: "Quick" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Balanced" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Quality" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Maximum" })).toBeVisible(); + + // Sliders + await expect(page.getByText("Strength")).toBeVisible(); + await expect(page.getByText("Detail Preservation")).toBeVisible(); + await expect(page.getByText("Color Noise")).toBeVisible(); + + // Output format buttons + await expect(page.getByRole("button", { name: "Original" })).toBeVisible(); + await expect(page.getByRole("button", { name: "PNG" })).toBeVisible(); + await expect(page.getByRole("button", { name: "JPEG" })).toBeVisible(); + await expect(page.getByRole("button", { name: "WEBP" })).toBeVisible(); + + // Submit button is disabled with no file + await expect(page.getByTestId("noise-removal-submit")).toBeDisabled(); + }); + + test("submit button enables after file upload", async ({ loggedInPage: page }) => { + await page.goto("/noise-removal"); + await expect(page.getByTestId("noise-removal-submit")).toBeDisabled(); + await uploadFile(page, fixturePath("test-200x150.png")); + await expect(page.getByTestId("noise-removal-submit")).toBeEnabled(); + }); + + test("quality slider shows when JPEG or WEBP is selected", async ({ loggedInPage: page }) => { + await page.goto("/noise-removal"); + + // Quality slider hidden for original/PNG + await expect(page.getByText("Quality")).not.toBeVisible(); + + await page.getByRole("button", { name: "JPEG" }).click(); + await expect(page.getByText("Quality")).toBeVisible(); + + await page.getByRole("button", { name: "WEBP" }).click(); + await expect(page.getByText("Quality")).toBeVisible(); + + await page.getByRole("button", { name: "PNG" }).click(); + await expect(page.getByText("Quality")).not.toBeVisible(); + }); + + test("GIF + AI tier warning appears and disappears correctly", async ({ loggedInPage: page }) => { + await page.goto("/noise-removal"); + await uploadFile(page, fixturePath("animated.gif")); + + // No warning with balanced tier (default) + const warning = page.getByText(/AI denoising on GIF/i); + await expect(warning).not.toBeVisible(); + + // Warning appears with Quality tier + await page.getByRole("button", { name: "Quality" }).click(); + await expect(warning).toBeVisible(); + + // Warning disappears when switching back to Quick + await page.getByRole("button", { name: "Quick" }).click(); + await expect(warning).not.toBeVisible(); + }); + + test("PNG - quick tier removes noise and shows download button", async ({ + loggedInPage: page, + }) => { + await page.goto("/noise-removal"); + await uploadFile(page, fixturePath("test-200x150.png")); + + await page.getByRole("button", { name: "Quick" }).click(); + await removeNoiseAndWait(page); + + // Image preview is visible + await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible(); + + // No error shown + await expect(page.getByText("Network error")).not.toBeVisible(); + await expect(page.getByText("Noise removal failed")).not.toBeVisible(); + }); + + test("JPG - balanced tier removes noise and shows download button", async ({ + loggedInPage: page, + }) => { + await page.goto("/noise-removal"); + await uploadFile(page, fixturePath("test-100x100.jpg")); + + // Balanced is default, no need to click it + await removeNoiseAndWait(page); + + await expect(page.locator("section[aria-label='Image area'] img").first()).toBeVisible(); + await expect(page.getByText("Network error")).not.toBeVisible(); + }); + + test("WEBP output format - processes and download link is correct type", async ({ + loggedInPage: page, + }) => { + await page.goto("/noise-removal"); + await uploadFile(page, fixturePath("test-200x150.png")); + + await page.getByRole("button", { name: "Quick" }).click(); + await page.getByRole("button", { name: "WEBP" }).click(); + await removeNoiseAndWait(page); + + const downloadLink = page.getByTestId("noise-removal-download"); + await expect(downloadLink).toBeVisible(); + }); + + test("download link has correct href after processing", async ({ loggedInPage: page }) => { + await page.goto("/noise-removal"); + await uploadFile(page, fixturePath("test-200x150.png")); + + await page.getByRole("button", { name: "Quick" }).click(); + await removeNoiseAndWait(page); + + const downloadLink = page.getByTestId("noise-removal-download"); + const href = await downloadLink.getAttribute("href"); + expect(href).toBeTruthy(); + // Should point to the API download endpoint, not a blob URL + expect(href).toContain("/api/v1/download/"); + expect(href).toContain("_denoised"); + }); +});