Files
SnapOtter/tests/e2e/gui-tools-overlay.spec.ts
T
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

780 lines
30 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import type { Page } from "@playwright/test";
import {
expect,
getE2eRunRoot,
getTestImagePath,
test,
uploadTestImage,
waitForProcessing,
} from "./helpers";
// ---------------------------------------------------------------------------
// GUI E2E: Watermark & Overlay Tools
// (watermark-text, watermark-image, text-overlay, compose, border)
// ---------------------------------------------------------------------------
test.describe("GUI Watermark & Overlay Tools", () => {
// ========================================================================
// WATERMARK TEXT
// ========================================================================
test.describe("Watermark Text", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await expect(page.getByText("Text Watermark").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows watermark text input and settings after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-text")).toBeVisible();
// Default text is "Sample Watermark"
await expect(page.locator("#watermark-text-text")).toHaveValue("Sample Watermark");
});
test("font size slider visible", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-font-size")).toBeVisible();
});
test("submit disabled without file, enabled with file", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
// The settings panel and submit button render only after a file is
// uploaded. Before upload the page shows just the dropzone.
await expect(page.getByTestId("watermark-text-submit")).toHaveCount(0);
await uploadTestImage(page);
await expect(page.getByTestId("watermark-text-submit")).toBeEnabled();
});
test("shows color picker for watermark text", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-color")).toBeVisible();
});
test("shows opacity slider", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-opacity")).toBeVisible();
});
test("shows position dropdown with all options", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
const select = page.locator("#watermark-text-position");
await expect(select).toBeVisible();
const options = select.locator("option");
await expect(options).toHaveCount(6); // center, top-left, top-right, bottom-left, bottom-right, tiled
});
test("shows rotation slider", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-rotation")).toBeVisible();
});
test("opacity slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
const slider = page.locator("#watermark-text-opacity");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("font size slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
const slider = page.locator("#watermark-text-font-size");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("changing text input updates value", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await page.locator("#watermark-text-text").fill("My Custom Text");
await expect(page.locator("#watermark-text-text")).toHaveValue("My Custom Text");
});
test("processes watermark and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await page.locator("#watermark-text-text").fill("Test Watermark");
await page.getByTestId("watermark-text-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("watermark-text-download")).toBeVisible({
timeout: 15_000,
});
});
});
// ========================================================================
// WATERMARK IMAGE
// ========================================================================
test.describe("Watermark Image", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await expect(page.getByText("Image Watermark").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows position and opacity controls after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
// Position selector should be visible
await expect(page.getByText(/position/i).first()).toBeVisible();
// Opacity slider
await expect(page.getByText(/opacity/i).first()).toBeVisible();
});
test("shows watermark upload area after main image upload", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
// Should see a prompt to upload the watermark/logo image
await expect(page.getByText(/watermark|logo|overlay/i).first()).toBeVisible();
});
test("shows position dropdown with five options", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
const select = page.locator("#watermark-image-position");
await expect(select).toBeVisible();
const options = select.locator("option");
await expect(options).toHaveCount(5); // center, top-left, top-right, bottom-left, bottom-right
});
test("shows opacity slider with percentage", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
await expect(page.locator("#watermark-image-opacity")).toBeVisible();
});
test("shows scale slider with percentage", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
await expect(page.locator("#watermark-image-scale")).toBeVisible();
});
test("submit disabled without watermark file", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-image");
await uploadTestImage(page);
const submitBtn = page.getByTestId("watermark-image-submit");
await expect(submitBtn).toBeDisabled();
});
});
// ========================================================================
// TEXT OVERLAY
// ========================================================================
test.describe("Text Overlay", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await expect(page.getByText("Text Overlay").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows text input and font size slider after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await expect(page.locator("#text-overlay-text")).toBeVisible();
await expect(page.locator("#text-overlay-text")).toHaveValue("Your Text Here");
await expect(page.locator("#text-overlay-font-size")).toBeVisible();
});
test("submit disabled without file, enabled with file", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
// The settings panel and submit button render only after a file is
// uploaded. Before upload the page shows just the dropzone.
await expect(page.getByTestId("text-overlay-submit")).toHaveCount(0);
await uploadTestImage(page);
await expect(page.getByTestId("text-overlay-submit")).toBeEnabled();
});
test("shows text color picker", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await expect(page.locator("#text-overlay-color")).toBeVisible();
});
test("shows position dropdown with three options", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
const select = page.locator("#text-overlay-position");
await expect(select).toBeVisible();
const options = select.locator("option");
await expect(options).toHaveCount(3); // top, center, bottom
});
test("shows drop shadow checkbox", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await expect(page.getByText("Shadow")).toBeVisible();
});
test("background box checkbox reveals box color picker", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await expect(page.getByText("Background Box")).toBeVisible();
// Box color should NOT be visible by default
await expect(page.locator("#text-overlay-box-color")).not.toBeVisible();
// Check Background Box
await page
.locator("label")
.filter({ hasText: "Background Box" })
.locator("input[type='checkbox']")
.check();
// Box color picker should now appear
await expect(page.locator("#text-overlay-box-color")).toBeVisible();
});
test("font size slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
const slider = page.locator("#text-overlay-font-size");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("changing text input updates value", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await page.locator("#text-overlay-text").fill("Custom Overlay");
await expect(page.locator("#text-overlay-text")).toHaveValue("Custom Overlay");
});
test("processes text overlay and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await page.getByTestId("text-overlay-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("text-overlay-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// COMPOSE (Image Composition)
//
// The compose page has two dashed-border elements: the overlay upload
// button in the settings sidebar and the main dropzone. The generic
// uploadTestImage helper picks the first border-dashed element (overlay
// button), so compose tests use the dropzone's aria-label and set
// overlay files directly on the hidden input.
// ========================================================================
test.describe("Compose", () => {
async function uploadBaseImage(page: Page) {
const fileChooserPromise = page.waitForEvent("filechooser");
await page
.locator("section[aria-label='File drop zone']")
.getByRole("button", { name: /upload from computer/i })
.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(getTestImagePath());
await page.waitForTimeout(500);
}
async function uploadOverlayImage(page: Page, filePath?: string) {
await page.locator("#compose-overlay-image").setInputFiles(filePath ?? getTestImagePath());
await page.waitForTimeout(500);
}
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await expect(page.getByText("Image Composition").first()).toBeVisible();
await expect(page.locator("section[aria-label='File drop zone']")).toBeVisible();
});
test("shows overlay upload and position controls", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
// Settings render only after a base image is loaded; before that the
// page shows just the dropzone.
await uploadBaseImage(page);
await expect(page.getByText("X Position")).toBeVisible();
await expect(page.getByText("Y Position")).toBeVisible();
await expect(page.getByText("Opacity").first()).toBeVisible();
await expect(page.getByText("Blend Mode")).toBeVisible();
await expect(page.getByTestId("compose-submit")).toBeVisible();
});
test("shows overlay image upload button", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await expect(page.getByText("Overlay Image").first()).toBeVisible();
await expect(page.getByText("Choose overlay image")).toBeVisible();
});
test("shows X and Y position number inputs", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await expect(page.locator("#compose-x-position")).toBeVisible();
await expect(page.locator("#compose-y-position")).toBeVisible();
});
test("shows opacity slider", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await expect(page.locator("#compose-opacity")).toBeVisible();
});
test("blend mode dropdown has all options", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
const select = page.locator("#compose-blend-mode");
await expect(select).toBeVisible();
const options = select.locator("option");
await expect(options).toHaveCount(10);
});
test("submit disabled without overlay file", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
const submitBtn = page.getByTestId("compose-submit");
await expect(submitBtn).toBeDisabled();
});
test("submit enabled after uploading both base and overlay", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await uploadOverlayImage(page);
await expect(page.getByTestId("compose-submit")).toBeEnabled();
});
test("processes composition and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await uploadOverlayImage(page);
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
});
test("processes with custom position and opacity", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await uploadOverlayImage(page);
await page.locator("#compose-x-position").fill("10");
await page.locator("#compose-y-position").fill("20");
await page.locator("#compose-opacity").fill("50");
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
});
test("processes with multiply blend mode", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await uploadOverlayImage(page);
await page.locator("#compose-blend-mode").selectOption("multiply");
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
});
test("overlay filename shown after selection", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
// The overlay file input lives in the settings panel, which renders only
// after a base image is loaded.
await uploadBaseImage(page);
// Use a distinct overlay fixture so its filename is unambiguous (the
// base image is test-image.png, which would otherwise also match).
await uploadOverlayImage(page, "tests/fixtures/image/valid/test-50x50.webp");
await expect(page.getByText("test-50x50.webp")).toBeVisible();
});
test("shows size info after processing", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
await uploadOverlayImage(page);
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(/Original:/).first()).toBeVisible();
await expect(page.getByText(/Processed:/).first()).toBeVisible();
});
test("processes webp overlay on png base", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
const webpPath = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-50x50.webp",
);
await uploadOverlayImage(page, webpPath);
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
});
test("shows error for corrupt overlay file", async ({ loggedInPage: page }) => {
await page.goto("/image/compose");
await uploadBaseImage(page);
const tmpDir = getE2eRunRoot();
fs.mkdirSync(tmpDir, { recursive: true });
const corruptPath = path.join(tmpDir, "corrupt-overlay.png");
fs.writeFileSync(corruptPath, Buffer.from("not-an-image"));
await uploadOverlayImage(page, corruptPath);
await page.getByTestId("compose-submit").click();
await expect(page.getByText(/Processing failed|Invalid image|error/i)).toBeVisible({
timeout: 15_000,
});
});
});
// ========================================================================
// BORDER
// ========================================================================
test.describe("Border", () => {
test("renders tool page with dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await expect(page.getByText("Border").first()).toBeVisible();
await expect(page.getByText("Upload from computer")).toBeVisible();
});
test("shows border preset buttons after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
// Presets from border-settings.tsx
await expect(page.getByText("Clean White").first()).toBeVisible();
await expect(page.getByText("Gallery Black").first()).toBeVisible();
await expect(page.getByText("Shadow").first()).toBeVisible();
await expect(page.getByText("Rounded").first()).toBeVisible();
});
test("shows border width and color controls after upload", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.getByText(/border width|width/i).first()).toBeVisible();
});
test("submit button uses data-testid", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.getByTestId("border-submit")).toBeVisible();
});
test("shows border width slider", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#border-width")).toBeVisible();
});
test("shows border color swatches", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#border-color")).toBeVisible();
});
test("shows padding slider", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#border-padding")).toBeVisible();
});
test("shows padding color swatches", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#padding-color")).toBeVisible();
});
test("shows corner radius slider", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#border-corner-radius")).toBeVisible();
});
test("shadow toggle reveals shadow controls", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
// Shadow toggle switch
const shadowToggle = page.locator("button[role='switch'][aria-checked]").first();
await expect(shadowToggle).toBeVisible();
// Shadow blur should not be visible before toggle
await expect(page.locator("#shadow-blur")).not.toBeVisible();
// Toggle shadow on
await shadowToggle.click();
// Shadow controls should now appear
await expect(page.locator("#shadow-blur")).toBeVisible();
await expect(page.locator("#shadow-offset-x")).toBeVisible();
await expect(page.locator("#shadow-offset-y")).toBeVisible();
await expect(page.locator("#shadow-color")).toBeVisible();
await expect(page.locator("#shadow-opacity")).toBeVisible();
});
test("shows all preset buttons", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.getByText("Polaroid").first()).toBeVisible();
await expect(page.getByText("Vintage").first()).toBeVisible();
await expect(page.getByText("Minimal").first()).toBeVisible();
await expect(page.getByText("Cinematic").first()).toBeVisible();
});
test("border width slider is interactive", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
const slider = page.locator("#border-width");
await expect(slider).toBeVisible();
await expect(slider).toHaveAttribute("type", "range");
});
test("clicking a preset updates border settings", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
// Click Polaroid preset
await page.getByText("Polaroid").first().click();
// Click Gallery Black preset
await page.getByText("Gallery Black").first().click();
});
test("submit disabled without file, enabled with file", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
// Submit is not rendered until a file is uploaded.
await expect(page.getByTestId("border-submit")).toHaveCount(0);
await uploadTestImage(page);
await expect(page.getByTestId("border-submit")).toBeEnabled();
});
test("processes border and shows download", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await page.getByTestId("border-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("border-download")).toBeVisible({ timeout: 15_000 });
});
});
// ========================================================================
// UNDO / STATE RESET (Overlay tools)
// ========================================================================
test.describe("Undo and State Reset", () => {
test("watermark-text: undo after processing returns to text input", async ({
loggedInPage: page,
}) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await page.locator("#watermark-text-text").fill("Undo Test");
await page.getByTestId("watermark-text-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("watermark-text-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("watermark-text-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("watermark-text-download")).not.toBeVisible();
await expect(page.locator("#watermark-text-text")).toBeVisible();
});
test("text-overlay: undo after processing returns to settings", async ({
loggedInPage: page,
}) => {
await page.goto("/image/text-overlay");
await uploadTestImage(page);
await page.getByTestId("text-overlay-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("text-overlay-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("text-overlay-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("text-overlay-download")).not.toBeVisible();
await expect(page.locator("#text-overlay-text")).toBeVisible();
});
test("compose: undo after processing returns to position controls", async ({
loggedInPage: page,
}) => {
await page.goto("/image/compose");
// Use compose-specific upload helpers
const fileChooserPromise = page.waitForEvent("filechooser");
await page
.locator("section[aria-label='File drop zone']")
.getByRole("button", { name: /upload from computer/i })
.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(getTestImagePath());
await page.waitForTimeout(500);
await page.locator("#compose-overlay-image").setInputFiles(getTestImagePath());
await page.waitForTimeout(500);
await page.getByTestId("compose-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("compose-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("compose-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.locator("#compose-x-position")).toBeVisible();
await expect(page.locator("#compose-y-position")).toBeVisible();
});
test("border: undo after processing returns to preset buttons", async ({
loggedInPage: page,
}) => {
await page.goto("/image/border");
await uploadTestImage(page);
await page.getByTestId("border-submit").click();
await waitForProcessing(page);
await expect(page.getByTestId("border-download")).toBeVisible({ timeout: 15_000 });
await page.getByRole("button", { name: /adjust settings/i }).click();
await expect(page.getByTestId("border-submit")).toBeVisible({ timeout: 5_000 });
await expect(page.getByTestId("border-download")).not.toBeVisible();
await expect(page.getByText("Clean White").first()).toBeVisible();
});
test("watermark-text: clear all returns to dropzone", async ({ loggedInPage: page }) => {
await page.goto("/image/watermark-text");
await uploadTestImage(page);
await expect(page.locator("#watermark-text-text")).toBeVisible();
await page.getByText("Clear all").click();
await expect(page.getByText("Upload from computer")).toBeVisible({ timeout: 5_000 });
});
test("border: navigate away resets state", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
await expect(page.locator("#border-width")).toBeVisible();
await page.goto("/image/watermark-text");
await page.goto("/image/border");
await expect(page.getByText("Upload from computer")).toBeVisible();
});
});
// ========================================================================
// BORDER: LIVE PREVIEW VERIFICATION
// ========================================================================
test.describe("Border Live Preview", () => {
test("selecting a preset updates the live preview styling", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
const previewImg = page.locator("img").first();
await expect(previewImg).toBeVisible();
// Click Polaroid preset
await page.getByText("Polaroid").first().click();
await page.waitForTimeout(500);
// The image wrapper should have styling applied (border, padding, etc.)
// Check that some wrapper element has a non-default style
const wrapper = previewImg.locator("..");
const _bgColor = await wrapper.evaluate((el) => window.getComputedStyle(el).backgroundColor);
// Polaroid preset uses white background -- just verify the preview didn't error
await expect(previewImg).toBeVisible();
});
test("changing border width updates preview in real-time", async ({ loggedInPage: page }) => {
await page.goto("/image/border");
await uploadTestImage(page);
const previewImg = page.locator("img").first();
await expect(previewImg).toBeVisible();
// Change border width
await page.locator("#border-width").fill("20");
await page.waitForTimeout(500);
// Preview should still be visible and updated
await expect(previewImg).toBeVisible();
});
});
});