Files
SnapOtter/tests/e2e/gui-tools-color.spec.ts
SnapOtterandGitHub d10d0f544f fix: release QA hardening across processing, media, security, and CI gates (#649)
A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
2026-07-27 15:37:30 +08:00

653 lines
26 KiB
TypeScript

import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
// ---------------------------------------------------------------------------
// GUI E2E: Color & Adjustment Tools
// (adjust-colors, sharpening, color-palette, replace-color, image-enhancement)
// ---------------------------------------------------------------------------
test.describe("GUI Color & Adjustment Tools", () => {
// ========================================================================
// ADJUST COLORS
// ========================================================================
test.describe("Adjust Colors", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await expect(page.getByText("Adjust Colors").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows light/color/detail/effects sections after upload", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await expect(page.getByText("Light").first()).toBeVisible();
await expect(page.getByText("Color").first()).toBeVisible();
await expect(page.getByText("Detail").first()).toBeVisible();
await expect(page.getByText("Effects").first()).toBeVisible();
});
test("brightness/contrast/exposure sliders visible in Light section", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await expect(page.locator("#color-slider-brightness")).toBeVisible();
await expect(page.locator("#color-slider-contrast")).toBeVisible();
await expect(page.locator("#color-slider-exposure")).toBeVisible();
});
test("saturation/temperature/tint/hue sliders visible in Color section", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await expect(page.locator("#color-slider-saturation")).toBeVisible();
await expect(page.locator("#color-slider-temperature")).toBeVisible();
await expect(page.locator("#color-slider-tint")).toBeVisible();
await expect(page.locator("#color-slider-hue")).toBeVisible();
});
test("effect buttons (none, grayscale, sepia, invert)", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await expect(page.getByRole("button", { name: "none" })).toBeVisible();
await expect(page.getByRole("button", { name: "grayscale" })).toBeVisible();
await expect(page.getByRole("button", { name: "sepia" })).toBeVisible();
await expect(page.getByRole("button", { name: "invert" })).toBeVisible();
});
test("color channels expandable section", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
// Click Color Channels to expand
await page.getByText("Color Channels").click();
await expect(page.locator("#color-slider-red")).toBeVisible();
await expect(page.locator("#color-slider-green")).toBeVisible();
await expect(page.locator("#color-slider-blue")).toBeVisible();
});
test("submit disabled without changes, enabled after slider change", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const submitBtn = page.getByTestId("adjust-colors-submit");
await expect(submitBtn).toBeDisabled();
// Adjust brightness
await page.locator("#color-slider-brightness").fill("20");
await expect(submitBtn).toBeEnabled();
});
test("Reset All button appears after changes", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await page.locator("#color-slider-brightness").fill("20");
await expect(page.getByRole("button", { name: "Reset All" })).toBeVisible();
});
test("selecting grayscale effect enables submit", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const submitBtn = page.getByTestId("adjust-colors-submit");
await expect(submitBtn).toBeDisabled();
await page.getByRole("button", { name: "grayscale" }).click();
await expect(submitBtn).toBeEnabled();
});
test("detail section shows clarity and texture sliders", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
// Detail section should have sharpness/clarity controls
await expect(page.getByText("Detail").first()).toBeVisible();
});
test("Reset All reverts sliders to defaults", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
// Make a change first
await page.locator("#color-slider-brightness").fill("30");
await expect(page.getByRole("button", { name: "Reset All" })).toBeVisible();
// Click Reset All
await page.getByRole("button", { name: "Reset All" }).click();
// Submit should be disabled again after reset
const submitBtn = page.getByTestId("adjust-colors-submit");
await expect(submitBtn).toBeDisabled();
});
test("contrast slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const slider = page.locator("#color-slider-contrast");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("selecting sepia effect enables submit", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const submitBtn = page.getByTestId("adjust-colors-submit");
await expect(submitBtn).toBeDisabled();
await page.getByRole("button", { name: "sepia" }).click();
await expect(submitBtn).toBeEnabled();
});
test("processes color adjustment and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await page.locator("#color-slider-brightness").fill("20");
await page.getByTestId("adjust-colors-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("adjust-colors-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// SHARPENING
// ========================================================================
test.describe("Sharpening", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await expect(page.getByText("Sharpen Image").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows method selector with three options", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await expect(page.getByRole("button", { name: "Adaptive" })).toBeVisible();
await expect(page.getByRole("button", { name: "Unsharp Mask" })).toBeVisible();
await expect(page.getByRole("button", { name: "High-Pass" })).toBeVisible();
});
test("shows presets in adaptive mode", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
// Adaptive mode is default
await expect(page.getByRole("button", { name: "Light" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Medium" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Strong" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Portrait" })).toBeVisible();
await expect(page.getByRole("button", { name: "Landscape" })).toBeVisible();
});
test("noise reduction buttons visible", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await expect(page.getByRole("button", { name: "off" })).toBeVisible();
await expect(page.getByRole("button", { name: "light" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "medium" }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "strong" }).first()).toBeVisible();
});
test("advanced controls toggle reveals extra sliders", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await page.getByText("Advanced Controls").click();
// Adaptive advanced controls
await expect(page.locator("#sharpen-slider-radius")).toBeVisible();
await expect(page.locator("#sharpen-slider-flat-protection")).toBeVisible();
});
test("switching to unsharp mask shows amount slider", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await page.getByRole("button", { name: "Unsharp Mask" }).click();
await expect(page.locator("#sharpen-slider-amount")).toBeVisible();
});
test("switching to high-pass shows strength slider", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await page.getByRole("button", { name: "High-Pass" }).click();
// High-pass mode shows a Strength slider.
await expect(page.locator("#sharpen-slider-strength")).toBeVisible();
});
test("selecting a preset enables submit", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
// Adaptive mode with Medium preset should enable submit
await page.getByRole("button", { name: "Medium" }).first().click();
await expect(page.getByTestId("sharpening-submit")).toBeEnabled();
});
test("processes sharpening and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await page.getByTestId("sharpening-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("sharpening-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// COLOR PALETTE
// ========================================================================
test.describe("Color Palette", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/color-palette");
await expect(page.getByText("Color Palette").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows Extract Palette button after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/color-palette");
await uploadTestImage(page);
const btn = page.getByTestId("color-palette-submit");
await expect(btn).toBeVisible();
await expect(btn).toHaveText(/Extract Palette/);
});
test("submit button is enabled with file uploaded", async ({ loggedInPage: page }) => {
await page.goto("/image/color-palette");
await uploadTestImage(page);
const submitBtn = page.getByTestId("color-palette-submit");
await expect(submitBtn).toBeEnabled();
});
test("extracts colors and displays palette", async ({ loggedInPage: page }) => {
await page.goto("/image/color-palette");
await uploadTestImage(page);
await page.getByTestId("color-palette-submit").click();
await waitForProcessing(page);
// Should show "Dominant Colors" heading with color swatches
await expect(page.getByText("Dominant Colors").first()).toBeVisible({ timeout: 15_000 });
});
test("submit button text says Extract Palette", async ({ loggedInPage: page }) => {
await page.goto("/image/color-palette");
await uploadTestImage(page);
await expect(page.getByTestId("color-palette-submit")).toHaveText(/Extract Palette/);
});
});
// ========================================================================
// REPLACE COLOR
// ========================================================================
test.describe("Replace Color", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await expect(page.getByText("Replace").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows source and target color pickers after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
await expect(page.locator("#replace-source-color")).toBeVisible();
await expect(page.locator("#replace-target-color")).toBeVisible();
});
test("tolerance slider visible", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
await expect(page.locator("#replace-tolerance")).toBeVisible();
await expect(page.getByText("Exact match")).toBeVisible();
await expect(page.getByText("Wide range")).toBeVisible();
});
test("make transparent checkbox hides target color", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
// Check Make transparent
await page
.locator("label")
.filter({ hasText: "Make transparent instead" })
.locator("input[type='checkbox']")
.check();
// Target color picker should be hidden
await expect(page.locator("#replace-target-color")).not.toBeVisible();
});
test("tolerance slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
const slider = page.locator("#replace-tolerance");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("submit button uses data-testid", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
await expect(page.getByTestId("replace-color-submit")).toBeVisible();
});
test("processes color replacement and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
await page.getByTestId("replace-color-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("replace-color-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// IMAGE ENHANCEMENT
// ========================================================================
test.describe("Image Enhancement", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await expect(page.getByText("Image Enhancement").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows enhancement mode and controls after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
// Wait for analysis to complete
await expect(
page.locator("text=Intensity").or(page.locator("text=Enhancement Mode")).first(),
).toBeVisible({ timeout: 10_000 });
});
test("shows all six enhancement mode buttons after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
await expect(page.getByText("Enhancement Mode")).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole("button", { name: "Auto" })).toBeVisible();
await expect(page.getByRole("button", { name: "Portrait" })).toBeVisible();
await expect(page.getByRole("button", { name: "Landscape" })).toBeVisible();
await expect(page.getByRole("button", { name: "Low Light" })).toBeVisible();
await expect(page.getByRole("button", { name: "Food" })).toBeVisible();
await expect(page.getByRole("button", { name: "Document" })).toBeVisible();
});
test("shows intensity slider with percentage", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
await expect(page.getByText("Intensity")).toBeVisible({ timeout: 10_000 });
// Intensity slider
const slider = page.locator("input[type='range']").first();
await expect(slider).toBeVisible();
});
test("switching enhancement mode changes active button", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
await expect(page.getByText("Enhancement Mode")).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: "Landscape" }).click();
await page.getByRole("button", { name: "Portrait" }).click();
await page.getByRole("button", { name: "Auto" }).click();
});
test("shows Deep Enhance AI toggle", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
await expect(page.getByText("Deep Enhance (AI)")).toBeVisible({ timeout: 10_000 });
});
test("submit button uses data-testid", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
await expect(page.getByTestId("image-enhancement-submit")).toBeVisible({ timeout: 10_000 });
});
test("processes enhancement and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/image-enhancement");
await uploadTestImage(page);
// Wait for analysis
await expect(
page.locator("text=Intensity").or(page.locator("text=Enhancement Mode")).first(),
).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: /^enhance$/i }).click();
await waitForProcessing(page);
await expect(page.getByRole("link", { name: /download/i }).first()).toBeVisible({
timeout: 15_000,
});
});
});
// ========================================================================
// COLOR BLINDNESS SIMULATOR
// ========================================================================
test.describe("Color Blindness Simulator", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/color-blindness");
await expect(page.getByText("Color Blindness").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows simulation type dropdown after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
await expect(page.locator("#cb-simulation-type")).toBeVisible();
});
test("dropdown has grouped options (Red-Green, Blue-Yellow, Monochromatic)", async ({
loggedInPage: page,
}) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
const select = page.locator("#cb-simulation-type");
// Verify optgroups exist
const optgroups = select.locator("optgroup");
await expect(optgroups).toHaveCount(3);
});
test("shows description text for selected type", async ({ loggedInPage: page }) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
// Default is deuteranomaly -- should show description
await expect(page.getByText("Reduced green sensitivity")).toBeVisible();
});
test("changing simulation type updates description", async ({ loggedInPage: page }) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
await page.selectOption("#cb-simulation-type", "achromatopsia");
await expect(page.getByText("Complete color blindness")).toBeVisible();
});
test("submit button disabled without file, enabled with file", async ({
loggedInPage: page,
}) => {
await page.goto("/image/color-blindness");
// Pre-upload the page shows only the dropzone; the settings panel and its
// submit render after a file is loaded.
await expect(page.getByTestId("color-blindness-submit")).toHaveCount(0);
await uploadTestImage(page);
const submitBtn = page.getByTestId("color-blindness-submit");
await expect(submitBtn).toBeEnabled();
await expect(submitBtn).toHaveText(/Simulate/);
});
test("processes simulation and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
await page.getByTestId("color-blindness-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("color-blindness-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// UNDO / STATE RESET (Color tools)
// ========================================================================
test.describe("Undo and State Reset", () => {
test("adjust-colors: undo after processing returns to sliders", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await page.locator("#color-slider-brightness").fill("20");
await page.getByTestId("adjust-colors-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("adjust-colors-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("adjust-colors-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("adjust-colors-download")).not.toBeVisible();
// Sliders should be back
await expect(page.locator("#color-slider-brightness")).toBeVisible();
});
test("sharpening: undo after processing returns to method selector", async ({
loggedInPage: page,
}) => {
await page.goto("/image/sharpening");
await uploadTestImage(page);
await page.getByTestId("sharpening-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("sharpening-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("sharpening-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("sharpening-download")).not.toBeVisible();
});
test("replace-color: undo after processing returns to color pickers", async ({
loggedInPage: page,
}) => {
await page.goto("/image/replace-color");
await uploadTestImage(page);
await page.getByTestId("replace-color-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("replace-color-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("replace-color-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("replace-color-download")).not.toBeVisible();
await expect(page.locator("#replace-source-color")).toBeVisible();
});
test("color-blindness: undo after processing returns to type selector", async ({
loggedInPage: page,
}) => {
await page.goto("/image/color-blindness");
await uploadTestImage(page);
await page.getByTestId("color-blindness-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("color-blindness-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("color-blindness-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("color-blindness-download")).not.toBeVisible();
await expect(page.locator("#cb-simulation-type")).toBeVisible();
});
test("adjust-colors: clear all returns to dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await expect(page.locator("#color-slider-brightness")).toBeVisible();
await page.getByText("Clear all").click();
await expect(page.getByText("Upload from computer")).toBeVisible({ timeout: 5_000 });
});
test("adjust-colors: navigate away resets state", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
await page.locator("#color-slider-brightness").fill("30");
await page.goto("/image/sharpening");
await page.goto("/image/adjust-colors");
await expect(page.getByText("Upload from computer")).toBeVisible();
});
});
// ========================================================================
// ADJUST COLORS: LIVE PREVIEW VERIFICATION
// ========================================================================
test.describe("Adjust Colors Live Preview", () => {
test("changing brightness applies CSS filter to preview image", async ({
loggedInPage: page,
}) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const preview = page.getByRole("img", { name: "test-image.png" });
await expect(preview).toBeVisible();
await page.locator("#color-slider-brightness").fill("30");
await expect(preview).toHaveCSS("filter", "brightness(1.3)");
});
test("selecting grayscale effect applies CSS filter", async ({ loggedInPage: page }) => {
await page.goto("/image/adjust-colors");
await uploadTestImage(page);
const preview = page.getByRole("img", { name: "test-image.png" });
await expect(preview).toBeVisible();
await page.getByRole("button", { name: "grayscale" }).click();
await expect(preview).toHaveCSS("filter", "grayscale(1)");
});
});
});